From fefc9f5d1591012f0642197af2f6a8b8463bba00 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 2 Dec 2024 16:42:47 +0530 Subject: [PATCH 01/36] feat: VariableDataTable - component integration --- src/assets/icons/ic-choices-dropdown.svg | 20 + src/assets/icons/ic-sliders-vertical.svg | 3 + .../CIPipelineN/InputPluginSelect.tsx | 2 +- .../CIPipelineN/VariableContainer.tsx | 71 +-- .../VariableDataTable/AddChoicesOverlay.tsx | 191 ++++++++ .../VariableDataTable/VariableDataTable.tsx | 430 ++++++++++++++++++ .../VariableDataTableRowAction.tsx | 73 +++ .../VariableDataTable/constants.ts | 48 ++ .../CIPipelineN/VariableDataTable/helpers.tsx | 12 + .../CIPipelineN/VariableDataTable/index.ts | 1 + .../CIPipelineN/VariableDataTable/types.ts | 57 +++ .../CIPipelineN/VariableDataTable/utils.tsx | 215 +++++++++ .../ciPipeline/ciPipeline.service.ts | 3 +- src/css/base.scss | 4 + 14 files changed, 1059 insertions(+), 71 deletions(-) create mode 100644 src/assets/icons/ic-choices-dropdown.svg create mode 100644 src/assets/icons/ic-sliders-vertical.svg create mode 100644 src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/constants.ts create mode 100644 src/components/CIPipelineN/VariableDataTable/helpers.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/index.ts create mode 100644 src/components/CIPipelineN/VariableDataTable/types.ts create mode 100644 src/components/CIPipelineN/VariableDataTable/utils.tsx diff --git a/src/assets/icons/ic-choices-dropdown.svg b/src/assets/icons/ic-choices-dropdown.svg new file mode 100644 index 0000000000..6e7346e1ed --- /dev/null +++ b/src/assets/icons/ic-choices-dropdown.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/ic-sliders-vertical.svg b/src/assets/icons/ic-sliders-vertical.svg new file mode 100644 index 0000000000..0bab3f6efa --- /dev/null +++ b/src/assets/icons/ic-sliders-vertical.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/CIPipelineN/InputPluginSelect.tsx b/src/components/CIPipelineN/InputPluginSelect.tsx index d3a9256109..290ea9e0c9 100644 --- a/src/components/CIPipelineN/InputPluginSelect.tsx +++ b/src/components/CIPipelineN/InputPluginSelect.tsx @@ -236,7 +236,7 @@ export const InputPluginSelection = ({ placeholder={placeholder} refVar={refVar} tabIndex={selectedVariableIndex} - handleKeyDown={handleOnKeyDown} + onKeyDown={handleOnKeyDown} /> {(variableData.refVariableStage || (variableData?.variableType && variableData.variableType !== 'NEW')) && ( diff --git a/src/components/CIPipelineN/VariableContainer.tsx b/src/components/CIPipelineN/VariableContainer.tsx index 7c395e4e70..0323162f1f 100644 --- a/src/components/CIPipelineN/VariableContainer.tsx +++ b/src/components/CIPipelineN/VariableContainer.tsx @@ -22,6 +22,7 @@ import { PluginVariableType } from '../ciPipeline/types' import CustomInputVariableSelect from './CustomInputVariableSelect' import { ReactComponent as AlertTriangle } from '../../assets/icons/ic-alert-triangle.svg' import { pipelineContext } from '../workflowEditor/workflowEditor' +import { VariableDataTable } from './VariableDataTable' export const VariableContainer = ({ type }: { type: PluginVariableType }) => { const [collapsedSection, setCollapsedSection] = useState(type !== PluginVariableType.INPUT) @@ -65,75 +66,7 @@ export const VariableContainer = ({ type }: { type: PluginVariableType }) => {
No {type} variables
)} - {!collapsedSection && variableLength > 0 && ( -
-
Variable
-
Format
-
- {type === PluginVariableType.INPUT ? 'Value' : 'Description'} -
- {formData[activeStageName].steps[selectedTaskIndex].pluginRefStepDetail[ - type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' - ]?.map((variable: VariableType, index) => { - const errorObj = - formDataErrorObj[activeStageName].steps[selectedTaskIndex]?.pluginRefStepDetail - .inputVariables[index] - - const isInputVariableRequired = type === PluginVariableType.INPUT && !variable.allowEmptyValue - return ( - - {type === PluginVariableType.INPUT && variable.description ? ( - - - {variable.name} - -
- {variable.description} - - } - > -
- - {variable.name} - -
-
- ) : ( - {variable.name} - )} - - {variable.format} - {type === PluginVariableType.INPUT ? ( -
- - {errorObj && !errorObj.isValid && ( - - - {errorObj.message} - - )} -
- ) : ( -

{variable.description}

- )} -
- ) - })} -
- )} + {!collapsedSection && variableLength > 0 && } ) } diff --git a/src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx new file mode 100644 index 0000000000..2265b5c696 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx @@ -0,0 +1,191 @@ +import { ChangeEvent } from 'react' + +import { + Button, + ButtonStyleType, + ButtonVariantType, + Checkbox, + CHECKBOX_VALUE, + ComponentSizeType, + CustomInput, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib' + +import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' +import { ReactComponent as ICClose } from '@Icons/ic-close.svg' +import { ReactComponent as ICChoicesDropdown } from '@Icons/ic-choices-dropdown.svg' + +import { AddChoicesOverlayProps, VariableDataTableActionType } from './types' +import { validateChoice } from './utils' + +export const AddChoicesOverlay = ({ + choices, + askValueAtRuntime, + blockCustomValue, + rowId, + handleRowUpdateAction, +}: AddChoicesOverlayProps) => { + // METHODS + const handleAddChoices = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId, + headerKey: null, + actionValue: (currentChoices) => [{ value: '', id: currentChoices.length, error: '' }, ...currentChoices], + }) + } + + const handleChoiceChange = (choiceId: number) => (e: ChangeEvent) => { + const choiceValue = e.target.value + // TODO: Rethink validation disc with product + const error = !validateChoice(choiceValue) ? 'This is a required field' : '' + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId, + headerKey: null, + actionValue: (currentChoices) => + currentChoices.map((choice) => + choice.id === choiceId ? { id: choiceId, value: choiceValue, error } : choice, + ), + }) + } + + const handleChoiceDelete = (choiceId: number) => () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId, + headerKey: null, + actionValue: (currentChoices) => currentChoices.filter(({ id }) => id !== choiceId), + }) + } + + const handleAllowCustomInput = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT, + rowId, + headerKey: null, + actionValue: !blockCustomValue, + }) + } + + const handleAskValueAtRuntime = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME, + rowId, + headerKey: null, + actionValue: !askValueAtRuntime, + }) + } + + return ( +
+ {choices.length ? ( +
+
+
+
+ {choices.map(({ id, value, error }) => ( +
+ +
+ ))} +
+
+ ) : ( +
+
+ +
+

Set value choices

+

Allow users to select a value from a pre-defined set of choices

+
+
+
+
+
+ )} +
+ {!!choices.length && ( + + +

Allow custom input

+

Allow entering any value other than provided choices

+
+ } + > +
Allow Custom input
+ + + )} + + +

Ask value at runtime

+

+ Value can be provided at runtime. Entered value will be pre-filled as default +

+
+ } + > +
Ask value at runtime
+ + + + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx new file mode 100644 index 0000000000..8a848fc806 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx @@ -0,0 +1,430 @@ +import { useContext, useState, useEffect } from 'react' + +import { + DynamicDataTable, + DynamicDataTableProps, + DynamicDataTableRowDataType, + PluginType, + RefVariableType, + VariableType, +} from '@devtron-labs/devtron-fe-common-lib' + +import { pipelineContext } from '@Components/workflowEditor/workflowEditor' +import { PluginVariableType } from '@Components/ciPipeline/types' + +import { ExtendedOptionType } from '@Components/app/types' +import { getVariableDataTableHeaders } from './constants' +import { getSystemVariableIcon } from './helpers' +import { HandleRowUpdateActionProps, VariableDataKeys, VariableDataRowType, VariableDataTableActionType } from './types' +import { + getEmptyVariableDataTableRow, + getFormatColumnRowProps, + getValColumnRowProps, + getVariableColumnRowProps, +} from './utils' + +import { VariableDataTableRowAction } from './VariableDataTableRowAction' + +export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { + // CONTEXTS + const { + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + formDataErrorObj, + validateTask, + setFormData, + setFormDataErrorObj, + } = useContext(pipelineContext) + + // CONSTANTS + const emptyRowParams = { + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + type, + } + const currentStepTypeVariable = + formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE + ? 'inlineStepDetail' + : 'pluginRefStepDetail' + + const ioVariables: VariableType[] = + formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + ] + + const ioVariablesError: { isValid: boolean; message: string }[] = + formDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + ] + + // STATES + const [rows, setRows] = useState([]) + + // INITIAL ROWS + const getInitialRows = (): VariableDataRowType[] => + ioVariables.map( + ( + { + name, + allowEmptyValue, + description, + format, + variableType, + value, + refVariableName, + refVariableStage, + valueConstraint, + isRuntimeArg, + }, + id, + ) => { + const isInputVariableRequired = type === PluginVariableType.INPUT && !allowEmptyValue + + return { + data: { + variable: { + ...getVariableColumnRowProps(), + value: name, + required: isInputVariableRequired, + disabled: true, + }, + format: { + ...getFormatColumnRowProps(), + type: DynamicDataTableRowDataType.TEXT, + value: format, + disabled: true, + props: {}, + }, + val: + type === PluginVariableType.INPUT + ? { + type: DynamicDataTableRowDataType.SELECT_TEXT, + value: variableType === RefVariableType.NEW ? value : refVariableName || '', + props: { + ...getValColumnRowProps(emptyRowParams, id).props, + Icon: + refVariableStage || variableType !== RefVariableType.NEW + ? getSystemVariableIcon() + : null, + }, + } + : { + type: DynamicDataTableRowDataType.TEXT, + value: description, + disabled: true, + props: {}, + }, + }, + customState: { + choices: (valueConstraint?.choices || []).map((choiceValue, index) => ({ + id: index, + value: choiceValue, + error: '', + })), + askValueAtRuntime: isRuntimeArg ?? false, + blockCustomValue: valueConstraint?.blockCustomValue ?? false, + selectedValue: null, + }, + id, + } + }, + ) + + useEffect(() => { + setRows(getInitialRows()) + }, [JSON.stringify(ioVariables)]) + + useEffect(() => { + if (rows.length) { + const updatedFormData = structuredClone(formData) + const updatedFormDataErrorObj = structuredClone(formDataErrorObj) + + const updatedIOVariables: VariableType[] = rows.map(({ customState }, index) => { + const { askValueAtRuntime, blockCustomValue, choices, selectedValue } = customState + let variableDetail + + if (selectedValue) { + if (selectedValue.refVariableStepIndex) { + variableDetail = { + value: '', + variableType: RefVariableType.FROM_PREVIOUS_STEP, + refVariableStepIndex: selectedValue.refVariableStepIndex, + refVariableName: selectedValue.label, + format: selectedValue.format, + refVariableStage: selectedValue.refVariableStage, + } + } else if (selectedValue.variableType === RefVariableType.GLOBAL) { + variableDetail = { + variableType: RefVariableType.GLOBAL, + refVariableStepIndex: 0, + refVariableName: selectedValue.label, + format: selectedValue.format, + value: '', + refVariableStage: '', + } + } else { + variableDetail = { + variableType: RefVariableType.NEW, + value: selectedValue.label, + refVariableName: '', + refVariableStage: '', + } + } + if (formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.PLUGIN_REF) { + variableDetail.format = ioVariables[index].format + } + } + + return { + ...ioVariables[index], + ...variableDetail, + isRuntimeArg: askValueAtRuntime, + valueConstraint: { + choices: choices.map(({ value }) => value), + blockCustomValue, + constraint: null, + }, + } + }) + + updatedFormData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + ] = updatedIOVariables + + validateTask( + updatedFormData[activeStageName].steps[selectedTaskIndex], + updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex], + ) + setFormDataErrorObj(updatedFormDataErrorObj) + setFormData(updatedFormData) + } + }, [rows]) + + // METHODS + const handleRowUpdateAction = ({ actionType, actionValue, headerKey, rowId }: HandleRowUpdateActionProps) => { + let updatedRows = [...rows] + + switch (actionType) { + case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: + updatedRows = updatedRows.map((row) => { + const { id, data, customState } = row + + if (id === rowId) { + return { + ...row, + data: { + ...data, + val: + data.val.type === DynamicDataTableRowDataType.SELECT_TEXT + ? { + ...data.val, + props: { + ...data.val.props, + options: [ + { + label: 'Default variables', + options: customState.choices.map(({ value }) => ({ + label: value, + value, + })), + }, + ...data.val.props.options.filter( + ({ label }) => label !== 'Default variables', + ), + ], + }, + } + : data.val, + }, + } + } + + return row + }) + break + + case VariableDataTableActionType.UPDATE_CHOICES: + updatedRows = updatedRows.map((row) => + row.id === rowId + ? { ...row, customState: { ...row.customState, choices: actionValue(row.customState.choices) } } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: + updatedRows = updatedRows.map((row) => + row.id === rowId + ? { ...row, customState: { ...row.customState, blockCustomValue: actionValue } } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME: + updatedRows = updatedRows.map((row) => + row.id === rowId + ? { ...row, customState: { ...row.customState, askValueAtRuntime: actionValue } } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ROW: + updatedRows = rows.map((row) => + row.id === rowId + ? { + ...row, + data: { + ...row.data, + [headerKey]: { + ...row.data[headerKey], + value: actionValue, + }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_VAL_COLUMN: + updatedRows = updatedRows.map((row) => { + if (row.id === rowId && row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { + return { + ...row, + data: { + ...row.data, + val: { + ...row.data.val, + value: actionValue.value, + props: { + ...row.data.val.props, + Icon: + actionValue.value && + ((actionValue.selectedValue as ExtendedOptionType).refVariableStage || + ((actionValue.selectedValue as ExtendedOptionType)?.variableType && + (actionValue.selectedValue as ExtendedOptionType).variableType !== + RefVariableType.NEW)) + ? getSystemVariableIcon() + : null, + }, + }, + }, + customState: { + ...row.customState, + selectedValue: actionValue.selectedValue, + }, + } + } + return row + }) + break + + default: + break + } + + setRows(updatedRows) + } + + const dataTableHandleAddition = () => { + const data = getEmptyVariableDataTableRow(emptyRowParams) + const editedRows = [data, ...rows] + setRows(editedRows) + } + + const dataTableHandleChange = ( + updatedRow: VariableDataRowType, + headerKey: VariableDataKeys, + value: string, + extraData, + ) => { + if (headerKey !== 'val') { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: value, + headerKey, + rowId: updatedRow.id, + }) + } else if (headerKey === 'val') { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_VAL_COLUMN, + actionValue: { value, selectedValue: extraData.selectedValue }, + headerKey, + rowId: updatedRow.id, + }) + } + } + + const dataTableHandleDelete = (row: VariableDataRowType) => { + const remainingRows = rows.filter(({ id }) => id !== row.id) + + if (remainingRows.length === 0) { + const emptyRowData = getEmptyVariableDataTableRow(emptyRowParams) + setRows([emptyRowData]) + return + } + + setRows(remainingRows) + } + + const handleChoicesAddToValColumn = (rowId: string | number) => () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS, + rowId, + headerKey: null, + actionValue: null, + }) + } + + const validationSchema: DynamicDataTableProps['validationSchema'] = (_, key, { id }) => { + if (key === 'val') { + const index = rows.findIndex((row) => row.id === id) + if (index > -1 && ioVariablesError[index]) { + const { isValid, message } = ioVariablesError[index] + return { isValid, errorMessages: [message] } + } + } + + return { isValid: true, errorMessages: [] } + } + + const actionButtonRenderer = (row: VariableDataRowType) => ( + + ) + + const getActionButtonConfig = (): DynamicDataTableProps['actionButtonConfig'] => { + if (type === PluginVariableType.INPUT) { + return { + renderer: actionButtonRenderer, + key: 'val', + position: 'end', + } + } + return null + } + + return ( + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx new file mode 100644 index 0000000000..d0f7505509 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' + +import { TippyCustomized, TippyTheme } from '@devtron-labs/devtron-fe-common-lib' + +import { ReactComponent as ICSlidersVertical } from '@Icons/ic-sliders-vertical.svg' + +import { VariableDataTableActionType, VariableDataTableRowActionProps } from './types' +import { getValidatedChoices } from './utils' + +import { AddChoicesOverlay } from './AddChoicesOverlay' + +export const VariableDataTableRowAction = ({ + handleRowUpdateAction, + onClose, + row, +}: VariableDataTableRowActionProps) => { + const { data, customState, id } = row + const [visible, setVisible] = useState(false) + + const handleClose = () => { + const { choices, isValid } = getValidatedChoices(customState.choices) + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId: row.id, + headerKey: null, + actionValue: () => choices, + }) + if (isValid) { + setVisible(false) + onClose() + } + } + + const handleAction = () => { + if (visible) { + handleClose() + } else { + setVisible(true) + } + } + + return ( + + } + appendTo={document.getElementById('visible-modal')} + > + + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts new file mode 100644 index 0000000000..163387725c --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -0,0 +1,48 @@ +import { DynamicDataTableHeaderType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' + +import { PluginVariableType } from '@Components/ciPipeline/types' + +import { VariableDataKeys } from './types' + +export const getVariableDataTableHeaders = ( + type: PluginVariableType, +): DynamicDataTableHeaderType[] => [ + { + label: 'VARIABLE', + key: 'variable', + width: '200px', + }, + { + label: 'TYPE', + key: 'format', + width: '100px', + }, + { + label: type === PluginVariableType.INPUT ? 'VALUE' : 'DESCRIPTION', + key: 'val', + width: '1fr', + }, +] + +export const FORMAT_COLUMN_OPTIONS: SelectPickerOptionType[] = [ + { + label: 'STRING', + value: 'STRING', + }, + { + label: 'NUMBER', + value: 'NUMBER', + }, + { + label: 'BOOLEAN', + value: 'BOOLEAN', + }, + { + label: 'FILE', + value: 'FILE', + }, + { + label: 'DATE', + value: 'DATE', + }, +] diff --git a/src/components/CIPipelineN/VariableDataTable/helpers.tsx b/src/components/CIPipelineN/VariableDataTable/helpers.tsx new file mode 100644 index 0000000000..763a3ca6fe --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/helpers.tsx @@ -0,0 +1,12 @@ +import { Tooltip } from '@devtron-labs/devtron-fe-common-lib' + +import { ReactComponent as Var } from '@Icons/ic-var-initial.svg' +import { TIPPY_VAR_MSG } from '../Constants' + +export const getSystemVariableIcon = () => ( + +
+ +
+
+) diff --git a/src/components/CIPipelineN/VariableDataTable/index.ts b/src/components/CIPipelineN/VariableDataTable/index.ts new file mode 100644 index 0000000000..ee1427a9a9 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/index.ts @@ -0,0 +1 @@ +export * from './VariableDataTable' diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts new file mode 100644 index 0000000000..a339c7741f --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -0,0 +1,57 @@ +import { DynamicDataTableRowType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' + +export type VariableDataKeys = 'variable' | 'format' | 'val' + +export type VariableDataCustomState = { + choices: { id: number; value: string; error: string }[] + askValueAtRuntime: boolean + blockCustomValue: boolean + selectedValue: Record +} + +export type VariableDataRowType = DynamicDataTableRowType + +export enum VariableDataTableActionType { + UPDATE_ROW = 'update_row', + UPDATE_VAL_COLUMN = 'update_val_column', + ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS = 'add_choices_to_value_column_options', + UPDATE_CHOICES = 'update_choices', + UPDATE_ASK_VALUE_AT_RUNTIME = 'update_ask_value_at_runtime', + UPDATE_ALLOW_CUSTOM_INPUT = 'update_allow_custom_input', +} + +type VariableDataTableActionPropsMap = { + [VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT]: VariableDataCustomState['blockCustomValue'] + [VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME]: VariableDataCustomState['askValueAtRuntime'] + [VariableDataTableActionType.UPDATE_CHOICES]: ( + currentChoices: VariableDataCustomState['choices'], + ) => VariableDataCustomState['choices'] + [VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS]: null + [VariableDataTableActionType.UPDATE_VAL_COLUMN]: { + value: string + selectedValue: SelectPickerOptionType + } + [VariableDataTableActionType.UPDATE_ROW]: string +} + +export type VariableDataTableAction< + T extends keyof VariableDataTableActionPropsMap = keyof VariableDataTableActionPropsMap, +> = T extends keyof VariableDataTableActionPropsMap + ? { actionType: T; actionValue: VariableDataTableActionPropsMap[T] } + : never + +export type HandleRowUpdateActionProps = VariableDataTableAction & { + rowId: string | number + headerKey: VariableDataKeys +} + +export interface VariableDataTableRowActionProps { + row: VariableDataRowType + onClose: () => void + handleRowUpdateAction: (props: HandleRowUpdateActionProps) => void +} + +export type AddChoicesOverlayProps = Pick & + Pick & { + rowId: VariableDataTableRowActionProps['row']['id'] + } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx new file mode 100644 index 0000000000..2eaf20e1ac --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -0,0 +1,215 @@ +import { + DynamicDataTableRowDataType, + PluginType, + RefVariableStageType, + VariableType, +} from '@devtron-labs/devtron-fe-common-lib' + +import { BuildStageVariable, PATTERNS } from '@Config/constants' + +import { PipelineContext } from '@Components/workflowEditor/types' +import { PluginVariableType } from '@Components/ciPipeline/types' +import { excludeVariables } from '../Constants' +import { FORMAT_COLUMN_OPTIONS } from './constants' +import { VariableDataRowType } from './types' + +export const getValueColumnOptions = ( + { + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + type, + }: Pick< + PipelineContext, + | 'activeStageName' + | 'selectedTaskIndex' + | 'inputVariablesListFromPrevStep' + | 'formData' + | 'globalVariables' + | 'isCdPipeline' + > & { type: PluginVariableType }, + index: number, +) => { + const currentStepTypeVariable = + formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE + ? 'inlineStepDetail' + : 'pluginRefStepDetail' + + const ioVariables: VariableType[] = + formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + ] + + const previousStepVariables = [] + const defaultVariables = (ioVariables[index]?.valueConstraint?.choices || []).map((value) => ({ + label: value, + value, + })) + + if (inputVariablesListFromPrevStep[activeStageName].length > 0) { + inputVariablesListFromPrevStep[activeStageName][selectedTaskIndex].forEach((element) => { + previousStepVariables.push({ + ...element, + label: element.name, + value: element.name, + refVariableTaskName: formData[activeStageName]?.steps[element.refVariableStepIndex - 1].name, + }) + }) + } + + if (activeStageName === BuildStageVariable.PostBuild) { + const preBuildStageVariables = [] + const preBuildTaskLength = formData[BuildStageVariable.PreBuild]?.steps?.length + if (preBuildTaskLength >= 1 && !isCdPipeline) { + if (inputVariablesListFromPrevStep[BuildStageVariable.PreBuild].length > 0) { + inputVariablesListFromPrevStep[BuildStageVariable.PreBuild][preBuildTaskLength - 1].forEach( + (element) => { + preBuildStageVariables.push({ + ...element, + label: element.name, + value: element.name, + refVariableTaskName: + formData[BuildStageVariable.PreBuild]?.steps[element.refVariableStepIndex - 1].name, + }) + }, + ) + } + + const stepTypeVariable = + formData[BuildStageVariable.PreBuild].steps[preBuildTaskLength - 1].stepType === PluginType.INLINE + ? 'inlineStepDetail' + : 'pluginRefStepDetail' + const preBuildStageLastTaskOutputVariables = + formData[BuildStageVariable.PreBuild].steps[preBuildTaskLength - 1][stepTypeVariable]?.outputVariables + const outputVariablesLength = preBuildStageLastTaskOutputVariables?.length || 0 + for (let j = 0; j < outputVariablesLength; j++) { + if (preBuildStageLastTaskOutputVariables[j].name) { + const currentVariableDetails = preBuildStageLastTaskOutputVariables[j] + preBuildStageVariables.push({ + ...currentVariableDetails, + label: currentVariableDetails.name, + value: currentVariableDetails.name, + refVariableStepIndex: preBuildTaskLength, + refVariableTaskName: formData[BuildStageVariable.PreBuild].steps[preBuildTaskLength - 1].name, + refVariableStage: RefVariableStageType.PRE_CI, + }) + } + } + } + + return [ + { + label: 'Default variables', + options: defaultVariables, + }, + { + label: 'From Pre-build Stage', + options: preBuildStageVariables, + }, + { + label: 'From Post-build Stage', + options: previousStepVariables, + }, + { + label: 'System variables', + options: globalVariables, + }, + ] + } + + return [ + { + label: 'Default variables', + options: defaultVariables, + }, + { + label: 'From Previous Steps', + options: previousStepVariables, + }, + { + label: 'System variables', + options: globalVariables.filter( + (variable) => + (isCdPipeline && variable.stageType !== 'post-cd') || !excludeVariables.includes(variable.value), + ), + }, + ] +} + +export const getVariableColumnRowProps = (): VariableDataRowType['data']['variable'] => { + const data: VariableDataRowType['data']['variable'] = { + value: '', + type: DynamicDataTableRowDataType.TEXT, + props: {}, + } + + return data +} + +export const getFormatColumnRowProps = () => { + const data: VariableDataRowType['data']['format'] = { + value: FORMAT_COLUMN_OPTIONS[0].value, + type: DynamicDataTableRowDataType.DROPDOWN, + props: { + options: FORMAT_COLUMN_OPTIONS, + }, + } + + return data +} + +export const getValColumnRowProps = (params: Parameters[0], index: number) => { + const data: VariableDataRowType['data']['val'] = { + value: '', + type: DynamicDataTableRowDataType.SELECT_TEXT, + props: { + placeholder: 'Select source or input value', + options: getValueColumnOptions(params, index), + }, + } + + return data +} + +export const getEmptyVariableDataTableRow = ( + params: Pick< + PipelineContext, + | 'activeStageName' + | 'selectedTaskIndex' + | 'inputVariablesListFromPrevStep' + | 'formData' + | 'globalVariables' + | 'isCdPipeline' + > & { type: PluginVariableType }, +): VariableDataRowType => { + const id = (Date.now() * Math.random()).toString(16) + const data: VariableDataRowType = { + data: { + variable: getVariableColumnRowProps(), + format: getFormatColumnRowProps(), + val: getValColumnRowProps(params, -1), + }, + id, + } + + return data +} + +export const validateChoice = (choice: string) => PATTERNS.STRING.test(choice) + +export const getValidatedChoices = (choices: VariableDataRowType['customState']['choices']) => { + let isValid = true + + const updatedChoices: VariableDataRowType['customState']['choices'] = choices.map((choice) => { + const error = !validateChoice(choice.value) ? 'This is a required field' : '' + if (isValid && !!error) { + isValid = false + } + return { ...choice, error } + }) + + return { isValid, choices: updatedChoices } +} diff --git a/src/components/ciPipeline/ciPipeline.service.ts b/src/components/ciPipeline/ciPipeline.service.ts index 7e777621e0..b8673fc27a 100644 --- a/src/components/ciPipeline/ciPipeline.service.ts +++ b/src/components/ciPipeline/ciPipeline.service.ts @@ -23,6 +23,7 @@ import { PluginType, RefVariableType, PipelineBuildStageType, + VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' import { Routes, SourceTypeMap, TriggerType, ViewType } from '../../config' import { getSourceConfig, getWebhookDataMetaConfig } from '../../services/service' @@ -407,7 +408,7 @@ function migrateOldData( ): PipelineBuildStageType { const commonFields = { value: '', - format: 'STRING', + format: VariableTypeFormat.STRING, description: '', defaultValue: '', variableType: RefVariableType.GLOBAL, diff --git a/src/css/base.scss b/src/css/base.scss index 725ae3e252..dc60bbe7df 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -441,6 +441,10 @@ a:focus { right: -3px; } +.dc__right-8 { + right: 8px; +} + .dc__right-10 { right: 10px; } From 69f304ef2fad8a29e4a2af12bb73066f8ac42e56 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Thu, 5 Dec 2024 17:55:07 +0530 Subject: [PATCH 02/36] feat: CI/CD Pipeline - Pre/Post Custom Task & Plugin Integration --- src/components/CIPipelineN/CIPipeline.tsx | 4 + .../CIPipelineN/CreatePluginModal/utils.tsx | 1 + .../CIPipelineN/TaskDetailComponent.tsx | 14 +- .../CIPipelineN/VariableContainer.tsx | 4 +- .../VariableDataTable/AddChoicesOverlay.tsx | 191 ----- .../VariableDataTable/ValueConfigOverlay.tsx | 316 +++++++ .../VariableConfigOverlay.tsx | 84 ++ .../VariableDataTable/VariableDataTable.tsx | 778 +++++++++++------- .../VariableDataTablePopupMenu.tsx | 72 ++ .../VariableDataTableRowAction.tsx | 73 -- .../VariableDataTable/constants.ts | 64 +- .../CIPipelineN/VariableDataTable/types.ts | 147 +++- .../CIPipelineN/VariableDataTable/utils.tsx | 487 +++++++++-- src/components/cdPipeline/CDPipeline.tsx | 3 + src/components/cdPipeline/cdpipeline.util.tsx | 2 +- .../ciPipeline/ciPipeline.service.ts | 44 +- src/components/ciPipeline/types.ts | 8 + src/components/workflowEditor/types.ts | 2 + src/config/constants.ts | 1 + src/css/base.scss | 6 +- 20 files changed, 1611 insertions(+), 690 deletions(-) delete mode 100644 src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx create mode 100644 src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx delete mode 100644 src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index a0496e2dba..9f826b9dd2 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -62,6 +62,7 @@ import { getInitData, getInitDataWithCIPipeline, saveCIPipeline, + uploadCIPipelineFile, } from '../ciPipeline/ciPipeline.service' import { ValidationRules } from '../ciPipeline/validationRules' import { CIBuildType, CIPipelineBuildType, CIPipelineDataType, CIPipelineType } from '../ciPipeline/types' @@ -781,6 +782,8 @@ export default function CIPipeline({ } } + const uploadFile = (file: File[]) => uploadCIPipelineFile({ appId: +appId, ciPipelineId: +ciPipelineId, file }) + const contextValue = useMemo( () => ({ formData, @@ -808,6 +811,7 @@ export default function CIPipeline({ handleDisableParentModalCloseUpdate, handleValidateMandatoryPlugins, mandatoryPluginData, + uploadFile, }), [ formData, diff --git a/src/components/CIPipelineN/CreatePluginModal/utils.tsx b/src/components/CIPipelineN/CreatePluginModal/utils.tsx index ed6b9a398e..73b6a77846 100644 --- a/src/components/CIPipelineN/CreatePluginModal/utils.tsx +++ b/src/components/CIPipelineN/CreatePluginModal/utils.tsx @@ -156,6 +156,7 @@ const parseInputVariablesIntoCreatePluginPayload = ( valueType: variable.variableType, referenceVariableName: variable.refVariableName, isExposed: true, + // TODO: handle file type here })) || [] export const getCreatePluginPayload = ({ diff --git a/src/components/CIPipelineN/TaskDetailComponent.tsx b/src/components/CIPipelineN/TaskDetailComponent.tsx index 7cfcba114e..bfda4e0bc2 100644 --- a/src/components/CIPipelineN/TaskDetailComponent.tsx +++ b/src/components/CIPipelineN/TaskDetailComponent.tsx @@ -31,13 +31,13 @@ import { PluginDataStoreType, getUpdatedPluginStore, } from '@devtron-labs/devtron-fe-common-lib' -import CustomInputOutputVariables from './CustomInputOutputVariables' import { PluginDetailHeader } from './PluginDetailHeader' import { TaskTypeDetailComponent } from './TaskTypeDetailComponent' import { ValidationRules } from '../ciPipeline/validationRules' import { pipelineContext } from '../workflowEditor/workflowEditor' import { PluginDetailHeaderProps, TaskDetailComponentParamsType } from './types' import { filterInvalidConditionDetails } from '@Components/cdPipeline/cdpipeline.util' +import { VariableDataTable } from './VariableDataTable' export const TaskDetailComponent = () => { const { @@ -273,7 +273,9 @@ export const TaskDetailComponent = () => {
{selectedStep.stepType === PluginType.INLINE ? ( - + <> + + ) : ( )}{' '} @@ -289,7 +291,13 @@ export const TaskDetailComponent = () => { {formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].scriptType !== ScriptType.CONTAINERIMAGE && ( - + <> + + )} ) : ( diff --git a/src/components/CIPipelineN/VariableContainer.tsx b/src/components/CIPipelineN/VariableContainer.tsx index 0323162f1f..6abd456c1a 100644 --- a/src/components/CIPipelineN/VariableContainer.tsx +++ b/src/components/CIPipelineN/VariableContainer.tsx @@ -26,7 +26,7 @@ import { VariableDataTable } from './VariableDataTable' export const VariableContainer = ({ type }: { type: PluginVariableType }) => { const [collapsedSection, setCollapsedSection] = useState(type !== PluginVariableType.INPUT) - const { formData, selectedTaskIndex, activeStageName, formDataErrorObj } = useContext(pipelineContext) + const { formData, selectedTaskIndex, activeStageName, formDataErrorObj } = useContext(pipelineContext) const variableLength = formData[activeStageName].steps[selectedTaskIndex].pluginRefStepDetail[ type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' @@ -66,7 +66,7 @@ export const VariableContainer = ({ type }: { type: PluginVariableType }) => {
No {type} variables
)} - {!collapsedSection && variableLength > 0 && } + {!collapsedSection && variableLength > 0 && } ) } diff --git a/src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx deleted file mode 100644 index 2265b5c696..0000000000 --- a/src/components/CIPipelineN/VariableDataTable/AddChoicesOverlay.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import { ChangeEvent } from 'react' - -import { - Button, - ButtonStyleType, - ButtonVariantType, - Checkbox, - CHECKBOX_VALUE, - ComponentSizeType, - CustomInput, - Tooltip, -} from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' -import { ReactComponent as ICClose } from '@Icons/ic-close.svg' -import { ReactComponent as ICChoicesDropdown } from '@Icons/ic-choices-dropdown.svg' - -import { AddChoicesOverlayProps, VariableDataTableActionType } from './types' -import { validateChoice } from './utils' - -export const AddChoicesOverlay = ({ - choices, - askValueAtRuntime, - blockCustomValue, - rowId, - handleRowUpdateAction, -}: AddChoicesOverlayProps) => { - // METHODS - const handleAddChoices = () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId, - headerKey: null, - actionValue: (currentChoices) => [{ value: '', id: currentChoices.length, error: '' }, ...currentChoices], - }) - } - - const handleChoiceChange = (choiceId: number) => (e: ChangeEvent) => { - const choiceValue = e.target.value - // TODO: Rethink validation disc with product - const error = !validateChoice(choiceValue) ? 'This is a required field' : '' - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId, - headerKey: null, - actionValue: (currentChoices) => - currentChoices.map((choice) => - choice.id === choiceId ? { id: choiceId, value: choiceValue, error } : choice, - ), - }) - } - - const handleChoiceDelete = (choiceId: number) => () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId, - headerKey: null, - actionValue: (currentChoices) => currentChoices.filter(({ id }) => id !== choiceId), - }) - } - - const handleAllowCustomInput = () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT, - rowId, - headerKey: null, - actionValue: !blockCustomValue, - }) - } - - const handleAskValueAtRuntime = () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME, - rowId, - headerKey: null, - actionValue: !askValueAtRuntime, - }) - } - - return ( -
- {choices.length ? ( -
-
-
-
- {choices.map(({ id, value, error }) => ( -
- -
- ))} -
-
- ) : ( -
-
- -
-

Set value choices

-

Allow users to select a value from a pre-defined set of choices

-
-
-
-
-
- )} -
- {!!choices.length && ( - - -

Allow custom input

-

Allow entering any value other than provided choices

-
- } - > -
Allow Custom input
- - - )} - - -

Ask value at runtime

-

- Value can be provided at runtime. Entered value will be pre-filled as default -

-
- } - > -
Ask value at runtime
- - - - - ) -} diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx new file mode 100644 index 0000000000..2d94cd8667 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx @@ -0,0 +1,316 @@ +import { ChangeEvent } from 'react' + +import { + Button, + ButtonStyleType, + ButtonVariantType, + Checkbox, + CHECKBOX_VALUE, + ComponentSizeType, + CustomInput, + ResizableTextarea, + SelectPicker, + SelectPickerOptionType, + Tooltip, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' + +import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' +import { ReactComponent as ICClose } from '@Icons/ic-close.svg' +import { ReactComponent as ICChoicesDropdown } from '@Icons/ic-choices-dropdown.svg' +import { ReactComponent as ICInfoOutlineGrey } from '@Icons/ic-info-outline-grey.svg' + +import { ConfigOverlayProps, VariableDataTableActionType } from './types' +import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_OPTIONS_MAP } from './constants' +import { testValueForNumber } from './utils' + +export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlayProps) => { + const { id: rowId, data, customState } = row + const { choices, askValueAtRuntime, blockCustomValue, fileInfo } = customState + + // CONSTANTS + const isFormatNumber = data.format.value === VariableTypeFormat.NUMBER + const isFormatBoolOrDate = + data.format.value === VariableTypeFormat.BOOL || data.format.value === VariableTypeFormat.DATE + const isFormatFile = data.format.value === VariableTypeFormat.FILE + + // METHODS + const handleAddChoices = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId, + actionValue: (currentChoices) => [{ value: '', id: currentChoices.length, error: '' }, ...currentChoices], + }) + } + + const handleChoiceChange = (choiceId: number) => (e: ChangeEvent) => { + const choiceValue = e.target.value + if (isFormatNumber && !testValueForNumber(choiceValue)) { + return + } + + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId, + actionValue: (currentChoices) => + currentChoices.map((choice) => + choice.id === choiceId ? { id: choiceId, value: choiceValue } : choice, + ), + }) + } + + const handleChoiceDelete = (choiceId: number) => () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_CHOICES, + rowId, + actionValue: (currentChoices) => currentChoices.filter(({ id }) => id !== choiceId), + }) + } + + const handleAllowCustomInput = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT, + rowId, + actionValue: !blockCustomValue, + }) + } + + const handleAskValueAtRuntime = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME, + rowId, + actionValue: !askValueAtRuntime, + }) + } + + const handleFileMountChange = (e: ChangeEvent) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_MOUNT, + rowId, + actionValue: e.target.value, + }) + } + + const handleFileAllowedExtensionsChange = (e: ChangeEvent) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS, + rowId, + actionValue: e.target.value, + }) + } + + const handleFileMaxSizeChange = (e: ChangeEvent) => { + const maxSize = e.target.value + if (!testValueForNumber(maxSize)) { + return + } + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_MAX_SIZE, + rowId, + actionValue: { + size: maxSize, + unit: fileInfo.unit, + }, + }) + } + + const handleFileSizeUnitChange = (unit: SelectPickerOptionType) => { + if (fileInfo.unit !== unit) { + const maxSize = fileInfo.maxUploadSize + ? (parseFloat(fileInfo.maxUploadSize) * unit.value).toString() + : fileInfo.maxUploadSize + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_MAX_SIZE, + rowId, + actionValue: { + size: maxSize, + unit, + }, + }) + } + } + + // RENDERERS + const renderContent = () => { + if (isFormatFile) { + return ( +
+ +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + +
+
+
+ +
+
+ +
+
+
+ ) + } + + if (isFormatBoolOrDate) { + return ( +
+
+ +
+

Choices not allowed

+

{`Variable type "${FORMAT_OPTIONS_MAP[data.format.value]}" does not support choices`}

+
+
+
+ ) + } + + if (choices.length) { + return ( +
+
+
+
+ {choices.map(({ id, value }) => ( +
+ +
+ ))} +
+
+ ) + } + + return ( +
+
+ +
+

Set value choices

+

Allow users to select a value from a pre-defined set of choices

+
+
+
+
+
+ ) + } + + return ( + <> + {renderContent()} +
+ {!!choices.length && ( + + +

Allow custom input

+

Allow entering any value other than provided choices

+
+ } + > +
Allow Custom input
+ + + )} + + +

Ask value at runtime

+

+ Value can be provided at runtime. Entered value will be pre-filled as default +

+ + } + > +
Ask value at runtime
+
+
+ + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx new file mode 100644 index 0000000000..8527ff22a3 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx @@ -0,0 +1,84 @@ +import { ChangeEvent } from 'react' + +import { Checkbox, CHECKBOX_VALUE, CustomInput, ResizableTextarea, Tooltip } from '@devtron-labs/devtron-fe-common-lib' + +import { ConfigOverlayProps, VariableDataTableActionType } from './types' + +export const VariableConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlayProps) => { + const { id: rowId, data, customState } = row + const { variableDescription, isVariableRequired } = customState + + // METHODS + const handleVariableName = (e: ChangeEvent) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + rowId, + headerKey: 'variable', + actionValue: e.target.value, + }) + } + + const handleVariableDescriptionChange = (e: ChangeEvent) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION, + rowId, + actionValue: e.target.value, + }) + } + + const handleVariableRequired = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED, + rowId, + actionValue: !isVariableRequired, + }) + } + + return ( + <> +
+ +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + +
+
+
+ + +

Value is required

+

Get this tooltip from Utkarsh

+
+ } + > +
Value is required
+ + + + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx index 8a848fc806..8cb15c0e43 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx @@ -1,4 +1,4 @@ -import { useContext, useState, useEffect } from 'react' +import { useContext, useState, useEffect, useRef } from 'react' import { DynamicDataTable, @@ -6,26 +6,50 @@ import { DynamicDataTableRowDataType, PluginType, RefVariableType, + SelectPickerOptionType, + ToastManager, + ToastVariantType, VariableType, + VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' import { pipelineContext } from '@Components/workflowEditor/workflowEditor' import { PluginVariableType } from '@Components/ciPipeline/types' - import { ExtendedOptionType } from '@Components/app/types' -import { getVariableDataTableHeaders } from './constants' + +import { + FILE_UPLOAD_SIZE_UNIT_OPTIONS, + getVariableDataTableHeaders, + VAL_COLUMN_CHOICES_DROPDOWN_LABEL, +} from './constants' import { getSystemVariableIcon } from './helpers' -import { HandleRowUpdateActionProps, VariableDataKeys, VariableDataRowType, VariableDataTableActionType } from './types' import { + HandleRowUpdateActionProps, + VariableDataCustomState, + VariableDataKeys, + VariableDataRowType, + VariableDataTableActionType, +} from './types' +import { + convertVariableDataTableToFormData, getEmptyVariableDataTableRow, - getFormatColumnRowProps, getValColumnRowProps, - getVariableColumnRowProps, + getValColumnRowValue, + getVariableDataTableInitialRows, + validateMaxFileSize, } from './utils' -import { VariableDataTableRowAction } from './VariableDataTableRowAction' - -export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { +import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' +import { VariableConfigOverlay } from './VariableConfigOverlay' +import { ValueConfigOverlay } from './ValueConfigOverlay' + +export const VariableDataTable = ({ + type, + isCustomTask = false, +}: { + type: PluginVariableType + isCustomTask?: boolean +}) => { // CONTEXTS const { inputVariablesListFromPrevStep, @@ -38,6 +62,8 @@ export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { validateTask, setFormData, setFormDataErrorObj, + calculateLastStepDetail, + uploadFile, } = useContext(pipelineContext) // CONSTANTS @@ -49,7 +75,16 @@ export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { globalVariables, isCdPipeline, type, + description: null, + format: VariableTypeFormat.STRING, + variableType: RefVariableType.NEW, + value: '', + refVariableName: null, + refVariableStage: null, + valueConstraint: null, + id: 0, } + const currentStepTypeVariable = formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE ? 'inlineStepDetail' @@ -68,321 +103,463 @@ export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { // STATES const [rows, setRows] = useState([]) - // INITIAL ROWS - const getInitialRows = (): VariableDataRowType[] => - ioVariables.map( - ( - { - name, - allowEmptyValue, - description, - format, - variableType, - value, - refVariableName, - refVariableStage, - valueConstraint, - isRuntimeArg, - }, - id, - ) => { - const isInputVariableRequired = type === PluginVariableType.INPUT && !allowEmptyValue - - return { - data: { - variable: { - ...getVariableColumnRowProps(), - value: name, - required: isInputVariableRequired, - disabled: true, - }, - format: { - ...getFormatColumnRowProps(), - type: DynamicDataTableRowDataType.TEXT, - value: format, - disabled: true, - props: {}, - }, - val: - type === PluginVariableType.INPUT - ? { - type: DynamicDataTableRowDataType.SELECT_TEXT, - value: variableType === RefVariableType.NEW ? value : refVariableName || '', - props: { - ...getValColumnRowProps(emptyRowParams, id).props, - Icon: - refVariableStage || variableType !== RefVariableType.NEW - ? getSystemVariableIcon() - : null, - }, - } - : { - type: DynamicDataTableRowDataType.TEXT, - value: description, - disabled: true, - props: {}, - }, - }, - customState: { - choices: (valueConstraint?.choices || []).map((choiceValue, index) => ({ - id: index, - value: choiceValue, - error: '', - })), - askValueAtRuntime: isRuntimeArg ?? false, - blockCustomValue: valueConstraint?.blockCustomValue ?? false, - selectedValue: null, - }, - id, - } - }, - ) - - useEffect(() => { - setRows(getInitialRows()) - }, [JSON.stringify(ioVariables)]) + // REFS + const initialRowsSet = useRef('') useEffect(() => { - if (rows.length) { - const updatedFormData = structuredClone(formData) - const updatedFormDataErrorObj = structuredClone(formDataErrorObj) - - const updatedIOVariables: VariableType[] = rows.map(({ customState }, index) => { - const { askValueAtRuntime, blockCustomValue, choices, selectedValue } = customState - let variableDetail - - if (selectedValue) { - if (selectedValue.refVariableStepIndex) { - variableDetail = { - value: '', - variableType: RefVariableType.FROM_PREVIOUS_STEP, - refVariableStepIndex: selectedValue.refVariableStepIndex, - refVariableName: selectedValue.label, - format: selectedValue.format, - refVariableStage: selectedValue.refVariableStage, - } - } else if (selectedValue.variableType === RefVariableType.GLOBAL) { - variableDetail = { - variableType: RefVariableType.GLOBAL, - refVariableStepIndex: 0, - refVariableName: selectedValue.label, - format: selectedValue.format, - value: '', - refVariableStage: '', - } - } else { - variableDetail = { - variableType: RefVariableType.NEW, - value: selectedValue.label, - refVariableName: '', - refVariableStage: '', - } - } - if (formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.PLUGIN_REF) { - variableDetail.format = ioVariables[index].format - } - } - - return { - ...ioVariables[index], - ...variableDetail, - isRuntimeArg: askValueAtRuntime, - valueConstraint: { - choices: choices.map(({ value }) => value), - blockCustomValue, - constraint: null, - }, - } - }) + setRows( + ioVariables?.length + ? getVariableDataTableInitialRows({ emptyRowParams, ioVariables, isCustomTask, type }) + : [getEmptyVariableDataTableRow(emptyRowParams)], + ) + initialRowsSet.current = 'set' + }, []) - updatedFormData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ - type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' - ] = updatedIOVariables - - validateTask( - updatedFormData[activeStageName].steps[selectedTaskIndex], - updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex], - ) - setFormDataErrorObj(updatedFormDataErrorObj) - setFormData(updatedFormData) - } - }, [rows]) + // useEffect(() => { + // console.log('meg', rows, ioVariables, formDataErrorObj) + // }, [JSON.stringify(ioVariables)]) // METHODS - const handleRowUpdateAction = ({ actionType, actionValue, headerKey, rowId }: HandleRowUpdateActionProps) => { - let updatedRows = [...rows] - - switch (actionType) { - case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: - updatedRows = updatedRows.map((row) => { - const { id, data, customState } = row - - if (id === rowId) { - return { - ...row, - data: { - ...data, - val: - data.val.type === DynamicDataTableRowDataType.SELECT_TEXT - ? { - ...data.val, - props: { - ...data.val.props, - options: [ - { - label: 'Default variables', - options: customState.choices.map(({ value }) => ({ - label: value, - value, - })), - }, - ...data.val.props.options.filter( - ({ label }) => label !== 'Default variables', + const handleRowUpdateAction = (rowAction: HandleRowUpdateActionProps) => { + const { actionType } = rowAction + + setRows((prevRows) => { + let updatedRows = [...prevRows] + switch (actionType) { + case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: + updatedRows = updatedRows.map((row) => { + const { id, data, customState } = row + const choicesOptions = customState.choices + .filter(({ value }) => !!value) + .map(({ value }) => ({ label: value, value })) + + if (id === rowAction.rowId && choicesOptions.length > 0) { + return { + ...row, + data: { + ...data, + val: + data.val.type === DynamicDataTableRowDataType.SELECT_TEXT + ? { + ...data.val, + props: { + ...data.val.props, + options: data.val.props.options.map((option) => + option.label === VAL_COLUMN_CHOICES_DROPDOWN_LABEL + ? { + label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, + options: choicesOptions, + } + : option, ), - ], - }, - } - : data.val, - }, + }, + } + : data.val, + }, + } } - } - return row - }) - break - - case VariableDataTableActionType.UPDATE_CHOICES: - updatedRows = updatedRows.map((row) => - row.id === rowId - ? { ...row, customState: { ...row.customState, choices: actionValue(row.customState.choices) } } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: - updatedRows = updatedRows.map((row) => - row.id === rowId - ? { ...row, customState: { ...row.customState, blockCustomValue: actionValue } } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME: - updatedRows = updatedRows.map((row) => - row.id === rowId - ? { ...row, customState: { ...row.customState, askValueAtRuntime: actionValue } } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_ROW: - updatedRows = rows.map((row) => - row.id === rowId - ? { - ...row, - data: { - ...row.data, - [headerKey]: { - ...row.data[headerKey], - value: actionValue, + return row + }) + break + + case VariableDataTableActionType.UPDATE_CHOICES: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + choices: rowAction.actionValue(row.customState.choices), + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + data: { + ...row.data, + ...(row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT + ? { + val: { + ...row.data.val, + props: { + ...row.data.val.props, + selectPickerProps: { + isCreatable: + row.data.format.value !== VariableTypeFormat.BOOL && + row.data.format.value !== VariableTypeFormat.DATE && + !row.customState?.blockCustomValue, + }, + }, + }, + } + : {}), + }, + customState: { ...row.customState, blockCustomValue: rowAction.actionValue }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { ...row, customState: { ...row.customState, askValueAtRuntime: rowAction.actionValue } } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { ...row.customState, variableDescription: rowAction.actionValue }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + data: { + ...row.data, + variable: { ...row.data.variable, required: rowAction.actionValue }, + }, + customState: { ...row.customState, isVariableRequired: rowAction.actionValue }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_MOUNT: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + fileInfo: { ...row.customState.fileInfo, mountDir: rowAction.actionValue }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + data: + row.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD + ? { + ...row.data, + val: { + ...row.data.val, + props: { + ...row.data.val.props, + fileTypes: rowAction.actionValue.split(','), + }, + }, + } + : row.data, + customState: { + ...row.customState, + fileInfo: { + ...row.customState.fileInfo, + allowedExtensions: rowAction.actionValue, + }, }, - }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_VAL_COLUMN: - updatedRows = updatedRows.map((row) => { - if (row.id === rowId && row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { - return { - ...row, - data: { - ...row.data, - val: { - ...row.data.val, - value: actionValue.value, - props: { - ...row.data.val.props, - Icon: - actionValue.value && - ((actionValue.selectedValue as ExtendedOptionType).refVariableStage || - ((actionValue.selectedValue as ExtendedOptionType)?.variableType && - (actionValue.selectedValue as ExtendedOptionType).variableType !== - RefVariableType.NEW)) - ? getSystemVariableIcon() - : null, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_MAX_SIZE: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + fileInfo: { + ...row.customState.fileInfo, + maxUploadSize: rowAction.actionValue.size, + unit: rowAction.actionValue.unit, + }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.ADD_ROW: + updatedRows = [ + getEmptyVariableDataTableRow({ ...emptyRowParams, id: rowAction.actionValue }), + ...updatedRows, + ] + break + + case VariableDataTableActionType.DELETE_ROW: + updatedRows = updatedRows.filter((row) => row.id !== rowAction.rowId) + if (updatedRows.length === 0) { + updatedRows = [getEmptyVariableDataTableRow(emptyRowParams)] + } + break + + case VariableDataTableActionType.UPDATE_ROW: + updatedRows = rows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + data: { + ...row.data, + [rowAction.headerKey]: { + ...row.data[rowAction.headerKey], + value: rowAction.actionValue, + }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + fileInfo: { + ...row.customState.fileInfo, + id: rowAction.actionValue.fileReferenceId, + }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_VAL_COLUMN: + updatedRows = updatedRows.map((row) => { + if ( + row.id === rowAction.rowId && + row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT + ) { + const { selectedValue, value } = rowAction.actionValue as { + selectedValue: SelectPickerOptionType & ExtendedOptionType + value: string + } + const isSystemVariable = + !!selectedValue.refVariableStage || + (selectedValue?.variableType && selectedValue.variableType !== RefVariableType.NEW) + + return { + ...row, + data: { + ...row.data, + val: { + ...row.data.val, + value: getValColumnRowValue( + row.data.val.value, + row.data.format.value as VariableTypeFormat, + value, + selectedValue, + isSystemVariable, + ), + props: { + ...row.data.val.props, + Icon: value && isSystemVariable ? getSystemVariableIcon() : null, + }, }, }, - }, - customState: { - ...row.customState, - selectedValue: actionValue.selectedValue, - }, + customState: { + ...row.customState, + selectedValue: rowAction.actionValue.selectedValue, + }, + } } - } - return row - }) - break - default: - break - } + return row + }) + break + + case VariableDataTableActionType.UPDATE_FORMAT_COLUMN: + updatedRows = updatedRows.map((row) => { + if ( + row.id === rowAction.rowId && + row.data.format.type === DynamicDataTableRowDataType.DROPDOWN + ) { + return { + ...row, + data: { + ...row.data, + format: { + ...row.data.format, + value: rowAction.actionValue.value, + }, + val: getValColumnRowProps({ + ...emptyRowParams, + activeStageName, + formData, + type, + format: rowAction.actionValue.value as VariableTypeFormat, + id: rowAction.rowId as number, + }), + }, + customState: { + isVariableRequired: false, + variableDescription: '', + selectedValue: rowAction.actionValue.selectedValue, + choices: [], + blockCustomValue: false, + askValueAtRuntime: false, + fileInfo: { + id: null, + allowedExtensions: '', + maxUploadSize: '', + mountDir: '', + unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], + }, + }, + } + } + return row + }) + break + + default: + break + } + + const { updatedFormData, updatedFormDataErrorObj } = convertVariableDataTableToFormData({ + rows: updatedRows, + activeStageName, + formData, + formDataErrorObj, + selectedTaskIndex, + type, + validateTask, + calculateLastStepDetail, + }) + setFormDataErrorObj(updatedFormDataErrorObj) + setFormData(updatedFormData) - setRows(updatedRows) + return updatedRows + }) } const dataTableHandleAddition = () => { - const data = getEmptyVariableDataTableRow(emptyRowParams) - const editedRows = [data, ...rows] - setRows(editedRows) + handleRowUpdateAction({ + actionType: VariableDataTableActionType.ADD_ROW, + actionValue: rows.length, + }) } - const dataTableHandleChange = ( - updatedRow: VariableDataRowType, - headerKey: VariableDataKeys, - value: string, + const dataTableHandleChange: DynamicDataTableProps['onRowEdit'] = ( + updatedRow, + headerKey, + value, extraData, ) => { - if (headerKey !== 'val') { + if (headerKey === 'val' && updatedRow.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ROW, - actionValue: value, - headerKey, + actionType: VariableDataTableActionType.UPDATE_VAL_COLUMN, + actionValue: { value, selectedValue: extraData.selectedValue, files: extraData.files }, rowId: updatedRow.id, }) - } else if (headerKey === 'val') { + } else if ( + headerKey === 'val' && + updatedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD && + extraData.files.length + ) { + const isFileSizeValid = validateMaxFileSize( + extraData.files[0], + parseFloat(updatedRow.customState.fileInfo.maxUploadSize), + updatedRow.customState.fileInfo.unit.label as string, + ) + + if (isFileSizeValid) { + // TODO: check this merge with UPDATE_FILE_UPLOAD_INFO after loading state + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: value, + headerKey, + rowId: updatedRow.id, + }) + + uploadFile(extraData.files) + .then((res) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: res.id }, + rowId: updatedRow.id, + }) + }) + .catch(() => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: '', + headerKey, + rowId: updatedRow.id, + }) + }) + } else { + // TODO: get message from Utkarsh + ToastManager.showToast({ + title: 'Large File Size', + description: 'Large File Size', + variant: ToastVariantType.error, + }) + } + } else if (headerKey === 'format' && updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_VAL_COLUMN, + actionType: VariableDataTableActionType.UPDATE_FORMAT_COLUMN, actionValue: { value, selectedValue: extraData.selectedValue }, + rowId: updatedRow.id, + }) + } else { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: value, headerKey, rowId: updatedRow.id, }) } } - const dataTableHandleDelete = (row: VariableDataRowType) => { - const remainingRows = rows.filter(({ id }) => id !== row.id) - - if (remainingRows.length === 0) { - const emptyRowData = getEmptyVariableDataTableRow(emptyRowParams) - setRows([emptyRowData]) - return - } - - setRows(remainingRows) + const dataTableHandleDelete: DynamicDataTableProps['onRowDelete'] = ( + row, + ) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.DELETE_ROW, + rowId: row.id, + }) } - const handleChoicesAddToValColumn = (rowId: string | number) => () => { + const onActionButtonPopupClose = (rowId: string | number) => () => { handleRowUpdateAction({ actionType: VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS, rowId, - headerKey: null, - actionValue: null, }) } - const validationSchema: DynamicDataTableProps['validationSchema'] = (_, key, { id }) => { + const validationSchema: DynamicDataTableProps['validationSchema'] = ( + _, + key, + { id }, + ) => { if (key === 'val') { const index = rows.findIndex((row) => row.id === id) if (index > -1 && ioVariablesError[index]) { @@ -395,11 +572,12 @@ export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { } const actionButtonRenderer = (row: VariableDataRowType) => ( - + + + ) const getActionButtonConfig = (): DynamicDataTableProps['actionButtonConfig'] => { @@ -413,16 +591,30 @@ export const VariableDataTable = ({ type }: { type: PluginVariableType }) => { return null } + const getTrailingCellIcon = (): DynamicDataTableProps['trailingCellIcon'] => ({ + variable: + isCustomTask && type === PluginVariableType.INPUT + ? (row: VariableDataRowType) => ( + + + + ) + : null, + }) + return ( - + key={initialRowsSet.current} headers={getVariableDataTableHeaders(type)} rows={rows} - isAdditionNotAllowed - isDeletionNotAllowed + readOnly={!isCustomTask && type === PluginVariableType.OUTPUT} + isAdditionNotAllowed={!isCustomTask} + isDeletionNotAllowed={!isCustomTask} + trailingCellIcon={getTrailingCellIcon()} onRowEdit={dataTableHandleChange} onRowDelete={dataTableHandleDelete} onRowAdd={dataTableHandleAddition} - showError + // showError validationSchema={validationSchema} actionButtonConfig={getActionButtonConfig()} /> diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx new file mode 100644 index 0000000000..9169b6c3d9 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx @@ -0,0 +1,72 @@ +import { useState } from 'react' + +import { + Button, + ButtonStyleType, + ButtonVariantType, + ComponentSizeType, + PopupMenu, +} from '@devtron-labs/devtron-fe-common-lib' + +import { ReactComponent as ICClose } from '@Icons/ic-close.svg' +import { ReactComponent as ICSlidersVertical } from '@Icons/ic-sliders-vertical.svg' + +import { VariableDataTablePopupMenuProps } from './types' + +export const VariableDataTablePopupMenu = ({ + showIcon, + heading, + children, + onClose, +}: VariableDataTablePopupMenuProps) => { + // STATES + const [visible, setVisible] = useState(false) + + // METHODS + const handleClose = () => { + setVisible(false) + onClose?.() + } + + const handleAction = (open: boolean) => { + if (visible !== open) { + if (open) { + setVisible(true) + } else { + handleClose() + } + } + } + + return ( + + + + + + {visible && ( +
+
+
+ {showIcon && } +

{heading}

+
+
+ {children} +
+ )} +
+ {visible &&
} + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx deleted file mode 100644 index d0f7505509..0000000000 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTableRowAction.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useState } from 'react' - -import { TippyCustomized, TippyTheme } from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as ICSlidersVertical } from '@Icons/ic-sliders-vertical.svg' - -import { VariableDataTableActionType, VariableDataTableRowActionProps } from './types' -import { getValidatedChoices } from './utils' - -import { AddChoicesOverlay } from './AddChoicesOverlay' - -export const VariableDataTableRowAction = ({ - handleRowUpdateAction, - onClose, - row, -}: VariableDataTableRowActionProps) => { - const { data, customState, id } = row - const [visible, setVisible] = useState(false) - - const handleClose = () => { - const { choices, isValid } = getValidatedChoices(customState.choices) - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId: row.id, - headerKey: null, - actionValue: () => choices, - }) - if (isValid) { - setVisible(false) - onClose() - } - } - - const handleAction = () => { - if (visible) { - handleClose() - } else { - setVisible(true) - } - } - - return ( - - } - appendTo={document.getElementById('visible-modal')} - > - - - ) -} diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts index 163387725c..1589e5e9c7 100644 --- a/src/components/CIPipelineN/VariableDataTable/constants.ts +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -1,4 +1,8 @@ -import { DynamicDataTableHeaderType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' +import { + DynamicDataTableHeaderType, + SelectPickerOptionType, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' import { PluginVariableType } from '@Components/ciPipeline/types' @@ -24,25 +28,63 @@ export const getVariableDataTableHeaders = ( }, ] +export const VAL_COLUMN_CHOICES_DROPDOWN_LABEL = 'Default values' + +export const FORMAT_OPTIONS_MAP = { + [VariableTypeFormat.STRING]: 'String', + [VariableTypeFormat.NUMBER]: 'Number', + [VariableTypeFormat.BOOL]: 'Boolean', + [VariableTypeFormat.DATE]: 'Date', + [VariableTypeFormat.FILE]: 'File', +} + export const FORMAT_COLUMN_OPTIONS: SelectPickerOptionType[] = [ { - label: 'STRING', - value: 'STRING', + label: 'String', + value: VariableTypeFormat.STRING, }, { - label: 'NUMBER', - value: 'NUMBER', + label: 'Number', + value: VariableTypeFormat.NUMBER, }, { - label: 'BOOLEAN', - value: 'BOOLEAN', + label: 'Boolean', + value: VariableTypeFormat.BOOL, }, { - label: 'FILE', - value: 'FILE', + label: 'Date', + value: VariableTypeFormat.DATE, }, { - label: 'DATE', - value: 'DATE', + label: 'File', + value: VariableTypeFormat.FILE, }, ] + +export const VAL_COLUMN_BOOL_OPTIONS: SelectPickerOptionType[] = [ + { label: 'TRUE', value: 'TRUE' }, + { label: 'FALSE', value: 'FALSE' }, +] + +export const VAL_COLUMN_DATE_OPTIONS: SelectPickerOptionType[] = [ + { label: 'YYYY-MM-DD', value: 'YYYY-MM-DD', description: 'RFC 3339' }, + { label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm', description: 'RFC 3339 with mins' }, + { label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss', description: 'RFC 3339 with secs' }, + { label: 'YYYY-MM-DD HH:mm:ssZ', value: 'YYYY-MM-DD HH:mm:ssZ', description: 'RFC 3339 with secs and TZ' }, + { label: 'YYYY-MM-DDT15Z0700', value: 'ISO', description: 'ISO8601 with hours' }, + { label: 'YYYY-MM-DDTHH:mm:ss[Z]', value: 'YYYY-MM-DDTHH:mm:ss[Z]', description: 'ISO8601 with secs' }, + { label: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', value: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', description: 'ISO8601 with nanosecs' }, +] + +export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ + { + label: 'KB', + value: 1024, + }, + { + label: 'MB', + value: 1 / 1024, + }, +] + +export const DECIMAL_REGEX = /^\d*\.?\d*$/ diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index a339c7741f..a84fb9e9ad 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -1,57 +1,158 @@ -import { DynamicDataTableRowType, SelectPickerOptionType } from '@devtron-labs/devtron-fe-common-lib' +import { PluginVariableType } from '@Components/ciPipeline/types' +import { PipelineContext } from '@Components/workflowEditor/types' +import { DynamicDataTableRowType, SelectPickerOptionType, VariableType } from '@devtron-labs/devtron-fe-common-lib' export type VariableDataKeys = 'variable' | 'format' | 'val' export type VariableDataCustomState = { - choices: { id: number; value: string; error: string }[] + variableDescription: string + isVariableRequired: boolean + choices: { id: number; value: string }[] askValueAtRuntime: boolean blockCustomValue: boolean + // Check for support in the TableRowTypes selectedValue: Record + fileInfo: { + id: number + mountDir: string + allowedExtensions: string + maxUploadSize: string + unit: SelectPickerOptionType + } } export type VariableDataRowType = DynamicDataTableRowType export enum VariableDataTableActionType { + // GENERAL ACTIONS + ADD_ROW = 'add-row', UPDATE_ROW = 'update_row', + DELETE_ROW = 'delete_row', UPDATE_VAL_COLUMN = 'update_val_column', + UPDATE_FORMAT_COLUMN = 'update_format_column', + UPDATE_FILE_UPLOAD_INFO = 'update_file_upload_info', + + // TABLE ROW ACTIONS ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS = 'add_choices_to_value_column_options', UPDATE_CHOICES = 'update_choices', UPDATE_ASK_VALUE_AT_RUNTIME = 'update_ask_value_at_runtime', UPDATE_ALLOW_CUSTOM_INPUT = 'update_allow_custom_input', + UPDATE_FILE_MOUNT = 'update_file_mount', + UPDATE_FILE_ALLOWED_EXTENSIONS = 'update_file_allowed_extensions', + UPDATE_FILE_MAX_SIZE = 'update_file_max_size', + + // VARIABLE COLUMN ACTIONS + UPDATE_VARIABLE_DESCRIPTION = 'update_variable_description', + UPDATE_VARIABLE_REQUIRED = 'update_variable_required', } type VariableDataTableActionPropsMap = { - [VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT]: VariableDataCustomState['blockCustomValue'] - [VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME]: VariableDataCustomState['askValueAtRuntime'] - [VariableDataTableActionType.UPDATE_CHOICES]: ( - currentChoices: VariableDataCustomState['choices'], - ) => VariableDataCustomState['choices'] - [VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS]: null + [VariableDataTableActionType.ADD_ROW]: { actionValue: number } + [VariableDataTableActionType.UPDATE_ROW]: { + actionValue: string + headerKey: VariableDataKeys + rowId: string | number + } + [VariableDataTableActionType.DELETE_ROW]: { + rowId: string | number + } [VariableDataTableActionType.UPDATE_VAL_COLUMN]: { - value: string - selectedValue: SelectPickerOptionType + actionValue: { + value: string + selectedValue: SelectPickerOptionType + files: File[] + } + rowId: string | number + } + [VariableDataTableActionType.UPDATE_FORMAT_COLUMN]: { + actionValue: { + value: string + selectedValue: SelectPickerOptionType + } + rowId: string | number + } + [VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO]: { + actionValue: Pick + rowId: string | number + } + + [VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT]: { + actionValue: VariableDataCustomState['blockCustomValue'] + rowId: string | number + } + [VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME]: { + actionValue: VariableDataCustomState['askValueAtRuntime'] + rowId: string | number } - [VariableDataTableActionType.UPDATE_ROW]: string + [VariableDataTableActionType.UPDATE_CHOICES]: { + actionValue: (currentChoices: VariableDataCustomState['choices']) => VariableDataCustomState['choices'] + rowId: string | number + } + [VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS]: { + rowId: string | number + } + [VariableDataTableActionType.UPDATE_FILE_MOUNT]: { + rowId: string | number + actionValue: string + } + [VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS]: { + rowId: string | number + actionValue: string + } + [VariableDataTableActionType.UPDATE_FILE_MAX_SIZE]: { + rowId: string | number + actionValue: { + size: string + unit: SelectPickerOptionType + } + } + + [VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION]: { actionValue: string; rowId: string | number } + [VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED]: { actionValue: boolean; rowId: string | number } } export type VariableDataTableAction< T extends keyof VariableDataTableActionPropsMap = keyof VariableDataTableActionPropsMap, -> = T extends keyof VariableDataTableActionPropsMap - ? { actionType: T; actionValue: VariableDataTableActionPropsMap[T] } - : never +> = T extends keyof VariableDataTableActionPropsMap ? { actionType: T } & VariableDataTableActionPropsMap[T] : never + +export type HandleRowUpdateActionProps = VariableDataTableAction -export type HandleRowUpdateActionProps = VariableDataTableAction & { - rowId: string | number - headerKey: VariableDataKeys +export interface VariableDataTablePopupMenuProps { + heading: string + showIcon?: boolean + onClose?: () => void + children: JSX.Element } -export interface VariableDataTableRowActionProps { +export interface ConfigOverlayProps { row: VariableDataRowType - onClose: () => void handleRowUpdateAction: (props: HandleRowUpdateActionProps) => void } -export type AddChoicesOverlayProps = Pick & - Pick & { - rowId: VariableDataTableRowActionProps['row']['id'] - } +export type GetValColumnRowPropsType = Pick< + PipelineContext, + | 'activeStageName' + | 'formData' + | 'globalVariables' + | 'isCdPipeline' + | 'selectedTaskIndex' + | 'inputVariablesListFromPrevStep' +> & + Pick< + VariableType, + | 'format' + | 'value' + | 'refVariableName' + | 'refVariableStage' + | 'valueConstraint' + | 'description' + | 'variableType' + | 'id' + > & { type: PluginVariableType } + +export interface GetVariableDataTableInitialRowsProps { + ioVariables: VariableType[] + type: PluginVariableType + isCustomTask: boolean + emptyRowParams: GetValColumnRowPropsType +} diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 2eaf20e1ac..c2427e047c 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -1,65 +1,78 @@ +import dayjs from 'dayjs' + import { + ConditionType, DynamicDataTableRowDataType, PluginType, RefVariableStageType, + RefVariableType, + SelectPickerOptionType, VariableType, + VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' -import { BuildStageVariable, PATTERNS } from '@Config/constants' - +import { BuildStageVariable } from '@Config/constants' import { PipelineContext } from '@Components/workflowEditor/types' import { PluginVariableType } from '@Components/ciPipeline/types' -import { excludeVariables } from '../Constants' -import { FORMAT_COLUMN_OPTIONS } from './constants' -import { VariableDataRowType } from './types' - -export const getValueColumnOptions = ( - { - inputVariablesListFromPrevStep, - activeStageName, - selectedTaskIndex, - formData, - globalVariables, - isCdPipeline, - type, - }: Pick< - PipelineContext, - | 'activeStageName' - | 'selectedTaskIndex' - | 'inputVariablesListFromPrevStep' - | 'formData' - | 'globalVariables' - | 'isCdPipeline' - > & { type: PluginVariableType }, - index: number, -) => { - const currentStepTypeVariable = - formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE - ? 'inlineStepDetail' - : 'pluginRefStepDetail' - const ioVariables: VariableType[] = - formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ - type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' - ] +import { ExtendedOptionType } from '@Components/app/types' +import { excludeVariables } from '../Constants' +import { + DECIMAL_REGEX, + FILE_UPLOAD_SIZE_UNIT_OPTIONS, + FORMAT_COLUMN_OPTIONS, + VAL_COLUMN_BOOL_OPTIONS, + VAL_COLUMN_CHOICES_DROPDOWN_LABEL, + VAL_COLUMN_DATE_OPTIONS, +} from './constants' +import { GetValColumnRowPropsType, GetVariableDataTableInitialRowsProps, VariableDataRowType } from './types' +import { getSystemVariableIcon } from './helpers' +export const getOptionsForValColumn = ({ + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + format, + valueConstraint, +}: Pick< + PipelineContext, + | 'activeStageName' + | 'selectedTaskIndex' + | 'inputVariablesListFromPrevStep' + | 'formData' + | 'globalVariables' + | 'isCdPipeline' +> & + Pick) => { const previousStepVariables = [] - const defaultVariables = (ioVariables[index]?.valueConstraint?.choices || []).map((value) => ({ + const defaultValues = (valueConstraint?.choices || []).map>((value) => ({ label: value, value, })) - if (inputVariablesListFromPrevStep[activeStageName].length > 0) { - inputVariablesListFromPrevStep[activeStageName][selectedTaskIndex].forEach((element) => { - previousStepVariables.push({ - ...element, - label: element.name, - value: element.name, - refVariableTaskName: formData[activeStageName]?.steps[element.refVariableStepIndex - 1].name, - }) - }) + if (format === VariableTypeFormat.BOOL) { + defaultValues.push(...VAL_COLUMN_BOOL_OPTIONS) + } + + if (format === VariableTypeFormat.DATE) { + defaultValues.push(...VAL_COLUMN_DATE_OPTIONS) } + if (format) + if (inputVariablesListFromPrevStep[activeStageName].length > 0) { + inputVariablesListFromPrevStep[activeStageName][selectedTaskIndex].forEach((element) => { + previousStepVariables.push({ + ...element, + label: element.name, + value: element.name, + refVariableTaskName: formData[activeStageName]?.steps[element.refVariableStepIndex - 1].name, + }) + }) + } + if (activeStageName === BuildStageVariable.PostBuild) { const preBuildStageVariables = [] const preBuildTaskLength = formData[BuildStageVariable.PreBuild]?.steps?.length @@ -102,8 +115,8 @@ export const getValueColumnOptions = ( return [ { - label: 'Default variables', - options: defaultVariables, + label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, + options: defaultValues, }, { label: 'From Pre-build Stage', @@ -122,8 +135,8 @@ export const getValueColumnOptions = ( return [ { - label: 'Default variables', - options: defaultVariables, + label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, + options: defaultValues, }, { label: 'From Previous Steps', @@ -139,77 +152,369 @@ export const getValueColumnOptions = ( ] } -export const getVariableColumnRowProps = (): VariableDataRowType['data']['variable'] => { +export const getVariableColumnRowProps = () => { const data: VariableDataRowType['data']['variable'] = { value: '', type: DynamicDataTableRowDataType.TEXT, - props: {}, + props: { + placeholder: 'Enter variable name', + }, } return data } -export const getFormatColumnRowProps = () => { - const data: VariableDataRowType['data']['format'] = { - value: FORMAT_COLUMN_OPTIONS[0].value, - type: DynamicDataTableRowDataType.DROPDOWN, - props: { - options: FORMAT_COLUMN_OPTIONS, - }, +export const getFormatColumnRowProps = ({ + format, + isCustomTask, +}: Pick & { isCustomTask: boolean }): VariableDataRowType['data']['format'] => { + if (isCustomTask) { + return { + value: format, + type: DynamicDataTableRowDataType.DROPDOWN, + props: { + options: FORMAT_COLUMN_OPTIONS, + }, + } } - return data + return { + type: DynamicDataTableRowDataType.TEXT, + value: format, + disabled: true, + props: {}, + } } -export const getValColumnRowProps = (params: Parameters[0], index: number) => { - const data: VariableDataRowType['data']['val'] = { - value: '', - type: DynamicDataTableRowDataType.SELECT_TEXT, - props: { - placeholder: 'Select source or input value', - options: getValueColumnOptions(params, index), - }, +export const getValColumnRowProps = ({ + format, + type, + variableType, + value, + refVariableName, + refVariableStage, + valueConstraint, + description, + activeStageName, + formData, + globalVariables, + isCdPipeline, + selectedTaskIndex, + inputVariablesListFromPrevStep, +}: GetValColumnRowPropsType): VariableDataRowType['data']['format'] => { + if (type === PluginVariableType.INPUT) { + if (format === VariableTypeFormat.FILE) { + return { + type: DynamicDataTableRowDataType.FILE_UPLOAD, + value, + props: { + fileTypes: valueConstraint?.constraint?.fileProperty?.allowedExtensions || [], + }, + } + } + + return { + type: DynamicDataTableRowDataType.SELECT_TEXT, + value: variableType === RefVariableType.NEW ? value : refVariableName || '', + props: { + placeholder: 'Enter value or variable', + options: getOptionsForValColumn({ + activeStageName, + formData, + globalVariables, + isCdPipeline, + selectedTaskIndex, + inputVariablesListFromPrevStep, + format, + valueConstraint, + }), + selectPickerProps: { + isCreatable: + format !== VariableTypeFormat.BOOL && + (!valueConstraint?.choices?.length || !valueConstraint.blockCustomValue), + }, + Icon: + refVariableStage || (variableType && variableType !== RefVariableType.NEW) + ? getSystemVariableIcon() + : null, + }, + } } - return data + return { + type: DynamicDataTableRowDataType.TEXT, + value: description, + props: {}, + } +} + +export const testValueForNumber = (value: string) => !value || DECIMAL_REGEX.test(value) + +export const getValColumnRowValue = ( + currentValue: string, + format: VariableTypeFormat, + value: string, + selectedValue: SelectPickerOptionType & ExtendedOptionType, + isSystemVariable: boolean, +) => { + const isNumberFormat = !isSystemVariable && format === VariableTypeFormat.NUMBER + if (isNumberFormat && !testValueForNumber(value)) { + return currentValue + } + + const isDateFormat = !isSystemVariable && value && format === VariableTypeFormat.DATE + if (isDateFormat && selectedValue.description) { + const now = dayjs() + return selectedValue.value !== 'ISO' ? now.format(selectedValue.value) : now.toISOString() + } + + return value } -export const getEmptyVariableDataTableRow = ( - params: Pick< - PipelineContext, - | 'activeStageName' - | 'selectedTaskIndex' - | 'inputVariablesListFromPrevStep' - | 'formData' - | 'globalVariables' - | 'isCdPipeline' - > & { type: PluginVariableType }, -): VariableDataRowType => { - const id = (Date.now() * Math.random()).toString(16) +export const getEmptyVariableDataTableRow = (params: GetValColumnRowPropsType): VariableDataRowType => { const data: VariableDataRowType = { data: { variable: getVariableColumnRowProps(), - format: getFormatColumnRowProps(), - val: getValColumnRowProps(params, -1), + format: getFormatColumnRowProps({ format: VariableTypeFormat.STRING, isCustomTask: true }), + val: getValColumnRowProps(params), + }, + id: params.id, + customState: { + variableDescription: '', + isVariableRequired: false, + choices: [], + askValueAtRuntime: false, + blockCustomValue: false, + selectedValue: null, + fileInfo: { + id: null, + mountDir: '/devtroncd', + allowedExtensions: '', + maxUploadSize: '', + unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], + }, }, - id, } return data } -export const validateChoice = (choice: string) => PATTERNS.STRING.test(choice) +export const getVariableDataTableInitialRows = ({ + ioVariables, + type, + isCustomTask, + emptyRowParams, +}: GetVariableDataTableInitialRowsProps): VariableDataRowType[] => + ioVariables.map( + ({ + name, + allowEmptyValue, + description, + format, + variableType, + value, + refVariableName, + refVariableStage, + valueConstraint, + isRuntimeArg, + fileMountDir, + fileReferenceId, + id, + }) => { + const isInputVariableRequired = type === PluginVariableType.INPUT && !allowEmptyValue + + return { + data: { + variable: { + ...getVariableColumnRowProps(), + value: name, + required: isInputVariableRequired, + disabled: !isCustomTask, + }, + format: getFormatColumnRowProps({ format, isCustomTask }), + val: getValColumnRowProps({ + ...emptyRowParams, + description, + format, + variableType, + value, + refVariableName, + refVariableStage, + valueConstraint, + id, + }), + }, + customState: { + isVariableRequired: isInputVariableRequired, + variableDescription: description ?? '', + choices: (valueConstraint?.choices || []).map((choiceValue, index) => ({ + id: index, + value: choiceValue, + })), + askValueAtRuntime: isRuntimeArg ?? false, + blockCustomValue: valueConstraint?.blockCustomValue ?? false, + selectedValue: null, + fileInfo: { + id: fileReferenceId, + mountDir: fileMountDir, + allowedExtensions: + valueConstraint?.constraint?.fileProperty?.allowedExtensions.join(', ') || '', + maxUploadSize: ( + (valueConstraint?.constraint?.fileProperty?.maxUploadSize || null) / 1024 || '' + ).toString(), + unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], + }, + }, + id, + } + }, + ) + +export const convertVariableDataTableToFormData = ({ + rows, + type, + activeStageName, + selectedTaskIndex, + formData, + formDataErrorObj, + validateTask, + calculateLastStepDetail, +}: Pick< + PipelineContext, + | 'activeStageName' + | 'selectedTaskIndex' + | 'formData' + | 'formDataErrorObj' + | 'validateTask' + | 'calculateLastStepDetail' +> & { + type: PluginVariableType + rows: VariableDataRowType[] +}) => { + const updatedFormData = structuredClone(formData) + const updatedFormDataErrorObj = structuredClone(formDataErrorObj) + + const currentStepTypeVariable = + updatedFormData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE + ? 'inlineStepDetail' + : 'pluginRefStepDetail' + + const ioVariables: VariableType[] = + updatedFormData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + ] -export const getValidatedChoices = (choices: VariableDataRowType['customState']['choices']) => { - let isValid = true + const updatedIOVariables = rows.map(({ data, customState, id }) => { + const selectedIOVariable = ioVariables?.find((ioVariable) => ioVariable.id === id) + const { + askValueAtRuntime, + blockCustomValue, + choices, + selectedValue, + isVariableRequired, + variableDescription, + fileInfo, + } = customState - const updatedChoices: VariableDataRowType['customState']['choices'] = choices.map((choice) => { - const error = !validateChoice(choice.value) ? 'This is a required field' : '' - if (isValid && !!error) { - isValid = false + const variableDetail: VariableType = { + ...selectedIOVariable, + format: data.format.value as VariableTypeFormat, + name: data.variable.value, + description: variableDescription, + allowEmptyValue: !isVariableRequired, + isRuntimeArg: askValueAtRuntime, + valueConstraint: { + choices: choices.map(({ value }) => value), + blockCustomValue, + constraint: null, + }, + } + + if (fileInfo) { + const unitMultiplier = fileInfo.unit.label === 'MB' ? 1024 : 1 + variableDetail.value = data.val.value + variableDetail.fileReferenceId = fileInfo.id + variableDetail.fileMountDir = fileInfo.mountDir + variableDetail.valueConstraint.constraint = { + fileProperty: { + allowedExtensions: fileInfo.allowedExtensions + .split(',') + .map((value) => value.trim()) + .filter((value) => !!value), + maxUploadSize: parseFloat(fileInfo.maxUploadSize) * unitMultiplier * 1024, + }, + } } - return { ...choice, error } + + if (selectedValue) { + if (selectedValue.refVariableStepIndex) { + variableDetail.value = '' + variableDetail.variableType = RefVariableType.FROM_PREVIOUS_STEP + variableDetail.refVariableStepIndex = selectedValue.refVariableStepIndex + variableDetail.refVariableName = selectedValue.label + variableDetail.format = selectedValue.format + variableDetail.refVariableStage = selectedValue.refVariableStage + } else if (selectedValue.variableType === RefVariableType.GLOBAL) { + variableDetail.variableType = RefVariableType.GLOBAL + variableDetail.refVariableStepIndex = 0 + variableDetail.refVariableName = selectedValue.label + variableDetail.format = selectedValue.format + variableDetail.value = '' + variableDetail.refVariableStage = null + } else { + variableDetail.variableType = RefVariableType.NEW + variableDetail.value = selectedValue.label + variableDetail.refVariableName = '' + variableDetail.refVariableStage = null + } + if (formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.PLUGIN_REF) { + variableDetail.format = selectedIOVariable.format + } + } + + return variableDetail }) - return { isValid, choices: updatedChoices } + updatedFormData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + ] = updatedIOVariables + + if (type === PluginVariableType.OUTPUT) { + calculateLastStepDetail(false, updatedFormData, activeStageName, selectedTaskIndex) + } + + if (updatedIOVariables.length === 1 && !updatedIOVariables[0].name && !updatedIOVariables[0].value) { + updatedIOVariables.pop() + } + + if (updatedIOVariables.length === 0) { + const { conditionDetails } = updatedFormData[activeStageName].steps[selectedTaskIndex].inlineStepDetail + for (let i = 0; i < conditionDetails?.length; i++) { + if ( + (type === PluginVariableType.OUTPUT && + (conditionDetails[i].conditionType === ConditionType.PASS || + conditionDetails[i].conditionType === ConditionType.FAIL)) || + (type === PluginVariableType.INPUT && + (conditionDetails[i].conditionType === ConditionType.TRIGGER || + conditionDetails[i].conditionType === ConditionType.SKIP)) + ) { + conditionDetails.splice(i, 1) + i -= 1 + } + } + updatedFormData[activeStageName].steps[selectedTaskIndex].inlineStepDetail.conditionDetails = conditionDetails + } + + validateTask( + updatedFormData[activeStageName].steps[selectedTaskIndex], + updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex], + ) + + return { updatedFormDataErrorObj, updatedFormData } +} + +// VALIDATIONS +export const validateMaxFileSize = (file: File, maxFileSizeInMB: number, unit: string = 'KB') => { + const unitMultiplier = unit === 'MB' ? 1024 : 1 + return typeof maxFileSizeInMB !== 'number' || file.size <= maxFileSizeInMB * unitMultiplier * 1024 } diff --git a/src/components/cdPipeline/CDPipeline.tsx b/src/components/cdPipeline/CDPipeline.tsx index 806919c594..0e14761217 100644 --- a/src/components/cdPipeline/CDPipeline.tsx +++ b/src/components/cdPipeline/CDPipeline.tsx @@ -49,6 +49,7 @@ import { ProcessPluginDataReturnType, ResourceKindType, getEnvironmentListMinPublic, + noop, } from '@devtron-labs/devtron-fe-common-lib' import { useEffect, useMemo, useRef, useState } from 'react' import { Redirect, Route, Switch, useParams, useRouteMatch } from 'react-router-dom' @@ -1237,6 +1238,8 @@ export default function CDPipeline({ handleDisableParentModalCloseUpdate, handleValidateMandatoryPlugins, mandatoryPluginData, + // TODO: Handle for CD File Upload (Rohit) + uploadFile: noop } }, [ formData, diff --git a/src/components/cdPipeline/cdpipeline.util.tsx b/src/components/cdPipeline/cdpipeline.util.tsx index e9e7aace0d..2a7efa6e1f 100644 --- a/src/components/cdPipeline/cdpipeline.util.tsx +++ b/src/components/cdPipeline/cdpipeline.util.tsx @@ -378,4 +378,4 @@ export const getNamespacePlaceholder = (isVirtualEnvironment: boolean, namespace return 'Not available' } return 'Will be auto-populated based on environment' -} \ No newline at end of file +} diff --git a/src/components/ciPipeline/ciPipeline.service.ts b/src/components/ciPipeline/ciPipeline.service.ts index b8673fc27a..26750be32b 100644 --- a/src/components/ciPipeline/ciPipeline.service.ts +++ b/src/components/ciPipeline/ciPipeline.service.ts @@ -24,13 +24,16 @@ import { RefVariableType, PipelineBuildStageType, VariableTypeFormat, + getIsRequestAborted, + showError, } from '@devtron-labs/devtron-fe-common-lib' import { Routes, SourceTypeMap, TriggerType, ViewType } from '../../config' import { getSourceConfig, getWebhookDataMetaConfig } from '../../services/service' import { CiPipelineSourceTypeBaseOptions } from '../CIPipelineN/ciPipeline.utils' -import { PatchAction } from './types' +import { PatchAction, UploadCIPipelineFileDTO } from './types' import { safeTrim } from '../../util/Util' import { ChangeCIPayloadType } from '../workflowEditor/types' +import { MutableRefObject } from 'react' const emptyStepsData = () => { return { id: 0, steps: [] } @@ -414,7 +417,14 @@ function migrateOldData( variableType: RefVariableType.GLOBAL, refVariableStepIndex: 0, allowEmptyValue: false, + fileMountDir: null, + fileReferenceId: null, + valueConstraintId: null, + valueConstraint: null, + isRuntimeArg: null, + refVariableUsed: null, } + const updatedData = { id: 0, steps: oldDataArr.map((data) => { @@ -597,3 +607,35 @@ export async function getGlobalVariable(appId: number, isCD?: boolean): Promise< return { result: variableList } } + +export const uploadCIPipelineFile = async ({ + file, + appId, + ciPipelineId, + abortControllerRef, +}: { + file: File[] + appId: number + ciPipelineId: number + abortControllerRef?: MutableRefObject +}): Promise => { + const formData = new FormData() + formData.append('file', file[0]) + + try { + const { result } = await post( + `${Routes.CI_CONFIG_GET}/${appId}/${ciPipelineId}/${Routes.FILE_UPLOAD}`, + formData, + { abortControllerRef }, + true, + ) + + return result + } catch (err) { + if (getIsRequestAborted(err)) { + return + } + showError(err) + throw err + } +} diff --git a/src/components/ciPipeline/types.ts b/src/components/ciPipeline/types.ts index 588495567a..b08001c232 100644 --- a/src/components/ciPipeline/types.ts +++ b/src/components/ciPipeline/types.ts @@ -428,3 +428,11 @@ export interface WebhookConditionType { deleteWebhookCondition: (index: number) => void canEditSelectorCondition: boolean } + +export interface UploadCIPipelineFileDTO { + id: number + name: string + size: number + mimeType: string + extension: string +} diff --git a/src/components/workflowEditor/types.ts b/src/components/workflowEditor/types.ts index 7070aef45d..db5a64bdfd 100644 --- a/src/components/workflowEditor/types.ts +++ b/src/components/workflowEditor/types.ts @@ -29,6 +29,7 @@ import { MandatoryPluginDataType, CiPipeline, } from '@devtron-labs/devtron-fe-common-lib' +import { UploadCIPipelineFileDTO } from '@Components/ciPipeline/types' import { RouteComponentProps } from 'react-router-dom' import { HostURLConfig } from '../../services/service.types' import { CIPipelineNodeType, CdPipelineResult } from '../app/details/triggerView/types' @@ -329,6 +330,7 @@ export interface PipelineContext { handleDisableParentModalCloseUpdate?: (disableParentModalClose: boolean) => void handleValidateMandatoryPlugins: (params: HandleValidateMandatoryPluginsParamsType) => void mandatoryPluginData: MandatoryPluginDataType + uploadFile: (file: File[]) => Promise } export interface SourceTypeCardProps { diff --git a/src/config/constants.ts b/src/config/constants.ts index 1844e17147..99a91660e6 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -128,6 +128,7 @@ export const Routes = { SSO_LOGIN_SERVICES: 'login-service', API_TOKEN: 'api-token', API_TOKEN_WEBHOOK: 'api-token/webhook', + FILE_UPLOAD: 'file/upload', DEPLOYMENT_METRICS: 'deployment-metrics', APP_CONFIG_MAP_GET: 'configmap/applevel/get', diff --git a/src/css/base.scss b/src/css/base.scss index dc60bbe7df..48964df975 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -3392,6 +3392,10 @@ textarea, } //min width +.min-w-0 { + min-width: 0; +} + .min-w-200 { min-width: 200px; } @@ -5267,4 +5271,4 @@ textarea::placeholder { background: rgba(0, 0, 0, 0.75); z-index: var(--modal-index); backdrop-filter: blur(1px); -} \ No newline at end of file +} From 4119dcdc15dbfa7e44ca7b8aea2027ab17a4133d Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Thu, 5 Dec 2024 18:02:01 +0530 Subject: [PATCH 03/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 249362b5a8..c60af5f11b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.1.6", + "@devtron-labs/devtron-fe-common-lib": "1.2.2-beta-3", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index f8e583a36d..5cc6f8b565 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.1.6": - version "1.1.6" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.1.6.tgz#13390eaef23265dfaf2525506fc37c263df3416c" - integrity sha512-t5o8rjIBxaUfL97yhXZHJUZDhiwhRQOgWGqaulD2PqzCei9I8rP+0wlwQbbRHh73HFSc3sxmesItYoHGeQhNJw== +"@devtron-labs/devtron-fe-common-lib@1.2.2-beta-3": + version "1.2.2-beta-3" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.2-beta-3.tgz#b5fb175d47689f116ce675d7ba1a5114eb3dcf36" + integrity sha512-ZcQpgD1LNrMSfnizMs2l9AoW9Pz1iC8nzZJKHVnTKSKILII39wWL87Z8bU+IHbj72Z1diAGTEQQAtBCKIOtNBg== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" @@ -988,7 +988,6 @@ marked "^13.0.3" react-dates "^21.8.0" react-diff-viewer-continued "^3.4.0" - react-monaco-editor "^0.54.0" sass "^1.69.7" tslib "2.7.0" @@ -6616,13 +6615,6 @@ react-moment-proptypes@^1.6.0: dependencies: moment ">=1.6.0" -react-monaco-editor@^0.54.0: - version "0.54.0" - resolved "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.54.0.tgz" - integrity sha512-9JwO69851mfpuhYLHlKbae7omQWJ/2ICE2lbL0VHyNyZR8rCOH7440u+zAtDgiOMpLwmYdY1sEZCdRefywX6GQ== - dependencies: - prop-types "^15.8.1" - react-monaco-editor@^0.55.0: version "0.55.0" resolved "https://registry.npmjs.org/react-monaco-editor/-/react-monaco-editor-0.55.0.tgz" From 584cc742b3d7b297c2cb8a187c1c4f0d26ec35f4 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 6 Dec 2024 00:20:02 +0530 Subject: [PATCH 04/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a0d659390e..2f15a947a3 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.4", + "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-1", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index 27a5b0d181..a252d284d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.4": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4.tgz#4af6cc9803efb2eba7fd4988daca92805b8e2853" - integrity sha512-Azi5EofwNjuevapxExkERXREZqsnttM8A652EhSRd9xDOz2mcv/ebXvu/1YKMdIP/0mETUaaEb6pkYSZAsy6OQ== +"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-1": + version "1.2.4-beta-1" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-1.tgz#ee3cf0ba6f9af55f6d463d281fcb57e12a56a00e" + integrity sha512-GyCFZ+EP7J0i29I+f+ENR+ZUDY5+vUYh7+AprrU+Hptz3p/96Xc5riDBZJSk6pvHzvbXyhJCsOSE4HmaLedOZA== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From ef580e139aaecf6ec047e291131fb16883faf36a Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 6 Dec 2024 03:20:59 +0530 Subject: [PATCH 05/36] feat: Build CI - Runtime Params Integration --- .../Details/TriggerView/BulkCITrigger.tsx | 4 ++ .../Details/TriggerView/EnvTriggerView.tsx | 8 ++- src/components/CIPipelineN/CIPipeline.tsx | 5 +- .../VariableDataTable/ValueConfigOverlay.tsx | 9 ++- .../VariableDataTable/VariableDataTable.tsx | 71 +++++++++---------- .../VariableDataTablePopupMenu.tsx | 7 +- .../CIPipelineN/VariableDataTable/types.ts | 11 ++- .../CIPipelineN/VariableDataTable/utils.tsx | 49 ++++++++----- .../app/details/triggerView/TriggerView.tsx | 33 ++++++--- .../app/details/triggerView/ciMaterial.tsx | 3 + .../app/details/triggerView/types.ts | 7 ++ .../ciPipeline/ciPipeline.service.ts | 34 +-------- src/components/ciPipeline/types.ts | 8 --- .../GitInfoMaterialCard/GitInfoMaterial.tsx | 16 +++-- .../helpers/GitInfoMaterialCard/types.ts | 3 + src/components/workflowEditor/types.ts | 5 +- src/config/constants.ts | 1 - 17 files changed, 148 insertions(+), 126 deletions(-) diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx index 1053e8a23b..a6b0ac1055 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx @@ -40,6 +40,7 @@ import { ButtonVariantType, ComponentSizeType, ButtonStyleType, + noop, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { getCIPipelineURL, getParsedBranchValuesForPlugin, importComponentFromFELibrary } from '../../../common' @@ -526,6 +527,9 @@ const BulkCITrigger = ({ isWebhookPayloadLoading={isWebhookPayloadLoading} isBulk appId={selectedApp.appId.toString()} + runtimeParamsV2={[]} + handleRuntimeParamChangeV2={noop} + uploadFile={noop} /> ) } diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index 3769e79f3b..52e39a77e8 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -47,6 +47,7 @@ import { ToastManager, ToastVariantType, BlockedStateData, + noop, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { @@ -1982,9 +1983,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou if (selectedCINode?.id) { return ( - + diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index c702800832..7d290f27b5 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -46,6 +46,7 @@ import { ToastManager, ProcessPluginDataParamsType, ResourceKindType, + uploadCIPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { @@ -62,7 +63,6 @@ import { getInitData, getInitDataWithCIPipeline, saveCIPipeline, - uploadCIPipelineFile, } from '../ciPipeline/ciPipeline.service' import { ValidationRules } from '../ciPipeline/validationRules' import { CIBuildType, CIPipelineBuildType, CIPipelineDataType, CIPipelineType } from '../ciPipeline/types' @@ -784,7 +784,8 @@ export default function CIPipeline({ } } - const uploadFile = (file: File[]) => uploadCIPipelineFile({ appId: +appId, ciPipelineId: +ciPipelineId, file }) + const uploadFile: PipelineContext['uploadFile'] = ({ allowedExtensions, file, maxUploadSize }) => + uploadCIPipelineFile({ appId: +appId, ciPipelineId: +ciPipelineId, file, allowedExtensions, maxUploadSize }) const contextValue = useMemo( () => ({ diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx index 2d94cd8667..09d1663ea5 100644 --- a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx @@ -84,10 +84,14 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay } const handleFileMountChange = (e: ChangeEvent) => { + const fileMountValue = e.target.value handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FILE_MOUNT, rowId, - actionValue: e.target.value, + actionValue: { + error: !fileMountValue ? 'This field is required' : '', + value: fileMountValue, + }, }) } @@ -139,11 +143,12 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay name="fileMount" label="File mount path" placeholder="Enter file mount path" - value={fileInfo.mountDir} + value={fileInfo.mountDir.value} onChange={handleFileMountChange} dataTestid={`file-mount-${rowId}`} inputWrapClassName="w-100" isRequiredField + error={fileInfo.mountDir.error} />
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx index 8cb15c0e43..a8bf1a8035 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx @@ -7,8 +7,6 @@ import { PluginType, RefVariableType, SelectPickerOptionType, - ToastManager, - ToastVariantType, VariableType, VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' @@ -33,10 +31,10 @@ import { import { convertVariableDataTableToFormData, getEmptyVariableDataTableRow, + getUploadFileConstraints, getValColumnRowProps, getValColumnRowValue, getVariableDataTableInitialRows, - validateMaxFileSize, } from './utils' import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' @@ -429,7 +427,10 @@ export const VariableDataTable = ({ id: null, allowedExtensions: '', maxUploadSize: '', - mountDir: '', + mountDir: { + value: '/devtroncd', + error: '', + }, unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], }, }, @@ -484,45 +485,37 @@ export const VariableDataTable = ({ updatedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD && extraData.files.length ) { - const isFileSizeValid = validateMaxFileSize( - extraData.files[0], - parseFloat(updatedRow.customState.fileInfo.maxUploadSize), - updatedRow.customState.fileInfo.unit.label as string, - ) - - if (isFileSizeValid) { - // TODO: check this merge with UPDATE_FILE_UPLOAD_INFO after loading state - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ROW, - actionValue: value, - headerKey, - rowId: updatedRow.id, - }) + // TODO: check this merge with UPDATE_FILE_UPLOAD_INFO after loading state + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: value, + headerKey, + rowId: updatedRow.id, + }) - uploadFile(extraData.files) - .then((res) => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, - actionValue: { fileReferenceId: res.id }, - rowId: updatedRow.id, - }) + uploadFile({ + file: extraData.files, + ...getUploadFileConstraints({ + unit: updatedRow.customState.fileInfo.unit.label as string, + allowedExtensions: updatedRow.customState.fileInfo.allowedExtensions, + maxUploadSize: updatedRow.customState.fileInfo.maxUploadSize, + }), + }) + .then((res) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: res.id }, + rowId: updatedRow.id, }) - .catch(() => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ROW, - actionValue: '', - headerKey, - rowId: updatedRow.id, - }) + }) + .catch(() => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: '', + headerKey, + rowId: updatedRow.id, }) - } else { - // TODO: get message from Utkarsh - ToastManager.showToast({ - title: 'Large File Size', - description: 'Large File Size', - variant: ToastVariantType.error, }) - } } else if (headerKey === 'format' && updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FORMAT_COLUMN, diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx index 9169b6c3d9..e473b75110 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx @@ -18,14 +18,17 @@ export const VariableDataTablePopupMenu = ({ heading, children, onClose, + disableClose, }: VariableDataTablePopupMenuProps) => { // STATES const [visible, setVisible] = useState(false) // METHODS const handleClose = () => { - setVisible(false) - onClose?.() + if (!disableClose) { + setVisible(false) + onClose?.() + } } const handleAction = (open: boolean) => { diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index a84fb9e9ad..a43b1ed796 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -14,7 +14,10 @@ export type VariableDataCustomState = { selectedValue: Record fileInfo: { id: number - mountDir: string + mountDir: { + value: string + error: string + } allowedExtensions: string maxUploadSize: string unit: SelectPickerOptionType @@ -93,7 +96,10 @@ type VariableDataTableActionPropsMap = { } [VariableDataTableActionType.UPDATE_FILE_MOUNT]: { rowId: string | number - actionValue: string + actionValue: { + value: string + error: string + } } [VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS]: { rowId: string | number @@ -120,6 +126,7 @@ export type HandleRowUpdateActionProps = VariableDataTableAction export interface VariableDataTablePopupMenuProps { heading: string showIcon?: boolean + disableClose?: boolean onClose?: () => void children: JSX.Element } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index c2427e047c..8cc3dc6f95 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -288,7 +288,10 @@ export const getEmptyVariableDataTableRow = (params: GetValColumnRowPropsType): selectedValue: null, fileInfo: { id: null, - mountDir: '/devtroncd', + mountDir: { + value: '/devtroncd', + error: '', + }, allowedExtensions: '', maxUploadSize: '', unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], @@ -330,6 +333,8 @@ export const getVariableDataTableInitialRows = ({ value: name, required: isInputVariableRequired, disabled: !isCustomTask, + showTooltip: !isCustomTask && !!description, + tooltipText: description, }, format: getFormatColumnRowProps({ format, isCustomTask }), val: getValColumnRowProps({ @@ -356,7 +361,7 @@ export const getVariableDataTableInitialRows = ({ selectedValue: null, fileInfo: { id: fileReferenceId, - mountDir: fileMountDir, + mountDir: { value: fileMountDir, error: '' }, allowedExtensions: valueConstraint?.constraint?.fileProperty?.allowedExtensions.join(', ') || '', maxUploadSize: ( @@ -370,6 +375,25 @@ export const getVariableDataTableInitialRows = ({ }, ) +export const getUploadFileConstraints = ({ + unit, + allowedExtensions, + maxUploadSize, +}: { + unit: string + allowedExtensions: string + maxUploadSize: string +}) => { + const unitMultiplier = unit === 'MB' ? 1024 : 1 + return { + allowedExtensions: allowedExtensions + .split(',') + .map((value) => value.trim()) + .filter((value) => !!value), + maxUploadSize: maxUploadSize ? parseFloat(maxUploadSize) * unitMultiplier * 1024 : null, + } +} + export const convertVariableDataTableToFormData = ({ rows, type, @@ -431,18 +455,15 @@ export const convertVariableDataTableToFormData = ({ } if (fileInfo) { - const unitMultiplier = fileInfo.unit.label === 'MB' ? 1024 : 1 variableDetail.value = data.val.value variableDetail.fileReferenceId = fileInfo.id - variableDetail.fileMountDir = fileInfo.mountDir + variableDetail.fileMountDir = fileInfo.mountDir.value variableDetail.valueConstraint.constraint = { - fileProperty: { - allowedExtensions: fileInfo.allowedExtensions - .split(',') - .map((value) => value.trim()) - .filter((value) => !!value), - maxUploadSize: parseFloat(fileInfo.maxUploadSize) * unitMultiplier * 1024, - }, + fileProperty: getUploadFileConstraints({ + allowedExtensions: fileInfo.allowedExtensions, + maxUploadSize: fileInfo.maxUploadSize, + unit: fileInfo.unit.label as string, + }), } } @@ -512,9 +533,3 @@ export const convertVariableDataTableToFormData = ({ return { updatedFormDataErrorObj, updatedFormData } } - -// VALIDATIONS -export const validateMaxFileSize = (file: File, maxFileSizeInMB: number, unit: string = 'KB') => { - const unitMultiplier = unit === 'MB' ? 1024 : 1 - return typeof maxFileSizeInMB !== 'number' || file.size <= maxFileSizeInMB * unitMultiplier * 1024 -} diff --git a/src/components/app/details/triggerView/TriggerView.tsx b/src/components/app/details/triggerView/TriggerView.tsx index 4429bf4ede..3ec8abea0d 100644 --- a/src/components/app/details/triggerView/TriggerView.tsx +++ b/src/components/app/details/triggerView/TriggerView.tsx @@ -34,6 +34,7 @@ import { TOAST_ACCESS_DENIED, BlockedStateData, getEnvironmentListMinPublic, + uploadCIPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import ReactGA from 'react-ga4' import { withRouter, NavLink, Route, Switch } from 'react-router-dom' @@ -104,6 +105,7 @@ const getCIBlockState: (...props) => Promise = importComponent const ImagePromotionRouter = importComponentFromFELibrary('ImagePromotionRouter', null, 'function') const getRuntimeParams = importComponentFromFELibrary('getRuntimeParams', null, 'function') const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') +const getRuntimeParamsV2Payload = importComponentFromFELibrary('getRuntimeParamsV2Payload', null, 'function') class TriggerView extends Component { timerRef @@ -145,6 +147,7 @@ class TriggerView extends Component { searchImageTag: '', resourceFilters: [], runtimeParams: [], + runtimeParamsV2: [], } this.refreshMaterial = this.refreshMaterial.bind(this) this.onClickCIMaterial = this.onClickCIMaterial.bind(this) @@ -713,7 +716,7 @@ class TriggerView extends Component { this.props.appContext.currentAppName, ) : null, - getRuntimeParams?.(ciNodeId) ?? null, + getRuntimeParams?.(ciNodeId, true) ?? null, ]) .then((resp) => { // For updateCIMaterialList, it's already being set inside the same function so not setting that @@ -739,7 +742,7 @@ class TriggerView extends Component { if (resp[2]) { // Not saving as null since page ViewType is set as Error in case of error this.setState({ - runtimeParams: resp[2] || [], + runtimeParamsV2: resp[2] || [], }) } }) @@ -905,7 +908,7 @@ class TriggerView extends Component { } // No need to validate here since ciMaterial handles it for trigger view - const runtimeParamsPayload = getRuntimeParamsPayload?.(this.state.runtimeParams ?? []) + const runtimeParamsPayload = getRuntimeParamsV2Payload?.(this.state.runtimeParamsV2 ?? []) const payload = { pipelineId: +this.state.ciNodeId, @@ -1095,7 +1098,6 @@ class TriggerView extends Component { this.getWorkflowStatus() } - onClickWebhookTimeStamp = () => { if (this.state.webhookTimeStampOrder === TIME_STAMP_ORDER.DESCENDING) { this.setState({ webhookTimeStampOrder: TIME_STAMP_ORDER.ASCENDING }) @@ -1130,6 +1132,21 @@ class TriggerView extends Component { }) } + handleRuntimeParamChangeV2: CIMaterialProps['handleRuntimeParamChangeV2'] = (updatedRuntimeParams) => { + this.setState({ + runtimeParamsV2: updatedRuntimeParams, + }) + } + + uploadFile: CIMaterialProps['uploadFile'] = ({ file, allowedExtensions, maxUploadSize }) => + uploadCIPipelineFile({ + appId: +this.props.match.params.appId, + ciPipelineId: this.getCINode().parentCiPipeline, + file, + allowedExtensions, + maxUploadSize, + }) + setLoader = (isLoader) => { this.setState({ loader: isLoader, @@ -1172,16 +1189,13 @@ class TriggerView extends Component { this.abortCIBuild = new AbortController() } - renderCIMaterial = () => { if (this.state.ciNodeId) { const nd: CommonNodeAttr = this.getCINode() const material = nd?.[this.state.materialType] || [] return ( - + { isJobCI={!!nd?.isJobCI} runtimeParams={this.state.runtimeParams} handleRuntimeParamChange={this.handleRuntimeParamChange} + runtimeParamsV2={this.state.runtimeParamsV2} + handleRuntimeParamChangeV2={this.handleRuntimeParamChangeV2} closeCIModal={this.closeCIModal} abortController={this.abortCIBuild} resetAbortController={this.resetAbortController} + uploadFile={this.uploadFile} /> diff --git a/src/components/app/details/triggerView/ciMaterial.tsx b/src/components/app/details/triggerView/ciMaterial.tsx index 2c235b859c..5fcbdef404 100644 --- a/src/components/app/details/triggerView/ciMaterial.tsx +++ b/src/components/app/details/triggerView/ciMaterial.tsx @@ -309,6 +309,9 @@ class CIMaterial extends Component { handleRuntimeParamChange={this.props.handleRuntimeParamChange} handleRuntimeParamError={this.handleRuntimeParamError} appId={this.props.appId} + runtimeParamsV2={this.props.runtimeParamsV2} + handleRuntimeParamChangeV2={this.props.handleRuntimeParamChangeV2} + uploadFile={this.props.uploadFile} /> {this.props.isCITriggerBlocked ? null : this.renderMaterialStartBuild(canTrigger)} diff --git a/src/components/app/details/triggerView/types.ts b/src/components/app/details/triggerView/types.ts index 6f18d5839f..a53f015953 100644 --- a/src/components/app/details/triggerView/types.ts +++ b/src/components/app/details/triggerView/types.ts @@ -43,6 +43,9 @@ import { CdPipeline, ConsequenceType, PolicyKindType, + RuntimePluginVariables, + UploadFileDTO, + UploadFileProps, } from '@devtron-labs/devtron-fe-common-lib' import React from 'react' import { EnvironmentWithSelectPickerType } from '@Components/CIPipelineN/types' @@ -389,6 +392,7 @@ export interface TriggerViewState { searchImageTag?: string resourceFilters?: FilterConditionsListType[] runtimeParams?: RuntimeParamsListItemType[] + runtimeParamsV2: RuntimePluginVariables[] } export interface CIMaterialProps @@ -424,7 +428,10 @@ export interface CIMaterialProps setSelectedEnv?: React.Dispatch> isJobCI?: boolean handleRuntimeParamChange: HandleRuntimeParamChange + runtimeParamsV2: RuntimePluginVariables[] + handleRuntimeParamChangeV2: (runtimeParams: RuntimePluginVariables[]) => void runtimeParams: KeyValueListType[] + uploadFile: (props: UploadFileProps) => Promise } // -- begining of response type objects for trigger view diff --git a/src/components/ciPipeline/ciPipeline.service.ts b/src/components/ciPipeline/ciPipeline.service.ts index 26750be32b..e16350fc51 100644 --- a/src/components/ciPipeline/ciPipeline.service.ts +++ b/src/components/ciPipeline/ciPipeline.service.ts @@ -30,7 +30,7 @@ import { import { Routes, SourceTypeMap, TriggerType, ViewType } from '../../config' import { getSourceConfig, getWebhookDataMetaConfig } from '../../services/service' import { CiPipelineSourceTypeBaseOptions } from '../CIPipelineN/ciPipeline.utils' -import { PatchAction, UploadCIPipelineFileDTO } from './types' +import { PatchAction } from './types' import { safeTrim } from '../../util/Util' import { ChangeCIPayloadType } from '../workflowEditor/types' import { MutableRefObject } from 'react' @@ -607,35 +607,3 @@ export async function getGlobalVariable(appId: number, isCD?: boolean): Promise< return { result: variableList } } - -export const uploadCIPipelineFile = async ({ - file, - appId, - ciPipelineId, - abortControllerRef, -}: { - file: File[] - appId: number - ciPipelineId: number - abortControllerRef?: MutableRefObject -}): Promise => { - const formData = new FormData() - formData.append('file', file[0]) - - try { - const { result } = await post( - `${Routes.CI_CONFIG_GET}/${appId}/${ciPipelineId}/${Routes.FILE_UPLOAD}`, - formData, - { abortControllerRef }, - true, - ) - - return result - } catch (err) { - if (getIsRequestAborted(err)) { - return - } - showError(err) - throw err - } -} diff --git a/src/components/ciPipeline/types.ts b/src/components/ciPipeline/types.ts index b08001c232..588495567a 100644 --- a/src/components/ciPipeline/types.ts +++ b/src/components/ciPipeline/types.ts @@ -428,11 +428,3 @@ export interface WebhookConditionType { deleteWebhookCondition: (index: number) => void canEditSelectorCondition: boolean } - -export interface UploadCIPipelineFileDTO { - id: number - name: string - size: number - mimeType: string - extension: string -} diff --git a/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx b/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx index 3729695f3d..60080f4af2 100644 --- a/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx +++ b/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx @@ -52,7 +52,7 @@ import { ReceivedWebhookRedirectButton } from './ReceivedWebhookRedirectButton' const MissingPluginBlockState = importComponentFromFELibrary('MissingPluginBlockState', null, 'function') const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') -const RuntimeParameters = importComponentFromFELibrary('RuntimeParameters', null, 'function') +const RuntimeParametersV2 = importComponentFromFELibrary('RuntimeParametersV2', null, 'function') export const GitInfoMaterial = ({ dataTestId = '', @@ -74,8 +74,6 @@ export const GitInfoMaterial = ({ // Not required for BulkCI currentSidebarTab, handleSidebarTabChange, - runtimeParams, - handleRuntimeParamChange, handleRuntimeParamError, isBulkCIWebhook, webhookPayloads, @@ -83,6 +81,9 @@ export const GitInfoMaterial = ({ setIsWebhookBulkCI, isBulk = false, appId, + runtimeParamsV2, + handleRuntimeParamChangeV2, + uploadFile, }: GitInfoMaterialProps) => { const { push } = useHistory() const location = useLocation() @@ -365,15 +366,16 @@ export const GitInfoMaterial = ({ ) } - if (RuntimeParameters && currentSidebarTab === CIMaterialSidebarType.PARAMETERS) { + if (RuntimeParametersV2 && currentSidebarTab === CIMaterialSidebarType.PARAMETERS) { return ( - ) } diff --git a/src/components/common/helpers/GitInfoMaterialCard/types.ts b/src/components/common/helpers/GitInfoMaterialCard/types.ts index 1aa0b505e9..6fdfa18ba1 100644 --- a/src/components/common/helpers/GitInfoMaterialCard/types.ts +++ b/src/components/common/helpers/GitInfoMaterialCard/types.ts @@ -15,6 +15,9 @@ export interface GitInfoMaterialProps | 'pipelineId' | 'runtimeParams' | 'appId' + | 'runtimeParamsV2' + | 'handleRuntimeParamChangeV2' + | 'uploadFile' > { dataTestId?: string material: CIMaterialType[] diff --git a/src/components/workflowEditor/types.ts b/src/components/workflowEditor/types.ts index db5a64bdfd..25594cfb72 100644 --- a/src/components/workflowEditor/types.ts +++ b/src/components/workflowEditor/types.ts @@ -28,8 +28,9 @@ import { PipelineFormType, MandatoryPluginDataType, CiPipeline, + UploadFileDTO, + UploadFileProps, } from '@devtron-labs/devtron-fe-common-lib' -import { UploadCIPipelineFileDTO } from '@Components/ciPipeline/types' import { RouteComponentProps } from 'react-router-dom' import { HostURLConfig } from '../../services/service.types' import { CIPipelineNodeType, CdPipelineResult } from '../app/details/triggerView/types' @@ -330,7 +331,7 @@ export interface PipelineContext { handleDisableParentModalCloseUpdate?: (disableParentModalClose: boolean) => void handleValidateMandatoryPlugins: (params: HandleValidateMandatoryPluginsParamsType) => void mandatoryPluginData: MandatoryPluginDataType - uploadFile: (file: File[]) => Promise + uploadFile: (file: UploadFileProps) => Promise } export interface SourceTypeCardProps { diff --git a/src/config/constants.ts b/src/config/constants.ts index 99a91660e6..1844e17147 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -128,7 +128,6 @@ export const Routes = { SSO_LOGIN_SERVICES: 'login-service', API_TOKEN: 'api-token', API_TOKEN_WEBHOOK: 'api-token/webhook', - FILE_UPLOAD: 'file/upload', DEPLOYMENT_METRICS: 'deployment-metrics', APP_CONFIG_MAP_GET: 'configmap/applevel/get', From a400ecd8ad7b7cb30f1c67116601850f8d1041bf Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 6 Dec 2024 03:25:44 +0530 Subject: [PATCH 06/36] fix: VariableDataTable - showError bool -> true --- .../CIPipelineN/VariableDataTable/VariableDataTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx index a8bf1a8035..41f3f33cd6 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx @@ -607,7 +607,7 @@ export const VariableDataTable = ({ onRowEdit={dataTableHandleChange} onRowDelete={dataTableHandleDelete} onRowAdd={dataTableHandleAddition} - // showError + showError validationSchema={validationSchema} actionButtonConfig={getActionButtonConfig()} /> From ebfe434cf1bf058b6c9aba86a88261e02962b849 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 6 Dec 2024 11:09:05 +0530 Subject: [PATCH 07/36] feat: CD Pipeline - Runtime Params Integration --- .../ApplicationGroup/AppGroup.types.ts | 6 ++-- .../Details/TriggerView/BulkCDTrigger.tsx | 34 +++++++++++-------- .../Details/TriggerView/BulkCITrigger.tsx | 21 ++++++++---- .../Details/TriggerView/EnvTriggerView.tsx | 16 +++++---- .../app/details/triggerView/TriggerView.tsx | 14 ++------ .../app/details/triggerView/cdMaterial.tsx | 24 +++++++------ .../app/details/triggerView/ciMaterial.tsx | 2 -- .../app/details/triggerView/types.ts | 18 ++++------ src/components/app/types.ts | 4 +-- src/components/cdPipeline/CDPipeline.tsx | 7 ++-- .../GitInfoMaterialCard/GitInfoMaterial.tsx | 8 ++--- .../helpers/GitInfoMaterialCard/types.ts | 2 -- 12 files changed, 79 insertions(+), 77 deletions(-) diff --git a/src/components/ApplicationGroup/AppGroup.types.ts b/src/components/ApplicationGroup/AppGroup.types.ts index b0fbf8fd6c..4677b96f46 100644 --- a/src/components/ApplicationGroup/AppGroup.types.ts +++ b/src/components/ApplicationGroup/AppGroup.types.ts @@ -26,9 +26,9 @@ import { WorkflowType, AppInfoListType, GVKType, - RuntimeParamsListItemType, UseUrlFiltersReturnType, CommonNodeAttr, + RuntimePluginVariables, } from '@devtron-labs/devtron-fe-common-lib' import { CDMaterialProps } from '../app/details/triggerView/types' import { EditDescRequest, NodeType, Nodes, OptionType } from '../app/types' @@ -103,8 +103,8 @@ export interface ResponseRowType { } interface BulkRuntimeParamsType { - runtimeParams: Record - setRuntimeParams: React.Dispatch>> + runtimeParams: Record + setRuntimeParams: React.Dispatch>> runtimeParamsErrorState: Record setRuntimeParamsErrorState: React.Dispatch>> } diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx index 5d1229a441..e9e825fcb0 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx @@ -38,10 +38,12 @@ import { SelectPicker, CDMaterialSidebarType, CD_MATERIAL_SIDEBAR_TABS, - RuntimeParamsListItemType, ToastManager, ToastVariantType, CommonNodeAttr, + RuntimePluginVariables, + uploadCDPipelineFile, + UploadFileProps, } from '@devtron-labs/devtron-fe-common-lib' import { useHistory, useLocation } from 'react-router-dom' import { ReactComponent as Close } from '../../../../assets/icons/ic-cross.svg' @@ -166,12 +168,21 @@ export default function BulkCDTrigger({ })) } - const handleRuntimeParamChange = (currentAppRuntimeParams: RuntimeParamsListItemType[]) => { + const handleRuntimeParamChange = (currentAppRuntimeParams: RuntimePluginVariables[]) => { const clonedRuntimeParams = structuredClone(runtimeParams) clonedRuntimeParams[selectedApp.appId] = currentAppRuntimeParams setRuntimeParams(clonedRuntimeParams) } + const bulkUploadFile = ({ file, allowedExtensions, maxUploadSize }: UploadFileProps) => + uploadCDPipelineFile({ + file, + allowedExtensions, + maxUploadSize, + appId: selectedApp.appId, + envId: selectedApp.envId, + }) + const getDeploymentWindowData = async (_cdMaterialResponse) => { const currentEnv = appList[0].envId const appEnvMap = [] @@ -429,13 +440,9 @@ export default function BulkCDTrigger({ if (tagNotFoundWarningsMap.has(app.appId)) { return (
- + - - {tagNotFoundWarningsMap.get(app.appId)} - + {tagNotFoundWarningsMap.get(app.appId)}
) } @@ -454,13 +461,9 @@ export default function BulkCDTrigger({ if (!!warningMessage && !app.showPluginWarning) { return (
- + - - {warningMessage} - + {warningMessage}
) } @@ -473,7 +476,7 @@ export default function BulkCDTrigger({ nodeType={commonNodeAttrType} shouldRenderAdditionalInfo={isAppSelected} /> - ) + ) } return null @@ -795,6 +798,7 @@ export default function BulkCDTrigger({ bulkRuntimeParams={runtimeParams[selectedApp.appId] || []} handleBulkRuntimeParamChange={handleRuntimeParamChange} handleBulkRuntimeParamError={handleRuntimeParamError} + bulkUploadFile={bulkUploadFile} bulkSidebarTab={currentSidebarTab} selectedAppName={selectedApp.name} /> diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx index a6b0ac1055..87b23788e5 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/BulkCITrigger.tsx @@ -27,7 +27,6 @@ import { GenericEmptyState, CIMaterialSidebarType, ApiQueuingWithBatch, - RuntimeParamsListItemType, ModuleNameMap, SourceTypeMap, ToastManager, @@ -41,6 +40,9 @@ import { ComponentSizeType, ButtonStyleType, noop, + RuntimePluginVariables, + uploadCIPipelineFile, + UploadFileProps, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { getCIPipelineURL, getParsedBranchValuesForPlugin, importComponentFromFELibrary } from '../../../common' @@ -169,14 +171,14 @@ const BulkCITrigger = ({ [appDetails.ciPipelineId]: [], }) } - return () => getRuntimeParams(appDetails.ciPipelineId) + return () => getRuntimeParams(appDetails.ciPipelineId, true) }) if (runtimeParamsServiceList.length) { try { // Appending any for legacy code, since we did not had generics in APIQueuingWithBatch const responses: any[] = await ApiQueuingWithBatch(runtimeParamsServiceList, true) - const _runtimeParams: Record = {} + const _runtimeParams: Record = {} responses.forEach((res, index) => { _runtimeParams[appList[index]?.ciPipelineId] = res.value || [] }) @@ -249,6 +251,15 @@ const BulkCITrigger = ({ setRuntimeParams(updatedRuntimeParams) } + const uploadFile = ({ file, allowedExtensions, maxUploadSize }: UploadFileProps) => + uploadCIPipelineFile({ + file, + allowedExtensions, + maxUploadSize, + appId: selectedApp.appId, + ciPipelineId: +selectedApp.ciPipelineId, + }) + const handleSidebarTabChange = (e: React.ChangeEvent) => { if (runtimeParamsErrorState[selectedApp.ciPipelineId]) { ToastManager.showToast({ @@ -520,6 +531,7 @@ const BulkCITrigger = ({ runtimeParams={runtimeParams[selectedApp.ciPipelineId] || []} handleRuntimeParamChange={handleRuntimeParamChange} handleRuntimeParamError={handleRuntimeParamError} + uploadFile={uploadFile} appName={selectedApp?.name} isBulkCIWebhook={isWebhookBulkCI} setIsWebhookBulkCI={setIsWebhookBulkCI} @@ -527,9 +539,6 @@ const BulkCITrigger = ({ isWebhookPayloadLoading={isWebhookPayloadLoading} isBulk appId={selectedApp.appId.toString()} - runtimeParamsV2={[]} - handleRuntimeParamChangeV2={noop} - uploadFile={noop} /> ) } diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index 52e39a77e8..0941c75840 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -42,12 +42,12 @@ import { ApiQueuingWithBatch, usePrompt, SourceTypeMap, - RuntimeParamsListItemType, preventBodyScroll, ToastManager, ToastVariantType, BlockedStateData, - noop, + RuntimePluginVariables, + uploadCIPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { @@ -186,7 +186,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou const [isConfigPresent, setConfigPresent] = useState(false) const [isDefaultConfigPresent, setDefaultConfig] = useState(false) // Mapping pipelineId (in case of CI) and appId (in case of CD) to runtime params - const [runtimeParams, setRuntimeParams] = useState>({}) + const [runtimeParams, setRuntimeParams] = useState>({}) const [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState>({}) const [isBulkTriggerLoading, setIsBulkTriggerLoading] = useState(false) @@ -873,7 +873,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou _appName, ) : null, - getRuntimeParams ? getRuntimeParams(ciNodeId) : null, + getRuntimeParams ? getRuntimeParams(ciNodeId, true) : null, ]) .then((resp) => { // need to set result for getCIBlockState call only as for updateCIMaterialList @@ -1874,6 +1874,10 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou } } + const uploadFile: CIMaterialProps['uploadFile'] = ({ file, allowedExtensions, maxUploadSize }) => null + // TODO: rohit confirm with BE + // uploadCIPipelineFile({ file, allowedExtensions, maxUploadSize, appId: +appId, ciPipelineId: null }) + const createBulkCITriggerData = (): BulkCIDetailType[] => { const _selectedAppWorkflowList: BulkCIDetailType[] = [] filteredWorkflows.forEach((wf) => { @@ -2026,12 +2030,10 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou isJobCI={!!nd?.isJobCI} runtimeParams={runtimeParams[nd?.id] ?? []} handleRuntimeParamChange={handleRuntimeParamChange} + uploadFile={uploadFile} closeCIModal={closeCIModal} abortController={abortCIBuildRef.current} resetAbortController={resetAbortController} - runtimeParamsV2={[]} - handleRuntimeParamChangeV2={noop} - uploadFile={noop} /> diff --git a/src/components/app/details/triggerView/TriggerView.tsx b/src/components/app/details/triggerView/TriggerView.tsx index 3ec8abea0d..4e3491bc3b 100644 --- a/src/components/app/details/triggerView/TriggerView.tsx +++ b/src/components/app/details/triggerView/TriggerView.tsx @@ -105,7 +105,6 @@ const getCIBlockState: (...props) => Promise = importComponent const ImagePromotionRouter = importComponentFromFELibrary('ImagePromotionRouter', null, 'function') const getRuntimeParams = importComponentFromFELibrary('getRuntimeParams', null, 'function') const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') -const getRuntimeParamsV2Payload = importComponentFromFELibrary('getRuntimeParamsV2Payload', null, 'function') class TriggerView extends Component { timerRef @@ -147,7 +146,6 @@ class TriggerView extends Component { searchImageTag: '', resourceFilters: [], runtimeParams: [], - runtimeParamsV2: [], } this.refreshMaterial = this.refreshMaterial.bind(this) this.onClickCIMaterial = this.onClickCIMaterial.bind(this) @@ -742,7 +740,7 @@ class TriggerView extends Component { if (resp[2]) { // Not saving as null since page ViewType is set as Error in case of error this.setState({ - runtimeParamsV2: resp[2] || [], + runtimeParams: resp[2] || [], }) } }) @@ -908,7 +906,7 @@ class TriggerView extends Component { } // No need to validate here since ciMaterial handles it for trigger view - const runtimeParamsPayload = getRuntimeParamsV2Payload?.(this.state.runtimeParamsV2 ?? []) + const runtimeParamsPayload = getRuntimeParamsPayload?.(this.state.runtimeParams ?? []) const payload = { pipelineId: +this.state.ciNodeId, @@ -1132,12 +1130,6 @@ class TriggerView extends Component { }) } - handleRuntimeParamChangeV2: CIMaterialProps['handleRuntimeParamChangeV2'] = (updatedRuntimeParams) => { - this.setState({ - runtimeParamsV2: updatedRuntimeParams, - }) - } - uploadFile: CIMaterialProps['uploadFile'] = ({ file, allowedExtensions, maxUploadSize }) => uploadCIPipelineFile({ appId: +this.props.match.params.appId, @@ -1241,8 +1233,6 @@ class TriggerView extends Component { isJobCI={!!nd?.isJobCI} runtimeParams={this.state.runtimeParams} handleRuntimeParamChange={this.handleRuntimeParamChange} - runtimeParamsV2={this.state.runtimeParamsV2} - handleRuntimeParamChangeV2={this.handleRuntimeParamChangeV2} closeCIModal={this.closeCIModal} abortController={this.abortCIBuild} resetAbortController={this.resetAbortController} diff --git a/src/components/app/details/triggerView/cdMaterial.tsx b/src/components/app/details/triggerView/cdMaterial.tsx index b565600070..5ecb36deeb 100644 --- a/src/components/app/details/triggerView/cdMaterial.tsx +++ b/src/components/app/details/triggerView/cdMaterial.tsx @@ -65,7 +65,6 @@ import { useDownload, SearchBar, CDMaterialSidebarType, - RuntimeParamsListItemType, CDMaterialResponseType, CD_MATERIAL_SIDEBAR_TABS, getIsManualApprovalConfigured, @@ -78,6 +77,8 @@ import { ResponseType, ApiResponseResultType, CommonNodeAttr, + RuntimePluginVariables, + uploadCDPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { @@ -134,7 +135,7 @@ const getDeploymentWindowProfileMetaData = importComponentFromFELibrary( const MaintenanceWindowInfoBar = importComponentFromFELibrary('MaintenanceWindowInfoBar') const DeploymentWindowConfirmationDialog = importComponentFromFELibrary('DeploymentWindowConfirmationDialog') const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') -const RuntimeParameters = importComponentFromFELibrary('RuntimeParameters', null, 'function') +const RuntimeParametersV2 = importComponentFromFELibrary('RuntimeParametersV2', null, 'function') const getIsImageApproverFromUserApprovalMetaData: ( email: string, userApprovalMetadata: UserApprovalMetadataType, @@ -186,6 +187,7 @@ const CDMaterial = ({ consequence, configurePluginURL, isTriggerBlockedDueToPlugin, + bulkUploadFile, }: Readonly) => { // stageType should handle approval node, compute CDMaterialServiceEnum, create queryParams state // FIXME: the query params returned by useSearchString seems faulty @@ -270,7 +272,7 @@ const CDMaterial = ({ const [appliedFilterList, setAppliedFilterList] = useState([]) // ----- RUNTIME PARAMS States (To be overridden by parent props in case of bulk) ------- const [currentSidebarTab, setCurrentSidebarTab] = useState(CDMaterialSidebarType.IMAGE) - const [runtimeParamsList, setRuntimeParamsList] = useState([]) + const [runtimeParamsList, setRuntimeParamsList] = useState([]) const [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState(false) const [value, setValue] = useState() const [showDeploymentWindowConfirmation, setShowDeploymentWindowConfirmation] = useState(false) @@ -561,9 +563,7 @@ const CDMaterial = ({ })) } - const handleRuntimeParamChange: typeof handleBulkRuntimeParamChange = ( - updatedRuntimeParams: RuntimeParamsListItemType[], - ) => { + const handleRuntimeParamChange: typeof handleBulkRuntimeParamChange = (updatedRuntimeParams) => { setRuntimeParamsList(updatedRuntimeParams) } @@ -571,6 +571,9 @@ const CDMaterial = ({ setRuntimeParamsErrorState(errorState) } + const uploadFile: typeof bulkUploadFile = ({ file, allowedExtensions, maxUploadSize }) => + uploadCDPipelineFile({ file, allowedExtensions, maxUploadSize, appId, envId }) + const clearSearch = (e: React.MouseEvent): void => { stopPropagation(e) if (state.searchText) { @@ -1496,7 +1499,7 @@ const CDMaterial = ({ {(bulkSidebarTab ? bulkSidebarTab === CDMaterialSidebarType.IMAGE - : currentSidebarTab === CDMaterialSidebarType.IMAGE) || !RuntimeParameters ? ( + : currentSidebarTab === CDMaterialSidebarType.IMAGE) || !RuntimeParametersV2 ? ( <> {isApprovalConfigured && renderMaterial(consumedImage, true, isApprovalConfigured)}
@@ -1543,11 +1546,11 @@ const CDMaterial = ({ )} ) : ( - )} @@ -1713,8 +1716,7 @@ const CDMaterial = ({ )} > - {AllowedWithWarningTippy && - showPluginWarningBeforeTrigger ? ( + {AllowedWithWarningTippy && showPluginWarningBeforeTrigger ? ( { handleRuntimeParamChange={this.props.handleRuntimeParamChange} handleRuntimeParamError={this.handleRuntimeParamError} appId={this.props.appId} - runtimeParamsV2={this.props.runtimeParamsV2} - handleRuntimeParamChangeV2={this.props.handleRuntimeParamChangeV2} uploadFile={this.props.uploadFile} /> {this.props.isCITriggerBlocked ? null : this.renderMaterialStartBuild(canTrigger)} diff --git a/src/components/app/details/triggerView/types.ts b/src/components/app/details/triggerView/types.ts index a53f015953..895dc922e2 100644 --- a/src/components/app/details/triggerView/types.ts +++ b/src/components/app/details/triggerView/types.ts @@ -31,13 +31,10 @@ import { PipelineType, WorkflowType, Material, - KeyValueListType, CIMaterialSidebarType, ArtifactPromotionMetadata, DeploymentWithConfigType, CIMaterialType, - RuntimeParamsListItemType, - KeyValueTableProps, CDMaterialSidebarType, CiPipeline, CdPipeline, @@ -55,15 +52,16 @@ import { DeploymentHistoryDetail } from '../cdDetails/cd.type' import { TIME_STAMP_ORDER } from './Constants' import { Offset, WorkflowDimensions } from './config' -export type HandleRuntimeParamChange = (updatedRuntimeParams: RuntimeParamsListItemType[]) => void +export type HandleRuntimeParamChange = (updatedRuntimeParams: RuntimePluginVariables[]) => void type CDMaterialBulkRuntimeParams = | { isFromBulkCD: true - bulkRuntimeParams: RuntimeParamsListItemType[] + bulkRuntimeParams: RuntimePluginVariables[] handleBulkRuntimeParamChange: HandleRuntimeParamChange - handleBulkRuntimeParamError: KeyValueTableProps['onError'] + handleBulkRuntimeParamError: (errorState: boolean) => void bulkSidebarTab: CDMaterialSidebarType + bulkUploadFile: (props: UploadFileProps) => Promise } | { isFromBulkCD?: false @@ -71,6 +69,7 @@ type CDMaterialBulkRuntimeParams = handleBulkRuntimeParamChange?: never handleBulkRuntimeParamError?: never bulkSidebarTab?: never + bulkUploadFile?: never } type CDMaterialPluginWarningProps = @@ -391,8 +390,7 @@ export interface TriggerViewState { isDefaultConfigPresent?: boolean searchImageTag?: string resourceFilters?: FilterConditionsListType[] - runtimeParams?: RuntimeParamsListItemType[] - runtimeParamsV2: RuntimePluginVariables[] + runtimeParams?: RuntimePluginVariables[] } export interface CIMaterialProps @@ -428,9 +426,7 @@ export interface CIMaterialProps setSelectedEnv?: React.Dispatch> isJobCI?: boolean handleRuntimeParamChange: HandleRuntimeParamChange - runtimeParamsV2: RuntimePluginVariables[] - handleRuntimeParamChangeV2: (runtimeParams: RuntimePluginVariables[]) => void - runtimeParams: KeyValueListType[] + runtimeParams: RuntimePluginVariables[] uploadFile: (props: UploadFileProps) => Promise } diff --git a/src/components/app/types.ts b/src/components/app/types.ts index 1719bce688..fe22a80f43 100644 --- a/src/components/app/types.ts +++ b/src/components/app/types.ts @@ -25,9 +25,9 @@ import { ReleaseMode, AppEnvironment, DeploymentNodeType, - RuntimeParamsListItemType, RuntimeParamsTriggerPayloadType, HelmReleaseStatus, + RuntimePluginVariables, } from '@devtron-labs/devtron-fe-common-lib' import { DeploymentStatusDetailsBreakdownDataType, ErrorItem } from './details/appDetails/appDetails.type' import { GroupFilterType } from '../ApplicationGroup/AppGroup.types' @@ -649,7 +649,7 @@ export interface TriggerCDNodeServiceProps { /** * Would be available only case of PRE/POST CD */ - runtimeParams?: RuntimeParamsListItemType[] + runtimeParams?: RuntimePluginVariables[] } export interface TriggerCDPipelinePayloadType { diff --git a/src/components/cdPipeline/CDPipeline.tsx b/src/components/cdPipeline/CDPipeline.tsx index 0e14761217..2007a6ab26 100644 --- a/src/components/cdPipeline/CDPipeline.tsx +++ b/src/components/cdPipeline/CDPipeline.tsx @@ -50,6 +50,7 @@ import { ResourceKindType, getEnvironmentListMinPublic, noop, + uploadCDPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import { useEffect, useMemo, useRef, useState } from 'react' import { Redirect, Route, Switch, useParams, useRouteMatch } from 'react-router-dom' @@ -1200,6 +1201,9 @@ export default function CDPipeline({ setFormData({ ...formData, releaseMode: ReleaseMode.NEW_DEPLOYMENT }) } + const uploadFile: PipelineContext['uploadFile'] = ({ file, allowedExtensions, maxUploadSize }) => + uploadCDPipelineFile({ file, allowedExtensions, maxUploadSize, appId: +appId, envId: formData.environmentId }) + const contextValue = useMemo(() => { return { formData, @@ -1238,8 +1242,7 @@ export default function CDPipeline({ handleDisableParentModalCloseUpdate, handleValidateMandatoryPlugins, mandatoryPluginData, - // TODO: Handle for CD File Upload (Rohit) - uploadFile: noop + uploadFile, } }, [ formData, diff --git a/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx b/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx index 60080f4af2..e7706d960d 100644 --- a/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx +++ b/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx @@ -74,6 +74,8 @@ export const GitInfoMaterial = ({ // Not required for BulkCI currentSidebarTab, handleSidebarTabChange, + runtimeParams, + handleRuntimeParamChange, handleRuntimeParamError, isBulkCIWebhook, webhookPayloads, @@ -81,8 +83,6 @@ export const GitInfoMaterial = ({ setIsWebhookBulkCI, isBulk = false, appId, - runtimeParamsV2, - handleRuntimeParamChangeV2, uploadFile, }: GitInfoMaterialProps) => { const { push } = useHistory() @@ -372,8 +372,8 @@ export const GitInfoMaterial = ({ // Have to add key for appId since key value config would not be updated incase of app change key={`runtime-parameters-${appId}`} heading={getRuntimeParametersHeading()} - parameters={runtimeParamsV2} - handleChange={handleRuntimeParamChangeV2} + parameters={runtimeParams} + handleChange={handleRuntimeParamChange} onError={handleRuntimeParamError} uploadFile={uploadFile} /> diff --git a/src/components/common/helpers/GitInfoMaterialCard/types.ts b/src/components/common/helpers/GitInfoMaterialCard/types.ts index 6fdfa18ba1..9cb1b1625c 100644 --- a/src/components/common/helpers/GitInfoMaterialCard/types.ts +++ b/src/components/common/helpers/GitInfoMaterialCard/types.ts @@ -15,8 +15,6 @@ export interface GitInfoMaterialProps | 'pipelineId' | 'runtimeParams' | 'appId' - | 'runtimeParamsV2' - | 'handleRuntimeParamChangeV2' | 'uploadFile' > { dataTestId?: string From 80aaeaae69695e4ead0c5d9125f84a6f27259410 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 6 Dec 2024 16:21:33 +0530 Subject: [PATCH 08/36] feat: VariableDataTable - validation schema update, CI / CD Pipeline file upload util update --- .../Details/TriggerView/EnvTriggerView.tsx | 5 -- src/components/CIPipelineN/CIPipeline.tsx | 2 +- .../VariableDataTable/VariableDataTable.tsx | 80 ++++++------------- .../CIPipelineN/VariableDataTable/utils.tsx | 6 +- .../VariableDataTable/validationSchema.ts | 43 ++++++++++ .../details/triggerView/CIMaterialModal.tsx | 18 ++++- .../app/details/triggerView/TriggerView.tsx | 10 --- .../app/details/triggerView/types.ts | 2 +- src/components/cdPipeline/CDPipeline.tsx | 1 - 9 files changed, 86 insertions(+), 81 deletions(-) create mode 100644 src/components/CIPipelineN/VariableDataTable/validationSchema.ts diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index 0941c75840..2bd9037d42 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -1874,10 +1874,6 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou } } - const uploadFile: CIMaterialProps['uploadFile'] = ({ file, allowedExtensions, maxUploadSize }) => null - // TODO: rohit confirm with BE - // uploadCIPipelineFile({ file, allowedExtensions, maxUploadSize, appId: +appId, ciPipelineId: null }) - const createBulkCITriggerData = (): BulkCIDetailType[] => { const _selectedAppWorkflowList: BulkCIDetailType[] = [] filteredWorkflows.forEach((wf) => { @@ -2030,7 +2026,6 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou isJobCI={!!nd?.isJobCI} runtimeParams={runtimeParams[nd?.id] ?? []} handleRuntimeParamChange={handleRuntimeParamChange} - uploadFile={uploadFile} closeCIModal={closeCIModal} abortController={abortCIBuildRef.current} resetAbortController={resetAbortController} diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index 7d290f27b5..a279d61905 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -982,7 +982,7 @@ export default function CIPipeline({ <> {renderFloatingVariablesWidget()} - + {renderCIPipelineModal()} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx index 41f3f33cd6..24ccd7ab64 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx @@ -36,6 +36,7 @@ import { getValColumnRowValue, getVariableDataTableInitialRows, } from './utils' +import { variableDataTableValidationSchema } from './validationSchema' import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' import { VariableConfigOverlay } from './VariableConfigOverlay' @@ -93,11 +94,6 @@ export const VariableDataTable = ({ type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' ] - const ioVariablesError: { isValid: boolean; message: string }[] = - formDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ - type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' - ] - // STATES const [rows, setRows] = useState([]) @@ -105,18 +101,10 @@ export const VariableDataTable = ({ const initialRowsSet = useRef('') useEffect(() => { - setRows( - ioVariables?.length - ? getVariableDataTableInitialRows({ emptyRowParams, ioVariables, isCustomTask, type }) - : [getEmptyVariableDataTableRow(emptyRowParams)], - ) + setRows(getVariableDataTableInitialRows({ emptyRowParams, ioVariables, isCustomTask, type })) initialRowsSet.current = 'set' }, []) - // useEffect(() => { - // console.log('meg', rows, ioVariables, formDataErrorObj) - // }, [JSON.stringify(ioVariables)]) - // METHODS const handleRowUpdateAction = (rowAction: HandleRowUpdateActionProps) => { const { actionType } = rowAction @@ -310,9 +298,6 @@ export const VariableDataTable = ({ case VariableDataTableActionType.DELETE_ROW: updatedRows = updatedRows.filter((row) => row.id !== rowAction.rowId) - if (updatedRows.length === 0) { - updatedRows = [getEmptyVariableDataTableRow(emptyRowParams)] - } break case VariableDataTableActionType.UPDATE_ROW: @@ -464,7 +449,7 @@ export const VariableDataTable = ({ const dataTableHandleAddition = () => { handleRowUpdateAction({ actionType: VariableDataTableActionType.ADD_ROW, - actionValue: rows.length, + actionValue: Math.floor(new Date().valueOf() * Math.random()), }) } @@ -548,22 +533,7 @@ export const VariableDataTable = ({ }) } - const validationSchema: DynamicDataTableProps['validationSchema'] = ( - _, - key, - { id }, - ) => { - if (key === 'val') { - const index = rows.findIndex((row) => row.id === id) - if (index > -1 && ioVariablesError[index]) { - const { isValid, message } = ioVariablesError[index] - return { isValid, errorMessages: [message] } - } - } - - return { isValid: true, errorMessages: [] } - } - + // RENDERERS const actionButtonRenderer = (row: VariableDataRowType) => ( ) - const getActionButtonConfig = (): DynamicDataTableProps['actionButtonConfig'] => { - if (type === PluginVariableType.INPUT) { - return { - renderer: actionButtonRenderer, - key: 'val', - position: 'end', - } - } - return null - } + const variableTrailingCellIcon = (row: VariableDataRowType) => ( + + + + ) - const getTrailingCellIcon = (): DynamicDataTableProps['trailingCellIcon'] => ({ - variable: - isCustomTask && type === PluginVariableType.INPUT - ? (row: VariableDataRowType) => ( - - - - ) - : null, - }) + const trailingCellIcon: DynamicDataTableProps['trailingCellIcon'] = { + variable: isCustomTask && type === PluginVariableType.INPUT ? variableTrailingCellIcon : null, + } return ( @@ -603,13 +561,21 @@ export const VariableDataTable = ({ readOnly={!isCustomTask && type === PluginVariableType.OUTPUT} isAdditionNotAllowed={!isCustomTask} isDeletionNotAllowed={!isCustomTask} - trailingCellIcon={getTrailingCellIcon()} + trailingCellIcon={trailingCellIcon} onRowEdit={dataTableHandleChange} onRowDelete={dataTableHandleDelete} onRowAdd={dataTableHandleAddition} showError - validationSchema={validationSchema} - actionButtonConfig={getActionButtonConfig()} + validationSchema={variableDataTableValidationSchema} + {...(type === PluginVariableType.INPUT + ? { + actionButtonConfig: { + renderer: actionButtonRenderer, + key: 'val', + position: 'end', + }, + } + : {})} /> ) } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 8cc3dc6f95..58842b9488 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -308,7 +308,7 @@ export const getVariableDataTableInitialRows = ({ isCustomTask, emptyRowParams, }: GetVariableDataTableInitialRowsProps): VariableDataRowType[] => - ioVariables.map( + (ioVariables || []).map( ({ name, allowEmptyValue, @@ -504,10 +504,6 @@ export const convertVariableDataTableToFormData = ({ calculateLastStepDetail(false, updatedFormData, activeStageName, selectedTaskIndex) } - if (updatedIOVariables.length === 1 && !updatedIOVariables[0].name && !updatedIOVariables[0].value) { - updatedIOVariables.pop() - } - if (updatedIOVariables.length === 0) { const { conditionDetails } = updatedFormData[activeStageName].steps[selectedTaskIndex].inlineStepDetail for (let i = 0; i < conditionDetails?.length; i++) { diff --git a/src/components/CIPipelineN/VariableDataTable/validationSchema.ts b/src/components/CIPipelineN/VariableDataTable/validationSchema.ts new file mode 100644 index 0000000000..4110e8bc44 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/validationSchema.ts @@ -0,0 +1,43 @@ +import { DynamicDataTableProps } from '@devtron-labs/devtron-fe-common-lib' + +import { PATTERNS } from '@Config/constants' + +import { VariableDataCustomState, VariableDataKeys } from './types' + +export const variableDataTableValidationSchema: DynamicDataTableProps< + VariableDataKeys, + VariableDataCustomState +>['validationSchema'] = (value, key, { data, customState }) => { + const { variableDescription, isVariableRequired } = customState + + const re = new RegExp(PATTERNS.VARIABLE) + + if (key === 'variable') { + const variableValue = !isVariableRequired || data.val.value + + if (!value && !variableValue && !variableDescription) { + return { errorMessages: ['Please complete or remove this variable'], isValid: false } + } + + if (!value) { + return { errorMessages: ['Variable name is required'], isValid: false } + } + + if (!re.test(value)) { + return { errorMessages: [`Invalid name. Only alphanumeric chars and (_) is allowed`], isValid: false } + } + + // TODO: need to confirm this validation from product + // if (availableInputVariables.get(name)) { + // return { errorMessages: ['Variable name should be unique'], isValid: false } + // } + } + + if (key === 'val') { + if (isVariableRequired && !value) { + return { errorMessages: ['Variable value is required'], isValid: false } + } + } + + return { errorMessages: [], isValid: true } +} diff --git a/src/components/app/details/triggerView/CIMaterialModal.tsx b/src/components/app/details/triggerView/CIMaterialModal.tsx index 7a51404aa4..5e1cfe7083 100644 --- a/src/components/app/details/triggerView/CIMaterialModal.tsx +++ b/src/components/app/details/triggerView/CIMaterialModal.tsx @@ -24,6 +24,7 @@ import { Progressing, VisibleModal, stopPropagation, + uploadCIPipelineFile, usePrompt, } from '@devtron-labs/devtron-fe-common-lib' import CIMaterial from './ciMaterial' @@ -44,6 +45,15 @@ export const CIMaterialModal = ({ [props.filteredCIPipelines, props.pipelineId], ) + const uploadFile = ({ file, allowedExtensions, maxUploadSize }) => + uploadCIPipelineFile({ + file, + allowedExtensions, + maxUploadSize, + appId: +props.appId, + ciPipelineId: +props.pipelineId, + }) + usePrompt({ shouldPrompt: isLoading }) useEffect( @@ -84,7 +94,13 @@ export const CIMaterialModal = ({
) : ( - + )}
) diff --git a/src/components/app/details/triggerView/TriggerView.tsx b/src/components/app/details/triggerView/TriggerView.tsx index 4e3491bc3b..d5d673d67b 100644 --- a/src/components/app/details/triggerView/TriggerView.tsx +++ b/src/components/app/details/triggerView/TriggerView.tsx @@ -1130,15 +1130,6 @@ class TriggerView extends Component { }) } - uploadFile: CIMaterialProps['uploadFile'] = ({ file, allowedExtensions, maxUploadSize }) => - uploadCIPipelineFile({ - appId: +this.props.match.params.appId, - ciPipelineId: this.getCINode().parentCiPipeline, - file, - allowedExtensions, - maxUploadSize, - }) - setLoader = (isLoader) => { this.setState({ loader: isLoader, @@ -1236,7 +1227,6 @@ class TriggerView extends Component { closeCIModal={this.closeCIModal} abortController={this.abortCIBuild} resetAbortController={this.resetAbortController} - uploadFile={this.uploadFile} /> diff --git a/src/components/app/details/triggerView/types.ts b/src/components/app/details/triggerView/types.ts index 895dc922e2..81ef82de91 100644 --- a/src/components/app/details/triggerView/types.ts +++ b/src/components/app/details/triggerView/types.ts @@ -684,7 +684,7 @@ export interface RenderCTAType { disableSelection: boolean } -export interface CIMaterialModalProps extends CIMaterialProps { +export interface CIMaterialModalProps extends Omit { closeCIModal: () => void abortController: AbortController resetAbortController: () => void diff --git a/src/components/cdPipeline/CDPipeline.tsx b/src/components/cdPipeline/CDPipeline.tsx index 2007a6ab26..e5ad88411b 100644 --- a/src/components/cdPipeline/CDPipeline.tsx +++ b/src/components/cdPipeline/CDPipeline.tsx @@ -49,7 +49,6 @@ import { ProcessPluginDataReturnType, ResourceKindType, getEnvironmentListMinPublic, - noop, uploadCDPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import { useEffect, useMemo, useRef, useState } from 'react' From df86d535bb476e203ff2b84ab18c7b5759e623e9 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 6 Dec 2024 16:27:04 +0530 Subject: [PATCH 09/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2f15a947a3..bee7b9acc2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-1", + "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-5", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index a252d284d5..a428842b7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-1": - version "1.2.4-beta-1" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-1.tgz#ee3cf0ba6f9af55f6d463d281fcb57e12a56a00e" - integrity sha512-GyCFZ+EP7J0i29I+f+ENR+ZUDY5+vUYh7+AprrU+Hptz3p/96Xc5riDBZJSk6pvHzvbXyhJCsOSE4HmaLedOZA== +"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-5": + version "1.2.4-beta-5" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-5.tgz#61e02e770b9756efc904fefd9739fc3706959ea7" + integrity sha512-59SesiSFaAxP+tePpGTy505GRCbX7cVHO0cg5GM4QOJ8GGBBMdYG9e88pecsZtYpdLc4PYztBwZnCgOssB0Oiw== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From aead12bce926c0082d58bc74e02241baeae8babd Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Sun, 8 Dec 2024 22:14:04 +0530 Subject: [PATCH 10/36] refactor: VariableDataTable - code refactor --- .../VariableDataTable/VariableDataTable.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx index 24ccd7ab64..c65154857e 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx @@ -453,7 +453,7 @@ export const VariableDataTable = ({ }) } - const dataTableHandleChange: DynamicDataTableProps['onRowEdit'] = ( + const dataTableHandleChange: DynamicDataTableProps['onRowEdit'] = async ( updatedRow, headerKey, value, @@ -478,29 +478,29 @@ export const VariableDataTable = ({ rowId: updatedRow.id, }) - uploadFile({ - file: extraData.files, - ...getUploadFileConstraints({ - unit: updatedRow.customState.fileInfo.unit.label as string, - allowedExtensions: updatedRow.customState.fileInfo.allowedExtensions, - maxUploadSize: updatedRow.customState.fileInfo.maxUploadSize, - }), - }) - .then((res) => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, - actionValue: { fileReferenceId: res.id }, - rowId: updatedRow.id, - }) + try { + const { id } = await uploadFile({ + file: extraData.files, + ...getUploadFileConstraints({ + unit: updatedRow.customState.fileInfo.unit.label as string, + allowedExtensions: updatedRow.customState.fileInfo.allowedExtensions, + maxUploadSize: updatedRow.customState.fileInfo.maxUploadSize, + }), }) - .catch(() => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ROW, - actionValue: '', - headerKey, - rowId: updatedRow.id, - }) + + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: id }, + rowId: updatedRow.id, + }) + } catch { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: '', + headerKey, + rowId: updatedRow.id, }) + } } else if (headerKey === 'format' && updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FORMAT_COLUMN, From 05aff25185e96e49ca24f51bf8dbf1d908255dfe Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 9 Dec 2024 15:18:19 +0530 Subject: [PATCH 11/36] refactor: VariableDataTable - update validations, date type formats update, review fixes, code refactor --- src/components/CIPipelineN/CIPipeline.tsx | 9 +- .../CIPipelineN/CreatePluginModal/utils.tsx | 3 +- .../CIPipelineN/TaskDetailComponent.tsx | 14 +-- ...le.tsx => VariableDataTable.component.tsx} | 50 ++++++----- .../VariableDataTablePopupMenu.tsx | 2 +- .../VariableDataTable/constants.ts | 49 +++++++++-- .../CIPipelineN/VariableDataTable/helpers.tsx | 12 --- .../CIPipelineN/VariableDataTable/index.ts | 2 +- .../CIPipelineN/VariableDataTable/types.ts | 27 +++++- .../CIPipelineN/VariableDataTable/utils.tsx | 83 ++++++++++++------ .../VariableDataTable/validationSchema.ts | 85 +++++++++++-------- src/components/ciPipeline/validationRules.ts | 1 + src/css/base.scss | 4 - 13 files changed, 214 insertions(+), 127 deletions(-) rename src/components/CIPipelineN/VariableDataTable/{VariableDataTable.tsx => VariableDataTable.component.tsx} (95%) delete mode 100644 src/components/CIPipelineN/VariableDataTable/helpers.tsx diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index a279d61905..3e1c50fd73 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -785,7 +785,14 @@ export default function CIPipeline({ } const uploadFile: PipelineContext['uploadFile'] = ({ allowedExtensions, file, maxUploadSize }) => - uploadCIPipelineFile({ appId: +appId, ciPipelineId: +ciPipelineId, file, allowedExtensions, maxUploadSize }) + uploadCIPipelineFile({ + appId: +appId, + envId: isJobView ? selectedEnv?.id : null, + ciPipelineId: +ciPipelineId, + file, + allowedExtensions, + maxUploadSize, + }) const contextValue = useMemo( () => ({ diff --git a/src/components/CIPipelineN/CreatePluginModal/utils.tsx b/src/components/CIPipelineN/CreatePluginModal/utils.tsx index 73b6a77846..ab4dd287c2 100644 --- a/src/components/CIPipelineN/CreatePluginModal/utils.tsx +++ b/src/components/CIPipelineN/CreatePluginModal/utils.tsx @@ -156,7 +156,8 @@ const parseInputVariablesIntoCreatePluginPayload = ( valueType: variable.variableType, referenceVariableName: variable.refVariableName, isExposed: true, - // TODO: handle file type here + fileMountDir: variable.fileMountDir, + fileReferenceId: variable.fileReferenceId, })) || [] export const getCreatePluginPayload = ({ diff --git a/src/components/CIPipelineN/TaskDetailComponent.tsx b/src/components/CIPipelineN/TaskDetailComponent.tsx index bfda4e0bc2..2ec2787908 100644 --- a/src/components/CIPipelineN/TaskDetailComponent.tsx +++ b/src/components/CIPipelineN/TaskDetailComponent.tsx @@ -273,12 +273,10 @@ export const TaskDetailComponent = () => {

{selectedStep.stepType === PluginType.INLINE ? ( - <> - - + ) : ( - )}{' '} + )}
{selectedStep[currentStepTypeVariable]?.inputVariables?.length > 0 && ( <> @@ -291,13 +289,7 @@ export const TaskDetailComponent = () => { {formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].scriptType !== ScriptType.CONTAINERIMAGE && ( - <> - - + )} ) : ( diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx similarity index 95% rename from src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx rename to src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index c65154857e..6ee6aaee64 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -1,4 +1,4 @@ -import { useContext, useState, useEffect, useRef } from 'react' +import { useContext, useState, useEffect, useRef, useMemo } from 'react' import { DynamicDataTable, @@ -6,49 +6,43 @@ import { DynamicDataTableRowDataType, PluginType, RefVariableType, - SelectPickerOptionType, VariableType, VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' import { pipelineContext } from '@Components/workflowEditor/workflowEditor' import { PluginVariableType } from '@Components/ciPipeline/types' -import { ExtendedOptionType } from '@Components/app/types' import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, getVariableDataTableHeaders, VAL_COLUMN_CHOICES_DROPDOWN_LABEL, } from './constants' -import { getSystemVariableIcon } from './helpers' import { HandleRowUpdateActionProps, VariableDataCustomState, VariableDataKeys, VariableDataRowType, VariableDataTableActionType, + VariableDataTableProps, } from './types' import { + checkForSystemVariable, convertVariableDataTableToFormData, getEmptyVariableDataTableRow, + getSystemVariableIcon, getUploadFileConstraints, getValColumnRowProps, getValColumnRowValue, getVariableDataTableInitialRows, } from './utils' -import { variableDataTableValidationSchema } from './validationSchema' +import { getVariableDataTableValidationSchema } from './validationSchema' import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' import { VariableConfigOverlay } from './VariableConfigOverlay' import { ValueConfigOverlay } from './ValueConfigOverlay' -export const VariableDataTable = ({ - type, - isCustomTask = false, -}: { - type: PluginVariableType - isCustomTask?: boolean -}) => { +export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTableProps) => { // CONTEXTS const { inputVariablesListFromPrevStep, @@ -97,6 +91,22 @@ export const VariableDataTable = ({ // STATES const [rows, setRows] = useState([]) + // KEYS FREQUENCY MAP + const keysFrequencyMap: Record = useMemo( + () => + rows.reduce( + (acc, curr) => { + const currentKey = curr.data.variable.value + if (currentKey) { + acc[currentKey] = (acc[currentKey] || 0) + 1 + } + return acc + }, + {} as Record, + ), + [rows], + ) + // REFS const initialRowsSet = useRef('') @@ -340,13 +350,8 @@ export const VariableDataTable = ({ row.id === rowAction.rowId && row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT ) { - const { selectedValue, value } = rowAction.actionValue as { - selectedValue: SelectPickerOptionType & ExtendedOptionType - value: string - } - const isSystemVariable = - !!selectedValue.refVariableStage || - (selectedValue?.variableType && selectedValue.variableType !== RefVariableType.NEW) + const { selectedValue, value } = rowAction.actionValue + const isSystemVariable = checkForSystemVariable(selectedValue) return { ...row, @@ -355,11 +360,9 @@ export const VariableDataTable = ({ val: { ...row.data.val, value: getValColumnRowValue( - row.data.val.value, row.data.format.value as VariableTypeFormat, value, selectedValue, - isSystemVariable, ), props: { ...row.data.val.props, @@ -538,6 +541,9 @@ export const VariableDataTable = ({ @@ -566,7 +572,7 @@ export const VariableDataTable = ({ onRowDelete={dataTableHandleDelete} onRowAdd={dataTableHandleAddition} showError - validationSchema={variableDataTableValidationSchema} + validationSchema={getVariableDataTableValidationSchema({ keysFrequencyMap, pluginVariableType: type })} {...(type === PluginVariableType.INPUT ? { actionButtonConfig: { diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx index e473b75110..a303760f53 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx @@ -42,7 +42,7 @@ export const VariableDataTablePopupMenu = ({ } return ( - + diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts index 1589e5e9c7..972a0547f9 100644 --- a/src/components/CIPipelineN/VariableDataTable/constants.ts +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -67,13 +67,46 @@ export const VAL_COLUMN_BOOL_OPTIONS: SelectPickerOptionType[] = [ ] export const VAL_COLUMN_DATE_OPTIONS: SelectPickerOptionType[] = [ - { label: 'YYYY-MM-DD', value: 'YYYY-MM-DD', description: 'RFC 3339' }, - { label: 'YYYY-MM-DD HH:mm', value: 'YYYY-MM-DD HH:mm', description: 'RFC 3339 with mins' }, - { label: 'YYYY-MM-DD HH:mm:ss', value: 'YYYY-MM-DD HH:mm:ss', description: 'RFC 3339 with secs' }, - { label: 'YYYY-MM-DD HH:mm:ssZ', value: 'YYYY-MM-DD HH:mm:ssZ', description: 'RFC 3339 with secs and TZ' }, - { label: 'YYYY-MM-DDT15Z0700', value: 'ISO', description: 'ISO8601 with hours' }, - { label: 'YYYY-MM-DDTHH:mm:ss[Z]', value: 'YYYY-MM-DDTHH:mm:ss[Z]', description: 'ISO8601 with secs' }, - { label: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', value: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', description: 'ISO8601 with nanosecs' }, + { + label: 'YYYY-MM-DD', + value: 'YYYY-MM-DD', + description: 'RFC 3339', + }, + { + label: 'YYYY-MM-DD HH:mm', + value: 'YYYY-MM-DD HH:mm', + description: 'RFC 3339 with minutes', + }, + { + label: 'YYYY-MM-DD HH:mm:ss', + value: 'YYYY-MM-DD HH:mm:ss', + description: 'RFC 3339 with seconds', + }, + { + label: 'YYYY-MM-DD HH:mm:ssZ', + value: 'YYYY-MM-DD HH:mm:ssZ', + description: 'RFC 3339 with seconds and timezone', + }, + { + label: 'YYYY-MM-DDTHH[Z]', + value: 'YYYY-MM-DDTHH[Z]', + description: 'ISO8601 with hour', + }, + { + label: 'YYYY-MM-DDTHH:mm[Z]', + value: 'YYYY-MM-DDTHH:mm[Z]', + description: 'ISO8601 with minutes', + }, + { + label: 'YYYY-MM-DDTHH:mm:ss[Z]', + value: 'YYYY-MM-DDTHH:mm:ss[Z]', + description: 'ISO8601 with seconds', + }, + { + label: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', + value: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', + description: 'ISO8601 with nanoseconds', + }, ] export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ @@ -87,4 +120,4 @@ export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ }, ] -export const DECIMAL_REGEX = /^\d*\.?\d*$/ +export const DECIMAL_WITH_SCOPE_VARIABLES_REGEX = /^(\d+(\.\d+)?|@{{[a-zA-Z0-9-]+}})$/ diff --git a/src/components/CIPipelineN/VariableDataTable/helpers.tsx b/src/components/CIPipelineN/VariableDataTable/helpers.tsx deleted file mode 100644 index 763a3ca6fe..0000000000 --- a/src/components/CIPipelineN/VariableDataTable/helpers.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Tooltip } from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as Var } from '@Icons/ic-var-initial.svg' -import { TIPPY_VAR_MSG } from '../Constants' - -export const getSystemVariableIcon = () => ( - -
- -
-
-) diff --git a/src/components/CIPipelineN/VariableDataTable/index.ts b/src/components/CIPipelineN/VariableDataTable/index.ts index ee1427a9a9..46eeb15f03 100644 --- a/src/components/CIPipelineN/VariableDataTable/index.ts +++ b/src/components/CIPipelineN/VariableDataTable/index.ts @@ -1 +1 @@ -export * from './VariableDataTable' +export * from './VariableDataTable.component' diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index a43b1ed796..bc739d390e 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -1,6 +1,25 @@ import { PluginVariableType } from '@Components/ciPipeline/types' import { PipelineContext } from '@Components/workflowEditor/types' -import { DynamicDataTableRowType, SelectPickerOptionType, VariableType } from '@devtron-labs/devtron-fe-common-lib' +import { + DynamicDataTableRowType, + RefVariableStageType, + RefVariableType, + SelectPickerOptionType, + VariableType, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' + +export interface VariableDataTableProps { + type: PluginVariableType + isCustomTask?: boolean +} + +export interface VariableDataTableSelectPickerOptionType extends SelectPickerOptionType { + format?: VariableTypeFormat + variableType?: RefVariableType + refVariableStage?: RefVariableStageType + refVariableName?: string +} export type VariableDataKeys = 'variable' | 'format' | 'val' @@ -11,7 +30,7 @@ export type VariableDataCustomState = { askValueAtRuntime: boolean blockCustomValue: boolean // Check for support in the TableRowTypes - selectedValue: Record + selectedValue: VariableDataTableSelectPickerOptionType & Record fileInfo: { id: number mountDir: { @@ -62,7 +81,7 @@ type VariableDataTableActionPropsMap = { [VariableDataTableActionType.UPDATE_VAL_COLUMN]: { actionValue: { value: string - selectedValue: SelectPickerOptionType + selectedValue: VariableDataTableSelectPickerOptionType files: File[] } rowId: string | number @@ -70,7 +89,7 @@ type VariableDataTableActionPropsMap = { [VariableDataTableActionType.UPDATE_FORMAT_COLUMN]: { actionValue: { value: string - selectedValue: SelectPickerOptionType + selectedValue: VariableDataTableSelectPickerOptionType } rowId: string | number } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 58842b9488..872bd8a965 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -1,4 +1,4 @@ -import dayjs from 'dayjs' +import moment from 'moment' import { ConditionType, @@ -9,24 +9,29 @@ import { SelectPickerOptionType, VariableType, VariableTypeFormat, + Tooltip, } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as Var } from '@Icons/ic-var-initial.svg' import { BuildStageVariable } from '@Config/constants' import { PipelineContext } from '@Components/workflowEditor/types' import { PluginVariableType } from '@Components/ciPipeline/types' -import { ExtendedOptionType } from '@Components/app/types' -import { excludeVariables } from '../Constants' +import { excludeVariables, TIPPY_VAR_MSG } from '../Constants' import { - DECIMAL_REGEX, + DECIMAL_WITH_SCOPE_VARIABLES_REGEX, FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_COLUMN_OPTIONS, VAL_COLUMN_BOOL_OPTIONS, VAL_COLUMN_CHOICES_DROPDOWN_LABEL, VAL_COLUMN_DATE_OPTIONS, } from './constants' -import { GetValColumnRowPropsType, GetVariableDataTableInitialRowsProps, VariableDataRowType } from './types' -import { getSystemVariableIcon } from './helpers' +import { + GetValColumnRowPropsType, + GetVariableDataTableInitialRowsProps, + VariableDataTableSelectPickerOptionType, + VariableDataRowType, +} from './types' export const getOptionsForValColumn = ({ inputVariablesListFromPrevStep, @@ -152,6 +157,14 @@ export const getOptionsForValColumn = ({ ] } +export const getSystemVariableIcon = () => ( + +
+ +
+
+) + export const getVariableColumnRowProps = () => { const data: VariableDataRowType['data']['variable'] = { value: '', @@ -248,24 +261,27 @@ export const getValColumnRowProps = ({ } } -export const testValueForNumber = (value: string) => !value || DECIMAL_REGEX.test(value) +export const testValueForNumber = (value: string) => !value || DECIMAL_WITH_SCOPE_VARIABLES_REGEX.test(value) + +export const checkForSystemVariable = (option: VariableDataTableSelectPickerOptionType) => { + const isSystemVariable = + !!option?.refVariableStage || (option?.variableType && option.variableType !== RefVariableType.NEW) + + return isSystemVariable +} export const getValColumnRowValue = ( - currentValue: string, format: VariableTypeFormat, value: string, - selectedValue: SelectPickerOptionType & ExtendedOptionType, - isSystemVariable: boolean, + selectedValue: VariableDataTableSelectPickerOptionType, ) => { - const isNumberFormat = !isSystemVariable && format === VariableTypeFormat.NUMBER - if (isNumberFormat && !testValueForNumber(value)) { - return currentValue - } + const isSystemVariable = checkForSystemVariable(selectedValue) const isDateFormat = !isSystemVariable && value && format === VariableTypeFormat.DATE if (isDateFormat && selectedValue.description) { - const now = dayjs() - return selectedValue.value !== 'ISO' ? now.format(selectedValue.value) : now.toISOString() + const now = moment.now() + const formattedDate = moment(now).format(selectedValue.value) + return formattedDate.replace('Z', moment().format('Z')) } return value @@ -447,23 +463,29 @@ export const convertVariableDataTableToFormData = ({ description: variableDescription, allowEmptyValue: !isVariableRequired, isRuntimeArg: askValueAtRuntime, - valueConstraint: { + } + + if (choices.length) { + variableDetail.valueConstraint = { + ...variableDetail.valueConstraint, choices: choices.map(({ value }) => value), blockCustomValue, - constraint: null, - }, + } } if (fileInfo) { variableDetail.value = data.val.value variableDetail.fileReferenceId = fileInfo.id variableDetail.fileMountDir = fileInfo.mountDir.value - variableDetail.valueConstraint.constraint = { - fileProperty: getUploadFileConstraints({ - allowedExtensions: fileInfo.allowedExtensions, - maxUploadSize: fileInfo.maxUploadSize, - unit: fileInfo.unit.label as string, - }), + variableDetail.valueConstraint = { + ...variableDetail.valueConstraint, + constraint: { + fileProperty: getUploadFileConstraints({ + allowedExtensions: fileInfo.allowedExtensions, + maxUploadSize: fileInfo.maxUploadSize, + unit: fileInfo.unit.label as string, + }), + }, } } @@ -472,22 +494,27 @@ export const convertVariableDataTableToFormData = ({ variableDetail.value = '' variableDetail.variableType = RefVariableType.FROM_PREVIOUS_STEP variableDetail.refVariableStepIndex = selectedValue.refVariableStepIndex - variableDetail.refVariableName = selectedValue.label + variableDetail.refVariableName = selectedValue.label as string variableDetail.format = selectedValue.format variableDetail.refVariableStage = selectedValue.refVariableStage } else if (selectedValue.variableType === RefVariableType.GLOBAL) { variableDetail.variableType = RefVariableType.GLOBAL variableDetail.refVariableStepIndex = 0 - variableDetail.refVariableName = selectedValue.label + variableDetail.refVariableName = selectedValue.label as string variableDetail.format = selectedValue.format variableDetail.value = '' variableDetail.refVariableStage = null } else { variableDetail.variableType = RefVariableType.NEW - variableDetail.value = selectedValue.label + if (data.format.value === VariableTypeFormat.DATE) { + variableDetail.value = data.val.value + } else { + variableDetail.value = selectedValue.label as string + } variableDetail.refVariableName = '' variableDetail.refVariableStage = null } + if (formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.PLUGIN_REF) { variableDetail.format = selectedIOVariable.format } diff --git a/src/components/CIPipelineN/VariableDataTable/validationSchema.ts b/src/components/CIPipelineN/VariableDataTable/validationSchema.ts index 4110e8bc44..dee292e5ac 100644 --- a/src/components/CIPipelineN/VariableDataTable/validationSchema.ts +++ b/src/components/CIPipelineN/VariableDataTable/validationSchema.ts @@ -1,43 +1,60 @@ -import { DynamicDataTableProps } from '@devtron-labs/devtron-fe-common-lib' +import { DynamicDataTableProps, VariableTypeFormat } from '@devtron-labs/devtron-fe-common-lib' +import { PluginVariableType } from '@Components/ciPipeline/types' import { PATTERNS } from '@Config/constants' import { VariableDataCustomState, VariableDataKeys } from './types' - -export const variableDataTableValidationSchema: DynamicDataTableProps< - VariableDataKeys, - VariableDataCustomState ->['validationSchema'] = (value, key, { data, customState }) => { - const { variableDescription, isVariableRequired } = customState - - const re = new RegExp(PATTERNS.VARIABLE) - - if (key === 'variable') { - const variableValue = !isVariableRequired || data.val.value - - if (!value && !variableValue && !variableDescription) { - return { errorMessages: ['Please complete or remove this variable'], isValid: false } - } - - if (!value) { - return { errorMessages: ['Variable name is required'], isValid: false } +import { checkForSystemVariable, testValueForNumber } from './utils' + +export const getVariableDataTableValidationSchema = + ({ + pluginVariableType, + keysFrequencyMap, + }: { + pluginVariableType: PluginVariableType + keysFrequencyMap: Record + }): DynamicDataTableProps['validationSchema'] => + (value, key, { data, customState }) => { + const { variableDescription, isVariableRequired, selectedValue, askValueAtRuntime } = customState + + const re = new RegExp(PATTERNS.VARIABLE) + + if (key === 'variable') { + const variableValue = !isVariableRequired || data.val.value + + if (!value && !variableValue && !variableDescription) { + return { errorMessages: ['Please complete or remove this variable'], isValid: false } + } + + if (!value) { + return { errorMessages: ['Variable name is required'], isValid: false } + } + + if (!re.test(value)) { + return { + errorMessages: [`Invalid name. Only alphanumeric chars and (_) is allowed`], + isValid: false, + } + } + + if ((keysFrequencyMap[value] || 0) > 1) { + return { errorMessages: ['Variable name should be unique'], isValid: false } + } } - if (!re.test(value)) { - return { errorMessages: [`Invalid name. Only alphanumeric chars and (_) is allowed`], isValid: false } + if (pluginVariableType === PluginVariableType.INPUT && key === 'val') { + const checkForVariable = isVariableRequired && !askValueAtRuntime + if (checkForVariable && !value) { + return { errorMessages: ['Variable value is required'], isValid: false } + } + + if (data.format.value === VariableTypeFormat.NUMBER) { + return { + isValid: checkForSystemVariable(selectedValue) || testValueForNumber(value), + errorMessages: ['Variable value is not a number'], + } + } } - // TODO: need to confirm this validation from product - // if (availableInputVariables.get(name)) { - // return { errorMessages: ['Variable name should be unique'], isValid: false } - // } + return { errorMessages: [], isValid: true } } - - if (key === 'val') { - if (isVariableRequired && !value) { - return { errorMessages: ['Variable value is required'], isValid: false } - } - } - - return { errorMessages: [], isValid: true } -} diff --git a/src/components/ciPipeline/validationRules.ts b/src/components/ciPipeline/validationRules.ts index d9cefe6a03..02ad278f5b 100644 --- a/src/components/ciPipeline/validationRules.ts +++ b/src/components/ciPipeline/validationRules.ts @@ -92,6 +92,7 @@ export class ValidationRules { (value['variableType'] === RefVariableType.FROM_PREVIOUS_STEP && value['refVariableStepIndex'] && value['refVariableStage']))) + if (!value['name'] && !variableValue && !value['description']) { return { message: 'Please complete or remove this variable', isValid: false } } diff --git a/src/css/base.scss b/src/css/base.scss index 48964df975..b0337b5d3d 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -3392,10 +3392,6 @@ textarea, } //min width -.min-w-0 { - min-width: 0; -} - .min-w-200 { min-width: 200px; } From 870be7f382ec77eb8c10cfe91a164837f0d6fc21 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 9 Dec 2024 15:27:20 +0530 Subject: [PATCH 12/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bee7b9acc2..5f5874e319 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-5", + "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-11", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index a428842b7b..81c6a72cba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-5": - version "1.2.4-beta-5" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-5.tgz#61e02e770b9756efc904fefd9739fc3706959ea7" - integrity sha512-59SesiSFaAxP+tePpGTy505GRCbX7cVHO0cg5GM4QOJ8GGBBMdYG9e88pecsZtYpdLc4PYztBwZnCgOssB0Oiw== +"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-11": + version "1.2.4-beta-11" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-11.tgz#2354b7bd6280fd1c0fbd846e68136d76cf41941c" + integrity sha512-STpXqlKFLqXwOcNxptRJuWEBclGSMyN/2kZlUNOSbpfVUBCAkVA9Q5/PttC9hAKYCxL1JvinBfbDkwiVujTotQ== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From b95146aa937b4273801d32690b11094f62a64b7c Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 9 Dec 2024 19:11:05 +0530 Subject: [PATCH 13/36] feat: CI/CD Custom Task - remove value validations for SaveAsPlugin & update validations in case of NUMBER format, code refactor --- .../PluginDetailHeader/CreatePluginButton.tsx | 1 + .../CIPipelineN/VariableDataTable/constants.ts | 14 ++++++-------- .../CIPipelineN/VariableDataTable/utils.tsx | 14 +++++++------- .../app/details/triggerView/CIMaterialModal.tsx | 1 + src/components/cdPipeline/cdpipeline.util.tsx | 4 ++-- src/components/ciPipeline/validationRules.ts | 16 +++++++++++++--- src/components/workflowEditor/types.ts | 2 +- src/config/constants.ts | 1 + 8 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx b/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx index 4a18c48f13..1bcc17160b 100644 --- a/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx +++ b/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx @@ -22,6 +22,7 @@ const CreatePluginButton = () => { validateTask( formData[activeStageName].steps[selectedTaskIndex], clonedFormErrorObj[activeStageName].steps[selectedTaskIndex], + true, ) setFormDataErrorObj(clonedFormErrorObj) diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts index 972a0547f9..0da03ee178 100644 --- a/src/components/CIPipelineN/VariableDataTable/constants.ts +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -83,28 +83,28 @@ export const VAL_COLUMN_DATE_OPTIONS: SelectPickerOptionType[] = [ description: 'RFC 3339 with seconds', }, { - label: 'YYYY-MM-DD HH:mm:ssZ', + label: 'YYYY-MM-DD HH:mm:ss-TZ', value: 'YYYY-MM-DD HH:mm:ssZ', description: 'RFC 3339 with seconds and timezone', }, { - label: 'YYYY-MM-DDTHH[Z]', + label: "YYYY-MM-DDTHH'Z'ZZZZ", value: 'YYYY-MM-DDTHH[Z]', description: 'ISO8601 with hour', }, { - label: 'YYYY-MM-DDTHH:mm[Z]', + label: "YYYY-MM-DDTHH:mm'Z'ZZZZ", value: 'YYYY-MM-DDTHH:mm[Z]', description: 'ISO8601 with minutes', }, { - label: 'YYYY-MM-DDTHH:mm:ss[Z]', + label: "YYYY-MM-DDTHH:mm:ss'Z'ZZZZ", value: 'YYYY-MM-DDTHH:mm:ss[Z]', description: 'ISO8601 with seconds', }, { - label: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', - value: 'YYYY-MM-DDTHH:mm:ss.SSS[Z]', + label: "YYYY-MM-DDTHH:mm:ss.SSSSSSSSS'Z'ZZZZ", + value: 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSS[Z]', description: 'ISO8601 with nanoseconds', }, ] @@ -119,5 +119,3 @@ export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ value: 1 / 1024, }, ] - -export const DECIMAL_WITH_SCOPE_VARIABLES_REGEX = /^(\d+(\.\d+)?|@{{[a-zA-Z0-9-]+}})$/ diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 872bd8a965..a5873085cc 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -13,13 +13,12 @@ import { } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as Var } from '@Icons/ic-var-initial.svg' -import { BuildStageVariable } from '@Config/constants' +import { BuildStageVariable, PATTERNS } from '@Config/constants' import { PipelineContext } from '@Components/workflowEditor/types' import { PluginVariableType } from '@Components/ciPipeline/types' import { excludeVariables, TIPPY_VAR_MSG } from '../Constants' import { - DECIMAL_WITH_SCOPE_VARIABLES_REGEX, FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_COLUMN_OPTIONS, VAL_COLUMN_BOOL_OPTIONS, @@ -261,7 +260,7 @@ export const getValColumnRowProps = ({ } } -export const testValueForNumber = (value: string) => !value || DECIMAL_WITH_SCOPE_VARIABLES_REGEX.test(value) +export const testValueForNumber = (value: string) => !value || PATTERNS.NUMBERS_WITH_SCOPE_VARIABLES.test(value) export const checkForSystemVariable = (option: VariableDataTableSelectPickerOptionType) => { const isSystemVariable = @@ -276,12 +275,13 @@ export const getValColumnRowValue = ( selectedValue: VariableDataTableSelectPickerOptionType, ) => { const isSystemVariable = checkForSystemVariable(selectedValue) - const isDateFormat = !isSystemVariable && value && format === VariableTypeFormat.DATE + if (isDateFormat && selectedValue.description) { - const now = moment.now() - const formattedDate = moment(now).format(selectedValue.value) - return formattedDate.replace('Z', moment().format('Z')) + const now = moment() + const formattedDate = now.format(selectedValue.value) + const timezone = now.format('Z').replace(/([+/-])(\d{2})[:.](\d{2})/, '$1$2$3') + return formattedDate.replace('Z', timezone) } return value diff --git a/src/components/app/details/triggerView/CIMaterialModal.tsx b/src/components/app/details/triggerView/CIMaterialModal.tsx index 5e1cfe7083..a855923004 100644 --- a/src/components/app/details/triggerView/CIMaterialModal.tsx +++ b/src/components/app/details/triggerView/CIMaterialModal.tsx @@ -52,6 +52,7 @@ export const CIMaterialModal = ({ maxUploadSize, appId: +props.appId, ciPipelineId: +props.pipelineId, + envId: props.isJobView && props.selectedEnv ? +props.selectedEnv : null, }) usePrompt({ shouldPrompt: isLoading }) diff --git a/src/components/cdPipeline/cdpipeline.util.tsx b/src/components/cdPipeline/cdpipeline.util.tsx index 2a7efa6e1f..d557386387 100644 --- a/src/components/cdPipeline/cdpipeline.util.tsx +++ b/src/components/cdPipeline/cdpipeline.util.tsx @@ -53,7 +53,7 @@ export const ValueContainer = (props) => { ) } -export const validateTask = (taskData: StepType, taskErrorObj: TaskErrorObj): void => { +export const validateTask = (taskData: StepType, taskErrorObj: TaskErrorObj, isSaveAsPlugin = false): void => { const validationRules = new ValidationRules() if (taskData && taskErrorObj) { taskErrorObj.name = validationRules.requiredField(taskData.name) @@ -68,7 +68,7 @@ export const validateTask = (taskData: StepType, taskErrorObj: TaskErrorObj): vo taskErrorObj[currentStepTypeVariable].inputVariables = [] taskData[currentStepTypeVariable].inputVariables?.forEach((element, index) => { taskErrorObj[currentStepTypeVariable].inputVariables.push( - validationRules.inputVariable(element, inputVarMap), + validationRules.inputVariable(element, inputVarMap, isSaveAsPlugin), ) taskErrorObj.isValid = taskErrorObj.isValid && taskErrorObj[currentStepTypeVariable].inputVariables[index].isValid diff --git a/src/components/ciPipeline/validationRules.ts b/src/components/ciPipeline/validationRules.ts index 02ad278f5b..3903675cca 100644 --- a/src/components/ciPipeline/validationRules.ts +++ b/src/components/ciPipeline/validationRules.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { RefVariableType } from '@devtron-labs/devtron-fe-common-lib' +import { RefVariableType, VariableTypeFormat } from '@devtron-labs/devtron-fe-common-lib' import { PATTERNS } from '../../config' import { CHARACTER_ERROR_MIN, @@ -81,10 +81,14 @@ export class ValidationRules { inputVariable = ( value: object, availableInputVariables: Map, + /** disable value check when save as plugin is true */ + isSaveAsPlugin = false, ): { message: string | null; isValid: boolean } => { const re = new RegExp(PATTERNS.VARIABLE) + const numberReg = new RegExp(PATTERNS.NUMBERS_WITH_SCOPE_VARIABLES) const variableValue = value['allowEmptyValue'] || + (!value['allowEmptyValue'] && value['isRuntimeArg']) || (!value['allowEmptyValue'] && value['defaultValue'] && value['defaultValue'] !== '') || (value['variableType'] === RefVariableType.NEW && value['value']) || (value['refVariableName'] && @@ -108,8 +112,14 @@ export class ValidationRules { if (!re.test(value['name'])) { return { message: `Invalid name. Only alphanumeric chars and (_) is allowed`, isValid: false } } - if (!variableValue) { - return { message: 'Variable value is required', isValid: false } + if (!isSaveAsPlugin) { + if (!variableValue) { + return { message: 'Variable value is required', isValid: false } + } + // test for numbers and scope variables when format is "NUMBER". + if (value['format'] === VariableTypeFormat.NUMBER && variableValue && !numberReg.test(value['value'])) { + return { message: 'Variable value is not a number', isValid: false } + } } return { message: null, isValid: true } } diff --git a/src/components/workflowEditor/types.ts b/src/components/workflowEditor/types.ts index 25594cfb72..417d7627dc 100644 --- a/src/components/workflowEditor/types.ts +++ b/src/components/workflowEditor/types.ts @@ -278,7 +278,7 @@ export interface PipelineContext { } formDataErrorObj: PipelineFormDataErrorType setFormDataErrorObj: React.Dispatch> - validateTask: (taskData: StepType, taskErrorobj: TaskErrorObj) => void + validateTask: (taskData: StepType, taskErrorobj: TaskErrorObj, isSaveAsPlugin?: boolean) => void setSelectedTaskIndex: React.Dispatch> validateStage: ( stageName: string, diff --git a/src/config/constants.ts b/src/config/constants.ts index 1844e17147..000d62c112 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -305,6 +305,7 @@ export const PATTERNS = { CUSTOM_TAG: /^(?![.-])([a-zA-Z0-9_.-]*\{[Xx]\}[a-zA-Z0-9_.-]*)(? Date: Mon, 9 Dec 2024 19:25:18 +0530 Subject: [PATCH 14/36] fix: VariableDataTable - number validation fix, value update fix --- .../VariableDataTable/VariableDataTable.component.tsx | 8 ++++---- src/components/CIPipelineN/VariableDataTable/types.ts | 5 +---- src/components/CIPipelineN/VariableDataTable/utils.tsx | 4 ---- src/components/ciPipeline/validationRules.ts | 7 ++++++- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index 6ee6aaee64..b181a61637 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -393,21 +393,21 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ...row.data, format: { ...row.data.format, - value: rowAction.actionValue.value, + value: rowAction.actionValue, }, val: getValColumnRowProps({ ...emptyRowParams, activeStageName, formData, type, - format: rowAction.actionValue.value as VariableTypeFormat, + format: rowAction.actionValue, id: rowAction.rowId as number, }), }, customState: { isVariableRequired: false, variableDescription: '', - selectedValue: rowAction.actionValue.selectedValue, + selectedValue: null, choices: [], blockCustomValue: false, askValueAtRuntime: false, @@ -507,7 +507,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa } else if (headerKey === 'format' && updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FORMAT_COLUMN, - actionValue: { value, selectedValue: extraData.selectedValue }, + actionValue: value as VariableTypeFormat, rowId: updatedRow.id, }) } else { diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index bc739d390e..726c082c42 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -87,10 +87,7 @@ type VariableDataTableActionPropsMap = { rowId: string | number } [VariableDataTableActionType.UPDATE_FORMAT_COLUMN]: { - actionValue: { - value: string - selectedValue: VariableDataTableSelectPickerOptionType - } + actionValue: VariableTypeFormat rowId: string | number } [VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO]: { diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index a5873085cc..8475d51b8a 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -514,10 +514,6 @@ export const convertVariableDataTableToFormData = ({ variableDetail.refVariableName = '' variableDetail.refVariableStage = null } - - if (formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.PLUGIN_REF) { - variableDetail.format = selectedIOVariable.format - } } return variableDetail diff --git a/src/components/ciPipeline/validationRules.ts b/src/components/ciPipeline/validationRules.ts index 3903675cca..5bcf815055 100644 --- a/src/components/ciPipeline/validationRules.ts +++ b/src/components/ciPipeline/validationRules.ts @@ -117,7 +117,12 @@ export class ValidationRules { return { message: 'Variable value is required', isValid: false } } // test for numbers and scope variables when format is "NUMBER". - if (value['format'] === VariableTypeFormat.NUMBER && variableValue && !numberReg.test(value['value'])) { + if ( + value['format'] === VariableTypeFormat.NUMBER && + variableValue && + !!value['value'] && + !numberReg.test(value['value']) + ) { return { message: 'Variable value is not a number', isValid: false } } } From a0c90acb350b173681b100b2e6c10bda8b1b43f1 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 10 Dec 2024 00:35:33 +0530 Subject: [PATCH 15/36] fix: VariableDataTable - payload conversion incorrect data fix --- .../CIPipelineN/VariableDataTable/utils.tsx | 99 ++++++++++--------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 8475d51b8a..6374286d57 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -458,61 +458,70 @@ export const convertVariableDataTableToFormData = ({ const variableDetail: VariableType = { ...selectedIOVariable, + value: data.val.value, format: data.format.value as VariableTypeFormat, name: data.variable.value, - description: variableDescription, - allowEmptyValue: !isVariableRequired, - isRuntimeArg: askValueAtRuntime, + description: type === PluginVariableType.INPUT ? variableDescription : data.val.value, + variableType: selectedIOVariable?.variableType ?? RefVariableType.NEW, } - if (choices.length) { - variableDetail.valueConstraint = { - ...variableDetail.valueConstraint, - choices: choices.map(({ value }) => value), - blockCustomValue, - } - } + if (type === PluginVariableType.INPUT) { + variableDetail.allowEmptyValue = !isVariableRequired + variableDetail.isRuntimeArg = askValueAtRuntime - if (fileInfo) { - variableDetail.value = data.val.value - variableDetail.fileReferenceId = fileInfo.id - variableDetail.fileMountDir = fileInfo.mountDir.value - variableDetail.valueConstraint = { - ...variableDetail.valueConstraint, - constraint: { - fileProperty: getUploadFileConstraints({ - allowedExtensions: fileInfo.allowedExtensions, - maxUploadSize: fileInfo.maxUploadSize, - unit: fileInfo.unit.label as string, - }), - }, + if ( + (variableDetail.format === VariableTypeFormat.STRING || + variableDetail.format === VariableTypeFormat.NUMBER) && + choices.length + ) { + variableDetail.valueConstraint = { + ...variableDetail.valueConstraint, + choices: choices.map(({ value }) => value), + blockCustomValue, + } } - } - if (selectedValue) { - if (selectedValue.refVariableStepIndex) { - variableDetail.value = '' - variableDetail.variableType = RefVariableType.FROM_PREVIOUS_STEP - variableDetail.refVariableStepIndex = selectedValue.refVariableStepIndex - variableDetail.refVariableName = selectedValue.label as string - variableDetail.format = selectedValue.format - variableDetail.refVariableStage = selectedValue.refVariableStage - } else if (selectedValue.variableType === RefVariableType.GLOBAL) { - variableDetail.variableType = RefVariableType.GLOBAL - variableDetail.refVariableStepIndex = 0 - variableDetail.refVariableName = selectedValue.label as string - variableDetail.format = selectedValue.format - variableDetail.value = '' - variableDetail.refVariableStage = null - } else { + if (variableDetail.format === VariableTypeFormat.FILE && fileInfo) { variableDetail.variableType = RefVariableType.NEW - if (data.format.value === VariableTypeFormat.DATE) { - variableDetail.value = data.val.value - } else { - variableDetail.value = selectedValue.label as string - } variableDetail.refVariableName = '' variableDetail.refVariableStage = null + variableDetail.fileReferenceId = fileInfo.id + variableDetail.fileMountDir = fileInfo.mountDir.value + variableDetail.valueConstraint = { + ...variableDetail.valueConstraint, + constraint: { + fileProperty: getUploadFileConstraints({ + allowedExtensions: fileInfo.allowedExtensions, + maxUploadSize: fileInfo.maxUploadSize, + unit: fileInfo.unit.label as string, + }), + }, + } + } + + if (selectedValue) { + if (selectedValue.refVariableStepIndex) { + variableDetail.value = '' + variableDetail.variableType = RefVariableType.FROM_PREVIOUS_STEP + variableDetail.refVariableStepIndex = selectedValue.refVariableStepIndex + variableDetail.refVariableName = selectedValue.label as string + variableDetail.format = selectedValue.format + variableDetail.refVariableStage = selectedValue.refVariableStage + } else if (selectedValue.variableType === RefVariableType.GLOBAL) { + variableDetail.value = '' + variableDetail.variableType = RefVariableType.GLOBAL + variableDetail.refVariableStepIndex = 0 + variableDetail.refVariableName = selectedValue.label as string + variableDetail.format = selectedValue.format + variableDetail.refVariableStage = null + } else { + if (variableDetail.format !== VariableTypeFormat.DATE) { + variableDetail.value = selectedValue.label as string + } + variableDetail.variableType = RefVariableType.NEW + variableDetail.refVariableName = '' + variableDetail.refVariableStage = null + } } } From 1a9df27e4f6c3a4d294d58b0c6137ab06870aa8c Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 10 Dec 2024 11:21:37 +0530 Subject: [PATCH 16/36] refactor: VariableDataTable code refactor, replace PopupMenu with TippyCustomized for overlay --- .../VariableDataTable.component.tsx | 10 ++- .../VariableDataTablePopupMenu.tsx | 79 ++++++++----------- .../CIPipelineN/VariableDataTable/utils.tsx | 34 +++++--- 3 files changed, 64 insertions(+), 59 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index b181a61637..71644b5b6d 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -150,7 +150,12 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ), }, } - : data.val, + : getValColumnRowProps({ + ...emptyRowParams, + valueConstraint: { + choices: choicesOptions.map(({ label }) => label), + }, + }), }, } } @@ -397,9 +402,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }, val: getValColumnRowProps({ ...emptyRowParams, - activeStageName, - formData, - type, format: rowAction.actionValue, id: rowAction.rowId as number, }), diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx index a303760f53..a7f118f204 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx @@ -1,14 +1,7 @@ import { useState } from 'react' -import { - Button, - ButtonStyleType, - ButtonVariantType, - ComponentSizeType, - PopupMenu, -} from '@devtron-labs/devtron-fe-common-lib' +import { TippyCustomized, TippyTheme } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as ICClose } from '@Icons/ic-close.svg' import { ReactComponent as ICSlidersVertical } from '@Icons/ic-sliders-vertical.svg' import { VariableDataTablePopupMenuProps } from './types' @@ -18,7 +11,7 @@ export const VariableDataTablePopupMenu = ({ heading, children, onClose, - disableClose, + disableClose = false, }: VariableDataTablePopupMenuProps) => { // STATES const [visible, setVisible] = useState(false) @@ -31,45 +24,41 @@ export const VariableDataTablePopupMenu = ({ } } - const handleAction = (open: boolean) => { - if (visible !== open) { - if (open) { - setVisible(true) - } else { - handleClose() - } - } + const handleOpen = () => { + setVisible(true) } return ( - - - - - - {visible && ( -
-
-
- {showIcon && } -

{heading}

-
-
- {children} -
- )} -
+ <> + {heading}

} + Icon={showIcon ? ICSlidersVertical : null} + iconSize={16} + additionalContent={
{children}
} + > + +
{visible &&
} - + ) } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 6374286d57..abdd956c7c 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -225,21 +225,35 @@ export const getValColumnRowProps = ({ } } + const optionsForValColumn = getOptionsForValColumn({ + activeStageName, + formData, + globalVariables, + isCdPipeline, + selectedTaskIndex, + inputVariablesListFromPrevStep, + format, + valueConstraint, + }) + + const isOptionsEmpty = optionsForValColumn.every(({ options }) => !options.length) + + if (isOptionsEmpty) { + return { + type: DynamicDataTableRowDataType.TEXT, + value, + props: { + placeholder: 'Enter value or variable', + }, + } + } + return { type: DynamicDataTableRowDataType.SELECT_TEXT, value: variableType === RefVariableType.NEW ? value : refVariableName || '', props: { placeholder: 'Enter value or variable', - options: getOptionsForValColumn({ - activeStageName, - formData, - globalVariables, - isCdPipeline, - selectedTaskIndex, - inputVariablesListFromPrevStep, - format, - valueConstraint, - }), + options: optionsForValColumn, selectPickerProps: { isCreatable: format !== VariableTypeFormat.BOOL && From d8d694608d356ae4321f1995bd4822febf7fec84 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 10 Dec 2024 13:04:32 +0530 Subject: [PATCH 17/36] refactor: VariableDataTable - common code moved to common-lib, feat: add global variables to runtime params --- src/components/CIPipelineN/CIPipeline.tsx | 27 ++-- .../VariableDataTable.component.tsx | 4 +- .../VariableDataTable/constants.ts | 58 +-------- .../CIPipelineN/VariableDataTable/utils.tsx | 116 ++++++++---------- .../app/details/triggerView/cdMaterial.tsx | 3 +- src/components/cdPipeline/CDPipeline.tsx | 17 +-- .../ciPipeline/ciPipeline.service.ts | 11 -- .../GitInfoMaterialCard/GitInfoMaterial.tsx | 1 + src/config/constants.ts | 1 - 9 files changed, 74 insertions(+), 164 deletions(-) diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index 3e1c50fd73..7a3fa84066 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -47,6 +47,7 @@ import { ProcessPluginDataParamsType, ResourceKindType, uploadCIPipelineFile, + getGlobalVariables, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { @@ -59,7 +60,6 @@ import { import { BuildStageVariable, BuildTabText, JobPipelineTabText, TriggerType, URLS, ViewType } from '../../config' import { deleteCIPipeline, - getGlobalVariable, getInitData, getInitDataWithCIPipeline, saveCIPipeline, @@ -121,7 +121,7 @@ export default function CIPipeline({ const [apiInProgress, setApiInProgress] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const [selectedTaskIndex, setSelectedTaskIndex] = useState(0) - const [globalVariables, setGlobalVariables] = useState<{ label: string; value: string; format: string }[]>([]) + const [globalVariables, setGlobalVariables] = useState([]) const [inputVariablesListFromPrevStep, setInputVariablesListFromPrevStep] = useState<{ preBuildStage: Map[] postBuildStage: Map[] @@ -346,23 +346,12 @@ export default function CIPipeline({ } } - const getGlobalVariables = async (): Promise => { + const callGlobalVariables = async () => { try { - const globalVariablesResponse = await getGlobalVariable(Number(appId)) - const globalVariablesResult = globalVariablesResponse?.result ?? [] - const _globalVariableOptions = globalVariablesResult.map((variable) => { - variable.label = variable.name - variable.value = variable.name - variable.description = variable.description || '' - variable.variableType = RefVariableType.GLOBAL - delete variable.name - return variable - }) - setGlobalVariables(_globalVariableOptions || []) - } catch (error) { - if (error instanceof ServerErrors && error.code !== 403) { - showError(error) - } + const globalVariableOptions = await getGlobalVariables({ appId: Number(appId) }) + setGlobalVariables(globalVariableOptions) + } catch { + // HANDLED IN SERVICE } } @@ -499,7 +488,7 @@ export default function CIPipeline({ await getEnvironments(0) } } - await getGlobalVariables() + await callGlobalVariables() setPageState(ViewType.FORM) } catch (error) { setPageState(ViewType.ERROR) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index 71644b5b6d..acca5bc4aa 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -6,6 +6,7 @@ import { DynamicDataTableRowDataType, PluginType, RefVariableType, + SystemVariableIcon, VariableType, VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' @@ -30,7 +31,6 @@ import { checkForSystemVariable, convertVariableDataTableToFormData, getEmptyVariableDataTableRow, - getSystemVariableIcon, getUploadFileConstraints, getValColumnRowProps, getValColumnRowValue, @@ -371,7 +371,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ), props: { ...row.data.val.props, - Icon: value && isSystemVariable ? getSystemVariableIcon() : null, + Icon: value && isSystemVariable ? : null, }, }, }, diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts index 0da03ee178..4180b52ba3 100644 --- a/src/components/CIPipelineN/VariableDataTable/constants.ts +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -40,75 +40,27 @@ export const FORMAT_OPTIONS_MAP = { export const FORMAT_COLUMN_OPTIONS: SelectPickerOptionType[] = [ { - label: 'String', + label: FORMAT_OPTIONS_MAP.STRING, value: VariableTypeFormat.STRING, }, { - label: 'Number', + label: FORMAT_OPTIONS_MAP.NUMBER, value: VariableTypeFormat.NUMBER, }, { - label: 'Boolean', + label: FORMAT_OPTIONS_MAP.BOOL, value: VariableTypeFormat.BOOL, }, { - label: 'Date', + label: FORMAT_OPTIONS_MAP.DATE, value: VariableTypeFormat.DATE, }, { - label: 'File', + label: FORMAT_OPTIONS_MAP.FILE, value: VariableTypeFormat.FILE, }, ] -export const VAL_COLUMN_BOOL_OPTIONS: SelectPickerOptionType[] = [ - { label: 'TRUE', value: 'TRUE' }, - { label: 'FALSE', value: 'FALSE' }, -] - -export const VAL_COLUMN_DATE_OPTIONS: SelectPickerOptionType[] = [ - { - label: 'YYYY-MM-DD', - value: 'YYYY-MM-DD', - description: 'RFC 3339', - }, - { - label: 'YYYY-MM-DD HH:mm', - value: 'YYYY-MM-DD HH:mm', - description: 'RFC 3339 with minutes', - }, - { - label: 'YYYY-MM-DD HH:mm:ss', - value: 'YYYY-MM-DD HH:mm:ss', - description: 'RFC 3339 with seconds', - }, - { - label: 'YYYY-MM-DD HH:mm:ss-TZ', - value: 'YYYY-MM-DD HH:mm:ssZ', - description: 'RFC 3339 with seconds and timezone', - }, - { - label: "YYYY-MM-DDTHH'Z'ZZZZ", - value: 'YYYY-MM-DDTHH[Z]', - description: 'ISO8601 with hour', - }, - { - label: "YYYY-MM-DDTHH:mm'Z'ZZZZ", - value: 'YYYY-MM-DDTHH:mm[Z]', - description: 'ISO8601 with minutes', - }, - { - label: "YYYY-MM-DDTHH:mm:ss'Z'ZZZZ", - value: 'YYYY-MM-DDTHH:mm:ss[Z]', - description: 'ISO8601 with seconds', - }, - { - label: "YYYY-MM-DDTHH:mm:ss.SSSSSSSSS'Z'ZZZZ", - value: 'YYYY-MM-DDTHH:mm:ss.SSSSSSSSS[Z]', - description: 'ISO8601 with nanoseconds', - }, -] - export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ { label: 'KB', diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index abdd956c7c..fd3b50ad11 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -1,30 +1,24 @@ -import moment from 'moment' - import { ConditionType, DynamicDataTableRowDataType, + getGoLangFormattedDateWithTimezone, + IO_VARIABLES_VALUE_COLUMN_BOOL_OPTIONS, + IO_VARIABLES_VALUE_COLUMN_DATE_OPTIONS, PluginType, RefVariableStageType, RefVariableType, SelectPickerOptionType, + SystemVariableIcon, VariableType, VariableTypeFormat, - Tooltip, } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as Var } from '@Icons/ic-var-initial.svg' import { BuildStageVariable, PATTERNS } from '@Config/constants' import { PipelineContext } from '@Components/workflowEditor/types' import { PluginVariableType } from '@Components/ciPipeline/types' -import { excludeVariables, TIPPY_VAR_MSG } from '../Constants' -import { - FILE_UPLOAD_SIZE_UNIT_OPTIONS, - FORMAT_COLUMN_OPTIONS, - VAL_COLUMN_BOOL_OPTIONS, - VAL_COLUMN_CHOICES_DROPDOWN_LABEL, - VAL_COLUMN_DATE_OPTIONS, -} from './constants' +import { excludeVariables } from '../Constants' +import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_COLUMN_OPTIONS, VAL_COLUMN_CHOICES_DROPDOWN_LABEL } from './constants' import { GetValColumnRowPropsType, GetVariableDataTableInitialRowsProps, @@ -51,18 +45,22 @@ export const getOptionsForValColumn = ({ | 'isCdPipeline' > & Pick) => { + const isBuildStagePostBuild = activeStageName === BuildStageVariable.PostBuild + const previousStepVariables = [] + const preBuildStageVariables = [] + const defaultValues = (valueConstraint?.choices || []).map>((value) => ({ label: value, value, })) if (format === VariableTypeFormat.BOOL) { - defaultValues.push(...VAL_COLUMN_BOOL_OPTIONS) + defaultValues.push(...IO_VARIABLES_VALUE_COLUMN_BOOL_OPTIONS) } if (format === VariableTypeFormat.DATE) { - defaultValues.push(...VAL_COLUMN_DATE_OPTIONS) + defaultValues.push(...IO_VARIABLES_VALUE_COLUMN_DATE_OPTIONS) } if (format) @@ -77,8 +75,7 @@ export const getOptionsForValColumn = ({ }) } - if (activeStageName === BuildStageVariable.PostBuild) { - const preBuildStageVariables = [] + if (isBuildStagePostBuild) { const preBuildTaskLength = formData[BuildStageVariable.PreBuild]?.steps?.length if (preBuildTaskLength >= 1 && !isCdPipeline) { if (inputVariablesListFromPrevStep[BuildStageVariable.PreBuild].length > 0) { @@ -116,25 +113,17 @@ export const getOptionsForValColumn = ({ } } } + } - return [ - { - label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, - options: defaultValues, - }, - { - label: 'From Pre-build Stage', - options: preBuildStageVariables, - }, - { - label: 'From Post-build Stage', - options: previousStepVariables, - }, - { - label: 'System variables', - options: globalVariables, - }, - ] + const isOptionsEmpty = + !defaultValues.length && + (isBuildStagePostBuild + ? !preBuildStageVariables.length && !previousStepVariables.length + : !previousStepVariables.length) && + !globalVariables.length + + if (isOptionsEmpty) { + return [] } return [ @@ -142,28 +131,36 @@ export const getOptionsForValColumn = ({ label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, options: defaultValues, }, - { - label: 'From Previous Steps', - options: previousStepVariables, - }, + ...(isBuildStagePostBuild + ? [ + { + label: 'From Pre-build Stage', + options: preBuildStageVariables, + }, + { + label: 'From Post-build Stage', + options: previousStepVariables, + }, + ] + : [ + { + label: 'From Previous Steps', + options: previousStepVariables, + }, + ]), { label: 'System variables', - options: globalVariables.filter( - (variable) => - (isCdPipeline && variable.stageType !== 'post-cd') || !excludeVariables.includes(variable.value), - ), + options: isBuildStagePostBuild + ? globalVariables + : globalVariables.filter( + (variable) => + (isCdPipeline && variable.stageType !== 'post-cd') || + !excludeVariables.includes(variable.value), + ), }, ] } -export const getSystemVariableIcon = () => ( - -
- -
-
-) - export const getVariableColumnRowProps = () => { const data: VariableDataRowType['data']['variable'] = { value: '', @@ -236,9 +233,7 @@ export const getValColumnRowProps = ({ valueConstraint, }) - const isOptionsEmpty = optionsForValColumn.every(({ options }) => !options.length) - - if (isOptionsEmpty) { + if (!optionsForValColumn.length) { return { type: DynamicDataTableRowDataType.TEXT, value, @@ -260,9 +255,9 @@ export const getValColumnRowProps = ({ (!valueConstraint?.choices?.length || !valueConstraint.blockCustomValue), }, Icon: - refVariableStage || (variableType && variableType !== RefVariableType.NEW) - ? getSystemVariableIcon() - : null, + refVariableStage || (variableType && variableType !== RefVariableType.NEW) ? ( + + ) : null, }, } } @@ -291,14 +286,7 @@ export const getValColumnRowValue = ( const isSystemVariable = checkForSystemVariable(selectedValue) const isDateFormat = !isSystemVariable && value && format === VariableTypeFormat.DATE - if (isDateFormat && selectedValue.description) { - const now = moment() - const formattedDate = now.format(selectedValue.value) - const timezone = now.format('Z').replace(/([+/-])(\d{2})[:.](\d{2})/, '$1$2$3') - return formattedDate.replace('Z', timezone) - } - - return value + return isDateFormat ? getGoLangFormattedDateWithTimezone(selectedValue.value) : value } export const getEmptyVariableDataTableRow = (params: GetValColumnRowPropsType): VariableDataRowType => { diff --git a/src/components/app/details/triggerView/cdMaterial.tsx b/src/components/app/details/triggerView/cdMaterial.tsx index 5ecb36deeb..434c803bb0 100644 --- a/src/components/app/details/triggerView/cdMaterial.tsx +++ b/src/components/app/details/triggerView/cdMaterial.tsx @@ -1547,11 +1547,12 @@ const CDMaterial = ({ ) : ( )} diff --git a/src/components/cdPipeline/CDPipeline.tsx b/src/components/cdPipeline/CDPipeline.tsx index e5ad88411b..5178b21cac 100644 --- a/src/components/cdPipeline/CDPipeline.tsx +++ b/src/components/cdPipeline/CDPipeline.tsx @@ -50,6 +50,7 @@ import { ResourceKindType, getEnvironmentListMinPublic, uploadCDPipelineFile, + getGlobalVariables, } from '@devtron-labs/devtron-fe-common-lib' import { useEffect, useMemo, useRef, useState } from 'react' import { Redirect, Route, Switch, useParams, useRouteMatch } from 'react-router-dom' @@ -76,7 +77,6 @@ import { import { Sidebar } from '../CIPipelineN/Sidebar' import DeleteCDNode from './DeleteCDNode' import { PreBuild } from '../CIPipelineN/PreBuild' -import { getGlobalVariable } from '../ciPipeline/ciPipeline.service' import { ValidationRules } from '../ciPipeline/validationRules' import './cdPipeline.scss' import { @@ -356,10 +356,10 @@ export default function CDPipeline({ const getInit = () => { Promise.all([ getDeploymentStrategyList(appId), - getGlobalVariable(Number(appId), true), + getGlobalVariables({ appId: Number(appId), isCD: true }), getDockerRegistryMinAuth(appId, true), ]) - .then(([pipelineStrategyResponse, envResponse, dockerResponse]) => { + .then(([pipelineStrategyResponse, globalVariablesOptions, dockerResponse]) => { const strategies = pipelineStrategyResponse.result.pipelineStrategy || [] const dockerRegistries = dockerResponse.result || [] const _allStrategies = {} @@ -385,16 +385,7 @@ export default function CDPipeline({ } } - const _globalVariableOptions = envResponse.result?.map((variable) => { - variable.label = variable.name - variable.value = variable.name - variable.description = variable.description || '' - variable.variableType = RefVariableType.GLOBAL - delete variable.name - return variable - }) - - setGlobalVariables(_globalVariableOptions || []) + setGlobalVariables(globalVariablesOptions) setDockerRegistries(dockerRegistries) }) .catch((error: ServerErrors) => { diff --git a/src/components/ciPipeline/ciPipeline.service.ts b/src/components/ciPipeline/ciPipeline.service.ts index e16350fc51..6008a6f949 100644 --- a/src/components/ciPipeline/ciPipeline.service.ts +++ b/src/components/ciPipeline/ciPipeline.service.ts @@ -24,8 +24,6 @@ import { RefVariableType, PipelineBuildStageType, VariableTypeFormat, - getIsRequestAborted, - showError, } from '@devtron-labs/devtron-fe-common-lib' import { Routes, SourceTypeMap, TriggerType, ViewType } from '../../config' import { getSourceConfig, getWebhookDataMetaConfig } from '../../services/service' @@ -598,12 +596,3 @@ function createCurlRequest(externalCiConfig): string { const curl = `curl -X POST -H 'Content-type: application/json' --data '${json}' ${url}/${externalCiConfig.accessKey}` return curl } - -export async function getGlobalVariable(appId: number, isCD?: boolean): Promise { - let variableList = [] - await get(`${Routes.GLOBAL_VARIABLES}?appId=${appId}`).then((response) => { - variableList = response.result?.filter((item) => (isCD ? item.stageType !== 'ci' : item.stageType === 'ci')) - }) - - return { result: variableList } -} diff --git a/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx b/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx index e7706d960d..d308bd632d 100644 --- a/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx +++ b/src/components/common/helpers/GitInfoMaterialCard/GitInfoMaterial.tsx @@ -371,6 +371,7 @@ export const GitInfoMaterial = ({ Date: Tue, 10 Dec 2024 16:28:44 +0530 Subject: [PATCH 18/36] feat: VariableDataTable - choices validations update, filter global variables based on format --- .../VariableDataTable/ValueConfigOverlay.tsx | 40 ++++--- .../VariableConfigOverlay.tsx | 2 +- .../VariableDataTable.component.tsx | 109 +++++++++++++----- .../VariableDataTable/constants.ts | 8 +- .../CIPipelineN/VariableDataTable/types.ts | 3 +- .../CIPipelineN/VariableDataTable/utils.tsx | 64 +++++----- src/css/base.scss | 6 + 7 files changed, 157 insertions(+), 75 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx index 09d1663ea5..68dc58b6cb 100644 --- a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx @@ -45,16 +45,19 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay const handleChoiceChange = (choiceId: number) => (e: ChangeEvent) => { const choiceValue = e.target.value - if (isFormatNumber && !testValueForNumber(choiceValue)) { - return - } handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_CHOICES, rowId, actionValue: (currentChoices) => currentChoices.map((choice) => - choice.id === choiceId ? { id: choiceId, value: choiceValue } : choice, + choice.id === choiceId + ? { + id: choiceId, + value: choiceValue, + error: isFormatNumber && !testValueForNumber(choiceValue) ? 'Choice is not a number' : '', + } + : choice, ), }) } @@ -215,7 +218,7 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay />
- {choices.map(({ id, value }) => ( + {choices.map(({ id, value, error }) => (
-
))} @@ -282,7 +288,7 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay className="w-200" placement="bottom-start" content={ -
+

Allow custom input

Allow entering any value other than provided choices

@@ -304,7 +310,7 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay className="w-200" placement="bottom-start" content={ -
+

Ask value at runtime

Value can be provided at runtime. Entered value will be pre-filled as default diff --git a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx index 8527ff22a3..62a8d9c5a8 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx @@ -69,7 +69,7 @@ export const VariableConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOver className="w-200" placement="bottom-start" content={ -

+

Value is required

Get this tooltip from Utkarsh

diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index acca5bc4aa..550d33ee05 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -1,4 +1,5 @@ import { useContext, useState, useEffect, useRef, useMemo } from 'react' +import Tippy from '@tippyjs/react' import { DynamicDataTable, @@ -11,14 +12,11 @@ import { VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as Info } from '@Icons/info-filled.svg' import { pipelineContext } from '@Components/workflowEditor/workflowEditor' import { PluginVariableType } from '@Components/ciPipeline/types' -import { - FILE_UPLOAD_SIZE_UNIT_OPTIONS, - getVariableDataTableHeaders, - VAL_COLUMN_CHOICES_DROPDOWN_LABEL, -} from './constants' +import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, getVariableDataTableHeaders } from './constants' import { HandleRowUpdateActionProps, VariableDataCustomState, @@ -31,6 +29,7 @@ import { checkForSystemVariable, convertVariableDataTableToFormData, getEmptyVariableDataTableRow, + getOptionsForValColumn, getUploadFileConstraints, getValColumnRowProps, getValColumnRowValue, @@ -125,11 +124,13 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: updatedRows = updatedRows.map((row) => { const { id, data, customState } = row - const choicesOptions = customState.choices - .filter(({ value }) => !!value) - .map(({ value }) => ({ label: value, value })) + // FILTERING EMPTY CHOICE VALUES + const choicesOptions = customState.choices.filter(({ value }) => !!value) + + // RESETTING TO DEFAULT STATE IF CHOICES ARE EMPTY + const blockCustomValue = choicesOptions.length ? row.customState.blockCustomValue : false - if (id === rowAction.rowId && choicesOptions.length > 0) { + if (id === rowAction.rowId) { return { ...row, data: { @@ -140,23 +141,36 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ...data.val, props: { ...data.val.props, - options: data.val.props.options.map((option) => - option.label === VAL_COLUMN_CHOICES_DROPDOWN_LABEL - ? { - label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, - options: choicesOptions, - } - : option, - ), + options: getOptionsForValColumn({ + activeStageName, + format: row.data.format.value as VariableTypeFormat, + formData, + globalVariables, + selectedTaskIndex, + inputVariablesListFromPrevStep, + isCdPipeline, + valueConstraint: { + blockCustomValue, + choices: choicesOptions.map(({ value }) => value), + }, + }), }, } : getValColumnRowProps({ ...emptyRowParams, + value: data.val.value, + format: data.format.value as VariableTypeFormat, valueConstraint: { - choices: choicesOptions.map(({ label }) => label), + blockCustomValue, + choices: choicesOptions.map(({ value }) => value), }, }), }, + customState: { + ...row.customState, + blockCustomValue, + choices: choicesOptions, + }, } } @@ -191,11 +205,26 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ...row.data.val, props: { ...row.data.val.props, + options: getOptionsForValColumn({ + activeStageName, + format: row.data.format.value as VariableTypeFormat, + formData, + globalVariables, + selectedTaskIndex, + inputVariablesListFromPrevStep, + isCdPipeline, + valueConstraint: { + blockCustomValue: rowAction.actionValue, + choices: row.customState.choices.map( + ({ value }) => value, + ), + }, + }), selectPickerProps: { isCreatable: row.data.format.value !== VariableTypeFormat.BOOL && row.data.format.value !== VariableTypeFormat.DATE && - !row.customState?.blockCustomValue, + !rowAction.actionValue, }, }, }, @@ -544,21 +573,49 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa heading={row.data.variable.value || 'Value configuration'} onClose={onActionButtonPopupClose(row.id)} disableClose={ - row.data.format.value === VariableTypeFormat.FILE && !!row.customState.fileInfo.mountDir.error + (row.data.format.value === VariableTypeFormat.FILE && !!row.customState.fileInfo.mountDir.error) || + (row.data.format.value === VariableTypeFormat.NUMBER && + row.customState.choices.some(({ error }) => !!error)) } > ) - const variableTrailingCellIcon = (row: VariableDataRowType) => ( - - - - ) + const variableTrailingCellIcon = (row: VariableDataRowType) => + isCustomTask && type === PluginVariableType.INPUT ? ( + + + + ) : null + + const valTrailingCellIcon = (row: VariableDataRowType) => + row.data.format.value === VariableTypeFormat.FILE ? ( + +

File mount path

+

+ {row.customState.fileInfo.mountDir.value} +
+
+ Ensure the uploaded file name is unique to avoid conflicts or overrides. +

+
+ } + > +
+ +
+ + ) : null const trailingCellIcon: DynamicDataTableProps['trailingCellIcon'] = { - variable: isCustomTask && type === PluginVariableType.INPUT ? variableTrailingCellIcon : null, + variable: variableTrailingCellIcon, + val: valTrailingCellIcon, } return ( diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts index 4180b52ba3..67e854b055 100644 --- a/src/components/CIPipelineN/VariableDataTable/constants.ts +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -28,7 +28,13 @@ export const getVariableDataTableHeaders = ( }, ] -export const VAL_COLUMN_CHOICES_DROPDOWN_LABEL = 'Default values' +export const VAL_COLUMN_DROPDOWN_LABEL = { + CHOICES: 'Default values', + SYSTEM_VARIABLES: 'System variables', + PRE_BUILD_STAGE: 'From Pre-build Stage', + POST_BUILD_STAGE: 'From Post-build Stage', + PREVIOUS_STEPS: 'From Previous Steps', +} export const FORMAT_OPTIONS_MAP = { [VariableTypeFormat.STRING]: 'String', diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index 726c082c42..3588af0fd4 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -26,10 +26,9 @@ export type VariableDataKeys = 'variable' | 'format' | 'val' export type VariableDataCustomState = { variableDescription: string isVariableRequired: boolean - choices: { id: number; value: string }[] + choices: { id: number; value: string; error: string }[] askValueAtRuntime: boolean blockCustomValue: boolean - // Check for support in the TableRowTypes selectedValue: VariableDataTableSelectPickerOptionType & Record fileInfo: { id: number diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index fd3b50ad11..6189ec3097 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -18,7 +18,7 @@ import { PipelineContext } from '@Components/workflowEditor/types' import { PluginVariableType } from '@Components/ciPipeline/types' import { excludeVariables } from '../Constants' -import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_COLUMN_OPTIONS, VAL_COLUMN_CHOICES_DROPDOWN_LABEL } from './constants' +import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_COLUMN_OPTIONS, VAL_COLUMN_DROPDOWN_LABEL } from './constants' import { GetValColumnRowPropsType, GetVariableDataTableInitialRowsProps, @@ -115,12 +115,14 @@ export const getOptionsForValColumn = ({ } } + const filteredGlobalVariablesBasedOnFormat = globalVariables.filter((variable) => variable.format === format) + const isOptionsEmpty = !defaultValues.length && (isBuildStagePostBuild ? !preBuildStageVariables.length && !previousStepVariables.length : !previousStepVariables.length) && - !globalVariables.length + !filteredGlobalVariablesBasedOnFormat.length if (isOptionsEmpty) { return [] @@ -128,36 +130,40 @@ export const getOptionsForValColumn = ({ return [ { - label: VAL_COLUMN_CHOICES_DROPDOWN_LABEL, + label: VAL_COLUMN_DROPDOWN_LABEL.CHOICES, options: defaultValues, }, - ...(isBuildStagePostBuild + ...(!valueConstraint?.blockCustomValue ? [ + ...(isBuildStagePostBuild + ? [ + { + label: VAL_COLUMN_DROPDOWN_LABEL.PRE_BUILD_STAGE, + options: preBuildStageVariables, + }, + { + label: VAL_COLUMN_DROPDOWN_LABEL.POST_BUILD_STAGE, + options: previousStepVariables, + }, + ] + : [ + { + label: VAL_COLUMN_DROPDOWN_LABEL.PREVIOUS_STEPS, + options: previousStepVariables, + }, + ]), { - label: 'From Pre-build Stage', - options: preBuildStageVariables, - }, - { - label: 'From Post-build Stage', - options: previousStepVariables, + label: VAL_COLUMN_DROPDOWN_LABEL.SYSTEM_VARIABLES, + options: isBuildStagePostBuild + ? filteredGlobalVariablesBasedOnFormat + : filteredGlobalVariablesBasedOnFormat.filter( + (variable) => + (isCdPipeline && variable.stageType !== 'post-cd') || + !excludeVariables.includes(variable.value), + ), }, ] - : [ - { - label: 'From Previous Steps', - options: previousStepVariables, - }, - ]), - { - label: 'System variables', - options: isBuildStagePostBuild - ? globalVariables - : globalVariables.filter( - (variable) => - (isCdPipeline && variable.stageType !== 'post-cd') || - !excludeVariables.includes(variable.value), - ), - }, + : []), ] } @@ -351,8 +357,9 @@ export const getVariableDataTableInitialRows = ({ value: name, required: isInputVariableRequired, disabled: !isCustomTask, - showTooltip: !isCustomTask && !!description, - tooltipText: description, + tooltip: { + content: !isCustomTask && description, + }, }, format: getFormatColumnRowProps({ format, isCustomTask }), val: getValColumnRowProps({ @@ -373,6 +380,7 @@ export const getVariableDataTableInitialRows = ({ choices: (valueConstraint?.choices || []).map((choiceValue, index) => ({ id: index, value: choiceValue, + error: '', })), askValueAtRuntime: isRuntimeArg ?? false, blockCustomValue: valueConstraint?.blockCustomValue ?? false, diff --git a/src/css/base.scss b/src/css/base.scss index b0337b5d3d..7650eb410b 100644 --- a/src/css/base.scss +++ b/src/css/base.scss @@ -1531,6 +1531,12 @@ button.anchor { line-height: 1.6; border: 1px solid rgba(255, 255, 255, 0.4); background-color: var(--N900); + + &.no-content-padding { + .tippy-content { + padding: 0; + } + } } .tippy-box.default-white { From 4889c59fa649665ab69fe5a81b55ca97ac881851 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 10 Dec 2024 16:45:18 +0530 Subject: [PATCH 19/36] refactor: TaskDetailComponent - add heading for IO Variables --- src/components/CIPipelineN/TaskDetailComponent.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/CIPipelineN/TaskDetailComponent.tsx b/src/components/CIPipelineN/TaskDetailComponent.tsx index 2ec2787908..c0221bf9e4 100644 --- a/src/components/CIPipelineN/TaskDetailComponent.tsx +++ b/src/components/CIPipelineN/TaskDetailComponent.tsx @@ -273,7 +273,10 @@ export const TaskDetailComponent = () => {

{selectedStep.stepType === PluginType.INLINE ? ( - +
+

Input variables

+ +
) : ( )} @@ -289,7 +292,14 @@ export const TaskDetailComponent = () => { {formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].scriptType !== ScriptType.CONTAINERIMAGE && ( - +
+

Output variables

+ +
)} ) : ( From 3eebba5e080b78d12dc4070d1d505e1475f87858 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Tue, 10 Dec 2024 16:46:17 +0530 Subject: [PATCH 20/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5f5874e319..ac3e24581e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-11", + "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-13", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index 81c6a72cba..509e08b6de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-11": - version "1.2.4-beta-11" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-11.tgz#2354b7bd6280fd1c0fbd846e68136d76cf41941c" - integrity sha512-STpXqlKFLqXwOcNxptRJuWEBclGSMyN/2kZlUNOSbpfVUBCAkVA9Q5/PttC9hAKYCxL1JvinBfbDkwiVujTotQ== +"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-13": + version "1.2.4-beta-13" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-13.tgz#f6ed73388e8d0dc12b5757189cd3c211b9e69adb" + integrity sha512-WK76lSq3Cm3pW8f7vtTCXDgS4LQC+seFjkBHFEdqFmnnK56YHCizsztzhj6iCt6V+25wtlGmspDeh201ScleBw== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From 3f21fc302a771cf2a54e963e35c15c621bb66f76 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Thu, 12 Dec 2024 09:18:31 +0530 Subject: [PATCH 21/36] feat: VariableDataTable - CI/CD Pipeline - validations functionality update --- src/components/CIPipelineN/CIPipeline.tsx | 1 + .../PluginDetailHeader/CreatePluginButton.tsx | 2 +- .../ValueConfigFileTippy.tsx | 26 + .../VariableDataTable.component.tsx | 828 ++++++++++-------- .../CIPipelineN/VariableDataTable/types.ts | 40 +- .../CIPipelineN/VariableDataTable/utils.tsx | 90 +- .../VariableDataTable/validationSchema.ts | 60 -- .../VariableDataTable/validations.ts | 84 ++ src/components/cdPipeline/CDPipeline.tsx | 1 + src/components/cdPipeline/cdpipeline.util.tsx | 53 +- src/components/workflowEditor/types.ts | 10 +- 11 files changed, 695 insertions(+), 500 deletions(-) create mode 100644 src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx delete mode 100644 src/components/CIPipelineN/VariableDataTable/validationSchema.ts create mode 100644 src/components/CIPipelineN/VariableDataTable/validations.ts diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index 7a3fa84066..2616fecf83 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -438,6 +438,7 @@ export default function CIPipeline({ if (!_formDataErrorObj[stageName].steps[i]) { _formDataErrorObj[stageName].steps.push({ isValid: true }) } + _formDataErrorObj.triggerValidation = true validateTask(_formData[stageName].steps[i], _formDataErrorObj[stageName].steps[i]) isStageValid = isStageValid && _formDataErrorObj[stageName].steps[i].isValid } diff --git a/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx b/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx index 1bcc17160b..32942928bd 100644 --- a/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx +++ b/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx @@ -22,7 +22,7 @@ const CreatePluginButton = () => { validateTask( formData[activeStageName].steps[selectedTaskIndex], clonedFormErrorObj[activeStageName].steps[selectedTaskIndex], - true, + { isSaveAsPlugin: true }, ) setFormDataErrorObj(clonedFormErrorObj) diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx new file mode 100644 index 0000000000..36cf8c0f9e --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx @@ -0,0 +1,26 @@ +import Tippy from '@tippyjs/react' + +import { ReactComponent as Info } from '@Icons/info-filled.svg' + +export const ValueConfigFileTippy = ({ mountDir }: { mountDir: string }) => ( + +

File mount path

+

+ {mountDir} +
+
+ Ensure the uploaded file name is unique to avoid conflicts or overrides. +

+
+ } + > +
+ +
+ +) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index 550d33ee05..c59d75bc8b 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -1,23 +1,22 @@ import { useContext, useState, useEffect, useRef, useMemo } from 'react' -import Tippy from '@tippyjs/react' import { DynamicDataTable, + DynamicDataTableCellErrorType, DynamicDataTableProps, DynamicDataTableRowDataType, PluginType, RefVariableType, - SystemVariableIcon, VariableType, VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' -import { ReactComponent as Info } from '@Icons/info-filled.svg' import { pipelineContext } from '@Components/workflowEditor/workflowEditor' import { PluginVariableType } from '@Components/ciPipeline/types' import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, getVariableDataTableHeaders } from './constants' import { + GetValColumnRowPropsType, HandleRowUpdateActionProps, VariableDataCustomState, VariableDataKeys, @@ -26,20 +25,20 @@ import { VariableDataTableProps, } from './types' import { - checkForSystemVariable, convertVariableDataTableToFormData, getEmptyVariableDataTableRow, - getOptionsForValColumn, getUploadFileConstraints, getValColumnRowProps, getValColumnRowValue, + getVariableDataTableInitialCellError, getVariableDataTableInitialRows, } from './utils' -import { getVariableDataTableValidationSchema } from './validationSchema' +import { getVariableDataTableCellValidateState, validateVariableDataTable } from './validations' import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' import { VariableConfigOverlay } from './VariableConfigOverlay' import { ValueConfigOverlay } from './ValueConfigOverlay' +import { ValueConfigFileTippy } from './ValueConfigFileTippy' export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTableProps) => { // CONTEXTS @@ -59,7 +58,8 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa } = useContext(pipelineContext) // CONSTANTS - const emptyRowParams = { + const headers = getVariableDataTableHeaders(type) + const defaultRowValColumnParams: GetValColumnRowPropsType = { inputVariablesListFromPrevStep, activeStageName, selectedTaskIndex, @@ -67,14 +67,13 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa globalVariables, isCdPipeline, type, - description: null, format: VariableTypeFormat.STRING, variableType: RefVariableType.NEW, value: '', + description: null, refVariableName: null, refVariableStage: null, valueConstraint: null, - id: 0, } const currentStepTypeVariable = @@ -87,8 +86,12 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' ] + const isTableValid = + formDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].isValid ?? true + // STATES const [rows, setRows] = useState([]) + const [cellError, setCellError] = useState>({}) // KEYS FREQUENCY MAP const keysFrequencyMap: Record = useMemo( @@ -110,380 +113,454 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa const initialRowsSet = useRef('') useEffect(() => { - setRows(getVariableDataTableInitialRows({ emptyRowParams, ioVariables, isCustomTask, type })) + // SETTING INITIAL ROWS & ERROR STATE + const initialRows = getVariableDataTableInitialRows({ + ioVariables, + isCustomTask, + type, + activeStageName, + formData, + globalVariables, + selectedTaskIndex, + inputVariablesListFromPrevStep, + isCdPipeline, + }) + const updatedCellError = getVariableDataTableInitialCellError(initialRows, headers) + + setRows(initialRows) + setCellError(updatedCellError) + initialRowsSet.current = 'set' }, []) + useEffect(() => { + // Validate the table when: + // 1. Rows have been initialized (`initialRowsSet.current` is 'set'). + // 2. Validation is explicitly triggered (`formDataErrorObj.triggerValidation` is true) + // or the table is currently invalid (`!isTableValid` -> this is only triggered on mount) + if (initialRowsSet.current === 'set') { + if (formDataErrorObj.triggerValidation || !isTableValid) { + setCellError( + validateVariableDataTable({ + headers, + rows, + keysFrequencyMap, + pluginVariableType: type, + }), + ) + // Reset the triggerValidation flag after validation is complete. + setFormDataErrorObj((prevState) => ({ + ...prevState, + triggerValidation: false, + })) + } + } + }, [initialRowsSet.current, formDataErrorObj.triggerValidation]) + // METHODS const handleRowUpdateAction = (rowAction: HandleRowUpdateActionProps) => { const { actionType } = rowAction + let updatedRows = rows + const updatedCellError = structuredClone(cellError) - setRows((prevRows) => { - let updatedRows = [...prevRows] - switch (actionType) { - case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: - updatedRows = updatedRows.map((row) => { - const { id, data, customState } = row + switch (actionType) { + case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: + updatedRows = updatedRows.map((row) => { + const { id, data, customState } = row + + if (id === rowAction.rowId) { // FILTERING EMPTY CHOICE VALUES const choicesOptions = customState.choices.filter(({ value }) => !!value) - // RESETTING TO DEFAULT STATE IF CHOICES ARE EMPTY - const blockCustomValue = choicesOptions.length ? row.customState.blockCustomValue : false - - if (id === rowAction.rowId) { - return { - ...row, - data: { - ...data, - val: - data.val.type === DynamicDataTableRowDataType.SELECT_TEXT - ? { - ...data.val, - props: { - ...data.val.props, - options: getOptionsForValColumn({ - activeStageName, - format: row.data.format.value as VariableTypeFormat, - formData, - globalVariables, - selectedTaskIndex, - inputVariablesListFromPrevStep, - isCdPipeline, - valueConstraint: { - blockCustomValue, - choices: choicesOptions.map(({ value }) => value), - }, - }), - }, - } - : getValColumnRowProps({ - ...emptyRowParams, - value: data.val.value, - format: data.format.value as VariableTypeFormat, - valueConstraint: { - blockCustomValue, - choices: choicesOptions.map(({ value }) => value), - }, - }), - }, - customState: { - ...row.customState, - blockCustomValue, - choices: choicesOptions, - }, - } + const blockCustomValue = !!choicesOptions.length && row.customState.blockCustomValue + + const isCurrentValueValid = + !blockCustomValue || + ((!customState.selectedValue || + customState.selectedValue?.variableType === RefVariableType.NEW) && + choicesOptions.some(({ value }) => value === data.val.value)) + + updatedCellError[row.id].val = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + key: 'val', + row, + }) + + return { + ...row, + data: { + ...data, + val: getValColumnRowProps({ + ...defaultRowValColumnParams, + ...(!blockCustomValue && customState.selectedValue + ? { + variableType: customState.selectedValue.variableType, + refVariableName: customState.selectedValue.value, + refVariableStage: customState.selectedValue.refVariableStage, + } + : {}), + value: isCurrentValueValid ? data.val.value : '', + format: data.format.value as VariableTypeFormat, + valueConstraint: { + blockCustomValue, + choices: choicesOptions.map(({ value }) => value), + }, + }), + }, + customState: { + ...customState, + selectedValue: !blockCustomValue ? customState.selectedValue : null, + blockCustomValue, + choices: choicesOptions, + }, } + } - return row - }) - break - - case VariableDataTableActionType.UPDATE_CHOICES: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - customState: { - ...row.customState, - choices: rowAction.actionValue(row.customState.choices), - }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - data: { - ...row.data, - ...(row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT - ? { - val: { - ...row.data.val, - props: { - ...row.data.val.props, - options: getOptionsForValColumn({ - activeStageName, - format: row.data.format.value as VariableTypeFormat, - formData, - globalVariables, - selectedTaskIndex, - inputVariablesListFromPrevStep, - isCdPipeline, - valueConstraint: { - blockCustomValue: rowAction.actionValue, - choices: row.customState.choices.map( - ({ value }) => value, - ), - }, - }), - selectPickerProps: { - isCreatable: - row.data.format.value !== VariableTypeFormat.BOOL && - row.data.format.value !== VariableTypeFormat.DATE && - !rowAction.actionValue, - }, - }, - }, - } - : {}), - }, - customState: { ...row.customState, blockCustomValue: rowAction.actionValue }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { ...row, customState: { ...row.customState, askValueAtRuntime: rowAction.actionValue } } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - customState: { ...row.customState, variableDescription: rowAction.actionValue }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - data: { - ...row.data, - variable: { ...row.data.variable, required: rowAction.actionValue }, - }, - customState: { ...row.customState, isVariableRequired: rowAction.actionValue }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_FILE_MOUNT: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - customState: { - ...row.customState, - fileInfo: { ...row.customState.fileInfo, mountDir: rowAction.actionValue }, - }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - data: - row.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD - ? { - ...row.data, - val: { - ...row.data.val, - props: { - ...row.data.val.props, - fileTypes: rowAction.actionValue.split(','), - }, + return row + }) + break + + case VariableDataTableActionType.UPDATE_CHOICES: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + choices: rowAction.actionValue(row.customState.choices), + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + blockCustomValue: rowAction.actionValue, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME: + updatedRows = updatedRows.map((row) => { + if (row.id === rowAction.rowId) { + return { ...row, customState: { ...row.customState, askValueAtRuntime: rowAction.actionValue } } + } + + return row + }) + break + + case VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { ...row.customState, variableDescription: rowAction.actionValue }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED: + updatedRows = updatedRows.map((row) => { + if (row.id === rowAction.rowId) { + const updatedRow = { + ...row, + data: { + ...row.data, + variable: { ...row.data.variable, required: rowAction.actionValue }, + }, + customState: { ...row.customState, isVariableRequired: rowAction.actionValue }, + } + updatedCellError[row.id].variable = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + key: 'variable', + row: updatedRow, + }) + updatedCellError[row.id].val = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + key: 'val', + row: updatedRow, + }) + + return updatedRow + } + + return row + }) + break + + case VariableDataTableActionType.UPDATE_FILE_MOUNT: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + fileInfo: { ...row.customState.fileInfo, mountDir: rowAction.actionValue }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + data: + row.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD + ? { + ...row.data, + val: { + ...row.data.val, + props: { + ...row.data.val.props, + fileTypes: rowAction.actionValue.split(','), }, - } - : row.data, - customState: { - ...row.customState, - fileInfo: { - ...row.customState.fileInfo, - allowedExtensions: rowAction.actionValue, - }, - }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_FILE_MAX_SIZE: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - customState: { - ...row.customState, - fileInfo: { - ...row.customState.fileInfo, - maxUploadSize: rowAction.actionValue.size, - unit: rowAction.actionValue.unit, - }, + }, + } + : row.data, + customState: { + ...row.customState, + fileInfo: { + ...row.customState.fileInfo, + allowedExtensions: rowAction.actionValue, }, - } - : row, - ) - break - - case VariableDataTableActionType.ADD_ROW: - updatedRows = [ - getEmptyVariableDataTableRow({ ...emptyRowParams, id: rowAction.actionValue }), - ...updatedRows, - ] - break - - case VariableDataTableActionType.DELETE_ROW: - updatedRows = updatedRows.filter((row) => row.id !== rowAction.rowId) - break - - case VariableDataTableActionType.UPDATE_ROW: - updatedRows = rows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - data: { - ...row.data, - [rowAction.headerKey]: { - ...row.data[rowAction.headerKey], - value: rowAction.actionValue, - }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_MAX_SIZE: + updatedRows = updatedRows.map((row) => + row.id === rowAction.rowId + ? { + ...row, + customState: { + ...row.customState, + fileInfo: { + ...row.customState.fileInfo, + maxUploadSize: rowAction.actionValue.size, + unit: rowAction.actionValue.unit, }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - customState: { - ...row.customState, - fileInfo: { - ...row.customState.fileInfo, - id: rowAction.actionValue.fileReferenceId, - }, - }, - } - : row, - ) - break - - case VariableDataTableActionType.UPDATE_VAL_COLUMN: - updatedRows = updatedRows.map((row) => { - if ( - row.id === rowAction.rowId && - row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT - ) { - const { selectedValue, value } = rowAction.actionValue - const isSystemVariable = checkForSystemVariable(selectedValue) - - return { - ...row, - data: { - ...row.data, - val: { - ...row.data.val, - value: getValColumnRowValue( - row.data.format.value as VariableTypeFormat, - value, - selectedValue, - ), - props: { - ...row.data.val.props, - Icon: value && isSystemVariable ? : null, - }, + }, + } + : row, + ) + break + + case VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO: + updatedRows = updatedRows.map((row) => { + if (row.id === rowAction.rowId && row.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD) { + updatedCellError[row.id].val = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + value: rowAction.actionValue.fileName, + key: 'val', + row, + }) + + return { + ...row, + data: { + ...row.data, + val: { + ...row.data.val, + value: rowAction.actionValue.fileName, + props: { + ...row.data.val.props, + isLoading: rowAction.actionValue.isLoading, }, }, - customState: { - ...row.customState, - selectedValue: rowAction.actionValue.selectedValue, + }, + customState: { + ...row.customState, + fileInfo: { + ...row.customState.fileInfo, + id: rowAction.actionValue.fileReferenceId, }, - } + }, } + } - return row - }) - break - - case VariableDataTableActionType.UPDATE_FORMAT_COLUMN: - updatedRows = updatedRows.map((row) => { - if ( - row.id === rowAction.rowId && - row.data.format.type === DynamicDataTableRowDataType.DROPDOWN - ) { - return { - ...row, - data: { - ...row.data, - format: { - ...row.data.format, - value: rowAction.actionValue, + return row + }) + break + + case VariableDataTableActionType.ADD_ROW: + updatedRows = [ + getEmptyVariableDataTableRow({ ...defaultRowValColumnParams, id: rowAction.rowId }), + ...updatedRows, + ] + updatedCellError[rowAction.rowId] = {} + break + + case VariableDataTableActionType.DELETE_ROW: + updatedRows = updatedRows.filter((row) => row.id !== rowAction.rowId) + delete updatedCellError[rowAction.rowId] + break + + case VariableDataTableActionType.UPDATE_ROW: + updatedRows = rows.map((row) => { + if (row.id === rowAction.rowId) { + updatedCellError[row.id][rowAction.headerKey] = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + value: rowAction.actionValue, + key: rowAction.headerKey, + row, + }) + + return { + ...row, + data: { + ...row.data, + [rowAction.headerKey]: { + ...row.data[rowAction.headerKey], + value: rowAction.actionValue, + }, + }, + } + } + return row + }) + break + + case VariableDataTableActionType.UPDATE_VAL_COLUMN: + updatedRows = updatedRows.map((row) => { + if (row.id === rowAction.rowId && row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { + const { selectedValue, value } = rowAction.actionValue + const valColumnRowValue = getValColumnRowValue( + row.data.format.value as VariableTypeFormat, + value, + selectedValue, + ) + + updatedCellError[row.id].val = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + value: valColumnRowValue, + key: 'val', + row, + }) + + return { + ...row, + data: { + ...row.data, + val: getValColumnRowProps({ + ...defaultRowValColumnParams, + value: valColumnRowValue, + ...(!row.customState.blockCustomValue && rowAction.actionValue.selectedValue + ? { + variableType: rowAction.actionValue.selectedValue.variableType, + refVariableName: rowAction.actionValue.selectedValue.value, + refVariableStage: rowAction.actionValue.selectedValue.refVariableStage, + } + : {}), + format: row.data.format.value as VariableTypeFormat, + valueConstraint: { + blockCustomValue: row.customState.blockCustomValue, + choices: row.customState.choices.map((choice) => choice.value), }, - val: getValColumnRowProps({ - ...emptyRowParams, - format: rowAction.actionValue, - id: rowAction.rowId as number, - }), + }), + }, + customState: { + ...row.customState, + selectedValue: rowAction.actionValue.selectedValue, + }, + } + } + + return row + }) + break + + case VariableDataTableActionType.UPDATE_FORMAT_COLUMN: + updatedRows = updatedRows.map((row) => { + if (row.id === rowAction.rowId && row.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { + updatedCellError[row.id].val = getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType: type, + key: 'val', + row, + }) + + return { + ...row, + data: { + ...row.data, + format: { + ...row.data.format, + value: rowAction.actionValue, }, - customState: { - isVariableRequired: false, - variableDescription: '', - selectedValue: null, - choices: [], - blockCustomValue: false, - askValueAtRuntime: false, - fileInfo: { - id: null, - allowedExtensions: '', - maxUploadSize: '', - mountDir: { - value: '/devtroncd', - error: '', - }, - unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], + val: getValColumnRowProps({ + ...defaultRowValColumnParams, + format: rowAction.actionValue, + }), + }, + customState: { + ...row.customState, + selectedValue: null, + choices: [], + blockCustomValue: false, + fileInfo: { + id: null, + allowedExtensions: '', + maxUploadSize: '', + mountDir: { + value: '/devtroncd', + error: '', }, + unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], }, - } + }, } - return row - }) - break - - default: - break - } + } + return row + }) + break - const { updatedFormData, updatedFormDataErrorObj } = convertVariableDataTableToFormData({ - rows: updatedRows, - activeStageName, - formData, - formDataErrorObj, - selectedTaskIndex, - type, - validateTask, - calculateLastStepDetail, - }) - setFormDataErrorObj(updatedFormDataErrorObj) - setFormData(updatedFormData) + default: + break + } - return updatedRows + const { updatedFormData, updatedFormDataErrorObj } = convertVariableDataTableToFormData({ + rows: updatedRows, + cellError: updatedCellError, + activeStageName, + formData, + formDataErrorObj, + selectedTaskIndex, + type, + validateTask, + calculateLastStepDetail, }) + setFormDataErrorObj(updatedFormDataErrorObj) + setFormData(updatedFormData) + + setRows(updatedRows) + setCellError(updatedCellError) } const dataTableHandleAddition = () => { handleRowUpdateAction({ actionType: VariableDataTableActionType.ADD_ROW, - actionValue: Math.floor(new Date().valueOf() * Math.random()), + rowId: Math.floor(new Date().valueOf() * Math.random()), }) } @@ -496,7 +573,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa if (headerKey === 'val' && updatedRow.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_VAL_COLUMN, - actionValue: { value, selectedValue: extraData.selectedValue, files: extraData.files }, + actionValue: { value, selectedValue: extraData.selectedValue }, rowId: updatedRow.id, }) } else if ( @@ -504,16 +581,14 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa updatedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD && extraData.files.length ) { - // TODO: check this merge with UPDATE_FILE_UPLOAD_INFO after loading state handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ROW, - actionValue: value, - headerKey, + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: null, isLoading: true, fileName: value }, rowId: updatedRow.id, }) try { - const { id } = await uploadFile({ + const { id, name } = await uploadFile({ file: extraData.files, ...getUploadFileConstraints({ unit: updatedRow.customState.fileInfo.unit.label as string, @@ -524,14 +599,13 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, - actionValue: { fileReferenceId: id }, + actionValue: { fileReferenceId: id, isLoading: false, fileName: name }, rowId: updatedRow.id, }) } catch { handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_ROW, - actionValue: '', - headerKey, + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: null, isLoading: false, fileName: '' }, rowId: updatedRow.id, }) } @@ -582,47 +656,29 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ) - const variableTrailingCellIcon = (row: VariableDataRowType) => + const getTrailingCellIconForVariableColumn = (row: VariableDataRowType) => isCustomTask && type === PluginVariableType.INPUT ? ( ) : null - const valTrailingCellIcon = (row: VariableDataRowType) => + const getTrailingCellIconForValueColumn = (row: VariableDataRowType) => row.data.format.value === VariableTypeFormat.FILE ? ( - -

File mount path

-

- {row.customState.fileInfo.mountDir.value} -
-
- Ensure the uploaded file name is unique to avoid conflicts or overrides. -

-
- } - > -
- -
- + ) : null const trailingCellIcon: DynamicDataTableProps['trailingCellIcon'] = { - variable: variableTrailingCellIcon, - val: valTrailingCellIcon, + variable: getTrailingCellIconForVariableColumn, + val: getTrailingCellIconForValueColumn, } return ( key={initialRowsSet.current} - headers={getVariableDataTableHeaders(type)} + headers={headers} rows={rows} + cellError={cellError} readOnly={!isCustomTask && type === PluginVariableType.OUTPUT} isAdditionNotAllowed={!isCustomTask} isDeletionNotAllowed={!isCustomTask} @@ -630,8 +686,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa onRowEdit={dataTableHandleChange} onRowDelete={dataTableHandleDelete} onRowAdd={dataTableHandleAddition} - showError - validationSchema={getVariableDataTableValidationSchema({ keysFrequencyMap, pluginVariableType: type })} {...(type === PluginVariableType.INPUT ? { actionButtonConfig: { diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index 3588af0fd4..a386592df3 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -1,6 +1,7 @@ import { PluginVariableType } from '@Components/ciPipeline/types' import { PipelineContext } from '@Components/workflowEditor/types' import { + DynamicDataTableHeaderType, DynamicDataTableRowType, RefVariableStageType, RefVariableType, @@ -24,6 +25,7 @@ export interface VariableDataTableSelectPickerOptionType extends SelectPickerOpt export type VariableDataKeys = 'variable' | 'format' | 'val' export type VariableDataCustomState = { + defaultValue: string variableDescription: string isVariableRequired: boolean choices: { id: number; value: string; error: string }[] @@ -68,7 +70,7 @@ export enum VariableDataTableActionType { } type VariableDataTableActionPropsMap = { - [VariableDataTableActionType.ADD_ROW]: { actionValue: number } + [VariableDataTableActionType.ADD_ROW]: { rowId: number } [VariableDataTableActionType.UPDATE_ROW]: { actionValue: string headerKey: VariableDataKeys @@ -81,7 +83,6 @@ type VariableDataTableActionPropsMap = { actionValue: { value: string selectedValue: VariableDataTableSelectPickerOptionType - files: File[] } rowId: string | number } @@ -90,7 +91,10 @@ type VariableDataTableActionPropsMap = { rowId: string | number } [VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO]: { - actionValue: Pick + actionValue: Pick & { + fileName: string + isLoading: boolean + } rowId: string | number } @@ -162,19 +166,29 @@ export type GetValColumnRowPropsType = Pick< > & Pick< VariableType, - | 'format' - | 'value' - | 'refVariableName' - | 'refVariableStage' - | 'valueConstraint' - | 'description' - | 'variableType' - | 'id' + 'format' | 'value' | 'refVariableName' | 'refVariableStage' | 'valueConstraint' | 'description' | 'variableType' > & { type: PluginVariableType } -export interface GetVariableDataTableInitialRowsProps { +export interface GetVariableDataTableInitialRowsProps + extends Omit< + GetValColumnRowPropsType, + 'description' | 'format' | 'variableType' | 'value' | 'refVariableName' | 'refVariableStage' | 'valueConstraint' + > { ioVariables: VariableType[] type: PluginVariableType isCustomTask: boolean - emptyRowParams: GetValColumnRowPropsType +} + +export interface GetValidateCellProps { + pluginVariableType: PluginVariableType + keysFrequencyMap: Record + key: VariableDataKeys + row: VariableDataRowType + value?: string +} + +export interface ValidateVariableDataTableProps + extends Pick { + rows: VariableDataRowType[] + headers: DynamicDataTableHeaderType[] } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 6189ec3097..3d6df2fdde 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -1,5 +1,7 @@ import { ConditionType, + DynamicDataTableCellErrorType, + DynamicDataTableHeaderType, DynamicDataTableRowDataType, getGoLangFormattedDateWithTimezone, IO_VARIABLES_VALUE_COLUMN_BOOL_OPTIONS, @@ -24,6 +26,7 @@ import { GetVariableDataTableInitialRowsProps, VariableDataTableSelectPickerOptionType, VariableDataRowType, + VariableDataKeys, } from './types' export const getOptionsForValColumn = ({ @@ -115,13 +118,28 @@ export const getOptionsForValColumn = ({ } } - const filteredGlobalVariablesBasedOnFormat = globalVariables.filter((variable) => variable.format === format) + const filteredGlobalVariables = isBuildStagePostBuild + ? globalVariables + : globalVariables.filter( + (variable) => + (isCdPipeline && variable.stageType !== 'post-cd') || !excludeVariables.includes(variable.value), + ) + + const filteredGlobalVariablesBasedOnFormat = filteredGlobalVariables.filter( + (variable) => variable.format === format, + ) + const filteredPreBuildStageVariablesBasedOnFormat = preBuildStageVariables.filter( + (variable) => variable.format === format, + ) + const filteredPreviousStepVariablesBasedOnFormat = previousStepVariables.filter( + (variable) => variable.format === format, + ) const isOptionsEmpty = !defaultValues.length && (isBuildStagePostBuild - ? !preBuildStageVariables.length && !previousStepVariables.length - : !previousStepVariables.length) && + ? !filteredPreBuildStageVariablesBasedOnFormat.length && !filteredPreviousStepVariablesBasedOnFormat.length + : !filteredPreviousStepVariablesBasedOnFormat.length) && !filteredGlobalVariablesBasedOnFormat.length if (isOptionsEmpty) { @@ -139,28 +157,22 @@ export const getOptionsForValColumn = ({ ? [ { label: VAL_COLUMN_DROPDOWN_LABEL.PRE_BUILD_STAGE, - options: preBuildStageVariables, + options: filteredPreBuildStageVariablesBasedOnFormat, }, { label: VAL_COLUMN_DROPDOWN_LABEL.POST_BUILD_STAGE, - options: previousStepVariables, + options: filteredPreviousStepVariablesBasedOnFormat, }, ] : [ { label: VAL_COLUMN_DROPDOWN_LABEL.PREVIOUS_STEPS, - options: previousStepVariables, + options: filteredPreviousStepVariablesBasedOnFormat, }, ]), { label: VAL_COLUMN_DROPDOWN_LABEL.SYSTEM_VARIABLES, - options: isBuildStagePostBuild - ? filteredGlobalVariablesBasedOnFormat - : filteredGlobalVariablesBasedOnFormat.filter( - (variable) => - (isCdPipeline && variable.stageType !== 'post-cd') || - !excludeVariables.includes(variable.value), - ), + options: filteredGlobalVariablesBasedOnFormat, }, ] : []), @@ -270,7 +282,7 @@ export const getValColumnRowProps = ({ return { type: DynamicDataTableRowDataType.TEXT, - value: description, + value: description || 'No description available', props: {}, } } @@ -295,15 +307,19 @@ export const getValColumnRowValue = ( return isDateFormat ? getGoLangFormattedDateWithTimezone(selectedValue.value) : value } -export const getEmptyVariableDataTableRow = (params: GetValColumnRowPropsType): VariableDataRowType => { +export const getEmptyVariableDataTableRow = ({ + id, + ...params +}: GetValColumnRowPropsType & { id: number }): VariableDataRowType => { const data: VariableDataRowType = { data: { variable: getVariableColumnRowProps(), format: getFormatColumnRowProps({ format: VariableTypeFormat.STRING, isCustomTask: true }), val: getValColumnRowProps(params), }, - id: params.id, + id, customState: { + defaultValue: '', variableDescription: '', isVariableRequired: false, choices: [], @@ -330,7 +346,7 @@ export const getVariableDataTableInitialRows = ({ ioVariables, type, isCustomTask, - emptyRowParams, + ...restProps }: GetVariableDataTableInitialRowsProps): VariableDataRowType[] => (ioVariables || []).map( ({ @@ -346,6 +362,7 @@ export const getVariableDataTableInitialRows = ({ isRuntimeArg, fileMountDir, fileReferenceId, + defaultValue, id, }) => { const isInputVariableRequired = type === PluginVariableType.INPUT && !allowEmptyValue @@ -363,18 +380,19 @@ export const getVariableDataTableInitialRows = ({ }, format: getFormatColumnRowProps({ format, isCustomTask }), val: getValColumnRowProps({ - ...emptyRowParams, + ...restProps, + type, description, format, - variableType, value, + variableType, refVariableName, refVariableStage, valueConstraint, - id, }), }, customState: { + defaultValue, isVariableRequired: isInputVariableRequired, variableDescription: description ?? '', choices: (valueConstraint?.choices || []).map((choiceValue, index) => ({ @@ -384,7 +402,13 @@ export const getVariableDataTableInitialRows = ({ })), askValueAtRuntime: isRuntimeArg ?? false, blockCustomValue: valueConstraint?.blockCustomValue ?? false, - selectedValue: null, + selectedValue: { + label: refVariableName || value, + value: refVariableName || value, + refVariableName, + refVariableStage, + variableType: refVariableName ? RefVariableType.GLOBAL : RefVariableType.NEW, + }, fileInfo: { id: fileReferenceId, mountDir: { value: fileMountDir, error: '' }, @@ -401,6 +425,21 @@ export const getVariableDataTableInitialRows = ({ }, ) +export const getVariableDataTableInitialCellError = ( + rows: VariableDataRowType[], + headers: DynamicDataTableHeaderType[], +) => + rows.reduce((acc, curr) => { + if (!acc[curr.id]) { + acc[curr.id] = headers.reduce( + (headerAcc, { key }) => ({ ...headerAcc, [key]: { isValid: true, errorMessages: [] } }), + {}, + ) + } + + return acc + }, {}) + export const getUploadFileConstraints = ({ unit, allowedExtensions, @@ -422,6 +461,7 @@ export const getUploadFileConstraints = ({ export const convertVariableDataTableToFormData = ({ rows, + cellError, type, activeStageName, selectedTaskIndex, @@ -440,6 +480,7 @@ export const convertVariableDataTableToFormData = ({ > & { type: PluginVariableType rows: VariableDataRowType[] + cellError: DynamicDataTableCellErrorType }) => { const updatedFormData = structuredClone(formData) const updatedFormDataErrorObj = structuredClone(formDataErrorObj) @@ -564,9 +605,16 @@ export const convertVariableDataTableToFormData = ({ updatedFormData[activeStageName].steps[selectedTaskIndex].inlineStepDetail.conditionDetails = conditionDetails } + const isValid = Object.values(cellError).reduce( + (acc, curr) => acc && !Object.values(curr).some((item) => !item.isValid), + true, + ) + updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].isValid = isValid + validateTask( updatedFormData[activeStageName].steps[selectedTaskIndex], updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex], + { validateIOVariables: false }, ) return { updatedFormDataErrorObj, updatedFormData } diff --git a/src/components/CIPipelineN/VariableDataTable/validationSchema.ts b/src/components/CIPipelineN/VariableDataTable/validationSchema.ts deleted file mode 100644 index dee292e5ac..0000000000 --- a/src/components/CIPipelineN/VariableDataTable/validationSchema.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { DynamicDataTableProps, VariableTypeFormat } from '@devtron-labs/devtron-fe-common-lib' - -import { PluginVariableType } from '@Components/ciPipeline/types' -import { PATTERNS } from '@Config/constants' - -import { VariableDataCustomState, VariableDataKeys } from './types' -import { checkForSystemVariable, testValueForNumber } from './utils' - -export const getVariableDataTableValidationSchema = - ({ - pluginVariableType, - keysFrequencyMap, - }: { - pluginVariableType: PluginVariableType - keysFrequencyMap: Record - }): DynamicDataTableProps['validationSchema'] => - (value, key, { data, customState }) => { - const { variableDescription, isVariableRequired, selectedValue, askValueAtRuntime } = customState - - const re = new RegExp(PATTERNS.VARIABLE) - - if (key === 'variable') { - const variableValue = !isVariableRequired || data.val.value - - if (!value && !variableValue && !variableDescription) { - return { errorMessages: ['Please complete or remove this variable'], isValid: false } - } - - if (!value) { - return { errorMessages: ['Variable name is required'], isValid: false } - } - - if (!re.test(value)) { - return { - errorMessages: [`Invalid name. Only alphanumeric chars and (_) is allowed`], - isValid: false, - } - } - - if ((keysFrequencyMap[value] || 0) > 1) { - return { errorMessages: ['Variable name should be unique'], isValid: false } - } - } - - if (pluginVariableType === PluginVariableType.INPUT && key === 'val') { - const checkForVariable = isVariableRequired && !askValueAtRuntime - if (checkForVariable && !value) { - return { errorMessages: ['Variable value is required'], isValid: false } - } - - if (data.format.value === VariableTypeFormat.NUMBER) { - return { - isValid: checkForSystemVariable(selectedValue) || testValueForNumber(value), - errorMessages: ['Variable value is not a number'], - } - } - } - - return { errorMessages: [], isValid: true } - } diff --git a/src/components/CIPipelineN/VariableDataTable/validations.ts b/src/components/CIPipelineN/VariableDataTable/validations.ts new file mode 100644 index 0000000000..c7f8fa6efa --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/validations.ts @@ -0,0 +1,84 @@ +import { DynamicDataTableCellValidationState, VariableTypeFormat } from '@devtron-labs/devtron-fe-common-lib' + +import { PluginVariableType } from '@Components/ciPipeline/types' +import { PATTERNS } from '@Config/constants' + +import { GetValidateCellProps, ValidateVariableDataTableProps } from './types' +import { checkForSystemVariable, testValueForNumber } from './utils' + +export const getVariableDataTableCellValidateState = ({ + pluginVariableType, + keysFrequencyMap, + row: { data, customState }, + key, + value: latestValue, +}: GetValidateCellProps): DynamicDataTableCellValidationState => { + const value = latestValue ?? data[key].value + const { variableDescription, isVariableRequired, selectedValue, askValueAtRuntime, defaultValue } = customState + const re = new RegExp(PATTERNS.VARIABLE) + + if (key === 'variable') { + const variableValue = !isVariableRequired || data.val.value + + if (!value && !variableValue && !variableDescription) { + return { errorMessages: ['Please complete or remove this variable'], isValid: false } + } + + if (!value) { + return { errorMessages: ['Variable name is required'], isValid: false } + } + + if (!re.test(value)) { + return { + errorMessages: [`Invalid name. Only alphanumeric chars and (_) is allowed`], + isValid: false, + } + } + + if ((keysFrequencyMap[value] || 0) > 1) { + return { errorMessages: ['Variable name should be unique'], isValid: false } + } + } + + if (pluginVariableType === PluginVariableType.INPUT && key === 'val') { + const checkForVariable = isVariableRequired && !askValueAtRuntime && !defaultValue + if (checkForVariable && !value) { + return { errorMessages: ['Variable value is required'], isValid: false } + } + + if (data.format.value === VariableTypeFormat.NUMBER) { + return { + isValid: checkForSystemVariable(selectedValue) || testValueForNumber(value), + errorMessages: ['Variable value is not a number'], + } + } + } + + return { errorMessages: [], isValid: true } +} + +export const validateVariableDataTable = ({ + rows, + headers, + keysFrequencyMap, + pluginVariableType, +}: ValidateVariableDataTableProps) => { + const cellError = rows.reduce((acc, row) => { + acc[row.id] = headers.reduce( + (headerAcc, { key }) => ({ + ...headerAcc, + [key]: getVariableDataTableCellValidateState({ + keysFrequencyMap, + pluginVariableType, + key, + row, + }), + }), + {}, + ) + + return acc + }, {}) + + return cellError +} diff --git a/src/components/cdPipeline/CDPipeline.tsx b/src/components/cdPipeline/CDPipeline.tsx index 5178b21cac..82dcc62f07 100644 --- a/src/components/cdPipeline/CDPipeline.tsx +++ b/src/components/cdPipeline/CDPipeline.tsx @@ -877,6 +877,7 @@ export default function CDPipeline({ if (!_formDataErrorObj[stageName].steps[i]) { _formDataErrorObj[stageName].steps.push({ isValid: true }) } + _formDataErrorObj.triggerValidation = true validateTask(_formData[stageName].steps[i], _formDataErrorObj[stageName].steps[i]) isStageValid = isStageValid && _formDataErrorObj[stageName].steps[i].isValid } diff --git a/src/components/cdPipeline/cdpipeline.util.tsx b/src/components/cdPipeline/cdpipeline.util.tsx index d557386387..38f3053271 100644 --- a/src/components/cdPipeline/cdpipeline.util.tsx +++ b/src/components/cdPipeline/cdpipeline.util.tsx @@ -53,7 +53,15 @@ export const ValueContainer = (props) => { ) } -export const validateTask = (taskData: StepType, taskErrorObj: TaskErrorObj, isSaveAsPlugin = false): void => { +export const validateTask = ( + taskData: StepType, + taskErrorObj: TaskErrorObj, + options?: { + isSaveAsPlugin?: boolean + validateIOVariables?: boolean + }, +) => { + const { isSaveAsPlugin = false, validateIOVariables = true } = options ?? {} const validationRules = new ValidationRules() if (taskData && taskErrorObj) { taskErrorObj.name = validationRules.requiredField(taskData.name) @@ -65,26 +73,37 @@ export const validateTask = (taskData: StepType, taskErrorObj: TaskErrorObj, isS const currentStepTypeVariable = taskData.stepType === PluginType.INLINE ? 'inlineStepDetail' : 'pluginRefStepDetail' + taskErrorObj[currentStepTypeVariable].isValid = taskErrorObj[currentStepTypeVariable].isValid ?? true + taskErrorObj[currentStepTypeVariable].inputVariables = [] - taskData[currentStepTypeVariable].inputVariables?.forEach((element, index) => { - taskErrorObj[currentStepTypeVariable].inputVariables.push( - validationRules.inputVariable(element, inputVarMap, isSaveAsPlugin), - ) - taskErrorObj.isValid = - taskErrorObj.isValid && taskErrorObj[currentStepTypeVariable].inputVariables[index].isValid - inputVarMap.set(element.name, true) - }) + if (validateIOVariables) { + taskData[currentStepTypeVariable].inputVariables?.forEach((element, index) => { + taskErrorObj[currentStepTypeVariable].inputVariables.push( + validationRules.inputVariable(element, inputVarMap, isSaveAsPlugin), + ) + taskErrorObj[currentStepTypeVariable].isValid = + taskErrorObj[currentStepTypeVariable].isValid && + taskErrorObj[currentStepTypeVariable].inputVariables[index].isValid + inputVarMap.set(element.name, true) + }) + } + taskErrorObj.isValid = taskErrorObj.isValid && taskErrorObj[currentStepTypeVariable].isValid if (taskData.stepType === PluginType.INLINE) { taskErrorObj.inlineStepDetail.outputVariables = [] - taskData.inlineStepDetail.outputVariables?.forEach((element, index) => { - taskErrorObj.inlineStepDetail.outputVariables.push( - validationRules.outputVariable(element, outputVarMap), - ) - taskErrorObj.isValid = - taskErrorObj.isValid && taskErrorObj.inlineStepDetail.outputVariables[index].isValid - outputVarMap.set(element.name, true) - }) + if (validateIOVariables) { + taskData.inlineStepDetail.outputVariables?.forEach((element, index) => { + taskErrorObj.inlineStepDetail.outputVariables.push( + validationRules.outputVariable(element, outputVarMap), + ) + taskErrorObj[currentStepTypeVariable].isValid = + taskErrorObj[currentStepTypeVariable].isValid && + taskErrorObj.inlineStepDetail.outputVariables[index].isValid + outputVarMap.set(element.name, true) + }) + } + taskErrorObj.isValid = taskErrorObj.isValid && taskErrorObj[currentStepTypeVariable].isValid + if (taskData.inlineStepDetail['scriptType'] === ScriptType.SHELL) { taskErrorObj.inlineStepDetail['script'] = validationRules.requiredField( taskData.inlineStepDetail['script'], diff --git a/src/components/workflowEditor/types.ts b/src/components/workflowEditor/types.ts index 417d7627dc..1b27a6878a 100644 --- a/src/components/workflowEditor/types.ts +++ b/src/components/workflowEditor/types.ts @@ -254,6 +254,7 @@ export interface PipelineFormDataErrorType { isValid: boolean } userApprovalConfig?: ValidationResponseType + triggerValidation?: boolean } interface HandleValidateMandatoryPluginsParamsType { @@ -278,7 +279,14 @@ export interface PipelineContext { } formDataErrorObj: PipelineFormDataErrorType setFormDataErrorObj: React.Dispatch> - validateTask: (taskData: StepType, taskErrorobj: TaskErrorObj, isSaveAsPlugin?: boolean) => void + validateTask: ( + taskData: StepType, + taskErrorobj: TaskErrorObj, + options?: { + isSaveAsPlugin?: boolean + validateIOVariables?: boolean + }, + ) => void setSelectedTaskIndex: React.Dispatch> validateStage: ( stageName: string, From 5333918bfd702c00ad1041ef2b9fc3a699f6cec4 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Thu, 12 Dec 2024 09:28:26 +0530 Subject: [PATCH 22/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ac3e24581e..0425e3a8c9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-13", + "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-15", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index 509e08b6de..76d43acacc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-13": - version "1.2.4-beta-13" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-13.tgz#f6ed73388e8d0dc12b5757189cd3c211b9e69adb" - integrity sha512-WK76lSq3Cm3pW8f7vtTCXDgS4LQC+seFjkBHFEdqFmnnK56YHCizsztzhj6iCt6V+25wtlGmspDeh201ScleBw== +"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-15": + version "1.2.4-beta-15" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-15.tgz#938bee0a468e2f799e5301c8a096a2f73efaf4c1" + integrity sha512-zGi4uY2Yc/onyuPq9B/SU881rS2TKLxxfxSpvzuTvQNWWj/Pb/FiYRd5HgT30kow+ujvZOzdbC5OLq+RP17F5A== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From 8baba09dc2b46c1495d7c876e5249dc5bdc24a3f Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Thu, 12 Dec 2024 12:22:14 +0530 Subject: [PATCH 23/36] fix: VariableDataTable - undefined error fix, refactor --- .../VariableDataTable.component.tsx | 66 +++++----- .../CIPipelineN/VariableDataTable/types.ts | 5 +- .../CIPipelineN/VariableDataTable/utils.tsx | 114 ++++++++++-------- .../VariableDataTable/validations.ts | 5 +- 4 files changed, 100 insertions(+), 90 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index c59d75bc8b..4b727bddbe 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -135,25 +135,23 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa useEffect(() => { // Validate the table when: - // 1. Rows have been initialized (`initialRowsSet.current` is 'set'). + // 1. Rows have been initialized (`initialRowsSet.current` is 'set' & rows is not empty). // 2. Validation is explicitly triggered (`formDataErrorObj.triggerValidation` is true) // or the table is currently invalid (`!isTableValid` -> this is only triggered on mount) - if (initialRowsSet.current === 'set') { - if (formDataErrorObj.triggerValidation || !isTableValid) { - setCellError( - validateVariableDataTable({ - headers, - rows, - keysFrequencyMap, - pluginVariableType: type, - }), - ) - // Reset the triggerValidation flag after validation is complete. - setFormDataErrorObj((prevState) => ({ - ...prevState, - triggerValidation: false, - })) - } + if (initialRowsSet.current === 'set' && rows.length && (formDataErrorObj.triggerValidation || !isTableValid)) { + setCellError( + validateVariableDataTable({ + headers, + rows, + keysFrequencyMap, + pluginVariableType: type, + }), + ) + // Reset the triggerValidation flag after validation is complete. + setFormDataErrorObj((prevState) => ({ + ...prevState, + triggerValidation: false, + })) } }, [initialRowsSet.current, formDataErrorObj.triggerValidation]) @@ -176,8 +174,8 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa const isCurrentValueValid = !blockCustomValue || - ((!customState.selectedValue || - customState.selectedValue?.variableType === RefVariableType.NEW) && + ((!customState.valColumnSelectedValue || + customState.valColumnSelectedValue?.variableType === RefVariableType.NEW) && choicesOptions.some(({ value }) => value === data.val.value)) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ @@ -193,11 +191,11 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ...data, val: getValColumnRowProps({ ...defaultRowValColumnParams, - ...(!blockCustomValue && customState.selectedValue + ...(!blockCustomValue && customState.valColumnSelectedValue ? { - variableType: customState.selectedValue.variableType, - refVariableName: customState.selectedValue.value, - refVariableStage: customState.selectedValue.refVariableStage, + variableType: customState.valColumnSelectedValue.variableType, + refVariableName: customState.valColumnSelectedValue.value, + refVariableStage: customState.valColumnSelectedValue.refVariableStage, } : {}), value: isCurrentValueValid ? data.val.value : '', @@ -210,7 +208,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }, customState: { ...customState, - selectedValue: !blockCustomValue ? customState.selectedValue : null, + valColumnSelectedValue: !blockCustomValue ? customState.valColumnSelectedValue : null, blockCustomValue, choices: choicesOptions, }, @@ -443,11 +441,11 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa case VariableDataTableActionType.UPDATE_VAL_COLUMN: updatedRows = updatedRows.map((row) => { if (row.id === rowAction.rowId && row.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { - const { selectedValue, value } = rowAction.actionValue + const { valColumnSelectedValue, value } = rowAction.actionValue const valColumnRowValue = getValColumnRowValue( row.data.format.value as VariableTypeFormat, value, - selectedValue, + valColumnSelectedValue, ) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ @@ -465,11 +463,13 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa val: getValColumnRowProps({ ...defaultRowValColumnParams, value: valColumnRowValue, - ...(!row.customState.blockCustomValue && rowAction.actionValue.selectedValue + ...(!row.customState.blockCustomValue && + rowAction.actionValue.valColumnSelectedValue ? { - variableType: rowAction.actionValue.selectedValue.variableType, - refVariableName: rowAction.actionValue.selectedValue.value, - refVariableStage: rowAction.actionValue.selectedValue.refVariableStage, + variableType: rowAction.actionValue.valColumnSelectedValue.variableType, + refVariableName: rowAction.actionValue.valColumnSelectedValue.value, + refVariableStage: + rowAction.actionValue.valColumnSelectedValue.refVariableStage, } : {}), format: row.data.format.value as VariableTypeFormat, @@ -481,7 +481,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }, customState: { ...row.customState, - selectedValue: rowAction.actionValue.selectedValue, + valColumnSelectedValue: rowAction.actionValue.valColumnSelectedValue, }, } } @@ -515,7 +515,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }, customState: { ...row.customState, - selectedValue: null, + valColumnSelectedValue: null, choices: [], blockCustomValue: false, fileInfo: { @@ -573,7 +573,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa if (headerKey === 'val' && updatedRow.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_VAL_COLUMN, - actionValue: { value, selectedValue: extraData.selectedValue }, + actionValue: { value, valColumnSelectedValue: extraData.selectedValue }, rowId: updatedRow.id, }) } else if ( diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index a386592df3..212164b2c1 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -20,6 +20,7 @@ export interface VariableDataTableSelectPickerOptionType extends SelectPickerOpt variableType?: RefVariableType refVariableStage?: RefVariableStageType refVariableName?: string + refVariableStepIndex?: number } export type VariableDataKeys = 'variable' | 'format' | 'val' @@ -31,7 +32,7 @@ export type VariableDataCustomState = { choices: { id: number; value: string; error: string }[] askValueAtRuntime: boolean blockCustomValue: boolean - selectedValue: VariableDataTableSelectPickerOptionType & Record + valColumnSelectedValue: VariableDataTableSelectPickerOptionType fileInfo: { id: number mountDir: { @@ -82,7 +83,7 @@ type VariableDataTableActionPropsMap = { [VariableDataTableActionType.UPDATE_VAL_COLUMN]: { actionValue: { value: string - selectedValue: VariableDataTableSelectPickerOptionType + valColumnSelectedValue: VariableDataTableSelectPickerOptionType } rowId: string | number } diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index 3d6df2fdde..11588a935f 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -216,7 +216,7 @@ export const getFormatColumnRowProps = ({ export const getValColumnRowProps = ({ format, type, - variableType, + variableType = RefVariableType.NEW, value, refVariableName, refVariableStage, @@ -299,12 +299,12 @@ export const checkForSystemVariable = (option: VariableDataTableSelectPickerOpti export const getValColumnRowValue = ( format: VariableTypeFormat, value: string, - selectedValue: VariableDataTableSelectPickerOptionType, + valColumnSelectedValue: VariableDataTableSelectPickerOptionType, ) => { - const isSystemVariable = checkForSystemVariable(selectedValue) + const isSystemVariable = checkForSystemVariable(valColumnSelectedValue) const isDateFormat = !isSystemVariable && value && format === VariableTypeFormat.DATE - return isDateFormat ? getGoLangFormattedDateWithTimezone(selectedValue.value) : value + return isDateFormat ? getGoLangFormattedDateWithTimezone(valColumnSelectedValue.value) : value } export const getEmptyVariableDataTableRow = ({ @@ -325,7 +325,7 @@ export const getEmptyVariableDataTableRow = ({ choices: [], askValueAtRuntime: false, blockCustomValue: false, - selectedValue: null, + valColumnSelectedValue: null, fileInfo: { id: null, mountDir: { @@ -349,23 +349,37 @@ export const getVariableDataTableInitialRows = ({ ...restProps }: GetVariableDataTableInitialRowsProps): VariableDataRowType[] => (ioVariables || []).map( - ({ - name, - allowEmptyValue, - description, - format, - variableType, - value, - refVariableName, - refVariableStage, - valueConstraint, - isRuntimeArg, - fileMountDir, - fileReferenceId, - defaultValue, - id, - }) => { + ( + { + name, + allowEmptyValue, + description, + format, + variableType, + value, + refVariableName, + refVariableStage, + valueConstraint, + isRuntimeArg, + fileMountDir, + fileReferenceId, + defaultValue, + id, + }, + index, + ) => { const isInputVariableRequired = type === PluginVariableType.INPUT && !allowEmptyValue + const valColumnValue = getValColumnRowProps({ + ...restProps, + type, + description, + format, + value, + variableType, + refVariableName, + refVariableStage, + valueConstraint, + }) return { data: { @@ -379,36 +393,30 @@ export const getVariableDataTableInitialRows = ({ }, }, format: getFormatColumnRowProps({ format, isCustomTask }), - val: getValColumnRowProps({ - ...restProps, - type, - description, - format, - value, - variableType, - refVariableName, - refVariableStage, - valueConstraint, - }), + val: valColumnValue, }, customState: { defaultValue, isVariableRequired: isInputVariableRequired, variableDescription: description ?? '', - choices: (valueConstraint?.choices || []).map((choiceValue, index) => ({ - id: index, + choices: (valueConstraint?.choices || []).map((choiceValue, choiceIndex) => ({ + id: choiceIndex, value: choiceValue, error: '', })), askValueAtRuntime: isRuntimeArg ?? false, blockCustomValue: valueConstraint?.blockCustomValue ?? false, - selectedValue: { - label: refVariableName || value, - value: refVariableName || value, - refVariableName, - refVariableStage, - variableType: refVariableName ? RefVariableType.GLOBAL : RefVariableType.NEW, - }, + valColumnSelectedValue: + valColumnValue.type === DynamicDataTableRowDataType.SELECT_TEXT + ? { + label: refVariableName || value, + value: refVariableName || value, + refVariableName, + refVariableStage, + variableType: refVariableName ? RefVariableType.GLOBAL : RefVariableType.NEW, + format, + } + : null, fileInfo: { id: fileReferenceId, mountDir: { value: fileMountDir, error: '' }, @@ -420,7 +428,7 @@ export const getVariableDataTableInitialRows = ({ unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], }, }, - id, + id: id || index, } }, ) @@ -501,7 +509,7 @@ export const convertVariableDataTableToFormData = ({ askValueAtRuntime, blockCustomValue, choices, - selectedValue, + valColumnSelectedValue, isVariableRequired, variableDescription, fileInfo, @@ -550,24 +558,24 @@ export const convertVariableDataTableToFormData = ({ } } - if (selectedValue) { - if (selectedValue.refVariableStepIndex) { + if (valColumnSelectedValue) { + if (valColumnSelectedValue.refVariableStepIndex) { variableDetail.value = '' variableDetail.variableType = RefVariableType.FROM_PREVIOUS_STEP - variableDetail.refVariableStepIndex = selectedValue.refVariableStepIndex - variableDetail.refVariableName = selectedValue.label as string - variableDetail.format = selectedValue.format - variableDetail.refVariableStage = selectedValue.refVariableStage - } else if (selectedValue.variableType === RefVariableType.GLOBAL) { + variableDetail.refVariableStepIndex = valColumnSelectedValue.refVariableStepIndex + variableDetail.refVariableName = valColumnSelectedValue.label as string + variableDetail.format = valColumnSelectedValue.format + variableDetail.refVariableStage = valColumnSelectedValue.refVariableStage + } else if (valColumnSelectedValue.variableType === RefVariableType.GLOBAL) { variableDetail.value = '' variableDetail.variableType = RefVariableType.GLOBAL variableDetail.refVariableStepIndex = 0 - variableDetail.refVariableName = selectedValue.label as string - variableDetail.format = selectedValue.format + variableDetail.refVariableName = valColumnSelectedValue.label as string + variableDetail.format = valColumnSelectedValue.format variableDetail.refVariableStage = null } else { if (variableDetail.format !== VariableTypeFormat.DATE) { - variableDetail.value = selectedValue.label as string + variableDetail.value = valColumnSelectedValue.label as string } variableDetail.variableType = RefVariableType.NEW variableDetail.refVariableName = '' diff --git a/src/components/CIPipelineN/VariableDataTable/validations.ts b/src/components/CIPipelineN/VariableDataTable/validations.ts index c7f8fa6efa..ca0904f81d 100644 --- a/src/components/CIPipelineN/VariableDataTable/validations.ts +++ b/src/components/CIPipelineN/VariableDataTable/validations.ts @@ -14,7 +14,8 @@ export const getVariableDataTableCellValidateState = ({ value: latestValue, }: GetValidateCellProps): DynamicDataTableCellValidationState => { const value = latestValue ?? data[key].value - const { variableDescription, isVariableRequired, selectedValue, askValueAtRuntime, defaultValue } = customState + const { variableDescription, isVariableRequired, valColumnSelectedValue, askValueAtRuntime, defaultValue } = + customState const re = new RegExp(PATTERNS.VARIABLE) if (key === 'variable') { @@ -48,7 +49,7 @@ export const getVariableDataTableCellValidateState = ({ if (data.format.value === VariableTypeFormat.NUMBER) { return { - isValid: checkForSystemVariable(selectedValue) || testValueForNumber(value), + isValid: checkForSystemVariable(valColumnSelectedValue) || testValueForNumber(value), errorMessages: ['Variable value is not a number'], } } From f01a4f6e866ea0deae9a0dbf23381df78c8f853a Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Thu, 12 Dec 2024 18:00:45 +0530 Subject: [PATCH 24/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0425e3a8c9..800452ef5b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-15", + "@devtron-labs/devtron-fe-common-lib": "1.2.4-beta-16", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index 76d43acacc..4b9a3345b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-15": - version "1.2.4-beta-15" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-15.tgz#938bee0a468e2f799e5301c8a096a2f73efaf4c1" - integrity sha512-zGi4uY2Yc/onyuPq9B/SU881rS2TKLxxfxSpvzuTvQNWWj/Pb/FiYRd5HgT30kow+ujvZOzdbC5OLq+RP17F5A== +"@devtron-labs/devtron-fe-common-lib@1.2.4-beta-16": + version "1.2.4-beta-16" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.4-beta-16.tgz#b381bb2d07cc4e912ad08735626c04646d74bd8e" + integrity sha512-imTvdBZJzo4/G4kc170qd9KYXvT0JfP6uW96pY3DiUDs7SIRWOFOgArRtjTmTvq/40e8fKqcB9gCaPIRD2qAVQ== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1" From d817ac70e6e2da839e0f9c9d65f797a9cf4ae93b Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 13 Dec 2024 02:02:43 +0530 Subject: [PATCH 25/36] fix: VariableDataTable - duplicate variable name validation fix, restrict usage of file type for oss --- .../VariableDataTable.component.tsx | 59 +++++++------- .../VariableDataTable/constants.ts | 24 +++++- .../CIPipelineN/VariableDataTable/types.ts | 9 +-- .../VariableDataTable/validations.ts | 81 +++++++++++++++---- 4 files changed, 120 insertions(+), 53 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index 4b727bddbe..1e2b0a597c 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -1,4 +1,4 @@ -import { useContext, useState, useEffect, useRef, useMemo } from 'react' +import { useContext, useState, useEffect, useRef } from 'react' import { DynamicDataTable, @@ -33,7 +33,11 @@ import { getVariableDataTableInitialCellError, getVariableDataTableInitialRows, } from './utils' -import { getVariableDataTableCellValidateState, validateVariableDataTable } from './validations' +import { + getVariableDataTableCellValidateState, + validateVariableDataTable, + validateVariableDataTableVariableKeys, +} from './validations' import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' import { VariableConfigOverlay } from './VariableConfigOverlay' @@ -93,22 +97,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa const [rows, setRows] = useState([]) const [cellError, setCellError] = useState>({}) - // KEYS FREQUENCY MAP - const keysFrequencyMap: Record = useMemo( - () => - rows.reduce( - (acc, curr) => { - const currentKey = curr.data.variable.value - if (currentKey) { - acc[currentKey] = (acc[currentKey] || 0) + 1 - } - return acc - }, - {} as Record, - ), - [rows], - ) - // REFS const initialRowsSet = useRef('') @@ -143,7 +131,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa validateVariableDataTable({ headers, rows, - keysFrequencyMap, pluginVariableType: type, }), ) @@ -175,11 +162,12 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa const isCurrentValueValid = !blockCustomValue || ((!customState.valColumnSelectedValue || - customState.valColumnSelectedValue?.variableType === RefVariableType.NEW) && + (customState.valColumnSelectedValue?.variableType !== RefVariableType.GLOBAL && + customState.valColumnSelectedValue?.variableType !== + RefVariableType.FROM_PREVIOUS_STEP)) && choicesOptions.some(({ value }) => value === data.val.value)) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ - keysFrequencyMap, pluginVariableType: type, key: 'val', row, @@ -201,6 +189,13 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa value: isCurrentValueValid ? data.val.value : '', format: data.format.value as VariableTypeFormat, valueConstraint: { + constraint: { + fileProperty: getUploadFileConstraints({ + allowedExtensions: customState.fileInfo.allowedExtensions, + maxUploadSize: customState.fileInfo.maxUploadSize, + unit: customState.fileInfo.unit.label as string, + }), + }, blockCustomValue, choices: choicesOptions.map(({ value }) => value), }, @@ -280,13 +275,11 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa customState: { ...row.customState, isVariableRequired: rowAction.actionValue }, } updatedCellError[row.id].variable = getVariableDataTableCellValidateState({ - keysFrequencyMap, pluginVariableType: type, key: 'variable', row: updatedRow, }) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ - keysFrequencyMap, pluginVariableType: type, key: 'val', row: updatedRow, @@ -365,7 +358,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa updatedRows = updatedRows.map((row) => { if (row.id === rowAction.rowId && row.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD) { updatedCellError[row.id].val = getVariableDataTableCellValidateState({ - keysFrequencyMap, pluginVariableType: type, value: rowAction.actionValue.fileName, key: 'val', @@ -410,18 +402,29 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa case VariableDataTableActionType.DELETE_ROW: updatedRows = updatedRows.filter((row) => row.id !== rowAction.rowId) delete updatedCellError[rowAction.rowId] + validateVariableDataTableVariableKeys({ + rows: updatedRows, + cellError: updatedCellError, + }) break case VariableDataTableActionType.UPDATE_ROW: updatedRows = rows.map((row) => { if (row.id === rowAction.rowId) { - updatedCellError[row.id][rowAction.headerKey] = getVariableDataTableCellValidateState({ - keysFrequencyMap, + updatedCellError[rowAction.rowId][rowAction.headerKey] = getVariableDataTableCellValidateState({ pluginVariableType: type, value: rowAction.actionValue, - key: rowAction.headerKey, row, + key: rowAction.headerKey, }) + if (rowAction.headerKey === 'variable') { + validateVariableDataTableVariableKeys({ + rows, + cellError: updatedCellError, + rowId: rowAction.rowId, + value: rowAction.actionValue, + }) + } return { ...row, @@ -449,7 +452,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ - keysFrequencyMap, pluginVariableType: type, value: valColumnRowValue, key: 'val', @@ -494,7 +496,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa updatedRows = updatedRows.map((row) => { if (row.id === rowAction.rowId && row.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { updatedCellError[row.id].val = getVariableDataTableCellValidateState({ - keysFrequencyMap, pluginVariableType: type, key: 'val', row, diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts index 67e854b055..2f4ff88448 100644 --- a/src/components/CIPipelineN/VariableDataTable/constants.ts +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -4,10 +4,13 @@ import { VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' +import { importComponentFromFELibrary } from '@Components/common' import { PluginVariableType } from '@Components/ciPipeline/types' import { VariableDataKeys } from './types' +const isFELibAvailable = importComponentFromFELibrary('isFELibAvailable', null, 'function') + export const getVariableDataTableHeaders = ( type: PluginVariableType, ): DynamicDataTableHeaderType[] => [ @@ -61,10 +64,14 @@ export const FORMAT_COLUMN_OPTIONS: SelectPickerOptionType[] = [ label: FORMAT_OPTIONS_MAP.DATE, value: VariableTypeFormat.DATE, }, - { - label: FORMAT_OPTIONS_MAP.FILE, - value: VariableTypeFormat.FILE, - }, + ...(isFELibAvailable + ? [ + { + label: FORMAT_OPTIONS_MAP.FILE, + value: VariableTypeFormat.FILE, + }, + ] + : []), ] export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ @@ -77,3 +84,12 @@ export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ value: 1 / 1024, }, ] + +export const VARIABLE_DATA_TABLE_CELL_ERROR_MSGS = { + EMPTY_ROW: 'Please complete or remove this variable', + VARIABLE_NAME_REQUIRED: 'Variable name is required', + INVALID_VARIABLE_NAME: 'Invalid name. Only alphanumeric chars and (_) is allowed', + UNIQUE_VARIABLE_NAME: 'Variable name should be unique', + VARIABLE_VALUE_REQUIRED: 'Variable value is required', + VARIABLE_VALUE_NOT_A_NUMBER: 'Variable value is not a number', +} diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index 212164b2c1..c4710b68e0 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -180,16 +180,15 @@ export interface GetVariableDataTableInitialRowsProps isCustomTask: boolean } -export interface GetValidateCellProps { +export type GetValidateCellProps = { pluginVariableType: PluginVariableType - keysFrequencyMap: Record - key: VariableDataKeys row: VariableDataRowType value?: string + key: VariableDataKeys + keysFrequencyMap?: Record } -export interface ValidateVariableDataTableProps - extends Pick { +export interface ValidateVariableDataTableProps extends Pick { rows: VariableDataRowType[] headers: DynamicDataTableHeaderType[] } diff --git a/src/components/CIPipelineN/VariableDataTable/validations.ts b/src/components/CIPipelineN/VariableDataTable/validations.ts index ca0904f81d..9bb28531f6 100644 --- a/src/components/CIPipelineN/VariableDataTable/validations.ts +++ b/src/components/CIPipelineN/VariableDataTable/validations.ts @@ -1,17 +1,38 @@ -import { DynamicDataTableCellValidationState, VariableTypeFormat } from '@devtron-labs/devtron-fe-common-lib' +import { + DynamicDataTableCellErrorType, + DynamicDataTableCellValidationState, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' import { PluginVariableType } from '@Components/ciPipeline/types' import { PATTERNS } from '@Config/constants' -import { GetValidateCellProps, ValidateVariableDataTableProps } from './types' +import { GetValidateCellProps, ValidateVariableDataTableProps, VariableDataKeys, VariableDataRowType } from './types' import { checkForSystemVariable, testValueForNumber } from './utils' +import { VARIABLE_DATA_TABLE_CELL_ERROR_MSGS } from './constants' + +export const getVariableDataTableVariableKeysFrequency = ( + rows: VariableDataRowType[], + rowId?: string | number, + value?: string, +) => { + const keysFrequencyMap: Record = rows.reduce((acc, curr) => { + const currentKey = curr.id === rowId ? value : curr.data.variable.value + if (currentKey) { + acc[currentKey] = (acc[currentKey] ?? 0) + 1 + } + return acc + }, {}) + + return keysFrequencyMap +} export const getVariableDataTableCellValidateState = ({ - pluginVariableType, - keysFrequencyMap, row: { data, customState }, key, value: latestValue, + pluginVariableType, + keysFrequencyMap = {}, }: GetValidateCellProps): DynamicDataTableCellValidationState => { const value = latestValue ?? data[key].value const { variableDescription, isVariableRequired, valColumnSelectedValue, askValueAtRuntime, defaultValue } = @@ -22,35 +43,35 @@ export const getVariableDataTableCellValidateState = ({ const variableValue = !isVariableRequired || data.val.value if (!value && !variableValue && !variableDescription) { - return { errorMessages: ['Please complete or remove this variable'], isValid: false } + return { errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.EMPTY_ROW], isValid: false } } if (!value) { - return { errorMessages: ['Variable name is required'], isValid: false } + return { errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.VARIABLE_NAME_REQUIRED], isValid: false } } if (!re.test(value)) { return { - errorMessages: [`Invalid name. Only alphanumeric chars and (_) is allowed`], + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.INVALID_VARIABLE_NAME], isValid: false, } } if ((keysFrequencyMap[value] || 0) > 1) { - return { errorMessages: ['Variable name should be unique'], isValid: false } + return { errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.UNIQUE_VARIABLE_NAME], isValid: false } } } if (pluginVariableType === PluginVariableType.INPUT && key === 'val') { const checkForVariable = isVariableRequired && !askValueAtRuntime && !defaultValue if (checkForVariable && !value) { - return { errorMessages: ['Variable value is required'], isValid: false } + return { errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.VARIABLE_VALUE_REQUIRED], isValid: false } } if (data.format.value === VariableTypeFormat.NUMBER) { return { isValid: checkForSystemVariable(valColumnSelectedValue) || testValueForNumber(value), - errorMessages: ['Variable value is not a number'], + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.VARIABLE_VALUE_NOT_A_NUMBER], } } } @@ -58,12 +79,42 @@ export const getVariableDataTableCellValidateState = ({ return { errorMessages: [], isValid: true } } -export const validateVariableDataTable = ({ +export const validateVariableDataTableVariableKeys = ({ rows, - headers, - keysFrequencyMap, - pluginVariableType, -}: ValidateVariableDataTableProps) => { + rowId, + value, + cellError, +}: Pick & { + cellError: DynamicDataTableCellErrorType + rowId?: string | number + value?: string +}) => { + const updatedCellError = cellError + const keysFrequencyMap = getVariableDataTableVariableKeysFrequency(rows, rowId, value) + + rows.forEach(({ data, id }) => { + const cellValue = rowId === id ? value : data.variable.value + const variableErrorState = updatedCellError[id].variable + if (variableErrorState.isValid && keysFrequencyMap[cellValue] > 1) { + updatedCellError[id].variable = { + isValid: false, + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.UNIQUE_VARIABLE_NAME], + } + } else if ( + keysFrequencyMap[cellValue] < 2 && + variableErrorState.errorMessages[0] === VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.UNIQUE_VARIABLE_NAME + ) { + updatedCellError[id].variable = { + isValid: true, + errorMessages: [], + } + } + }) +} + +export const validateVariableDataTable = ({ rows, headers, pluginVariableType }: ValidateVariableDataTableProps) => { + const keysFrequencyMap = getVariableDataTableVariableKeysFrequency(rows) + const cellError = rows.reduce((acc, row) => { acc[row.id] = headers.reduce( (headerAcc, { key }) => ({ From a5927fddee34a41f93864f037236ef10b17673a3 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Fri, 13 Dec 2024 23:50:17 +0530 Subject: [PATCH 26/36] refactor: VariableDataTable - removed component level state - using parent state, optimised validations, various improvements & fixes --- src/components/CIPipelineN/CIPipeline.tsx | 1 - .../CIPipelineN/TaskDetailComponent.tsx | 14 +- .../CIPipelineN/VariableContainer.tsx | 4 +- .../ValueConfigFileTippy.tsx | 6 +- .../VariableDataTable/ValueConfigOverlay.tsx | 162 +++++----- .../VariableConfigOverlay.tsx | 99 +++--- .../VariableDataTable.component.tsx | 289 ++++++++---------- .../VariableDataTablePopupMenu.tsx | 2 +- .../VariableDataTable/constants.ts | 20 +- .../CIPipelineN/VariableDataTable/types.ts | 58 ++-- .../CIPipelineN/VariableDataTable/utils.tsx | 118 +++---- .../VariableDataTable/validations.ts | 149 +++++---- src/components/cdPipeline/CDPipeline.tsx | 1 - src/components/cdPipeline/cdpipeline.util.tsx | 100 ++++-- .../ciPipeline/ciPipeline.service.ts | 2 +- src/components/ciPipeline/types.ts | 7 +- src/components/ciPipeline/validationRules.ts | 70 +---- src/components/workflowEditor/types.ts | 7 +- 18 files changed, 557 insertions(+), 552 deletions(-) diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index 2616fecf83..7a3fa84066 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -438,7 +438,6 @@ export default function CIPipeline({ if (!_formDataErrorObj[stageName].steps[i]) { _formDataErrorObj[stageName].steps.push({ isValid: true }) } - _formDataErrorObj.triggerValidation = true validateTask(_formData[stageName].steps[i], _formDataErrorObj[stageName].steps[i]) isStageValid = isStageValid && _formDataErrorObj[stageName].steps[i].isValid } diff --git a/src/components/CIPipelineN/TaskDetailComponent.tsx b/src/components/CIPipelineN/TaskDetailComponent.tsx index c0221bf9e4..2ec2787908 100644 --- a/src/components/CIPipelineN/TaskDetailComponent.tsx +++ b/src/components/CIPipelineN/TaskDetailComponent.tsx @@ -273,10 +273,7 @@ export const TaskDetailComponent = () => {
{selectedStep.stepType === PluginType.INLINE ? ( -
-

Input variables

- -
+ ) : ( )} @@ -292,14 +289,7 @@ export const TaskDetailComponent = () => { {formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].scriptType !== ScriptType.CONTAINERIMAGE && ( -
-

Output variables

- -
+ )} ) : ( diff --git a/src/components/CIPipelineN/VariableContainer.tsx b/src/components/CIPipelineN/VariableContainer.tsx index 6abd456c1a..5d7ce96957 100644 --- a/src/components/CIPipelineN/VariableContainer.tsx +++ b/src/components/CIPipelineN/VariableContainer.tsx @@ -33,9 +33,9 @@ export const VariableContainer = ({ type }: { type: PluginVariableType }) => { ]?.length || 0 useEffect(() => { if (collapsedSection) { - const invalidInputVariables = formDataErrorObj[activeStageName].steps[ + const invalidInputVariables = !formDataErrorObj[activeStageName].steps[ selectedTaskIndex - ].pluginRefStepDetail.inputVariables?.some((inputVariable) => !inputVariable.isValid) + ].pluginRefStepDetail.isValid if (invalidInputVariables) { setCollapsedSection(false) // expand input variables in case of error } diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx index 36cf8c0f9e..da9032912a 100644 --- a/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigFileTippy.tsx @@ -1,8 +1,10 @@ import Tippy from '@tippyjs/react' +import { VariableType } from '@devtron-labs/devtron-fe-common-lib' + import { ReactComponent as Info } from '@Icons/info-filled.svg' -export const ValueConfigFileTippy = ({ mountDir }: { mountDir: string }) => ( +export const ValueConfigFileTippy = ({ fileMountDir }: Pick) => ( (

File mount path

- {mountDir} + {fileMountDir}

Ensure the uploaded file name is unique to avoid conflicts or overrides. diff --git a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx index 68dc58b6cb..f90709188f 100644 --- a/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx @@ -1,4 +1,4 @@ -import { ChangeEvent } from 'react' +import { ChangeEvent, useState } from 'react' import { Button, @@ -23,51 +23,45 @@ import { ReactComponent as ICInfoOutlineGrey } from '@Icons/ic-info-outline-grey import { ConfigOverlayProps, VariableDataTableActionType } from './types' import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, FORMAT_OPTIONS_MAP } from './constants' import { testValueForNumber } from './utils' +import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlayProps) => { const { id: rowId, data, customState } = row - const { choices, askValueAtRuntime, blockCustomValue, fileInfo } = customState + const { choices: initialChoices, askValueAtRuntime, blockCustomValue, fileInfo } = customState + + // STATES + const [choices, setChoices] = useState(initialChoices.map((choice, id) => ({ id, value: choice, error: '' }))) // CONSTANTS const isFormatNumber = data.format.value === VariableTypeFormat.NUMBER const isFormatBoolOrDate = data.format.value === VariableTypeFormat.BOOL || data.format.value === VariableTypeFormat.DATE const isFormatFile = data.format.value === VariableTypeFormat.FILE + const hasChoicesError = choices.some(({ error }) => !!error) + const hasFileMountError = !fileInfo.fileMountDir // METHODS const handleAddChoices = () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId, - actionValue: (currentChoices) => [{ value: '', id: currentChoices.length, error: '' }, ...currentChoices], - }) + setChoices([{ value: '', id: choices.length + 1, error: '' }, ...choices]) } const handleChoiceChange = (choiceId: number) => (e: ChangeEvent) => { const choiceValue = e.target.value - - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId, - actionValue: (currentChoices) => - currentChoices.map((choice) => - choice.id === choiceId - ? { - id: choiceId, - value: choiceValue, - error: isFormatNumber && !testValueForNumber(choiceValue) ? 'Choice is not a number' : '', - } - : choice, - ), - }) + setChoices( + choices.map((choice) => + choice.id === choiceId + ? { + id: choiceId, + value: choiceValue, + error: isFormatNumber && !testValueForNumber(choiceValue) ? 'Choice is not a number' : '', + } + : choice, + ), + ) } const handleChoiceDelete = (choiceId: number) => () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.UPDATE_CHOICES, - rowId, - actionValue: (currentChoices) => currentChoices.filter(({ id }) => id !== choiceId), - }) + setChoices(choices.filter(({ id }) => id !== choiceId)) } const handleAllowCustomInput = () => { @@ -87,14 +81,11 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay } const handleFileMountChange = (e: ChangeEvent) => { - const fileMountValue = e.target.value + const fileMountDir = e.target.value handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FILE_MOUNT, rowId, - actionValue: { - error: !fileMountValue ? 'This field is required' : '', - value: fileMountValue, - }, + actionValue: fileMountDir, }) } @@ -137,6 +128,14 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay } } + const handlePopupClose = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS, + rowId, + actionValue: choices.filter(({ value }) => !!value).map(({ value }) => value), + }) + } + // RENDERERS const renderContent = () => { if (isFormatFile) { @@ -146,12 +145,13 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay name="fileMount" label="File mount path" placeholder="Enter file mount path" - value={fileInfo.mountDir.value} + value={fileInfo.fileMountDir} onChange={handleFileMountChange} dataTestid={`file-mount-${rowId}`} inputWrapClassName="w-100" isRequiredField - error={fileInfo.mountDir.error} + error={hasFileMountError ? 'This field is required' : ''} + autoFocus />

{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} @@ -218,21 +218,22 @@ export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlay />
- {choices.map(({ id, value, error }) => ( + {choices.map(({ id, value, error }, index) => (
- } - > -
Ask value at runtime
- - -
- +
+ + ) } diff --git a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx index 62a8d9c5a8..589abb4196 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx @@ -1,8 +1,16 @@ import { ChangeEvent } from 'react' -import { Checkbox, CHECKBOX_VALUE, CustomInput, ResizableTextarea, Tooltip } from '@devtron-labs/devtron-fe-common-lib' +import { + Checkbox, + CHECKBOX_VALUE, + CustomInput, + InputOutputVariablesHeaderKeys, + ResizableTextarea, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib' import { ConfigOverlayProps, VariableDataTableActionType } from './types' +import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' export const VariableConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlayProps) => { const { id: rowId, data, customState } = row @@ -13,7 +21,7 @@ export const VariableConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOver handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_ROW, rowId, - headerKey: 'variable', + headerKey: InputOutputVariablesHeaderKeys.VARIABLE, actionValue: e.target.value, }) } @@ -35,50 +43,53 @@ export const VariableConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOver } return ( - <> -
- -
- {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - - + <> +
+ +
+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + +
-
-
- - -

Value is required

-

Get this tooltip from Utkarsh

-
- } +
+ -
Value is required
- -
-
- + +

Value is required

+

Get this tooltip from Utkarsh

+
+ } + > +
Value is required
+ + +
+ + ) } diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx index 1e2b0a597c..4e22ad4eff 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -1,25 +1,32 @@ -import { useContext, useState, useEffect, useRef } from 'react' +import { useContext, useMemo } from 'react' import { + Button, + ButtonVariantType, DynamicDataTable, DynamicDataTableCellErrorType, DynamicDataTableProps, DynamicDataTableRowDataType, + InputOutputVariablesHeaderKeys, PluginType, RefVariableType, VariableType, VariableTypeFormat, } from '@devtron-labs/devtron-fe-common-lib' +import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' import { pipelineContext } from '@Components/workflowEditor/workflowEditor' import { PluginVariableType } from '@Components/ciPipeline/types' -import { FILE_UPLOAD_SIZE_UNIT_OPTIONS, getVariableDataTableHeaders } from './constants' +import { + FILE_UPLOAD_SIZE_UNIT_OPTIONS, + getVariableDataTableHeaders, + VARIABLE_DATA_TABLE_EMPTY_ROW_MESSAGE, +} from './constants' import { GetValColumnRowPropsType, HandleRowUpdateActionProps, VariableDataCustomState, - VariableDataKeys, VariableDataRowType, VariableDataTableActionType, VariableDataTableProps, @@ -31,15 +38,10 @@ import { getValColumnRowProps, getValColumnRowValue, getVariableDataTableInitialCellError, - getVariableDataTableInitialRows, + getVariableDataTableRows, } from './utils' -import { - getVariableDataTableCellValidateState, - validateVariableDataTable, - validateVariableDataTableVariableKeys, -} from './validations' +import { getVariableDataTableCellValidateState, validateVariableDataTableVariableKeys } from './validations' -import { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' import { VariableConfigOverlay } from './VariableConfigOverlay' import { ValueConfigOverlay } from './ValueConfigOverlay' import { ValueConfigFileTippy } from './ValueConfigFileTippy' @@ -80,6 +82,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa valueConstraint: null, } + const isInputPluginVariable = type === PluginVariableType.INPUT const currentStepTypeVariable = formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE ? 'inlineStepDetail' @@ -87,66 +90,45 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa const ioVariables: VariableType[] = formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ - type === PluginVariableType.INPUT ? 'inputVariables' : 'outputVariables' + isInputPluginVariable ? 'inputVariables' : 'outputVariables' ] - const isTableValid = - formDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable].isValid ?? true - - // STATES - const [rows, setRows] = useState([]) - const [cellError, setCellError] = useState>({}) + const ioVariablesError = + formDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + isInputPluginVariable ? 'inputVariables' : 'outputVariables' + ] - // REFS - const initialRowsSet = useRef('') + // TABLE ROWS + const rows = useMemo( + () => + getVariableDataTableRows({ + ioVariables, + isCustomTask, + type, + activeStageName, + formData, + globalVariables, + selectedTaskIndex, + inputVariablesListFromPrevStep, + isCdPipeline, + }), + [ioVariables], + ) - useEffect(() => { - // SETTING INITIAL ROWS & ERROR STATE - const initialRows = getVariableDataTableInitialRows({ - ioVariables, - isCustomTask, - type, - activeStageName, - formData, - globalVariables, - selectedTaskIndex, - inputVariablesListFromPrevStep, - isCdPipeline, - }) - const updatedCellError = getVariableDataTableInitialCellError(initialRows, headers) - - setRows(initialRows) - setCellError(updatedCellError) - - initialRowsSet.current = 'set' - }, []) - - useEffect(() => { - // Validate the table when: - // 1. Rows have been initialized (`initialRowsSet.current` is 'set' & rows is not empty). - // 2. Validation is explicitly triggered (`formDataErrorObj.triggerValidation` is true) - // or the table is currently invalid (`!isTableValid` -> this is only triggered on mount) - if (initialRowsSet.current === 'set' && rows.length && (formDataErrorObj.triggerValidation || !isTableValid)) { - setCellError( - validateVariableDataTable({ - headers, - rows, - pluginVariableType: type, - }), - ) - // Reset the triggerValidation flag after validation is complete. - setFormDataErrorObj((prevState) => ({ - ...prevState, - triggerValidation: false, - })) - } - }, [initialRowsSet.current, formDataErrorObj.triggerValidation]) + // TABLE CELL ERROR + const cellError = useMemo>( + () => + Object.keys(ioVariablesError).length + ? ioVariablesError + : getVariableDataTableInitialCellError(rows, headers), + [ioVariablesError, rows], + ) // METHODS const handleRowUpdateAction = (rowAction: HandleRowUpdateActionProps) => { const { actionType } = rowAction let updatedRows = rows - const updatedCellError = structuredClone(cellError) + const updatedCellError = cellError switch (actionType) { case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: @@ -154,22 +136,20 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa const { id, data, customState } = row if (id === rowAction.rowId) { - // FILTERING EMPTY CHOICE VALUES - const choicesOptions = customState.choices.filter(({ value }) => !!value) + const choicesOptions = rowAction.actionValue // RESETTING TO DEFAULT STATE IF CHOICES ARE EMPTY const blockCustomValue = !!choicesOptions.length && row.customState.blockCustomValue const isCurrentValueValid = !blockCustomValue || ((!customState.valColumnSelectedValue || - (customState.valColumnSelectedValue?.variableType !== RefVariableType.GLOBAL && - customState.valColumnSelectedValue?.variableType !== - RefVariableType.FROM_PREVIOUS_STEP)) && - choicesOptions.some(({ value }) => value === data.val.value)) + !customState.valColumnSelectedValue?.variableType || + customState.valColumnSelectedValue.variableType === RefVariableType.NEW) && + choicesOptions.some((value) => value === data.val.value)) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ pluginVariableType: type, - key: 'val', + key: InputOutputVariablesHeaderKeys.VALUE, row, }) @@ -197,7 +177,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }), }, blockCustomValue, - choices: choicesOptions.map(({ value }) => value), + choices: choicesOptions, }, }), }, @@ -214,20 +194,6 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }) break - case VariableDataTableActionType.UPDATE_CHOICES: - updatedRows = updatedRows.map((row) => - row.id === rowAction.rowId - ? { - ...row, - customState: { - ...row.customState, - choices: rowAction.actionValue(row.customState.choices), - }, - } - : row, - ) - break - case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: updatedRows = updatedRows.map((row) => row.id === rowAction.rowId @@ -276,12 +242,12 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa } updatedCellError[row.id].variable = getVariableDataTableCellValidateState({ pluginVariableType: type, - key: 'variable', + key: InputOutputVariablesHeaderKeys.VARIABLE, row: updatedRow, }) updatedCellError[row.id].val = getVariableDataTableCellValidateState({ pluginVariableType: type, - key: 'val', + key: InputOutputVariablesHeaderKeys.VALUE, row: updatedRow, }) @@ -299,7 +265,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ...row, customState: { ...row.customState, - fileInfo: { ...row.customState.fileInfo, mountDir: rowAction.actionValue }, + fileInfo: { ...row.customState.fileInfo, fileMountDir: rowAction.actionValue }, }, } : row, @@ -360,7 +326,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa updatedCellError[row.id].val = getVariableDataTableCellValidateState({ pluginVariableType: type, value: rowAction.actionValue.fileName, - key: 'val', + key: InputOutputVariablesHeaderKeys.VALUE, row, }) @@ -381,7 +347,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa ...row.customState, fileInfo: { ...row.customState.fileInfo, - id: rowAction.actionValue.fileReferenceId, + fileReferenceId: rowAction.actionValue.fileReferenceId, }, }, } @@ -417,7 +383,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa row, key: rowAction.headerKey, }) - if (rowAction.headerKey === 'variable') { + if (rowAction.headerKey === InputOutputVariablesHeaderKeys.VARIABLE) { validateVariableDataTableVariableKeys({ rows, cellError: updatedCellError, @@ -454,7 +420,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa updatedCellError[row.id].val = getVariableDataTableCellValidateState({ pluginVariableType: type, value: valColumnRowValue, - key: 'val', + key: InputOutputVariablesHeaderKeys.VALUE, row, }) @@ -477,7 +443,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa format: row.data.format.value as VariableTypeFormat, valueConstraint: { blockCustomValue: row.customState.blockCustomValue, - choices: row.customState.choices.map((choice) => choice.value), + choices: row.customState.choices, }, }), }, @@ -497,7 +463,7 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa if (row.id === rowAction.rowId && row.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { updatedCellError[row.id].val = getVariableDataTableCellValidateState({ pluginVariableType: type, - key: 'val', + key: InputOutputVariablesHeaderKeys.VALUE, row, }) @@ -520,13 +486,10 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa choices: [], blockCustomValue: false, fileInfo: { - id: null, + fileReferenceId: null, allowedExtensions: '', maxUploadSize: '', - mountDir: { - value: '/devtroncd', - error: '', - }, + fileMountDir: '/devtroncd', unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], }, }, @@ -553,32 +516,30 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa }) setFormDataErrorObj(updatedFormDataErrorObj) setFormData(updatedFormData) - - setRows(updatedRows) - setCellError(updatedCellError) } - const dataTableHandleAddition = () => { + const handleRowAdd = () => { handleRowUpdateAction({ actionType: VariableDataTableActionType.ADD_ROW, rowId: Math.floor(new Date().valueOf() * Math.random()), }) } - const dataTableHandleChange: DynamicDataTableProps['onRowEdit'] = async ( - updatedRow, - headerKey, - value, - extraData, - ) => { - if (headerKey === 'val' && updatedRow.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { + const handleRowEdit: DynamicDataTableProps< + InputOutputVariablesHeaderKeys, + VariableDataCustomState + >['onRowEdit'] = async (updatedRow, headerKey, value, extraData) => { + if ( + headerKey === InputOutputVariablesHeaderKeys.VALUE && + updatedRow.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT + ) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_VAL_COLUMN, actionValue: { value, valColumnSelectedValue: extraData.selectedValue }, rowId: updatedRow.id, }) } else if ( - headerKey === 'val' && + headerKey === InputOutputVariablesHeaderKeys.VALUE && updatedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD && extraData.files.length ) { @@ -610,7 +571,10 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa rowId: updatedRow.id, }) } - } else if (headerKey === 'format' && updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { + } else if ( + headerKey === InputOutputVariablesHeaderKeys.FORMAT && + updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN + ) { handleRowUpdateAction({ actionType: VariableDataTableActionType.UPDATE_FORMAT_COLUMN, actionValue: value as VariableTypeFormat, @@ -626,76 +590,81 @@ export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTa } } - const dataTableHandleDelete: DynamicDataTableProps['onRowDelete'] = ( - row, - ) => { + const handleRowDelete: DynamicDataTableProps< + InputOutputVariablesHeaderKeys, + VariableDataCustomState + >['onRowDelete'] = (row) => { handleRowUpdateAction({ actionType: VariableDataTableActionType.DELETE_ROW, rowId: row.id, }) } - const onActionButtonPopupClose = (rowId: string | number) => () => { - handleRowUpdateAction({ - actionType: VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS, - rowId, - }) - } - // RENDERERS const actionButtonRenderer = (row: VariableDataRowType) => ( - !!error)) - } - > - - + ) const getTrailingCellIconForVariableColumn = (row: VariableDataRowType) => - isCustomTask && type === PluginVariableType.INPUT ? ( - - - + isCustomTask && isInputPluginVariable ? ( + ) : null const getTrailingCellIconForValueColumn = (row: VariableDataRowType) => - row.data.format.value === VariableTypeFormat.FILE ? ( - + isInputPluginVariable && row.data.format.value === VariableTypeFormat.FILE ? ( + ) : null - const trailingCellIcon: DynamicDataTableProps['trailingCellIcon'] = { + const trailingCellIcon: DynamicDataTableProps['trailingCellIcon'] = { variable: getTrailingCellIconForVariableColumn, val: getTrailingCellIconForValueColumn, } return ( - - key={initialRowsSet.current} - headers={headers} - rows={rows} - cellError={cellError} - readOnly={!isCustomTask && type === PluginVariableType.OUTPUT} - isAdditionNotAllowed={!isCustomTask} - isDeletionNotAllowed={!isCustomTask} - trailingCellIcon={trailingCellIcon} - onRowEdit={dataTableHandleChange} - onRowDelete={dataTableHandleDelete} - onRowAdd={dataTableHandleAddition} - {...(type === PluginVariableType.INPUT - ? { - actionButtonConfig: { - renderer: actionButtonRenderer, - key: 'val', - position: 'end', - }, - } - : {})} - /> +
+ {isCustomTask && ( +
+

+ {isInputPluginVariable ? 'Input variables' : 'Output variables'} +

+ {!rows.length && ( +
+ )} + {rows.length ? ( + + headers={headers} + rows={rows} + cellError={cellError} + readOnly={!isCustomTask && !isInputPluginVariable} + isAdditionNotAllowed={!isCustomTask} + isDeletionNotAllowed={!isCustomTask} + trailingCellIcon={trailingCellIcon} + onRowEdit={handleRowEdit} + onRowDelete={handleRowDelete} + onRowAdd={handleRowAdd} + {...(isInputPluginVariable + ? { + actionButtonConfig: { + renderer: actionButtonRenderer, + key: InputOutputVariablesHeaderKeys.VALUE, + position: 'end', + }, + } + : {})} + /> + ) : ( +
+

{VARIABLE_DATA_TABLE_EMPTY_ROW_MESSAGE[type]}

+
+ )} +
) } diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx index a7f118f204..d447f12e2a 100644 --- a/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx @@ -47,7 +47,7 @@ export const VariableDataTablePopupMenu = ({ heading={

{heading}

} Icon={showIcon ? ICSlidersVertical : null} iconSize={16} - additionalContent={
{children}
} + additionalContent={visible &&
{children}
} > {visible &&
} diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts index 2d9ab4c7a4..9b23cbfa65 100644 --- a/src/components/CIPipelineN/VariableDataTable/types.ts +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -127,7 +127,8 @@ export type HandleRowUpdateActionProps = VariableDataTableAction export interface VariableDataTablePopupMenuProps extends Pick { heading: string - showIcon?: boolean + showHeaderIcon?: boolean + showIconDot?: boolean disableClose?: boolean onClose?: () => void children: JSX.Element diff --git a/src/css/icons.scss b/src/css/icons.scss index 52a236e381..123dfb8890 100644 --- a/src/css/icons.scss +++ b/src/css/icons.scss @@ -332,3 +332,28 @@ } } } + +@mixin dot-icon-size($size) { + position: absolute; + top: 0; + right: 0; + transform: translate(50%, -50%); + width: 0.625em; + height: 0.625em; + + > circle { + stroke-width: 0.125em; + } +} + +@each $size in 16, 24, 32, 48 { + .show-icon-dot-#{$size} { + position: relative; + font-size: #{$size}px; + display: flex; + + & .ic-dot { + @include dot-icon-size($size); + } + } +} From 7e6f6edb6bb8e9668c6d0bb585aa5cc130198c05 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 16 Dec 2024 19:48:44 +0530 Subject: [PATCH 35/36] fix: VariableDataTable - incorrect check for payload fix --- src/components/CIPipelineN/VariableDataTable/utils.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx index d376f0ae74..deec39344b 100644 --- a/src/components/CIPipelineN/VariableDataTable/utils.tsx +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -535,13 +535,12 @@ export const convertVariableDataTableToFormData = ({ variableDetail.isRuntimeArg = askValueAtRuntime if ( - (variableDetail.format === VariableTypeFormat.STRING || - variableDetail.format === VariableTypeFormat.NUMBER) && - choices.length + variableDetail.format === VariableTypeFormat.STRING || + variableDetail.format === VariableTypeFormat.NUMBER ) { variableDetail.valueConstraint = { ...variableDetail.valueConstraint, - choices, + choices: choices.length ? choices : null, blockCustomValue, } } else if (variableDetail.format === VariableTypeFormat.FILE && fileInfo) { From 8ba5395717e6658903cf9307cb6a10101c6d2be1 Mon Sep 17 00:00:00 2001 From: Rohit Raj Date: Mon, 16 Dec 2024 20:07:30 +0530 Subject: [PATCH 36/36] chore: common-lib version bump --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b68fc0a5d6..22c3d552aa 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.2.16", + "@devtron-labs/devtron-fe-common-lib": "1.2.17", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/yarn.lock b/yarn.lock index 3ee264a141..4305c52dac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -974,10 +974,10 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" -"@devtron-labs/devtron-fe-common-lib@1.2.16": - version "1.2.16" - resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.16.tgz#de42ed1d22b18e604b71e68f683426c0d4395835" - integrity sha512-v0FfaFTyMsvYrZ5v9n4fMnt86lqKrOSxmAKbpuiAimbGW01V5VBZuyZg3aEZA0nkt3bJvzzUyypjTF5kyfLmvg== +"@devtron-labs/devtron-fe-common-lib@1.2.17": + version "1.2.17" + resolved "https://registry.yarnpkg.com/@devtron-labs/devtron-fe-common-lib/-/devtron-fe-common-lib-1.2.17.tgz#9cf425f5e14a58cabf31a7563145c729d431dfcb" + integrity sha512-5L3JNVYjSXdRU3vJOBxOUZC1hD1E1KDnDRen/z4xnU1Y5KMtEWCfD3w1TXUIE9qgqVMdi8UAGwA2lkyrYgmaVw== dependencies: "@types/react-dates" "^21.8.6" ansi_up "^5.2.1"