-
} />
-
-
-
{EDIT_TAINTS_MODAL_MESSAGING.addTaint}
-
- {taintList?.map((taintDetails, index) => {
- const _errorObj = errorObj?.taintErrorList[index]
- return (
-
-
-
-
- {
- onEffectChange(selectedValue, index)
- }}
- data-index={index}
- value={{
- label: taintDetails.effect,
- value: taintDetails.effect,
- }}
- size={ComponentSizeType.large}
- />
-
-
}
- dataTestId={`delete-taint-${index}`}
- onClick={deleteTaint}
- data-index={index}
- ariaLabel="Delete Taint"
- showAriaLabelInTippy={false}
- size={ComponentSizeType.small}
- variant={ButtonVariantType.borderLess}
- style={ButtonStyleType.negativeGrey}
+
+ {isTaintListEmpty ? (
+
(
+ }
+ onClick={handleAddTaint}
+ />
+ )}
+ />
+ ) : (
+ <>
+
+
+
+
+ {EDIT_TAINTS_MODAL_MESSAGING.infoTitle}
+
+ }
/>
- )
- })}
-
-
-
-
-
+
}
+ size={ComponentSizeType.small}
+ text={EDIT_TAINTS_MODAL_MESSAGING.addTaint}
+ onClick={handleAddTaint}
+ />
+
+
+ headers={TAINTS_TABLE_HEADERS}
+ rows={taintList}
+ onRowAdd={handleAddTaint}
+ onRowDelete={handleDeleteTaint}
+ onRowEdit={handleEditTaint}
+ cellError={taintCellError}
+ isAdditionNotAllowed
+ shouldAutoFocusOnMount
+ />
+ >
+ )}
+ {!isTaintListEmpty && (
+
+
+
+
+ )}
)
}
+
+export default EditTaintsModal
diff --git a/src/components/ClusterNodes/NodeActions/utils.ts b/src/components/ClusterNodes/NodeActions/utils.ts
new file mode 100644
index 0000000000..d110e8deaa
--- /dev/null
+++ b/src/components/ClusterNodes/NodeActions/utils.ts
@@ -0,0 +1,219 @@
+/*
+ * Copyright (c) 2024. Devtron Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ DynamicDataTableCellValidationState,
+ DynamicDataTableRowDataType,
+ getUniqueId,
+} from '@devtron-labs/devtron-fe-common-lib'
+
+import { PATTERNS } from '@Config/constants'
+
+import { TAINT_OPTIONS, TAINTS_TABLE_HEADERS } from '../constants'
+import { EFFECT_TYPE, TaintsTableHeaderKeys, TaintsTableType, TaintType } from '../types'
+
+export const getTaintsTableRow = (taint?: TaintType, id?: number): TaintsTableType['rows'][number] => ({
+ id: id ?? getUniqueId(),
+ data: {
+ [TaintsTableHeaderKeys.KEY]: {
+ type: DynamicDataTableRowDataType.TEXT,
+ value: taint?.key || '',
+ props: {
+ placeholder: 'Enter key',
+ },
+ },
+ [TaintsTableHeaderKeys.VALUE]: {
+ type: DynamicDataTableRowDataType.TEXT,
+ value: taint?.value || '',
+ props: {
+ placeholder: 'Enter value',
+ },
+ },
+ [TaintsTableHeaderKeys.EFFECT]: {
+ type: DynamicDataTableRowDataType.DROPDOWN,
+ value: taint?.effect || EFFECT_TYPE.PreferNoSchedule,
+ props: {
+ options: TAINT_OPTIONS,
+ },
+ },
+ },
+})
+
+export const getTaintsTableRows = (taints: TaintType[]): TaintsTableType['rows'] =>
+ taints?.length ? taints.map(getTaintsTableRow) : []
+
+export const getTaintsRowCellError = () =>
+ TAINTS_TABLE_HEADERS.reduce(
+ (headerAcc, { key }) => ({ ...headerAcc, [key]: { isValid: true, errorMessages: [] } }),
+ {},
+ )
+
+export const getTaintsTableCellError = (taintList: TaintsTableType['rows']): TaintsTableType['cellError'] =>
+ taintList.reduce((acc, curr) => {
+ if (!acc[curr.id]) {
+ acc[curr.id] = getTaintsRowCellError()
+ }
+
+ return acc
+ }, {})
+
+export const getTaintsTableCellValidateState = (
+ headerKey: TaintsTableHeaderKeys,
+ value: string,
+): DynamicDataTableCellValidationState => {
+ if (headerKey === TaintsTableHeaderKeys.KEY) {
+ const keyPrefixRegex = new RegExp(PATTERNS.KUBERNETES_KEY_PREFIX)
+ const keyNameRegex = new RegExp(PATTERNS.KUBERNETES_KEY_NAME)
+
+ if (!value) {
+ return { errorMessages: ['Key is required'], isValid: false }
+ }
+
+ if (value.length > 253) {
+ return { errorMessages: ['Maximum 253 chars are allowed'], isValid: false }
+ }
+
+ if (value.indexOf('/') !== -1) {
+ const keyArr = value.split('/')
+
+ if (keyArr.length > 2 || !keyPrefixRegex.test(keyArr[0])) {
+ return {
+ errorMessages: ["The key can begin with a DNS subdomain prefix and a single '/'"],
+ isValid: false,
+ }
+ }
+
+ if (!keyNameRegex.test(keyArr[1])) {
+ return {
+ errorMessages: [
+ 'The key must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores',
+ ],
+ isValid: false,
+ }
+ }
+ } else if (!keyNameRegex.test(value)) {
+ return {
+ errorMessages: [
+ 'The key must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores',
+ ],
+ isValid: false,
+ }
+ }
+ }
+
+ if (headerKey === TaintsTableHeaderKeys.VALUE && value) {
+ const valueRegex = new RegExp(PATTERNS.KUBERNETES_VALUE)
+
+ if (value.length > 63) {
+ return { errorMessages: ['Maximum 63 chars are allowed'], isValid: false }
+ }
+ if (!valueRegex.test(value)) {
+ return {
+ errorMessages: [
+ 'The value must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores',
+ ],
+ isValid: false,
+ }
+ }
+ }
+
+ return {
+ errorMessages: [],
+ isValid: true,
+ }
+}
+
+const getTaintUniqueKey = (data: TaintsTableType['rows'][number]['data']) =>
+ `${data[TaintsTableHeaderKeys.KEY].value}-${data[TaintsTableHeaderKeys.EFFECT].value}`
+
+export const validateUniqueTaintKey = ({
+ taintList,
+ taintCellError,
+}: {
+ taintList: TaintsTableType['rows']
+ taintCellError: TaintsTableType['cellError']
+}) => {
+ const updatedCellError = taintCellError
+ const uniqueTaintKeyMap = taintList.reduce((acc, curr) => {
+ const key = getTaintUniqueKey(curr.data)
+ acc[key] = (acc[key] || 0) + 1
+
+ return acc
+ }, {})
+ const uniqueKeyErrorMsg = 'Key and effect must be a unique combination'
+
+ taintList.forEach(({ id, data }) => {
+ const key = getTaintUniqueKey(data)
+
+ if (data[TaintsTableHeaderKeys.KEY].value && data[TaintsTableHeaderKeys.EFFECT].value) {
+ if (
+ updatedCellError[id][TaintsTableHeaderKeys.KEY].isValid &&
+ updatedCellError[id][TaintsTableHeaderKeys.EFFECT].isValid &&
+ uniqueTaintKeyMap[key] > 1
+ ) {
+ updatedCellError[id][TaintsTableHeaderKeys.KEY] = {
+ errorMessages: [uniqueKeyErrorMsg],
+ isValid: false,
+ }
+ updatedCellError[id][TaintsTableHeaderKeys.EFFECT] = {
+ errorMessages: [uniqueKeyErrorMsg],
+ isValid: false,
+ }
+ } else if (uniqueTaintKeyMap[key] < 2) {
+ if (updatedCellError[id][TaintsTableHeaderKeys.KEY].errorMessages[0] === uniqueKeyErrorMsg) {
+ updatedCellError[id][TaintsTableHeaderKeys.KEY] = {
+ errorMessages: [],
+ isValid: true,
+ }
+ }
+ if (updatedCellError[id][TaintsTableHeaderKeys.EFFECT].errorMessages[0] === uniqueKeyErrorMsg) {
+ updatedCellError[id][TaintsTableHeaderKeys.EFFECT] = {
+ errorMessages: [],
+ isValid: true,
+ }
+ }
+ }
+ }
+ })
+}
+
+export const getTaintTableValidateState = ({ taintList }: { taintList: TaintsTableType['rows'] }) => {
+ const taintCellError: TaintsTableType['cellError'] = taintList.reduce((acc, curr) => {
+ acc[curr.id] = TAINTS_TABLE_HEADERS.reduce(
+ (headerAcc, { key }) => ({
+ ...headerAcc,
+ [key]: getTaintsTableCellValidateState(key, curr.data[key].value),
+ }),
+ {},
+ )
+ return acc
+ }, {})
+
+ validateUniqueTaintKey({ taintCellError, taintList })
+
+ const isInvalid = Object.values(taintCellError).some(
+ ({ effect, key, value }) => !(effect.isValid && key.isValid && value.isValid),
+ )
+
+ return { isValid: !isInvalid, taintCellError }
+}
+
+export const getTaintsPayload = (taintList: TaintsTableType['rows']) =>
+ taintList.map(({ data }) => ({
+ key: data.key.value,
+ value: data.value.value,
+ effect: data.effect.value as EFFECT_TYPE,
+ }))
diff --git a/src/components/ClusterNodes/NodeActions/validationRules.ts b/src/components/ClusterNodes/NodeActions/validationRules.ts
deleted file mode 100644
index 33e117cf53..0000000000
--- a/src/components/ClusterNodes/NodeActions/validationRules.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright (c) 2024. Devtron Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { PATTERNS } from '../../../config'
-
-export class ValidationRules {
- taintKey = (key: string): { message: string | null; isValid: boolean } => {
- const keyPrefixRegex = new RegExp(PATTERNS.KUBERNETES_KEY_PREFIX)
- const keyNameRegex = new RegExp(PATTERNS.KUBERNETES_KEY_NAME)
-
- if (!key) {
- return { message: 'Key is required', isValid: false }
- }
- if (key.length > 253) {
- return { message: 'Maximum 253 chars are allowed', isValid: false }
- }
- if (key.indexOf('/') !== -1) {
- const keyArr = key.split('/')
- if (keyArr.length > 2) {
- return { message: 'Maximum one ( / ) allowed', isValid: false }
- }
- if (!keyPrefixRegex.test(keyArr[0])) {
- return { message: 'Invalid prefix in key', isValid: false }
- }
- if (!keyNameRegex.test(keyArr[1])) {
- return { message: 'Invalid name in key', isValid: false }
- }
- } else if (!keyNameRegex.test(key)) {
- return { message: 'Invalid key', isValid: false }
- }
-
- return { message: null, isValid: true }
- }
-
- taintValue = (value: string) => {
- const valueRegex = new RegExp(PATTERNS.KUBERNETES_VALUE)
- if (value) {
- if (value.length > 63) {
- return { message: 'Maximum 63 chars are allowed', isValid: false }
- }
- if (!valueRegex.test(value)) {
- return { message: 'Invalid value', isValid: false }
- }
- }
- return { message: null, isValid: true }
- }
-}
diff --git a/src/components/ClusterNodes/constants.ts b/src/components/ClusterNodes/constants.ts
index cec213a6ae..fe661081ea 100644
--- a/src/components/ClusterNodes/constants.ts
+++ b/src/components/ClusterNodes/constants.ts
@@ -17,7 +17,7 @@
import { ClusterFiltersType, ClusterStatusType } from '@devtron-labs/devtron-fe-common-lib'
import { multiSelectStyles } from '../v2/common/ReactSelectCustomization'
-import { DescriptionDataType, EFFECT_TYPE } from './types'
+import { DescriptionDataType, EFFECT_TYPE, TaintsTableHeaderKeys, TaintsTableType } from './types'
export const clusterSelectStyle = {
...multiSelectStyles,
@@ -30,7 +30,7 @@ export const clusterSelectStyle = {
backgroundColor: 'var(--bg-menu-primary)',
border: '1px solid var(--N200)',
}),
- control: (base, state) => ({
+ control: (base) => ({
...base,
borderColor: 'transparent',
backgroundColor: 'transparent',
@@ -38,7 +38,7 @@ export const clusterSelectStyle = {
height: '28px',
minHeight: '28px',
}),
- singleValue: (base, state) => ({
+ singleValue: (base) => ({
...base,
fontWeight: 600,
color: 'var(--N900)',
@@ -46,11 +46,11 @@ export const clusterSelectStyle = {
textAlign: 'left',
marginLeft: '2px',
}),
- indicatorsContainer: (base, state) => ({
+ indicatorsContainer: (base) => ({
...base,
height: '28px',
}),
- valueContainer: (base, state) => ({
+ valueContainer: (base) => ({
...base,
height: '28px',
padding: '0 6px',
@@ -104,19 +104,22 @@ export const CLUSTER_NODE_ACTIONS_LABELS = {
export const EDIT_TAINTS_MODAL_MESSAGING = {
titlePrefix: 'Edit taints for node ',
+ infoTitle: 'Taints',
infoText:
- 'Add taints to nodes so that pods are not scheduled to the nodes or not scheduled to the nodes if possible. After you add taints to nodes, you can set tolerations on a pod to allow the pod to be scheduled to nodes with certain taints.',
- infoLinkText: 'Check taint validations.',
- tippyTitle: 'Taint validations',
- tippyDescription: {
- message: 'A taint consists of a key, value, and effect.',
+ 'Add taints to nodes to prevent or discourage pods from being scheduled on them. Use tolerations on pods to let them run on nodes with matching taints.',
+ description: {
+ title: 'A taint consists of a key, value, and effect.',
messageList: [
- `1. Key: The key must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to 253 characters.`,
- `Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app.`,
- `2. Value(Optional) :If given, it must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to 63 characters.`,
- `3. Combination of
must be unique.`,
+ "Key: The key must begin with a letter or number, and may contain letters, numbers, hyphens, dots,and underscores, up to 253 characters. Optionally, the key can begin with a DNS subdomain prefix and a single '/', like example.com/my-app.",
+ 'Value(Optional): If given, it must begin with a letter or number, and may contain letters, numbers, hyphens, dots, and underscores, up to 63 characters.',
+ 'Combination of must be unique',
],
},
+ emptyState: {
+ title: 'Manage node taints',
+ subTitle:
+ 'Add taints to nodes to prevent or discourage pods from being scheduled on them. Use tolerations on pods to let them run on nodes with matching taints.',
+ },
addTaint: 'Add taint',
Actions: {
cancel: 'Cancel',
@@ -156,7 +159,7 @@ export const AUTO_SELECT = { label: 'Auto select', value: 'autoSelectNode' }
export const clusterImageSelect = {
...clusterSelectStyle,
- menu: (base, state) => ({
+ menu: (base) => ({
...base,
zIndex: 9999,
textAlign: 'left',
@@ -165,8 +168,8 @@ export const clusterImageSelect = {
backgroundColor: 'var(--bg-menu-primary)',
border: '1px solid var(--N200)',
}),
- control: (base, state) => ({
- ...clusterSelectStyle.control(base, state),
+ control: (base) => ({
+ ...clusterSelectStyle.control(base),
maxWidth: '300px',
}),
}
@@ -330,11 +333,16 @@ export enum ClusterMapListSortableTitle {
}
export enum CLUSTER_PROD_TYPE {
- PRODUCTION= 'Production',
- NON_PRODUCTION= 'Non Production',
-
+ PRODUCTION = 'Production',
+ NON_PRODUCTION = 'Non Production',
}
+export const TAINTS_TABLE_HEADERS: TaintsTableType['headers'] = [
+ { key: TaintsTableHeaderKeys.KEY, label: 'KEY', width: '1fr' },
+ { key: TaintsTableHeaderKeys.VALUE, label: 'VALUE', width: '1fr' },
+ { key: TaintsTableHeaderKeys.EFFECT, label: 'TAINT EFFECT', width: '250px' },
+]
+
export const CLUSTER_CONFIG_POLLING_INTERVAL = 1000 * 30 // half a minute
export const CLUSTER_DESCRIPTION_DUMMY_DATA: DescriptionDataType = {
@@ -342,4 +350,4 @@ export const CLUSTER_DESCRIPTION_DUMMY_DATA: DescriptionDataType = {
descriptionText: defaultClusterNote,
descriptionUpdatedBy: '',
descriptionUpdatedOn: '',
-}
\ No newline at end of file
+}
diff --git a/src/components/ClusterNodes/types.ts b/src/components/ClusterNodes/types.ts
index 4c004b14f2..7132de0e19 100644
--- a/src/components/ClusterNodes/types.ts
+++ b/src/components/ClusterNodes/types.ts
@@ -18,6 +18,7 @@ import {
APIOptions,
ClusterCapacityType,
ClusterDetail,
+ DynamicDataTableProps,
K8sResourceDetailDataType,
NodeActionRequest,
NodeTaintType,
@@ -163,19 +164,6 @@ export const TEXT_COLOR_CLASS = {
'Not ready': 'cr-5',
}
-interface ErrorObj {
- isValid: boolean
- message: string | null
-}
-
-export interface TaintErrorObj {
- isValid: boolean
- taintErrorList: {
- key: ErrorObj
- value: ErrorObj
- }[]
-}
-
export interface NodeActionModalPropType extends NodeActionRequest {
closePopup: (refreshData?: boolean) => void
}
@@ -296,6 +284,14 @@ export interface ClusterMapInitialStatusType {
errorInNodeListing: string
}
+export enum TaintsTableHeaderKeys {
+ KEY = 'key',
+ VALUE = 'value',
+ EFFECT = 'effect',
+}
+
+export type TaintsTableType = DynamicDataTableProps
+
export interface GetClusterOverviewDetailsProps {
clusterId: string
requestAbortControllerRef: APIOptions['abortControllerRef']
diff --git a/yarn.lock b/yarn.lock
index a1a6313462..ec4c987cbb 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1124,10 +1124,10 @@
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
-"@devtron-labs/devtron-fe-common-lib@1.14.1-pre-2":
- version "1.14.1-pre-2"
- resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.14.1-pre-2.tgz#f1b097522875dccae6ea1f29b643d02e6b3f754b"
- integrity sha512-kyjv9w8ygKskPVUHIA+FVzSQEh5bpuCOmhUMNJEFC3wsphJfDR11/GauXwycgeT2RuwPgGNzzTtKzlwu1sUEow==
+"@devtron-labs/devtron-fe-common-lib@1.14.1-pre-3":
+ version "1.14.1-pre-3"
+ resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.14.1-pre-3.tgz#30b21199c933b7167a0022ab8a37cf3fd7e050ce"
+ integrity sha512-rtWz1NRvDvzbOHfoFynT520Qb1S4NpHpJZHZNS5kT4jPklGN7JywgB9LVyw1Bsskx2iq58Tac7dKpC9t+wygaQ==
dependencies:
"@codemirror/lang-json" "6.0.1"
"@codemirror/lang-yaml" "6.1.2"