Skip to content

Commit 502fbf5

Browse files
committed
feat: add app detail fetching and enhance AppStatusModal with improved error handling and polling mechanism
1 parent 59627db commit 502fbf5

File tree

9 files changed

+228
-141
lines changed

9 files changed

+228
-141
lines changed

src/Common/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export const ROUTES = {
134134
ATTRIBUTES_CREATE: 'attributes/create',
135135
ATTRIBUTES_UPDATE: 'attributes/update',
136136
APP_LIST_MIN: 'app/min',
137+
APP_DETAIL: 'app/detail',
137138
CLUSTER_LIST_MIN: 'cluster/autocomplete',
138139
CLUSTER_LIST_RAW: 'k8s/capacity/cluster/list/raw',
139140
PLUGIN_GLOBAL_LIST_DETAIL_V2: 'plugin/global/list/detail/v2',

src/Shared/Components/AppStatusModal/AppStatusBody.tsx

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const InfoCardItem = ({ heading, value, isLast = false }: { heading: string; val
1515
className={`py-12 px-16 flexbox dc__align-items-center dc__gap-16 ${!isLast ? 'border__secondary--bottom' : ''}`}
1616
>
1717
<Tooltip content={heading}>
18-
<h3 className="cn-9 fs-13 fw-4 lh-1-5 dc__truncate">{heading}</h3>
18+
<h3 className="cn-9 fs-13 fw-4 lh-1-5 dc__truncate m-0">{heading}</h3>
1919
</Tooltip>
2020

2121
{typeof value === 'string' ? <ShowMoreText textClass="cn-9 fs-13 fw-4 lh-1-5" text={value} /> : value}
@@ -32,19 +32,26 @@ export const AppStatusBody = ({ appDetails, type, handleShowConfigDriftModal }:
3232
heading: type !== 'stack-manager' ? 'Application Status' : 'Status',
3333
value: <AppStatus status={appDetails.resourceTree?.status?.toUpperCase() || appDetails.appStatus} />,
3434
},
35-
message && {
36-
id: 2,
37-
heading: 'Message',
38-
value: message,
39-
},
40-
customMessage && {
41-
id: 3,
42-
heading: 'Message',
43-
value: customMessage,
44-
},
35+
...(message
36+
? [
37+
{
38+
id: 2,
39+
heading: 'Message',
40+
value: message,
41+
},
42+
]
43+
: []),
44+
...(customMessage
45+
? [
46+
{
47+
id: 3,
48+
heading: 'Message',
49+
value: customMessage,
50+
},
51+
]
52+
: []),
4553
]
4654

47-
// TODO: Reminder to add footer here
4855
return (
4956
<div className="flexbox-col dc__gap-16">
5057
{/* Info card */}

src/Shared/Components/AppStatusModal/AppStatusContent.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const AppStatusContent = ({
6666
const renderRows = () => {
6767
if (!flattenedNodes.length) {
6868
return (
69-
<div className="flexbox-col dc__gap-4 dc__align-center h-100">
69+
<div className="flex column py-16 dc__gap-4 dc__align-center h-100">
7070
<Icon name="ic-info-filled" size={20} color={null} />
7171
<span>Checking resources status</span>
7272
</div>
@@ -117,9 +117,9 @@ const AppStatusContent = ({
117117
}
118118

119119
return (
120-
<div className={`flexbox-col ${isCardLayout ? 'br-6 border__primary' : ''}`}>
120+
<div className={`flexbox-col ${isCardLayout ? 'br-6 border__primary dc__overflow-hidden' : ''}`}>
121121
{!!flattenedNodes.length && (
122-
<div>
122+
<div className="p-12">
123123
<StatusFilterButtonComponent
124124
nodes={flattenedNodes}
125125
selectedTab={currentFilter}

src/Shared/Components/AppStatusModal/AppStatusModal.component.tsx

Lines changed: 121 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
import { useState } from 'react'
1+
import { useEffect, useRef, useState } from 'react'
22

3-
import { Drawer, stopPropagation } from '@Common/index'
3+
import { ReactComponent as ICChatCircleDots } from '@Icons/ic-chat-circle-dots.svg'
4+
import {
5+
abortPreviousRequests,
6+
DISCORD_LINK,
7+
Drawer,
8+
getIsRequestAborted,
9+
stopPropagation,
10+
useAsync,
11+
} from '@Common/index'
412
import { ComponentSizeType } from '@Shared/constants'
513

6-
import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
14+
import { APIResponseHandler } from '../APIResponseHandler'
15+
import { Button, ButtonComponentType, ButtonStyleType, ButtonVariantType } from '../Button'
716
import { Icon } from '../Icon'
817
import { AppStatusBody } from './AppStatusBody'
18+
import { getAppDetails } from './service'
919
import { AppStatusModalProps } from './types'
1020

1121
import './AppStatusModal.scss'
@@ -14,12 +24,82 @@ const AppStatusModal = ({
1424
title,
1525
handleClose,
1626
type,
17-
appDetails,
27+
appDetails: appDetailsProp,
1828
isConfigDriftEnabled,
1929
configDriftModal: ConfigDriftModal,
30+
appId,
31+
envId,
2032
}: AppStatusModalProps) => {
2133
const [showConfigDriftModal, setShowConfigDriftModal] = useState(false)
2234

35+
const abortControllerRef = useRef<AbortController>(new AbortController())
36+
const pollingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
37+
38+
const getAppDetailsWrapper = async () => {
39+
const response = await abortPreviousRequests(
40+
() => getAppDetails(appId, envId, abortControllerRef),
41+
abortControllerRef,
42+
)
43+
44+
return response
45+
}
46+
47+
const [
48+
areInitialAppDetailsLoading,
49+
fetchedAppDetails,
50+
fetchedAppDetailsError,
51+
reloadInitialAppDetails,
52+
setFetchedAppDetails,
53+
] = useAsync(getAppDetailsWrapper, [appId, envId, type], type === 'release')
54+
55+
const handleExternalSync = async () => {
56+
try {
57+
const response = await getAppDetailsWrapper()
58+
setFetchedAppDetails(response)
59+
60+
pollingTimeoutRef.current = setTimeout(
61+
() => {
62+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
63+
handleExternalSync()
64+
},
65+
Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || 30000,
66+
)
67+
} catch {
68+
// Do nothing
69+
}
70+
}
71+
72+
const areInitialAppDetailsLoadingWithAbortedError =
73+
areInitialAppDetailsLoading || getIsRequestAborted(fetchedAppDetailsError)
74+
75+
const appDetails = type === 'release' ? fetchedAppDetails : appDetailsProp
76+
77+
// Adding useEffect to initiate timer for external sync and clear it on unmount
78+
useEffect(() => {
79+
if (
80+
!areInitialAppDetailsLoading &&
81+
!fetchedAppDetailsError &&
82+
fetchedAppDetails &&
83+
!pollingTimeoutRef.current
84+
) {
85+
pollingTimeoutRef.current = setTimeout(
86+
() => {
87+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
88+
handleExternalSync()
89+
},
90+
Number(window._env_.DEVTRON_APP_DETAILS_POLLING_INTERVAL) || 30000,
91+
)
92+
}
93+
94+
return () => {
95+
if (pollingTimeoutRef.current) {
96+
clearTimeout(pollingTimeoutRef.current)
97+
}
98+
99+
abortControllerRef.current.abort()
100+
}
101+
}, [areInitialAppDetailsLoading, fetchedAppDetails, fetchedAppDetailsError])
102+
23103
const handleShowConfigDriftModal = isConfigDriftEnabled
24104
? () => {
25105
setShowConfigDriftModal(true)
@@ -64,11 +144,43 @@ const AppStatusModal = ({
64144
</div>
65145

66146
<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-
/>
147+
<APIResponseHandler
148+
isLoading={areInitialAppDetailsLoadingWithAbortedError}
149+
progressingProps={{
150+
pageLoader: true,
151+
}}
152+
error={fetchedAppDetailsError}
153+
errorScreenManagerProps={{
154+
code: fetchedAppDetailsError?.code,
155+
reload: reloadInitialAppDetails,
156+
}}
157+
>
158+
<AppStatusBody
159+
appDetails={appDetails}
160+
type={type}
161+
handleShowConfigDriftModal={handleShowConfigDriftModal}
162+
/>
163+
164+
{type === 'stack-manager' && (
165+
<div className="bg__primary flexbox dc__content-space dc__border-top p-16 fs-13 fw-6">
166+
<span className="fs-13 fw-6">Facing issues in installing integration?</span>
167+
168+
<Button
169+
dataTestId="chat-with-support-button"
170+
component={ButtonComponentType.anchor}
171+
anchorProps={{
172+
href: DISCORD_LINK,
173+
target: '_blank',
174+
rel: 'noreferrer noopener',
175+
}}
176+
// TODO: Can we use Icon component
177+
icon={<ICChatCircleDots />}
178+
ariaLabel="Chat with support"
179+
showAriaLabelInTippy={false}
180+
/>
181+
</div>
182+
)}
183+
</APIResponseHandler>
72184
</div>
73185
</div>
74186
</Drawer>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { get, getIsRequestAborted } from '@Common/API'
2+
import { ROUTES } from '@Common/Constants'
3+
import { showError } from '@Common/Helper'
4+
import { APIOptions } from '@Common/Types'
5+
6+
export const getAppDetails = async (
7+
appId: number,
8+
envId: number,
9+
abortControllerRef: APIOptions['abortControllerRef'],
10+
) => {
11+
try {
12+
const [appDetails, resourceTree] = await Promise.all([
13+
get(`${ROUTES.APP_DETAIL}/v2?app-id=${appId}&env-id=${envId}`, {
14+
abortControllerRef,
15+
}),
16+
get(`${ROUTES.APP_DETAIL}/resource-tree?app-id=${appId}&env-id=${envId}`, {
17+
abortControllerRef,
18+
}),
19+
])
20+
21+
return {
22+
...(appDetails.result || {}),
23+
resourceTree: resourceTree.result,
24+
}
25+
} catch (error) {
26+
if (getIsRequestAborted(error)) {
27+
showError(error)
28+
}
29+
throw error
30+
}
31+
}

src/Shared/Components/AppStatusModal/types.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,31 @@ import { FunctionComponent, ReactNode } from 'react'
22

33
import { AppDetails, ConfigDriftModalProps } from '@Shared/types'
44

5-
export interface AppStatusModalProps {
5+
export type AppStatusModalProps = {
66
title: ReactNode
77
handleClose: () => void
8-
type: 'devtron-app' | 'external-apps' | 'stack-manager' | 'release'
9-
/**
10-
* If not given would assume to hide config drift related info
11-
*/
12-
handleShowConfigDriftModal: () => void | null
138
/**
149
* If given would not poll for app details and resource tree, Polling for gitops timeline would still be done
1510
*/
1611
appDetails?: AppDetails
1712
isConfigDriftEnabled: boolean
1813
configDriftModal: FunctionComponent<ConfigDriftModalProps>
19-
}
14+
} & (
15+
| {
16+
type: 'release'
17+
appId: number
18+
envId: number
19+
}
20+
| {
21+
type: 'devtron-app' | 'external-apps' | 'stack-manager'
22+
appId?: never
23+
envId?: never
24+
}
25+
)
2026

21-
export interface AppStatusBodyProps
22-
extends Pick<AppStatusModalProps, 'appDetails' | 'type' | 'handleShowConfigDriftModal'> {}
27+
export interface AppStatusBodyProps extends Pick<AppStatusModalProps, 'appDetails' | 'type'> {
28+
handleShowConfigDriftModal: () => void
29+
}
2330

2431
export interface AppStatusContentProps extends Pick<AppStatusBodyProps, 'appDetails' | 'handleShowConfigDriftModal'> {
2532
/**

src/Shared/Components/CICDHistory/AppStatusDetailsChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ const AppStatusDetailsChart = ({
8888
if (
8989
_appDetails.resourceTree?.resourcesSyncResult &&
9090
// eslint-disable-next-line no-prototype-builtins
91-
_appDetails.resourceTree?.resourcesSyncResult.hasOwnProperty(`${kind}/${name}`)
91+
_appDetails.resourceTree.resourcesSyncResult.hasOwnProperty(`${kind}/${name}`)
9292
) {
9393
return _appDetails.resourceTree.resourcesSyncResult[`${kind}/${name}`]
9494
}

src/Shared/Components/CICDHistory/StatusFilterButtonComponent.scss

Lines changed: 0 additions & 77 deletions
This file was deleted.

0 commit comments

Comments
 (0)