diff --git a/.eslintignore b/.eslintignore index e82439818d..75f0ff3a09 100755 --- a/.eslintignore +++ b/.eslintignore @@ -103,8 +103,6 @@ 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/__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 dd849f2b5d..4279e2a0f6 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-2", + "@devtron-labs/devtron-fe-common-lib": "1.19.0-beta-2", "@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 e12bfc56c2..7788a53a51 100644 --- a/src/components/ApplicationGroup/AppGroup.types.ts +++ b/src/components/ApplicationGroup/AppGroup.types.ts @@ -79,7 +79,7 @@ export type BulkCDDetailDerivedFromNode = Required< > > & { stageNotAvailable: boolean - warningMessage: string + errorMessage: string triggerBlockedInfo: TriggerBlockedInfo consequence: CommonNodeAttr['pluginBlockState'] showPluginWarning: CommonNodeAttr['showPluginWarning'] @@ -92,6 +92,7 @@ export type BulkCDDetailType = BulkCDDetailDerivedFromNode & */ areMaterialsLoading: boolean materialError: ServerErrors | null + tagsWarningMessage: string } export interface BulkCDDetailTypeResponse { diff --git a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx index bcd169b71b..83be81921b 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/EnvTriggerView.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useState } from 'react' import { Prompt, Route, Switch, useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom' import Tippy from '@tippyjs/react' @@ -29,7 +29,6 @@ import { DEFAULT_ROUTE_PROMPT_MESSAGE, DeploymentNodeType, ErrorScreenManager, - handleAnalyticsEvent, PopupMenu, Progressing, ServerErrors, @@ -43,7 +42,8 @@ import { } from '@devtron-labs/devtron-fe-common-lib' import { BuildImageModal, BulkBuildImageModal } from '@Components/app/details/triggerView/BuildImageModal' -import { BulkDeployModal, DeployImageModal } from '@Components/app/details/triggerView/DeployImageModal' +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' @@ -58,7 +58,7 @@ import { TRIGGER_VIEW_PARAMS } from '../../../app/details/triggerView/Constants' 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 { @@ -70,14 +70,12 @@ import { import { processWorkflowStatuses } from '../../AppGroup.utils' import { BulkResponseStatus, - ENV_TRIGGER_VIEW_GA_EVENTS, GetBranchChangeStatus, SKIPPED_RESOURCES_MESSAGE, SKIPPED_RESOURCES_STATUS_TEXT, } from '../../Constants' import BulkSourceChange from './BulkSourceChange' -import { RenderCDMaterialContentProps } from './types' -import { getSelectedNodeAndAppId, getSelectedNodeAndMeta } from './utils' +import { getSelectedNodeAndAppId } from './utils' import './EnvTriggerView.scss' @@ -492,7 +490,7 @@ const EnvTriggerView = ({ filteredAppIds, isVirtualEnv }: AppGroupDetailDefaultT workflows={filteredWorkflows} isVirtualEnvironment={isVirtualEnv} envId={+envId} - handleSuccess={reloadTriggerView} + handleSuccess={getWorkflowStatusData} /> ) } @@ -529,95 +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, - ) - - const cdMaterialType = location.search.includes(TRIGGER_VIEW_PARAMS.CD_NODE) - ? MATERIAL_TYPE.inputMaterialList - : MATERIAL_TYPE.rollbackMaterialList - - return ( - - ) - } - - const renderCDMaterial = (): JSX.Element | null => { - if ( - location.search.includes(TRIGGER_VIEW_PARAMS.CD_NODE) || - location.search.includes(TRIGGER_VIEW_PARAMS.ROLLBACK_NODE) - ) { - const { node, appId, workflowId, appName, selectedCINode } = getSelectedNodeAndMeta( - filteredWorkflows, - location.search, - ) - - if (!node?.id) { - return null - } - - return renderCDMaterialContent({ - node, - appId, - selectedAppName: appName, - workflowId, - doesWorkflowContainsWebhook: selectedCINode?.type === WorkflowNodeType.WEBHOOK, - ciNodeId: selectedCINode?.id, - }) - } - - return null - } - const renderApprovalMaterial = () => { if (ApprovalMaterialModal && location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE)) { const { node, appId } = getSelectedNodeAndAppId(filteredWorkflows, location.search) - if (!node?.id || !appId) { - showError('Invalid node id') - return null - } - return ( - {renderCDMaterial()} + {renderBulkCDMaterial()} {renderBulkCIMaterial()} {renderApprovalMaterial()} diff --git a/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx b/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx index fef6990771..fcb40f8a72 100644 --- a/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx +++ b/src/components/ApplicationGroup/Details/TriggerView/TriggerResponseModal.tsx @@ -81,7 +81,7 @@ const TriggerResponseModalBody = ({ responseList, isLoading, isVirtualEnv }: Tri return } return ( -
+
feasiblePipelineIds.has(pipelineId)) } -export const getSelectedNodeAndMeta = ( - workflows: WorkflowType[], - search: string, -): { node: CommonNodeAttr; workflowId: string; appId: number; appName: string; selectedCINode: CommonNodeAttr } => { - const { cdNodeId, nodeType } = getNodeIdAndTypeFromSearch(search) - - const result = workflows.reduce( - (acc, workflow) => { - if (acc.node) return acc - const foundNode = workflow.nodes.find((node) => cdNodeId === node.id && node.type === nodeType) - - if (foundNode) { - const selectedCINode = workflow.nodes.find( - (node) => node.type === WorkflowNodeType.CI || node.type === WorkflowNodeType.WEBHOOK, - ) - return { - node: foundNode, - workflowId: workflow.id, - appId: workflow.appId, - appName: workflow.name, - selectedCINode, - } - } - return acc - }, - { node: undefined, workflowId: undefined, appId: undefined, appName: undefined, selectedCINode: undefined }, - ) - - return ( - result || { - node: undefined, - workflowId: undefined, - appId: undefined, - appName: undefined, - selectedCINode: undefined, - } - ) -} - export const getSelectedNodeAndAppId = ( workflows: WorkflowType[], search: string, diff --git a/src/components/app/details/appDetails/AppDetails.tsx b/src/components/app/details/appDetails/AppDetails.tsx index 06f86b4e96..b89913da53 100644 --- a/src/components/app/details/appDetails/AppDetails.tsx +++ b/src/components/app/details/appDetails/AppDetails.tsx @@ -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 1f339a0960..a1783e1206 100644 --- a/src/components/app/details/appDetails/AppDetailsCDModal.tsx +++ b/src/components/app/details/appDetails/AppDetailsCDModal.tsx @@ -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) && ( { + const location = useLocation() + const { currentAppName: triggerViewAppName } = useAppContext() + + if ( + location.search.includes(TRIGGER_VIEW_PARAMS.CD_NODE) || + location.search.includes(TRIGGER_VIEW_PARAMS.ROLLBACK_NODE) + ) { + const { node: cdNode, cdNodeId } = getSelectedNodeFromWorkflows(workflows, location.search) + + const materialType = location.search.includes(TRIGGER_VIEW_PARAMS.CD_NODE) + ? MATERIAL_TYPE.inputMaterialList + : MATERIAL_TYPE.rollbackMaterialList + + const selectedWorkflow = workflows.find((wf) => wf.nodes.some((node) => node.id === cdNodeId)) + const selectedCINode = selectedWorkflow?.nodes.find((node) => node.type === 'CI' || node.type === 'WEBHOOK') + const doesWorkflowContainsWebhook = selectedCINode?.type === 'WEBHOOK' + + const appId = selectedWorkflow?.appId ?? 0 + + const configurePluginURL = getCDPipelineURL( + String(appId), + selectedWorkflow?.id || '0', + doesWorkflowContainsWebhook ? '0' : selectedCINode?.id, + doesWorkflowContainsWebhook, + cdNode.id || '0', + true, + ) + + return ( + + ) + } + + return null +} + +export default CDMaterial diff --git a/src/components/app/details/triggerView/DeployImageModal/BulkDeployEmptyState.tsx b/src/components/app/details/triggerView/DeployImageModal/BulkDeployEmptyState.tsx index e9803bebf7..e39fd9fa13 100644 --- a/src/components/app/details/triggerView/DeployImageModal/BulkDeployEmptyState.tsx +++ b/src/components/app/details/triggerView/DeployImageModal/BulkDeployEmptyState.tsx @@ -52,7 +52,7 @@ const BulkDeployEmptyState = ({ return ( ) diff --git a/src/components/app/details/triggerView/DeployImageModal/BulkDeployModal.tsx b/src/components/app/details/triggerView/DeployImageModal/BulkDeployModal.tsx index 4c798ac8f9..e08388074a 100644 --- a/src/components/app/details/triggerView/DeployImageModal/BulkDeployModal.tsx +++ b/src/components/app/details/triggerView/DeployImageModal/BulkDeployModal.tsx @@ -1,4 +1,5 @@ import { Dispatch, SetStateAction, SyntheticEvent, useEffect, useMemo, useRef, useState } from 'react' +import { Prompt } from 'react-router-dom' import { AnimatedDeployButton, @@ -7,11 +8,13 @@ import { ButtonStyleType, CDMaterialResponseType, CDMaterialServiceEnum, + DEFAULT_ROUTE_PROMPT_MESSAGE, DeploymentNodeType, DeploymentStrategyTypeWithDefault, Drawer, genericCDMaterialsService, GenericEmptyState, + getIsApprovalPolicyConfigured, Icon, MODAL_TYPE, ModuleNameMap, @@ -27,6 +30,7 @@ import { uploadCDPipelineFile, useAsync, useMainContext, + usePrompt, } from '@devtron-labs/devtron-fe-common-lib' import { ResponseRowType } from '@Components/ApplicationGroup/AppGroup.types' @@ -97,6 +101,8 @@ const BulkDeployModal = ({ const isSecurityModuleInstalled = moduleInfoRes && moduleInfoRes?.result?.status === ModuleStatus.INSTALLED const isCDStage = stageType === DeploymentNodeType.CD + usePrompt({ shouldPrompt: isDeploymentLoading }) + useEffect( () => () => { initialDataAbortControllerRef.current.abort() @@ -139,7 +145,7 @@ const BulkDeployModal = ({ (node) => node.type === stageType && +node.environmentId === +envId, ) - if (!currentStageNode) { + if (!currentStageNode || baseBulkCDDetailMap[workflow.appId].errorMessage) { return () => null } @@ -167,7 +173,7 @@ const BulkDeployModal = ({ ) const appEnvList = validWorkflows - .filter((workflow) => !baseBulkCDDetailMap[workflow.appId].warningMessage) + .filter((workflow) => !baseBulkCDDetailMap[workflow.appId].errorMessage) .map((workflow) => ({ appId: workflow.appId, envId: +envId, @@ -223,7 +229,8 @@ const BulkDeployModal = ({ }, })) - const { deploymentWindowMetadata, materialError, warningMessage } = response[selectedAppId] || {} + const { deploymentWindowMetadata, materialError, errorMessage, tagsWarningMessage } = + response[selectedAppId] || {} if (materialError) { showError(materialError) @@ -243,7 +250,8 @@ const BulkDeployModal = ({ [selectedAppId]: { ...prev[selectedAppId], materialResponse: response[selectedAppId]?.materialResponse, - warningMessage, + errorMessage, + tagsWarningMessage, materialError, deploymentWindowMetadata, deployViewState: { @@ -385,8 +393,6 @@ const BulkDeployModal = ({ return } - setIsDeploymentLoading(true) - const { cdTriggerPromiseFunctions, triggeredAppIds } = getTriggerCDPromiseMethods({ appInfoMap, appsToRetry, @@ -396,11 +402,14 @@ const BulkDeployModal = ({ bulkDeploymentStrategy, }) + setIsDeploymentLoading(true) + setNumberOfAppsLoading(triggeredAppIds.length) + if (!triggeredAppIds.length) { setIsDeploymentLoading(false) ToastManager.showToast({ variant: ToastVariantType.error, - description: 'No applications selected for deployment', + description: 'No valid applications are present for deployment', }) return } @@ -418,6 +427,7 @@ const BulkDeployModal = ({ setResponseList(newResponseList) setIsDeploymentLoading(false) + setNumberOfAppsLoading(0) } const setDeployViewState: DeployImageContentProps['setDeployViewState'] = (getUpdatedDeployViewState) => { @@ -469,11 +479,9 @@ const BulkDeployModal = ({ const { tagsWarning, updatedMaterials } = getUpdatedMaterialsForTagSelection( tagOption.value, appDetails.materialResponse?.materials || [], - ) - - const { tagsWarning: previousTagWarning } = getUpdatedMaterialsForTagSelection( - selectedImageTagOption.value, - appDetails.materialResponse?.materials || [], + !getIsApprovalPolicyConfigured( + appDetails.materialResponse?.deploymentApprovalInfo?.approvalConfigData, + ) || getIsExceptionUser(appDetails.materialResponse), ) updatedAppInfoMap[appDetails.appId] = { @@ -482,8 +490,7 @@ const BulkDeployModal = ({ ...appDetails.materialResponse, materials: updatedMaterials, }, - warningMessage: - previousTagWarning || !appDetails.warningMessage ? tagsWarning : appDetails.warningMessage, + tagsWarningMessage: tagsWarning, } }) return updatedAppInfoMap @@ -502,16 +509,37 @@ const BulkDeployModal = ({ await onClickDeploy(e) } - const isDeployButtonDisabled = useMemo( - () => + const onImageSelection: DeployImageContentProps['onImageSelection'] = () => { + // Will just clear the tagsWarningMessage for app others are handled in DeployImageContent + setAppInfoMap((prev) => ({ + ...prev, + [selectedAppId]: { + ...prev[selectedAppId], + tagsWarningMessage: '', + }, + })) + } + + const isDeployButtonDisabled = useMemo(() => { + const atleastOneImageSelected = Object.values(appInfoMap).some((appDetails) => + (appDetails.materialResponse?.materials || []).some((material) => material.isSelected), + ) + + if (!atleastOneImageSelected) { + return true + } + + return ( isDeploymentLoading || isLoadingAppInfoMap || + // Not disabling deploy button even if there is a warning message, since apps with warning will not have selected materials + // and hence will not be deployed Object.values(appInfoMap).some((appDetails) => { - const { materialResponse, deployViewState, areMaterialsLoading } = appDetails - return areMaterialsLoading || !materialResponse || !deployViewState - }), - [appInfoMap, isDeploymentLoading, isLoadingAppInfoMap], - ) + const { areMaterialsLoading } = appDetails + return areMaterialsLoading + }) + ) + }, [appInfoMap, isDeploymentLoading, isLoadingAppInfoMap]) const canDeployWithoutApproval = useMemo( () => @@ -632,6 +660,7 @@ const BulkDeployModal = ({ handleTagChange={handleTagChange} changeApp={changeApp} selectedTagName={selectedImageTagOption.value} + onImageSelection={onImageSelection} /> ) } @@ -682,7 +711,7 @@ const BulkDeployModal = ({ onButtonClick={onClickStartDeploy} disabled={isDeployButtonDisabled} isLoading={isDeploymentLoading} - animateStartIcon={isCDStage} + animateStartIcon={isCDStage && !isDeployButtonDisabled} style={ canDeployWithoutApproval || canImageApproverDeploy ? ButtonStyleType.warning @@ -701,36 +730,42 @@ const BulkDeployModal = ({ } return ( - -
-
- + <> + +
+
+ + +
{renderContent()}
+
-
{renderContent()}
+ {isLoadingAppInfoMap || showStrategyFeasibilityPage ? null : renderFooter()}
- {isLoadingAppInfoMap || showStrategyFeasibilityPage ? null : renderFooter()} -
+ {showResistanceBox && ( + + )} + - {showResistanceBox && ( - - )} - + + ) } diff --git a/src/components/app/details/triggerView/DeployImageModal/BulkTriggerSidebar.tsx b/src/components/app/details/triggerView/DeployImageModal/BulkTriggerSidebar.tsx index ff682ddc59..6c128a6447 100644 --- a/src/components/app/details/triggerView/DeployImageModal/BulkTriggerSidebar.tsx +++ b/src/components/app/details/triggerView/DeployImageModal/BulkTriggerSidebar.tsx @@ -152,12 +152,25 @@ const BulkTriggerSidebar = ({ return } - if ((!!app.warningMessage && !app.showPluginWarning) || app.materialError?.errors?.length) { + const isRuntimeParamsErrorPresent = + app.deployViewState?.runtimeParamsErrorState && !app.deployViewState.runtimeParamsErrorState.isValid + + if ( + (!!app.errorMessage && !app.showPluginWarning) || + app.materialError?.errors?.length || + app.tagsWarningMessage || + isRuntimeParamsErrorPresent + ) { return (
- - - {app.warningMessage || app.materialError?.errors?.[0]?.userMessage} +
+ +
+ + {app.errorMessage || + app.materialError?.errors?.[0]?.userMessage || + app.tagsWarningMessage || + 'Invalid runtime parameters'}
) diff --git a/src/components/app/details/triggerView/DeployImageModal/DeployImageContent.tsx b/src/components/app/details/triggerView/DeployImageModal/DeployImageContent.tsx index f602c9deac..52badd211a 100644 --- a/src/components/app/details/triggerView/DeployImageModal/DeployImageContent.tsx +++ b/src/components/app/details/triggerView/DeployImageModal/DeployImageContent.tsx @@ -21,6 +21,7 @@ import { InfoBlock, isNullOrUndefined, MaterialInfo, + noop, Progressing, SearchBar, SegmentedControlProps, @@ -59,6 +60,7 @@ const RuntimeParameters = importComponentFromFELibrary('RuntimeParameters', null const SecurityModalSidebar = importComponentFromFELibrary('SecurityModalSidebar', null, 'function') const CDMaterialInfo = importComponentFromFELibrary('CDMaterialInfo') const ConfiguredFilters = importComponentFromFELibrary('ConfiguredFilters') +const DeploymentWindowInfoBar = importComponentFromFELibrary('DeploymentWindowInfoBar') const DeployImageContent = ({ appId, @@ -91,6 +93,7 @@ const DeployImageContent = ({ selectedTagName, handleTagChange, changeApp, + onImageSelection = noop, }: DeployImageContentProps) => { // WARNING: Pls try not to create a useState in this component, it is supposed to be a dumb component. const history = useHistory() @@ -110,6 +113,11 @@ const DeployImageContent = ({ const isConsumedImageAvailable = getIsConsumedImageAvailable(materials) const isPreOrPostCD = stageType === DeploymentNodeType.PRECD || stageType === DeploymentNodeType.POSTCD const isCDNode = stageType === DeploymentNodeType.CD + const showDeploymentWindowInfoBar = !!( + DeploymentWindowInfoBar && + isBulkTrigger && + deploymentWindowMetadata.warningMessage + ) const { searchText, @@ -209,6 +217,7 @@ const DeployImageContent = ({ } const handleImageSelection: ImageSelectionCTAProps['handleImageSelection'] = (materialIndex) => { + onImageSelection(materialIndex) setMaterialResponse((prevData) => { const updatedMaterialResponse = structuredClone(prevData) return { @@ -668,6 +677,7 @@ const DeployImageContent = ({ return ( <> {!showFiltersView && + !isBulkTrigger && isApprovalConfigured && !isExceptionUser && ApprovedImagesMessage && @@ -694,9 +704,20 @@ const DeployImageContent = ({ > {renderSidebar()}
- {renderContent()} + {showDeploymentWindowInfoBar && ( + + )} +
+ {renderContent()} +
diff --git a/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx b/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx index d2c18850fc..1894e39c50 100644 --- a/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx +++ b/src/components/app/details/triggerView/DeployImageModal/DeployImageModal.tsx @@ -175,6 +175,7 @@ const DeployImageModal = ({ const allowWarningWithTippyNodeTypeProp = getAllowWarningWithTippyNodeTypeProp(stageType) const runtimeParamsList = materialResponse?.runtimeParams || [] const requestedUserId = materialResponse?.requestedUserId + const showFiltersView = deployViewState.showAppliedFilters || deployViewState.showConfiguredFilters usePrompt({ shouldPrompt: isDeploymentLoading }) @@ -715,7 +716,7 @@ const DeployImageModal = ({ onClick={stopPropagation} >
- {!deployViewState.showAppliedFilters && !deployViewState.showConfiguredFilters && ( + {!showFiltersView && ( {renderContent()}
- {initialDataError || isInitialDataLoading || materialList.length === 0 ? null : ( + {initialDataError || isInitialDataLoading || showFiltersView || materialList.length === 0 ? null : (
{renderFooter()}
diff --git a/src/components/app/details/triggerView/DeployImageModal/types.ts b/src/components/app/details/triggerView/DeployImageModal/types.ts index db4d51ba62..2353fc52cd 100644 --- a/src/components/app/details/triggerView/DeployImageModal/types.ts +++ b/src/components/app/details/triggerView/DeployImageModal/types.ts @@ -162,6 +162,7 @@ export type DeployImageContentProps = Pick< appInfoMap: Record selectedTagName: string handleTagChange: (tagOption: SelectPickerOptionType) => void + onImageSelection: (materialIndex: number) => void changeApp: (appId: number) => void policyConsequences?: never } @@ -172,6 +173,7 @@ export type DeployImageContentProps = Pick< handleTagChange?: never policyConsequences: PolicyConsequencesDTO changeApp?: never + onImageSelection?: never } ) diff --git a/src/components/app/details/triggerView/DeployImageModal/utils.tsx b/src/components/app/details/triggerView/DeployImageModal/utils.tsx index 97d7466ae0..1ff64d5836 100644 --- a/src/components/app/details/triggerView/DeployImageModal/utils.tsx +++ b/src/components/app/details/triggerView/DeployImageModal/utils.tsx @@ -14,6 +14,7 @@ import { DeploymentWithConfigType, ExcludedImageNode, FilterStates, + getIsApprovalPolicyConfigured, getIsRequestAborted, getStageTitle, Icon, @@ -246,8 +247,6 @@ const processConsumedAndApprovedImages = (materials: CDMaterialType[]) => { !mat.userApprovalMetadata || mat.userApprovalMetadata.approvalRuntimeState !== ApprovalRuntimeStateType.approved ) { - // TODO: Check if this is needed - // mat.isSelected = false consumedImage.push(mat) } else { approvedImages.push(mat) @@ -383,7 +382,11 @@ export const getDeployButtonStyle = ( export const getIsConsumedImageAvailable = (materials: CDMaterialType[]) => materials.some((materialItem) => materialItem.deployed && materialItem.latest) ?? false -const getTagWarningRelatedToMaterial = (updatedMaterials: CDMaterialType[], selectedImageTagName: string): string => { +const getTagWarningRelatedToMaterial = ( + updatedMaterials: CDMaterialType[], + selectedImageTagName: string, + canSelectNonApprovedImage: boolean, +): string => { const selectedImage = updatedMaterials.find((material) => material.isSelected) const selectedTagParsedName = @@ -401,11 +404,23 @@ const getTagWarningRelatedToMaterial = (updatedMaterials: CDMaterialType[], sele return `Tag ${selectedTagParsedName} is not eligible for deployment` } + const isNonApprovedImage = + !selectedImage.userApprovalMetadata || + selectedImage.userApprovalMetadata.approvalRuntimeState !== ApprovalRuntimeStateType.approved + + if (isNonApprovedImage && !canSelectNonApprovedImage) { + return `Tag ${selectedTagParsedName} is not approved` + } + return '' } // If tag is not present, and image is selected we will show mixed tag -export const getUpdatedMaterialsForTagSelection = (tagName: string, materials: CDMaterialType[]) => { +export const getUpdatedMaterialsForTagSelection = ( + tagName: string, + materials: CDMaterialType[], + canSelectNonApprovedImage: boolean, +) => { const sourceMaterials = structuredClone(materials) const updatedMaterials = sourceMaterials.map((material, materialIndex) => { @@ -440,7 +455,7 @@ export const getUpdatedMaterialsForTagSelection = (tagName: string, materials: C const selectedImage = updatedMaterials.find((material) => material.isSelected) - const tagsWarning = getTagWarningRelatedToMaterial(updatedMaterials, tagName) + const tagsWarning = getTagWarningRelatedToMaterial(updatedMaterials, tagName, canSelectNonApprovedImage) if (selectedImage && tagsWarning) { selectedImage.isSelected = false @@ -497,7 +512,7 @@ export const getBaseBulkCDDetailsMap = (validWorkflows: WorkflowType[], stageTyp true, ), triggerType: currentStageNode?.triggerType, - warningMessage: noStageWarning || blockedStageWarning || '', + errorMessage: noStageWarning || blockedStageWarning || '', triggerBlockedInfo: currentStageNode?.triggerBlockedInfo, stageNotAvailable: !currentStageNode, showPluginWarning: currentStageNode?.showPluginWarning, @@ -524,32 +539,41 @@ export const getBulkCDDetailsMapFromResponse: GetBulkCDDetailsMapFromResponseTyp const { tagsWarning, updatedMaterials } = getUpdatedMaterialsForTagSelection( selectedTagName, materialResponse.value.materials, + !getIsApprovalPolicyConfigured(materialResponse?.value.deploymentApprovalInfo?.approvalConfigData) || + getIsExceptionUser(materialResponse.value), ) const parsedTagsWarning = searchText ? '' : tagsWarning + const newMaterials = searchText ? materialResponse.value.materials : updatedMaterials + + // In case of no search, we will show tag not found + const noImageSelectedWarning = + searchText && !newMaterials.some((material) => material.isSelected) ? 'No image selected' : '' const updatedWarningMessage = - baseBulkCDDetailMap[appId].warningMessage || - deploymentWindowMap[appId]?.warningMessage || - parsedTagsWarning + baseBulkCDDetailMap[appId].errorMessage || + noImageSelectedWarning || + deploymentWindowMap[appId]?.warningMessage // In case of search and reload even though method gives whole state, will only update deploymentWindowMetadata, warningMessage and materialResponse bulkCDDetailsMap[appId] = { ...baseBulkCDDetailMap[appId], materialResponse: { ...materialResponse.value, - materials: searchText ? materialResponse.value.materials : updatedMaterials, + materials: newMaterials, }, - deploymentWindowMetadata: deploymentWindowMap[appId], + deploymentWindowMetadata: deploymentWindowMap[appId] || ({} as DeploymentWindowProfileMetaData), areMaterialsLoading: false, deployViewState: structuredClone(INITIAL_DEPLOY_VIEW_STATE), - warningMessage: updatedWarningMessage, + errorMessage: updatedWarningMessage, + tagsWarningMessage: parsedTagsWarning, materialError: null, } } else { bulkCDDetailsMap[appId] = { ...baseBulkCDDetailMap[appId], - materialResponse: {} as CDMaterialResponseType, + tagsWarningMessage: '', + materialResponse: null, deploymentWindowMetadata: {} as DeploymentWindowProfileMetaData, deployViewState: structuredClone(INITIAL_DEPLOY_VIEW_STATE), materialError: diff --git a/src/components/app/details/triggerView/TriggerView.tsx b/src/components/app/details/triggerView/TriggerView.tsx index 0496bd9976..45a84911db 100644 --- a/src/components/app/details/triggerView/TriggerView.tsx +++ b/src/components/app/details/triggerView/TriggerView.tsx @@ -17,31 +17,20 @@ import React, { useState } from 'react' import { Route, Switch, useHistory, useLocation, useParams, useRouteMatch } from 'react-router-dom' -import { - CommonNodeAttr, - DeploymentNodeType, - DocLink, - ErrorScreenManager, - Progressing, -} from '@devtron-labs/devtron-fe-common-lib' +import { DocLink, ErrorScreenManager, Progressing } from '@devtron-labs/devtron-fe-common-lib' import { getExternalCIConfig } from '@Components/ciPipeline/Webhook/webhook.service' import { URLS } from '../../../../config' import { APP_DETAILS } from '../../../../config/constantMessaging' import { LinkedCIDetail } from '../../../../Pages/Shared/LinkedCIDetailsModal' -import { - getCDPipelineURL, - importComponentFromFELibrary, - InValidHostUrlWarningBlock, - useAppContext, -} from '../../../common' +import { importComponentFromFELibrary, InValidHostUrlWarningBlock, useAppContext } from '../../../common' import { getModuleInfo } from '../../../v2/devtronStackManager/DevtronStackManager.service' import { AppNotConfigured } from '../appDetails/AppDetails' import { Workflow } from './workflow/Workflow' import { BuildImageModal } from './BuildImageModal' +import CDMaterial from './CDMaterial' import { TRIGGER_VIEW_PARAMS } from './Constants' -import { DeployImageModal } from './DeployImageModal' import { useTriggerViewServices } from './TriggerView.service' import { getSelectedNodeFromWorkflows, shouldRenderWebhookAddImageModal } from './TriggerView.utils' import { CIMaterialRouterProps, MATERIAL_TYPE, TriggerViewProps } from './types' @@ -110,76 +99,20 @@ const TriggerView = ({ isJobView, filteredEnvIds }: TriggerViewProps) => { history.push(match.url) } - const renderCDMaterial = () => { - if ( - location.search.includes(TRIGGER_VIEW_PARAMS.CD_NODE) || - location.search.includes(TRIGGER_VIEW_PARAMS.ROLLBACK_NODE) - ) { - const cdNode: CommonNodeAttr = getSelectedNodeFromWorkflows(workflows, location.search) - if (!cdNode.id) { - return null - } - const materialType = location.search.includes(TRIGGER_VIEW_PARAMS.CD_NODE) - ? MATERIAL_TYPE.inputMaterialList - : MATERIAL_TYPE.rollbackMaterialList - - const selectedWorkflow = workflows.find((wf) => wf.nodes.some((node) => node.id === cdNode.id)) - const selectedCINode = selectedWorkflow?.nodes.find((node) => node.type === 'CI' || node.type === 'WEBHOOK') - const doesWorkflowContainsWebhook = selectedCINode?.type === 'WEBHOOK' - const configurePluginURL = getCDPipelineURL( - appId, - selectedWorkflow.id, - doesWorkflowContainsWebhook ? '0' : selectedCINode?.id, - doesWorkflowContainsWebhook, - cdNode.id, - true, - ) - - return ( - - ) - } - - return null - } - const renderApprovalMaterial = () => { if (ApprovalMaterialModal && location.search.includes(TRIGGER_VIEW_PARAMS.APPROVAL_NODE)) { - const node = getSelectedNodeFromWorkflows(workflows, location.search) - - if (!node.id) { - return null - } + const { node, cdNodeId } = getSelectedNodeFromWorkflows(workflows, location.search) return ( ) @@ -287,7 +220,12 @@ const TriggerView = ({ isJobView, filteredEnvIds }: TriggerViewProps) => { - {renderCDMaterial()} + {renderApprovalMaterial()}
{WorkflowActionRouter && ( diff --git a/src/components/app/details/triggerView/TriggerView.utils.tsx b/src/components/app/details/triggerView/TriggerView.utils.tsx index e552a172dd..32c9957153 100644 --- a/src/components/app/details/triggerView/TriggerView.utils.tsx +++ b/src/components/app/details/triggerView/TriggerView.utils.tsx @@ -22,7 +22,6 @@ import { DeploymentNodeType, DeploymentWithConfigType, handleAnalyticsEvent, - showError, WorkflowType, } from '@devtron-labs/devtron-fe-common-lib' @@ -212,24 +211,23 @@ export const getNodeIdAndTypeFromSearch = (search: string) => { return { cdNodeId, nodeType } } -export const getSelectedNodeFromWorkflows = (workflows: WorkflowType[], search: string): CommonNodeAttr => { +export const getSelectedNodeFromWorkflows = ( + workflows: WorkflowType[], + search: string, +): { cdNodeId: string; node: CommonNodeAttr } => { const { cdNodeId, nodeType } = getNodeIdAndTypeFromSearch(search) - if (!cdNodeId) { - showError('Invalid node id') - return {} as CommonNodeAttr - } - - // 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 (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 foundNode + if (foundNode) { + return { cdNodeId, node: foundNode } + } } - showError('Invalid node id') - return {} as CommonNodeAttr + return { cdNodeId: cdNodeId ?? '0', node: {} as CommonNodeAttr } } export const getCDNodeActionSearch = ({ diff --git a/src/components/app/details/triggerView/types.ts b/src/components/app/details/triggerView/types.ts index 2f075ec190..e4b887ec8a 100644 --- a/src/components/app/details/triggerView/types.ts +++ b/src/components/app/details/triggerView/types.ts @@ -14,43 +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 { DeployImageModalProps } from './DeployImageModal/types' import { Offset, WorkflowDimensions } from './config' export interface RuntimeParamsErrorState { @@ -60,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 @@ -624,11 +509,6 @@ export interface AddDimensionsToDownstreamDeploymentsParams { startY: number } -export interface RenderCTAType { - mat: CDMaterialType - disableSelection: boolean -} - export interface WebhookPayload { eventTime: string matchedFiltersCount: number @@ -700,3 +580,8 @@ export interface GetCDNodeSearchParams { nodeType?: DeploymentNodeType fromAppGroup?: boolean } + +export interface CDMaterialProps extends Pick { + workflows: WorkflowType[] + isTriggerView: boolean +} diff --git a/src/components/app/types.ts b/src/components/app/types.ts index 84f2c699c8..61299aeb1d 100644 --- a/src/components/app/types.ts +++ b/src/components/app/types.ts @@ -35,7 +35,6 @@ import { CreateAppFormStateType } from '@Pages/App/CreateAppModal/types' import { GroupFilterType } from '../ApplicationGroup/AppGroup.types' import { DetailsType, ErrorItem, HibernationModalTypes } from './details/appDetails/appDetails.type' import { DeployImageModalProps } from './details/triggerView/DeployImageModal/types' -import { CDMaterialProps } from './details/triggerView/types' interface CDModalProps { cdPipelineId?: number @@ -535,13 +534,12 @@ 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'] + handleSuccess?: DeployImageModalProps['handleSuccess'] materialType: DeployImageModalProps['materialType'] closeCDModal: () => void } diff --git a/yarn.lock b/yarn.lock index 88b83efba9..eeb73fac6f 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-2": - version: 1.19.0-pre-2 - resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.19.0-pre-2" +"@devtron-labs/devtron-fe-common-lib@npm:1.19.0-beta-2": + version: 1.19.0-beta-2 + resolution: "@devtron-labs/devtron-fe-common-lib@npm:1.19.0-beta-2" 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/fffdd3524d59a4c19206ca9454b3234010c80eb2b2861b792c6a1b20e66a9e9fd3c21b4a07cec811b9ee733420768efb08d782c901db416e8dd3e27f7dd5103e + checksum: 10c0/98ca8ce250bc4be7b99a9bda02ec0aa4021d872976d3ad42fc61d82da083fcf506471e8f1f131fc033e028896853cd9844ff948ebec6cd4647e256513628f2ad 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-2" + "@devtron-labs/devtron-fe-common-lib": "npm:1.19.0-beta-2" "@esbuild-plugins/node-globals-polyfill": "npm:0.2.3" "@playwright/test": "npm:^1.32.1" "@rjsf/core": "npm:^5.13.3"