diff --git a/.eslintignore b/.eslintignore index ecbb7d4089..75f0ff3a09 100755 --- a/.eslintignore +++ b/.eslintignore @@ -102,10 +102,7 @@ src/components/app/details/testViewer/TestRunDetails.tsx src/components/app/details/testViewer/TestRunList.tsx src/components/app/details/triggerView/EmptyStateCIMaterial.tsx src/components/app/details/triggerView/MaterialSource.tsx -src/components/app/details/triggerView/TriggerView.tsx src/components/app/details/triggerView/__tests__/triggerview.test.tsx -src/components/app/details/triggerView/cdMaterial.tsx -src/components/app/details/triggerView/ciMaterial.tsx src/components/app/details/triggerView/ciWebhook.service.ts src/components/app/details/triggerView/config.ts src/components/app/details/triggerView/workflow.service.ts diff --git a/package.json b/package.json index 0510a01230..22c3b8b80b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "homepage": "/dashboard", "dependencies": { - "@devtron-labs/devtron-fe-common-lib": "1.19.0-pre-4", + "@devtron-labs/devtron-fe-common-lib": "1.19.0-pre-5", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@rjsf/core": "^5.13.3", "@rjsf/utils": "^5.13.3", diff --git a/src/components/ApplicationGroup/AppGroup.types.ts b/src/components/ApplicationGroup/AppGroup.types.ts index e9b4b2b0b2..7788a53a51 100644 --- a/src/components/ApplicationGroup/AppGroup.types.ts +++ b/src/components/ApplicationGroup/AppGroup.types.ts @@ -14,33 +14,27 @@ * limitations under the License. */ -import { Dispatch, SetStateAction } from 'react' import { MultiValue } from 'react-select' import { ACTION_STATE, AppInfoListType, - ApprovalConfigDataType, - CDModalTabType, CIMaterialType, CommonNodeAttr, DeploymentNodeType, - DeploymentStrategyTypeWithDefault, - FilterConditionsListType, GVKType, MODAL_TYPE, OptionType, - PipelineIdsVsDeploymentStrategyMap, ResponseType, RuntimePluginVariables, ServerErrors, + TriggerBlockedInfo, UseUrlFiltersReturnType, - WorkflowNodeType, WorkflowType, } from '@devtron-labs/devtron-fe-common-lib' import { GitInfoMaterialProps } from '@Components/app/details/triggerView/BuildImageModal/types' -import { CDMaterialProps, RuntimeParamsErrorState } from '@Components/app/details/triggerView/types' +import { DeployImageContentProps } from '@Components/app/details/triggerView/DeployImageModal/types' import { AppConfigState, EnvConfigurationsNavProps, @@ -72,35 +66,40 @@ export interface BulkCIDetailType extends BulkTriggerAppDetailType { ignoreCache: boolean } +export type BulkCDDetailDerivedFromNode = Required< + Pick< + DeployImageContentProps, + | 'pipelineId' + | 'appId' + | 'parentEnvironmentName' + | 'isTriggerBlockedDueToPlugin' + | 'configurePluginURL' + | 'triggerType' + | 'appName' + > +> & { + stageNotAvailable: boolean + errorMessage: string + triggerBlockedInfo: TriggerBlockedInfo + consequence: CommonNodeAttr['pluginBlockState'] + showPluginWarning: CommonNodeAttr['showPluginWarning'] +} + +export type BulkCDDetailType = BulkCDDetailDerivedFromNode & + Pick & { + /** + * True in cases when we reload materials on single app + */ + areMaterialsLoading: boolean + materialError: ServerErrors | null + tagsWarningMessage: string + } + export interface BulkCDDetailTypeResponse { bulkCDDetailType: BulkCDDetailType[] uniqueReleaseTags: string[] } -export interface BulkCDDetailType - extends BulkTriggerAppDetailType, - Pick, - Partial> { - workFlowId: string - cdPipelineName?: string - cdPipelineId?: string - stageType?: DeploymentNodeType - triggerType?: string - envName: string - envId: number - parentPipelineId?: string - parentPipelineType?: WorkflowNodeType - parentEnvironmentName?: string - approvalConfigData?: ApprovalConfigDataType - requestedUserId?: number - appReleaseTags?: string[] - tagsEditable?: boolean - ciPipelineId?: number - hideImageTaggingHardDelete?: boolean - resourceFilters?: FilterConditionsListType[] - isExceptionUser?: boolean -} - export type TriggerVirtualEnvResponseRowType = | { isVirtual: true @@ -122,45 +121,6 @@ export type ResponseRowType = { envId?: number } & TriggerVirtualEnvResponseRowType -interface BulkRuntimeParamsType { - runtimeParams: Record - setRuntimeParams: React.Dispatch>> - runtimeParamsErrorState: Record - setRuntimeParamsErrorState: React.Dispatch>> -} - -export interface BulkCDTriggerType extends BulkRuntimeParamsType { - stage: DeploymentNodeType - appList: BulkCDDetailType[] - closePopup: (e) => void - updateBulkInputMaterial: (materialList: Record) => void - onClickTriggerBulkCD: ( - skipIfHibernated: boolean, - pipelineIdVsStrategyMap: PipelineIdsVsDeploymentStrategyMap, - appsToRetry?: Record, - ) => void - changeTab?: ( - materrialId: string | number, - artifactId: number, - tab: CDModalTabType, - selectedCDDetail?: { id: number; type: DeploymentNodeType }, - ) => void - toggleSourceInfo?: (materialIndex: number, selectedCDDetail?: { id: number; type: DeploymentNodeType }) => void - selectImage?: ( - index: number, - materialType: string, - selectedCDDetail?: { id: number; type: DeploymentNodeType }, - ) => void - responseList: ResponseRowType[] - isLoading: boolean - setLoading: React.Dispatch> - isVirtualEnv?: boolean - uniqueReleaseTags: string[] - feasiblePipelineIds: Set - bulkDeploymentStrategy: DeploymentStrategyTypeWithDefault - setBulkDeploymentStrategy: Dispatch> -} - export interface ProcessWorkFlowStatusType { cicdInProgress: boolean workflows: WorkflowType[] @@ -194,15 +154,11 @@ export interface TriggerResponseModalBodyProps { type RetryFailedType = | { - onClickRetryDeploy: BulkCDTriggerType['onClickTriggerBulkCD'] - skipHibernatedApps: boolean - pipelineIdVsStrategyMap: PipelineIdsVsDeploymentStrategyMap + onClickRetryDeploy: (appsToRetry: Record) => void onClickRetryBuild?: never } | { onClickRetryDeploy?: never - skipHibernatedApps?: never - pipelineIdVsStrategyMap?: never onClickRetryBuild: (appsToRetry: Record) => void } @@ -216,11 +172,6 @@ export interface TriggerModalRowType { isVirtualEnv?: boolean } -export interface WorkflowNodeSelectionType { - id: number - name: string - type: WorkflowNodeType -} export interface WorkflowAppSelectionType { id: number name: string diff --git a/src/components/ApplicationGroup/AppGroup.utils.ts b/src/components/ApplicationGroup/AppGroup.utils.ts index 0443df9fd0..3e20762308 100644 --- a/src/components/ApplicationGroup/AppGroup.utils.ts +++ b/src/components/ApplicationGroup/AppGroup.utils.ts @@ -96,28 +96,37 @@ export const processWorkflowStatuses = ( } }) } - // Update Workflow using maps - const _workflows = workflowsList.map((wf) => { - wf.nodes = wf.nodes.map((node) => { + // Update Workflow using maps, returning new objects for reactivity + const _workflows = workflowsList.map((wf) => ({ + ...wf, + nodes: wf.nodes.map((node) => { switch (node.type) { case 'CI': - node.status = ciMap[node.id]?.status - node.storageConfigured = ciMap[node.id]?.storageConfigured - break + return { + ...node, + status: ciMap[node.id]?.status, + storageConfigured: ciMap[node.id]?.storageConfigured, + } case 'PRECD': - node.status = preCDMap[node.id] - break + return { + ...node, + status: preCDMap[node.id], + } case 'POSTCD': - node.status = postCDMap[node.id] - break + return { + ...node, + status: postCDMap[node.id], + } case 'CD': - node.status = cdMap[node.id] - break + return { + ...node, + status: cdMap[node.id], + } + default: + return { ...node } } - return node }) - return wf - }) + })) return { cicdInProgress, workflows: _workflows } } diff --git a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx b/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx deleted file mode 100644 index f24733bb8e..0000000000 --- a/src/components/ApplicationGroup/Details/TriggerView/BulkCDTrigger.tsx +++ /dev/null @@ -1,1024 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { SyntheticEvent, useEffect, useRef, useState } from 'react' -import { useHistory, useLocation } from 'react-router-dom' - -import { - ACTION_STATE, - AnimatedDeployButton, - ApiQueuingWithBatch, - Button, - ButtonStyleType, - ButtonVariantType, - CD_MATERIAL_SIDEBAR_TABS, - CDMaterialResponseType, - CDMaterialServiceEnum, - CDMaterialSidebarType, - CDMaterialType, - CommonNodeAttr, - ComponentSizeType, - DEPLOYMENT_WINDOW_TYPE, - DeploymentNodeType, - DeploymentWindowProfileMetaData, - Drawer, - FilterStates, - genericCDMaterialsService, - GenericEmptyState, - Icon, - ImageComment, - MODAL_TYPE, - PipelineIdsVsDeploymentStrategyMap, - ReleaseTag, - RuntimePluginVariables, - SelectPicker, - showError, - stopPropagation, - ToastManager, - ToastVariantType, - TriggerBlockType, - uploadCDPipelineFile, - UploadFileProps, - useGetUserRoles, - useMainContext, -} from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as UnAuthorized } from '@Icons/ic-locked.svg' -import { ReactComponent as Tag } from '@Icons/ic-tag.svg' -import { ReactComponent as Error } from '@Icons/ic-warning.svg' -import { getIsMaterialApproved } from '@Components/app/details/triggerView/cdMaterials.utils' - -import emptyPreDeploy from '../../../../assets/img/empty-pre-deploy.webp' -import { ReactComponent as MechanicalOperation } from '../../../../assets/img/ic-mechanical-operation.svg' -import notAuthorized from '../../../../assets/img/ic-not-authorized.svg' -import CDMaterial from '../../../app/details/triggerView/cdMaterial' -import { BulkSelectionEvents, MATERIAL_TYPE, RuntimeParamsErrorState } from '../../../app/details/triggerView/types' -import { importComponentFromFELibrary } from '../../../common' -import { BulkCDDetailType, BulkCDTriggerType } from '../../AppGroup.types' -import { BULK_CD_DEPLOYMENT_STATUS, BULK_CD_MATERIAL_STATUS, BULK_CD_MESSAGING, BUTTON_TITLE } from '../../Constants' -import { BULK_ERROR_MESSAGES } from './constants' -import TriggerResponseModalBody, { TriggerResponseModalFooter } from './TriggerResponseModal' -import { - getIsImageApprovedByDeployerSelected, - getIsNonApprovedImageSelected, - getSelectedAppListForBulkStrategy, -} from './utils' - -const DeploymentWindowInfoBar = importComponentFromFELibrary('DeploymentWindowInfoBar') -const BulkDeployResistanceTippy = importComponentFromFELibrary('BulkDeployResistanceTippy') -const processDeploymentWindowMetadata = importComponentFromFELibrary( - 'processDeploymentWindowMetadata', - null, - 'function', -) -const getDeploymentWindowStateAppGroup = importComponentFromFELibrary( - 'getDeploymentWindowStateAppGroup', - null, - 'function', -) -const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') -const MissingPluginBlockState = importComponentFromFELibrary('MissingPluginBlockState', null, 'function') -const PolicyEnforcementMessage = importComponentFromFELibrary('PolicyEnforcementMessage') -const TriggerBlockedError = importComponentFromFELibrary('TriggerBlockedError', null, 'function') -const TriggerBlockEmptyState = importComponentFromFELibrary('TriggerBlockEmptyState', null, 'function') -const validateRuntimeParameters = importComponentFromFELibrary( - 'validateRuntimeParameters', - () => ({ isValid: true, cellError: {} }), - 'function', -) -const SkipHibernatedCheckbox = importComponentFromFELibrary('SkipHibernatedCheckbox', null, 'function') -const SelectDeploymentStrategy = importComponentFromFELibrary('SelectDeploymentStrategy', null, 'function') -const BulkCDStrategy = importComponentFromFELibrary('BulkCDStrategy', null, 'function') - -// TODO: Fix release tags selection -const BulkCDTrigger = ({ - stage, - appList, - closePopup, - // NOTE: Using this to update the appList in the parent component, should remove this later - updateBulkInputMaterial, - // NOTE: Should trigger the bulk cd here only but since its also calling another parent function not refactoring right now - onClickTriggerBulkCD, - feasiblePipelineIds, - responseList, - isLoading, - setLoading, - isVirtualEnv, - uniqueReleaseTags, - runtimeParams, - setRuntimeParams, - bulkDeploymentStrategy, - setBulkDeploymentStrategy, - runtimeParamsErrorState, - setRuntimeParamsErrorState, -}: BulkCDTriggerType) => { - const { canFetchHelmAppStatus } = useMainContext() - const [selectedApp, setSelectedApp] = useState( - appList.find((app) => !app.warningMessage) || appList[0], - ) - const [tagNotFoundWarningsMap, setTagNotFoundWarningsMap] = useState>(new Map()) - const [unauthorizedAppList, setUnauthorizedAppList] = useState>({}) - const abortControllerRef = useRef(new AbortController()) - const [appSearchTextMap, setAppSearchTextMap] = useState>({}) - const [selectedImages, setSelectedImages] = useState>({}) - // This signifies any action that needs to be propagated to the child - const [selectedImageFromBulk, setSelectedImageFromBulk] = useState(null) - const [appDeploymentWindowMap, setAppDeploymentWindowMap] = useState< - Record - >({}) - const [isPartialActionAllowed, setIsPartialActionAllowed] = useState(false) - const [showResistanceBox, setShowResistanceBox] = useState(false) - const [currentSidebarTab, setCurrentSidebarTab] = useState(CDMaterialSidebarType.IMAGE) - const [skipHibernatedApps, setSkipHibernatedApps] = useState(false) - const [showStrategyFeasibilityPage, setShowStrategyFeasibilityPage] = useState(false) - const [pipelineIdVsStrategyMap, setPipelineIdVsStrategyMap] = useState({}) - - const location = useLocation() - const history = useHistory() - const { isSuperAdmin } = useGetUserRoles() - const isBulkDeploymentTriggered = useRef(false) - - const showRuntimeParams = - RuntimeParamTabs && (stage === DeploymentNodeType.PRECD || stage === DeploymentNodeType.POSTCD) - - useEffect(() => { - const searchParams = new URLSearchParams(location.search) - const search = searchParams.get('search') - const _appSearchTextMap = { ...appSearchTextMap } - - if (search) { - _appSearchTextMap[selectedApp.appId] = search - } else { - delete _appSearchTextMap[selectedApp.appId] - } - - setAppSearchTextMap(_appSearchTextMap) - }, [location]) - - const closeBulkCDModal = (e: React.MouseEvent): void => { - e.stopPropagation() - abortControllerRef.current.abort() - closePopup(e) - } - - const [selectedTagName, setSelectedTagName] = useState<{ label: string; value: string }>({ - label: 'latest', - value: 'latest', - }) - - const handleSidebarTabChange = (e: React.ChangeEvent) => { - setCurrentSidebarTab(e.target.value as CDMaterialSidebarType) - } - - const handleRuntimeParamError = (errorState: RuntimeParamsErrorState) => { - setRuntimeParamsErrorState((prevErrorState) => ({ - ...prevErrorState, - [selectedApp.appId]: errorState, - })) - } - - 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 = [] - let _isPartialActionAllowed = false - for (const appDetails of appList) { - if (_cdMaterialResponse[appDetails.appId]) { - appEnvMap.push({ appId: appDetails.appId, envId: appDetails.envId }) - } - } - const { result } = await getDeploymentWindowStateAppGroup(appEnvMap) - const _appDeploymentWindowMap = {} - result?.appData?.forEach((data) => { - _appDeploymentWindowMap[data.appId] = processDeploymentWindowMetadata( - data.deploymentProfileList, - currentEnv, - ) - if (!_isPartialActionAllowed) { - _isPartialActionAllowed = - _appDeploymentWindowMap[data.appId].type === DEPLOYMENT_WINDOW_TYPE.BLACKOUT || - !_appDeploymentWindowMap[data.appId].isActive - ? _appDeploymentWindowMap[data.appId].userActionState === ACTION_STATE.PARTIAL - : false - } - }) - setIsPartialActionAllowed(_isPartialActionAllowed) - setAppDeploymentWindowMap(_appDeploymentWindowMap) - } - - const resolveMaterialData = (_cdMaterialResponse, _unauthorizedAppList) => (response) => { - if (response.status === 'fulfilled') { - setRuntimeParams((prevState) => { - const updatedRuntimeParams = { ...prevState } - updatedRuntimeParams[response.value.appId] = response.value.runtimeParams || [] - return updatedRuntimeParams - }) - - _cdMaterialResponse[response.value.appId] = response.value - // if first image does not have filerState.ALLOWED then unselect all images and set SELECT_NONE for selectedImage and for first app send the trigger of SELECT_NONE from selectedImageFromBulk - if ( - response.value.materials?.length > 0 && - (response.value.materials[0].filterState !== FilterStates.ALLOWED || - response.value.materials[0].vulnerable) - ) { - const updatedMaterials = response.value.materials.map((mat) => ({ - ...mat, - isSelected: false, - })) - _cdMaterialResponse[response.value.appId] = { - ...response.value, - materials: updatedMaterials, - } - setSelectedImages((prevSelectedImages) => ({ - ...prevSelectedImages, - [response.value.appId]: BulkSelectionEvents.SELECT_NONE, - })) - - const _warningMessage = response.value.materials[0].vulnerable - ? 'has security vulnerabilities' - : 'is not eligible' - - setTagNotFoundWarningsMap((prevTagNotFoundWarningsMap) => { - const _tagNotFoundWarningsMap = new Map(prevTagNotFoundWarningsMap) - _tagNotFoundWarningsMap.set( - response.value.appId, - `Tag '${ - selectedTagName.value?.length > 15 - ? `${selectedTagName.value.substring(0, 10)}...` - : selectedTagName.value - }' ${_warningMessage}`, - ) - return _tagNotFoundWarningsMap - }) - if (response.value.appId === selectedApp.appId) { - setSelectedImageFromBulk(BulkSelectionEvents.SELECT_NONE) - } - } else if (response.value.materials?.length === 0) { - setTagNotFoundWarningsMap((prevTagNotFoundWarningsMap) => { - const _tagNotFoundWarningsMap = new Map(prevTagNotFoundWarningsMap) - _tagNotFoundWarningsMap.set( - response.value.appId, - `Tag '${ - selectedTagName.value?.length > 15 - ? `${selectedTagName.value.substring(0, 10)}...` - : selectedTagName.value - }' not found`, - ) - return _tagNotFoundWarningsMap - }) - } - - delete _unauthorizedAppList[response.value.appId] - } else { - const errorReason = response?.reason - if (errorReason?.code === 403) { - _unauthorizedAppList[errorReason.appId] = true - } - } - } - - const getCDMaterialFunction = (appDetails) => () => - // Not sending any query params since its not necessary on mount and filters and handled by other service) - genericCDMaterialsService( - CDMaterialServiceEnum.CD_MATERIALS, - Number(appDetails.cdPipelineId), - appDetails.stageType, - abortControllerRef.current.signal, - { - offset: 0, - size: 20, - }, - ) - .then((data) => ({ appId: appDetails.appId, ...data })) - .catch((e) => { - if (!abortControllerRef.current.signal.aborted) { - throw { response: e?.response, appId: appDetails.appId } - } - }) - - /** - * Gets triggered during the mount state of the component through useEffect - * Fetches the material data pushes them into promise list - * Promise list is resolved using Promise.allSettled - * If the promise is fulfilled, the data is pushed into cdMaterialResponse - */ - const getMaterialData = (): void => { - abortControllerRef.current = new AbortController() - const _unauthorizedAppList: Record = {} - const _cdMaterialResponse: Record = {} - abortControllerRef.current = new AbortController() - const _cdMaterialFunctionsList = [] - for (const appDetails of appList) { - if (!appDetails.warningMessage) { - _unauthorizedAppList[appDetails.appId] = false - _cdMaterialFunctionsList.push(getCDMaterialFunction(appDetails)) - } - } - - if (!_cdMaterialFunctionsList.length) { - setLoading(false) - return - } - - ApiQueuingWithBatch(_cdMaterialFunctionsList) - .then(async (responses: any[]) => { - responses.forEach(resolveMaterialData(_cdMaterialResponse, _unauthorizedAppList)) - if (getDeploymentWindowStateAppGroup) { - await getDeploymentWindowData(_cdMaterialResponse) - } - updateBulkInputMaterial(_cdMaterialResponse) - setUnauthorizedAppList(_unauthorizedAppList) - setLoading(false) - }) - .catch((error) => { - setLoading(false) - showError(error) - }) - } - - useEffect(() => { - getMaterialData() - }, []) - - const handleBackFromStrategySelection = () => { - setShowStrategyFeasibilityPage(false) - setPipelineIdVsStrategyMap({}) - } - - const renderHeaderSection = (): JSX.Element => ( -
-
- {showStrategyFeasibilityPage && ( -
-
- ) - - const changeApp = (e): void => { - const updatedErrorState = validateRuntimeParameters(runtimeParams[selectedApp.appId]) - handleRuntimeParamError(updatedErrorState) - if (!updatedErrorState.isValid) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: BULK_ERROR_MESSAGES.CHANGE_APPLICATION, - }) - return - } - - const _selectedApp = appList[e.currentTarget.dataset.index] - setSelectedApp(_selectedApp) - setSelectedImageFromBulk(selectedImages[_selectedApp.appId]) - - if (appSearchTextMap[_selectedApp.appId]) { - const newSearchParams = new URLSearchParams(location.search) - newSearchParams.set('search', appSearchTextMap[_selectedApp.appId]) - - history.push({ - search: newSearchParams.toString(), - }) - } else { - history.push({ - search: '', - }) - } - } - - const renderEmptyView = (): JSX.Element => { - if (selectedApp.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG) { - return - } - - if (selectedApp.isTriggerBlockedDueToPlugin) { - const commonNodeAttrType: CommonNodeAttr['type'] = - selectedApp.stageType === DeploymentNodeType.PRECD ? 'PRECD' : 'POSTCD' - - return ( - - ) - } - - if (unauthorizedAppList[selectedApp.appId]) { - return ( - - ) - } - return ( - - ) - } - - const renderDeploymentWithoutApprovalWarning = (app: BulkCDDetailType) => { - if (!app.isExceptionUser) { - return null - } - - const selectedMaterial: CDMaterialType = app.material?.find((mat: CDMaterialType) => mat.isSelected) - - if (!selectedMaterial || getIsMaterialApproved(selectedMaterial?.userApprovalMetadata)) { - return null - } - - return ( -
- -

Non-approved image selected

-
- ) - } - - const renderAppWarningAndErrors = (app: BulkCDDetailType) => { - const commonNodeAttrType: CommonNodeAttr['type'] = - app.stageType === DeploymentNodeType.PRECD ? 'PRECD' : 'POSTCD' - - const warningMessage = app.warningMessage || appDeploymentWindowMap[app.appId]?.warningMessage - - const isAppSelected = selectedApp.appId === app.appId - - if (unauthorizedAppList[app.appId]) { - return ( -
- - {BULK_CD_MESSAGING.unauthorized.title} -
- ) - } - - if (tagNotFoundWarningsMap.has(app.appId)) { - return ( -
- - - {tagNotFoundWarningsMap.get(app.appId)} -
- ) - } - - if (app.isTriggerBlockedDueToPlugin) { - return ( - - ) - } - - if (app.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG) { - return - } - - if (!!warningMessage && !app.showPluginWarning) { - return ( -
- - {warningMessage} -
- ) - } - - if (app.showPluginWarning) { - return ( - - ) - } - - return null - } - - const responseListLength = responseList.length - - const renderBodySection = (): JSX.Element => { - if (responseListLength) { - return ( - - ) - } - - if (isLoading) { - const message = isBulkDeploymentTriggered.current - ? BULK_CD_DEPLOYMENT_STATUS(appList.length, appList[0].envName) - : BULK_CD_MATERIAL_STATUS(appList.length) - return ( - - ) - } - - const updateCurrentAppMaterial = (matId: number, releaseTags?: ReleaseTag[], imageComment?: ImageComment) => { - const updatedCurrentApp = selectedApp - updatedCurrentApp?.material.forEach((mat) => { - if (mat.id === matId) { - if (releaseTags) { - mat.imageReleaseTags = releaseTags - } - if (imageComment) { - mat.imageComment = imageComment - } - } - }) - updatedCurrentApp && setSelectedApp(updatedCurrentApp) - } - - const _currentApp = appList.find((app) => app.appId === selectedApp.appId) ?? ({} as BulkCDDetailType) - uniqueReleaseTags.sort((a, b) => a.localeCompare(b)) - - const tagsList = ['latest', 'active'] - - tagsList.push(...uniqueReleaseTags) - const options = tagsList.map((tag) => ({ label: tag, value: tag })) - - const appWiseTagsToArtifactIdMapMappings = {} - appList.forEach((app) => { - if (!app.material?.length) { - appWiseTagsToArtifactIdMapMappings[app.appId] = {} - } else { - const tagsToArtifactIdMap = { latest: 0 } - for (let i = 0; i < app.material?.length; i++) { - const mat = app.material?.[i] - mat.imageReleaseTags?.forEach((imageTag) => { - tagsToArtifactIdMap[imageTag.tagName] = i - }) - - if (mat.deployed && mat.latest) { - tagsToArtifactIdMap['active'] = i - } - } - appWiseTagsToArtifactIdMapMappings[app.appId] = tagsToArtifactIdMap - } - }) - - // Don't use it as single, use it through update function - const selectImageLocal = (index: number, appId: number, selectedImageTag: string) => { - setSelectedImages({ ...selectedImages, [appId]: selectedImageTag }) - - if (appWiseTagsToArtifactIdMapMappings[appId][selectedTagName.value] !== index) { - const _tagNotFoundWarningsMap = tagNotFoundWarningsMap - _tagNotFoundWarningsMap.delete(appId) - setTagNotFoundWarningsMap(_tagNotFoundWarningsMap) - setSelectedTagName({ value: 'Multiple tags', label: 'Multiple tags' }) - } else { - // remove warning if any - const _tagNotFoundWarningsMap = new Map(tagNotFoundWarningsMap) - _tagNotFoundWarningsMap.delete(appId) - setTagNotFoundWarningsMap(_tagNotFoundWarningsMap) - } - } - - const parseApplistIntoCDMaterialResponse = ( - appListData: BulkCDDetailType, - updatedMaterials?: CDMaterialType, - ) => ({ - materials: updatedMaterials ?? appListData.material, - requestedUserId: appListData.requestedUserId, - approvalConfigData: appListData.approvalConfigData, - appReleaseTagNames: appListData.appReleaseTags, - tagsEditable: appListData.tagsEditable, - }) - - const handleTagChange = (selectedTag) => { - setSelectedTagName(selectedTag) - const _tagNotFoundWarningsMap = new Map() - const _cdMaterialResponse: Record = {} - - for (let i = 0; i < (appList?.length ?? 0); i++) { - const app = appList[i] - const tagsToArtifactIdMap = appWiseTagsToArtifactIdMapMappings[app.appId] - let artifactIndex = -1 - if (typeof tagsToArtifactIdMap[selectedTag.value] !== 'undefined') { - artifactIndex = tagsToArtifactIdMap[selectedTag.value] - } - - // Handling the behavior for excluded filter state - if (artifactIndex !== -1) { - const selectedImageFilterState = app.material?.[artifactIndex]?.filterState - - if (selectedImageFilterState !== FilterStates.ALLOWED) { - artifactIndex = -1 - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' is not eligible`, - ) - } else if (app.material?.[artifactIndex]?.vulnerable) { - artifactIndex = -1 - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' has security vulnerabilities`, - ) - } - } else { - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' not found`, - ) - } - - if (artifactIndex !== -1 && selectedTag.value !== 'latest' && selectedTag.value !== 'active') { - const releaseTag = app.material[artifactIndex]?.imageReleaseTags.find( - (releaseTag) => releaseTag.tagName === selectedTag.value, - ) - if (releaseTag?.deleted) { - artifactIndex = -1 - _tagNotFoundWarningsMap.set( - app.appId, - `Tag '${ - selectedTag.value?.length > 15 - ? `${selectedTag.value.substring(0, 10)}...` - : selectedTag.value - }' is soft-deleted`, - ) - } - } - - if (artifactIndex !== -1) { - const selectedImageName = app.material?.[artifactIndex]?.image - const updatedMaterials: any = app.material?.map((mat, index) => ({ - ...mat, - isSelected: index === artifactIndex, - })) - - _cdMaterialResponse[app.appId] = parseApplistIntoCDMaterialResponse(app, updatedMaterials) - - setSelectedImages((prevSelectedImages) => ({ - ...prevSelectedImages, - [app.appId]: selectedImageName, - })) - } else { - const updatedMaterials: any = app.material?.map((mat) => ({ - ...mat, - isSelected: false, - })) - - _cdMaterialResponse[app.appId] = parseApplistIntoCDMaterialResponse(app, updatedMaterials) - - setSelectedImages((prevSelectedImages) => ({ - ...prevSelectedImages, - [app.appId]: BulkSelectionEvents.SELECT_NONE, - })) - } - } - - updateBulkInputMaterial(_cdMaterialResponse) - - // Handling to behviour of current app to send a trigger to child - const selectedImageName = _cdMaterialResponse[selectedApp.appId]?.materials?.find( - (mat: CDMaterialType) => mat.isSelected === true, - )?.image - - if (selectedImageName) { - setSelectedImageFromBulk(selectedImageName) - } else { - setSelectedImageFromBulk(BulkSelectionEvents.SELECT_NONE) - } - - setTagNotFoundWarningsMap(_tagNotFoundWarningsMap) - } - - const updateBulkCDMaterialsItem = (singleCDMaterialResponse) => { - const _cdMaterialResponse: Record = {} - _cdMaterialResponse[selectedApp.appId] = singleCDMaterialResponse - - updateBulkInputMaterial(_cdMaterialResponse) - - const selectedArtifact = singleCDMaterialResponse?.materials?.find( - (mat: CDMaterialType) => mat.isSelected === true, - ) - - if (selectedArtifact) { - selectImageLocal(selectedArtifact.index, selectedApp.appId, selectedArtifact.image) - } - - // Setting it to null since since only wants to trigger change inside if user changes app or tag - setSelectedImageFromBulk(null) - } - - return ( -
-
-
- {showRuntimeParams && ( -
- -
- )} - {currentSidebarTab === CDMaterialSidebarType.IMAGE && ( - <> - Select image by release tag -
- } - onChange={handleTagChange} - isDisabled={false} - classNamePrefix="build-config__select-repository-containing-code" - autoFocus - /> -
- - )} -
- APPLICATIONS -
-
- {appList.map((app, index) => ( -
- {app.name} - {renderDeploymentWithoutApprovalWarning(app)} - {renderAppWarningAndErrors(app)} -
- ))} -
-
- {selectedApp.warningMessage || unauthorizedAppList[selectedApp.appId] ? ( - renderEmptyView() - ) : ( - // TODO: Handle isSuperAdmin prop - - <> - {DeploymentWindowInfoBar && - appDeploymentWindowMap[selectedApp.appId] && - appDeploymentWindowMap[selectedApp.appId].warningMessage && ( - - )} - - - )} -
-
- ) - } - const hideResistanceBox = (): void => { - setShowResistanceBox(false) - } - - const onClickDeploy = (e: SyntheticEvent) => { - if (showStrategyFeasibilityPage) { - setShowStrategyFeasibilityPage(false) - } - if (isPartialActionAllowed && BulkDeployResistanceTippy && !showResistanceBox) { - setShowResistanceBox(true) - } else { - isBulkDeploymentTriggered.current = true - stopPropagation(e) - onClickTriggerBulkCD(skipHibernatedApps, pipelineIdVsStrategyMap) - setShowResistanceBox(false) - } - } - - const onClickStartDeploy = (e): void => { - if (BulkCDStrategy && bulkDeploymentStrategy !== 'DEFAULT') { - setShowStrategyFeasibilityPage(true) - return - } - onClickDeploy(e) - } - - const isDeployDisabled = (): boolean => - appList.every((app) => app.warningMessage || tagNotFoundWarningsMap.has(app.appId) || !app.material?.length) - - const renderFooterSection = (): JSX.Element => { - if (responseListLength) { - return ( - - ) - } - - const isCDStage = stage === DeploymentNodeType.CD - const isDeployButtonDisabled: boolean = isDeployDisabled() - const canDeployWithoutApproval = getIsNonApprovedImageSelected(appList) - const canImageApproverDeploy = getIsImageApprovedByDeployerSelected(appList) - const showSkipHibernatedCheckbox = !!SkipHibernatedCheckbox && canFetchHelmAppStatus - - return ( -
- {showSkipHibernatedCheckbox && ( - app.appId)} - skipHibernated={skipHibernatedApps} - setSkipHibernated={setSkipHibernatedApps} - /> - )} -
- {isCDStage && SelectDeploymentStrategy && !isLoading && !responseListLength && ( - +app.cdPipelineId)} - isBulkStrategyChange - deploymentStrategy={bulkDeploymentStrategy} - setDeploymentStrategy={setBulkDeploymentStrategy} - /> - )} -
- } - onButtonClick={onClickStartDeploy} - disabled={isDeployButtonDisabled} - isLoading={isLoading} - animateStartIcon={isCDStage} - style={ - canDeployWithoutApproval || canImageApproverDeploy - ? ButtonStyleType.warning - : ButtonStyleType.default - } - tooltipContent={ - canDeployWithoutApproval || canImageApproverDeploy - ? 'You are authorized to deploy as an exception user for some applications' - : '' - } - /> -
-
-
- ) - } - - return ( - -
- {renderHeaderSection()} - {BulkCDStrategy && showStrategyFeasibilityPage ? ( - - ) : ( - <> - {renderBodySection()} - {renderFooterSection()} - - )} -
- {showResistanceBox && ( - - )} -
- ) -} - -export default BulkCDTrigger diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index 0712452b7d..83be81921b 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -14,91 +14,68 @@ * limitations under the License. */ -import React, { useEffect, useRef, useState } from 'react' -import ReactGA from 'react-ga4' +import React, { useEffect, useState } from 'react' import { Prompt, Route, Switch, useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom' import Tippy from '@tippyjs/react' import { - API_STATUS_CODES, - ApiQueuingWithBatch, Button, ButtonStyleType, ButtonVariantType, - CDMaterialResponseType, Checkbox, CHECKBOX_VALUE, CommonNodeAttr, ComponentSizeType, DEFAULT_ROUTE_PROMPT_MESSAGE, DeploymentNodeType, - DeploymentStrategyTypeWithDefault, ErrorScreenManager, - getStageTitle, - PipelineIdsVsDeploymentStrategyMap, PopupMenu, Progressing, - RuntimePluginVariables, ServerErrors, showError, sortCallback, - stopPropagation, ToastManager, ToastVariantType, - TriggerBlockType, - triggerCDNode, usePrompt, - VisibleModal, WorkflowNodeType, WorkflowType, } from '@devtron-labs/devtron-fe-common-lib' import { BuildImageModal, BulkBuildImageModal } from '@Components/app/details/triggerView/BuildImageModal' +import CDMaterial from '@Components/app/details/triggerView/CDMaterial' +import { BulkDeployModal } from '@Components/app/details/triggerView/DeployImageModal' import { shouldRenderWebhookAddImageModal } from '@Components/app/details/triggerView/TriggerView.utils' import { getExternalCIConfig } from '@Components/ciPipeline/Webhook/webhook.service' import { ReactComponent as Dropdown } from '../../../../assets/icons/ic-chevron-down.svg' -import { ReactComponent as CloseIcon } from '../../../../assets/icons/ic-close.svg' import { ReactComponent as Close } from '../../../../assets/icons/ic-cross.svg' import { ReactComponent as DeployIcon } from '../../../../assets/icons/ic-nav-rocket.svg' import { ReactComponent as Pencil } from '../../../../assets/icons/ic-pencil.svg' import { URLS, ViewType } from '../../../../config' import { LinkedCIDetail } from '../../../../Pages/Shared/LinkedCIDetailsModal' import { AppNotConfigured } from '../../../app/details/appDetails/AppDetails' -import CDMaterial from '../../../app/details/triggerView/cdMaterial' -import { TriggerViewContext } from '../../../app/details/triggerView/config' import { TRIGGER_VIEW_PARAMS } from '../../../app/details/triggerView/Constants' -import { CIMaterialRouterProps, MATERIAL_TYPE, RuntimeParamsErrorState } from '../../../app/details/triggerView/types' +import { CIMaterialRouterProps, MATERIAL_TYPE } from '../../../app/details/triggerView/types' import { Workflow } from '../../../app/details/triggerView/workflow/Workflow' import { triggerBranchChange } from '../../../app/service' -import { getCDPipelineURL, importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../../common' +import { importComponentFromFELibrary, sortObjectArrayAlphabetically } from '../../../common' import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service' import { getWorkflows, getWorkflowStatus } from '../../AppGroup.service' import { AppGroupDetailDefaultType, - BulkCDDetailType, - BulkCDDetailTypeResponse, ProcessWorkFlowStatusType, ResponseRowType, - TriggerVirtualEnvResponseRowType, WorkflowAppSelectionType, - WorkflowNodeSelectionType, } from '../../AppGroup.types' import { processWorkflowStatuses } from '../../AppGroup.utils' import { - BULK_CD_RESPONSE_STATUS_TEXT, - BULK_CI_RESPONSE_STATUS_TEXT, - BULK_VIRTUAL_RESPONSE_STATUS, BulkResponseStatus, - ENV_TRIGGER_VIEW_GA_EVENTS, GetBranchChangeStatus, SKIPPED_RESOURCES_MESSAGE, SKIPPED_RESOURCES_STATUS_TEXT, } from '../../Constants' -import BulkCDTrigger from './BulkCDTrigger' import BulkSourceChange from './BulkSourceChange' -import { RenderCDMaterialContentProps } from './types' -import { getSelectedCDNode } from './utils' +import { getSelectedNodeAndAppId } from './utils' import './EnvTriggerView.scss' @@ -108,16 +85,9 @@ const processDeploymentWindowStateAppGroup = importComponentFromFELibrary( null, 'function', ) -const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') -const validateRuntimeParameters = importComponentFromFELibrary( - 'validateRuntimeParameters', - () => ({ isValid: true, cellError: {} }), - 'function', -) const ChangeImageSource = importComponentFromFELibrary('ChangeImageSource', null, 'function') const WebhookAddImageModal = importComponentFromFELibrary('WebhookAddImageModal', null, 'function') -// FIXME: IN CIMaterials we are sending isCDLoading while in CD materials we are sending isCILoading let inprogressStatusTimer const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultType) => { const { envId } = useParams<{ envId: string }>() @@ -126,13 +96,7 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT const match = useRouteMatch() const { url } = useRouteMatch() - // ref to make sure that on initial mount after we fetch workflows we handle modal based on url - const handledLocation = useRef(false) - const abortControllerRef = useRef(new AbortController()) - const [pageViewType, setPageViewType] = useState(ViewType.LOADING) - const [isCILoading, setCILoading] = useState(false) - const [isCDLoading, setCDLoading] = useState(false) const [isBranchChangeLoading, setIsBranchChangeLoading] = useState(false) const [showPreDeployment, setShowPreDeployment] = useState(false) const [showPostDeployment, setShowPostDeployment] = useState(false) @@ -143,30 +107,16 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT const [selectedAppList, setSelectedAppList] = useState([]) const [workflows, setWorkflows] = useState([]) const [filteredWorkflows, setFilteredWorkflows] = useState([]) - const [selectedCDNode, setSelectedCDNode] = useState(null) const [filteredCIPipelines, setFilteredCIPipelines] = useState(null) const [bulkTriggerType, setBulkTriggerType] = useState(null) - const [materialType, setMaterialType] = useState(MATERIAL_TYPE.inputMaterialList) const [responseList, setResponseList] = useState([]) const [isSelectAll, setSelectAll] = useState(false) const [selectAllValue, setSelectAllValue] = useState(CHECKBOX_VALUE.CHECKED) - // Mapping pipelineId (in case of CI) and appId (in case of CD) to runtime params - const [runtimeParams, setRuntimeParams] = useState>({}) - const [runtimeParamsErrorState, setRuntimeParamsErrorState] = useState>({}) - const [isBulkTriggerLoading, setIsBulkTriggerLoading] = useState(false) const [selectedWebhookNode, setSelectedWebhookNode] = useState<{ appId: number; id: number }>(null) - const [bulkDeploymentStrategy, setBulkDeploymentStrategy] = useState('DEFAULT') - const enableRoutePrompt = isBranchChangeLoading || isBulkTriggerLoading + const enableRoutePrompt = isBranchChangeLoading usePrompt({ shouldPrompt: enableRoutePrompt }) - useEffect( - () => () => { - handledLocation.current = false - }, - [], - ) - useEffect(() => { if (envId) { setPageViewType(ViewType.LOADING) @@ -184,72 +134,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT getWorkflowsData() } - useEffect(() => { - if (!handledLocation.current && filteredWorkflows?.length) { - handledLocation.current = true - // Would have been better if filteredWorkflows had default value to null since we are using it as a flag - // URL Encoding for Bulk is not planned as of now - setShowBulkCDModal(false) - if (location.search.includes('approval-node')) { - const searchParams = new URLSearchParams(location.search) - const nodeId = Number(searchParams.get('approval-node')) - if (!isNaN(nodeId)) { - onClickCDMaterial(nodeId, DeploymentNodeType.CD, true) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - } - } else if (location.search.includes('rollback-node')) { - const searchParams = new URLSearchParams(location.search) - const nodeId = Number(searchParams.get('rollback-node')) - if (!isNaN(nodeId)) { - onClickRollbackMaterial(nodeId) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - } - } else if (location.search.includes('cd-node')) { - const searchParams = new URLSearchParams(location.search) - const nodeId = Number(searchParams.get('cd-node')) - const nodeType = searchParams.get('node-type') ?? DeploymentNodeType.CD - - if ( - nodeType !== DeploymentNodeType.CD && - nodeType !== DeploymentNodeType.PRECD && - nodeType !== DeploymentNodeType.POSTCD - ) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node type', - }) - history.push({ - search: '', - }) - } else if (!isNaN(nodeId)) { - onClickCDMaterial(nodeId, nodeType as DeploymentNodeType) - } else { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - } - } - } - }, [filteredWorkflows]) - const preserveSelection = (_workflows: WorkflowType[]) => { if (!workflows || !_workflows) { return @@ -450,109 +334,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT ) } - const onClickCDMaterial = (cdNodeId, nodeType: DeploymentNodeType, isApprovalNode: boolean = false): void => { - ReactGA.event( - isApprovalNode ? ENV_TRIGGER_VIEW_GA_EVENTS.ApprovalNodeClicked : ENV_TRIGGER_VIEW_GA_EVENTS.ImageClicked, - ) - - let _workflowId - let _appID - let _selectedNode - - // FIXME: This needs to be replicated in rollback, env group since we need cipipelineid as 0 in external case - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (cdNodeId == node.id && node.type === nodeType) { - // TODO: Ig not using this, can remove it - if (node.type === WorkflowNodeType.CD) { - node.approvalConfigData = workflow.approvalConfiguredIdsMap[cdNodeId] - } - _selectedNode = node - _workflowId = workflow.id - _appID = workflow.appId - } - return node - }) - workflow.nodes = nodes - return workflow - }) - - if (!_selectedNode) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - return - } - - setFilteredWorkflows(_workflows) - setSelectedCDNode({ id: +cdNodeId, name: _selectedNode.name, type: _selectedNode.type }) - setMaterialType(MATERIAL_TYPE.inputMaterialList) - - const newParams = new URLSearchParams(location.search) - newParams.set(isApprovalNode ? 'approval-node' : 'cd-node', cdNodeId.toString()) - if (!isApprovalNode) { - newParams.set('node-type', nodeType) - } else { - const currentApprovalState = newParams.get(TRIGGER_VIEW_PARAMS.APPROVAL_STATE) - // If the current state is pending, then we should change the state to pending - const approvalState = - currentApprovalState === TRIGGER_VIEW_PARAMS.PENDING - ? TRIGGER_VIEW_PARAMS.PENDING - : TRIGGER_VIEW_PARAMS.APPROVAL - - newParams.set(TRIGGER_VIEW_PARAMS.APPROVAL_STATE, approvalState) - newParams.delete(TRIGGER_VIEW_PARAMS.CD_NODE) - newParams.delete(TRIGGER_VIEW_PARAMS.NODE_TYPE) - } - history.push({ - search: newParams.toString(), - }) - } - - const onClickRollbackMaterial = (cdNodeId: number) => { - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.RollbackClicked) - - let _selectedNode - - const _workflows = [...filteredWorkflows].map((workflow) => { - const nodes = workflow.nodes.map((node) => { - if (node.type === 'CD' && +node.id == cdNodeId) { - node.approvalConfigData = workflow.approvalConfiguredIdsMap[cdNodeId] - _selectedNode = node - } - return node - }) - workflow.nodes = nodes - return workflow - }) - - if (!_selectedNode) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Invalid node id', - }) - history.push({ - search: '', - }) - return - } - - setFilteredWorkflows(_workflows) - setSelectedCDNode({ id: +cdNodeId, name: _selectedNode.name, type: _selectedNode.type }) - setMaterialType(MATERIAL_TYPE.rollbackMaterialList) - getWorkflowStatusData(_workflows) - - const newParams = new URLSearchParams(location.search) - newParams.set('rollback-node', cdNodeId.toString()) - history.push({ - search: newParams.toString(), - }) - } - const isBuildAndBranchTriggerAllowed = (node: CommonNodeAttr): boolean => !node.isLinkedCI && !node.isLinkedCD && node.type !== WorkflowNodeType.WEBHOOK @@ -596,8 +377,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT if (!appIds.length) { updateResponseListData(skippedResources) setIsBranchChangeLoading(false) - setCDLoading(false) - setCILoading(false) return } @@ -615,8 +394,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT }) }) updateResponseListData([..._responseList, ...skippedResources]) - setCDLoading(false) - setCILoading(false) }) .catch((error) => { showError(error) @@ -626,16 +403,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT }) } - const closeCDModal = (e: React.MouseEvent): void => { - e?.stopPropagation() - abortControllerRef.current.abort() - setCDLoading(false) - history.push({ - search: '', - }) - getWorkflowStatusData(workflows) - } - const closeApprovalModal = (e: React.MouseEvent): void => { e.stopPropagation() history.push({ @@ -645,12 +412,8 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT } const hideBulkCDModal = () => { - setCDLoading(false) setShowBulkCDModal(false) setResponseList([]) - setBulkDeploymentStrategy('DEFAULT') - setRuntimeParams({}) - setRuntimeParamsErrorState({}) history.push({ search: '', @@ -658,28 +421,17 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT } const onShowBulkCDModal = (e) => { - setCDLoading(true) setBulkTriggerType(e.currentTarget.dataset.triggerType) - setMaterialType(MATERIAL_TYPE.inputMaterialList) - setTimeout(() => { - setShowBulkCDModal(true) - }, 100) + setShowBulkCDModal(true) } const hideBulkCIModal = () => { - setCILoading(false) setShowBulkCIModal(false) setResponseList([]) - - setRuntimeParams({}) - setRuntimeParamsErrorState({}) } const onShowBulkCIModal = () => { - setCILoading(true) - setTimeout(() => { - setShowBulkCIModal(true) - }, 100) + setShowBulkCIModal(true) } const hideChangeSourceModal = () => { @@ -702,146 +454,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT setShowBulkSourceChangeModal(true) } - const updateBulkCDInputMaterial = (cdMaterialResponse: Record): void => { - const _workflows = filteredWorkflows.map((wf) => { - if (wf.isSelected && cdMaterialResponse[wf.appId]) { - const _appId = wf.appId - const _cdNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CD && node.environmentId === +envId, - ) - let _selectedNode: CommonNodeAttr - const _materialData = cdMaterialResponse[_appId] - - if (bulkTriggerType === DeploymentNodeType.PRECD) { - _selectedNode = _cdNode.preNode - } else if (bulkTriggerType === DeploymentNodeType.CD) { - _selectedNode = _cdNode - _selectedNode.requestedUserId = _materialData.requestedUserId - _selectedNode.approvalConfigData = _materialData.deploymentApprovalInfo?.approvalConfigData - } else if (bulkTriggerType === DeploymentNodeType.POSTCD) { - _selectedNode = _cdNode.postNode - } - - if (_selectedNode) { - _selectedNode.inputMaterialList = _materialData.materials - } - wf.appReleaseTags = _materialData?.appReleaseTagNames - wf.tagsEditable = _materialData?.tagsEditable - wf.canApproverDeploy = _materialData?.canApproverDeploy ?? false - wf.isExceptionUser = _materialData?.deploymentApprovalInfo?.approvalConfigData?.isExceptionUser ?? false - } - - return wf - }) - setFilteredWorkflows(_workflows) - } - - const validateBulkRuntimeParams = (): boolean => { - 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) - setCILoading(false) - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Please resolve all the runtime parameter errors before triggering the pipeline', - }) - return false - } - - return true - } - - // Helper to get selected CD nodes - const getSelectedCDNodesWithArtifacts = ( - selectedWorkflows: WorkflowType[], - ): { node: CommonNodeAttr; wf: WorkflowType }[] => - selectedWorkflows - .filter((wf) => wf.isSelected) - .map((wf) => { - const _cdNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CD && node.environmentId === +envId, - ) - if (!_cdNode) return null - - const _selectedNode: CommonNodeAttr | undefined = getSelectedCDNode(bulkTriggerType, _cdNode) - - const selectedArtifacts = _selectedNode?.[materialType]?.filter((artifact) => artifact.isSelected) ?? [] - if (selectedArtifacts.length > 0) { - return { node: _selectedNode, wf } - } - return null - }) - .filter(Boolean) - - const onClickTriggerBulkCD = ( - skipIfHibernated: boolean, - pipelineIdVsStrategyMap: PipelineIdsVsDeploymentStrategyMap, - appsToRetry?: Record, - ) => { - if (isCDLoading || !validateBulkRuntimeParams()) { - return - } - - ReactGA.event(ENV_TRIGGER_VIEW_GA_EVENTS.BulkCDTriggered(bulkTriggerType)) - setCDLoading(true) - const _appIdMap = new Map() - const nodeList: CommonNodeAttr[] = [] - const triggeredAppList: { appId: number; envId?: number; appName: string }[] = [] - - const eligibleNodes = getSelectedCDNodesWithArtifacts( - filteredWorkflows.filter((wf) => !appsToRetry || appsToRetry[wf.appId]), - ) - eligibleNodes.forEach(({ node: eligibleNode, wf }) => { - nodeList.push(eligibleNode) - _appIdMap.set(eligibleNode.id, wf.appId.toString()) - triggeredAppList.push({ appId: wf.appId, appName: wf.name, envId: eligibleNode.environmentId }) - }) - - const _CDTriggerPromiseFunctionList = [] - nodeList.forEach((node, index) => { - let ciArtifact = null - const currentAppId = _appIdMap.get(node.id) - - node[materialType].forEach((artifact) => { - if (artifact.isSelected == true) { - ciArtifact = artifact - } - }) - const pipelineId = Number(node.id) - const strategy = pipelineIdVsStrategyMap[pipelineId] - - // skip app if bulkDeploymentStrategy is not default and strategy is not configured for app - if (ciArtifact && (bulkDeploymentStrategy === 'DEFAULT' || !!strategy)) { - _CDTriggerPromiseFunctionList.push(() => - triggerCDNode({ - pipelineId, - ciArtifactId: Number(ciArtifact.id), - appId: Number(currentAppId), - stageType: bulkTriggerType, - ...(getRuntimeParamsPayload - ? { runtimeParamsPayload: getRuntimeParamsPayload(runtimeParams[currentAppId] ?? []) } - : {}), - skipIfHibernated, - // strategy DEFAULT means custom chart - ...(strategy && strategy !== 'DEFAULT' ? { strategy } : {}), - }), - ) - } else { - triggeredAppList.splice(index, 1) - } - }) - handleBulkTrigger(_CDTriggerPromiseFunctionList, triggeredAppList, WorkflowNodeType.CD) - } - const updateResponseListData = (_responseList) => { setResponseList((prevList) => { const resultMap = new Map(_responseList.map((data) => [data.appId, data])) @@ -852,233 +464,6 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT }) } - const filterStatusType = ( - type: WorkflowNodeType, - CIStatus: string, - VirtualStatus: string, - CDStatus: string, - ): string => { - if (type === WorkflowNodeType.CI) { - return CIStatus - } - if (isVirtualEnv) { - return VirtualStatus - } - return CDStatus - } - - const handleBulkTrigger = ( - promiseFunctionList: any[], - triggeredAppList: { appId: number; envId?: number; appName: string }[], - type: WorkflowNodeType, - skippedResources: ResponseRowType[] = [], - ): void => { - setIsBulkTriggerLoading(true) - const _responseList = skippedResources - if (promiseFunctionList.length) { - ApiQueuingWithBatch(promiseFunctionList).then((responses: any[]) => { - responses.forEach((response, index) => { - if (response.status === 'fulfilled') { - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.PASS], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.PASS], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.PASS], - ) - - const virtualEnvResponseRowType: TriggerVirtualEnvResponseRowType = - [DeploymentNodeType.CD, DeploymentNodeType.POSTCD, DeploymentNodeType.PRECD].includes( - bulkTriggerType, - ) && isVirtualEnv - ? { - isVirtual: true, - helmPackageName: response.value?.result?.helmPackageName, - cdWorkflowType: bulkTriggerType, - } - : {} - - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.PASS, - envId: triggeredAppList[index].envId, - message: '', - ...virtualEnvResponseRowType, - }) - } else { - const errorReason = response.reason - if (errorReason.code === API_STATUS_CODES.EXPECTATION_FAILED) { - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.SKIP], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.SKIP], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.SKIP], - ) - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.SKIP, - message: errorReason.errors[0].userMessage, - }) - } else if (errorReason.code === 403 || errorReason.code === 422) { - // Adding 422 to handle the unauthorized state due to deployment window - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.UNAUTHORIZE], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.UNAUTHORIZE], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.UNAUTHORIZE], - ) - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.UNAUTHORIZE, - message: errorReason.errors[0].userMessage, - }) - } else { - const statusType = filterStatusType( - type, - BULK_CI_RESPONSE_STATUS_TEXT[BulkResponseStatus.FAIL], - BULK_VIRTUAL_RESPONSE_STATUS[BulkResponseStatus.FAIL], - BULK_CD_RESPONSE_STATUS_TEXT[BulkResponseStatus.FAIL], - ) - _responseList.push({ - appId: triggeredAppList[index].appId, - appName: triggeredAppList[index].appName, - statusText: statusType, - status: BulkResponseStatus.FAIL, - message: errorReason.errors[0].userMessage, - }) - } - } - }) - updateResponseListData(_responseList) - setCDLoading(false) - setCILoading(false) - setIsBulkTriggerLoading(false) - getWorkflowStatusData(workflows) - }) - } else { - setCDLoading(false) - setCILoading(false) - setIsBulkTriggerLoading(false) - - if (!skippedResources.length) { - hideBulkCIModal() - hideBulkCDModal() - } else { - updateResponseListData(_responseList) - } - } - } - - // Would only set data no need to get data related to materials from it, we will get that in bulk trigger - const createBulkCDTriggerData = (): BulkCDDetailTypeResponse => { - const uniqueReleaseTags: string[] = [] - const uniqueTagsSet = new Set() - const _selectedAppWorkflowList: BulkCDDetailType[] = [] - - filteredWorkflows.forEach((wf) => { - if (wf.isSelected) { - // extract unique tags for this workflow - wf.appReleaseTags?.forEach((tag) => { - if (!uniqueTagsSet.has(tag)) { - uniqueReleaseTags.push(tag) - } - uniqueTagsSet.add(tag) - }) - const _cdNode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CD && node.environmentId === +envId, - ) - const selectedCINode = wf.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - const doesWorkflowContainsWebhook = selectedCINode?.type === WorkflowNodeType.WEBHOOK - - let _selectedNode: CommonNodeAttr - if (bulkTriggerType === DeploymentNodeType.PRECD) { - _selectedNode = _cdNode.preNode - } else if (bulkTriggerType === DeploymentNodeType.CD) { - _selectedNode = _cdNode - } else if (bulkTriggerType === DeploymentNodeType.POSTCD) { - _selectedNode = _cdNode.postNode - } - if (_selectedNode) { - const stageType = DeploymentNodeType[_selectedNode.type] - const isTriggerBlockedDueToPlugin = - _selectedNode.isTriggerBlocked && _selectedNode.showPluginWarning - const isTriggerBlockedDueToMandatoryTags = - _selectedNode.isTriggerBlocked && - _selectedNode.triggerBlockedInfo?.blockedBy === TriggerBlockType.MANDATORY_TAG - const stageText = getStageTitle(stageType) - - _selectedAppWorkflowList.push({ - workFlowId: wf.id, - appId: wf.appId, - name: wf.name, - cdPipelineName: _cdNode.title, - cdPipelineId: _cdNode.id, - stageType, - triggerType: _cdNode.triggerType, - envName: _selectedNode.environmentName, - envId: _selectedNode.environmentId, - parentPipelineId: _selectedNode.parentPipelineId, - parentPipelineType: WorkflowNodeType[_selectedNode.parentPipelineType], - parentEnvironmentName: _selectedNode.parentEnvironmentName, - material: _selectedNode.inputMaterialList, - approvalConfigData: _selectedNode.approvalConfigData, - requestedUserId: _selectedNode.requestedUserId, - appReleaseTags: wf.appReleaseTags, - tagsEditable: wf.tagsEditable, - ciPipelineId: _selectedNode.connectingCiPipelineId, - hideImageTaggingHardDelete: wf.hideImageTaggingHardDelete, - showPluginWarning: _selectedNode.showPluginWarning, - isTriggerBlockedDueToPlugin, - configurePluginURL: getCDPipelineURL( - String(wf.appId), - wf.id, - doesWorkflowContainsWebhook ? '0' : selectedCINode?.id, - doesWorkflowContainsWebhook, - _selectedNode.id, - true, - ), - consequence: _selectedNode.pluginBlockState, - warningMessage: - isTriggerBlockedDueToPlugin || isTriggerBlockedDueToMandatoryTags - ? `${stageText} is blocked` - : '', - triggerBlockedInfo: _selectedNode.triggerBlockedInfo, - isExceptionUser: wf.isExceptionUser, - }) - } else { - let warningMessage = '' - if (bulkTriggerType === DeploymentNodeType.PRECD) { - warningMessage = 'No pre-deployment stage' - } else if (bulkTriggerType === DeploymentNodeType.CD) { - warningMessage = 'No deployment stage' - } else if (bulkTriggerType === DeploymentNodeType.POSTCD) { - warningMessage = 'No post-deployment stage' - } - _selectedAppWorkflowList.push({ - workFlowId: wf.id, - appId: wf.appId, - name: wf.name, - warningMessage, - envName: _cdNode.environmentName, - envId: _cdNode.environmentId, - }) - } - } - }) - _selectedAppWorkflowList.sort((a, b) => sortCallback('name', a, b)) - return { - bulkCDDetailType: _selectedAppWorkflowList, - uniqueReleaseTags, - } - } - if (pageViewType === ViewType.LOADING) { return } @@ -1098,36 +483,14 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT return null } - const bulkCDDetailTypeResponse = createBulkCDTriggerData() - const _selectedAppWorkflowList: BulkCDDetailType[] = bulkCDDetailTypeResponse.bulkCDDetailType - - const { uniqueReleaseTags } = bulkCDDetailTypeResponse - - const feasiblePipelineIds = new Set( - getSelectedCDNodesWithArtifacts(filteredWorkflows).map(({ node }) => +node.id), - ) - - // Have to look for its each prop carefully - // No need to send uniqueReleaseTags will get those in BulkCDTrigger itself return ( - ) } @@ -1164,136 +527,18 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT ) } - const renderCDMaterialContent = ({ - node, - appId, - workflowId, - selectedAppName, - doesWorkflowContainsWebhook, - ciNodeId, - }: RenderCDMaterialContentProps) => { - const configurePluginURL = getCDPipelineURL( - String(appId), - workflowId, - doesWorkflowContainsWebhook ? '0' : ciNodeId, - doesWorkflowContainsWebhook, - node?.id, - true, - ) - - return ( - - ) - } - - const renderCDMaterial = (): JSX.Element | null => { - if (!selectedCDNode?.id) { - return null - } - - if (location.search.includes('cd-node') || location.search.includes('rollback-node')) { - let node: CommonNodeAttr - let _appID - let selectedAppName: string - let workflowId: string - let selectedCINode: CommonNodeAttr - - if (selectedCDNode?.id) { - for (const _wf of filteredWorkflows) { - node = _wf.nodes.find((el) => +el.id == selectedCDNode.id && el.type == selectedCDNode.type) - if (node) { - selectedCINode = _wf.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - workflowId = _wf.id - _appID = _wf.appId - selectedAppName = _wf.name - break - } - } - } - const material = node?.[materialType] || [] - - return ( - -
0 ? '' : 'no-material' - }`} - onClick={stopPropagation} - > - {isCDLoading ? ( - <> -
- -
-
- -
- - ) : ( - renderCDMaterialContent({ - node, - appId: _appID, - selectedAppName, - workflowId, - doesWorkflowContainsWebhook: selectedCINode?.type === WorkflowNodeType.WEBHOOK, - ciNodeId: selectedCINode?.id, - }) - )} -
-
- ) - } - - return null - } - const renderApprovalMaterial = () => { if (ApprovalMaterialModal && location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE)) { - let node: CommonNodeAttr - let _appID - if (selectedCDNode?.id) { - for (const _wf of filteredWorkflows) { - node = _wf.nodes.find((el) => +el.id == selectedCDNode.id && el.type == selectedCDNode.type) - if (node) { - _appID = _wf.appId - break - } - } - } + const { node, appId } = getSelectedNodeAndAppId(filteredWorkflows, location.search) return (
{_showPopupMenu && renderDeployPopupMenu()}
@@ -1474,6 +712,7 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT index={index} handleWebhookAddImageClick={handleWebhookAddImageClick(workflow.appId)} openCIMaterialModal={openCIMaterialModal} + reloadTriggerView={reloadTriggerView} /> ))} @@ -1498,34 +737,31 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT - - {renderWorkflow()} - - - - - - - {renderCDMaterial()} - {renderBulkCDMaterial()} - {renderBulkCIMaterial()} - {renderApprovalMaterial()} - {renderBulkSourceChange()} - + {renderWorkflow()} + + + + + + + + {renderBulkCDMaterial()} + {renderBulkCIMaterial()} + {renderApprovalMaterial()} + {renderBulkSourceChange()}
{!!selectedAppList.length && ( diff --git a/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx b/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx index 93c1a0d328..fcb40f8a72 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx @@ -33,10 +33,8 @@ export const TriggerResponseModalFooter = ({ closePopup, isLoading, responseList, - skipHibernatedApps, onClickRetryBuild, onClickRetryDeploy, - pipelineIdVsStrategyMap, }: TriggerResponseModalFooterProps) => { const isShowRetryButton = responseList?.some((response) => response.status === BulkResponseStatus.FAIL) @@ -52,7 +50,7 @@ export const TriggerResponseModalFooter = ({ if (onClickRetryBuild) { onClickRetryBuild(appsToRetry) } else { - onClickRetryDeploy(skipHibernatedApps, pipelineIdVsStrategyMap, appsToRetry) + onClickRetryDeploy(appsToRetry) } } @@ -83,7 +81,7 @@ const TriggerResponseModalBody = ({ responseList, isLoading, isVirtualEnv }: Tri return } return ( -
+
- appList.some((app) => { - if (!app.isExceptionUser) { - return false - } - - return (app.material || []).some( - (material) => material.isSelected && !getIsMaterialApproved(material.userApprovalMetadata), - ) - }) - -export const getIsImageApprovedByDeployerSelected = (appList: BulkCDDetailType[]): boolean => - appList.some((app) => { - if (!app.isExceptionUser) { - return false - } - - return (app.material || []).some( - (material) => - material.isSelected && - !material.canApproverDeploy && - material.userApprovalMetadata?.hasCurrentUserApproved, - ) - }) - export const getSelectedCDNode = (bulkTriggerType: DeploymentNodeType, _cdNode: CommonNodeAttr) => { if (bulkTriggerType === DeploymentNodeType.PRECD) { return _cdNode.preNode @@ -58,7 +34,41 @@ export const getSelectedCDNode = (bulkTriggerType: DeploymentNodeType, _cdNode: return null } -export const getSelectedAppListForBulkStrategy = (appList: BulkCDDetailType[], feasiblePipelineIds: Set) => - appList - .map((app) => ({ pipelineId: +app.cdPipelineId, appName: app.name })) - .filter(({ pipelineId }) => feasiblePipelineIds.has(pipelineId)) +export const getSelectedAppListForBulkStrategy = ( + appInfoRes: DeployImageContentProps['appInfoMap'], +): Pick[] => { + const feasiblePipelineIds: Set = Object.values(appInfoRes).reduce((acc, appDetails) => { + const materials = appDetails.materialResponse?.materials || [] + const isMaterialSelected = materials.some((material) => material.isSelected) + + if (isMaterialSelected) { + acc.add(+appDetails.pipelineId) + } + + return acc + }, new Set()) + + const appList: Pick[] = Object.values(appInfoRes).map((appDetails) => ({ + pipelineId: +appDetails.pipelineId, + appName: appDetails.appName, + })) + + return appList.filter(({ pipelineId }) => feasiblePipelineIds.has(pipelineId)) +} + +export const getSelectedNodeAndAppId = ( + workflows: WorkflowType[], + search: string, +): { node: CommonNodeAttr; appId: number } => { + const { cdNodeId, nodeType } = getNodeIdAndTypeFromSearch(search) + + const result = workflows.reduce( + (acc, workflow) => { + if (acc.node) return acc + const node = workflow.nodes.find((n) => n.id === cdNodeId && n.type === nodeType) + return node ? { node, appId: workflow.appId } : acc + }, + { node: undefined, appId: undefined }, + ) + return result +} diff --git a/src/components/app/details/appDetails/AppDetails.tsx b/src/components/app/details/appDetails/AppDetails.tsx index b06c88a57d..b89913da53 100644 --- a/src/components/app/details/appDetails/AppDetails.tsx +++ b/src/components/app/details/appDetails/AppDetails.tsx @@ -78,7 +78,7 @@ import RotatePodsModal from '../../../v2/appDetails/sourceInfo/rotatePods/Rotate import SyncErrorComponent from '../../../v2/appDetails/SyncError.component' import { TriggerUrlModal } from '../../list/TriggerUrl' import { fetchAppDetailsInTime, fetchResourceTreeInTime } from '../../service' -import { AggregatedNodes } from '../../types' +import { AggregatedNodes, AppDetailsCDModalType } from '../../types' import { renderCIListHeader } from '../cdDetails/utils' import { MATERIAL_TYPE } from '../triggerView/types' import AppEnvSelector from './AppDetails.components' @@ -235,7 +235,7 @@ const Details: React.FC = ({ const [rotateModal, setRotateModal] = useState(false) const [hibernating, setHibernating] = useState(false) const [showIssuesModal, toggleIssuesModal] = useState(false) - const [CDModalMaterialType, setCDModalMaterialType] = useState(null) + const [CDModalMaterialType, setCDModalMaterialType] = useState(null) const [appDetailsError, setAppDetailsError] = useState(undefined) const [hibernationPatchChartName, setHibernationPatchChartName] = useState('') @@ -591,8 +591,8 @@ const Details: React.FC = ({ environmentId={appDetails.environmentId} environmentName={appDetails.environmentName} isVirtualEnvironment={appDetails.isVirtualEnvironment} + appName={appDetails.appName} deploymentAppType={appDetails.deploymentAppType} - loadingDetails={loadingDetails} cdModal={{ cdPipelineId: appDetails.cdPipelineId, ciPipelineId: appDetails.ciPipelineId, diff --git a/src/components/app/details/appDetails/AppDetailsCDModal.tsx b/src/components/app/details/appDetails/AppDetailsCDModal.tsx index ffd198b866..a1783e1206 100644 --- a/src/components/app/details/appDetails/AppDetailsCDModal.tsx +++ b/src/components/app/details/appDetails/AppDetailsCDModal.tsx @@ -16,14 +16,14 @@ import { useHistory, useLocation } from 'react-router-dom' -import { DeploymentNodeType, stopPropagation, VisibleModal } from '@devtron-labs/devtron-fe-common-lib' +import { DeploymentNodeType } from '@devtron-labs/devtron-fe-common-lib' import { importComponentFromFELibrary } from '../../../common' import { URL_PARAM_MODE_TYPE } from '../../../common/helpers/types' import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service' import { AppDetailsCDModalType } from '../../types' -import CDMaterial from '../triggerView/cdMaterial' import { TRIGGER_VIEW_PARAMS } from '../triggerView/Constants' +import { DeployImageModal } from '../triggerView/DeployImageModal' const ApprovalMaterialModal = importComponentFromFELibrary('ApprovalMaterialModal') @@ -34,7 +34,6 @@ const AppDetailsCDModal = ({ cdModal, deploymentAppType, isVirtualEnvironment, - loadingDetails, environmentName, handleSuccess, materialType, @@ -55,7 +54,6 @@ const AppDetailsCDModal = ({ ApprovalMaterialModal && location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE) && ( (mode === URL_PARAM_MODE_TYPE.LIST || mode === URL_PARAM_MODE_TYPE.REVIEW_CONFIG) && ( - -
- -
-
+ ) return ( diff --git a/src/components/app/details/appDetails/IssuesCard.tsx b/src/components/app/details/appDetails/IssuesCard.tsx index 979a90c288..93c2c99c2a 100644 --- a/src/components/app/details/appDetails/IssuesCard.tsx +++ b/src/components/app/details/appDetails/IssuesCard.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, SyntheticEvent } from 'react' import Tippy from '@tippyjs/react' import { DeploymentAppTypes, @@ -27,6 +27,7 @@ import { ToastManager, ForceDeleteConfirmationModal, LoadingCard, + stopPropagation, } from '@devtron-labs/devtron-fe-common-lib' import { ReactComponent as ICHelpOutline } from '../../../../assets/icons/ic-help-outline.svg' import { ReactComponent as ErrorIcon } from '../../../../assets/icons/ic-warning.svg' @@ -50,7 +51,8 @@ const IssuesCard = ({ cardLoading, setErrorsList, toggleIssuesModal, setDetailed const appDetails = IndexStore.getAppDetails() const conditions = appDetails?.resourceTree?.conditions || [] - const showIssuesListingModal = () => { + const showIssuesListingModal = (e?: SyntheticEvent) => { + stopPropagation(e) toggleIssuesModal(true) } diff --git a/src/components/app/details/triggerView/BranchRegexModal.tsx b/src/components/app/details/triggerView/BranchRegexModal.tsx index 1bcbeeaa13..b7b2debcbe 100644 --- a/src/components/app/details/triggerView/BranchRegexModal.tsx +++ b/src/components/app/details/triggerView/BranchRegexModal.tsx @@ -72,7 +72,7 @@ const BranchRegexModal = ({ const isRegexValueInvalid = (_cm): void => { const regExp = new RegExp(_cm.source.regex) const regVal = structuredClone(regexValue[_cm.gitMaterialId]) - if (!regExp.test(regVal.value)) { + if (!regExp.test(regVal.value) || !regVal.value) { const _regexVal = { ...regexValue, [_cm.gitMaterialId]: { value: regVal.value, isInvalid: true }, @@ -84,42 +84,39 @@ const BranchRegexModal = ({ const handleSave = async (e: React.FormEvent) => { e.preventDefault() setIsSavingRegexValue(true) - const payload: Parameters[0] = { - appId: +appId, - id: +workflowId, - ciPipelineMaterial: [], - } - if (selectedCIPipeline?.ciMaterial?.length) { - payload.ciPipelineMaterial = selectedCIPipeline.ciMaterial.map((_cm) => { - const regVal = regexValue[_cm.gitMaterialId] - let _updatedCM + try { + const payload: Parameters[0] = { + appId: +appId, + id: +workflowId, + ciPipelineMaterial: selectedCIPipeline.ciMaterial.map((_cm) => { + const regVal = regexValue[_cm.gitMaterialId] + let _updatedCM - if (regVal?.value && _cm.source.regex) { - isRegexValueInvalid(_cm) + if (regVal?.value && _cm.source.regex) { + isRegexValueInvalid(_cm) - _updatedCM = { - ..._cm, - type: SourceTypeMap.BranchFixed, - value: regVal.value, - regex: _cm.source.regex, + _updatedCM = { + ..._cm, + type: SourceTypeMap.BranchFixed, + value: regVal.value, + regex: _cm.source.regex, + } + } else { + // Maintain the flattened object structure for unchanged values + _updatedCM = { + ..._cm, + ..._cm.source, + } } - } else { - // Maintain the flattened object structure for unchanged values - _updatedCM = { - ..._cm, - ..._cm.source, - } - } - // Deleting as it's not required in the request payload - delete _updatedCM.source + // Deleting as it's not required in the request payload + delete _updatedCM.source - return _updatedCM - }) - } + return _updatedCM + }), + } - try { await savePipeline(payload, { isRegexMaterial: true, isTemplateView: false, diff --git a/src/components/app/details/triggerView/BuildImageModal/BuildImageModal.tsx b/src/components/app/details/triggerView/BuildImageModal/BuildImageModal.tsx index 3c6a55979b..0836a65db8 100644 --- a/src/components/app/details/triggerView/BuildImageModal/BuildImageModal.tsx +++ b/src/components/app/details/triggerView/BuildImageModal/BuildImageModal.tsx @@ -1,11 +1,3 @@ -/** - * Service layer requirements: - * - We need to have filteredCIPipelines - * - Will find the selected pipeline from filteredCIPipelines from which we will get envId [in case of job view] - * - isLoading as prop for when workflows are loading - * - If !isLoading and filteredCIPipelines does not contain the selected pipeline, we will show error - */ - import { useEffect, useRef, useState } from 'react' import { Prompt, useHistory, useParams } from 'react-router-dom' @@ -179,7 +171,7 @@ const BuildImageModal = ({ } const getCIPipelineURLWrapper = (): string => - getCIPipelineURL(String(appId), String(workflowId), true, ciNodeId, false, ciNode?.isJobCI, false) + getCIPipelineURL(String(appId), String(workflowId), true, ciNodeId, false, ciNode.isJobCI, false) const redirectToCIPipeline = () => { push(getCIPipelineURLWrapper()) @@ -257,7 +249,7 @@ const BuildImageModal = ({ return (
+
+ + {materialList.length === 0 + ? renderMaterialListEmptyState() + : renderMaterialList(materialList, false)} + + {!areNoMoreImagesPresent && !!materialList?.length && ( + + )} + + ) + } + + return ( +
+ +
+ ) + } + + if (showFiltersView && !isBulkTrigger) { + return renderConfiguredFilters() + } + + if (materials.length === 0 && !isBulkTrigger) { + return renderMaterialListEmptyState() + } + + return ( + <> + {!showFiltersView && + !isBulkTrigger && + isApprovalConfigured && + !isExceptionUser && + ApprovedImagesMessage && + (isRollbackTrigger || materials.length - Number(isConsumedImageAvailable) > 0) && ( + } + /> + )} + {!showFiltersView && + !isBulkTrigger && + MaintenanceWindowInfoBar && + deploymentWindowMetadata.type === DEPLOYMENT_WINDOW_TYPE.MAINTENANCE && + deploymentWindowMetadata.isActive && ( + + )} + +
+ {renderSidebar()} +
+ {showDeploymentWindowInfoBar && ( + + )} +
+ {renderContent()} +
+
+
+ + ) +} + +export default DeployImageContent diff --git a/src/components/app/details/triggerView/DeployImageModal/DeployImageHeader.tsx b/src/components/app/details/triggerView/DeployImageModal/DeployImageHeader.tsx new file mode 100644 index 0000000000..2b5330f2ea --- /dev/null +++ b/src/components/app/details/triggerView/DeployImageModal/DeployImageHeader.tsx @@ -0,0 +1,63 @@ +import { + Button, + ButtonStyleType, + ButtonVariantType, + ComponentSizeType, + Icon, +} from '@devtron-labs/devtron-fe-common-lib' + +import { DeployImageHeaderProps } from './types' +import { getCDModalHeaderText } from './utils' + +const DeployImageHeader = ({ + envName, + handleClose, + stageType, + isRollbackTrigger, + isVirtualEnvironment, + handleNavigateToMaterialListView, + children, + title, +}: DeployImageHeaderProps) => ( +
+
+ {handleNavigateToMaterialListView && ( +
+ +
+) + +export default DeployImageHeader diff --git a/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx b/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx new file mode 100644 index 0000000000..1894e39c50 --- /dev/null +++ b/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx @@ -0,0 +1,775 @@ +import { Dispatch, SetStateAction, SyntheticEvent, useMemo, useState } from 'react' +import { Prompt, useHistory, useLocation } from 'react-router-dom' + +import { + ACTION_STATE, + AnimatedDeployButton, + API_STATUS_CODES, + ArtifactInfo, + ConditionalWrap, + DEFAULT_ROUTE_PROMPT_MESSAGE, + DEPLOYMENT_CONFIG_DIFF_SORT_KEY, + DeploymentAppTypes, + DeploymentNodeType, + DeploymentStrategyType, + DeploymentWithConfigType, + Drawer, + EnvResourceType, + ErrorScreenManager, + FilterStates, + getIsApprovalPolicyConfigured, + handleAnalyticsEvent, + Icon, + MODAL_TYPE, + ModuleNameMap, + ModuleStatus, + noop, + PipelineDeploymentStrategy, + ServerErrors, + showError, + SortingOrder, + stopPropagation, + ToastManager, + ToastVariantType, + Tooltip, + triggerCDNode, + uploadCDPipelineFile, + useAsync, + useDownload, + usePrompt, + useSearchString, +} from '@devtron-labs/devtron-fe-common-lib' + +import { importComponentFromFELibrary } from '@Components/common' +import { URL_PARAM_MODE_TYPE } from '@Components/common/helpers/types' +import { getModuleInfo } from '@Components/v2/devtronStackManager/DevtronStackManager.service' +import { URLS } from '@Config/routes' + +import { getCanDeployWithoutApproval, getCanImageApproverDeploy, getWfrId } from '../cdMaterials.utils' +import { CDButtonLabelMap } from '../config' +import { TRIGGER_VIEW_GA_EVENTS } from '../Constants' +import { PipelineConfigDiff, usePipelineDeploymentConfig } from '../PipelineConfigDiff' +import { PipelineConfigDiffStatusTile } from '../PipelineConfigDiff/PipelineConfigDiffStatusTile' +import { MATERIAL_TYPE } from '../types' +import { INITIAL_DEPLOY_VIEW_STATE } from './constants' +import DeployImageContent from './DeployImageContent' +import DeployImageHeader from './DeployImageHeader' +import MaterialListSkeleton from './MaterialListSkeleton' +import RuntimeParamsSidebar from './RuntimeParamsSidebar' +import { getMaterialResponseList, loadOlderImages } from './service' +import { DeployImageContentProps, DeployImageModalProps, DeployViewStateType, HandleDeploymentProps } from './types' +import { + getAllowWarningWithTippyNodeTypeProp, + getCDArtifactId, + getConfigToDeployValue, + getDeployButtonIcon, + getDeployButtonStyle, + getInitialSelectedConfigToDeploy, + getIsExceptionUser, + getIsImageApprover, + getTriggerArtifactInfoProps, + handleTriggerErrorMessageForHelmManifestPush, + renderDeployCTATippyData, + showErrorIfNotAborted, +} from './utils' + +const ApprovalInfoTippy = importComponentFromFELibrary('ApprovalInfoTippy') +const getDeploymentStrategies: (pipelineIds: number[]) => Promise = + importComponentFromFELibrary('getDeploymentStrategies', null, 'function') +const AllowedWithWarningTippy = importComponentFromFELibrary('AllowedWithWarningTippy') +const SelectDeploymentStrategy = importComponentFromFELibrary('SelectDeploymentStrategy', null, 'function') +const DeploymentWindowConfirmationDialog = importComponentFromFELibrary('DeploymentWindowConfirmationDialog') +const validateRuntimeParameters = importComponentFromFELibrary( + 'validateRuntimeParameters', + () => ({ isValid: true, cellError: {} }), + 'function', +) +const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') +const downloadManifestForVirtualEnvironment = importComponentFromFELibrary( + 'downloadManifestForVirtualEnvironment', + null, + 'function', +) + +const DeployImageModal = ({ + appId, + envId, + appName, + stageType, + pipelineId, + materialType, + handleClose: handleCloseProp, + handleSuccess, + deploymentAppType, + isVirtualEnvironment, + envName, + showPluginWarningBeforeTrigger: _showPluginWarningBeforeTrigger = false, + consequence, + configurePluginURL, + triggerType, + isRedirectedFromAppDetails, + isTriggerBlockedDueToPlugin, + parentEnvironmentName, +}: DeployImageModalProps) => { + const history = useHistory() + const { pathname } = useLocation() + const { searchParams } = useSearchString() + const { handleDownload } = useDownload() + const searchImageTag = searchParams.search || '' + const isCDNode = stageType === DeploymentNodeType.CD + + const [isInitialDataLoading, initialDataResponse, initialDataError, reloadInitialData, unTypedSetInitialData] = + useAsync( + () => + getMaterialResponseList({ + stageType, + pipelineId, + appId, + envId, + materialType, + initialSearch: searchImageTag, + }), + [searchImageTag], + !isTriggerBlockedDueToPlugin, + ) + + const [, moduleInfoRes] = useAsync(() => getModuleInfo(ModuleNameMap.SECURITY)) + + const isSecurityModuleInstalled = moduleInfoRes?.result?.status === ModuleStatus.INSTALLED + + const setInitialData: Dispatch> = unTypedSetInitialData + + const [pipelineStrategiesLoading, pipelineStrategies, pipelineStrategiesError, reloadStrategies] = useAsync( + () => getDeploymentStrategies([pipelineId]), + [pipelineId], + !!getDeploymentStrategies && !!pipelineId && isCDNode, + ) + + const [isDeploymentLoading, setIsDeploymentLoading] = useState(false) + const [deploymentStrategy, setDeploymentStrategy] = useState(null) + const [showPluginWarningOverlay, setShowPluginWarningOverlay] = useState(false) + const [showDeploymentWindowConfirmation, setShowDeploymentWindowConfirmation] = useState(false) + const [deployViewState, setDeployViewState] = useState>( + structuredClone(INITIAL_DEPLOY_VIEW_STATE), + ) + + const isPreOrPostCD = stageType === DeploymentNodeType.PRECD || stageType === DeploymentNodeType.POSTCD + const materialResponse = initialDataResponse?.[0] || null + const deploymentWindowMetadata = initialDataResponse?.[1] ?? ({} as (typeof initialDataResponse)[1]) + const policyConsequences = initialDataResponse?.[2] ?? ({} as (typeof initialDataResponse)[2]) + const materialList = materialResponse?.materials || [] + const selectedMaterial = materialList.find((material) => material.isSelected) + const isRollbackTrigger = materialType === MATERIAL_TYPE.rollbackMaterialList + const isExceptionUser = getIsExceptionUser(materialResponse) + const isApprovalConfigured = getIsApprovalPolicyConfigured( + materialResponse?.deploymentApprovalInfo?.approvalConfigData, + ) + const canApproverDeploy = materialResponse?.canApproverDeploy ?? false + const showConfigDiffView = searchParams.mode === URL_PARAM_MODE_TYPE.REVIEW_CONFIG && searchParams.deploy + const isSelectImageTrigger = materialType === MATERIAL_TYPE.inputMaterialList + const areAllImagesExcluded = materialList.every( + (materialDetails) => materialDetails.filterState !== FilterStates.ALLOWED, + ) + const selectedConfigToDeploy = getInitialSelectedConfigToDeploy(materialType, searchParams) + const showPluginWarningBeforeTrigger = _showPluginWarningBeforeTrigger && isPreOrPostCD + const allowWarningWithTippyNodeTypeProp = getAllowWarningWithTippyNodeTypeProp(stageType) + const runtimeParamsList = materialResponse?.runtimeParams || [] + const requestedUserId = materialResponse?.requestedUserId + const showFiltersView = deployViewState.showAppliedFilters || deployViewState.showConfiguredFilters + + usePrompt({ shouldPrompt: isDeploymentLoading }) + + const pipelineStrategyOptions = useMemo( + () => + (pipelineStrategies ?? []).flatMap(({ error, strategies }) => { + if (error) { + return [] + } + return strategies + }), + [pipelineStrategies], + ) + + const wfrId = getWfrId(selectedMaterial, materialList) + + const { + pipelineDeploymentConfigLoading, + pipelineDeploymentConfig, + radioSelectConfig, + diffFound, + noLastDeploymentConfig, + noSpecificDeploymentConfig, + canDeployWithConfig, + canReviewConfig, + scopeVariablesConfig, + urlFilters, + lastDeploymentWfrId, + errorConfig, + } = usePipelineDeploymentConfig({ + appId, + envId, + appName, + envName, + deploymentStrategy, + setDeploymentStrategy, + pipelineStrategyOptions, + isRollbackTriggerSelected: isRollbackTrigger, + pipelineId, + wfrId, + isCDNode, + }) + + const handleReload = () => { + reloadInitialData() + setDeployViewState((prev) => ({ + ...prev, + runtimeParamsErrorState: { + isValid: true, + cellError: {}, + }, + materialInEditModeMap: new Map(), + })) + } + + const handleClosePluginWarningOverlay = () => { + setShowPluginWarningOverlay(false) + } + + const handleConfirmationClose = (e: SyntheticEvent) => { + e.stopPropagation() + handleClosePluginWarningOverlay() + setShowDeploymentWindowConfirmation(false) + } + + const onClickSetInitialParams = (modeParamValue: URL_PARAM_MODE_TYPE) => { + const newParams = new URLSearchParams({ + ...searchParams, + sortBy: DEPLOYMENT_CONFIG_DIFF_SORT_KEY, + sortOrder: SortingOrder.ASC, + mode: modeParamValue, + deploy: getConfigToDeployValue(materialType, searchParams), + }) + + if (modeParamValue === URL_PARAM_MODE_TYPE.LIST) { + newParams.delete('sortOrder') + newParams.delete('sortBy') + } + + history.push({ + pathname: + modeParamValue === URL_PARAM_MODE_TYPE.REVIEW_CONFIG + ? // Replace consecutive trailing single slashes + `${pathname.replace(/\/+$/g, '')}/${URLS.APP_DIFF_VIEW}/${EnvResourceType.DeploymentTemplate}` + : `${pathname.split(`/${URLS.APP_DIFF_VIEW}`)[0]}`, + search: newParams.toString(), + }) + } + + const handleClose = () => { + if (isRedirectedFromAppDetails && showConfigDiffView) { + onClickSetInitialParams(URL_PARAM_MODE_TYPE.LIST) + } + handleCloseProp?.() + } + + const handleLoadOlderImages = async () => { + if (!deployViewState.isLoadingOlderImages) { + try { + setDeployViewState((prevState) => ({ + ...prevState, + isLoadingOlderImages: true, + })) + + const newMaterials = await loadOlderImages({ + materialList, + resourceFilters: materialResponse?.resourceFilters, + filterView: deployViewState.filterView, + appliedSearchText: searchImageTag, + stageType, + isRollbackTrigger, + pipelineId, + }) + + // Made a change not updating whole response rather updating only materials + setInitialData((prevData) => { + const updatedMaterialResponse = structuredClone(prevData[0]) + updatedMaterialResponse.materials = newMaterials + return [updatedMaterialResponse, prevData[1], prevData[2]] + }) + } catch (error) { + showError(error) + } finally { + setDeployViewState((prevState) => ({ + ...prevState, + isLoadingOlderImages: false, + })) + } + } + } + + const handleReviewConfigParams = () => onClickSetInitialParams(URL_PARAM_MODE_TYPE.REVIEW_CONFIG) + + const handleNavigateToListView = () => onClickSetInitialParams(URL_PARAM_MODE_TYPE.LIST) + + const isDeployButtonDisabled = () => { + const selectedImage = materialList.find((artifact) => artifact.isSelected) + + return ( + !selectedImage || + areAllImagesExcluded || + (isRollbackTrigger && (pipelineDeploymentConfigLoading || !canDeployWithConfig())) || + (selectedConfigToDeploy.value === DeploymentWithConfigType.LATEST_TRIGGER_CONFIG && noLastDeploymentConfig) + ) + } + + const renderDeployCTATippyContent = () => { + const isSelectedImagePresent = materialList.some((artifact) => artifact.isSelected) + + if (areAllImagesExcluded) { + return renderDeployCTATippyData( + 'No eligible images found!', + 'Please select an image that passes the configured filters to deploy', + ) + } + + if (!isSelectedImagePresent) { + return renderDeployCTATippyData('No image selected!', 'Please select an image to deploy') + } + + return renderDeployCTATippyData( + 'Selected Config not available!', + selectedConfigToDeploy.value === DeploymentWithConfigType.SPECIFIC_TRIGGER_CONFIG && + noSpecificDeploymentConfig + ? 'Please select a different image or configuration to deploy' + : 'Please select a different configuration to deploy', + ) + } + + const getDeployCTATippyWrapper = (children) => ( + + {children} + + ) + + const redirectToDeploymentStepsPage = () => { + history.push(`${URLS.APP}/${appId}/${URLS.APP_CD_DETAILS}/${envId}/${pipelineId}`) + } + + const handleDeployment = ({ ciArtifactId, deploymentWithConfig, computedWfrId }: HandleDeploymentProps) => { + const updatedRuntimeParamsErrorState = validateRuntimeParameters(runtimeParamsList) + setDeployViewState((prevState) => ({ + ...prevState, + runtimeParamsErrorState: updatedRuntimeParamsErrorState, + })) + if (!updatedRuntimeParamsErrorState.isValid) { + ToastManager.showToast({ + variant: ToastVariantType.error, + description: 'Please resolve all the errors before deploying', + }) + return + } + + handleAnalyticsEvent(TRIGGER_VIEW_GA_EVENTS.CDTriggered(stageType)) + setIsDeploymentLoading(true) + + if (appId && pipelineId && ciArtifactId) { + triggerCDNode({ + pipelineId: Number(pipelineId), + ciArtifactId: Number(ciArtifactId), + appId: Number(appId), + stageType, + deploymentWithConfig, + wfrId: computedWfrId, + abortControllerRef: null, + isRollbackTrigger, + ...(getRuntimeParamsPayload + ? { runtimeParamsPayload: getRuntimeParamsPayload(runtimeParamsList ?? []) } + : {}), + skipIfHibernated: false, + ...(SelectDeploymentStrategy && deploymentStrategy ? { strategy: deploymentStrategy } : {}), + }) + .then((response) => { + if (response.result) { + if (isVirtualEnvironment && deploymentAppType === DeploymentAppTypes.MANIFEST_DOWNLOAD) { + const { helmPackageName } = response.result + downloadManifestForVirtualEnvironment?.({ + appId, + envId, + helmPackageName, + cdWorkflowType: stageType, + handleDownload, + }) + } + + const msg = + materialType === MATERIAL_TYPE.rollbackMaterialList + ? 'Rollback Initiated' + : 'Deployment Initiated' + + ToastManager.showToast({ + variant: ToastVariantType.success, + description: msg, + }) + setIsDeploymentLoading(false) + handleSuccess?.() + handleClose() + } + }) + .catch((errors: ServerErrors) => { + if (isVirtualEnvironment && deploymentAppType === DeploymentAppTypes.MANIFEST_PUSH) { + handleTriggerErrorMessageForHelmManifestPush({ + serverError: errors, + searchParams, + redirectToDeploymentStepsPage, + }) + } else { + showErrorIfNotAborted(errors) + } + + setIsDeploymentLoading(false) + }) + } else { + let message = appId ? '' : 'app id missing ' + message += pipelineId ? '' : 'pipeline id missing ' + message += ciArtifactId ? '' : 'Artifact id missing ' + ToastManager.showToast({ + variant: ToastVariantType.error, + description: message, + }) + setIsDeploymentLoading(false) + } + } + + const deployTrigger = (e: SyntheticEvent) => { + stopPropagation(e) + handleConfirmationClose(e) + // Blocking the deploy action if already deploying or config is not available + if (isDeployButtonDisabled()) { + return + } + + const artifactId = +getCDArtifactId(selectedMaterial, materialList) + + if (isRollbackTrigger || isSelectImageTrigger) { + const computedWfrId = isRollbackTrigger ? wfrId : lastDeploymentWfrId + handleDeployment({ + ciArtifactId: artifactId, + deploymentWithConfig: selectedConfigToDeploy.value, + computedWfrId, + }) + return + } + + // Not sure when this call will come into play, but keeping it for now for backward compatibility + handleDeployment({ ciArtifactId: artifactId }) + } + + const onClickDeploy = (e: SyntheticEvent, disableDeployButton: boolean) => { + stopPropagation(e) + + if (!disableDeployButton) { + if (!showPluginWarningOverlay && showPluginWarningBeforeTrigger) { + setShowPluginWarningOverlay(true) + return + } + + if ( + deploymentWindowMetadata.userActionState && + deploymentWindowMetadata.userActionState !== ACTION_STATE.ALLOWED + ) { + setShowDeploymentWindowConfirmation(true) + return + } + + deployTrigger(e) + } + } + + const getOnClickDeploy = (disableDeployButton: boolean) => (e: SyntheticEvent) => + onClickDeploy(e, disableDeployButton) + + const onSearchApply = (newSearchText: string) => { + const newParams = new URLSearchParams({ + ...searchParams, + search: newSearchText, + }) + + setDeployViewState((prevState) => ({ + ...prevState, + searchText: newSearchText, + })) + + history.push({ + pathname, + search: newParams.toString(), + }) + } + + const uploadRuntimeParamsFile: DeployImageContentProps['uploadRuntimeParamsFile'] = ({ + file, + allowedExtensions, + maxUploadSize, + }) => uploadCDPipelineFile({ file, allowedExtensions, maxUploadSize, appId, envId }) + + const renderTriggerDeployButton = (disableDeployButton: boolean) => { + const { userActionState } = deploymentWindowMetadata + const canDeployWithoutApproval = getCanDeployWithoutApproval(selectedMaterial, isExceptionUser) + const canImageApproverDeploy = getCanImageApproverDeploy(selectedMaterial, canApproverDeploy, isExceptionUser) + + return ( + : null} + style={getDeployButtonStyle(userActionState, canDeployWithoutApproval, canImageApproverDeploy)} + disabled={disableDeployButton} + tooltipContent={ + canDeployWithoutApproval || canImageApproverDeploy + ? 'You are authorized to deploy as an exception user' + : '' + } + animateStartIcon={ + isCDNode && !disableDeployButton && (!userActionState || userActionState === ACTION_STATE.ALLOWED) + } + /> + ) + } + + const setMaterialResponse: DeployImageContentProps['setMaterialResponse'] = (callback) => { + setInitialData((prevData) => { + const updatedMaterialResponse = callback(structuredClone(prevData[0])) + return [updatedMaterialResponse, prevData[1], prevData[2]] + }) + } + + const renderFooter = () => { + const disableDeployButton = + isDeployButtonDisabled() || + (!isExceptionUser && + materialList.length > 0 && + !canApproverDeploy && + getIsImageApprover(selectedMaterial?.userApprovalMetadata)) + + const hideConfigDiffSelector = isApprovalConfigured && disableDeployButton + + return ( +
+ {!hideConfigDiffSelector && + (isRollbackTrigger || isSelectImageTrigger) && + !showConfigDiffView && + isCDNode ? ( + + ) : ( + // NOTE: needed so that the button is pushed to the right since justify-content is set to space-between +
+ )} +
+ {SelectDeploymentStrategy && + isCDNode && + +(pipelineStrategies?.[0]?.error?.code || 0) !== API_STATUS_CODES.NOT_FOUND && ( + + )} + + {AllowedWithWarningTippy && showPluginWarningBeforeTrigger ? ( + + {renderTriggerDeployButton(disableDeployButton)} + + ) : ( + renderTriggerDeployButton(disableDeployButton) + )} + +
+
+ ) + } + + const deployViewStateProps: DeployImageContentProps['deployViewState'] = { + ...deployViewState, + appliedSearchText: searchImageTag, + } + + const renderContent = () => { + if (isInitialDataLoading) { + return ( +
+ {isPreOrPostCD && ( + + )} + + +
+ ) + } + + if (initialDataError) { + return ( + + ) + } + + if (showConfigDiffView && canReviewConfig()) { + return ( + + ) + } + + return ( + + ) + } + + return ( + <> + +
+
+ {!showFiltersView && ( + + {showConfigDiffView && selectedMaterial && ( + + )} + + )} + +
{renderContent()}
+
+ + {initialDataError || isInitialDataLoading || showFiltersView || materialList.length === 0 ? null : ( +
+ {renderFooter()} +
+ )} +
+
+ + {DeploymentWindowConfirmationDialog && showDeploymentWindowConfirmation && ( + + )} + + + + ) +} + +export default DeployImageModal diff --git a/src/components/app/details/triggerView/DeployImageModal/ImageSelectionCTA.tsx b/src/components/app/details/triggerView/DeployImageModal/ImageSelectionCTA.tsx new file mode 100644 index 0000000000..cab90c1862 --- /dev/null +++ b/src/components/app/details/triggerView/DeployImageModal/ImageSelectionCTA.tsx @@ -0,0 +1,125 @@ +import { + Button, + ButtonVariantType, + ComponentSizeType, + EXCLUDED_IMAGE_TOOLTIP, + FilterStates, + Icon, + Tooltip, +} from '@devtron-labs/devtron-fe-common-lib' + +import { importComponentFromFELibrary } from '@Components/common' + +import { getIsMaterialApproved } from '../cdMaterials.utils' +import { ImageSelectionCTAProps } from './types' +import { getIsImageApprover } from './utils' + +const ExpireApproval = importComponentFromFELibrary('ExpireApproval') + +const ImageSelectionCTA = ({ + material, + disableSelection, + requestedUserId, + reloadMaterials, + appId, + pipelineId, + canApproverDeploy, + isExceptionUser, + handleImageSelection: handleImageSelectionProp, +}: ImageSelectionCTAProps) => { + const isApprovalRequester = + material.userApprovalMetadata?.requestedUserData && + material.userApprovalMetadata.requestedUserData.userId === requestedUserId + const isImageApprover = getIsImageApprover(material.userApprovalMetadata) + const isMaterialApproved = getIsMaterialApproved(material.userApprovalMetadata) + + const shouldRenderExpireApproval = + isApprovalRequester && + !isImageApprover && + !disableSelection && + isMaterialApproved && + material.userApprovalMetadata?.canCurrentUserApprove + + const handleImageSelection = () => { + handleImageSelectionProp(material.index) + } + + const renderMaterialCTA = () => { + if (material.filterState !== FilterStates.ALLOWED) { + return ( + + Excluded + + ) + } + + if (material.vulnerable) { + return ( + + Security Issues Found + + ) + } + if (disableSelection || (!isExceptionUser && !canApproverDeploy && isImageApprover)) { + return ( + + + SELECT + + + ) + } + if (material.isSelected) { + return + } + + return ( + + is applied on + +

+ ) + + const renderLoadMoreButton = () => ( + -
-
- -
- - ) : ( - this.renderCDMaterialContent(cdNode) - )} -
- - ) - } + const renderApprovalMaterial = () => { + if (ApprovalMaterialModal && location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE)) { + const { node, cdNodeId } = getSelectedNodeFromWorkflows(workflows, location.search) - return null - } - - renderApprovalMaterial() { - if (ApprovalMaterialModal && this.props.location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE)) { - const node: CommonNodeAttr = this.getCDNode() return ( ) } @@ -508,63 +121,49 @@ class TriggerView extends Component { return null } - renderWebhookAddImageModal() { - if ( - WebhookAddImageModal && - shouldRenderWebhookAddImageModal(this.props.location) && - this.state.selectedWebhookNodeId - ) { + const renderWebhookAddImageModal = () => { + if (WebhookAddImageModal && shouldRenderWebhookAddImageModal(location) && selectedWebhookNodeId) { return ( - + ) } return null } - revertToPreviousURL = () => { - this.props.history.push(this.props.match.url) - } - - renderWorkflow() { - return ( - <> - {this.state.workflows.map((workflow, index) => { - return ( - - ) - })} - - {this.renderWebhookAddImageModal()} - - ) - } - - renderHostErrorMessage() { - if (!this.state.hostURLConfig || this.state.hostURLConfig.value !== window.location.origin) { + const renderWorkflow = () => ( + <> + {workflows.map((workflow, index) => ( + + ))} + + {renderWebhookAddImageModal()} + + ) + + const renderHostErrorMessage = () => { + if (!hostUrlConfig || hostUrlConfig.value !== window.location.origin) { return (
@@ -575,89 +174,72 @@ class TriggerView extends Component { return null } - jobNotConfiguredSubtitle = () => { - return ( - <> - {APP_DETAILS.JOB_FULLY_NOT_CONFIGURED.subTitle}  - - - ) + if (isLoading) { + return } - render() { - if (this.state.view === ViewType.LOADING || this.state.isEnvListLoading) { - return - } - if (this.state.view === ViewType.ERROR) { - return - } - if (!this.state.workflows.length) { - return ( -
- {this.props.isJobView ? ( - - ) : ( - - )} -
- ) - } + if (workflowsError) { + return + } + if (!workflows.length) { return ( - <> -
- - {this.renderHostErrorMessage()} - {this.renderWorkflow()} - - - - - - - - {this.renderCDMaterial()} - {this.renderApprovalMaterial()} - -
- {WorkflowActionRouter && ( - + {isJobView ? ( + } + buttonTitle={APP_DETAILS.JOB_FULLY_NOT_CONFIGURED.buttonTitle} + isJobView={isJobView} /> + ) : ( + )} - +
) } + + return ( + <> +
+ {renderHostErrorMessage()} + {renderWorkflow()} + + + + + + + + + {renderApprovalMaterial()} +
+ {WorkflowActionRouter && ( + + )} + + ) } -export default withRouter(withAppContext(TriggerView)) +export default TriggerView diff --git a/src/components/app/details/triggerView/TriggerView.utils.tsx b/src/components/app/details/triggerView/TriggerView.utils.tsx index 5621dd9d06..32c9957153 100644 --- a/src/components/app/details/triggerView/TriggerView.utils.tsx +++ b/src/components/app/details/triggerView/TriggerView.utils.tsx @@ -16,13 +16,21 @@ import { useLocation } from 'react-router-dom' -import { DeploymentHistoryDetail, DeploymentWithConfigType } from '@devtron-labs/devtron-fe-common-lib' +import { + CommonNodeAttr, + DeploymentHistoryDetail, + DeploymentNodeType, + DeploymentWithConfigType, + handleAnalyticsEvent, + WorkflowType, +} from '@devtron-labs/devtron-fe-common-lib' +import { ENV_TRIGGER_VIEW_GA_EVENTS } from '@Components/ApplicationGroup/Constants' import { URLS } from '@Config/routes' import { deepEqual } from '../../../common' -import { TRIGGER_VIEW_PARAMS } from './Constants' -import { TriggerViewDeploymentConfigType } from './types' +import { TRIGGER_VIEW_GA_EVENTS, TRIGGER_VIEW_PARAMS } from './Constants' +import { CDNodeActions, GetCDNodeSearchParams, TriggerViewDeploymentConfigType } from './types' export const DEPLOYMENT_CONFIGURATION_NAV_MAP = { DEPLOYMENT_TEMPLATE: { @@ -190,3 +198,70 @@ export const shouldRenderWebhookAddImageModal = (location: ReturnType { + const searchParams = new URLSearchParams(search) + const cdNodeId = + searchParams.get(TRIGGER_VIEW_PARAMS.CD_NODE) || + searchParams.get(TRIGGER_VIEW_PARAMS.ROLLBACK_NODE) || + searchParams.get(TRIGGER_VIEW_PARAMS.APPROVAL_NODE) + + const nodeType = searchParams.get(TRIGGER_VIEW_PARAMS.NODE_TYPE) ?? DeploymentNodeType.CD + + return { cdNodeId, nodeType } +} + +export const getSelectedNodeFromWorkflows = ( + workflows: WorkflowType[], + search: string, +): { cdNodeId: string; node: CommonNodeAttr } => { + const { cdNodeId, nodeType } = getNodeIdAndTypeFromSearch(search) + + if (cdNodeId) { + // Use flatMap to flatten all nodes, then find the matching node + const allNodes = workflows.flatMap((workflow) => workflow.nodes) + const foundNode = allNodes.find((n) => cdNodeId === n.id && n.type === nodeType) + + if (foundNode) { + return { cdNodeId, node: foundNode } + } + } + + return { cdNodeId: cdNodeId ?? '0', node: {} as CommonNodeAttr } +} + +export const getCDNodeActionSearch = ({ + actionType, + cdNodeId, + nodeType = DeploymentNodeType.CD, + fromAppGroup, +}: GetCDNodeSearchParams) => { + switch (actionType) { + case CDNodeActions.APPROVAL: + handleAnalyticsEvent( + fromAppGroup + ? ENV_TRIGGER_VIEW_GA_EVENTS.ApprovalNodeClicked + : TRIGGER_VIEW_GA_EVENTS.ApprovalNodeClicked, + ) + return new URLSearchParams([ + [TRIGGER_VIEW_PARAMS.APPROVAL_NODE, cdNodeId.toString()], + [TRIGGER_VIEW_PARAMS.APPROVAL_STATE, TRIGGER_VIEW_PARAMS.APPROVAL], + ]).toString() + + case CDNodeActions.ROLLBACK_MATERIAL: + handleAnalyticsEvent( + fromAppGroup ? ENV_TRIGGER_VIEW_GA_EVENTS.RollbackClicked : TRIGGER_VIEW_GA_EVENTS.RollbackClicked, + ) + return new URLSearchParams([[TRIGGER_VIEW_PARAMS.ROLLBACK_NODE, cdNodeId.toString()]]).toString() + + case CDNodeActions.CD_MATERIAL: + default: + handleAnalyticsEvent( + fromAppGroup ? ENV_TRIGGER_VIEW_GA_EVENTS.MaterialClicked : TRIGGER_VIEW_GA_EVENTS.ImageClicked, + ) + return new URLSearchParams([ + [TRIGGER_VIEW_PARAMS.CD_NODE, cdNodeId.toString()], + [TRIGGER_VIEW_PARAMS.NODE_TYPE, nodeType], + ]).toString() + } +} diff --git a/src/components/app/details/triggerView/assets/cdMaterialFooter.png b/src/components/app/details/triggerView/assets/cdMaterialFooter.png deleted file mode 100644 index efeba342c3..0000000000 Binary files a/src/components/app/details/triggerView/assets/cdMaterialFooter.png and /dev/null differ diff --git a/src/components/app/details/triggerView/assets/configDiff.png b/src/components/app/details/triggerView/assets/configDiff.png deleted file mode 100644 index e625a19461..0000000000 Binary files a/src/components/app/details/triggerView/assets/configDiff.png and /dev/null differ diff --git a/src/components/app/details/triggerView/assets/configHeader.png b/src/components/app/details/triggerView/assets/configHeader.png deleted file mode 100644 index 44a4abdc84..0000000000 Binary files a/src/components/app/details/triggerView/assets/configHeader.png and /dev/null differ diff --git a/src/components/app/details/triggerView/cdMaterial.readme.md b/src/components/app/details/triggerView/cdMaterial.readme.md deleted file mode 100644 index 3b164bb4c7..0000000000 --- a/src/components/app/details/triggerView/cdMaterial.readme.md +++ /dev/null @@ -1,51 +0,0 @@ -# CDMaterials Documentation - -- Step - 1 : Computes Render -- Step - 2: Computes **isApprovalConfigured** -- Step - 3: If materials.length>0 then **Flow - 1** else **Flow -2** - -## Flow-1 - -Renders two different view: - -1. If coming from bulkCD render **triggerBody** -2. Otherwise render **CDModal** - -### renderTriggerBody: - -This function is the essence of this whole component, it contains three logical parts: - -- TriggerViewConfigDiff -- renderMaterialList -- Fetch more images in rollback. - -Coming to each components: - -- **TriggerViewConfigDiff**: This component is responsible for rendering the following view - ![Screenshot of Config View](./assets/configDiff.png) - -- **renderMaterialList:** This is a function returning JSX.Element containing list of materials, This function internally calls another function called **getConsumedAndAvailableMaterialList** which would contain the list of materials after filtering all the materials according to constraints. After that we compute the text to be shown on header and calls the renderMaterial function to render the material items with their detailing like Excluded state, add action button like expire approval, etc. -- **Load more:** This section aims to fetch more images from the server. This will be beneficial in case user wants more than N latest images, we will trigger load more function from which we will be fetching images from server, minding the fact that user can’t get images more than the configured set of number set in config map of devtron image. For that we will also be putting a check on the current length of materials and the limit we get from the API. - -### renderCDModal: - -1. Renders the header, if showing config view, it will render : - ![Screenshot of Config Header](./assets/configHeader.png) - - Otherwise it will render the basic header using renderCDModalHeader - -2. It renders trigger body using renderTriggerBody. -3. It renders renderTriggerModalCTA, This function is going to render the footer - - ![Screenshot of cdMaterialFooter](./assets/cdMaterialFooter.png) - -## Flow-2 - -Showing Empty states for two cases: BulkCD, CD - -Inside the empty states as well there are other cases: - -- searchImageTag: Based on this we show that on given search input we have no material. -- Based on length of eligible images passing filters we will be showing the empty state. -- Based on approvals there is going to be a state. -- Default empty state. diff --git a/src/components/app/details/triggerView/cdMaterial.tsx b/src/components/app/details/triggerView/cdMaterial.tsx deleted file mode 100644 index 02026a54b8..0000000000 --- a/src/components/app/details/triggerView/cdMaterial.tsx +++ /dev/null @@ -1,2054 +0,0 @@ -/* - * Copyright (c) 2024. Devtron Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React, { useContext, useEffect, useMemo, useRef, useState } from 'react' -import ReactGA from 'react-ga4' -import { Prompt, useHistory, useLocation } from 'react-router-dom' -import Tippy from '@tippyjs/react' - -import { - abortPreviousRequests, - ACTION_STATE, - AnimatedDeployButton, - API_STATUS_CODES, - ApprovalRuntimeStateType, - ArtifactInfo, - ArtifactInfoProps, - Button, - ButtonStyleType, - ButtonVariantType, - CD_MATERIAL_SIDEBAR_TABS, - CDMaterialResponseType, - CDMaterialServiceEnum, - CDMaterialSidebarType, - CDMaterialType, - CommonNodeAttr, - ComponentSizeType, - ConditionalWrap, - DEFAULT_ROUTE_PROMPT_MESSAGE, - DEPLOYMENT_CONFIG_DIFF_SORT_KEY, - DEPLOYMENT_WINDOW_TYPE, - DeploymentAppTypes, - DeploymentNodeType, - DeploymentStrategyType, - DeploymentWithConfigType, - EnvResourceType, - ErrorScreenManager, - EXCLUDED_IMAGE_TOOLTIP, - ExcludedImageNode, - FilterConditionsListType, - FilterStates, - genericCDMaterialsService, - GenericEmptyState, - getGitCommitInfo, - getIsApprovalPolicyConfigured, - getIsMaterialInfoAvailable, - getIsRequestAborted, - GetPolicyConsequencesProps, - GitCommitInfoGeneric, - handleUTCTime, - Icon, - ImageCard, - ImageCardAccordion, - ImageTaggingContainerType, - InfoBlock, - isNullOrUndefined, - MaterialInfo, - MODAL_TYPE, - ModuleNameMap, - ModuleStatus, - noop, - PipelineDeploymentStrategy, - PipelineStageBlockInfo, - PolicyConsequencesDTO, - Progressing, - RuntimePluginVariables, - SearchBar, - SegmentedControlProps, - SequentialCDCardTitleProps, - ServerErrors, - showError, - SortingOrder, - STAGE_TYPE, - stopPropagation, - ToastManager, - ToastVariantType, - triggerCDNode, - uploadCDPipelineFile, - useAsync, - useDownload, - useGetUserRoles, - usePrompt, - UserApprovalMetadataType, - useSearchString, -} from '@devtron-labs/devtron-fe-common-lib' - -import { ReactComponent as BackIcon } from '@Icons/ic-arrow-backward.svg' -import { ReactComponent as RefreshIcon } from '@Icons/ic-arrows_clockwise.svg' -import close from '@Icons/ic-close.svg' -import { ReactComponent as SearchIcon } from '@Icons/ic-search.svg' -import noArtifact from '../../../../assets/img/no-artifact.webp' -import { URLS } from '../../../../config' -import { EMPTY_STATE_STATUS, TOAST_BUTTON_TEXT_VIEW_DETAILS } from '../../../../config/constantMessaging' -import { importComponentFromFELibrary, useAppContext } from '../../../common' -import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service' -import { PipelineConfigDiffStatusTile } from './PipelineConfigDiff/PipelineConfigDiffStatusTile' -import { usePipelineDeploymentConfig } from './PipelineConfigDiff/usePipelineDeploymentConfig' -import { - getCanDeployWithoutApproval, - getCanImageApproverDeploy, - getInitialState, - getIsMaterialApproved, - getWfrId, -} from './cdMaterials.utils' -import { CDButtonLabelMap, TriggerViewContext } from './config' -import { CD_MATERIAL_GA_EVENT, TRIGGER_VIEW_GA_EVENTS, TRIGGER_VIEW_PARAMS } from './Constants' -import { PipelineConfigDiff } from './PipelineConfigDiff' -import { - LAST_SAVED_CONFIG_OPTION, - LATEST_TRIGGER_CONFIG_OPTION, - SPECIFIC_TRIGGER_CONFIG_OPTION, -} from './TriggerView.utils' -import { - BulkSelectionEvents, - CDMaterialProps, - CDMaterialState, - FilterConditionViews, - MATERIAL_TYPE, - RenderCTAType, - RuntimeParamsErrorState, - TriggerViewContextType, -} from './types' -import { URL_PARAM_MODE_TYPE } from '@Components/common/helpers/types' - -const ApprovalInfoTippy = importComponentFromFELibrary('ApprovalInfoTippy') -const ExpireApproval = importComponentFromFELibrary('ExpireApproval') -const ApprovedImagesMessage = importComponentFromFELibrary('ApprovedImagesMessage') -const ApprovalEmptyState = importComponentFromFELibrary('ApprovalEmptyState') -const FilterActionBar = importComponentFromFELibrary('FilterActionBar') -const ConfiguredFilters = importComponentFromFELibrary('ConfiguredFilters') -const CDMaterialInfo = importComponentFromFELibrary('CDMaterialInfo') -const downloadManifestForVirtualEnvironment = importComponentFromFELibrary( - 'downloadManifestForVirtualEnvironment', - null, - 'function', -) -const ImagePromotionInfoChip = importComponentFromFELibrary('ImagePromotionInfoChip', null, 'function') -const getDeploymentWindowProfileMetaData = importComponentFromFELibrary( - 'getDeploymentWindowProfileMetaData', - null, - 'function', -) -const MaintenanceWindowInfoBar = importComponentFromFELibrary('MaintenanceWindowInfoBar') -const DeploymentWindowConfirmationDialog = importComponentFromFELibrary('DeploymentWindowConfirmationDialog') -const RuntimeParamTabs = importComponentFromFELibrary('RuntimeParamTabs', null, 'function') -const RuntimeParameters = importComponentFromFELibrary('RuntimeParameters', null, 'function') -const SecurityModalSidebar = importComponentFromFELibrary('SecurityModalSidebar', null, 'function') -const AllowedWithWarningTippy = importComponentFromFELibrary('AllowedWithWarningTippy') -const MissingPluginBlockState = importComponentFromFELibrary('MissingPluginBlockState', null, 'function') -const TriggerBlockEmptyState = importComponentFromFELibrary('TriggerBlockEmptyState', null, 'function') -const getPolicyConsequences: ({ appId, envId }: GetPolicyConsequencesProps) => Promise = - importComponentFromFELibrary('getPolicyConsequences', null, 'function') -const getRuntimeParamsPayload = importComponentFromFELibrary('getRuntimeParamsPayload', null, 'function') -const validateRuntimeParameters = importComponentFromFELibrary( - 'validateRuntimeParameters', - () => ({ isValid: true, cellError: {} }), - 'function', -) -const SelectDeploymentStrategy = importComponentFromFELibrary('SelectDeploymentStrategy', null, 'function') -const getDeploymentStrategies: (pipelineIds: number[]) => Promise = - importComponentFromFELibrary('getDeploymentStrategies', null, 'function') - -const CDMaterial = ({ - materialType, - appId, - envId, - pipelineId, - stageType, - isFromBulkCD, - envName, - closeCDModal, - triggerType, - isVirtualEnvironment, - parentEnvironmentName, - isLoading, - // Handle the case of external pipeline, it might be undefined or zero in that case - ciPipelineId, - updateCurrentAppMaterial, - hideInfoTabsContainer, - isSaveLoading, - // WARNING: Be mindful that we need to send materials instead of material since its expecting response - updateBulkCDMaterialsItem, - // Have'nt sent this from Bulk since not required - deploymentAppType, - selectedImageFromBulk, - isRedirectedFromAppDetails, - selectedAppName, - bulkRuntimeParams, - handleBulkRuntimeParamChange, - bulkRuntimeParamErrorState, - handleBulkRuntimeParamError, - bulkSidebarTab, - showPluginWarningBeforeTrigger: _showPluginWarningBeforeTrigger = false, - consequence, - configurePluginURL, - isTriggerBlockedDueToPlugin, - bulkUploadFile, - handleSuccess, -}: Readonly) => { - // stageType should handle approval node, compute CDMaterialServiceEnum, create queryParams state - // FIXME: the query params returned by useSearchString seems faulty - const history = useHistory() - const { pathname } = useLocation() - const { searchParams } = useSearchString() - const { handleDownload } = useDownload() - // Add dep here - const { isSuperAdmin } = useGetUserRoles() - // NOTE: Won't be available in app group will use data from props for that - // DO Not consume directly, use appName variable instead - const { currentAppName } = useAppContext() - - const appName = selectedAppName || currentAppName - - const searchImageTag = searchParams.search - - const [material, setMaterial] = useState([]) - const [state, setState] = useState(getInitialState(materialType, material, searchImageTag)) - const [deploymentStrategy, setDeploymentStrategy] = useState(null) - - // It is derived from materialResult and can be fixed as a constant fix this - const [isConsumedImageAvailable, setIsConsumedImageAvailable] = useState(false) - const [showPluginWarningOverlay, setShowPluginWarningOverlay] = useState(false) - // Should be able to abort request using useAsync - const abortControllerRef = useRef(new AbortController()) - const abortDeployRef = useRef(null) - - const isPreOrPostCD = stageType === DeploymentNodeType.PRECD || stageType === DeploymentNodeType.POSTCD - const isCDNode = stageType === DeploymentNodeType.CD - const showPluginWarningBeforeTrigger = _showPluginWarningBeforeTrigger && isPreOrPostCD - // This check assumes we have isPreOrPostCD as true - const allowWarningWithTippyNodeTypeProp: CommonNodeAttr['type'] = - stageType === DeploymentNodeType.PRECD ? 'PRECD' : 'POSTCD' - - // this function can be used for other trigger block reasons once api supports it - const getIsTriggerBlocked = (cdPolicyConsequences: PipelineStageBlockInfo) => { - switch (stageType) { - case DeploymentNodeType.PRECD: - return cdPolicyConsequences.pre.isBlocked - case DeploymentNodeType.POSTCD: - return cdPolicyConsequences.post.isBlocked - case DeploymentNodeType.CD: - return cdPolicyConsequences.node.isBlocked - default: - return false - } - } - - const getMaterialResponseList = async (): Promise<[CDMaterialResponseType, any, PolicyConsequencesDTO]> => { - const response = await Promise.all([ - genericCDMaterialsService( - materialType === MATERIAL_TYPE.rollbackMaterialList - ? CDMaterialServiceEnum.ROLLBACK - : CDMaterialServiceEnum.CD_MATERIALS, - pipelineId, - // Don't think need to set stageType to approval in case of approval node - stageType ?? DeploymentNodeType.CD, - abortControllerRef.current.signal, - // It is meant to fetch the first 20 materials - { - offset: 0, - size: 20, - search: searchImageTag, - // Since by default we are setting filterView to eligible and in case of no filters everything is eligible - // So there should'nt be any additional api call - // NOTE: Uncomment this when backend supports the filtering, there will be some minor handling like number of images in segmented control - // filter: - // state.filterView === FilterConditionViews.ELIGIBLE && !state.searchApplied - // ? CDMaterialFilterQuery.RESOURCE - // : null, - }, - ), - getDeploymentWindowProfileMetaData && !isFromBulkCD - ? getDeploymentWindowProfileMetaData(appId, envId) - : null, - getPolicyConsequences ? getPolicyConsequences({ appId, envId }) : null, - ]) - - if (getPolicyConsequences && getIsTriggerBlocked(response[2].cd)) { - return [null, null, response[2]] - } - return response - } - - // TODO: Ask if pipelineId always changes on change of app else add appId as dependency - const [loadingMaterials, responseList, materialsError, reloadMaterials] = useAsync( - () => abortPreviousRequests(() => getMaterialResponseList(), abortControllerRef), - // NOTE: Add state.filterView if want to add filtering support from backend - [pipelineId, stageType, materialType, searchImageTag], - !!pipelineId && !isTriggerBlockedDueToPlugin, - ) - - const [pipelineStrategiesLoading, pipelineStrategies, pipelineStrategiesError, reloadStrategies] = useAsync( - () => getDeploymentStrategies([pipelineId]), - [pipelineId], - !!getDeploymentStrategies && !!pipelineId, - ) - - const materialsResult: CDMaterialResponseType = responseList?.[0] - const deploymentWindowMetadata = responseList?.[1] ?? {} - - const { onClickCDMaterial } = useContext(TriggerViewContext) - const [noMoreImages, setNoMoreImages] = useState(false) - const [tagsEditable, setTagsEditable] = useState(false) - const [appReleaseTagNames, setAppReleaseTagNames] = useState([]) - const [showAppliedFilters, setShowAppliedFilters] = useState(false) - const [deploymentLoading, setDeploymentLoading] = useState(false) - 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({ - isValid: true, - cellError: {}, - }) - const [value, setValue] = useState() - const [showDeploymentWindowConfirmation, setShowDeploymentWindowConfirmation] = useState(false) - - const resourceFilters = materialsResult?.resourceFilters ?? [] - const hideImageTaggingHardDelete = materialsResult?.hideImageTaggingHardDelete ?? false - const requestedUserId = materialsResult?.requestedUserId ?? '' - const isApprovalConfigured = getIsApprovalPolicyConfigured( - materialsResult?.deploymentApprovalInfo?.approvalConfigData, - ) - const canApproverDeploy = materialsResult?.canApproverDeploy ?? false - const showConfigDiffView = searchParams.mode === URL_PARAM_MODE_TYPE.REVIEW_CONFIG && searchParams.deploy - const isExceptionUser = materialsResult?.deploymentApprovalInfo?.approvalConfigData?.isExceptionUser ?? false - - const pipelineStrategyOptions = useMemo( - () => - (pipelineStrategies ?? []).flatMap(({ error, strategies }) => { - if (error) { - return [] - } - return strategies - }), - [pipelineStrategies], - ) - - const { - pipelineDeploymentConfigLoading, - pipelineDeploymentConfig, - radioSelectConfig, - diffFound, - noLastDeploymentConfig, - noSpecificDeploymentConfig, - canDeployWithConfig, - canReviewConfig, - scopeVariablesConfig, - urlFilters, - lastDeploymentWfrId, - errorConfig, - } = usePipelineDeploymentConfig({ - appId, - envId, - appName, - envName, - deploymentStrategy, - setDeploymentStrategy, - pipelineStrategyOptions, - isRollbackTriggerSelected: state.isRollbackTrigger, - pipelineId, - wfrId: getWfrId(state.selectedMaterial, material), - }) - - usePrompt({ shouldPrompt: deploymentLoading }) - - /* ------------ Utils required in useEffect ------------*/ - const getSecurityModuleStatus = async () => { - try { - const { result } = await getModuleInfo(ModuleNameMap.SECURITY) - if (result?.status === ModuleStatus.INSTALLED) { - setState((prevState) => ({ ...prevState, isSecurityModuleInstalled: true })) - } - } catch (error) { - setState((prevState) => ({ ...prevState, isSecurityModuleInstalled: false })) - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Issue while fetching security module status', - }) - } - } - - // Ask whether this id is true or not - const getCDArtifactId = () => - state.selectedMaterial ? state.selectedMaterial.id : material?.find((_mat) => _mat.isSelected)?.id - - const setSearchValue = (searchValue: string) => { - const newParams: any = { - ...searchParams, - search: searchValue, - } - - if (!searchValue) { - delete newParams.search - } - - history.push({ - search: new URLSearchParams(newParams).toString(), - }) - } - - /* ------------ UseEffects ------------*/ - useEffect(() => { - abortDeployRef.current = new AbortController() - return () => { - abortDeployRef.current.abort() - if (history.location.pathname.includes(URLS.APP_DIFF_VIEW)) { - history.replace(history.location.pathname.split(URLS.APP_DIFF_VIEW)[0]) - } - } - }, []) - - useEffect(() => { - if (materialsError) { - showError(materialsError) - return - } - - if (!loadingMaterials && materialsResult) { - if (selectedImageFromBulk) { - const selectedImageIndex = materialsResult.materials.findIndex( - (materialItem) => materialItem.image === selectedImageFromBulk, - ) - if (selectedImageIndex === -1 && selectedImageFromBulk !== BulkSelectionEvents.SELECT_NONE) { - setSearchValue(selectedImageFromBulk) - } else { - const _newMaterials = [...materialsResult.materials] - if (selectedImageIndex !== -1) { - _newMaterials[selectedImageIndex].isSelected = true - _newMaterials.forEach((mat, index) => { - if (index !== selectedImageIndex) { - mat.isSelected = false - } - }) - } else { - _newMaterials.forEach((mat) => { - mat.isSelected = false - }) - } - - setTagsEditable(materialsResult.tagsEditable) - setAppReleaseTagNames(materialsResult.appReleaseTagNames) - setNoMoreImages(materialsResult.materials.length >= materialsResult.totalCount) - setRuntimeParamsList(materialsResult.runtimeParams) - - setMaterial(_newMaterials) - const _isConsumedImageAvailable = - _newMaterials.some((materialItem) => materialItem.deployed && materialItem.latest) ?? false - - setIsConsumedImageAvailable(_isConsumedImageAvailable) - - getSecurityModuleStatus() - - const _newBulkResponse = { - ...materialsResult, - materials: _newMaterials, - } - updateBulkCDMaterialsItem?.(_newBulkResponse) - } - } else { - setTagsEditable(materialsResult.tagsEditable) - setAppReleaseTagNames(materialsResult.appReleaseTagNames) - setNoMoreImages(materialsResult.materials.length >= materialsResult.totalCount) - setRuntimeParamsList(materialsResult.runtimeParams) - - setMaterial(materialsResult.materials) - const _isConsumedImageAvailable = - materialsResult.materials?.some((materialItem) => materialItem.deployed && materialItem.latest) ?? - false - - setIsConsumedImageAvailable(_isConsumedImageAvailable) - - getSecurityModuleStatus() - - updateBulkCDMaterialsItem?.(materialsResult) - } - } - }, [materialsResult, loadingMaterials]) - - useEffect(() => { - // selectedImage is going to be updated since on selection of image we are updating the state - if (selectedImageFromBulk && material?.length) { - const selectedImageIndex = material.findIndex( - (materialItem) => materialItem.image === selectedImageFromBulk, - ) - - if (selectedImageFromBulk === BulkSelectionEvents.SELECT_NONE) { - const _newMaterials = [...material] - _newMaterials.forEach((mat) => { - mat.isSelected = false - }) - setMaterial([..._newMaterials]) - updateBulkCDMaterialsItem?.({ - ...materialsResult, - materials: _newMaterials, - }) - } else if (selectedImageIndex === -1) { - setSearchValue(selectedImageFromBulk) - } else if (!material[selectedImageIndex].isSelected) { - const _newMaterials = [...material] - _newMaterials[selectedImageIndex].isSelected = true - _newMaterials.forEach((mat, index) => { - if (index !== selectedImageIndex) { - mat.isSelected = false - } - }) - setMaterial([..._newMaterials]) - updateBulkCDMaterialsItem?.({ - ...materialsResult, - materials: _newMaterials, - }) - } - } - }, [material, selectedImageFromBulk]) - - useEffect(() => { - if (searchImageTag) { - setState((prevState) => ({ - ...prevState, - searchApplied: true, - showSearch: true, - searchText: searchImageTag, - })) - } else { - setState((prevState) => ({ - ...prevState, - searchApplied: false, - showSearch: false, - searchText: '', - })) - } - }, [searchImageTag]) - - useEffect(() => { - setState((prevState) => ({ - ...prevState, - selectedMaterial: material.find((_mat) => _mat.isSelected), - areMaterialsPassingFilters: - material.filter((materialDetails) => materialDetails.filterState === FilterStates.ALLOWED).length > 0, - })) - // The above states are derived from material so no need to make a state for them and shift the config diff here - }, [material]) - - const getInitialSelectedConfigToDeploy = () => { - if ( - (materialType === MATERIAL_TYPE.rollbackMaterialList && !searchParams.deploy) || - searchParams.deploy === DeploymentWithConfigType.SPECIFIC_TRIGGER_CONFIG - ) { - return SPECIFIC_TRIGGER_CONFIG_OPTION - } - if (searchParams.deploy === DeploymentWithConfigType.LATEST_TRIGGER_CONFIG) { - return LATEST_TRIGGER_CONFIG_OPTION - } - return LAST_SAVED_CONFIG_OPTION - } - useEffect(() => { - setState((prevState) => ({ - ...prevState, - isRollbackTrigger: materialType === MATERIAL_TYPE.rollbackMaterialList, - isSelectImageTrigger: materialType === MATERIAL_TYPE.inputMaterialList, - selectedConfigToDeploy: getInitialSelectedConfigToDeploy(), - })) - }, [materialType]) - - useEffect(() => { - if (searchParams.deploy) { - setState((prevState) => ({ - ...prevState, - selectedConfigToDeploy: getInitialSelectedConfigToDeploy(), - })) - } - }, [searchParams.deploy]) - - useEffect(() => { - setState((prevState) => ({ - ...prevState, - filterView: FilterConditionViews.ELIGIBLE, - showConfiguredFilters: false, - })) - setShowAppliedFilters(false) - }, [appId]) - - /* ------------ Helping utilities ------------*/ - const handleImageSelection = (index: number, selectedMaterial: CDMaterialType) => { - const _updatedMaterial = [...material] - _updatedMaterial[index].isSelected = true - - _updatedMaterial.forEach((mat, _index) => { - if (_index !== index) { - mat.isSelected = false - } - }) - - setMaterial(_updatedMaterial) - if ( - (materialType === 'none' || state.isSelectImageTrigger) && - state.selectedMaterial?.image !== selectedMaterial.image - ) { - setState((prevState) => ({ - ...prevState, - selectedMaterial, - })) - } - - // We have to update the parent state in case of bulkCD - updateBulkCDMaterialsItem?.({ - ...materialsResult, - materials: _updatedMaterial, - }) - } - - const handleDisableFiltersView = (e: React.MouseEvent) => { - e.stopPropagation() - setState((prevState) => ({ - ...prevState, - showConfiguredFilters: false, - })) - } - - const handleRuntimeParamChange: typeof handleBulkRuntimeParamChange = (updatedRuntimeParams) => { - setRuntimeParamsList(updatedRuntimeParams) - } - - 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) { - setSearchValue('') - } - } - - const viewAllImages = (e: React.MouseEvent) => { - e.stopPropagation() - if (isRedirectedFromAppDetails) { - history.push({ - search: `${TRIGGER_VIEW_PARAMS.APPROVAL_NODE}=${pipelineId}&${TRIGGER_VIEW_PARAMS.APPROVAL_STATE}=${TRIGGER_VIEW_PARAMS.APPROVAL}`, - }) - } else { - closeCDModal(e) - onClickCDMaterial(pipelineId, DeploymentNodeType.CD, true) - } - } - - const getIsApprovalRequester = (userApprovalMetadata?: UserApprovalMetadataType) => - userApprovalMetadata?.requestedUserData && userApprovalMetadata.requestedUserData.userId === requestedUserId - - const getIsImageApprover = (userApprovalMetadata?: UserApprovalMetadataType): boolean => - userApprovalMetadata?.hasCurrentUserApproved - - // NOTE: Pure - const getApprovedImageClass = (disableSelection: boolean, isApprovalConfigured: boolean) => { - const disabledClassPostfix = disableSelection ? '-disabled' : '' - return isApprovalConfigured ? `material-history__approved-image${disabledClassPostfix}` : '' - } - - const toggleCardMode = (index) => { - setState((prevState) => { - const _isEditModeList = new Map(prevState.materialInEditModeMap) - _isEditModeList.set(index, !_isEditModeList.get(index)) - return { - ...prevState, - materialInEditModeMap: _isEditModeList, - } - }) - } - - const processConsumedAndApprovedImages = () => { - const consumedImage = [] - const approvedImages = [] - material.forEach((mat) => { - if ( - !mat.userApprovalMetadata || - mat.userApprovalMetadata.approvalRuntimeState !== ApprovalRuntimeStateType.approved - ) { - mat.isSelected = false - consumedImage.push(mat) - } else { - approvedImages.push(mat) - } - }) - return { consumedImage, approvedImages } - } - - const getConsumedAndAvailableMaterialList = (isApprovalConfigured: boolean) => { - if (isExceptionUser) { - return { - consumedImage: [], - materialList: material, - eligibleImagesCount: material.filter((mat) => mat.filterState === FilterStates.ALLOWED).length, - } - } - - let _consumedImage = [] - let materialList: CDMaterialType[] = [] - - if (isApprovalConfigured) { - const { consumedImage, approvedImages } = processConsumedAndApprovedImages() - _consumedImage = consumedImage - materialList = approvedImages - } else { - materialList = material - } - - const eligibleImagesCount = materialList.filter((mat) => mat.filterState === FilterStates.ALLOWED).length - - if (!state.searchApplied && resourceFilters?.length && state.filterView === FilterConditionViews.ELIGIBLE) { - materialList = materialList.filter((mat) => mat.filterState === FilterStates.ALLOWED) - } - - return { - consumedImage: _consumedImage, - materialList, - eligibleImagesCount, - } - } - - const handleRefresh = (e) => { - stopPropagation(e) - - if (state.searchApplied) { - reloadMaterials() - } else { - reloadMaterials() - - setState((prevState) => ({ - ...prevState, - showSearch: false, - })) - } - } - - const handleSearchClick = (e) => { - stopPropagation(e) - setState((prevState) => ({ - ...prevState, - showSearch: true, - })) - } - - const handleFilterKeyPress = (_searchText: string): void => { - setState({ - ...state, - searchText: _searchText, - }) - if (_searchText !== searchImageTag) { - setSearchValue(_searchText) - } - } - - const handleEnableFiltersView = (e: React.MouseEvent) => { - e.stopPropagation() - setState((prevState) => ({ - ...prevState, - showConfiguredFilters: true, - })) - } - - const handleSidebarTabChange = (e: React.ChangeEvent) => { - setCurrentSidebarTab(e.target.value as CDMaterialSidebarType) - } - - const handleFilterTabsChange: SegmentedControlProps['onChange'] = (selectedSegment) => { - const { value } = selectedSegment - setState((prevState) => ({ - ...prevState, - filterView: value as FilterConditionViews, - })) - } - - const handleAllImagesView = (e: React.MouseEvent) => { - e.stopPropagation() - setState((prevState) => ({ - ...prevState, - filterView: FilterConditionViews.ALL, - })) - } - - const getFilterActionBarTabs = (filteredImagesCount: number, consumedImageCount: number) => [ - { - label: `Eligible images ${filteredImagesCount}/${material.length - consumedImageCount}`, - value: FilterConditionViews.ELIGIBLE, - }, - { - label: `Latest ${material.length - consumedImageCount} images`, - value: FilterConditionViews.ALL, - }, - ] - - const getConfigToDeployValue = () => { - if (searchParams.deploy) { - return searchParams.deploy - } - if (materialType === MATERIAL_TYPE.rollbackMaterialList) { - return DeploymentWithConfigType.SPECIFIC_TRIGGER_CONFIG - } - return DeploymentWithConfigType.LAST_SAVED_CONFIG - } - - const onClickSetInitialParams = (modeParamValue: URL_PARAM_MODE_TYPE) => { - const newParams = new URLSearchParams({ - ...searchParams, - sortBy: DEPLOYMENT_CONFIG_DIFF_SORT_KEY, - sortOrder: SortingOrder.ASC, - mode: modeParamValue, - deploy: getConfigToDeployValue(), - }) - - if (modeParamValue === URL_PARAM_MODE_TYPE.LIST) { - newParams.delete('sortOrder') - newParams.delete('sortBy') - } - - history.push({ - pathname: - modeParamValue === URL_PARAM_MODE_TYPE.REVIEW_CONFIG - ? // Replace consecutive trailing single slashes - `${pathname.replace(/\/+$/g, '')}/${URLS.APP_DIFF_VIEW}/${EnvResourceType.DeploymentTemplate}` - : `${pathname.split(`/${URLS.APP_DIFF_VIEW}`)[0]}`, - search: newParams.toString(), - }) - } - - const isDeployButtonDisabled = () => { - const selectedImage = material.find((artifact) => artifact.isSelected) - - return ( - !selectedImage || - !state.areMaterialsPassingFilters || - (state.isRollbackTrigger && (pipelineDeploymentConfigLoading || !canDeployWithConfig())) || - (state.selectedConfigToDeploy.value === DeploymentWithConfigType.LATEST_TRIGGER_CONFIG && - noLastDeploymentConfig) - ) - } - - // NOTE: In the three functions below we already have data from props so can be handled in a better way - const redirectToDeploymentStepsPage = (cdPipelineId: number, environmentId: number) => { - history.push(`/app/${appId}/cd-details/${environmentId}/${cdPipelineId}`) - } - - const handleTriggerErrorMessageForHelmManifestPush = ( - serverError: any, - cdPipelineId: number, - environmentId: number, - ) => { - if ( - serverError instanceof ServerErrors && - Array.isArray(serverError.errors) && - serverError.code !== 403 && - serverError.code !== 408 && - !getIsRequestAborted(searchParams) - ) { - serverError.errors.map(({ userMessage, internalMessage }) => { - ToastManager.showToast( - { - variant: ToastVariantType.error, - description: userMessage ?? internalMessage, - buttonProps: { - text: TOAST_BUTTON_TEXT_VIEW_DETAILS, - dataTestId: 'cd-material-view-details-btns', - onClick: () => redirectToDeploymentStepsPage(cdPipelineId, environmentId), - }, - }, - { - autoClose: false, - }, - ) - }) - } else { - showError(serverError) - } - } - - const showErrorIfNotAborted = (errors: ServerErrors) => { - if (!getIsRequestAborted(errors)) { - showError(errors) - } - } - - const handleDeployment = ( - nodeType: DeploymentNodeType, - _appId: number, - ciArtifactId: number, - e: React.MouseEvent, - deploymentWithConfig?: string, - wfrId?: number, - ) => { - const updatedRuntimeParamsErrorState = validateRuntimeParameters(parameters) - handleRuntimeParamsError(updatedRuntimeParamsErrorState) - if (!updatedRuntimeParamsErrorState.isValid) { - ToastManager.showToast({ - variant: ToastVariantType.error, - description: 'Please resolve all the errors before deploying', - }) - return - } - - ReactGA.event(TRIGGER_VIEW_GA_EVENTS.CDTriggered(nodeType)) - setDeploymentLoading(true) - - if (_appId && pipelineId && ciArtifactId) { - triggerCDNode({ - pipelineId: Number(pipelineId), - ciArtifactId: Number(ciArtifactId), - appId: Number(_appId), - stageType: nodeType, - deploymentWithConfig, - wfrId, - abortControllerRef: abortDeployRef, - isRollbackTrigger: state.isRollbackTrigger, - ...(getRuntimeParamsPayload - ? { runtimeParamsPayload: getRuntimeParamsPayload(runtimeParamsList ?? []) } - : {}), - skipIfHibernated: false, - ...(SelectDeploymentStrategy && deploymentStrategy ? { strategy: deploymentStrategy } : {}), - }) - .then((response: any) => { - if (response.result) { - if (isVirtualEnvironment && deploymentAppType == DeploymentAppTypes.MANIFEST_DOWNLOAD) { - const { helmPackageName } = response.result - downloadManifestForVirtualEnvironment?.({ - appId: _appId, - envId, - helmPackageName, - cdWorkflowType: nodeType, - handleDownload, - }) - } - - const msg = - materialType == MATERIAL_TYPE.rollbackMaterialList - ? 'Rollback Initiated' - : 'Deployment Initiated' - - ToastManager.showToast({ - variant: ToastVariantType.success, - description: msg, - }) - setDeploymentLoading(false) - handleSuccess?.() - closeCDModal(e) - } - }) - .catch((errors: ServerErrors) => { - // TODO: Ask why this was only there in TriggerView - isVirtualEnvironment && deploymentAppType == DeploymentAppTypes.MANIFEST_PUSH - ? handleTriggerErrorMessageForHelmManifestPush(errors, pipelineId, envId) - : showErrorIfNotAborted(errors) - setDeploymentLoading(false) - }) - } else { - let message = _appId ? '' : 'app id missing ' - message += pipelineId ? '' : 'pipeline id missing ' - message += ciArtifactId ? '' : 'Artifact id missing ' - ToastManager.showToast({ - variant: ToastVariantType.error, - description: message, - }) - setDeploymentLoading(false) - } - } - - const deployTrigger = (e: React.MouseEvent) => { - e.stopPropagation() - handleConfirmationClose(e) - // Blocking the deploy action if already deploying or config is not available - if (isLoading || isDeployButtonDisabled()) { - return - } - - if (state.isRollbackTrigger || state.isSelectImageTrigger) { - const wfrId = state.isRollbackTrigger ? getWfrId(state.selectedMaterial, material) : lastDeploymentWfrId - handleDeployment(stageType, appId, Number(getCDArtifactId()), e, state.selectedConfigToDeploy.value, wfrId) - return - } - - handleDeployment(stageType, appId, Number(getCDArtifactId()), e) - } - - const loadOlderImages = () => { - ReactGA.event(CD_MATERIAL_GA_EVENT.FetchMoreImagesClicked) - if (!state.loadingMore) { - setState((prevState) => ({ - ...prevState, - loadingMore: true, - })) - - abortControllerRef.current.abort() - abortControllerRef.current = new AbortController() - - genericCDMaterialsService( - materialType === MATERIAL_TYPE.rollbackMaterialList - ? CDMaterialServiceEnum.ROLLBACK - : CDMaterialServiceEnum.CD_MATERIALS, - pipelineId, - stageType, - abortControllerRef.current.signal, - { - offset: material.length - Number(isConsumedImageAvailable), - size: 20, - search: searchImageTag, - }, - ) - .then((materialsResponse) => { - if (materialsResponse) { - // NOTE: Looping through _newResponse and removing elements that are already deployed and latest - // NOTE: This is done to avoid duplicate images - const filteredNewMaterialResponse = [...materialsResponse.materials].filter( - (materialItem) => !(materialItem.deployed && materialItem.latest), - ) - - // updating the index of materials to maintain consistency - const _newMaterialsResponse = filteredNewMaterialResponse.map((materialItem, index) => ({ - ...materialItem, - index: material.length + index, - })) - - const _newMaterials = material.concat(_newMaterialsResponse) - setMaterial(_newMaterials) - - const _updatedMaterialResponse = { - ...materialsResponse, - materials: _newMaterials, - } - updateBulkCDMaterialsItem?.(_updatedMaterialResponse) - setNoMoreImages(_newMaterials.length >= materialsResponse.totalCount) - - const baseSuccessMessage = `Fetched ${_newMaterialsResponse.length} images.` - if (resourceFilters?.length && !state.searchApplied) { - const eligibleImages = _newMaterialsResponse.filter( - (mat) => mat.filterState === FilterStates.ALLOWED, - ).length - - const infoMessage = - eligibleImages === 0 - ? 'No new eligible images found.' - : `${eligibleImages} new eligible images found.` - - if (state.filterView === FilterConditionViews.ELIGIBLE) { - ToastManager.showToast({ - variant: ToastVariantType.info, - description: `${baseSuccessMessage} ${infoMessage}`, - }) - } else { - ToastManager.showToast({ - variant: ToastVariantType.success, - description: `${baseSuccessMessage} ${infoMessage}`, - }) - } - } else { - ToastManager.showToast({ - variant: ToastVariantType.success, - description: baseSuccessMessage, - }) - } - } - }) - .catch((error) => { - showError(error) - }) - .finally(() => { - setState((prevState) => ({ - ...prevState, - loadingMore: false, - })) - }) - } - } - - /* ------------ Render Utilities ------------*/ - const renderGenerateButton = () => ( - - ) - - const renderLoadMoreButton = () => ( - - is applied on - -

- ) - - const renderEmptyState = ( - isApprovalConfigured: boolean, - consumedImagePresent?: boolean, - noEligibleImages?: boolean, - ) => { - if (isTriggerBlockedDueToPlugin && MissingPluginBlockState) { - return ( - - ) - } - - if (TriggerBlockEmptyState && getIsTriggerBlocked(responseList?.[2]?.cd)) { - return - } - - if ( - resourceFilters?.length && - noEligibleImages && - !state.searchApplied && - material.length - Number(consumedImagePresent) > 0 - ) { - return ( - - ) - } - - if (searchImageTag) { - return ( - - ) - } - - if (ApprovalEmptyState && isApprovalConfigured && !isExceptionUser) { - return ( - - ) - } - - return ( - - ) - } - - const handleShowAppliedFilters = (e: React.MouseEvent, materialData: CDMaterialType) => { - e.stopPropagation() - setAppliedFilterList(materialData?.appliedFilters ?? []) - setShowAppliedFilters(true) - } - - const handleDisableAppliedFiltersView = (e: React.MouseEvent) => { - e.stopPropagation() - setAppliedFilterList([]) - setShowAppliedFilters(false) - } - - const reloadMaterialsPropagation = (e: React.MouseEvent) => { - e.stopPropagation() - reloadMaterials() - } - - const renderGitMaterialInfo = (materialData: CDMaterialType) => ( - <> - {materialData.materialInfo.map((mat: MaterialInfo, index) => { - const _gitCommit = getGitCommitInfo(mat) - - if ( - (materialData.appliedFilters?.length > 0 || - materialData.deploymentBlockedState?.isBlocked || - materialData.deploymentWindowArtifactMetadata?.type) && - CDMaterialInfo - ) { - return ( - handleShowAppliedFilters(e, materialData)} - filterState={materialData.appliedFiltersState} - dataSource={materialData.dataSource} - deploymentWindowArtifactMetadata={materialData.deploymentWindowArtifactMetadata} - isFilterApplied={materialData.appliedFilters?.length > 0} - triggerBlockedInfo={materialData.deploymentBlockedState} - > - {(_gitCommit.WebhookData?.Data || - _gitCommit.Author || - _gitCommit.Message || - _gitCommit.Date || - _gitCommit.Commit) && ( - - )} - - ) - } - - // FIXME: Key seems to be missing here, look into this issue later - return ( - (_gitCommit.WebhookData?.Data || - _gitCommit.Author || - _gitCommit.Message || - _gitCommit.Date || - _gitCommit.Commit) && ( -
- -
- ) - ) - })} - - ) - - const renderMaterialCTA = ( - mat: CDMaterialType, - isImageApprover: boolean = false, - disableSelection: boolean = false, - shouldRenderExpireApproval: boolean = false, - ) => { - if (mat.filterState !== FilterStates.ALLOWED) { - return ( - - Excluded - - ) - } - - if (mat.vulnerable) { - return ( - - Security Issues Found - - ) - } - if (disableSelection || (!isExceptionUser && !canApproverDeploy && isImageApprover)) { - return ( - - - SELECT - - - ) - } - if (mat.isSelected) { - return - } - const cursorClass = mat.isSelected ? 'cursor-default' : 'cursor' - const selectClassName = mat.vulnerable ? 'cursor-not-allowed' : cursorClass - - return ( - { - event.stopPropagation() - handleImageSelection(mat.index, mat) - }} - data-testid={`cd-artifact-select-${mat.index}`} - > - SELECT - - ) - } - - const renderCTA = ({ mat, disableSelection }: RenderCTAType) => { - const isApprovalRequester = getIsApprovalRequester(mat.userApprovalMetadata) - const isImageApprover = getIsImageApprover(mat.userApprovalMetadata) - const isMaterialApproved = getIsMaterialApproved(mat.userApprovalMetadata) - - const shouldRenderExpireApproval = - materialType !== MATERIAL_TYPE.none && - isApprovalRequester && - !isImageApprover && - !disableSelection && - isMaterialApproved && - mat.userApprovalMetadata?.canCurrentUserApprove - - return ( - <> - {shouldRenderExpireApproval && ExpireApproval && ( - <> - - - {mat.filterState !== FilterStates.ALLOWED && ( -
-
-
- )} - - )} - {renderMaterialCTA(mat, isImageApprover, disableSelection, shouldRenderExpireApproval)} - - ) - } - - // Not sending approvalChecksNode as it is not required in this case - const getArtifactInfoProps = (mat: CDMaterialType, showApprovalInfoTippy: boolean): ArtifactInfoProps => ({ - imagePath: mat.imagePath, - registryName: mat.registryName, - registryType: mat.registryType, - image: mat.image, - deployedTime: mat.deployedTime, - deployedBy: mat.deployedBy, - isRollbackTrigger: state.isRollbackTrigger, - excludedImagePathNode: - mat.filterState === FilterStates.ALLOWED ? null : , - approvalInfoTippy: showApprovalInfoTippy ? ( - - ) : null, - }) - - const getImageTagContainerProps = (mat: CDMaterialType): ImageTaggingContainerType => ({ - ciPipelineId, - artifactId: +mat.id, - imageComment: mat.imageComment, - imageReleaseTags: mat.imageReleaseTags, - appReleaseTagNames, - setAppReleaseTagNames, - tagsEditable, - toggleCardMode, - setTagsEditable, - forceReInit: true, - hideHardDelete: hideImageTaggingHardDelete, - updateCurrentAppMaterial, - isSuperAdmin, - }) - - const getSequentialCDCardTitleProps = (mat: CDMaterialType): SequentialCDCardTitleProps => { - const { promotionApprovalMetadata } = mat - const promotionApprovedBy = promotionApprovalMetadata?.approvedUsersData?.map((users) => users.userEmail) - - return { - isLatest: mat.latest, - isRunningOnParentCD: mat.runningOnParentCd, - artifactStatus: mat.artifactStatus, - environmentName: envName, - parentEnvironmentName, - stageType, - showLatestTag: +mat.index === 0 && materialType !== MATERIAL_TYPE.rollbackMaterialList && !searchImageTag, - isVirtualEnvironment, - targetPlatforms: mat.targetPlatforms, - additionalInfo: - ImagePromotionInfoChip && promotionApprovalMetadata?.promotedFromType ? ( - - ) : null, - } - } - - const renderMaterial = (materialList: CDMaterialType[], disableSelection: boolean, isApprovalConfigured: boolean) => - materialList.map((mat) => { - const isMaterialInfoAvailable = getIsMaterialInfoAvailable(mat.materialInfo) - const approvedImageClass = getApprovedImageClass(disableSelection, isApprovalConfigured) - const isImageApprover = getIsImageApprover(mat.userApprovalMetadata) - const hideSourceInfo = !state.materialInEditModeMap.get(+mat.id) - const showApprovalInfoTippy = - !disableSelection && - (isCDNode || state.isRollbackTrigger) && - isApprovalConfigured && - ApprovalInfoTippy && - !isNullOrUndefined(mat.userApprovalMetadata.approvalRuntimeState) - const imageCardRootClassName = - mat.isSelected && !disableSelection && !isImageApprover ? 'material-history-selected' : '' - - return ( - - {mat.materialInfo.length > 0 && - !hideInfoTabsContainer && - (isMaterialInfoAvailable || mat.appliedFilters?.length) && - hideSourceInfo && ( - - )} - - ) - }) - - const renderSearch = (): JSX.Element => ( - - ) - - const renderMaterialListBodyWrapper = (children: JSX.Element) => ( -
{children}
- ) - - const renderRuntimeParamsSidebar = (areTabsDisabled: boolean = false) => { - const showSidebar = isPreOrPostCD && !isFromBulkCD - if (!showSidebar) { - return null - } - - return ( -
- {RuntimeParamTabs && ( -
- -
- )} - -
- Application -
- -
- {appName} -
-
- ) - } - - const renderMaterialList = (isApprovalConfigured: boolean) => { - const { consumedImage, materialList, eligibleImagesCount } = - getConsumedAndAvailableMaterialList(isApprovalConfigured) - const selectImageTitle = state.isRollbackTrigger ? 'Select from previously deployed images' : 'Select Image' - const titleText = isApprovalConfigured && !isExceptionUser ? 'Approved images' : selectImageTitle - const showActionBar = - FilterActionBar && !state.searchApplied && !!resourceFilters?.length && !state.showConfiguredFilters - - return ( -
- {renderRuntimeParamsSidebar()} - - - {(bulkSidebarTab - ? bulkSidebarTab === CDMaterialSidebarType.IMAGE - : currentSidebarTab === CDMaterialSidebarType.IMAGE) || !RuntimeParameters ? ( - <> - {isApprovalConfigured && renderMaterial(consumedImage, true, isApprovalConfigured)} -
- {showActionBar ? ( - - ) : ( - {titleText} - )} - - - {state.showSearch ? ( - renderSearch() - ) : ( - - )} - - -
- - {materialList.length <= 0 - ? renderEmptyState(isApprovalConfigured, consumedImage.length > 0, !eligibleImagesCount) - : renderMaterial(materialList, false, isApprovalConfigured)} - - {!noMoreImages && !!materialList?.length && ( - - )} - - ) : ( -
- -
- )} -
-
- ) - } - - const renderCDModalHeader = (): JSX.Element | string => { - const _stageType = state.isRollbackTrigger ? STAGE_TYPE.ROLLBACK : stageType - switch (_stageType) { - case STAGE_TYPE.PRECD: - return 'Pre Deployment' - case STAGE_TYPE.CD: - return ( - <> - Deploy to   - {`${envName}${isVirtualEnvironment ? ' (Isolated)' : ''}`} - - ) - case STAGE_TYPE.POSTCD: - return 'Post Deployment' - case STAGE_TYPE.ROLLBACK: - return ( - <> - Rollback for {envName} - - ) - default: - return '' - } - } - - const renderTippyContent = () => { - if (!state.areMaterialsPassingFilters) { - return ( - <> -

No eligible images found!

-

- Please select an image that passes the configured filters to deploy -

- - ) - } - - return ( - <> -

Selected Config not available!

-

- {state.selectedConfigToDeploy.value === DeploymentWithConfigType.SPECIFIC_TRIGGER_CONFIG && - noSpecificDeploymentConfig - ? 'Please select a different image or configuration to deploy' - : 'Please select a different configuration to deploy'} -

- - ) - } - - const getDeployButtonIcon = () => { - if (deploymentWindowMetadata.userActionState === ACTION_STATE.BLOCKED) { - return null - } - if (stageType !== STAGE_TYPE.CD) { - return - } - return - } - - const getDeployButtonStyle = ( - userActionState: string, - canDeployWithoutApproval: boolean, - canImageApproverDeploy: boolean, - ): ButtonStyleType => { - if (userActionState === ACTION_STATE.BLOCKED) { - return ButtonStyleType.negative - } - if (userActionState === ACTION_STATE.PARTIAL || canDeployWithoutApproval || canImageApproverDeploy) { - return ButtonStyleType.warning - } - return ButtonStyleType.default - } - - const onClickDeploy = (e, disableDeployButton: boolean) => { - e.stopPropagation() - if (!disableDeployButton) { - if (!showPluginWarningOverlay && showPluginWarningBeforeTrigger) { - setShowPluginWarningOverlay(true) - return - } - - if ( - deploymentWindowMetadata.userActionState && - deploymentWindowMetadata.userActionState !== ACTION_STATE.ALLOWED - ) { - setShowDeploymentWindowConfirmation(true) - return - } - - deployTrigger(e) - } - } - - const renderTriggerDeployButton = (disableDeployButton: boolean) => { - const { userActionState } = deploymentWindowMetadata - const canDeployWithoutApproval = getCanDeployWithoutApproval(state, isExceptionUser) - const canImageApproverDeploy = getCanImageApproverDeploy(state, canApproverDeploy, isExceptionUser) - - return ( - onClickDeploy(e, disableDeployButton)} - startIcon={getDeployButtonIcon()} - text={ - canDeployWithoutApproval - ? 'Deploy without approval' - : `${ - userActionState === ACTION_STATE.BLOCKED - ? 'Deployment is blocked' - : CDButtonLabelMap[stageType] - }${isVirtualEnvironment ? ' to isolated env' : ''}` - } - endIcon={userActionState === ACTION_STATE.BLOCKED ? : null} - style={getDeployButtonStyle(userActionState, canDeployWithoutApproval, canImageApproverDeploy)} - disabled={disableDeployButton} - tooltipContent={ - canDeployWithoutApproval || canImageApproverDeploy - ? 'You are authorized to deploy as an exception user' - : '' - } - animateStartIcon={ - isCDNode && !disableDeployButton && (!userActionState || userActionState === ACTION_STATE.ALLOWED) - } - /> - ) - } - - const renderTriggerModalCTA = (isApprovalConfigured: boolean) => { - const disableDeployButton = - isDeployButtonDisabled() || - (!isExceptionUser && - material.length > 0 && - !canApproverDeploy && - getIsImageApprover(state.selectedMaterial?.userApprovalMetadata)) - const hideConfigDiffSelector = isApprovalConfigured && disableDeployButton - - return ( -
- {!hideConfigDiffSelector && - (state.isRollbackTrigger || state.isSelectImageTrigger) && - !showConfigDiffView && - isCDNode ? ( - onClickSetInitialParams(URL_PARAM_MODE_TYPE.REVIEW_CONFIG)} - noLastDeploymentConfig={noLastDeploymentConfig} - canReviewConfig={canReviewConfig()} - renderConfigNotAvailableTooltip={renderTippyContent} - /> - ) : ( - // NOTE: needed so that the button is pushed to the right since justify-content is set to space-between -
- )} -
- {/* == as we are expecting number but receiving string, 404 means custom charts */} - {SelectDeploymentStrategy && - isCDNode && - pipelineStrategies?.[0]?.error?.code != API_STATUS_CODES.NOT_FOUND && ( - - )} - ( - - {children} - - )} - > - {AllowedWithWarningTippy && showPluginWarningBeforeTrigger ? ( - onClickDeploy(e, disableDeployButton)} - nodeType={allowWarningWithTippyNodeTypeProp} - visible={showPluginWarningOverlay} - onClose={handleClosePluginWarningOverlay} - > - {renderTriggerDeployButton(disableDeployButton)} - - ) : ( - renderTriggerDeployButton(disableDeployButton) - )} - -
-
- ) - } - - const renderTriggerViewConfigDiff = () => ( - - ) - - const renderTriggerBody = (isApprovalConfigured: boolean) => ( -
- {showConfigDiffView && canReviewConfig() - ? renderTriggerViewConfigDiff() - : renderMaterialList(isApprovalConfigured)} -
- ) - - const handleClosePluginWarningOverlay = () => { - setShowPluginWarningOverlay(false) - } - - const handleConfirmationClose = (e) => { - e.stopPropagation() - handleClosePluginWarningOverlay() - setShowDeploymentWindowConfirmation(false) - } - - const renderCDModal = (isApprovalConfigured: boolean) => ( - <> -
- {showConfigDiffView ? ( -
- -

{renderCDModalHeader()}

- {state.selectedMaterial && ( - - )} -
- ) : ( -

{renderCDModalHeader()}

- )} -
- - {/* FIXME: This material.length>1 needs to be optimised */} - {isApprovalConfigured && - !isExceptionUser && - ApprovedImagesMessage && - (state.isRollbackTrigger || material.length - Number(isConsumedImageAvailable) > 0) && ( - } - /> - )} - {!isFromBulkCD && - MaintenanceWindowInfoBar && - deploymentWindowMetadata.type === DEPLOYMENT_WINDOW_TYPE.MAINTENANCE && - deploymentWindowMetadata.isActive && ( - - )} - {renderTriggerBody(isApprovalConfigured)} - {renderTriggerModalCTA(isApprovalConfigured)} - {DeploymentWindowConfirmationDialog && showDeploymentWindowConfirmation && ( - - )} - - - ) - - /* ------------Main Rendering logic ------------*/ - if (state.showConfiguredFilters && ConfiguredFilters) { - return ( - - ) - } - - if (showAppliedFilters) { - return ( - - ) - } - - // NOTE: Can make a skeleton component for loader - // TODO: Fix this condition for aborting the request - if (loadingMaterials || materialsError?.code === 0) { - return ( - <> - {!isFromBulkCD && ( -
-

{renderCDModalHeader()}

- -
- )} - -
- {renderRuntimeParamsSidebar(true)} - -
-
-
-
- -
-
-
-
- - ) - } - - if (materialsError) { - return ( - <> - {!isFromBulkCD && ( -
-

{renderCDModalHeader()}

- -
- )} - - - - ) - } - - if (material.length > 0) { - return isFromBulkCD ? renderTriggerBody(isApprovalConfigured) : renderCDModal(isApprovalConfigured) - } - - if (isFromBulkCD) { - return renderEmptyState(isApprovalConfigured) - } - - return ( - <> -
-

{renderCDModalHeader()}

- -
- - {renderEmptyState(isApprovalConfigured)} - - ) -} - -export default CDMaterial diff --git a/src/components/app/details/triggerView/cdMaterials.utils.ts b/src/components/app/details/triggerView/cdMaterials.utils.ts index ac7012343d..705e68bb0b 100644 --- a/src/components/app/details/triggerView/cdMaterials.utils.ts +++ b/src/components/app/details/triggerView/cdMaterials.utils.ts @@ -14,30 +14,9 @@ * limitations under the License. */ -import { ApprovalRuntimeStateType, CDMaterialType, FilterStates } from '@devtron-labs/devtron-fe-common-lib' +import { ApprovalRuntimeStateType, CDMaterialType } from '@devtron-labs/devtron-fe-common-lib' -import { LAST_SAVED_CONFIG_OPTION, SPECIFIC_TRIGGER_CONFIG_OPTION } from './TriggerView.utils' -import { CDMaterialState, FilterConditionViews, MATERIAL_TYPE, RegexValueType } from './types' - -export const getInitialState = (materialType: string, material: CDMaterialType[], searchImageTag: string) => () => ({ - isSecurityModuleInstalled: false, - loadingMore: false, - showOlderImages: true, - selectedConfigToDeploy: - materialType === MATERIAL_TYPE.rollbackMaterialList ? SPECIFIC_TRIGGER_CONFIG_OPTION : LAST_SAVED_CONFIG_OPTION, - selectedMaterial: material.find((_mat) => _mat.isSelected), - isRollbackTrigger: materialType === MATERIAL_TYPE.rollbackMaterialList, - isSelectImageTrigger: materialType === MATERIAL_TYPE.inputMaterialList, - materialInEditModeMap: new Map(), - showSearch: !!searchImageTag, - areMaterialsPassingFilters: - material.filter((materialDetails) => materialDetails.filterState === FilterStates.ALLOWED).length > 0, - searchApplied: !!searchImageTag, - searchText: searchImageTag ?? '', - showConfiguredFilters: false, - filterView: FilterConditionViews.ELIGIBLE, - resourceFilters: [], -}) +import { RegexValueType } from './types' export const getWfrId = (selectedMaterial: CDMaterialType, material: CDMaterialType[]) => selectedMaterial ? selectedMaterial.wfrId : material?.find((_mat) => _mat.isSelected)?.wfrId @@ -65,15 +44,13 @@ export const getIsMaterialApproved = (userApprovalMetadata: CDMaterialType['user return approvalRuntimeState === ApprovalRuntimeStateType.approved } -export const getCanDeployWithoutApproval = (state: CDMaterialState, isExceptionUser: boolean) => { - const isMaterialApproved = - state.selectedMaterial && getIsMaterialApproved(state.selectedMaterial.userApprovalMetadata) - +export const getCanDeployWithoutApproval = (selectedMaterial: CDMaterialType, isExceptionUser: boolean) => { + const isMaterialApproved = selectedMaterial && getIsMaterialApproved(selectedMaterial.userApprovalMetadata) return isExceptionUser && !isMaterialApproved } export const getCanImageApproverDeploy = ( - state: CDMaterialState, + selectedMaterial: CDMaterialType, canApproverDeploy: boolean, isExceptionUser: boolean, -) => isExceptionUser && !canApproverDeploy && state.selectedMaterial?.userApprovalMetadata?.hasCurrentUserApproved +) => isExceptionUser && !canApproverDeploy && selectedMaterial?.userApprovalMetadata?.hasCurrentUserApproved diff --git a/src/components/app/details/triggerView/config.ts b/src/components/app/details/triggerView/config.ts index ec6ac5ffbe..cb860cedac 100644 --- a/src/components/app/details/triggerView/config.ts +++ b/src/components/app/details/triggerView/config.ts @@ -14,19 +14,10 @@ * limitations under the License. */ -import { createContext } from 'react' -import { DeploymentNodeType } from '@devtron-labs/devtron-fe-common-lib' -import { TriggerViewContextType } from './types' import { importComponentFromFELibrary } from '@Components/common' const WebhookAddImageButton = importComponentFromFELibrary('WebhookAddImageButton', null, 'function') -export const TriggerViewContext = createContext({ - onClickCDMaterial: (cdNodeId, nodeType: DeploymentNodeType, isApprovalNode?: boolean, imageTag?: string) => {}, - onClickRollbackMaterial: (cdNodeId: number, offset?: number, size?: number) => {}, - reloadTriggerView: () => {}, -}) - export enum WorkflowDimensionType { TRIGGER = 'trigger', CREATE = 'create', diff --git a/src/components/app/details/triggerView/types.ts b/src/components/app/details/triggerView/types.ts index 4e10970bd6..e4b887ec8a 100644 --- a/src/components/app/details/triggerView/types.ts +++ b/src/components/app/details/triggerView/types.ts @@ -14,45 +14,34 @@ * limitations under the License. */ -import React from 'react' import { RouteComponentProps } from 'react-router-dom' import { AppConfigProps, ArtifactPromotionMetadata, - CDMaterialResponseType, - CDMaterialSidebarType, CDMaterialType, - CDModalTabType, CdPipeline, CIBuildConfigType, CIMaterialType, CiPipeline, CommonNodeAttr, - ConsequenceType, DeploymentAppTypes, DeploymentHistoryDetail, DeploymentNodeType, DeploymentWithConfigType, DynamicDataTableCellValidationState, - FilterConditionsListType, - ImageComment, Material, PipelineType, PolicyKindType, - ReleaseTag, RuntimePluginVariables, TaskErrorObj, - UploadFileDTO, - UploadFileProps, WorkflowType, } from '@devtron-labs/devtron-fe-common-lib' import { CIPipelineBuildType } from '@Components/ciPipeline/types' import { EnvironmentWithSelectPickerType } from '@Components/CIPipelineN/types' -import { AppContextType } from '@Components/common' -import { HostURLConfig } from '../../../../services/service.types' +import { DeployImageModalProps } from './DeployImageModal/types' import { Offset, WorkflowDimensions } from './config' export interface RuntimeParamsErrorState { @@ -62,112 +51,6 @@ export interface RuntimeParamsErrorState { export type HandleRuntimeParamChange = (updatedRuntimeParams: RuntimePluginVariables[]) => void -export type HandleRuntimeParamErrorState = (updatedErrorState: RuntimeParamsErrorState) => void - -type CDMaterialBulkRuntimeParams = - | { - isFromBulkCD: true - bulkRuntimeParams: RuntimePluginVariables[] - handleBulkRuntimeParamChange: HandleRuntimeParamChange - bulkRuntimeParamErrorState: RuntimeParamsErrorState - handleBulkRuntimeParamError: HandleRuntimeParamErrorState - bulkSidebarTab: CDMaterialSidebarType - bulkUploadFile: (props: UploadFileProps) => Promise - } - | { - isFromBulkCD?: false - bulkRuntimeParams?: never - handleBulkRuntimeParamChange?: never - bulkRuntimeParamErrorState?: never - handleBulkRuntimeParamError?: never - bulkSidebarTab?: never - bulkUploadFile?: never - } - -type CDMaterialPluginWarningProps = - | { - showPluginWarningBeforeTrigger: boolean - consequence: ConsequenceType - configurePluginURL: string - } - | { - showPluginWarningBeforeTrigger?: never - consequence?: never - configurePluginURL?: never - } - -export type CDMaterialProps = { - material?: CDMaterialType[] - isLoading: boolean - materialType: string - envName: string - envId?: number - redirectToCD?: () => void - stageType: DeploymentNodeType - changeTab?: ( - materrialId: string | number, - artifactId: number, - tab: CDModalTabType, - selectedCDDetail?: { id: number; type: DeploymentNodeType }, - appId?: number, - ) => void - selectImage?: ( - index: number, - materialType: string, - selectedCDDetail?: { id: number; type: DeploymentNodeType }, - appId?: number, - ) => void - toggleSourceInfo?: (materialIndex: number, selectedCDDetail?: { id: number; type: DeploymentNodeType }) => void - closeCDModal: (e: React.MouseEvent) => void - onClickRollbackMaterial?: ( - cdNodeId: number, - offset?: number, - size?: number, - callback?: (loadingMore: boolean, noMoreImages?: boolean) => void, - searchText?: string, - ) => void - parentPipelineId?: string - parentPipelineType?: string - parentEnvironmentName?: string - hideInfoTabsContainer?: boolean - appId?: number - pipelineId?: number - isFromBulkCD?: boolean - requestedUserId?: number - triggerType?: string - isVirtualEnvironment?: boolean - isSaveLoading?: boolean - ciPipelineId?: number - appReleaseTagNames?: string[] - setAppReleaseTagNames?: (appReleaseTags: string[]) => void - tagsEditable?: boolean - hideImageTaggingHardDelete?: boolean - setTagsEditable?: (tagsEditable: boolean) => void - updateCurrentAppMaterial?: (matId: number, releaseTags?: ReleaseTag[], imageComment?: ImageComment) => void - handleMaterialFilters?: ( - text: string, - cdNodeId, - nodeType: DeploymentNodeType, - isApprovalNode?: boolean, - fromRollback?: boolean, - ) => void - searchImageTag?: string - resourceFilters?: FilterConditionsListType[] - updateBulkCDMaterialsItem?: (singleCDMaterialResponse: CDMaterialResponseType) => void - deploymentAppType?: DeploymentAppTypes - selectedImageFromBulk?: string - isSuperAdmin?: boolean - isRedirectedFromAppDetails?: boolean - /** - * App name coming from app group view - * To be consumed through variable called appName - */ - selectedAppName?: string - isTriggerBlockedDueToPlugin?: boolean - handleSuccess?: () => void -} & CDMaterialBulkRuntimeParams & - CDMaterialPluginWarningProps - export interface ConfigToDeployOptionType { label: string value: DeploymentWithConfigType @@ -241,7 +124,8 @@ interface InputMaterials { export interface TriggerCDNodeProps extends RouteComponentProps<{ appId: string; envId: string }>, - Partial> { + Partial>, + Pick { x: number y: number height: number @@ -269,6 +153,8 @@ export interface TriggerCDNodeProps deploymentAppType: DeploymentAppTypes appId: number isDeploymentBlocked?: boolean + onClickCDMaterial: (cdNodeId: number, nodeType: DeploymentNodeType) => void + onClickRollbackMaterial: (cdNodeId: number) => void } export interface TriggerCDNodeState { @@ -280,7 +166,8 @@ export interface TriggerCDNodeState { export interface TriggerPrePostCDNodeProps extends RouteComponentProps<{ appId: string; envId: string }>, - Partial> { + Partial>, + Pick { x: number y: number height: number @@ -333,12 +220,7 @@ export interface WorkflowProps filteredCIPipelines?: any[] handleWebhookAddImageClick?: (webhookId: number) => void openCIMaterialModal: (ciNodeId: string) => void -} - -export interface TriggerViewContextType { - onClickCDMaterial: (cdNodeId, nodeType: DeploymentNodeType, isApprovalNode?: boolean) => void - onClickRollbackMaterial: (cdNodeId: number, offset?: number, size?: number) => void - reloadTriggerView: () => void + reloadTriggerView: () => Promise | void } export enum BulkSelectionEvents { @@ -350,13 +232,12 @@ export interface TriggerViewRouterProps { envId: string } -export interface TriggerViewProps extends RouteComponentProps { +export interface TriggerViewProps { isJobView?: boolean filteredEnvIds?: string - appContext: AppContextType } -interface FilteredCIPipelinesType { +export interface FilteredCIPipelinesType { active: boolean ciMaterial: CiMaterial[] dockerArgs: any @@ -377,35 +258,10 @@ interface FilteredCIPipelinesType { scanEnabled: boolean } -export interface TriggerViewState { - code: number - view: string - workflows: WorkflowType[] - nodeType: null | 'CI' | 'CD' | 'PRECD' | 'POSTCD' | 'APPROVAL' - cdNodeId: number - materialType: '' | 'inputMaterialList' | 'rollbackMaterialList' - isLoading: boolean - hostURLConfig: HostURLConfig - workflowId: number - filteredCIPipelines: FilteredCIPipelinesType[] - isSaveLoading?: boolean - environmentLists?: any[] - appReleaseTags?: string[] - tagsEditable?: boolean - hideImageTaggingHardDelete?: boolean - configs?: boolean - isDefaultConfigPresent?: boolean - searchImageTag?: string - resourceFilters?: FilterConditionsListType[] - selectedWebhookNodeId: number - isEnvListLoading?: boolean -} - -export type FilteredCIPipelineMapType = Map +export type FilteredCIPipelineMapType = Map export type BuildImageModalProps = Pick & { handleClose: () => void - reloadWorkflows: () => Promise workflows: WorkflowType[] /** * If not present would extract from selected workflow @@ -415,12 +271,14 @@ export type BuildImageModalProps = Pick & { reloadWorkflowStatus: () => void } & ( | { - filteredCIPipelines: TriggerViewState['filteredCIPipelines'] + filteredCIPipelines: FilteredCIPipelinesType[] filteredCIPipelineMap?: never + reloadWorkflows: () => Promise } | { filteredCIPipelineMap: FilteredCIPipelineMapType filteredCIPipelines?: never + reloadWorkflows: () => Promise } ) @@ -611,7 +469,7 @@ export const MATERIAL_TYPE = { rollbackMaterialList: 'rollbackMaterialList', inputMaterialList: 'inputMaterialList', none: 'none', -} +} as const export interface EmptyStateCIMaterialProps { isRepoError: boolean @@ -651,11 +509,6 @@ export interface AddDimensionsToDownstreamDeploymentsParams { startY: number } -export interface RenderCTAType { - mat: CDMaterialType - disableSelection: boolean -} - export interface WebhookPayload { eventTime: string matchedFiltersCount: number @@ -671,14 +524,13 @@ export interface WebhookReceivedFiltersType { match: boolean } -export interface CiWebhookModalProps - extends Pick, - Pick { +export interface CiWebhookModalProps extends Pick { ciPipelineMaterialId: number gitMaterialUrl: string ciPipelineId: number appId: string isJobCI: boolean + workflowId: number } export interface CIWebhookPayload { @@ -709,3 +561,27 @@ export interface CIPipelineMaterialDTO { } } } + +export interface UseTriggerViewServicesParams { + appId: string + isJobView: boolean + filteredEnvIds: string +} + +export enum CDNodeActions { + APPROVAL = 'approval', + CD_MATERIAL = 'cdMaterial', + ROLLBACK_MATERIAL = 'rollbackMaterial', +} + +export interface GetCDNodeSearchParams { + actionType: CDNodeActions + cdNodeId: number + nodeType?: DeploymentNodeType + fromAppGroup?: boolean +} + +export interface CDMaterialProps extends Pick { + workflows: WorkflowType[] + isTriggerView: boolean +} diff --git a/src/components/app/details/triggerView/workflow/Workflow.tsx b/src/components/app/details/triggerView/workflow/Workflow.tsx index 3bd348c3b8..6a7a52dd7e 100644 --- a/src/components/app/details/triggerView/workflow/Workflow.tsx +++ b/src/components/app/details/triggerView/workflow/Workflow.tsx @@ -14,15 +14,15 @@ * limitations under the License. */ -import React, { Component } from 'react' +import { Component } from 'react' import { Checkbox, CHECKBOX_VALUE, - DeploymentNodeType, noop, WorkflowNodeType, PipelineType, CommonNodeAttr, + DeploymentNodeType, } from '@devtron-labs/devtron-fe-common-lib' import { StaticNode } from './nodes/staticNode' import { TriggerCINode } from './nodes/triggerCINode' @@ -31,19 +31,16 @@ import { TriggerLinkedCINode } from './nodes/TriggerLinkedCINode' import { TriggerCDNode } from './nodes/triggerCDNode' import { TriggerPrePostCDNode } from './nodes/triggerPrePostCDNode' import { getCIPipelineURL, importComponentFromFELibrary, RectangularEdge as Edge } from '../../../../common' -import { WorkflowProps, TriggerViewContextType } from '../types' +import { CDNodeActions, WorkflowProps } from '../types' import { WebhookNode } from '../../../../workflowEditor/nodes/WebhookNode' import { GIT_BRANCH_NOT_CONFIGURED } from '../../../../../config' -import { TriggerViewContext } from '../config' - +import { getCDNodeActionSearch } from '../TriggerView.utils' const ApprovalNodeEdge = importComponentFromFELibrary('ApprovalNodeEdge') const LinkedCDNode = importComponentFromFELibrary('LinkedCDNode') const ImagePromotionLink = importComponentFromFELibrary('ImagePromotionLink', null, 'function') const BulkDeployLink = importComponentFromFELibrary('BulkDeployLink', null, 'function') export class Workflow extends Component { - static contextType?: React.Context = TriggerViewContext - goToWorkFlowEditor = (node: CommonNodeAttr) => { if (node.branch === GIT_BRANCH_NOT_CONFIGURED) { const ciPipelineURL = getCIPipelineURL( @@ -67,6 +64,34 @@ export class Workflow extends Component { } } + onClickCDMaterial = (cdNodeId: number, nodeType: DeploymentNodeType) => { + const search = getCDNodeActionSearch({ + actionType: CDNodeActions.CD_MATERIAL, + cdNodeId, + nodeType, + fromAppGroup: this.props.fromAppGrouping, + }) + this.props.history.push({ search }) + } + + onClickRollbackMaterial = (cdNodeId: number) => { + const search = getCDNodeActionSearch({ + actionType: CDNodeActions.ROLLBACK_MATERIAL, + cdNodeId, + fromAppGroup: this.props.fromAppGrouping, + }) + this.props.history.push({ search }) + } + + onClickApprovalNode = (cdNodeId: number) => { + const search = getCDNodeActionSearch({ + actionType: CDNodeActions.APPROVAL, + cdNodeId, + fromAppGroup: this.props.fromAppGrouping, + }) + this.props.history.push({ search }) + } + renderNodes() { return this.props.nodes.map((node: any) => { if (node.type === WorkflowNodeType.GIT) { @@ -269,6 +294,9 @@ export class Workflow extends Component { appId={this.props.appId} isDeploymentBlocked={node.isDeploymentBlocked} isTriggerBlocked={node.isTriggerBlocked} + onClickCDMaterial={this.onClickCDMaterial} + onClickRollbackMaterial={this.onClickRollbackMaterial} + reloadTriggerView={this.props.reloadTriggerView} /> ) } @@ -302,6 +330,8 @@ export class Workflow extends Component { appId={this.props.appId} isDeploymentBlocked={node.isDeploymentBlocked} isTriggerBlocked={node.isTriggerBlocked} + onClickCDMaterial={this.onClickCDMaterial} + reloadTriggerView={this.props.reloadTriggerView} /> ) } @@ -319,13 +349,6 @@ export class Workflow extends Component { }, []) } - onClickNodeEdge = (nodeId: number) => { - this.context.onClickCDMaterial(nodeId, DeploymentNodeType.CD, true) - this.props.history.push({ - search: `approval-node=${nodeId}`, - }) - } - renderEdgeList() { const edges = this.getEdges() // In the SVG, the bottom elements are rendered on top. @@ -338,7 +361,7 @@ export class Workflow extends Component { key={`trigger-edge-${edgeNode.startNode.id}${edgeNode.startNode.x}${edgeNode.startNode.y}-${edgeNode.endNode.id}`} startNode={edgeNode.startNode} endNode={edgeNode.endNode} - onClickEdge={() => this.onClickNodeEdge(edgeNode.endNode.id)} + onClickEdge={() => this.onClickApprovalNode(edgeNode.endNode.id)} edges={edges} /> ) diff --git a/src/components/app/details/triggerView/workflow/nodes/triggerCDNode.tsx b/src/components/app/details/triggerView/workflow/nodes/triggerCDNode.tsx index 779c92068f..87b0deaaf9 100644 --- a/src/components/app/details/triggerView/workflow/nodes/triggerCDNode.tsx +++ b/src/components/app/details/triggerView/workflow/nodes/triggerCDNode.tsx @@ -29,7 +29,6 @@ import { import { TriggerCDNodeProps, TriggerCDNodeState } from '../../types' import { ReactComponent as Rollback } from '../../../../../../assets/icons/ic-rollback.svg' import { DEFAULT_STATUS } from '../../../../../../config' -import { TriggerViewContext } from '../../config' import { envDescriptionTippy, getNodeSideHeadingAndClass } from './workflow.utils' import NoGitOpsRepoConfiguredWarning, { ReloadNoGitOpsRepoConfiguredModal, @@ -64,7 +63,7 @@ export class TriggerCDNode extends Component, prevState: Readonly): void { + componentDidUpdate(prevProps: Readonly): void { if (prevProps.isGitOpsRepoNotConfigured !== this.props.isGitOpsRepoNotConfigured) { this.setState({ gitOpsRepoWarningCondition: @@ -131,15 +130,15 @@ export class TriggerCDNode extends Component { - !this.state.gitOpsRepoWarningCondition && context.onClickRollbackMaterial(+this.props.id) + handleRollbackClick = (): void => { + !this.state.gitOpsRepoWarningCondition && this.props.onClickRollbackMaterial(+this.props.id) this.handleShowGitOpsRepoConfiguredWarning() } - handleImageSelection = (event, context): void => { + handleImageSelection = (event): void => { event.stopPropagation() !this.state.gitOpsRepoWarningCondition && - context.onClickCDMaterial(this.props.id, DeploymentNodeType[this.props.type]) + this.props.onClickCDMaterial(+this.props.id, DeploymentNodeType[this.props.type]) this.handleShowGitOpsRepoConfiguredWarning() } @@ -150,78 +149,70 @@ export class TriggerCDNode extends Component - {(context) => { - return ( - <> -
-
+
+
+ {heading} +
+
+
+ + Deploy: {this.props.deploymentStrategy} + + {envDescriptionTippy(this.props.environmentName, this.props.description)} +
+
+ +
+
+ {this.renderStatus()} +
+ {!this.props.isVirtualEnvironment && ( + +
-
-
- - Deploy: {this.props.deploymentStrategy} - - {envDescriptionTippy(this.props.environmentName, this.props.description)} -
-
- -
-
- {this.renderStatus()} -
- {!this.props.isVirtualEnvironment && ( - - - - )} - -
-
- {this.state.showGitOpsRepoConfiguredWarning && ( - - )} - {this.state.reloadNoGitOpsRepoConfiguredModal && ( - - )} - - ) - }} - + + + + )} + +
+
+ {this.state.showGitOpsRepoConfiguredWarning && ( + + )} + {this.state.reloadNoGitOpsRepoConfiguredModal && ( + + )} + ) } diff --git a/src/components/app/details/triggerView/workflow/nodes/triggerPrePostCDNode.tsx b/src/components/app/details/triggerView/workflow/nodes/triggerPrePostCDNode.tsx index dc502acaf7..524e6df928 100644 --- a/src/components/app/details/triggerView/workflow/nodes/triggerPrePostCDNode.tsx +++ b/src/components/app/details/triggerView/workflow/nodes/triggerPrePostCDNode.tsx @@ -25,7 +25,6 @@ import { import { TriggerPrePostCDNodeProps, TriggerPrePostCDNodeState } from '../../types' import { TriggerStatus } from '../../../../config' import { BUILD_STATUS, URLS, DEFAULT_STATUS } from '../../../../../../config' -import { TriggerViewContext } from '../../config' import NoGitOpsRepoConfiguredWarning from '../../../../../workflowEditor/NoGitOpsRepoConfiguredWarning' import { getNodeSideHeadingAndClass } from './workflow.utils' import { getAppGroupDeploymentHistoryLink } from '../../../../../ApplicationGroup/AppGroup.utils' @@ -93,9 +92,9 @@ export class TriggerPrePostCDNode extends Component { + handleImageSelection = (event): void => { event.stopPropagation() - !this.gitOpsRepoWarningCondition && context.onClickCDMaterial(this.props.id, this.props.type) + !this.gitOpsRepoWarningCondition && this.props.onClickCDMaterial(+this.props.id, this.props.type) this.handleShowGitOpsRepoConfiguredWarning() } @@ -107,61 +106,57 @@ export class TriggerPrePostCDNode extends Component - {(context) => { - const { heading, className: nodeSideClass } = getNodeSideHeadingAndClass( - this.props.isTriggerBlocked, - this.props.isDeploymentBlocked, - this.props.triggerType, - ) - return ( - <> -
{ - if (isClickable) { - this.redirectToCDDetails() - } - }} - > -
- {heading} -
-
-
- Stage - {stage} -
- -
- {this.renderStatus(isClickable, status)} -
- -
-
- {this.state.showGitOpsRepoConfiguredWarning && ( - - )} - - ) - }} - + <> +
{ + if (isClickable) { + this.redirectToCDDetails() + } + }} + > +
+ {heading} +
+
+
+ Stage + {stage} +
+ +
+ {this.renderStatus(isClickable, status)} +
+ +
+
+ {this.state.showGitOpsRepoConfiguredWarning && ( + + )} + ) } diff --git a/src/components/app/service.ts b/src/components/app/service.ts index 8556762afa..fbb80f0ad1 100644 --- a/src/components/app/service.ts +++ b/src/components/app/service.ts @@ -240,9 +240,9 @@ export const triggerBranchChange = (appIds: number[], envId: number, value: stri }) } -export const getWorkflowStatus = (appId: string) => { +export const getWorkflowStatus = (appId: number, options: APIOptions) => { const URL = `${Routes.APP_WORKFLOW_STATUS}/${appId}/${Routes.APP_LIST_V2}` - return get(URL) + return get(URL, options) } export const getCIPipelines = (appId, filteredEnvIds?: string, callback?: (...args) => void) => { diff --git a/src/components/app/types.ts b/src/components/app/types.ts index 80729570e4..61299aeb1d 100644 --- a/src/components/app/types.ts +++ b/src/components/app/types.ts @@ -34,7 +34,7 @@ import { CreateAppFormStateType } from '@Pages/App/CreateAppModal/types' import { GroupFilterType } from '../ApplicationGroup/AppGroup.types' import { DetailsType, ErrorItem, HibernationModalTypes } from './details/appDetails/appDetails.type' -import { CDMaterialProps } from './details/triggerView/types' +import { DeployImageModalProps } from './details/triggerView/DeployImageModal/types' interface CDModalProps { cdPipelineId?: number @@ -534,14 +534,13 @@ export interface SourceInfoType extends Pick, Partial< export interface AppDetailsCDModalType extends Pick< - AppDetails, - 'appId' | 'environmentId' | 'isVirtualEnvironment' | 'deploymentAppType' | 'environmentName' - >, - Pick { + AppDetails, + 'appId' | 'environmentId' | 'isVirtualEnvironment' | 'deploymentAppType' | 'environmentName' + > { cdModal: CDModalProps appName?: string - handleSuccess?: CDMaterialProps['handleSuccess'] - materialType: string + handleSuccess?: DeployImageModalProps['handleSuccess'] + materialType: DeployImageModalProps['materialType'] closeCDModal: () => void } diff --git a/yarn.lock b/yarn.lock index 57421478af..b4e3bc1428 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,9 +1722,9 @@ __metadata: languageName: node linkType: hard -"@devtron-labs/devtron-fe-common-lib@npm:1.19.0-pre-4": - version: 1.19.0-pre-4 - resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.19.0-pre-4" +"@devtron-labs/devtron-fe-common-lib@npm:1.19.0-pre-5": + version: 1.19.0-pre-5 + resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.19.0-pre-5" dependencies: "@codemirror/autocomplete": "npm:6.18.6" "@codemirror/lang-json": "npm:6.0.1" @@ -1774,7 +1774,7 @@ __metadata: react-select: 5.8.0 rxjs: ^7.8.1 yaml: ^2.4.1 - checksum: 10c0/744e41882e97f0b1c41c7aa62926244ef91d6d6fe4c5a1842ee7cbe03bdf08fadd4131d3429ad96a70071a30688494586e7aee527ccc7ef140d40e7db8a0e196 + checksum: 10c0/3a3548f036acf58e55b0887c14f5d288707d65f07eca21b210d48636c77f77a17721bcc124a72a44bfdcac55d5221a3465a5634585e05e6c5a0b0454a690e97d languageName: node linkType: hard @@ -5721,7 +5721,7 @@ __metadata: version: 0.0.0-use.local resolution: "dashboard@workspace:." dependencies: - "@devtron-labs/devtron-fe-common-lib": "npm:1.19.0-pre-4" + "@devtron-labs/devtron-fe-common-lib": "npm:1.19.0-pre-5" "@esbuild-plugins/node-globals-polyfill": "npm:0.2.3" "@playwright/test": "npm:^1.32.1" "@rjsf/core": "npm:^5.13.3"