From dd454fe8e943d7ccf683e94bbdeb67d08742eefd Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 16 Apr 2025 11:05:34 +0530 Subject: [PATCH 01/33] feat: add base AppStatusModal --- .../AppStatusModal.component.tsx | 36 +++++++++++++++++++ src/Shared/Components/AppStatusModal/index.ts | 1 + src/Shared/Components/AppStatusModal/types.ts | 6 ++++ src/Shared/Components/index.ts | 1 + 4 files changed, 44 insertions(+) create mode 100644 src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx create mode 100644 src/Shared/Components/AppStatusModal/index.ts create mode 100644 src/Shared/Components/AppStatusModal/types.ts diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx new file mode 100644 index 000000000..ad5a985cb --- /dev/null +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -0,0 +1,36 @@ +import { Drawer, stopPropagation } from '@Common/index' +import { ComponentSizeType } from '@Shared/constants' + +import { Button, ButtonStyleType, ButtonVariantType } from '../Button' +import { Icon } from '../Icon' +import { AppStatusModalProps } from './types' + +const AppStatusModal = ({ title, handleClose }: AppStatusModalProps) => ( + +
+
+
+ {title} + +
+
+ +
+
+ +) + +export default AppStatusModal diff --git a/src/Shared/Components/AppStatusModal/index.ts b/src/Shared/Components/AppStatusModal/index.ts new file mode 100644 index 000000000..3babb3c2f --- /dev/null +++ b/src/Shared/Components/AppStatusModal/index.ts @@ -0,0 +1 @@ +export { default as AppStatusModal } from './AppStatusModal.component' diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts new file mode 100644 index 000000000..e6dc47634 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react' + +export interface AppStatusModalProps { + title: ReactNode + handleClose: () => void +} diff --git a/src/Shared/Components/index.ts b/src/Shared/Components/index.ts index 673dbe2ca..21e127d74 100644 --- a/src/Shared/Components/index.ts +++ b/src/Shared/Components/index.ts @@ -20,6 +20,7 @@ export * from './AnimatedDeployButton' export * from './AnimatedTimer' export * from './AnnouncementBanner' export * from './APIResponseHandler' +export * from './AppStatusModal' export * from './ArtifactInfoModal' export * from './Backdrop' export * from './BulkOperations' From 7218d589fc019b6413a18ee5a6e4b937b9ea068e Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 16 Apr 2025 13:53:53 +0530 Subject: [PATCH 02/33] feat: implement AppStatusBody component and integrate into AppStatusModal --- .../AppStatusModal/AppStatusBody.tsx | 60 +++++++++++++++++++ .../AppStatusModal.component.tsx | 7 ++- .../Components/AppStatusModal/constants.ts | 5 ++ src/Shared/Components/AppStatusModal/types.ts | 11 ++++ src/Shared/Components/AppStatusModal/utils.ts | 31 ++++++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 src/Shared/Components/AppStatusModal/AppStatusBody.tsx create mode 100644 src/Shared/Components/AppStatusModal/constants.ts create mode 100644 src/Shared/Components/AppStatusModal/utils.ts diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx new file mode 100644 index 000000000..95a17a9a5 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -0,0 +1,60 @@ +import { ComponentProps, ReactNode, useMemo } from 'react' + +import { Tooltip } from '@Common/Tooltip' + +import { ShowMoreText } from '../ShowMoreText' +import { AppStatus } from '../StatusComponent' +import { APP_STATUS_CUSTOM_MESSAGES } from './constants' +import { AppStatusModalProps } from './types' +import { getAppStatusMessageFromAppDetails } from './utils' + +const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; value: ReactNode; isLast?: boolean }) => ( +
+ +

{heading}

