diff --git a/package-lock.json b/package-lock.json index bb371bffb..4ba478ef3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-5", + "version": "1.13.0-pre-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-5", + "version": "1.13.0-pre-6", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 58d6c1379..4227d5d2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@devtron-labs/devtron-fe-common-lib", - "version": "1.13.0-pre-5", + "version": "1.13.0-pre-6", "description": "Supporting common component library", "type": "module", "main": "dist/index.js", diff --git a/src/Assets/IconV2/ic-disconnect.svg b/src/Assets/IconV2/ic-disconnect.svg index 1ad7ac9c1..b4d32e886 100644 --- a/src/Assets/IconV2/ic-disconnect.svg +++ b/src/Assets/IconV2/ic-disconnect.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/src/Assets/Img/ic-celebration.svg b/src/Assets/Img/ic-celebration.svg new file mode 100644 index 000000000..5c51f12ca --- /dev/null +++ b/src/Assets/Img/ic-celebration.svg @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Assets/Img/ic-man-on-rocket.svg b/src/Assets/Img/ic-man-on-rocket.svg new file mode 100644 index 000000000..70bbd0991 --- /dev/null +++ b/src/Assets/Img/ic-man-on-rocket.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Common/Helper.tsx b/src/Common/Helper.tsx index 2180951a3..d060a1042 100644 --- a/src/Common/Helper.tsx +++ b/src/Common/Helper.tsx @@ -17,7 +17,7 @@ import React, { SyntheticEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import DOMPurify from 'dompurify' import { JSONPath, JSONPathOptions } from 'jsonpath-plus' -import { compare as compareJSON, applyPatch, unescapePathComponent,deepClone } from 'fast-json-patch' +import { compare as compareJSON, applyPatch, unescapePathComponent, deepClone } from 'fast-json-patch' import { components } from 'react-select' import * as Sentry from '@sentry/browser' import moment from 'moment' @@ -517,7 +517,7 @@ export const getUrlWithSearchParams = ({ @@ -615,7 +615,10 @@ const buildObjectFromPathTokens = (index: number, tokens: string[], value: any) const numberKey = Number(key) const isKeyNumber = !Number.isNaN(numberKey) return isKeyNumber - ? [...Array(numberKey).fill(UNCHANGED_ARRAY_ELEMENT_SYMBOL), buildObjectFromPathTokens(index + 1, tokens, value)] + ? [ + ...Array(numberKey).fill(UNCHANGED_ARRAY_ELEMENT_SYMBOL), + buildObjectFromPathTokens(index + 1, tokens, value), + ] : { [unescapePathComponent(key)]: buildObjectFromPathTokens(index + 1, tokens, value) } } @@ -665,7 +668,12 @@ export const powerSetOfSubstringsFromStart = (strings: string[], regex: RegExp) }) export const convertJSONPointerToJSONPath = (pointer: string) => - unescapePathComponent(pointer.replace(/\/([\*0-9]+)\//g, '[$1].').replace(/\//g, '.').replace(/\./, '$.')) + unescapePathComponent( + pointer + .replace(/\/([\*0-9]+)\//g, '[$1].') + .replace(/\//g, '.') + .replace(/\./, '$.'), + ) export const flatMapOfJSONPaths = ( paths: string[], @@ -1002,7 +1010,7 @@ export const getBranchIcon = (sourceType, _isRegex?: boolean, webhookEventName?: return } if (webhookEventName === WebhookEventNameType.TAG_CREATION) { - return + return } return } @@ -1026,7 +1034,6 @@ export const getIframeWithDefaultAttributes = (iframeString: string, defaultName const parentDiv = document.createElement('div') parentDiv.innerHTML = getSanitizedIframe(iframeString) - const iframe = parentDiv.querySelector('iframe') if (iframe) { if (!iframe.hasAttribute('title') && !!defaultName) { @@ -1095,7 +1102,7 @@ export const getTTLInHumanReadableFormat = (ttl: number): string => { } const humanizedDuration = moment.duration(absoluteTTL, 'seconds').humanize(false) // Since moment.js return "a" or "an" for singular values so replacing with 1. - return humanizedDuration.replace(/^(a|an) /, '1 '); + return humanizedDuration.replace(/^(a|an) /, '1 ') } const getAppTypeCategory = (appType: AppType) => { @@ -1116,3 +1123,13 @@ const getAppTypeCategory = (appType: AppType) => { export const getAIAnalyticsEvents = (context: string, appType?: AppType) => `AI_${appType ? `${getAppTypeCategory(appType)}_` : ''}${context}` + +export const findRight = (arr: T[], predicate: (item: T) => boolean): T | null => { + for (let i = arr.length - 1; i >= 0; i--) { + if (predicate(arr[i])) { + return arr[i] + } + } + + return null +} diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index e52a0dac1..7c8c2e2dd 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -1,20 +1,25 @@ -import { ComponentProps, ReactNode } from 'react' +import { ComponentProps } from 'react' import { getAIAnalyticsEvents } from '@Common/Helper' import { Tooltip } from '@Common/Tooltip' -import { AppType } from '@Shared/types' +import { ComponentSizeType } from '@Shared/constants' +import { getAppDetailsURL } from '@Shared/Helpers' +import { Button, ButtonComponentType, ButtonVariantType } from '../Button' +import { DeploymentStatusDetailBreakdown } from '../CICDHistory' +import { DEPLOYMENT_STATUS_TEXT_MAP } from '../DeploymentStatusBreakdown' import { ErrorBar } from '../Error' +import { Icon } from '../Icon' import { ShowMoreText } from '../ShowMoreText' -import { AppStatus, StatusType } from '../StatusComponent' +import { AppStatus, DeploymentStatus, StatusType } from '../StatusComponent' import AppStatusContent from './AppStatusContent' import { APP_STATUS_CUSTOM_MESSAGES } from './constants' -import { AppStatusBodyProps } from './types' +import { AppStatusBodyProps, AppStatusModalTabType, InfoCardItemProps, StatusHeadingContainerProps } from './types' import { getAppStatusMessageFromAppDetails } from './utils' -const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; value: ReactNode; isLast?: boolean }) => ( +const InfoCardItem = ({ heading, value, isLast = false, alignCenter = false }: InfoCardItemProps) => (

{heading}

@@ -33,72 +38,132 @@ const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; val
) +const StatusHeadingContainer = ({ children, type, appId, envId, actionItem }: StatusHeadingContainerProps) => ( +
+ {children} + +
+ {actionItem} + {type === 'release' ? ( +
+
+) + export const AppStatusBody = ({ appDetails, type, handleShowConfigDriftModal, + deploymentStatusDetailsBreakdownData, + selectedTab, debugWithAIButton: ExplainWithAIButton, }: AppStatusBodyProps) => { const appStatus = appDetails.resourceTree?.status?.toUpperCase() || appDetails.appStatus - const message = getAppStatusMessageFromAppDetails(appDetails) - const debugNode = appDetails.resourceTree?.nodes?.find( - (node) => node.kind === 'Deployment' || node.kind === 'Rollout', - ) - const debugObject = `${debugNode?.kind}/${debugNode?.name}` - const customMessage = - type === 'stack-manager' - ? 'The installation will complete when status for all the below resources become HEALTHY.' - : APP_STATUS_CUSTOM_MESSAGES[appStatus] - const infoCardItems: (Omit, 'isLast'> & { id: number })[] = [ - { - id: 1, - heading: type !== 'stack-manager' ? 'Application Status' : 'Status', - value: ( -
- {appStatus ? : '--'} + const getAppStatusInfoCardItems = (): (Omit, 'isLast'> & { id: string })[] => { + const message = getAppStatusMessageFromAppDetails(appDetails) + const customMessage = + type === 'stack-manager' + ? 'The installation will complete when status for all the below resources become HEALTHY.' + : APP_STATUS_CUSTOM_MESSAGES[appStatus] - {ExplainWithAIButton && - appDetails.appStatus?.toLowerCase() !== StatusType.HEALTHY.toLowerCase() && - (debugNode || message) && ( - - )} -
- ), - }, - ...(message - ? [ - { - id: 2, - heading: 'Message', - value: message, - }, - ] - : []), - ...(customMessage - ? [ + const debugNode = appDetails.resourceTree?.nodes?.find( + (node) => node.kind === 'Deployment' || node.kind === 'Rollout', + ) + const debugObject = `${debugNode?.kind}/${debugNode?.name}` + + return [ + { + id: 'app-status-row', + heading: type !== 'stack-manager' ? 'Application Status' : 'Status', + value: ( + + ) : null + } + > + {appStatus ? : '--'} + + ), + }, + ...(message + ? [ + { + id: 'app-status-primary-message', + heading: 'Message', + value: message, + }, + ] + : []), + ...(customMessage + ? [ + { + id: 'app-status-secondary-message', + heading: 'Message', + value: customMessage, + }, + ] + : []), + ] + } + + const infoCardItems: ReturnType = + selectedTab === AppStatusModalTabType.APP_STATUS + ? getAppStatusInfoCardItems() + : [ { - id: 3, - heading: 'Message', - value: customMessage, + id: 'deployment-status-row', + heading: 'Deployment Status', + value: ( + + {deploymentStatusDetailsBreakdownData?.deploymentStatus ? ( + + ) : ( + '--' + )} + + ), }, ] - : []), - ] return (
@@ -111,6 +176,7 @@ export const AppStatusBody = ({ heading={item.heading} value={item.value} isLast={index === infoCardItems.length - 1} + alignCenter={item.heading !== 'Message'} /> ))}
@@ -118,7 +184,16 @@ export const AppStatusBody = ({ - + {selectedTab === AppStatusModalTabType.APP_STATUS ? ( + + ) : ( + + )} ) } diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx index 9d071b89f..a2cd53503 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -5,15 +5,25 @@ import { abortPreviousRequests, getIsRequestAborted } from '@Common/API' import { DISCORD_LINK } from '@Common/Constants' import { Drawer } from '@Common/Drawer' import { GenericEmptyState } from '@Common/EmptyState' -import { stopPropagation, useAsync } from '@Common/Helper' -import { ComponentSizeType } from '@Shared/constants' +import { handleUTCTime, stopPropagation, useAsync } from '@Common/Helper' +import { DeploymentAppTypes, ImageType } from '@Common/Types' +import { + APP_DETAILS_FALLBACK_POLLING_INTERVAL, + ComponentSizeType, + PROGRESSING_DEPLOYMENT_STATUS_POLLING_INTERVAL, +} from '@Shared/constants' +import { AppType } from '@Shared/types' import { APIResponseHandler } from '../APIResponseHandler' import { Button, ButtonComponentType, ButtonStyleType, ButtonVariantType } from '../Button' +import { PROGRESSING_DEPLOYMENT_STATUS } from '../DeploymentStatusBreakdown' import { Icon } from '../Icon' +import { DeploymentStatus } from '../StatusComponent' import { AppStatusBody } from './AppStatusBody' -import { getAppDetails } from './service' -import { AppStatusModalProps } from './types' +import AppStatusModalTabList from './AppStatusModalTabList' +import { getAppDetails, getDeploymentStatusWithTimeline } from './service' +import { AppStatusModalProps, AppStatusModalTabType } from './types' +import { getEmptyViewImageFromHelmDeploymentStatus, getShowDeploymentStatusModal } from './utils' import './AppStatusModal.scss' @@ -22,54 +32,149 @@ const AppStatusModal = ({ handleClose, type, appDetails: appDetailsProp, + processVirtualEnvironmentDeploymentData, + updateDeploymentStatusDetailsBreakdownData, isConfigDriftEnabled, configDriftModal: ConfigDriftModal, appId, envId, + initialTab, debugWithAIButton, }: AppStatusModalProps) => { const [showConfigDriftModal, setShowConfigDriftModal] = useState(false) + const [selectedTab, setSelectedTab] = useState(initialTab || null) - const abortControllerRef = useRef(new AbortController()) - const pollingTimeoutRef = useRef | null>(null) + const appDetailsAbortControllerRef = useRef(new AbortController()) + const appDetailsPollingTimeoutRef = useRef | null>(null) const getAppDetailsWrapper = async () => { const response = await abortPreviousRequests( - () => getAppDetails(appId, envId, abortControllerRef), - abortControllerRef, + () => + getAppDetails({ + appId, + envId, + abortControllerRef: appDetailsAbortControllerRef, + }), + appDetailsAbortControllerRef, ) - return response } + /** + * Fetching logic for app details is we initially call from useAsync then through useEffect initiate polling + * Since the dependency of useAsync is empty array, it will only be called once and then we will call the polling method is triggered based on the polling interval set in the environment variables. + */ const [ areInitialAppDetailsLoading, fetchedAppDetails, fetchedAppDetailsError, reloadInitialAppDetails, setFetchedAppDetails, - ] = useAsync(getAppDetailsWrapper, [appId, envId], type === 'release') + ] = useAsync(getAppDetailsWrapper, [], type === 'release') + + const appDetails = type === 'release' ? fetchedAppDetails : appDetailsProp + + const showDeploymentStatusModal = getShowDeploymentStatusModal({ type, appDetails }) + const deploymentStatusAbortControllerRef = useRef(new AbortController()) + const deploymentStatusPollingTimeoutRef = useRef | null>(null) + + const getDeploymentStatusWrapper = async () => { + const response = await abortPreviousRequests( + () => + getDeploymentStatusWithTimeline({ + abortControllerRef: deploymentStatusAbortControllerRef, + appId: + appDetails.appType === AppType.DEVTRON_HELM_CHART + ? appDetails.installedAppId + : appDetails.appId, + envId: appDetails.environmentId, + showTimeline: + selectedTab === AppStatusModalTabType.DEPLOYMENT_STATUS && + appDetails.deploymentAppType !== DeploymentAppTypes.HELM && + !appDetails.isVirtualEnvironment, + virtualEnvironmentConfig: appDetails.isVirtualEnvironment + ? { + processVirtualEnvironmentDeploymentData, + wfrId: appDetails.resourceTree?.wfrId, + } + : null, + isHelmApp: appDetails.appType === AppType.DEVTRON_HELM_CHART, + }), + deploymentStatusAbortControllerRef, + ) - const handleExternalSync = async () => { - try { - pollingTimeoutRef.current = setTimeout( - async () => { + updateDeploymentStatusDetailsBreakdownData?.(response) + + return response + } + + /** + * Fetching logic for deployment status is we initially call from useAsync then through useEffect initiate polling + * Now on tab switch we need to clear the previous timeout and set a new one reason being tab would have changed and in polling method that would not be reflected since closure is created + * So we re-trigger useAsync to get the new data and set a new timeout + * resetOnChange is there so that user don't see the change in icon in tabs + */ + const [ + isDeploymentTimelineLoading, + deploymentStatusDetailsBreakdownData, + deploymentStatusDetailsBreakdownDataError, + reloadDeploymentStatusDetailsBreakdownData, + setDeploymentStatusDetailsBreakdownData, + ] = useAsync(getDeploymentStatusWrapper, [showDeploymentStatusModal, selectedTab], !!showDeploymentStatusModal, { + resetOnChange: false, + }) + + const handleAppDetailsExternalSync = async () => { + appDetailsPollingTimeoutRef.current = setTimeout( + async () => { + try { const response = await getAppDetailsWrapper() setFetchedAppDetails(response) - // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleExternalSync() - }, - Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || 30000, - ) - } catch { - // Do nothing - } + } catch { + // Do nothing + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleAppDetailsExternalSync() + }, + Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || APP_DETAILS_FALLBACK_POLLING_INTERVAL, + ) + } + + const handleDeploymentStatusExternalSync = async () => { + const isDeploymentInProgress = PROGRESSING_DEPLOYMENT_STATUS.includes( + deploymentStatusDetailsBreakdownData?.deploymentStatus, + ) + + const pollingIntervalFromFlag = + Number( + appDetails.appType !== AppType.DEVTRON_HELM_CHART + ? window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL + : window._env_.HELM_APP_DETAILS_POLLING_INTERVAL, + ) || APP_DETAILS_FALLBACK_POLLING_INTERVAL + + deploymentStatusPollingTimeoutRef.current = setTimeout( + async () => { + try { + const response = await getDeploymentStatusWrapper() + setDeploymentStatusDetailsBreakdownData(response) + } catch { + // Do nothing + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleDeploymentStatusExternalSync() + }, + isDeploymentInProgress ? PROGRESSING_DEPLOYMENT_STATUS_POLLING_INTERVAL : pollingIntervalFromFlag, + ) } const areInitialAppDetailsLoadingWithAbortedError = areInitialAppDetailsLoading || getIsRequestAborted(fetchedAppDetailsError) - const appDetails = type === 'release' ? fetchedAppDetails : appDetailsProp + const isDeploymentStatusLoadingWithAbortedError = + isDeploymentTimelineLoading || getIsRequestAborted(deploymentStatusDetailsBreakdownDataError) + + const isTimelineRequiredAndLoading = + selectedTab === AppStatusModalTabType.DEPLOYMENT_STATUS && isDeploymentStatusLoadingWithAbortedError // Adding useEffect to initiate timer for external sync and clear it on unmount useEffect(() => { @@ -77,20 +182,42 @@ const AppStatusModal = ({ !areInitialAppDetailsLoading && !fetchedAppDetailsError && fetchedAppDetails && - !pollingTimeoutRef.current + !appDetailsPollingTimeoutRef.current ) { // eslint-disable-next-line @typescript-eslint/no-floating-promises - handleExternalSync() + handleAppDetailsExternalSync() } }, [areInitialAppDetailsLoading, fetchedAppDetails, fetchedAppDetailsError]) + useEffect(() => { + if ( + !isDeploymentTimelineLoading && + !deploymentStatusDetailsBreakdownDataError && + deploymentStatusDetailsBreakdownData && + !deploymentStatusPollingTimeoutRef.current + ) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleDeploymentStatusExternalSync() + } + }, [isDeploymentTimelineLoading, deploymentStatusDetailsBreakdownData, deploymentStatusDetailsBreakdownDataError]) + + const handleClearDeploymentStatusTimeout = () => { + if (deploymentStatusPollingTimeoutRef.current) { + clearTimeout(deploymentStatusPollingTimeoutRef.current) + deploymentStatusPollingTimeoutRef.current = null + } + } + useEffect( () => () => { - if (pollingTimeoutRef.current) { - clearTimeout(pollingTimeoutRef.current) + if (appDetailsPollingTimeoutRef.current) { + clearTimeout(appDetailsPollingTimeoutRef.current) } - abortControllerRef.current.abort() + handleClearDeploymentStatusTimeout() + + appDetailsAbortControllerRef.current.abort() + deploymentStatusAbortControllerRef.current.abort() }, [], ) @@ -106,6 +233,11 @@ const AppStatusModal = ({ setShowConfigDriftModal(false) } + const handleSelectTab = async (updatedTab: AppStatusModalTabType) => { + handleClearDeploymentStatusTimeout() + setSelectedTab(updatedTab) + } + if (showConfigDriftModal) { return ( !!segment) + const getEmptyStateMessage = () => { + if (!selectedTab) { + return 'Status is not available' + } + + if (selectedTab === AppStatusModalTabType.APP_STATUS) { + if (!appDetails?.resourceTree) { + return 'Application status is not available' + } + return '' + } + + if (!deploymentStatusDetailsBreakdownData || !getShowDeploymentStatusModal({ type, appDetails })) { + return 'Deployment status is not available' + } + + return '' + } + const renderContent = () => { - if (!appDetails?.resourceTree) { - return + const emptyStateMessage = getEmptyStateMessage() + + if (emptyStateMessage) { + return + } + + // Empty states for helm based deployment status + if ( + selectedTab === AppStatusModalTabType.DEPLOYMENT_STATUS && + appDetails.deploymentAppType === DeploymentAppTypes.HELM + ) { + return ( + + Deployment status: + + + } + subTitle={`Triggered at ${handleUTCTime(deploymentStatusDetailsBreakdownData.deploymentTriggerTime)} by ${deploymentStatusDetailsBreakdownData.triggeredBy}`} + imageType={ImageType.Large} + /> + ) } return ( @@ -129,6 +304,8 @@ const AppStatusModal = ({ appDetails={appDetails} type={type} handleShowConfigDriftModal={handleShowConfigDriftModal} + deploymentStatusDetailsBreakdownData={deploymentStatusDetailsBreakdownData} + selectedTab={selectedTab} debugWithAIButton={debugWithAIButton} /> @@ -153,15 +330,23 @@ const AppStatusModal = ({ ) } + const timelineError = + selectedTab === AppStatusModalTabType.DEPLOYMENT_STATUS ? deploymentStatusDetailsBreakdownDataError : null + + const bodyErrorData = fetchedAppDetailsError || timelineError + const bodyErrorReload = fetchedAppDetailsError + ? reloadInitialAppDetails + : reloadDeploymentStatusDetailsBreakdownData + return (
-
-
-

+
+
+

{filteredTitleSegments.map((segment, index) => ( {segment} @@ -181,18 +366,28 @@ const AppStatusModal = ({ onClick={handleClose} />

+ + {!areInitialAppDetailsLoadingWithAbortedError && !fetchedAppDetailsError && !!appDetails && ( + + )}
{renderContent()} diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.scss b/src/Shared/Components/AppStatusModal/AppStatusModal.scss index 101f4c8a0..5dc8ee9ef 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.scss +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.scss @@ -2,4 +2,10 @@ .info-card-item { grid-template-columns: 140px 1fr; } + + &__header { + &:not(:has([role="tablist"])) { + padding-bottom: 12px; + } + } } \ No newline at end of file diff --git a/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx b/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx new file mode 100644 index 000000000..f50950417 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/AppStatusModalTabList.tsx @@ -0,0 +1,93 @@ +import { useEffect } from 'react' + +import { AppStatus, DeploymentStatus } from '../StatusComponent' +import { TabGroup, TabProps } from '../TabGroup' +import { AppStatusModalTabListProps, AppStatusModalTabType } from './types' +import { getShowDeploymentStatusModal } from './utils' + +const AppStatusModalTabList = ({ + handleSelectTab, + appDetails, + type, + selectedTab, + deploymentStatusDetailsBreakdownData, +}: AppStatusModalTabListProps) => { + const showDeploymentStatusModal = + selectedTab === AppStatusModalTabType.DEPLOYMENT_STATUS || + getShowDeploymentStatusModal({ + type, + appDetails, + }) + + const showApplicationStatus = selectedTab === AppStatusModalTabType.APP_STATUS || !!appDetails?.resourceTree + + const handleSelectAppStatus = () => { + handleSelectTab(AppStatusModalTabType.APP_STATUS) + } + + const handleSelectDeploymentStatusTab = () => { + handleSelectTab(AppStatusModalTabType.DEPLOYMENT_STATUS) + } + + const tabGroups: TabProps[] = [ + ...(showApplicationStatus + ? [ + { + id: AppStatusModalTabType.APP_STATUS, + label: 'Application Status', + tabType: 'button', + props: { + onClick: handleSelectAppStatus, + 'data-testid': 'app-status-tab', + }, + active: selectedTab === AppStatusModalTabType.APP_STATUS, + iconElement: ( + + ), + } satisfies TabProps, + ] + : []), + ...(showDeploymentStatusModal + ? [ + { + id: AppStatusModalTabType.DEPLOYMENT_STATUS, + label: 'Deployment Status', + tabType: 'button', + props: { + onClick: handleSelectDeploymentStatusTab, + 'data-testid': 'deployment-status-tab', + }, + active: selectedTab === AppStatusModalTabType.DEPLOYMENT_STATUS, + iconElement: ( + + ), + } satisfies TabProps, + ] + : []), + ] + + // Could have achieved via onDataLoad but, have done this through useEffect to avoid abrupt shift in case some tabs went missing after polling + useEffect(() => { + if (tabGroups.length && !selectedTab) { + handleSelectTab(tabGroups[0].id as AppStatusModalTabType) + } + }, []) + + if (tabGroups.length <= 1) { + return null + } + + return +} + +export default AppStatusModalTabList diff --git a/src/Shared/Components/AppStatusModal/index.ts b/src/Shared/Components/AppStatusModal/index.ts index 9e35374c8..117f02af6 100644 --- a/src/Shared/Components/AppStatusModal/index.ts +++ b/src/Shared/Components/AppStatusModal/index.ts @@ -1,2 +1,3 @@ export { default as AppStatusContent } from './AppStatusContent' export { default as AppStatusModal } from './AppStatusModal.component' +export { AppStatusModalTabType } from './types' diff --git a/src/Shared/Components/AppStatusModal/service.ts b/src/Shared/Components/AppStatusModal/service.ts index d92addf44..e862199d3 100644 --- a/src/Shared/Components/AppStatusModal/service.ts +++ b/src/Shared/Components/AppStatusModal/service.ts @@ -1,38 +1,72 @@ -import { get, getIsRequestAborted } from '@Common/API' +import { get } from '@Common/API' import { ROUTES } from '@Common/Constants' -import { getUrlWithSearchParams, showError } from '@Common/Helper' -import { APIOptions } from '@Common/Types' -import { AppDetails, AppType } from '@Shared/types' - -export const getAppDetails = async ( - appId: number, - envId: number, - abortControllerRef: APIOptions['abortControllerRef'], -): Promise => { - try { - const queryParams = getUrlWithSearchParams('', { - 'app-id': appId, - 'env-id': envId, - }) - - const [appDetails, resourceTree] = await Promise.all([ - get(`${ROUTES.APP_DETAIL}/v2${queryParams}`, { - abortControllerRef, - }), - get(`${ROUTES.APP_DETAIL}/resource-tree${queryParams}`, { - abortControllerRef, - }), - ]) - - return { - ...(appDetails.result || {}), - resourceTree: resourceTree.result, - appType: AppType.DEVTRON_APP, - } - } catch (error) { - if (!getIsRequestAborted(error)) { - showError(error) - } - throw error +import { getUrlWithSearchParams } from '@Common/Helper' +import { + AppDetails, + AppType, + DeploymentStatusDetailsBreakdownDataType, + DeploymentStatusDetailsType, +} from '@Shared/types' + +import { processDeploymentStatusDetailsData } from '../DeploymentStatusBreakdown' +import { GetAppDetailsParamsType, GetDeploymentStatusWithTimelineParamsType } from './types' + +export const getAppDetails = async ({ + appId, + envId, + abortControllerRef, +}: GetAppDetailsParamsType): Promise => { + const queryParams = getUrlWithSearchParams('', { + 'app-id': appId, + 'env-id': envId, + }) + + const [appDetailsResponse, resourceTreeResponse] = await Promise.allSettled([ + get>(`${ROUTES.APP_DETAIL}/v2${queryParams}`, { + abortControllerRef, + }), + get(`${ROUTES.APP_DETAIL}/resource-tree${queryParams}`, { + abortControllerRef, + }), + ]) + + if (appDetailsResponse.status === 'rejected') { + throw appDetailsResponse.reason + } + + const appDetails = appDetailsResponse.value + const resourceTree = resourceTreeResponse.status === 'fulfilled' ? resourceTreeResponse.value : null + + return { + ...(appDetails.result || ({} as AppDetails)), + resourceTree: resourceTree?.result, + appType: AppType.DEVTRON_APP, } } + +export const getDeploymentStatusWithTimeline = async ({ + abortControllerRef, + appId, + envId, + showTimeline, + virtualEnvironmentConfig, + isHelmApp, +}: GetDeploymentStatusWithTimelineParamsType): Promise => { + const baseURL = isHelmApp ? ROUTES.HELM_DEPLOYMENT_STATUS_TIMELINE_INSTALLED_APP : ROUTES.DEPLOYMENT_STATUS + + const deploymentStatusDetailsResponse = await get( + getUrlWithSearchParams(`${baseURL}/${appId}/${envId}`, { + showTimeline, + ...(virtualEnvironmentConfig && { + wfrId: virtualEnvironmentConfig.wfrId, + }), + }), + { + abortControllerRef, + }, + ) + + return virtualEnvironmentConfig + ? virtualEnvironmentConfig.processVirtualEnvironmentDeploymentData(deploymentStatusDetailsResponse.result) + : processDeploymentStatusDetailsData(deploymentStatusDetailsResponse.result) +} diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index 4c1194fa0..6b86ada85 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -1,27 +1,42 @@ -import { FunctionComponent } from 'react' +import { FunctionComponent, PropsWithChildren, ReactNode } from 'react' -import { AppDetails, ConfigDriftModalProps, IntelligenceConfig } from '@Shared/types' +import { APIOptions } from '@Common/Types' +import { + AppDetails, + ConfigDriftModalProps, + DeploymentStatusDetailsBreakdownDataType, + DeploymentStatusDetailsType, + IntelligenceConfig, +} from '@Shared/types' + +export enum AppStatusModalTabType { + APP_STATUS = 'appStatus', + DEPLOYMENT_STATUS = 'deploymentStatus', +} export type AppStatusModalProps = { titleSegments: string[] handleClose: () => void - /** - * If given would not poll for app details and resource tree, Polling for gitops timeline would still be done - */ - appDetails?: AppDetails - isConfigDriftEnabled: boolean configDriftModal: FunctionComponent - + processVirtualEnvironmentDeploymentData: ( + data?: DeploymentStatusDetailsType, + ) => DeploymentStatusDetailsBreakdownDataType debugWithAIButton: FunctionComponent<{ intelligenceConfig: IntelligenceConfig }> } & ( | { type: 'release' appId: number envId: number + appDetails?: never + initialTab?: never + updateDeploymentStatusDetailsBreakdownData?: never } | { type: 'devtron-app' | 'other-apps' | 'stack-manager' + appDetails: AppDetails + initialTab: AppStatusModalTabType + updateDeploymentStatusDetailsBreakdownData: (data: DeploymentStatusDetailsBreakdownDataType) => void appId?: never envId?: never } @@ -30,6 +45,8 @@ export type AppStatusModalProps = { export interface AppStatusBodyProps extends Required> { handleShowConfigDriftModal: () => void + selectedTab: AppStatusModalTabType + deploymentStatusDetailsBreakdownData: DeploymentStatusDetailsBreakdownDataType } export interface AppStatusContentProps @@ -47,3 +64,44 @@ export interface AppStatusContentProps export interface GetFilteredFlattenedNodesFromAppDetailsParamsType extends Pick {} + +/** + * Params for getAppDetails which is called in case of release [i.e, devtron apps] + */ +export interface GetAppDetailsParamsType extends Pick { + appId: number + envId: number +} + +export type GetDeploymentStatusWithTimelineParamsType = Pick & { + /** + * Incase of helm apps this is installed app id + */ + appId: number + envId: number + showTimeline: boolean + virtualEnvironmentConfig?: { + processVirtualEnvironmentDeploymentData: AppStatusModalProps['processVirtualEnvironmentDeploymentData'] + wfrId: AppDetails['resourceTree']['wfrId'] + } + isHelmApp?: boolean +} + +export interface AppStatusModalTabListProps extends Pick { + handleSelectTab: (updatedTab: AppStatusModalTabType) => void + selectedTab: AppStatusModalTabType + deploymentStatusDetailsBreakdownData: DeploymentStatusDetailsBreakdownDataType +} + +export interface StatusHeadingContainerProps extends PropsWithChildren> { + appId: number + envId?: number + actionItem?: ReactNode +} + +export interface InfoCardItemProps { + heading: string + value: ReactNode + isLast?: boolean + alignCenter?: boolean +} diff --git a/src/Shared/Components/AppStatusModal/utils.tsx b/src/Shared/Components/AppStatusModal/utils.tsx index 08bc992d5..f42b2bd8f 100644 --- a/src/Shared/Components/AppStatusModal/utils.tsx +++ b/src/Shared/Components/AppStatusModal/utils.tsx @@ -1,9 +1,23 @@ +import ICCelebration from '@Images/ic-celebration.svg' +import ICManOnRocket from '@Images/ic-man-on-rocket.svg' +import ICPageNotFound from '@Images/ic-page-not-found.svg' +import NoDeploymentStatusImage from '@Images/no-artifact.webp' +import { DeploymentAppTypes, GenericEmptyStateType } from '@Common/Types' import { DEPLOYMENT_STATUS } from '@Shared/constants' import { aggregateNodes } from '@Shared/Helpers' -import { AppDetails, Node } from '@Shared/types' +import { AppDetails, AppType, DeploymentStatusDetailsBreakdownDataType, Node } from '@Shared/types' +import { ReleaseMode } from '@Pages/index' import { AggregatedNodes, STATUS_SORTING_ORDER } from '../CICDHistory' -import { GetFilteredFlattenedNodesFromAppDetailsParamsType as GetFlattenedNodesFromAppDetailsParamsType } from './types' +import { + FAILED_DEPLOYMENT_STATUS, + PROGRESSING_DEPLOYMENT_STATUS, + SUCCESSFUL_DEPLOYMENT_STATUS, +} from '../DeploymentStatusBreakdown' +import { + AppStatusModalProps, + GetFilteredFlattenedNodesFromAppDetailsParamsType as GetFlattenedNodesFromAppDetailsParamsType, +} from './types' export const getAppStatusMessageFromAppDetails = (appDetails: AppDetails): string => { if (!appDetails?.resourceTree) { @@ -65,3 +79,40 @@ export const getFlattenedNodesFromAppDetails = ({ } export const getResourceKey = (nodeDetails: Node) => `${nodeDetails.kind}/${nodeDetails.name}` + +export const getShowDeploymentStatusModal = ({ + type, + appDetails, +}: Pick): boolean => { + if ( + !appDetails || + type === 'stack-manager' || + (appDetails.appType !== AppType.DEVTRON_APP && appDetails.appType !== AppType.DEVTRON_HELM_CHART) + ) { + return false + } + + if (appDetails.appType === AppType.DEVTRON_HELM_CHART) { + return !!appDetails.lastDeployedTime && appDetails.deploymentAppType !== DeploymentAppTypes.HELM + } + + return appDetails.releaseMode !== ReleaseMode.MIGRATE_EXTERNAL_APPS || appDetails.isPipelineTriggered +} + +export const getEmptyViewImageFromHelmDeploymentStatus = ( + status: DeploymentStatusDetailsBreakdownDataType['deploymentStatus'], +): GenericEmptyStateType['image'] => { + if (PROGRESSING_DEPLOYMENT_STATUS.includes(status)) { + return ICManOnRocket + } + + if (SUCCESSFUL_DEPLOYMENT_STATUS.includes(status)) { + return ICCelebration + } + + if (FAILED_DEPLOYMENT_STATUS.includes(status)) { + return ICPageNotFound + } + + return NoDeploymentStatusImage +} diff --git a/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx b/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx index 115cab8cf..8714dff54 100644 --- a/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentDetailSteps.tsx @@ -17,20 +17,20 @@ import { useEffect, useRef, useState } from 'react' import { useHistory, useParams, useRouteMatch } from 'react-router-dom' +import { IndexStore } from '@Shared/Store' +import { DeploymentStatusDetailsBreakdownDataType, DeploymentStatusDetailsType, TIMELINE_STATUS } from '@Shared/types' + import { ReactComponent as Arrow } from '../../../Assets/Icon/ic-arrow-forward.svg' import mechanicalOperation from '../../../Assets/Icon/ic-mechanical-operation.svg' import { DeploymentAppTypes, GenericEmptyState, Progressing, URLS } from '../../../Common' -import { DEPLOYMENT_STATUS, EMPTY_STATE_STATUS, TIMELINE_STATUS } from '../../constants' -import { getHandleOpenURL, getIsApprovalPolicyConfigured, processDeploymentStatusDetailsData } from '../../Helpers' +import { DEPLOYMENT_STATUS, EMPTY_STATE_STATUS } from '../../constants' +import { getHandleOpenURL, getIsApprovalPolicyConfigured } from '../../Helpers' +import { processDeploymentStatusDetailsData } from '../DeploymentStatusBreakdown' import CDEmptyState from './CDEmptyState' import { DEPLOYMENT_STATUS_QUERY_PARAM } from './constants' import DeploymentStatusDetailBreakdown from './DeploymentStatusBreakdown' import { getDeploymentStatusDetail } from './service' -import { - DeploymentDetailStepsType, - DeploymentStatusDetailsBreakdownDataType, - DeploymentStatusDetailsType, -} from './types' +import { DeploymentDetailStepsType } from './types' let deploymentStatusTimer = null const DeploymentDetailSteps = ({ @@ -51,6 +51,10 @@ const DeploymentDetailSteps = ({ const [deploymentListLoader, setDeploymentListLoader] = useState( deploymentStatus?.toUpperCase() !== TIMELINE_STATUS.ABORTED, ) + /** + * Only present for helm apps history + */ + const appDetails = IndexStore.getAppDetails() const isVirtualEnv = useRef(isVirtualEnvironment) const isDeploymentWithoutApprovalRef = useRef(isDeploymentWithoutApproval) @@ -165,6 +169,8 @@ const DeploymentDetailSteps = ({
) diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss new file mode 100644 index 000000000..0c33b2fca --- /dev/null +++ b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.scss @@ -0,0 +1,63 @@ +// Have moved the css from AppDetails can refactor later +.deployment-status-breakdown-container, +.deployment-approval-container { + .vertical-connector { + border-left: 1px solid var(--N300); + height: 15px; + position: relative; + left: 18px; + width: 5px; + } + .deployment-status-breakdown-row { + display: flex; + align-items: center; + justify-content: left; + + &.border-collapse { + border-radius: 4px 4px 0 0; + } + } + + .pulse-highlight { + width: 12px; + height: 12px; + border: solid 4px var(--O200); + background-color: var(--O500); + position: relative; + top: 5px; + right: -5px; + border-radius: 50%; + animation-name: pulse; + animation-duration: 2s; + animation-iteration-count: infinite; + animation-timing-function: linear; + } + + .green-tick { + path { + stroke: var(--G500); + } + } + + .app-status-row { + display: grid; + grid-template-columns: 150px 200px 150px auto; + grid-column-gap: 16px; + } + .resource-list { + .app-status-row { + &:hover { + background-color: var(--bg-secondary); + } + } + } + + .detail-tab_border { + border-radius: 0 0 4px 4px; + border-top: 0; + } +} + +.deployment-approval-container + .deployment-status-breakdown-container { + padding-top: 0px; +} diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx index 6172292a7..5ba7e5b59 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusBreakdown.tsx @@ -14,98 +14,94 @@ * limitations under the License. */ -import { useRouteMatch } from 'react-router-dom' +import { Fragment } from 'react' -import { URLS } from '../../../Common' -import { TIMELINE_STATUS } from '../../constants' -import { IndexStore } from '../../Store' -import ErrorBar from '../Error/ErrorBar' +import { TIMELINE_STATUS } from '@Shared/types' + +import { InfoBlock } from '../InfoBlock' import { DeploymentStatusDetailRow } from './DeploymentStatusDetailRow' -import { ErrorInfoStatusBar } from './ErrorInfoStatusBar' -import { DeploymentStatusDetailBreakdownType } from './types' +import { DeploymentStatusDetailBreakdownType, DeploymentStatusDetailRowType } from './types' + +import './DeploymentStatusBreakdown.scss' const DeploymentStatusDetailBreakdown = ({ deploymentStatusDetailsBreakdownData, isVirtualEnvironment, + appDetails, + rootClassName = '', }: DeploymentStatusDetailBreakdownType) => { - const _appDetails = IndexStore.getAppDetails() - const { url } = useRouteMatch() const isHelmManifestPushed = deploymentStatusDetailsBreakdownData.deploymentStatusBreakdown[ TIMELINE_STATUS.HELM_MANIFEST_PUSHED_TO_HELM_REPO ]?.showHelmManifest - return ( - <> - {!url.includes(`/${URLS.CD_DETAILS}`) && } -
- - {!( - isVirtualEnvironment && - deploymentStatusDetailsBreakdownData.deploymentStatusBreakdown[ - TIMELINE_STATUS.HELM_PACKAGE_GENERATED - ] - ) ? ( - <> - - - - + const deploymentStatusDetailRowProps: Pick = + { + appDetails, + deploymentDetailedData: deploymentStatusDetailsBreakdownData, + } - - + return ( +
+ + {!( + isVirtualEnvironment && + deploymentStatusDetailsBreakdownData.deploymentStatusBreakdown[TIMELINE_STATUS.HELM_PACKAGE_GENERATED] + ) ? ( + <> + {( + [ + TIMELINE_STATUS.GIT_COMMIT, + TIMELINE_STATUS.ARGOCD_SYNC, + TIMELINE_STATUS.KUBECTL_APPLY, + ] as DeploymentStatusDetailRowType['type'][] + ).map((timelineStatus) => ( + + {deploymentStatusDetailsBreakdownData.errorBarConfig?.nextTimelineToProcess === + timelineStatus && ( + <> + +
+ + )} + + + ))} + + + ) : ( + <> + + {isHelmManifestPushed && ( - - ) : ( - <> - - {isHelmManifestPushed && ( - - )} - - )} -
- + )} + + )} +
) } diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx index 6398dea88..0b23c111d 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx @@ -20,122 +20,125 @@ import { useParams } from 'react-router-dom' import moment from 'moment' import { ShowMoreText } from '@Shared/Components/ShowMoreText' -import { IndexStore } from '@Shared/Store' +import { AppType, TIMELINE_STATUS } from '@Shared/types' import { ReactComponent as DropDownIcon } from '../../../Assets/Icon/ic-chevron-down.svg' import { DATE_TIME_FORMATS, showError } from '../../../Common' -import { DEPLOYMENT_STATUS, statusIcon, TIMELINE_STATUS } from '../../constants' +import { ComponentSizeType, DEPLOYMENT_STATUS } from '../../constants' import { AppStatusContent } from '../AppStatusModal' +import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { APP_HEALTH_DROP_DOWN_LIST, MANIFEST_STATUS_HEADERS, TERMINAL_STATUS_MAP } from './constants' -import { ErrorInfoStatusBar } from './ErrorInfoStatusBar' import { getManualSync } from './service' import { DeploymentStatusDetailRowType } from './types' -import { renderIcon } from './utils' +import { getDeploymentTimelineBGColorFromIcon, renderDeploymentTimelineIcon } from './utils' export const DeploymentStatusDetailRow = ({ type, hideVerticalConnector, deploymentDetailedData, + appDetails, }: DeploymentStatusDetailRowType) => { - const { appId, envId } = useParams<{ appId: string; envId: string }>() - const statusBreakDownType = deploymentDetailedData.deploymentStatusBreakdown[type] - const [collapsed, toggleCollapsed] = useState(statusBreakDownType.isCollapsed) + // Won't be available in release, but appDetails will be available in the component in that case + // Can't use appDetails directly as in case of deployment history, appDetails will be null + const { appId: paramAppId, envId: paramEnvId } = useParams<{ appId: string; envId: string }>() - const isHelmManifestPushFailed = - type === TIMELINE_STATUS.HELM_MANIFEST_PUSHED_TO_HELM_REPO && - deploymentDetailedData.deploymentStatus === statusIcon.failed + const [isManualSyncLoading, setIsManualSyncLoading] = useState(false) - const appDetails = IndexStore.getAppDetails() + const statusBreakDownType = deploymentDetailedData.deploymentStatusBreakdown[type] + const [isCollapsed, setIsCollapsed] = useState(statusBreakDownType.isCollapsed) useEffect(() => { - toggleCollapsed(statusBreakDownType.isCollapsed) + setIsCollapsed(statusBreakDownType.isCollapsed) }, [statusBreakDownType.isCollapsed]) - async function manualSyncData() { + const manualSyncData = async () => { try { + setIsManualSyncLoading(true) + const { appId: appDetailsAppId, appType, environmentId: appDetailsEnvId, installedAppId } = appDetails || {} + const parsedAppIdFromAppDetails = appType === AppType.DEVTRON_HELM_CHART ? installedAppId : appDetailsAppId + + const appId = paramAppId || String(parsedAppIdFromAppDetails) + const envId = paramEnvId || String(appDetailsEnvId) + await getManualSync({ appId, envId }) } catch (error) { showError(error) + } finally { + setIsManualSyncLoading(false) } } const toggleDropdown = () => { - toggleCollapsed(!collapsed) + setIsCollapsed((prevState) => !prevState) } - const renderDetailedData = () => - !collapsed ? ( -
- {statusBreakDownType.timelineStatus && ( -
- {deploymentDetailedData.deploymentStatusBreakdown[type].timelineStatus} - {(deploymentDetailedData.deploymentStatus === DEPLOYMENT_STATUS.TIMED_OUT || - deploymentDetailedData.deploymentStatus === DEPLOYMENT_STATUS.UNABLE_TO_FETCH) && ( - - Try now - - )} + const renderDetailedData = () => { + if (type !== TIMELINE_STATUS.KUBECTL_APPLY) { + return null + } + + return ( +
+ {statusBreakDownType.subSteps?.map((items, index) => ( + // eslint-disable-next-line react/no-array-index-key +
+
+ {renderDeploymentTimelineIcon(items.icon)} + {items.message} +
- )} - {type === TIMELINE_STATUS.KUBECTL_APPLY && ( -
-
- {deploymentDetailedData.deploymentStatusBreakdown[ - TIMELINE_STATUS.KUBECTL_APPLY - ].kubeList?.map((items, index) => ( + ))} + {statusBreakDownType.resourceDetails?.length ? ( +
+
+ {MANIFEST_STATUS_HEADERS.map((headerKey, index) => ( // eslint-disable-next-line react/no-array-index-key -
- {renderIcon(items.icon)} - {items.message} +
+ {headerKey}
))}
- {statusBreakDownType.resourceDetails?.length ? ( -
-
- {MANIFEST_STATUS_HEADERS.map((headerKey, index) => ( - // eslint-disable-next-line react/no-array-index-key -
- {headerKey} -
- ))} -
-
- {statusBreakDownType.resourceDetails.map((nodeDetails) => ( -
-
{nodeDetails.resourceKind}
-
{nodeDetails.resourceName}
-
- {nodeDetails.resourceStatus} -
- -
- ))} +
+ {statusBreakDownType.resourceDetails.map((nodeDetails) => ( +
+
{nodeDetails.resourceKind}
+
{nodeDetails.resourceName}
+
+ {nodeDetails.resourceStatus} +
+
-
- ) : null} + ))} +
- )} + ) : null}
- ) : null + ) + } + + const isAccordion = + !!statusBreakDownType.subSteps?.length || + (type === TIMELINE_STATUS.APP_HEALTH && APP_HEALTH_DROP_DOWN_LIST.includes(statusBreakDownType.icon)) || + ((type === TIMELINE_STATUS.GIT_COMMIT || type === TIMELINE_STATUS.ARGOCD_SYNC) && + statusBreakDownType.icon === 'failed') + + const renderAccordionDetails = () => { + if (isCollapsed) { + return null + } - const renderDetailChart = () => - !collapsed && ( + return (
{statusBreakDownType.timelineStatus && (
{statusBreakDownType.timelineStatus} + {(deploymentDetailedData.deploymentStatus === DEPLOYMENT_STATUS.TIMED_OUT || deploymentDetailedData.deploymentStatus === DEPLOYMENT_STATUS.UNABLE_TO_FETCH) && ( - - Try now - +
)} -
- -
+ + {type === TIMELINE_STATUS.APP_HEALTH ? ( +
+ +
+ ) : ( + renderDetailedData() + )}
) - - const renderErrorInfoBar = () => ( - - ) + } return ( <>
- {renderIcon(statusBreakDownType.icon)} - - - {statusBreakDownType.displayText} +
+ {renderDeploymentTimelineIcon(statusBreakDownType.icon)} + + + {statusBreakDownType.displayText} + + {statusBreakDownType.displaySubText && ( + + {statusBreakDownType.displaySubText} + + )} - {statusBreakDownType.displaySubText && ( + + {statusBreakDownType.time !== '' && statusBreakDownType.icon !== 'inprogress' && ( - {statusBreakDownType.displaySubText} + {moment(statusBreakDownType.time, 'YYYY-MM-DDTHH:mm:ssZ').format( + DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT, + )} )} - - - {statusBreakDownType.time !== '' && statusBreakDownType.icon !== 'inprogress' && ( - - {moment(statusBreakDownType.time, 'YYYY-MM-DDTHH:mm:ssZ').format( - DATE_TIME_FORMATS.TWELVE_HOURS_FORMAT, - )} - - )} - {((type === TIMELINE_STATUS.KUBECTL_APPLY && statusBreakDownType.kubeList?.length) || - (type === TIMELINE_STATUS.APP_HEALTH && - APP_HEALTH_DROP_DOWN_LIST.includes(statusBreakDownType.icon)) || - ((type === TIMELINE_STATUS.GIT_COMMIT || type === TIMELINE_STATUS.ARGOCD_SYNC) && - statusBreakDownType.icon === 'failed')) && ( - + {isAccordion && ( +
- {isHelmManifestPushFailed && renderErrorInfoBar()}
- {type === TIMELINE_STATUS.GIT_COMMIT && renderDetailedData()} - {type === TIMELINE_STATUS.ARGOCD_SYNC && renderDetailedData()} - {type === TIMELINE_STATUS.KUBECTL_APPLY && renderDetailedData()} - {type === TIMELINE_STATUS.APP_HEALTH && renderDetailChart()} + {renderAccordionDetails()} {!hideVerticalConnector &&
} ) diff --git a/src/Shared/Components/CICDHistory/ErrorInfoStatusBar.tsx b/src/Shared/Components/CICDHistory/ErrorInfoStatusBar.tsx deleted file mode 100644 index 6a7094766..000000000 --- a/src/Shared/Components/CICDHistory/ErrorInfoStatusBar.tsx +++ /dev/null @@ -1,46 +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 { ReactComponent as Error } from '../../../Assets/Icon/ic-error-exclamation.svg' -import { TIMELINE_STATUS } from '../../constants' -import { ErrorInfoStatusBarType } from './types' - -export const ErrorInfoStatusBar = ({ - nonDeploymentError, - type, - errorMessage, - hideVerticalConnector, - hideErrorIcon, -}: ErrorInfoStatusBarType) => - nonDeploymentError === type ? ( - <> -
- {!hideErrorIcon && } - {errorMessage} - {type === TIMELINE_STATUS.HELM_MANIFEST_PUSHED_TO_HELM_REPO && ( -
    -
  1. Ensure provided repository path is valid
  2. -
  3. Check if credentials provided for OCI registry are valid and have PUSH permission
  4. -
- )} -
- {!hideVerticalConnector &&
} - - ) : null diff --git a/src/Shared/Components/CICDHistory/LogStageAccordion.tsx b/src/Shared/Components/CICDHistory/LogStageAccordion.tsx index aa21f4c34..cfb9577d9 100644 --- a/src/Shared/Components/CICDHistory/LogStageAccordion.tsx +++ b/src/Shared/Components/CICDHistory/LogStageAccordion.tsx @@ -43,6 +43,7 @@ const LogStageAccordion = ({ fullScreenView, searchIndex, targetPlatforms, + logsRendererRef, }: LogStageAccordionProps) => { const handleAccordionToggle = () => { if (isOpen) { @@ -78,6 +79,8 @@ const LogStageAccordion = ({ } } + const getLogsRendererReference = () => logsRendererRef.current + return (