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 (
+
+
+ }
+ variant={ButtonVariantType.text}
+ size={ComponentSizeType.small}
+ />
+
+
+ {choices.map(({ id, value, error }, index) => (
+
+
+
+ }
+ variant={ButtonVariantType.borderLess}
+ size={ComponentSizeType.medium}
+ onClick={handleChoiceDelete(id)}
+ style={ButtonStyleType.negativeGrey}
+ />
+
+
+ ))}
+
+
+ )
+ }
+
+ return (
+
+
+
+
+
Set value choices
+
Allow users to select a value from a pre-defined set of choices
+
+
+ }
+ variant={ButtonVariantType.text}
+ size={ComponentSizeType.small}
+ />
+
+
+
+ )
+ }
+
+ 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 && (
+ }
+ onClick={handleRowAdd}
+ />
+ )}
+
+ )}
+ {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) => (