+
+ + {typeof value === 'string' ? : value} +
+) + +export const AppStatusBody = ({ appDetails, type }: Pick) => { + const message = useMemo(() => getAppStatusMessageFromAppDetails(appDetails), [appDetails]) + const customMessage = APP_STATUS_CUSTOM_MESSAGES[appDetails.resourceTree?.status?.toUpperCase()] + + const infoCardItems: (Omit, 'isLast'> & { id: number })[] = [ + { + id: 1, + heading: type !== 'stack-manager' ? 'Application Status' : 'Status', + value: , + }, + message && { + id: 2, + heading: 'Message', + value: message, + }, + customMessage && { + id: 3, + heading: 'Message', + value: customMessage, + }, + ] + + return ( +
+ {/* Info card */} +
+ {infoCardItems.map((item, index) => ( + + ))} +
+
+ ) +} diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx index ad5a985cb..158d68310 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -3,9 +3,10 @@ import { ComponentSizeType } from '@Shared/constants' import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { Icon } from '../Icon' +import { AppStatusBody } from './AppStatusBody' import { AppStatusModalProps } from './types' -const AppStatusModal = ({ title, handleClose }: AppStatusModalProps) => ( +const AppStatusModal = ({ title, handleClose, type, appDetails }: AppStatusModalProps) => (
(
-
+
+ +
) diff --git a/src/Shared/Components/AppStatusModal/constants.ts b/src/Shared/Components/AppStatusModal/constants.ts new file mode 100644 index 000000000..78121b126 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/constants.ts @@ -0,0 +1,5 @@ +export const APP_STATUS_CUSTOM_MESSAGES = { + HIBERNATED: "This application's workloads are scaled down to 0 replicas", + 'PARTIALLY HIBERNATED': "Some of this application's workloads are scaled down to 0 replicas.", + INTEGRATION_INSTALLING: 'The installation will complete when status for all the below resources become HEALTHY.', +} diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index e6dc47634..ea71e34af 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -1,6 +1,17 @@ import { ReactNode } from 'react' +import { AppDetails } from '@Shared/types' + export interface AppStatusModalProps { title: ReactNode handleClose: () => void + type: 'devtron-app' | 'external-apps' | 'stack-manager' | 'release' + /** + * If not given + */ + handleShowConfigDriftModal?: () => void + /** + * If given would not poll for app details and resource tree, Polling for gitops timeline would still be done + */ + appDetails?: AppDetails } diff --git a/src/Shared/Components/AppStatusModal/utils.ts b/src/Shared/Components/AppStatusModal/utils.ts new file mode 100644 index 000000000..9be88f420 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/utils.ts @@ -0,0 +1,31 @@ +import { aggregateNodes } from '@Shared/Helpers' +import { AppDetails } from '@Shared/types' + +import { AggregatedNodes } from '../CICDHistory' + +export const getAppStatusMessageFromAppDetails = (appDetails: AppDetails): string => { + if (!appDetails?.resourceTree) { + return '' + } + + const nodes: AggregatedNodes = aggregateNodes( + appDetails.resourceTree.nodes || [], + appDetails.resourceTree.podMetadata || [], + ) + + const { conditions } = appDetails.resourceTree + const rollout = nodes?.nodes?.Rollout?.entries()?.next()?.value?.[1] + + if (Array.isArray(conditions) && conditions.length > 0 && conditions[0].message) { + return conditions[0].message + } + + if (rollout?.health?.message) { + return rollout.health.message + } + if (appDetails.FluxAppStatusDetail) { + return appDetails.FluxAppStatusDetail.message + } + + return '' +} From b1322a3b625e4f2a58bd73450b41ac47d7073820 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 16 Apr 2025 15:54:26 +0530 Subject: [PATCH 03/33] refactor: optimize ErrorBar component by replacing useEffect with useMemo for improved performance --- src/Shared/Components/Error/ErrorBar.tsx | 42 +++++++++--------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/src/Shared/Components/Error/ErrorBar.tsx b/src/Shared/Components/Error/ErrorBar.tsx index 4fc517ff5..41c17ef99 100644 --- a/src/Shared/Components/Error/ErrorBar.tsx +++ b/src/Shared/Components/Error/ErrorBar.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { useEffect, useState } from 'react' +import { useMemo } from 'react' import { NavLink } from 'react-router-dom' import { ReactComponent as ErrorInfo } from '../../../Assets/Icon/ic-errorInfo.svg' @@ -24,33 +24,21 @@ import { AppDetailsErrorType, ErrorBarType } from './types' import { renderErrorHeaderMessage } from './utils' const ErrorBar = ({ appDetails }: ErrorBarType) => { - const [isImagePullBackOff, setIsImagePullBackOff] = useState(false) - - useEffect(() => { - if (appDetails.appType === AppType.DEVTRON_APP && appDetails.resourceTree?.nodes?.length) { - for (let index = 0; index < appDetails.resourceTree.nodes.length; index++) { - const node = appDetails.resourceTree.nodes[index] - let _isImagePullBackOff = false - if (node.info?.length) { - for (let idx = 0; idx < node.info.length; idx++) { - const info = node.info[idx] - if ( - info.value && - (info.value.toLowerCase() === AppDetailsErrorType.ERRIMAGEPULL || - info.value.toLowerCase() === AppDetailsErrorType.IMAGEPULLBACKOFF) - ) { - _isImagePullBackOff = true - break - } - } - - if (_isImagePullBackOff) { - setIsImagePullBackOff(true) - break - } - } - } + const isImagePullBackOff = useMemo(() => { + if (appDetails?.appType === AppType.DEVTRON_APP && appDetails?.resourceTree?.nodes?.length) { + appDetails.resourceTree.nodes.some( + (node) => + !!node.info?.some((info) => { + const infoValueLowerCase = info?.value?.toLowerCase() + return ( + infoValueLowerCase === AppDetailsErrorType.ERRIMAGEPULL || + infoValueLowerCase === AppDetailsErrorType.IMAGEPULLBACKOFF + ) + }), + ) } + + return false }, [appDetails]) if ( From 04f7d19741b16602ced91a2e8ad01dbaa1702fa5 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Wed, 16 Apr 2025 18:35:18 +0530 Subject: [PATCH 04/33] feat: enhance AppStatusModal by adding ErrorBar component and refactor DeploymentStatusDetailRow for cleaner code --- .../AppStatusModal/AppStatusBody.tsx | 3 +++ .../CICDHistory/DeploymentStatusDetailRow.tsx | 7 +++--- src/Shared/Components/Error/ErrorBar.tsx | 22 +++---------------- src/Shared/Components/Error/utils.tsx | 20 ++++++++++++++++- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 95a17a9a5..f23586060 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -2,6 +2,7 @@ import { ComponentProps, ReactNode, useMemo } from 'react' import { Tooltip } from '@Common/Tooltip' +import { ErrorBar } from '../Error' import { ShowMoreText } from '../ShowMoreText' import { AppStatus } from '../StatusComponent' import { APP_STATUS_CUSTOM_MESSAGES } from './constants' @@ -55,6 +56,8 @@ export const AppStatusBody = ({ appDetails, type }: Pick ))} + + ) } diff --git a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx index d55511dbd..c9cd7b239 100644 --- a/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx +++ b/src/Shared/Components/CICDHistory/DeploymentStatusDetailRow.tsx @@ -31,6 +31,8 @@ import { getManualSync } from './service' import { DeploymentStatusDetailRowType } from './types' import { renderIcon } from './utils' +const appHealthDropDownlist = ['inprogress', 'failed', 'disconnect', 'timed_out'] + export const DeploymentStatusDetailRow = ({ type, hideVerticalConnector, @@ -39,7 +41,7 @@ export const DeploymentStatusDetailRow = ({ const { appId, envId } = useParams<{ appId: string; envId: string }>() const statusBreakDownType = deploymentDetailedData.deploymentStatusBreakdown[type] const [collapsed, toggleCollapsed] = useState(statusBreakDownType.isCollapsed) - const appHealthDropDownlist = ['inprogress', 'failed', 'disconnect', 'timed_out'] + const isHelmManifestPushFailed = type === TIMELINE_STATUS.HELM_MANIFEST_PUSHED_TO_HELM_REPO && deploymentDetailedData.deploymentStatus === statusIcon.failed @@ -50,8 +52,7 @@ export const DeploymentStatusDetailRow = ({ async function manualSyncData() { try { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const response = await getManualSync({ appId, envId }) + await getManualSync({ appId, envId }) } catch (error) { showError(error) } diff --git a/src/Shared/Components/Error/ErrorBar.tsx b/src/Shared/Components/Error/ErrorBar.tsx index 41c17ef99..79c05da30 100644 --- a/src/Shared/Components/Error/ErrorBar.tsx +++ b/src/Shared/Components/Error/ErrorBar.tsx @@ -14,32 +14,16 @@ * limitations under the License. */ -import { useMemo } from 'react' import { NavLink } from 'react-router-dom' import { ReactComponent as ErrorInfo } from '../../../Assets/Icon/ic-errorInfo.svg' import { URLS } from '../../../Common' import { AppType } from '../../types' -import { AppDetailsErrorType, ErrorBarType } from './types' -import { renderErrorHeaderMessage } from './utils' +import { ErrorBarType } from './types' +import { getIsImagePullBackOff, renderErrorHeaderMessage } from './utils' const ErrorBar = ({ appDetails }: ErrorBarType) => { - const isImagePullBackOff = useMemo(() => { - if (appDetails?.appType === AppType.DEVTRON_APP && appDetails?.resourceTree?.nodes?.length) { - appDetails.resourceTree.nodes.some( - (node) => - !!node.info?.some((info) => { - const infoValueLowerCase = info?.value?.toLowerCase() - return ( - infoValueLowerCase === AppDetailsErrorType.ERRIMAGEPULL || - infoValueLowerCase === AppDetailsErrorType.IMAGEPULLBACKOFF - ) - }), - ) - } - - return false - }, [appDetails]) + const isImagePullBackOff = getIsImagePullBackOff(appDetails) if ( !appDetails || diff --git a/src/Shared/Components/Error/utils.tsx b/src/Shared/Components/Error/utils.tsx index aadaa48c3..2c98612d3 100644 --- a/src/Shared/Components/Error/utils.tsx +++ b/src/Shared/Components/Error/utils.tsx @@ -14,7 +14,25 @@ * limitations under the License. */ -import { AppDetails } from '../../types' +import { AppDetails, AppType } from '../../types' +import { AppDetailsErrorType } from './types' + +export const getIsImagePullBackOff = (appDetails: AppDetails): boolean => { + if (appDetails?.appType === AppType.DEVTRON_APP && appDetails?.resourceTree?.nodes?.length) { + appDetails.resourceTree.nodes.some( + (node) => + !!node.info?.some((info) => { + const infoValueLowerCase = info?.value?.toLowerCase() + return ( + infoValueLowerCase === AppDetailsErrorType.ERRIMAGEPULL || + infoValueLowerCase === AppDetailsErrorType.IMAGEPULLBACKOFF + ) + }), + ) + } + + return false +} export const renderErrorHeaderMessage = (appDetails: AppDetails, key: string, onClickActionButton?): JSX.Element => (
From 6ad255689a8ef1085e13e9e306557d9b548d62f8 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Thu, 17 Apr 2025 10:57:13 +0530 Subject: [PATCH 05/33] chore: deprecated mark --- src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx index 7225c5a80..2667314fc 100644 --- a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx +++ b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx @@ -29,6 +29,9 @@ import { Button, ButtonStyleType, ButtonVariantType } from '../Button' import { StatusFilterButtonComponent } from './StatusFilterButtonComponent' import { AggregatedNodes, AppStatusDetailsChartType, NodeFilters, STATUS_SORTING_ORDER } from './types' +/** + * @deprecated Remove after migration to new modal + */ const AppStatusDetailsChart = ({ filterRemoveHealth = false, showFooter, From 59627db3c4f605fdbfe1ff1c0e3b4963a6cf14ac Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Mon, 21 Apr 2025 15:59:13 +0530 Subject: [PATCH 06/33] feat: enhance AppStatusModal with ConfigDrift functionality and refactor components for improved structure --- .../AppStatusModal/AppStatusBody.tsx | 8 +- .../AppStatusModal/AppStatusContent.tsx | 144 ++++++++++++++++++ .../AppStatusModal.component.tsx | 91 +++++++---- .../AppStatusModal/AppStatusModal.scss | 7 + src/Shared/Components/AppStatusModal/types.ts | 27 +++- src/Shared/Components/AppStatusModal/utils.ts | 39 ++++- src/Shared/types.ts | 6 + 7 files changed, 288 insertions(+), 34 deletions(-) create mode 100644 src/Shared/Components/AppStatusModal/AppStatusContent.tsx create mode 100644 src/Shared/Components/AppStatusModal/AppStatusModal.scss diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index f23586060..4fea02e6a 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -5,8 +5,9 @@ import { Tooltip } from '@Common/Tooltip' import { ErrorBar } from '../Error' import { ShowMoreText } from '../ShowMoreText' import { AppStatus } from '../StatusComponent' +import AppStatusContent from './AppStatusContent' import { APP_STATUS_CUSTOM_MESSAGES } from './constants' -import { AppStatusModalProps } from './types' +import { AppStatusBodyProps } from './types' import { getAppStatusMessageFromAppDetails } from './utils' const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; value: ReactNode; isLast?: boolean }) => ( @@ -21,7 +22,7 @@ const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; val
) -export const AppStatusBody = ({ appDetails, type }: Pick) => { +export const AppStatusBody = ({ appDetails, type, handleShowConfigDriftModal }: AppStatusBodyProps) => { const message = useMemo(() => getAppStatusMessageFromAppDetails(appDetails), [appDetails]) const customMessage = APP_STATUS_CUSTOM_MESSAGES[appDetails.resourceTree?.status?.toUpperCase()] @@ -43,6 +44,7 @@ export const AppStatusBody = ({ appDetails, type }: Pick {/* Info card */} @@ -58,6 +60,8 @@ export const AppStatusBody = ({ appDetails, type }: Pick + + ) } diff --git a/src/Shared/Components/AppStatusModal/AppStatusContent.tsx b/src/Shared/Components/AppStatusModal/AppStatusContent.tsx new file mode 100644 index 000000000..9de986031 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/AppStatusContent.tsx @@ -0,0 +1,144 @@ +import { useMemo, useState } from 'react' + +import { SortableTableHeaderCell } from '@Common/SortableTableHeaderCell' +import { Tooltip } from '@Common/Tooltip' +import { ALL_RESOURCE_KIND_FILTER, APP_STATUS_HEADERS, ComponentSizeType } from '@Shared/constants' +import { Node } from '@Shared/types' + +import { Button, ButtonStyleType, ButtonVariantType } from '../Button' +import { NodeFilters, StatusFilterButtonComponent } from '../CICDHistory' +import { Icon } from '../Icon' +import { AppStatusContentProps } from './types' +import { getFlattenedNodesFromAppDetails, getResourceKey } from './utils' + +const APP_STATUS_ROWS_BASE_CLASS = 'px-16 py-8 dc__grid dc__column-gap-16 app-status-content__row' + +const AppStatusContent = ({ + appDetails, + handleShowConfigDriftModal, + filterHealthyNodes = false, + isCardLayout = true, +}: AppStatusContentProps) => { + const [currentFilter, setCurrentFilter] = useState(ALL_RESOURCE_KIND_FILTER) + const { appId, environmentId: envId } = appDetails + + const flattenedNodes = useMemo( + () => + getFlattenedNodesFromAppDetails({ + appDetails, + filterHealthyNodes, + }), + [appDetails, filterHealthyNodes], + ) + + const filteredFlattenedNodes = useMemo( + () => + flattenedNodes.filter( + (nodeDetails) => + currentFilter === ALL_RESOURCE_KIND_FILTER || + (currentFilter === NodeFilters.drifted && nodeDetails.hasDrift) || + nodeDetails.health.status?.toLowerCase() === currentFilter, + ), + [flattenedNodes, currentFilter], + ) + + const handleFilterClick = (selectedFilter: string) => { + const lowerCaseSelectedFilter = selectedFilter.toLowerCase() + + if (currentFilter !== lowerCaseSelectedFilter) { + setCurrentFilter(lowerCaseSelectedFilter) + } + } + + const getNodeMessage = (nodeDetails: Node) => { + if ( + appDetails.resourceTree?.resourcesSyncResult && + // eslint-disable-next-line no-prototype-builtins + appDetails.resourceTree.resourcesSyncResult.hasOwnProperty(getResourceKey(nodeDetails)) + ) { + return appDetails.resourceTree.resourcesSyncResult[getResourceKey(nodeDetails)] + } + return '' + } + + const getNodeStatus = (nodeDetails: Node) => (nodeDetails.status ? nodeDetails.status : nodeDetails.health.status) + + const renderRows = () => { + if (!flattenedNodes.length) { + return ( +
+ + Checking resources status +
+ ) + } + + return ( + <> + {filteredFlattenedNodes.map((nodeDetails) => ( +
+ + {nodeDetails.kind} + + + {nodeDetails.name} + +
+ {getNodeStatus(nodeDetails)} +
+ +
+ {handleShowConfigDriftModal && nodeDetails.hasDrift && ( +
+ Config drift detected + {appId && envId && ( +
+ )} +
{getNodeMessage(nodeDetails)}
+
+
+ ))} + + ) + } + + return ( +
+ {!!flattenedNodes.length && ( +
+ +
+ )} + +
+ {APP_STATUS_HEADERS.map((headerKey) => ( + + ))} +
+ + {renderRows()} +
+ ) +} + +export default AppStatusContent diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx index 158d68310..d87caa14e 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react' + import { Drawer, stopPropagation } from '@Common/index' import { ComponentSizeType } from '@Shared/constants' @@ -6,34 +8,71 @@ import { Icon } from '../Icon' import { AppStatusBody } from './AppStatusBody' import { AppStatusModalProps } from './types' -const AppStatusModal = ({ title, handleClose, type, appDetails }: AppStatusModalProps) => ( - -
-
-
- {title} - -
-
-
- +
+ +
- -
-) + + ) +} export default AppStatusModal diff --git a/src/Shared/Components/AppStatusModal/AppStatusModal.scss b/src/Shared/Components/AppStatusModal/AppStatusModal.scss new file mode 100644 index 000000000..f2a89d6c1 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/AppStatusModal.scss @@ -0,0 +1,7 @@ +.app-status-modal { + .app-status-content { + &__row { + grid-template-columns: 150px 200px 100px auto; + } + } +} \ No newline at end of file diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index ea71e34af..c77cdb60e 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -1,17 +1,36 @@ -import { ReactNode } from 'react' +import { FunctionComponent, ReactNode } from 'react' -import { AppDetails } from '@Shared/types' +import { AppDetails, ConfigDriftModalProps } from '@Shared/types' export interface AppStatusModalProps { title: ReactNode handleClose: () => void type: 'devtron-app' | 'external-apps' | 'stack-manager' | 'release' /** - * If not given + * If not given would assume to hide config drift related info */ - handleShowConfigDriftModal?: () => void + handleShowConfigDriftModal: () => void | null /** * 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 } + +export interface AppStatusBodyProps + extends Pick {} + +export interface AppStatusContentProps extends Pick { + /** + * @default false + */ + filterHealthyNodes?: boolean + /** + * @default true + */ + isCardLayout?: boolean +} + +export interface GetFilteredFlattenedNodesFromAppDetailsParamsType + extends Pick {} diff --git a/src/Shared/Components/AppStatusModal/utils.ts b/src/Shared/Components/AppStatusModal/utils.ts index 9be88f420..65f28f3d4 100644 --- a/src/Shared/Components/AppStatusModal/utils.ts +++ b/src/Shared/Components/AppStatusModal/utils.ts @@ -1,7 +1,9 @@ +import { DEPLOYMENT_STATUS } from '@Shared/constants' import { aggregateNodes } from '@Shared/Helpers' -import { AppDetails } from '@Shared/types' +import { AppDetails, Node } from '@Shared/types' -import { AggregatedNodes } from '../CICDHistory' +import { AggregatedNodes, STATUS_SORTING_ORDER } from '../CICDHistory' +import { GetFilteredFlattenedNodesFromAppDetailsParamsType as GetFlattenedNodesFromAppDetailsParamsType } from './types' export const getAppStatusMessageFromAppDetails = (appDetails: AppDetails): string => { if (!appDetails?.resourceTree) { @@ -29,3 +31,36 @@ export const getAppStatusMessageFromAppDetails = (appDetails: AppDetails): strin return '' } + +export const getFlattenedNodesFromAppDetails = ({ + appDetails, + filterHealthyNodes, +}: GetFlattenedNodesFromAppDetailsParamsType): Node[] => { + const nodes: AggregatedNodes = aggregateNodes( + appDetails.resourceTree?.nodes || [], + appDetails.resourceTree?.podMetadata || [], + ) + + const flattenedNodes: Node[] = [] + + Object.entries(nodes?.nodes || {}).forEach(([, element]) => { + element.forEach((childElement) => { + if (childElement.health) { + flattenedNodes.push(childElement) + } + }) + }) + + flattenedNodes.sort( + (a, b) => + STATUS_SORTING_ORDER[a.health.status?.toLowerCase()] - STATUS_SORTING_ORDER[b.health.status?.toLowerCase()], + ) + + if (filterHealthyNodes) { + return flattenedNodes.filter((node) => node.health.status?.toLowerCase() !== DEPLOYMENT_STATUS.HEALTHY) + } + + return flattenedNodes +} + +export const getResourceKey = (nodeDetails: Node) => `${nodeDetails.kind}/${nodeDetails.name}` diff --git a/src/Shared/types.ts b/src/Shared/types.ts index 3a2cf6f2f..4d8805efc 100644 --- a/src/Shared/types.ts +++ b/src/Shared/types.ts @@ -153,6 +153,7 @@ export interface Node { canBeHibernated: boolean isHibernated: boolean hasDrift?: boolean + status?: string } // eslint-disable-next-line no-use-before-define @@ -246,6 +247,11 @@ export interface AppDetails { FluxAppStatusDetail?: FluxAppStatusDetail } +export interface ConfigDriftModalProps extends Pick { + handleCloseModal?: () => void + envId?: number +} + export enum RegistryType { GIT = 'git', GITHUB = 'github', From 502fbf5341e3d87ccc335304c434b70093f0b233 Mon Sep 17 00:00:00 2001 From: AbhishekA1509 Date: Tue, 22 Apr 2025 17:00:55 +0530 Subject: [PATCH 07/33] feat: add app detail fetching and enhance AppStatusModal with improved error handling and polling mechanism --- src/Common/Constants.ts | 1 + .../AppStatusModal/AppStatusBody.tsx | 31 +++-- .../AppStatusModal/AppStatusContent.tsx | 6 +- .../AppStatusModal.component.tsx | 130 ++++++++++++++++-- .../Components/AppStatusModal/service.ts | 31 +++++ src/Shared/Components/AppStatusModal/types.ts | 25 ++-- .../CICDHistory/AppStatusDetailsChart.tsx | 2 +- .../StatusFilterButtonComponent.scss | 77 ----------- .../StatusFilterButtonComponent.tsx | 66 +++++---- 9 files changed, 228 insertions(+), 141 deletions(-) create mode 100644 src/Shared/Components/AppStatusModal/service.ts delete mode 100644 src/Shared/Components/CICDHistory/StatusFilterButtonComponent.scss diff --git a/src/Common/Constants.ts b/src/Common/Constants.ts index ee637c143..0c1d46e66 100644 --- a/src/Common/Constants.ts +++ b/src/Common/Constants.ts @@ -134,6 +134,7 @@ export const ROUTES = { ATTRIBUTES_CREATE: 'attributes/create', ATTRIBUTES_UPDATE: 'attributes/update', APP_LIST_MIN: 'app/min', + APP_DETAIL: 'app/detail', CLUSTER_LIST_MIN: 'cluster/autocomplete', CLUSTER_LIST_RAW: 'k8s/capacity/cluster/list/raw', PLUGIN_GLOBAL_LIST_DETAIL_V2: 'plugin/global/list/detail/v2', diff --git a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx index 4fea02e6a..5cba650bc 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusBody.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusBody.tsx @@ -15,7 +15,7 @@ const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; val className={`py-12 px-16 flexbox dc__align-items-center dc__gap-16 ${!isLast ? 'border__secondary--bottom' : ''}`} > -

{heading}

+

{heading}

{typeof value === 'string' ? : value} @@ -32,19 +32,26 @@ export const AppStatusBody = ({ appDetails, type, handleShowConfigDriftModal }: heading: type !== 'stack-manager' ? 'Application Status' : 'Status', value: , }, - message && { - id: 2, - heading: 'Message', - value: message, - }, - customMessage && { - id: 3, - heading: 'Message', - value: customMessage, - }, + ...(message + ? [ + { + id: 2, + heading: 'Message', + value: message, + }, + ] + : []), + ...(customMessage + ? [ + { + id: 3, + heading: 'Message', + value: customMessage, + }, + ] + : []), ] - // TODO: Reminder to add footer here return (
{/* Info card */} diff --git a/src/Shared/Components/AppStatusModal/AppStatusContent.tsx b/src/Shared/Components/AppStatusModal/AppStatusContent.tsx index 9de986031..6bf69244e 100644 --- a/src/Shared/Components/AppStatusModal/AppStatusContent.tsx +++ b/src/Shared/Components/AppStatusModal/AppStatusContent.tsx @@ -66,7 +66,7 @@ const AppStatusContent = ({ const renderRows = () => { if (!flattenedNodes.length) { return ( -
+
Checking resources status
@@ -117,9 +117,9 @@ const AppStatusContent = ({ } return ( -
+
{!!flattenedNodes.length && ( -
+
{ const [showConfigDriftModal, setShowConfigDriftModal] = useState(false) + const abortControllerRef = useRef(new AbortController()) + const pollingTimeoutRef = useRef | null>(null) + + const getAppDetailsWrapper = async () => { + const response = await abortPreviousRequests( + () => getAppDetails(appId, envId, abortControllerRef), + abortControllerRef, + ) + + return response + } + + const [ + areInitialAppDetailsLoading, + fetchedAppDetails, + fetchedAppDetailsError, + reloadInitialAppDetails, + setFetchedAppDetails, + ] = useAsync(getAppDetailsWrapper, [appId, envId, type], type === 'release') + + const handleExternalSync = async () => { + try { + const response = await getAppDetailsWrapper() + setFetchedAppDetails(response) + + pollingTimeoutRef.current = setTimeout( + () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleExternalSync() + }, + Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || 30000, + ) + } catch { + // Do nothing + } + } + + const areInitialAppDetailsLoadingWithAbortedError = + areInitialAppDetailsLoading || getIsRequestAborted(fetchedAppDetailsError) + + const appDetails = type === 'release' ? fetchedAppDetails : appDetailsProp + + // Adding useEffect to initiate timer for external sync and clear it on unmount + useEffect(() => { + if ( + !areInitialAppDetailsLoading && + !fetchedAppDetailsError && + fetchedAppDetails && + !pollingTimeoutRef.current + ) { + pollingTimeoutRef.current = setTimeout( + () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + handleExternalSync() + }, + Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || 30000, + ) + } + + return () => { + if (pollingTimeoutRef.current) { + clearTimeout(pollingTimeoutRef.current) + } + + abortControllerRef.current.abort() + } + }, [areInitialAppDetailsLoading, fetchedAppDetails, fetchedAppDetailsError]) + const handleShowConfigDriftModal = isConfigDriftEnabled ? () => { setShowConfigDriftModal(true) @@ -64,11 +144,43 @@ const AppStatusModal = ({
- + + + + {type === 'stack-manager' && ( +
+ Facing issues in installing integration? + +
+ )} +
diff --git a/src/Shared/Components/AppStatusModal/service.ts b/src/Shared/Components/AppStatusModal/service.ts new file mode 100644 index 000000000..ccd2b5425 --- /dev/null +++ b/src/Shared/Components/AppStatusModal/service.ts @@ -0,0 +1,31 @@ +import { get, getIsRequestAborted } from '@Common/API' +import { ROUTES } from '@Common/Constants' +import { showError } from '@Common/Helper' +import { APIOptions } from '@Common/Types' + +export const getAppDetails = async ( + appId: number, + envId: number, + abortControllerRef: APIOptions['abortControllerRef'], +) => { + try { + const [appDetails, resourceTree] = await Promise.all([ + get(`${ROUTES.APP_DETAIL}/v2?app-id=${appId}&env-id=${envId}`, { + abortControllerRef, + }), + get(`${ROUTES.APP_DETAIL}/resource-tree?app-id=${appId}&env-id=${envId}`, { + abortControllerRef, + }), + ]) + + return { + ...(appDetails.result || {}), + resourceTree: resourceTree.result, + } + } catch (error) { + if (getIsRequestAborted(error)) { + showError(error) + } + throw error + } +} diff --git a/src/Shared/Components/AppStatusModal/types.ts b/src/Shared/Components/AppStatusModal/types.ts index c77cdb60e..3e5c42b9d 100644 --- a/src/Shared/Components/AppStatusModal/types.ts +++ b/src/Shared/Components/AppStatusModal/types.ts @@ -2,24 +2,31 @@ import { FunctionComponent, ReactNode } from 'react' import { AppDetails, ConfigDriftModalProps } from '@Shared/types' -export interface AppStatusModalProps { +export type AppStatusModalProps = { title: ReactNode handleClose: () => void - type: 'devtron-app' | 'external-apps' | 'stack-manager' | 'release' - /** - * If not given would assume to hide config drift related info - */ - handleShowConfigDriftModal: () => void | null /** * 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 -} +} & ( + | { + type: 'release' + appId: number + envId: number + } + | { + type: 'devtron-app' | 'external-apps' | 'stack-manager' + appId?: never + envId?: never + } +) -export interface AppStatusBodyProps - extends Pick {} +export interface AppStatusBodyProps extends Pick { + handleShowConfigDriftModal: () => void +} export interface AppStatusContentProps extends Pick { /** diff --git a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx index 2667314fc..21f3ae0b8 100644 --- a/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx +++ b/src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx @@ -88,7 +88,7 @@ const AppStatusDetailsChart = ({ if ( _appDetails.resourceTree?.resourcesSyncResult && // eslint-disable-next-line no-prototype-builtins - _appDetails.resourceTree?.resourcesSyncResult.hasOwnProperty(`${kind}/${name}`) + _appDetails.resourceTree.resourcesSyncResult.hasOwnProperty(`${kind}/${name}`) ) { return _appDetails.resourceTree.resourcesSyncResult[`${kind}/${name}`] } diff --git a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.scss b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.scss deleted file mode 100644 index 3dd121cb4..000000000 --- a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.scss +++ /dev/null @@ -1,77 +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. - */ - -.status-filter-button { - &.radio-group { - padding: 0; - height: 24px; - overflow: hidden; - border-radius: 4px; - - input + .radio__item-label { - border-radius: 0; - border: 1px solid var(--N200); - } - - input[type='checkbox']:checked + .radio__item-label { - border-radius: 0; - background-color: var(--N100); - color: var(--N900); - } - - .radio { - color: var(--N500); - - &:hover:not(.disabled) { - color: var(--N900); - } - - .radio__item-label { - border-right: unset; - padding: 2px 8px; - display: flex; - align-items: center; - gap: 6px; - } - - &:first-child input + .radio__item-label { - border-top-left-radius: 4px !important; - border-bottom-left-radius: 4px !important; - } - - &:last-child input + .radio__item-label { - border-top-right-radius: 4px !important; - border-bottom-right-radius: 4px !important; - border: 1px solid var(--N200); - } - } - } - - &.radio-group.with-menu-button { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - - .radio:last-child > .radio__item-label { - border-right: none; - border-top-right-radius: 0 !important; - border-bottom-right-radius: 0 !important; - } - - .radio:first-child > .radio__item-label { - padding: 2px 4px; - } - } -} diff --git a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx index 918276109..b61149ae3 100644 --- a/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx +++ b/src/Shared/Components/CICDHistory/StatusFilterButtonComponent.tsx @@ -14,16 +14,16 @@ * limitations under the License. */ -import { ChangeEvent, useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import { ReactComponent as ICCaretDown } from '@Icons/ic-caret-down.svg' +import { SegmentType } from '@Common/SegmentedControl/types' +import { ComponentSizeType } from '@Shared/constants' -import { PopupMenu, StyledRadioGroup as RadioGroup } from '../../../Common' +import { PopupMenu, SegmentedControl } from '../../../Common' import { StatusFilterButtonType } from './types' import { getAppStatusIcon, getNodesCount, getStatusFilters } from './utils' -import './StatusFilterButtonComponent.scss' - export const StatusFilterButtonComponent = ({ nodes, selectedTab, @@ -54,13 +54,14 @@ export const StatusFilterButtonComponent = ({ return statusFilters }, [statusFilters.length, overflowFilterIndex, maxInlineFiltersCount]) - const handleInlineFilterClick = (e: ChangeEvent) => { - const { value } = e.target + const handleInlineFilterClick = (segment: SegmentType) => { + const { value } = segment + if (value === allResourceKindFilter.status) { setOverflowFilterIndex(0) } if (selectedTab !== value) { - handleFilterClick(value) + handleFilterClick(value as string) } } @@ -71,32 +72,37 @@ export const StatusFilterButtonComponent = ({ } } + const segments: SegmentType[] = [ + { + value: allResourceKindFilter.status, + label: `All (${allResourceKindFilter.count})`, + }, + + ...inlineFilters.map(({ status, count }) => ({ + value: status, + label: ( + + {getAppStatusIcon(status, true)} + {count} + + ), + tooltipProps: { + content: status, + className: 'w-100 dc__first-letter-capitalize', + }, + })), + ] + return (
- - - {`${allResourceKindFilter.status} (${allResourceKindFilter.count})`} - - {inlineFilters.map(({ status, count }) => ( - - {getAppStatusIcon(status, true)} - {count} - - ))} - + name="status-filter-button" + size={ComponentSizeType.small} + /> + {showOverflowFilters && ( Date: Tue, 22 Apr 2025 21:37:11 +0530 Subject: [PATCH 08/33] feat: enhance SegmentedControl by adding unique input IDs and improving accessibility with memoized IDs --- src/Common/SegmentedControl/Segment.tsx | 11 +++++++---- .../Components/AppStatusModal/AppStatusContent.tsx | 6 ++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Common/SegmentedControl/Segment.tsx b/src/Common/SegmentedControl/Segment.tsx index 19f28853b..437fc9020 100644 --- a/src/Common/SegmentedControl/Segment.tsx +++ b/src/Common/SegmentedControl/Segment.tsx @@ -1,8 +1,9 @@ -import { ReactElement } from 'react' +import { ReactElement, useMemo } from 'react' import { Tooltip } from '@Common/Tooltip' import { Icon } from '@Shared/Components' import { ComponentSizeType } from '@Shared/constants' +import { getUniqueId } from '@Shared/Helpers' import { ConditionalWrap } from '../Helper' import { COMPONENT_SIZE_TO_ICON_CLASS_MAP, COMPONENT_SIZE_TO_SEGMENT_CLASS_MAP } from './constants' @@ -24,6 +25,8 @@ const Segment = ({ size, disabled, }: SegmentProps) => { + const inputId = useMemo(getUniqueId, []) + const { value, icon, isError, label, tooltipProps, ariaLabel } = segment const handleChange = () => { onChange(segment) @@ -32,13 +35,13 @@ const Segment = ({ return (