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/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-dot.svg b/src/assets/icons/ic-dot.svg new file mode 100644 index 0000000000..1387a073b5 --- /dev/null +++ b/src/assets/icons/ic-dot.svg @@ -0,0 +1,3 @@ + + + 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/ApplicationGroup/AppGroup.types.ts b/src/components/ApplicationGroup/AppGroup.types.ts index 870c47faae..7c5f06996f 100644 --- a/src/components/ApplicationGroup/AppGroup.types.ts +++ b/src/components/ApplicationGroup/AppGroup.types.ts @@ -26,11 +26,11 @@ import { WorkflowType, AppInfoListType, GVKType, - RuntimeParamsListItemType, UseUrlFiltersReturnType, CommonNodeAttr, + RuntimePluginVariables, } from '@devtron-labs/devtron-fe-common-lib' -import { CDMaterialProps } from '../app/details/triggerView/types' +import { CDMaterialProps, RuntimeParamsErrorState } from '../app/details/triggerView/types' import { EditDescRequest, NodeType, Nodes, OptionType } from '../app/types' import { MultiValue } from 'react-select' import { AppFilterTabs, BulkResponseStatus } from './Constants' @@ -103,10 +103,10 @@ export interface ResponseRowType { } interface BulkRuntimeParamsType { - runtimeParams: Record - setRuntimeParams: React.Dispatch>> - runtimeParamsErrorState: Record - setRuntimeParamsErrorState: React.Dispatch>> + runtimeParams: Record + setRuntimeParams: React.Dispatch>> + runtimeParamsErrorState: Record + setRuntimeParamsErrorState: React.Dispatch>> } export interface BulkCITriggerType extends BulkRuntimeParamsType { diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx index 5d1229a441..0427a623da 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' @@ -53,7 +55,7 @@ import { ReactComponent as Tag } from '../../../../assets/icons/ic-tag.svg' import emptyPreDeploy from '../../../../assets/img/empty-pre-deploy.png' import notAuthorized from '../../../../assets/img/ic-not-authorized.svg' import CDMaterial from '../../../app/details/triggerView/cdMaterial' -import { BulkSelectionEvents, MATERIAL_TYPE } from '../../../app/details/triggerView/types' +import { BulkSelectionEvents, MATERIAL_TYPE, RuntimeParamsErrorState } from '../../../app/details/triggerView/types' import { BulkCDDetailType, BulkCDTriggerType } from '../../AppGroup.types' import { BULK_CD_DEPLOYMENT_STATUS, BULK_CD_MATERIAL_STATUS, BULK_CD_MESSAGING, BUTTON_TITLE } from '../../Constants' import TriggerResponseModal from './TriggerResponseModal' @@ -77,6 +79,7 @@ const getDeploymentWindowStateAppGroup = importComponentFromFELibrary( const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') const MissingPluginBlockState = importComponentFromFELibrary('MissingPluginBlockState', null, 'function') const PolicyEnforcementMessage = importComponentFromFELibrary('PolicyEnforcementMessage') +const validateRuntimeParameters = importComponentFromFELibrary('validateRuntimeParameters', null, 'function') // TODO: Fix release tags selection export default function BulkCDTrigger({ @@ -148,30 +151,31 @@ export default function BulkCDTrigger({ }) const handleSidebarTabChange = (e: React.ChangeEvent) => { - if (runtimeParamsErrorState[selectedApp.appId]) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: BULK_ERROR_MESSAGES.CHANGE_SIDEBAR_TAB, - }) - return - } - setCurrentSidebarTab(e.target.value as CDMaterialSidebarType) } - const handleRuntimeParamError = (errorState: boolean) => { + const handleRuntimeParamError = (errorState: RuntimeParamsErrorState) => { setRuntimeParamsErrorState((prevErrorState) => ({ ...prevErrorState, [selectedApp.appId]: errorState, })) } - 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 = [] @@ -352,7 +356,9 @@ export default function BulkCDTrigger({ } const changeApp = (e): void => { - if (runtimeParamsErrorState[selectedApp.appId]) { + const updatedErrorState = validateRuntimeParameters(runtimeParams[selectedApp.appId]) + handleRuntimeParamError(updatedErrorState) + if (!updatedErrorState.isValid) { ToastManager.showToast({ variant: ToastVariantType.error, description: BULK_ERROR_MESSAGES.CHANGE_APPLICATION, @@ -429,13 +435,9 @@ export default function BulkCDTrigger({ if (tagNotFoundWarningsMap.has(app.appId)) { return (
- + - - {tagNotFoundWarningsMap.get(app.appId)} - + {tagNotFoundWarningsMap.get(app.appId)}
) } @@ -454,13 +456,9 @@ export default function BulkCDTrigger({ if (!!warningMessage && !app.showPluginWarning) { return (
- + - - {warningMessage} - + {warningMessage}
) } @@ -473,7 +471,7 @@ export default function BulkCDTrigger({ nodeType={commonNodeAttrType} shouldRenderAdditionalInfo={isAppSelected} /> - ) + ) } return null @@ -711,6 +709,11 @@ export default function BulkCDTrigger({ tabs={CD_MATERIAL_SIDEBAR_TABS} initialTab={currentSidebarTab} onChange={handleSidebarTabChange} + hasError={{ + [CDMaterialSidebarType.PARAMETERS]: + runtimeParamsErrorState[selectedApp.appId] && + !runtimeParamsErrorState[selectedApp.appId].isValid, + }} /> )} @@ -794,7 +797,11 @@ export default function BulkCDTrigger({ isSuperAdmin={isSuperAdmin} bulkRuntimeParams={runtimeParams[selectedApp.appId] || []} handleBulkRuntimeParamChange={handleRuntimeParamChange} + bulkRuntimeParamErrorState={ + runtimeParamsErrorState[selectedApp.appId] || { cellError: {}, isValid: true } + } 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 1053e8a23b..361c2862f6 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, @@ -40,6 +39,10 @@ import { ButtonVariantType, ComponentSizeType, ButtonStyleType, + noop, + RuntimePluginVariables, + uploadCIPipelineFile, + UploadFileProps, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { getCIPipelineURL, getParsedBranchValuesForPlugin, importComponentFromFELibrary } from '../../../common' @@ -58,7 +61,11 @@ import { DOCUMENTATION, SOURCE_NOT_CONFIGURED, URLS, ViewType } from '../../../. import MaterialSource from '../../../app/details/triggerView/MaterialSource' import { TriggerViewContext } from '../../../app/details/triggerView/config' import { getCIMaterialList } from '../../../app/service' -import { HandleRuntimeParamChange, RegexValueType } from '../../../app/details/triggerView/types' +import { + HandleRuntimeParamChange, + HandleRuntimeParamErrorState, + RegexValueType, +} from '../../../app/details/triggerView/types' import { EmptyView } from '../../../app/details/cicdHistory/History.components' import BranchRegexModal from '../../../app/details/triggerView/BranchRegexModal' import { savePipeline } from '../../../ciPipeline/ciPipeline.service' @@ -83,6 +90,7 @@ const getCIBlockState: (...props) => Promise = importComponent ) const getRuntimeParams = importComponentFromFELibrary('getRuntimeParams', null, 'function') const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') +const validateRuntimeParameters = importComponentFromFELibrary('validateRuntimeParameters', null, 'function') const BulkCITrigger = ({ appList, @@ -175,7 +183,7 @@ const BulkCITrigger = ({ 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 || [] }) @@ -235,7 +243,7 @@ const BulkCITrigger = ({ } } - const handleRuntimeParamError = (errorState: boolean) => { + const handleRuntimeParamError: HandleRuntimeParamErrorState = (errorState) => { setRuntimeParamsErrorState((prevErrorState) => ({ ...prevErrorState, [selectedApp.ciPipelineId]: errorState, @@ -248,15 +256,16 @@ const BulkCITrigger = ({ setRuntimeParams(updatedRuntimeParams) } - const handleSidebarTabChange = (e: React.ChangeEvent) => { - if (runtimeParamsErrorState[selectedApp.ciPipelineId]) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: BULK_ERROR_MESSAGES.CHANGE_SIDEBAR_TAB, - }) - return - } + const uploadFile = ({ file, allowedExtensions, maxUploadSize }: UploadFileProps) => + uploadCIPipelineFile({ + file, + allowedExtensions, + maxUploadSize, + appId: selectedApp.appId, + ciPipelineId: +selectedApp.ciPipelineId, + }) + const handleSidebarTabChange = (e: React.ChangeEvent) => { setCurrentSidebarTab(e.target.value as CIMaterialSidebarType) } @@ -358,7 +367,9 @@ const BulkCITrigger = ({ } const changeApp = (e): void => { - if (runtimeParamsErrorState[selectedApp.ciPipelineId]) { + const updatedErrorState = validateRuntimeParameters(runtimeParams[selectedApp.ciPipelineId]) + handleRuntimeParamError(updatedErrorState) + if (!updatedErrorState.isValid) { ToastManager.showToast({ variant: ToastVariantType.error, description: BULK_ERROR_MESSAGES.CHANGE_APPLICATION, @@ -518,7 +529,11 @@ const BulkCITrigger = ({ handleSidebarTabChange={handleSidebarTabChange} runtimeParams={runtimeParams[selectedApp.ciPipelineId] || []} handleRuntimeParamChange={handleRuntimeParamChange} + runtimeParamsErrorState={ + runtimeParamsErrorState[selectedApp.ciPipelineId] || { cellError: {}, isValid: true } + } handleRuntimeParamError={handleRuntimeParamError} + uploadFile={uploadFile} appName={selectedApp?.name} isBulkCIWebhook={isWebhookBulkCI} setIsWebhookBulkCI={setIsWebhookBulkCI} @@ -711,6 +726,11 @@ const BulkCITrigger = ({ tabs={sidebarTabs} initialTab={currentSidebarTab} onChange={handleSidebarTabChange} + hasError={{ + [CIMaterialSidebarType.PARAMETERS]: + runtimeParamsErrorState[selectedApp.ciPipelineId] && + !runtimeParamsErrorState[selectedApp.ciPipelineId].isValid, + }} /> ) : ( 'Applications' diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index 3769e79f3b..f4a43062cf 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -42,11 +42,12 @@ import { ApiQueuingWithBatch, usePrompt, SourceTypeMap, - RuntimeParamsListItemType, preventBodyScroll, ToastManager, ToastVariantType, BlockedStateData, + RuntimePluginVariables, + uploadCIPipelineFile, } from '@devtron-labs/devtron-fe-common-lib' import Tippy from '@tippyjs/react' import { @@ -64,6 +65,7 @@ import { CIMaterialRouterProps, CIPipelineNodeType, MATERIAL_TYPE, + RuntimeParamsErrorState, } from '../../../app/details/triggerView/types' import { Workflow } from '../../../app/details/triggerView/workflow/Workflow' import { @@ -139,6 +141,7 @@ const processDeploymentWindowStateAppGroup = importComponentFromFELibrary( 'function', ) const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') +const validateRuntimeParameters = importComponentFromFELibrary('validateRuntimeParameters', null, 'function') // FIXME: IN CIMaterials we are sending isCDLoading while in CD materials we are sending isCILoading let inprogressStatusTimer @@ -185,8 +188,8 @@ 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 [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState>({}) + const [runtimeParams, setRuntimeParams] = useState>({}) + const [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState>({}) const [isBulkTriggerLoading, setIsBulkTriggerLoading] = useState(false) const enableRoutePrompt = isBranchChangeLoading || isBulkTriggerLoading @@ -1416,9 +1419,15 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou } const validateBulkRuntimeParams = (): boolean => { - const isRuntimeParamErrorPresent = Object.keys(runtimeParamsErrorState).some( - (key) => runtimeParamsErrorState[key], - ) + let isRuntimeParamErrorPresent = false + + const updatedRuntimeParamsErrorState = Object.keys(runtimeParams).reduce((acc, key) => { + const validationState = validateRuntimeParameters(runtimeParams[key]) + acc[key] = validationState + isRuntimeParamErrorPresent = !isRuntimeParamErrorPresent && !validationState.isValid + return acc + }, {}) + setRuntimeParamsErrorState(updatedRuntimeParamsErrorState) if (isRuntimeParamErrorPresent) { setCDLoading(false) @@ -1982,9 +1991,7 @@ export default function EnvTriggerView({ filteredAppIds, isVirtualEnv }: AppGrou if (selectedCINode?.id) { return ( - + (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[] @@ -345,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 } } @@ -498,7 +488,7 @@ export default function CIPipeline({ await getEnvironments(0) } } - await getGlobalVariables() + await callGlobalVariables() setPageState(ViewType.FORM) } catch (error) { setPageState(ViewType.ERROR) @@ -783,6 +773,16 @@ export default function CIPipeline({ } } + const uploadFile: PipelineContext['uploadFile'] = ({ allowedExtensions, file, maxUploadSize }) => + uploadCIPipelineFile({ + appId: +appId, + envId: isJobView ? selectedEnv?.id : null, + ciPipelineId: +ciPipelineId, + file, + allowedExtensions, + maxUploadSize, + }) + const contextValue = useMemo( () => ({ formData, @@ -810,6 +810,7 @@ export default function CIPipeline({ handleDisableParentModalCloseUpdate, handleValidateMandatoryPlugins, mandatoryPluginData, + uploadFile, }), [ formData, @@ -977,7 +978,7 @@ export default function CIPipeline({ <> {renderFloatingVariablesWidget()} - + {renderCIPipelineModal()} diff --git a/src/components/CIPipelineN/CreatePluginModal/utils.tsx b/src/components/CIPipelineN/CreatePluginModal/utils.tsx index ed6b9a398e..ab4dd287c2 100644 --- a/src/components/CIPipelineN/CreatePluginModal/utils.tsx +++ b/src/components/CIPipelineN/CreatePluginModal/utils.tsx @@ -156,6 +156,8 @@ const parseInputVariablesIntoCreatePluginPayload = ( valueType: variable.variableType, referenceVariableName: variable.refVariableName, isExposed: true, + fileMountDir: variable.fileMountDir, + fileReferenceId: variable.fileReferenceId, })) || [] export const getCreatePluginPayload = ({ diff --git a/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx b/src/components/CIPipelineN/PluginDetailHeader/CreatePluginButton.tsx index 4a18c48f13..32942928bd 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], + { isSaveAsPlugin: true }, ) setFormDataErrorObj(clonedFormErrorObj) diff --git a/src/components/CIPipelineN/TaskDetailComponent.tsx b/src/components/CIPipelineN/TaskDetailComponent.tsx index 7cfcba114e..2ec2787908 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,10 +273,10 @@ export const TaskDetailComponent = () => {
{selectedStep.stepType === PluginType.INLINE ? ( - + ) : ( - )}{' '} + )}
{selectedStep[currentStepTypeVariable]?.inputVariables?.length > 0 && ( <> @@ -289,7 +289,7 @@ 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 7c395e4e70..5d7ce96957 100644 --- a/src/components/CIPipelineN/VariableContainer.tsx +++ b/src/components/CIPipelineN/VariableContainer.tsx @@ -22,19 +22,20 @@ 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) - 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' ]?.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 } @@ -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/ValueConfigOverlay.tsx b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx new file mode 100644 index 0000000000..53bbd6d6f7 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/ValueConfigOverlay.tsx @@ -0,0 +1,348 @@ +import { ChangeEvent, useState } from 'react' + +import { + Button, + ButtonStyleType, + ButtonVariantType, + Checkbox, + CHECKBOX_VALUE, + ComponentSizeType, + CustomInput, + PATTERNS, + 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 { VariableDataTablePopupMenu } from './VariableDataTablePopupMenu' + +export const ValueConfigOverlay = ({ row, handleRowUpdateAction }: ConfigOverlayProps) => { + const { id: rowId, data, customState } = row + 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 + const showIconDot = !!choices.length || askValueAtRuntime || blockCustomValue || isFormatFile + + // METHODS + const handleAddChoices = () => { + setChoices([{ value: '', id: choices.length + 1, error: '' }, ...choices]) + } + + const handleChoiceChange = (choiceId: number) => (e: ChangeEvent) => { + const choiceValue = e.target.value + setChoices( + choices.map((choice) => + choice.id === choiceId + ? { + id: choiceId, + value: choiceValue, + error: + isFormatNumber && !PATTERNS.NATURAL_NUMBERS.test(choiceValue) + ? 'Choice is not a number' + : '', + } + : choice, + ), + ) + } + + const handleChoiceDelete = (choiceId: number) => () => { + setChoices(choices.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) => { + const fileMountDir = e.target.value + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_MOUNT, + rowId, + actionValue: fileMountDir, + }) + } + + 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 (!PATTERNS.NATURAL_NUMBERS.test(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, + }, + }) + } + } + + const handlePopupClose = () => { + // FILTERING EMPTY VALUES + const filteredChoices = choices.filter(({ value }) => !!value) + setChoices(filteredChoices) + handleRowUpdateAction({ + actionType: VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS, + rowId, + actionValue: filteredChoices.map(({ value }) => value), + }) + } + + // 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, error }, index) => ( +
+ +
+
+
+ ))} +
+
+ ) + } + + 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..5ac885a510 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableConfigOverlay.tsx @@ -0,0 +1,97 @@ +import { ChangeEvent } from 'react' + +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 + const { variableDescription, isVariableRequired } = customState + + // METHODS + const handleVariableName = (e: ChangeEvent) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + rowId, + headerKey: InputOutputVariablesHeaderKeys.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

+

+ Value for required variables must be provided for pipeline execution +

+
+ } + > +
Value is required
+ + + + +
+ ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx new file mode 100644 index 0000000000..bdc3164364 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTable.component.tsx @@ -0,0 +1,547 @@ +import { useContext, useMemo } from 'react' + +import { + Button, + ButtonVariantType, + DynamicDataTable, + DynamicDataTableCellErrorType, + DynamicDataTableProps, + DynamicDataTableRowDataType, + FileConfigTippy, + 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, + VARIABLE_DATA_TABLE_EMPTY_ROW_MESSAGE, +} from './constants' +import { + GetValColumnRowPropsType, + HandleRowUpdateActionProps, + VariableDataCustomState, + VariableDataRowType, + VariableDataTableActionType, + VariableDataTableProps, +} from './types' +import { + convertVariableDataTableToFormData, + getEmptyVariableDataTableRow, + getUploadFileConstraints, + getValColumnRowProps, + getValColumnRowValue, + getVariableDataTableInitialCellError, + getVariableDataTableRows, +} from './utils' +import { + getVariableDataTableCellValidateState, + getVariableDataTableRowEmptyValidationState, + validateVariableDataTableVariableKeys, +} from './validations' + +import { VariableConfigOverlay } from './VariableConfigOverlay' +import { ValueConfigOverlay } from './ValueConfigOverlay' + +export const VariableDataTable = ({ type, isCustomTask = false }: VariableDataTableProps) => { + // CONTEXTS + const { + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + formDataErrorObj, + validateTask, + setFormData, + setFormDataErrorObj, + calculateLastStepDetail, + uploadFile, + } = useContext(pipelineContext) + + // CONSTANTS + const headers = getVariableDataTableHeaders(type) + const defaultRowValColumnParams: GetValColumnRowPropsType = { + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + type, + format: VariableTypeFormat.STRING, + variableType: RefVariableType.NEW, + value: '', + description: null, + refVariableName: null, + refVariableStage: null, + valueConstraint: null, + } + + const isInputPluginVariable = type === PluginVariableType.INPUT + const currentStepTypeVariable = + formData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE + ? 'inlineStepDetail' + : 'pluginRefStepDetail' + + const ioVariables: VariableType[] = + formData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + isInputPluginVariable ? 'inputVariables' : 'outputVariables' + ] + + const ioVariablesError = + formDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + isInputPluginVariable ? 'inputVariables' : 'outputVariables' + ] + + // TABLE ROWS + const rows = useMemo( + () => + getVariableDataTableRows({ + ioVariables, + isCustomTask, + type, + activeStageName, + formData, + globalVariables, + selectedTaskIndex, + inputVariablesListFromPrevStep, + isCdPipeline, + }), + [ioVariables], + ) + + // TABLE CELL ERROR + const cellError = useMemo>( + () => + Object.keys(ioVariablesError).length + ? ioVariablesError + : getVariableDataTableInitialCellError(rows, headers), + [ioVariablesError, rows], + ) + + // METHODS + const handleRowUpdateAction = (rowAction: HandleRowUpdateActionProps) => { + const { actionType, rowId } = rowAction + let updatedRows = rows + const selectedRowIndex = rows.findIndex((row) => row.id === rowId) + const selectedRow = rows[selectedRowIndex] + const updatedCellError = cellError + + switch (actionType) { + case VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS: + if (selectedRow) { + const { id, data, customState } = selectedRow + + const choicesOptions = rowAction.actionValue + // RESETTING TO DEFAULT STATE IF CHOICES ARE EMPTY + const blockCustomValue = !!choicesOptions.length && customState.blockCustomValue + + const isCurrentValueValid = + !blockCustomValue || + ((!customState.valColumnSelectedValue || + !customState.valColumnSelectedValue?.variableType || + customState.valColumnSelectedValue.variableType === RefVariableType.NEW) && + choicesOptions.some((value) => value === data.val.value)) + + updatedCellError[id].val = getVariableDataTableCellValidateState({ + pluginVariableType: type, + key: InputOutputVariablesHeaderKeys.VALUE, + row: selectedRow, + }) + + selectedRow.data.val = getValColumnRowProps({ + ...defaultRowValColumnParams, + ...(!blockCustomValue && customState.valColumnSelectedValue + ? { + variableType: customState.valColumnSelectedValue.variableType, + refVariableName: customState.valColumnSelectedValue.value, + refVariableStage: customState.valColumnSelectedValue.refVariableStage, + } + : {}), + 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, + }, + }) + + selectedRow.customState = { + ...customState, + valColumnSelectedValue: !blockCustomValue ? customState.valColumnSelectedValue : null, + blockCustomValue, + choices: choicesOptions, + } + } + break + + case VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT: + if (selectedRow) { + selectedRow.customState.blockCustomValue = rowAction.actionValue + } + break + + case VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME: + if (selectedRow) { + selectedRow.customState.askValueAtRuntime = rowAction.actionValue + } + break + + case VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION: + if (selectedRow) { + selectedRow.customState.variableDescription = rowAction.actionValue + } + break + + case VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED: + if (selectedRow) { + const { id } = selectedRow + selectedRow.data.variable.required = rowAction.actionValue + selectedRow.customState.isVariableRequired = rowAction.actionValue + updatedCellError[id].variable = getVariableDataTableCellValidateState({ + pluginVariableType: type, + key: InputOutputVariablesHeaderKeys.VARIABLE, + row: selectedRow, + }) + updatedCellError[id].val = getVariableDataTableCellValidateState({ + pluginVariableType: type, + key: InputOutputVariablesHeaderKeys.VALUE, + row: selectedRow, + }) + } + break + + case VariableDataTableActionType.UPDATE_FILE_MOUNT: + if (selectedRow) { + selectedRow.customState.fileInfo.fileMountDir = rowAction.actionValue + } + break + + case VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS: + if (selectedRow) { + if (selectedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD) { + selectedRow.data.val.props.fileTypes = rowAction.actionValue.split(',') + } + selectedRow.customState.fileInfo.allowedExtensions = rowAction.actionValue + } + break + + case VariableDataTableActionType.UPDATE_FILE_MAX_SIZE: + if (selectedRow) { + selectedRow.customState.fileInfo.maxUploadSize = rowAction.actionValue.size + selectedRow.customState.fileInfo.unit = rowAction.actionValue.unit + } + break + + case VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO: + if (selectedRow && selectedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD) { + selectedRow.data.val.value = rowAction.actionValue.fileName + selectedRow.data.val.props.isLoading = rowAction.actionValue.isLoading + selectedRow.customState.fileInfo.fileReferenceId = rowAction.actionValue.fileReferenceId + } + break + + case VariableDataTableActionType.ADD_ROW: + updatedRows = [ + getEmptyVariableDataTableRow({ ...defaultRowValColumnParams, id: rowAction.rowId }), + ...updatedRows, + ] + updatedCellError[rowAction.rowId] = getVariableDataTableRowEmptyValidationState() + break + + 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: + if (selectedRow) { + updatedCellError[rowAction.rowId][rowAction.headerKey] = getVariableDataTableCellValidateState({ + pluginVariableType: type, + value: rowAction.actionValue, + row: selectedRow, + key: rowAction.headerKey, + }) + if (rowAction.headerKey === InputOutputVariablesHeaderKeys.VARIABLE) { + validateVariableDataTableVariableKeys({ + rows, + cellError: updatedCellError, + rowId: rowAction.rowId, + value: rowAction.actionValue, + }) + } + selectedRow.data[rowAction.headerKey].value = rowAction.actionValue + } + break + + case VariableDataTableActionType.UPDATE_VAL_COLUMN: + if (selectedRow && selectedRow.data.val.type === DynamicDataTableRowDataType.SELECT_TEXT) { + const { id, data, customState } = selectedRow + const { valColumnSelectedValue, value } = rowAction.actionValue + const valColumnRowValue = getValColumnRowValue( + data.format.value as VariableTypeFormat, + value, + valColumnSelectedValue, + ) + + updatedCellError[id].val = getVariableDataTableCellValidateState({ + pluginVariableType: type, + value: valColumnRowValue, + key: InputOutputVariablesHeaderKeys.VALUE, + row: selectedRow, + }) + + selectedRow.data.val = getValColumnRowProps({ + ...defaultRowValColumnParams, + value: valColumnRowValue, + ...(!customState.blockCustomValue && rowAction.actionValue.valColumnSelectedValue + ? { + variableType: rowAction.actionValue.valColumnSelectedValue.variableType, + refVariableName: rowAction.actionValue.valColumnSelectedValue.value, + refVariableStage: rowAction.actionValue.valColumnSelectedValue.refVariableStage, + } + : {}), + format: data.format.value as VariableTypeFormat, + valueConstraint: { + blockCustomValue: customState.blockCustomValue, + choices: customState.choices, + }, + }) + selectedRow.customState.valColumnSelectedValue = rowAction.actionValue.valColumnSelectedValue + } + break + + case VariableDataTableActionType.UPDATE_FORMAT_COLUMN: + if (selectedRow && selectedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN) { + const { id, customState } = selectedRow + updatedCellError[id].val = getVariableDataTableCellValidateState({ + pluginVariableType: type, + key: InputOutputVariablesHeaderKeys.VALUE, + row: selectedRow, + }) + selectedRow.data.format.value = rowAction.actionValue + selectedRow.data.val = getValColumnRowProps({ + ...defaultRowValColumnParams, + format: rowAction.actionValue, + }) + selectedRow.customState = { + ...customState, + valColumnSelectedValue: null, + choices: [], + blockCustomValue: false, + fileInfo: { + fileReferenceId: null, + allowedExtensions: '', + maxUploadSize: '', + fileMountDir: '/devtroncd', + unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], + }, + } + } + break + + default: + break + } + + // Not updating selectedRow for ADD/DELETE row, since these actions update the rows array directly. + if ( + actionType !== VariableDataTableActionType.ADD_ROW && + actionType !== VariableDataTableActionType.DELETE_ROW && + selectedRowIndex > -1 + ) { + updatedRows[selectedRowIndex] = selectedRow + } + + const { updatedFormData, updatedFormDataErrorObj } = convertVariableDataTableToFormData({ + rows: updatedRows, + cellError: updatedCellError, + activeStageName, + formData, + formDataErrorObj, + selectedTaskIndex, + type, + validateTask, + calculateLastStepDetail, + }) + setFormDataErrorObj(updatedFormDataErrorObj) + setFormData(updatedFormData) + } + + const handleRowAdd = () => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.ADD_ROW, + rowId: Math.floor(new Date().valueOf() * Math.random()), + }) + } + + 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 === InputOutputVariablesHeaderKeys.VALUE && + updatedRow.data.val.type === DynamicDataTableRowDataType.FILE_UPLOAD + ) { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: null, isLoading: false, fileName: value }, + rowId: updatedRow.id, + }) + + if (extraData.files.length) { + try { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: null, isLoading: true, fileName: value }, + rowId: updatedRow.id, + }) + + const { id, name } = await uploadFile({ + file: extraData.files, + ...getUploadFileConstraints({ + unit: updatedRow.customState.fileInfo.unit.label as string, + allowedExtensions: updatedRow.customState.fileInfo.allowedExtensions, + maxUploadSize: updatedRow.customState.fileInfo.maxUploadSize, + }), + }) + + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: id, isLoading: false, fileName: name }, + rowId: updatedRow.id, + }) + } catch { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO, + actionValue: { fileReferenceId: null, isLoading: false, fileName: '' }, + rowId: updatedRow.id, + }) + } + } + } else if ( + headerKey === InputOutputVariablesHeaderKeys.FORMAT && + updatedRow.data.format.type === DynamicDataTableRowDataType.DROPDOWN + ) { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_FORMAT_COLUMN, + actionValue: value as VariableTypeFormat, + rowId: updatedRow.id, + }) + } else { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.UPDATE_ROW, + actionValue: value, + headerKey, + rowId: updatedRow.id, + }) + } + } + + const handleRowDelete: DynamicDataTableProps< + InputOutputVariablesHeaderKeys, + VariableDataCustomState + >['onRowDelete'] = (row) => { + handleRowUpdateAction({ + actionType: VariableDataTableActionType.DELETE_ROW, + rowId: row.id, + }) + } + + // RENDERERS + const actionButtonRenderer = (row: VariableDataRowType) => ( + + ) + + const getTrailingCellIconForVariableColumn = (row: VariableDataRowType) => + isCustomTask && isInputPluginVariable ? ( + + ) : null + + const getTrailingCellIconForValueColumn = (row: VariableDataRowType) => + isInputPluginVariable && row.data.format.value === VariableTypeFormat.FILE ? ( + + ) : null + + const trailingCellIcon: DynamicDataTableProps['trailingCellIcon'] = { + variable: getTrailingCellIconForVariableColumn, + val: getTrailingCellIconForValueColumn, + } + + return ( +
+ {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 new file mode 100644 index 0000000000..4bd4981a75 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/VariableDataTablePopupMenu.tsx @@ -0,0 +1,70 @@ +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 { ReactComponent as ICDot } from '@Icons/ic-dot.svg' + +import { VariableDataTablePopupMenuProps } from './types' + +export const VariableDataTablePopupMenu = ({ + showHeaderIcon, + showIconDot, + heading, + children, + onClose, + disableClose = false, + placement, +}: VariableDataTablePopupMenuProps) => { + // STATES + const [visible, setVisible] = useState(false) + + // METHODS + const handleClose = () => { + if (!disableClose) { + setVisible(false) + onClose?.() + } + } + + const handleOpen = () => { + setVisible(true) + } + + return ( + <> + {heading}

} + Icon={showHeaderIcon ? ICSlidersVertical : null} + iconSize={16} + additionalContent={visible ?
{children}
: null} + > + +
+ {visible &&
} + + ) +} diff --git a/src/components/CIPipelineN/VariableDataTable/constants.ts b/src/components/CIPipelineN/VariableDataTable/constants.ts new file mode 100644 index 0000000000..36526c6ab0 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/constants.ts @@ -0,0 +1,102 @@ +import { + DynamicDataTableHeaderType, + InputOutputVariablesHeaderKeys, + SelectPickerOptionType, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' + +import { importComponentFromFELibrary } from '@Components/common' +import { PluginVariableType } from '@Components/ciPipeline/types' + +const isFELibAvailable = importComponentFromFELibrary('isFELibAvailable', null, 'function') + +export const getVariableDataTableHeaders = ( + type: PluginVariableType, +): DynamicDataTableHeaderType[] => [ + { + label: 'VARIABLE', + key: InputOutputVariablesHeaderKeys.VARIABLE, + width: '200px', + }, + { + label: 'TYPE', + key: InputOutputVariablesHeaderKeys.FORMAT, + width: '100px', + }, + { + label: type === PluginVariableType.INPUT ? 'VALUE' : 'DESCRIPTION', + key: InputOutputVariablesHeaderKeys.VALUE, + width: '1fr', + }, +] + +export const VAL_COLUMN_DROPDOWN_LABEL = { + CHOICES: 'Choices', + SUPPORTED_DATE_FORMATS: 'Supported date formats', + 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: Record = { + [VariableTypeFormat.STRING]: 'String', + [VariableTypeFormat.NUMBER]: 'Number', + [VariableTypeFormat.BOOL]: 'Boolean', + [VariableTypeFormat.DATE]: 'Date', + [VariableTypeFormat.FILE]: 'File', +} + +export const FORMAT_COLUMN_OPTIONS: SelectPickerOptionType[] = [ + { + label: FORMAT_OPTIONS_MAP.STRING, + value: VariableTypeFormat.STRING, + }, + { + label: FORMAT_OPTIONS_MAP.NUMBER, + value: VariableTypeFormat.NUMBER, + }, + { + label: FORMAT_OPTIONS_MAP.BOOL, + value: VariableTypeFormat.BOOL, + }, + { + label: FORMAT_OPTIONS_MAP.DATE, + value: VariableTypeFormat.DATE, + }, + ...(isFELibAvailable + ? [ + { + label: FORMAT_OPTIONS_MAP.FILE, + value: VariableTypeFormat.FILE, + }, + ] + : []), +] + +export const FILE_UPLOAD_SIZE_UNIT_OPTIONS: SelectPickerOptionType[] = [ + { + label: 'KB', + value: 1024, + }, + { + label: 'MB', + value: 1 / 1024, + }, +] + +export const VARIABLE_DATA_TABLE_EMPTY_ROW_MESSAGE: Record = { + [PluginVariableType.INPUT]: + 'Input variables are available as environment variables and can be used in the script to inject values from previous tasks or other sources.', + [PluginVariableType.OUTPUT]: + 'Output variables must be set in the environment variables and can be used as input variables in other scripts.', +} + +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/index.ts b/src/components/CIPipelineN/VariableDataTable/index.ts new file mode 100644 index 0000000000..46eeb15f03 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/index.ts @@ -0,0 +1 @@ +export * from './VariableDataTable.component' diff --git a/src/components/CIPipelineN/VariableDataTable/types.ts b/src/components/CIPipelineN/VariableDataTable/types.ts new file mode 100644 index 0000000000..9b23cbfa65 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/types.ts @@ -0,0 +1,198 @@ +import { PluginVariableType } from '@Components/ciPipeline/types' +import { PipelineContext } from '@Components/workflowEditor/types' +import { + DynamicDataTableCellErrorType, + DynamicDataTableRowType, + InputOutputVariablesHeaderKeys, + RefVariableStageType, + RefVariableType, + SelectPickerOptionType, + TippyCustomizedProps, + 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 + refVariableStepIndex?: number +} + +export type VariableDataCustomState = { + defaultValue: string + variableDescription: string + isVariableRequired: boolean + choices: string[] + askValueAtRuntime: boolean + blockCustomValue: boolean + valColumnSelectedValue: VariableDataTableSelectPickerOptionType + fileInfo: Pick & { + 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_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.ADD_ROW]: {} + [VariableDataTableActionType.UPDATE_ROW]: { + actionValue: string + headerKey: InputOutputVariablesHeaderKeys + } + [VariableDataTableActionType.DELETE_ROW]: {} + [VariableDataTableActionType.UPDATE_VAL_COLUMN]: { + actionValue: { + value: string + valColumnSelectedValue: VariableDataTableSelectPickerOptionType + } + } + [VariableDataTableActionType.UPDATE_FORMAT_COLUMN]: { + actionValue: VariableTypeFormat + } + [VariableDataTableActionType.UPDATE_FILE_UPLOAD_INFO]: { + actionValue: Pick & { + fileName: string + isLoading: boolean + } + } + + [VariableDataTableActionType.UPDATE_ALLOW_CUSTOM_INPUT]: { + actionValue: VariableDataCustomState['blockCustomValue'] + } + [VariableDataTableActionType.UPDATE_ASK_VALUE_AT_RUNTIME]: { + actionValue: VariableDataCustomState['askValueAtRuntime'] + } + [VariableDataTableActionType.ADD_CHOICES_TO_VALUE_COLUMN_OPTIONS]: { + actionValue: string[] + } + [VariableDataTableActionType.UPDATE_FILE_MOUNT]: { + actionValue: string + } + [VariableDataTableActionType.UPDATE_FILE_ALLOWED_EXTENSIONS]: { + actionValue: string + } + [VariableDataTableActionType.UPDATE_FILE_MAX_SIZE]: { + actionValue: { + size: string + unit: SelectPickerOptionType + } + } + + [VariableDataTableActionType.UPDATE_VARIABLE_DESCRIPTION]: { + actionValue: VariableDataCustomState['variableDescription'] + } + [VariableDataTableActionType.UPDATE_VARIABLE_REQUIRED]: { + actionValue: VariableDataCustomState['isVariableRequired'] + } +} + +export type VariableDataTableAction< + T extends keyof VariableDataTableActionPropsMap = keyof VariableDataTableActionPropsMap, +> = T extends keyof VariableDataTableActionPropsMap + ? { actionType: T; rowId: string | number } & VariableDataTableActionPropsMap[T] + : never + +export type HandleRowUpdateActionProps = VariableDataTableAction + +export interface VariableDataTablePopupMenuProps extends Pick { + heading: string + showHeaderIcon?: boolean + showIconDot?: boolean + disableClose?: boolean + onClose?: () => void + children: JSX.Element +} + +export interface ConfigOverlayProps { + row: VariableDataRowType + handleRowUpdateAction: (props: HandleRowUpdateActionProps) => void +} + +export type GetValColumnRowPropsType = Pick< + PipelineContext, + | 'activeStageName' + | 'formData' + | 'globalVariables' + | 'isCdPipeline' + | 'selectedTaskIndex' + | 'inputVariablesListFromPrevStep' +> & + Pick< + VariableType, + 'format' | 'value' | 'refVariableName' | 'refVariableStage' | 'valueConstraint' | 'description' | 'variableType' + > & { type: PluginVariableType } + +export interface GetVariableDataTableInitialRowsProps + extends Omit< + GetValColumnRowPropsType, + 'description' | 'format' | 'variableType' | 'value' | 'refVariableName' | 'refVariableStage' | 'valueConstraint' + > { + ioVariables: VariableType[] + type: PluginVariableType + isCustomTask: boolean +} + +export type GetValidateCellProps = { + pluginVariableType: PluginVariableType + row: VariableDataRowType + value?: string + key: InputOutputVariablesHeaderKeys +} + +export interface ValidateVariableDataTableKeysProps { + rows: VariableDataRowType[] + cellError: DynamicDataTableCellErrorType + rowId?: string | number + value?: string +} + +export interface ValidateInputOutputVariableCellProps { + variable: Pick< + VariableType, + | 'allowEmptyValue' + | 'isRuntimeArg' + | 'defaultValue' + | 'variableType' + | 'refVariableName' + | 'value' + | 'description' + | 'name' + | 'refVariableStepIndex' + | 'refVariableStage' + | 'format' + > + key: InputOutputVariablesHeaderKeys + type: PluginVariableType + keysFrequencyMap?: Record +} diff --git a/src/components/CIPipelineN/VariableDataTable/utils.tsx b/src/components/CIPipelineN/VariableDataTable/utils.tsx new file mode 100644 index 0000000000..deec39344b --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/utils.tsx @@ -0,0 +1,639 @@ +import { + ConditionType, + DynamicDataTableCellErrorType, + DynamicDataTableHeaderType, + DynamicDataTableRowDataType, + getGoLangFormattedDateWithTimezone, + InputOutputVariablesHeaderKeys, + IO_VARIABLES_VALUE_COLUMN_BOOL_OPTIONS, + IO_VARIABLES_VALUE_COLUMN_DATE_OPTIONS, + PluginType, + RefVariableStageType, + RefVariableType, + SelectPickerOptionType, + SystemVariableIcon, + VariableType, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' + +import { BuildStageVariable } from '@Config/constants' +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_DROPDOWN_LABEL } from './constants' +import { + GetValColumnRowPropsType, + GetVariableDataTableInitialRowsProps, + VariableDataTableSelectPickerOptionType, + VariableDataRowType, +} from './types' + +export const getOptionsForValColumn = ({ + inputVariablesListFromPrevStep, + activeStageName, + selectedTaskIndex, + formData, + globalVariables, + isCdPipeline, + format, + valueConstraint, +}: Pick< + PipelineContext, + | 'activeStageName' + | 'selectedTaskIndex' + | 'inputVariablesListFromPrevStep' + | 'formData' + | 'globalVariables' + | 'isCdPipeline' +> & + Pick) => { + const isBuildStagePostBuild = activeStageName === BuildStageVariable.PostBuild + + const previousStepVariables = [] + const preBuildStageVariables = [] + + const supportedDataFormats = [] + const choices = (valueConstraint?.choices || []).map>((value) => ({ + label: value, + value, + })) + + if (format === VariableTypeFormat.BOOL) { + choices.push(...IO_VARIABLES_VALUE_COLUMN_BOOL_OPTIONS) + } + + if (format === VariableTypeFormat.DATE) { + supportedDataFormats.push(...IO_VARIABLES_VALUE_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 (isBuildStagePostBuild) { + 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, + }) + } + } + } + } + + 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 = + !choices.length && + !supportedDataFormats.length && + (isBuildStagePostBuild + ? !filteredPreBuildStageVariablesBasedOnFormat.length && !filteredPreviousStepVariablesBasedOnFormat.length + : !filteredPreviousStepVariablesBasedOnFormat.length) && + !filteredGlobalVariablesBasedOnFormat.length + + if (isOptionsEmpty) { + return [] + } + + return [ + { + label: VAL_COLUMN_DROPDOWN_LABEL.CHOICES, + options: choices, + }, + { + label: VAL_COLUMN_DROPDOWN_LABEL.SUPPORTED_DATE_FORMATS, + options: supportedDataFormats, + }, + ...(!valueConstraint?.blockCustomValue + ? [ + ...(isBuildStagePostBuild + ? [ + { + label: VAL_COLUMN_DROPDOWN_LABEL.PRE_BUILD_STAGE, + options: filteredPreBuildStageVariablesBasedOnFormat, + }, + { + label: VAL_COLUMN_DROPDOWN_LABEL.POST_BUILD_STAGE, + options: filteredPreviousStepVariablesBasedOnFormat, + }, + ] + : [ + { + label: VAL_COLUMN_DROPDOWN_LABEL.PREVIOUS_STEPS, + options: filteredPreviousStepVariablesBasedOnFormat, + }, + ]), + { + label: VAL_COLUMN_DROPDOWN_LABEL.SYSTEM_VARIABLES, + options: filteredGlobalVariablesBasedOnFormat, + }, + ] + : []), + ] +} + +export const getVariableColumnRowProps = () => { + const data: VariableDataRowType['data']['variable'] = { + value: '', + type: DynamicDataTableRowDataType.TEXT, + props: { + placeholder: 'Enter variable name', + }, + } + + return data +} + +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 { + type: DynamicDataTableRowDataType.TEXT, + value: format, + disabled: true, + props: {}, + } +} + +export const getValColumnRowProps = ({ + format, + type, + variableType = RefVariableType.NEW, + 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 || [], + }, + } + } + + const optionsForValColumn = getOptionsForValColumn({ + activeStageName, + formData, + globalVariables, + isCdPipeline, + selectedTaskIndex, + inputVariablesListFromPrevStep, + format, + valueConstraint, + }) + + if (!optionsForValColumn.length) { + 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: optionsForValColumn, + selectPickerProps: { + isCreatable: + format !== VariableTypeFormat.BOOL && + (!valueConstraint?.choices?.length || !valueConstraint.blockCustomValue), + formatCreateLabel: (inputValue) => `Use ${inputValue}`, + }, + Icon: + refVariableStage || (variableType && variableType !== RefVariableType.NEW) ? ( + + ) : null, + }, + } + } + + return { + type: DynamicDataTableRowDataType.TEXT, + value: description ?? '', + props: { + placeholder: !description ? 'No description available' : '', + }, + } +} + +export const checkForSystemVariable = (option: VariableDataTableSelectPickerOptionType) => { + const isSystemVariable = + !!option?.refVariableStage || (option?.variableType && option.variableType !== RefVariableType.NEW) + + return isSystemVariable +} + +export const getValColumnRowValue = ( + format: VariableTypeFormat, + value: string, + valColumnSelectedValue: VariableDataTableSelectPickerOptionType, +) => { + const isSystemVariable = checkForSystemVariable(valColumnSelectedValue) + const isDateFormat = !isSystemVariable && value && format === VariableTypeFormat.DATE + + return isDateFormat ? getGoLangFormattedDateWithTimezone(valColumnSelectedValue.value) : value +} + +export const getEmptyVariableDataTableRow = ({ + id, + ...params +}: GetValColumnRowPropsType & { id: string | number }): VariableDataRowType => { + const data: VariableDataRowType = { + data: { + variable: getVariableColumnRowProps(), + format: getFormatColumnRowProps({ format: VariableTypeFormat.STRING, isCustomTask: true }), + val: getValColumnRowProps(params), + }, + id, + customState: { + defaultValue: '', + variableDescription: '', + isVariableRequired: false, + choices: [], + askValueAtRuntime: false, + blockCustomValue: false, + valColumnSelectedValue: null, + fileInfo: { + fileReferenceId: null, + fileMountDir: '/devtroncd', + allowedExtensions: '', + maxUploadSize: '', + unit: FILE_UPLOAD_SIZE_UNIT_OPTIONS[0], + }, + }, + } + + return data +} + +export const getVariableDataTableRows = ({ + ioVariables, + type, + isCustomTask, + ...restProps +}: GetVariableDataTableInitialRowsProps): VariableDataRowType[] => + (ioVariables || []).map( + ({ + name, + allowEmptyValue, + description, + format, + variableType, + value, + refVariableName, + refVariableStage, + valueConstraint, + isRuntimeArg, + fileMountDir, + fileReferenceId, + defaultValue, + id, + }) => { + const isInputVariableRequired = type === PluginVariableType.INPUT && !allowEmptyValue + const valColumnValue = getValColumnRowProps({ + ...restProps, + type, + description, + format, + value, + variableType, + refVariableName, + refVariableStage, + valueConstraint, + }) + + return { + data: { + variable: { + ...getVariableColumnRowProps(), + value: name, + required: isInputVariableRequired, + disabled: !isCustomTask, + tooltip: { + content: type === PluginVariableType.INPUT && !isCustomTask && description, + }, + }, + format: getFormatColumnRowProps({ format, isCustomTask }), + val: valColumnValue, + }, + customState: { + defaultValue, + isVariableRequired: isInputVariableRequired, + variableDescription: description ?? '', + choices: valueConstraint?.choices || [], + askValueAtRuntime: isRuntimeArg ?? false, + blockCustomValue: valueConstraint?.blockCustomValue ?? false, + valColumnSelectedValue: + valColumnValue.type === DynamicDataTableRowDataType.SELECT_TEXT + ? { + label: refVariableName || value, + value: refVariableName || value, + refVariableName, + refVariableStage, + variableType: refVariableName ? RefVariableType.GLOBAL : RefVariableType.NEW, + format, + } + : null, + fileInfo: { + fileReferenceId, + 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 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, + 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, + cellError, + type, + activeStageName, + selectedTaskIndex, + formData, + formDataErrorObj, + validateTask, + calculateLastStepDetail, +}: Pick< + PipelineContext, + | 'activeStageName' + | 'selectedTaskIndex' + | 'formData' + | 'formDataErrorObj' + | 'validateTask' + | 'calculateLastStepDetail' +> & { + type: PluginVariableType + rows: VariableDataRowType[] + cellError: DynamicDataTableCellErrorType +}) => { + const updatedFormData = structuredClone(formData) + const updatedFormDataErrorObj = structuredClone(formDataErrorObj) + + const currentStepTypeVariable = + updatedFormData[activeStageName].steps[selectedTaskIndex].stepType === PluginType.INLINE + ? 'inlineStepDetail' + : 'pluginRefStepDetail' + + const isInputVariable = type === PluginVariableType.INPUT + + const updatedIOVariables = rows.map(({ data, customState, id }) => { + const { + askValueAtRuntime, + blockCustomValue, + choices, + valColumnSelectedValue, + isVariableRequired, + variableDescription, + fileInfo, + } = customState + + const variableDetail: VariableType = { + // setting undefined will not send these keys in payload + allowEmptyValue: undefined, + refVariableStepIndex: undefined, + refVariableName: undefined, + refVariableStage: undefined, + variableStepIndexInPlugin: undefined, + fileMountDir: undefined, + fileReferenceId: undefined, + valueConstraintId: undefined, + valueConstraint: undefined, + isRuntimeArg: undefined, + refVariableUsed: undefined, + defaultValue: customState.defaultValue, + id: +id, + value: data.val.value, + format: data.format.value as VariableTypeFormat, + name: data.variable.value, + description: isInputVariable ? variableDescription : data.val.value, + variableType: RefVariableType.NEW, + } + + if (isInputVariable) { + variableDetail.allowEmptyValue = !isVariableRequired + variableDetail.isRuntimeArg = askValueAtRuntime + + if ( + variableDetail.format === VariableTypeFormat.STRING || + variableDetail.format === VariableTypeFormat.NUMBER + ) { + variableDetail.valueConstraint = { + ...variableDetail.valueConstraint, + choices: choices.length ? choices : null, + blockCustomValue, + } + } else if (variableDetail.format === VariableTypeFormat.FILE && fileInfo) { + variableDetail.variableType = RefVariableType.NEW + variableDetail.refVariableName = '' + variableDetail.refVariableStage = null + variableDetail.fileReferenceId = fileInfo.fileReferenceId + variableDetail.fileMountDir = fileInfo.fileMountDir + variableDetail.valueConstraint = { + ...variableDetail.valueConstraint, + constraint: { + fileProperty: getUploadFileConstraints({ + allowedExtensions: fileInfo.allowedExtensions, + maxUploadSize: fileInfo.maxUploadSize, + unit: fileInfo.unit.label as string, + }), + }, + } + } + + if (valColumnSelectedValue) { + if (valColumnSelectedValue.refVariableStepIndex) { + variableDetail.value = '' + variableDetail.variableType = RefVariableType.FROM_PREVIOUS_STEP + 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 = valColumnSelectedValue.label as string + variableDetail.format = valColumnSelectedValue.format + variableDetail.refVariableStage = null + } else { + if (variableDetail.format !== VariableTypeFormat.DATE) { + variableDetail.value = valColumnSelectedValue.label as string + } + variableDetail.variableType = RefVariableType.NEW + variableDetail.refVariableName = '' + variableDetail.refVariableStage = null + } + } + } + + return variableDetail + }) + + updatedFormData[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + isInputVariable ? 'inputVariables' : 'outputVariables' + ] = updatedIOVariables + + if (type === PluginVariableType.OUTPUT) { + calculateLastStepDetail(false, updatedFormData, activeStageName, selectedTaskIndex) + } + + 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)) || + (isInputVariable && + (conditionDetails[i].conditionType === ConditionType.TRIGGER || + conditionDetails[i].conditionType === ConditionType.SKIP)) + ) { + conditionDetails.splice(i, 1) + i -= 1 + } + } + 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][ + isInputVariable ? 'isInputVariablesValid' : 'isOutputVariablesValid' + ] = isValid + + updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex][currentStepTypeVariable][ + isInputVariable ? 'inputVariables' : 'outputVariables' + ] = cellError + + validateTask( + updatedFormData[activeStageName].steps[selectedTaskIndex], + updatedFormDataErrorObj[activeStageName].steps[selectedTaskIndex], + { validateVariableDataTable: false }, + ) + + return { updatedFormDataErrorObj, updatedFormData } +} diff --git a/src/components/CIPipelineN/VariableDataTable/validations.ts b/src/components/CIPipelineN/VariableDataTable/validations.ts new file mode 100644 index 0000000000..c6e18635a1 --- /dev/null +++ b/src/components/CIPipelineN/VariableDataTable/validations.ts @@ -0,0 +1,187 @@ +import { + DynamicDataTableCellValidationState, + PATTERNS as FE_COMMON_LIB_PATTERNS, + InputOutputVariablesHeaderKeys, + RefVariableType, + VariableTypeFormat, +} from '@devtron-labs/devtron-fe-common-lib' + +import { PluginVariableType } from '@Components/ciPipeline/types' +import { PATTERNS } from '@Config/constants' + +import { + GetValidateCellProps, + ValidateInputOutputVariableCellProps, + ValidateVariableDataTableKeysProps, + VariableDataRowType, +} from './types' +import { VARIABLE_DATA_TABLE_CELL_ERROR_MSGS } from './constants' + +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 getVariableDataTableRowEmptyValidationState = (): Record< + InputOutputVariablesHeaderKeys, + DynamicDataTableCellValidationState +> => ({ + [InputOutputVariablesHeaderKeys.VARIABLE]: { + isValid: true, + errorMessages: [], + }, + [InputOutputVariablesHeaderKeys.FORMAT]: { + isValid: true, + errorMessages: [], + }, + [InputOutputVariablesHeaderKeys.VALUE]: { + isValid: true, + errorMessages: [], + }, +}) + +export const validateInputOutputVariableCell = ({ + variable, + key, + type, + keysFrequencyMap = {}, +}: ValidateInputOutputVariableCellProps): DynamicDataTableCellValidationState => { + const { + allowEmptyValue, + isRuntimeArg, + defaultValue, + value, + variableType, + refVariableName, + refVariableStepIndex, + refVariableStage, + description, + format, + name, + } = variable + + const variableNameReg = new RegExp(PATTERNS.VARIABLE) + const numberReg = new RegExp(FE_COMMON_LIB_PATTERNS.NUMBERS_WITH_SCOPE_VARIABLES) + + const isInputVariable = type === PluginVariableType.INPUT + + const variableValue = + allowEmptyValue || + (!allowEmptyValue && isRuntimeArg) || + (!allowEmptyValue && defaultValue && defaultValue !== '') || + (variableType === RefVariableType.NEW && value) || + (refVariableName && + (variableType === RefVariableType.GLOBAL || + (variableType === RefVariableType.FROM_PREVIOUS_STEP && refVariableStepIndex && refVariableStage))) + + if (key === 'variable') { + if (isInputVariable && !name && !variableValue && !description) { + return { errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.EMPTY_ROW], isValid: false } + } + if (!name) { + return { + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.VARIABLE_NAME_REQUIRED], + isValid: false, + } + } + if ((keysFrequencyMap[name] ?? 0) > 1) { + return { + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.UNIQUE_VARIABLE_NAME], + isValid: false, + } + } + if (!variableNameReg.test(name)) { + return { + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.INVALID_VARIABLE_NAME], + isValid: false, + } + } + } + + if (isInputVariable && key === 'val') { + if (!variableValue) { + return { + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.VARIABLE_VALUE_REQUIRED], + isValid: false, + } + } + // test for numbers and scope variables when format is "NUMBER". + if (format === VariableTypeFormat.NUMBER && variableValue && !!value && !numberReg.test(value)) { + return { + errorMessages: [VARIABLE_DATA_TABLE_CELL_ERROR_MSGS.VARIABLE_VALUE_NOT_A_NUMBER], + isValid: false, + } + } + } + + return { errorMessages: [], isValid: true } +} + +export const getVariableDataTableCellValidateState = ({ + row: { data, customState }, + key, + value: latestValue, + pluginVariableType, +}: GetValidateCellProps): DynamicDataTableCellValidationState => { + const value = latestValue ?? data[key].value + const { variableDescription, isVariableRequired, valColumnSelectedValue, askValueAtRuntime, defaultValue } = + customState + + return validateInputOutputVariableCell({ + key, + type: pluginVariableType, + variable: { + allowEmptyValue: !isVariableRequired, + isRuntimeArg: askValueAtRuntime, + defaultValue, + name: key === 'variable' ? value : data.variable.value, + value: key === 'val' ? value : data.val.value, + variableType: valColumnSelectedValue?.variableType ?? RefVariableType.NEW, + description: variableDescription, + format: data.format.value as VariableTypeFormat, + refVariableName: valColumnSelectedValue?.refVariableName, + refVariableStepIndex: valColumnSelectedValue?.refVariableStepIndex, + refVariableStage: valColumnSelectedValue?.refVariableStage, + }, + }) +} + +export const validateVariableDataTableVariableKeys = ({ + rows, + rowId, + value, + cellError, +}: ValidateVariableDataTableKeysProps) => { + 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: [], + } + } + }) +} diff --git a/src/components/app/details/triggerView/CIMaterialModal.tsx b/src/components/app/details/triggerView/CIMaterialModal.tsx index 7a51404aa4..a855923004 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,16 @@ export const CIMaterialModal = ({ [props.filteredCIPipelines, props.pipelineId], ) + const uploadFile = ({ file, allowedExtensions, maxUploadSize }) => + uploadCIPipelineFile({ + file, + allowedExtensions, + maxUploadSize, + appId: +props.appId, + ciPipelineId: +props.pipelineId, + envId: props.isJobView && props.selectedEnv ? +props.selectedEnv : null, + }) + usePrompt({ shouldPrompt: isLoading }) useEffect( @@ -84,7 +95,13 @@ export const CIMaterialModal = ({
) : ( - + )} ) diff --git a/src/components/app/details/triggerView/TriggerView.tsx b/src/components/app/details/triggerView/TriggerView.tsx index f8fdab5856..1929fafd54 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' @@ -1095,7 +1096,6 @@ class TriggerView extends Component { this.getWorkflowStatus() } - onClickWebhookTimeStamp = () => { if (this.state.webhookTimeStampOrder === TIME_STAMP_ORDER.DESCENDING) { this.setState({ webhookTimeStampOrder: TIME_STAMP_ORDER.ASCENDING }) @@ -1172,16 +1172,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 ( - + ) => { // stageType should handle approval node, compute CDMaterialServiceEnum, create queryParams state // FIXME: the query params returned by useSearchString seems faulty @@ -268,8 +273,11 @@ 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 [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState(false) + const [runtimeParamsList, setRuntimeParamsList] = useState([]) + const [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState({ + isValid: true, + cellError: {}, + }) const [value, setValue] = useState() const [showDeploymentWindowConfirmation, setShowDeploymentWindowConfirmation] = useState(false) @@ -383,7 +391,7 @@ const CDMaterial = ({ setTagsEditable(materialsResult.tagsEditable) setAppReleaseTagNames(materialsResult.appReleaseTagNames) setNoMoreImages(materialsResult.materials.length >= materialsResult.totalCount) - setRuntimeParamsList(materialsResult.runtimeParams || []) + setRuntimeParamsList(materialsResult.runtimeParams) setMaterial(_newMaterials) const _isConsumedImageAvailable = @@ -403,7 +411,7 @@ const CDMaterial = ({ setTagsEditable(materialsResult.tagsEditable) setAppReleaseTagNames(materialsResult.appReleaseTagNames) setNoMoreImages(materialsResult.materials.length >= materialsResult.totalCount) - setRuntimeParamsList(materialsResult.runtimeParams || []) + setRuntimeParamsList(materialsResult.runtimeParams) setMaterial(materialsResult.materials) const _isConsumedImageAvailable = @@ -559,16 +567,24 @@ const CDMaterial = ({ })) } - const handleRuntimeParamChange: typeof handleBulkRuntimeParamChange = ( - updatedRuntimeParams: RuntimeParamsListItemType[], - ) => { + const handleRuntimeParamChange: typeof handleBulkRuntimeParamChange = (updatedRuntimeParams) => { setRuntimeParamsList(updatedRuntimeParams) } - const handleRuntimeParamError = (errorState: boolean) => { - setRuntimeParamsErrorState(errorState) + const onRuntimeParamsError = (updatedRuntimeParamsErrorState: typeof runtimeParamsErrorState) => { + setRuntimeParamsErrorState(updatedRuntimeParamsErrorState) } + const handleUploadFile: typeof bulkUploadFile = ({ file, allowedExtensions, maxUploadSize }) => + uploadCDPipelineFile({ file, allowedExtensions, maxUploadSize, appId, envId }) + + // RUNTIME PARAMETERS PROPS + const parameters = bulkRuntimeParams || runtimeParamsList + const errorState = bulkRuntimeParamErrorState || runtimeParamsErrorState + const handleRuntimeParamsChange = handleBulkRuntimeParamChange || handleRuntimeParamChange + const handleRuntimeParamsError = handleBulkRuntimeParamError || onRuntimeParamsError + const uploadRuntimeParamsFile = bulkUploadFile || handleUploadFile + const clearSearch = (e: React.MouseEvent): void => { stopPropagation(e) if (state.searchText) { @@ -699,14 +715,6 @@ const CDMaterial = ({ } const handleSidebarTabChange = (e: React.ChangeEvent) => { - if (runtimeParamsErrorState) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Please resolve all the errors before switching tabs', - }) - return - } - setCurrentSidebarTab(e.target.value as CDMaterialSidebarType) } @@ -864,7 +872,9 @@ const CDMaterial = ({ deploymentWithConfig?: string, wfrId?: number, ) => { - if (runtimeParamsErrorState) { + const updatedRuntimeParamsErrorState = validateRuntimeParameters(parameters) + handleRuntimeParamsError(updatedRuntimeParamsErrorState) + if (!updatedRuntimeParamsErrorState.isValid) { ToastManager.showToast({ variant: ToastVariantType.error, description: 'Please resolve all the errors before deploying', @@ -1462,6 +1472,9 @@ const CDMaterial = ({ tabs={CD_MATERIAL_SIDEBAR_TABS} initialTab={currentSidebarTab} onChange={areTabsDisabled ? noop : handleSidebarTabChange} + hasError={{ + [CDMaterialSidebarType.PARAMETERS]: !runtimeParamsErrorState.isValid, + }} /> )} @@ -1542,11 +1555,13 @@ const CDMaterial = ({ ) : ( )} @@ -1711,8 +1726,7 @@ const CDMaterial = ({ )} > - {AllowedWithWarningTippy && - showPluginWarningBeforeTrigger ? ( + {AllowedWithWarningTippy && showPluginWarningBeforeTrigger ? ( { static contextType: React.Context = TriggerViewContext @@ -64,7 +66,10 @@ class CIMaterial extends Component { savingRegexValue: false, isBlobStorageConfigured: false, currentSidebarTab: CIMaterialSidebarType.CODE_SOURCE, - runtimeParamsErrorState: false, + runtimeParamsErrorState: { + isValid: true, + cellError: {}, + }, } } @@ -88,21 +93,13 @@ class CIMaterial extends Component { } catch (error) {} } - handleRuntimeParamError = (errorState: boolean) => { + handleRuntimeParamError = (updatedRuntimeParamsErrorState: typeof this.state.runtimeParamsErrorState) => { this.setState({ - runtimeParamsErrorState: errorState, + runtimeParamsErrorState: updatedRuntimeParamsErrorState, }) } handleSidebarTabChange = (e: React.ChangeEvent) => { - if (this.state.runtimeParamsErrorState) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Please resolve all the errors before switching tabs', - }) - return - } - this.setState({ currentSidebarTab: e.target.value as CIMaterialSidebarType, }) @@ -187,7 +184,10 @@ class CIMaterial extends Component { } handleStartBuildAction = (e) => { - if (this.state.runtimeParamsErrorState) { + const runtimeParamsErrorState = validateRuntimeParameters(this.props.runtimeParams) + this.handleRuntimeParamError(runtimeParamsErrorState) + + if (!runtimeParamsErrorState.isValid) { ToastManager.showToast({ variant: ToastVariantType.error, description: 'Please resolve all the errors before starting the build', @@ -212,7 +212,7 @@ class CIMaterial extends Component { ) } - renderCTAButtonWithIcon = (canTrigger, isCTAActionable: boolean = true ) => ( + renderCTAButtonWithIcon = (canTrigger, isCTAActionable: boolean = true) => (