Skip to content

Commit 12f9a58

Browse files
Merge pull request #976 from devtron-labs/feat/resource-conflict-main
feat: resource conflict
2 parents 18ef98a + 0f1d57f commit 12f9a58

17 files changed

Lines changed: 696 additions & 130 deletions

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@devtron-labs/devtron-fe-common-lib",
3-
"version": "1.21.0",
3+
"version": "1.21.1",
44
"description": "Supporting common component library",
55
"type": "module",
66
"main": "dist/index.js",

src/Common/API/reactQueryHooks.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,18 @@ import {
2828
import { ServerErrors } from '@Common/ServerError'
2929
import { ResponseType } from '@Common/Types'
3030

31-
export const useQuery = <TQueryFnData = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(
32-
options: UseQueryOptions<ResponseType<TQueryFnData>, ServerErrors, TData, TQueryKey>,
31+
export const useQuery = <
32+
TQueryFnData = unknown,
33+
TData = TQueryFnData,
34+
TQueryKey extends QueryKey = QueryKey,
35+
WrapWithResponseType extends boolean = true,
36+
>(
37+
options: UseQueryOptions<
38+
WrapWithResponseType extends true ? ResponseType<TQueryFnData> : TQueryFnData,
39+
ServerErrors,
40+
TData,
41+
TQueryKey
42+
>,
3343
): UseQueryResult<TData, ServerErrors> => rqUseQuery(options)
3444

3545
export const useMutation = <TData = unknown, TVariables = void, TContext = unknown>(

src/Common/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export const ROUTES = {
162162
LICENSE_DATA: 'license/data',
163163
ENV: 'env',
164164
APP_METADATA: 'app-metadata',
165+
RESOURCE_CONFLICTS_LIST: 'app/:appId/cd-pipeline/:pipelineId/history/:wfrId/helm-ownership-conflicts',
165166
} as const
166167

167168
export enum KEY_VALUE {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[id*="table__resource-conflict-details"] {
2+
.generic-table__row {
3+
.generic-table__cell {
4+
display: flex;
5+
align-items: center;
6+
padding: 10px 0;
7+
}
8+
}
9+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useMemo } from 'react'
2+
3+
import { FiltersTypeEnum, PaginationEnum, Table, TableViewWrapperProps } from '../Table'
4+
import { RowsType } from '../Table/types'
5+
import { CONFLICTED_RESOURCES_COLUMNS } from './constants'
6+
import { ConflictedResourcesTableProps, ResourceConflictItemType } from './types'
7+
8+
import './ConflictedResourcesTable.scss'
9+
10+
const Wrapper = ({ children }: TableViewWrapperProps<unknown, FiltersTypeEnum.STATE>) => (
11+
<div className="dc__overflow-hidden flexbox-col flex-grow-1">{children}</div>
12+
)
13+
const filter = () => true
14+
15+
const ConflictedResourcesTable = ({ resourceConflictDetails }: ConflictedResourcesTableProps) => {
16+
const rows: RowsType<ResourceConflictItemType> = useMemo(
17+
() =>
18+
(resourceConflictDetails || []).map<RowsType<ResourceConflictItemType>[number]>((resource) => ({
19+
data: resource,
20+
id: resource.id,
21+
})),
22+
[resourceConflictDetails],
23+
)
24+
25+
return (
26+
<Table<ResourceConflictItemType, FiltersTypeEnum.STATE>
27+
id="table__resource-conflict-details"
28+
columns={CONFLICTED_RESOURCES_COLUMNS}
29+
rows={rows}
30+
emptyStateConfig={{
31+
noRowsConfig: {
32+
title: 'No resource found',
33+
},
34+
noRowsForFilterConfig: {
35+
title: 'No results',
36+
subTitle: "We couldn't find any matching results",
37+
},
38+
}}
39+
paginationVariant={PaginationEnum.NOT_PAGINATED}
40+
additionalFilterProps={{
41+
initialSortKey: 'name' satisfies keyof ResourceConflictItemType,
42+
}}
43+
filtersVariant={FiltersTypeEnum.STATE}
44+
ViewWrapper={Wrapper}
45+
filter={filter}
46+
/>
47+
)
48+
}
49+
50+
export default ConflictedResourcesTable
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useState } from 'react'
2+
import { useHistory, useParams } from 'react-router-dom'
3+
4+
import { URLS } from '@Common/Constants'
5+
import { showError } from '@Common/Helper'
6+
7+
import { ButtonStyleType } from '../Button'
8+
import { ConfirmationModal, ConfirmationModalVariantType } from '../ConfirmationModal'
9+
import { Icon } from '../Icon'
10+
import { resourceConflictRedeploy } from './service'
11+
import { ResourceConflictDeployDialogProps, ResourceConflictDeployDialogURLParamsType } from './types'
12+
13+
const ResourceConflictDeployDialog = ({ appName, environmentName, handleClose }: ResourceConflictDeployDialogProps) => {
14+
const history = useHistory()
15+
const { appId, envId, pipelineId, triggerId } = useParams<ResourceConflictDeployDialogURLParamsType>()
16+
const [isLoading, setIsLoading] = useState(false)
17+
18+
const handleDeploy = async () => {
19+
setIsLoading(true)
20+
try {
21+
await resourceConflictRedeploy({
22+
pipelineId,
23+
triggerId,
24+
appId,
25+
})
26+
setIsLoading(false)
27+
history.push(`${URLS.APP}/${appId}/details/${envId}`)
28+
} catch (error) {
29+
showError(error)
30+
setIsLoading(false)
31+
}
32+
}
33+
34+
return (
35+
<ConfirmationModal
36+
variant={ConfirmationModalVariantType.custom}
37+
Icon={<Icon name="ic-warning" color={null} size={48} />}
38+
title="Take resource ownership and redeploy"
39+
subtitle={`Ensure the resources belong to the '${appName}' application and the '${environmentName}' environment to avoid destructive changes.`}
40+
handleClose={handleClose}
41+
buttonConfig={{
42+
secondaryButtonConfig: {
43+
text: 'Cancel',
44+
onClick: handleClose,
45+
},
46+
primaryButtonConfig: {
47+
isLoading,
48+
text: 'Re-deploy',
49+
onClick: handleDeploy,
50+
style: ButtonStyleType.warning,
51+
},
52+
}}
53+
/>
54+
)
55+
}
56+
57+
export default ResourceConflictDeployDialog
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { useState } from 'react'
2+
import { useHistory, useParams } from 'react-router-dom'
3+
4+
import { useQuery } from '@Common/API'
5+
import { URLS } from '@Common/Constants'
6+
import { Drawer } from '@Common/Drawer'
7+
import { showError, stopPropagation } from '@Common/Helper'
8+
import { ComponentSizeType } from '@Shared/constants'
9+
10+
import { APIResponseHandler } from '../APIResponseHandler'
11+
import { Button, ButtonStyleType, ButtonVariantType } from '../Button'
12+
import { Icon } from '../Icon'
13+
import ConflictedResourcesTable from './ConflictedResourcesTable'
14+
import { getResourceConflictDetails, resourceConflictRedeploy } from './service'
15+
import {
16+
ResourceConflictDeployDialogURLParamsType,
17+
ResourceConflictDetailsModalProps,
18+
ResourceConflictItemType,
19+
} from './types'
20+
21+
const ResourceConflictDetailsModal = ({ appName, environmentName, handleClose }: ResourceConflictDetailsModalProps) => {
22+
const { appId, envId, pipelineId, triggerId } = useParams<ResourceConflictDeployDialogURLParamsType>()
23+
const history = useHistory()
24+
25+
const {
26+
isFetching: isLoadingResourceData,
27+
data: resourceConflictDetails,
28+
refetch: refetchResourceConflictDetails,
29+
error: resourceConflictDetailsError,
30+
} = useQuery<ResourceConflictItemType[], ResourceConflictItemType[], [string, string, string, string], false>({
31+
queryKey: ['getResourceConflictDetails', pipelineId, triggerId, appId],
32+
queryFn: ({ signal }) => getResourceConflictDetails({ pipelineId, triggerId, appId, signal }),
33+
})
34+
35+
const [isDeploying, setIsDeploying] = useState(false)
36+
37+
const handleDeploy = async () => {
38+
setIsDeploying(true)
39+
try {
40+
await resourceConflictRedeploy({
41+
pipelineId,
42+
triggerId,
43+
appId,
44+
})
45+
setIsDeploying(false)
46+
history.push(`${URLS.APP}/${appId}/details/${envId}`)
47+
} catch (error) {
48+
showError(error)
49+
setIsDeploying(false)
50+
}
51+
}
52+
53+
return (
54+
<Drawer width="800px" onClose={handleClose} onEscape={handleClose} position="right">
55+
<div
56+
className="flexbox-col dc__content-space h-100 bg__modal--primary shadow__modal dc__overflow-auto"
57+
onClick={stopPropagation}
58+
>
59+
<div className="flexbox-col dc__overflow-auto flex-grow-1">
60+
<div className="px-20 py-12 flexbox dc__content-space dc__align-items-center border__primary--bottom">
61+
<h2 className="m-0 fs-16 fw-6 lh-24 cn-9">Resources with conflict</h2>
62+
63+
<Button
64+
dataTestId="header-close-button"
65+
ariaLabel="Close"
66+
showAriaLabelInTippy={false}
67+
onClick={handleClose}
68+
variant={ButtonVariantType.borderLess}
69+
style={ButtonStyleType.negativeGrey}
70+
icon={<Icon name="ic-close-large" color={null} />}
71+
size={ComponentSizeType.xs}
72+
/>
73+
</div>
74+
75+
<div className="flexbox-col flex-grow-1 dc__overflow-auto w-100">
76+
<APIResponseHandler
77+
isLoading={isLoadingResourceData}
78+
progressingProps={{
79+
pageLoader: true,
80+
}}
81+
error={resourceConflictDetailsError}
82+
errorScreenManagerProps={{
83+
code: resourceConflictDetailsError?.code,
84+
reload: refetchResourceConflictDetails,
85+
on404Redirect: handleClose,
86+
}}
87+
>
88+
<ConflictedResourcesTable resourceConflictDetails={resourceConflictDetails} />
89+
</APIResponseHandler>
90+
</div>
91+
</div>
92+
93+
<div className="flexbox dc__align-items-center dc__content-space dc__gap-20 py-16 px-20 border__primary--top dc__no-shrink">
94+
<div className="flexbox dc__gap-8">
95+
<Icon name="ic-warning" size={20} color={null} />
96+
<div className="flexbox-col">
97+
<span className="cn-9 fs-13 fw-6 lh-1-5">Take resource ownership and redeploy</span>
98+
<span>
99+
Ensure all resources strictly belong to the {appName} application and the&nbsp;
100+
{environmentName}
101+
environment. Any resource outside this Helm release may cause incorrect associations and
102+
potentially destructive changes.
103+
</span>
104+
</div>
105+
</div>
106+
<div className="dc__no-shrink">
107+
<Button
108+
dataTestId="footer-redeploy-button"
109+
variant={ButtonVariantType.primary}
110+
style={ButtonStyleType.warning}
111+
size={ComponentSizeType.large}
112+
text="Re-deploy"
113+
startIcon={<Icon name="ic-rocket-launch" color={null} />}
114+
isLoading={isDeploying}
115+
disabled={!resourceConflictDetails?.length}
116+
onClick={handleDeploy}
117+
/>
118+
</div>
119+
</div>
120+
</div>
121+
</Drawer>
122+
)
123+
}
124+
125+
export default ResourceConflictDetailsModal

src/Shared/Components/CICDHistory/Sidebar.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ import {
4141
HistorySummaryCardType,
4242
SidebarType,
4343
} from './types'
44-
import { getHistoryItemStatusIconFromWorkflowStages, getTriggerStatusIcon, getWorkflowNodeStatusTitle } from './utils'
44+
import {
45+
getHistoryItemStatusIconFromWorkflowStages,
46+
getSortedTriggerHistory,
47+
getTriggerStatusIcon,
48+
getWorkflowNodeStatusTitle,
49+
} from './utils'
4550

4651
/**
4752
* @description To be shown on deployment history or when we don't have workflowExecutionStages
@@ -378,30 +383,28 @@ const Sidebar = React.memo(
378383
{fetchIdData === FetchIdDataStatus.SUCCESS && (
379384
<ViewAllCardsTile handleViewAllHistory={handleViewAllHistory} />
380385
)}
381-
{Array.from(triggerHistory)
382-
.sort(([a], [b]) => b - a)
383-
.map(([triggerId, triggerDetails], index) => (
384-
<HistorySummaryCard
385-
dataTestId={`deployment-history-${index}`}
386-
key={triggerId}
387-
id={triggerId}
388-
status={triggerDetails.status}
389-
startedOn={triggerDetails.startedOn}
390-
triggeredBy={triggerDetails.triggeredBy}
391-
triggeredByEmail={triggerDetails.triggeredByEmail}
392-
ciMaterials={triggerDetails.ciMaterials}
393-
gitTriggers={triggerDetails.gitTriggers}
394-
artifact={triggerDetails.artifact}
395-
stage={triggerDetails.stage}
396-
type={type}
397-
runSource={triggerDetails.runSource}
398-
renderRunSource={renderRunSource}
399-
resourceId={resourceId}
400-
workflowExecutionStages={triggerDetails.workflowExecutionStages}
401-
podName={triggerDetails.podName}
402-
namespace={triggerDetails.namespace}
403-
/>
404-
))}
386+
{getSortedTriggerHistory(triggerHistory).map(([triggerId, triggerDetails], index) => (
387+
<HistorySummaryCard
388+
dataTestId={`deployment-history-${index}`}
389+
key={triggerId}
390+
id={triggerId}
391+
status={triggerDetails.status}
392+
startedOn={triggerDetails.startedOn}
393+
triggeredBy={triggerDetails.triggeredBy}
394+
triggeredByEmail={triggerDetails.triggeredByEmail}
395+
ciMaterials={triggerDetails.ciMaterials}
396+
gitTriggers={triggerDetails.gitTriggers}
397+
artifact={triggerDetails.artifact}
398+
stage={triggerDetails.stage}
399+
type={type}
400+
runSource={triggerDetails.runSource}
401+
renderRunSource={renderRunSource}
402+
resourceId={resourceId}
403+
workflowExecutionStages={triggerDetails.workflowExecutionStages}
404+
podName={triggerDetails.podName}
405+
namespace={triggerDetails.namespace}
406+
/>
407+
))}
405408
{hasMore && (fetchIdData === FetchIdDataStatus.SUSPEND || !fetchIdData) && (
406409
<DetectBottom callback={reloadNextAfterBottom} />
407410
)}

0 commit comments

Comments
 (0)