Skip to content

Commit 59627db

Browse files
committed
feat: enhance AppStatusModal with ConfigDrift functionality and refactor components for improved structure
1 parent 6ad2556 commit 59627db

File tree

7 files changed

+288
-34
lines changed

7 files changed

+288
-34
lines changed

src/Shared/Components/AppStatusModal/AppStatusBody.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import { Tooltip } from '@Common/Tooltip'
55
import { ErrorBar } from '../Error'
66
import { ShowMoreText } from '../ShowMoreText'
77
import { AppStatus } from '../StatusComponent'
8+
import AppStatusContent from './AppStatusContent'
89
import { APP_STATUS_CUSTOM_MESSAGES } from './constants'
9-
import { AppStatusModalProps } from './types'
10+
import { AppStatusBodyProps } from './types'
1011
import { getAppStatusMessageFromAppDetails } from './utils'
1112

1213
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
2122
</div>
2223
)
2324

24-
export const AppStatusBody = ({ appDetails, type }: Pick<AppStatusModalProps, 'appDetails' | 'type'>) => {
25+
export const AppStatusBody = ({ appDetails, type, handleShowConfigDriftModal }: AppStatusBodyProps) => {
2526
const message = useMemo(() => getAppStatusMessageFromAppDetails(appDetails), [appDetails])
2627
const customMessage = APP_STATUS_CUSTOM_MESSAGES[appDetails.resourceTree?.status?.toUpperCase()]
2728

@@ -43,6 +44,7 @@ export const AppStatusBody = ({ appDetails, type }: Pick<AppStatusModalProps, 'a
4344
},
4445
]
4546

47+
// TODO: Reminder to add footer here
4648
return (
4749
<div className="flexbox-col dc__gap-16">
4850
{/* Info card */}
@@ -58,6 +60,8 @@ export const AppStatusBody = ({ appDetails, type }: Pick<AppStatusModalProps, 'a
5860
</div>
5961

6062
<ErrorBar appDetails={appDetails} />
63+
64+
<AppStatusContent appDetails={appDetails} handleShowConfigDriftModal={handleShowConfigDriftModal} />
6165
</div>
6266
)
6367
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { useMemo, useState } from 'react'
2+
3+
import { SortableTableHeaderCell } from '@Common/SortableTableHeaderCell'
4+
import { Tooltip } from '@Common/Tooltip'
5+
import { ALL_RESOURCE_KIND_FILTER, APP_STATUS_HEADERS, ComponentSizeType } from '@Shared/constants'
6+
import { Node } from '@Shared/types'
7+
8+
import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
9+
import { NodeFilters, StatusFilterButtonComponent } from '../CICDHistory'
10+
import { Icon } from '../Icon'
11+
import { AppStatusContentProps } from './types'
12+
import { getFlattenedNodesFromAppDetails, getResourceKey } from './utils'
13+
14+
const APP_STATUS_ROWS_BASE_CLASS = 'px-16 py-8 dc__grid dc__column-gap-16 app-status-content__row'
15+
16+
const AppStatusContent = ({
17+
appDetails,
18+
handleShowConfigDriftModal,
19+
filterHealthyNodes = false,
20+
isCardLayout = true,
21+
}: AppStatusContentProps) => {
22+
const [currentFilter, setCurrentFilter] = useState<string>(ALL_RESOURCE_KIND_FILTER)
23+
const { appId, environmentId: envId } = appDetails
24+
25+
const flattenedNodes = useMemo(
26+
() =>
27+
getFlattenedNodesFromAppDetails({
28+
appDetails,
29+
filterHealthyNodes,
30+
}),
31+
[appDetails, filterHealthyNodes],
32+
)
33+
34+
const filteredFlattenedNodes = useMemo(
35+
() =>
36+
flattenedNodes.filter(
37+
(nodeDetails) =>
38+
currentFilter === ALL_RESOURCE_KIND_FILTER ||
39+
(currentFilter === NodeFilters.drifted && nodeDetails.hasDrift) ||
40+
nodeDetails.health.status?.toLowerCase() === currentFilter,
41+
),
42+
[flattenedNodes, currentFilter],
43+
)
44+
45+
const handleFilterClick = (selectedFilter: string) => {
46+
const lowerCaseSelectedFilter = selectedFilter.toLowerCase()
47+
48+
if (currentFilter !== lowerCaseSelectedFilter) {
49+
setCurrentFilter(lowerCaseSelectedFilter)
50+
}
51+
}
52+
53+
const getNodeMessage = (nodeDetails: Node) => {
54+
if (
55+
appDetails.resourceTree?.resourcesSyncResult &&
56+
// eslint-disable-next-line no-prototype-builtins
57+
appDetails.resourceTree.resourcesSyncResult.hasOwnProperty(getResourceKey(nodeDetails))
58+
) {
59+
return appDetails.resourceTree.resourcesSyncResult[getResourceKey(nodeDetails)]
60+
}
61+
return ''
62+
}
63+
64+
const getNodeStatus = (nodeDetails: Node) => (nodeDetails.status ? nodeDetails.status : nodeDetails.health.status)
65+
66+
const renderRows = () => {
67+
if (!flattenedNodes.length) {
68+
return (
69+
<div className="flexbox-col dc__gap-4 dc__align-center h-100">
70+
<Icon name="ic-info-filled" size={20} color={null} />
71+
<span>Checking resources status</span>
72+
</div>
73+
)
74+
}
75+
76+
return (
77+
<>
78+
{filteredFlattenedNodes.map((nodeDetails) => (
79+
<div
80+
className={`${APP_STATUS_ROWS_BASE_CLASS} cn-9 fs-13 fw-4 lh-20`}
81+
key={getResourceKey(nodeDetails)}
82+
>
83+
<Tooltip content={nodeDetails.kind}>
84+
<span>{nodeDetails.kind}</span>
85+
</Tooltip>
86+
87+
<span>{nodeDetails.name}</span>
88+
89+
<div
90+
className={`app-summary__status-name f-${getNodeStatus(nodeDetails)?.toLowerCase() || ''}`}
91+
>
92+
{getNodeStatus(nodeDetails)}
93+
</div>
94+
95+
<div className="flexbox-col dc__gap-4">
96+
{handleShowConfigDriftModal && nodeDetails.hasDrift && (
97+
<div className="flexbox dc__gap-8 dc__align-items-center">
98+
<span className="fs-13 fw-4 lh-20 cy-7">Config drift detected</span>
99+
{appId && envId && (
100+
<Button
101+
dataTestId="show-config-drift"
102+
text="Compare with desired"
103+
variant={ButtonVariantType.text}
104+
style={ButtonStyleType.default}
105+
onClick={handleShowConfigDriftModal}
106+
size={ComponentSizeType.small}
107+
/>
108+
)}
109+
</div>
110+
)}
111+
<div>{getNodeMessage(nodeDetails)}</div>
112+
</div>
113+
</div>
114+
))}
115+
</>
116+
)
117+
}
118+
119+
return (
120+
<div className={`flexbox-col ${isCardLayout ? 'br-6 border__primary' : ''}`}>
121+
{!!flattenedNodes.length && (
122+
<div>
123+
<StatusFilterButtonComponent
124+
nodes={flattenedNodes}
125+
selectedTab={currentFilter}
126+
handleFilterClick={handleFilterClick}
127+
/>
128+
</div>
129+
)}
130+
131+
<div
132+
className={`${APP_STATUS_ROWS_BASE_CLASS} cn-7 fs-13 fw-6 lh-20 border__secondary--bottom bg__primary`}
133+
>
134+
{APP_STATUS_HEADERS.map((headerKey) => (
135+
<SortableTableHeaderCell key={`header_${headerKey}`} isSortable={false} title={headerKey} />
136+
))}
137+
</div>
138+
139+
{renderRows()}
140+
</div>
141+
)
142+
}
143+
144+
export default AppStatusContent
Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useState } from 'react'
2+
13
import { Drawer, stopPropagation } from '@Common/index'
24
import { ComponentSizeType } from '@Shared/constants'
35

@@ -6,34 +8,71 @@ import { Icon } from '../Icon'
68
import { AppStatusBody } from './AppStatusBody'
79
import { AppStatusModalProps } from './types'
810

9-
const AppStatusModal = ({ title, handleClose, type, appDetails }: AppStatusModalProps) => (
10-
<Drawer position="right" width="1024px" onClose={handleClose} onEscape={handleClose}>
11-
<div
12-
className="flexbox-col dc__content-space h-100 border__primary--left bg__modal--primary shadow__modal"
13-
onClick={stopPropagation}
14-
>
15-
<div className="flexbox-col px-20 border__primary--bottom dc__no-shrink">
16-
<div className="flexbox py-12 dc__content-space">
17-
{title}
18-
19-
<Button
20-
dataTestId="close-modal-header-icon-button"
21-
variant={ButtonVariantType.borderLess}
22-
style={ButtonStyleType.negativeGrey}
23-
size={ComponentSizeType.xs}
24-
icon={<Icon name="ic-close-large" color={null} />}
25-
ariaLabel="Close modal"
26-
showAriaLabelInTippy={false}
27-
onClick={handleClose}
28-
/>
11+
import './AppStatusModal.scss'
12+
13+
const AppStatusModal = ({
14+
title,
15+
handleClose,
16+
type,
17+
appDetails,
18+
isConfigDriftEnabled,
19+
configDriftModal: ConfigDriftModal,
20+
}: AppStatusModalProps) => {
21+
const [showConfigDriftModal, setShowConfigDriftModal] = useState(false)
22+
23+
const handleShowConfigDriftModal = isConfigDriftEnabled
24+
? () => {
25+
setShowConfigDriftModal(true)
26+
}
27+
: null
28+
29+
const handleCloseConfigDriftModal = () => {
30+
setShowConfigDriftModal(false)
31+
}
32+
33+
if (showConfigDriftModal) {
34+
return (
35+
<ConfigDriftModal
36+
appId={appDetails.appId}
37+
envId={+appDetails.environmentId}
38+
handleCloseModal={handleCloseConfigDriftModal}
39+
/>
40+
)
41+
}
42+
43+
return (
44+
<Drawer position="right" width="1024px" onClose={handleClose} onEscape={handleClose}>
45+
<div
46+
className="flexbox-col dc__content-space h-100 border__primary--left bg__modal--primary shadow__modal app-status-modal"
47+
onClick={stopPropagation}
48+
>
49+
<div className="flexbox-col px-20 border__primary--bottom dc__no-shrink">
50+
<div className="flexbox py-12 dc__content-space">
51+
{title}
52+
53+
<Button
54+
dataTestId="close-modal-header-icon-button"
55+
variant={ButtonVariantType.borderLess}
56+
style={ButtonStyleType.negativeGrey}
57+
size={ComponentSizeType.xs}
58+
icon={<Icon name="ic-close-large" color={null} />}
59+
ariaLabel="Close modal"
60+
showAriaLabelInTippy={false}
61+
onClick={handleClose}
62+
/>
63+
</div>
2964
</div>
30-
</div>
3165

32-
<div className="flexbox-col flex-grow-1 dc__overflow-auto p-20 dc__gap-16">
33-
<AppStatusBody appDetails={appDetails} type={type} />
66+
<div className="flexbox-col flex-grow-1 dc__overflow-auto p-20 dc__gap-16">
67+
<AppStatusBody
68+
appDetails={appDetails}
69+
type={type}
70+
handleShowConfigDriftModal={handleShowConfigDriftModal}
71+
/>
72+
</div>
3473
</div>
35-
</div>
36-
</Drawer>
37-
)
74+
</Drawer>
75+
)
76+
}
3877

3978
export default AppStatusModal
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.app-status-modal {
2+
.app-status-content {
3+
&__row {
4+
grid-template-columns: 150px 200px 100px auto;
5+
}
6+
}
7+
}
Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1-
import { ReactNode } from 'react'
1+
import { FunctionComponent, ReactNode } from 'react'
22

3-
import { AppDetails } from '@Shared/types'
3+
import { AppDetails, ConfigDriftModalProps } from '@Shared/types'
44

55
export interface AppStatusModalProps {
66
title: ReactNode
77
handleClose: () => void
88
type: 'devtron-app' | 'external-apps' | 'stack-manager' | 'release'
99
/**
10-
* If not given
10+
* If not given would assume to hide config drift related info
1111
*/
12-
handleShowConfigDriftModal?: () => void
12+
handleShowConfigDriftModal: () => void | null
1313
/**
1414
* If given would not poll for app details and resource tree, Polling for gitops timeline would still be done
1515
*/
1616
appDetails?: AppDetails
17+
isConfigDriftEnabled: boolean
18+
configDriftModal: FunctionComponent<ConfigDriftModalProps>
1719
}
20+
21+
export interface AppStatusBodyProps
22+
extends Pick<AppStatusModalProps, 'appDetails' | 'type' | 'handleShowConfigDriftModal'> {}
23+
24+
export interface AppStatusContentProps extends Pick<AppStatusBodyProps, 'appDetails' | 'handleShowConfigDriftModal'> {
25+
/**
26+
* @default false
27+
*/
28+
filterHealthyNodes?: boolean
29+
/**
30+
* @default true
31+
*/
32+
isCardLayout?: boolean
33+
}
34+
35+
export interface GetFilteredFlattenedNodesFromAppDetailsParamsType
36+
extends Pick<AppStatusContentProps, 'appDetails' | 'filterHealthyNodes'> {}

src/Shared/Components/AppStatusModal/utils.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { DEPLOYMENT_STATUS } from '@Shared/constants'
12
import { aggregateNodes } from '@Shared/Helpers'
2-
import { AppDetails } from '@Shared/types'
3+
import { AppDetails, Node } from '@Shared/types'
34

4-
import { AggregatedNodes } from '../CICDHistory'
5+
import { AggregatedNodes, STATUS_SORTING_ORDER } from '../CICDHistory'
6+
import { GetFilteredFlattenedNodesFromAppDetailsParamsType as GetFlattenedNodesFromAppDetailsParamsType } from './types'
57

68
export const getAppStatusMessageFromAppDetails = (appDetails: AppDetails): string => {
79
if (!appDetails?.resourceTree) {
@@ -29,3 +31,36 @@ export const getAppStatusMessageFromAppDetails = (appDetails: AppDetails): strin
2931

3032
return ''
3133
}
34+
35+
export const getFlattenedNodesFromAppDetails = ({
36+
appDetails,
37+
filterHealthyNodes,
38+
}: GetFlattenedNodesFromAppDetailsParamsType): Node[] => {
39+
const nodes: AggregatedNodes = aggregateNodes(
40+
appDetails.resourceTree?.nodes || [],
41+
appDetails.resourceTree?.podMetadata || [],
42+
)
43+
44+
const flattenedNodes: Node[] = []
45+
46+
Object.entries(nodes?.nodes || {}).forEach(([, element]) => {
47+
element.forEach((childElement) => {
48+
if (childElement.health) {
49+
flattenedNodes.push(childElement)
50+
}
51+
})
52+
})
53+
54+
flattenedNodes.sort(
55+
(a, b) =>
56+
STATUS_SORTING_ORDER[a.health.status?.toLowerCase()] - STATUS_SORTING_ORDER[b.health.status?.toLowerCase()],
57+
)
58+
59+
if (filterHealthyNodes) {
60+
return flattenedNodes.filter((node) => node.health.status?.toLowerCase() !== DEPLOYMENT_STATUS.HEALTHY)
61+
}
62+
63+
return flattenedNodes
64+
}
65+
66+
export const getResourceKey = (nodeDetails: Node) => `${nodeDetails.kind}/${nodeDetails.name}`

0 commit comments

Comments
 (0)