Skip to content

Commit 4ed3e08

Browse files
committed
feat: move BulkOperations to common-lib from fe-lib
1 parent 34f6bcb commit 4ed3e08

File tree

17 files changed

+791
-54
lines changed

17 files changed

+791
-54
lines changed
Lines changed: 7 additions & 0 deletions
Loading

src/Common/Constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ export const ROUTES = {
129129
PLUGIN_GLOBAL_VARIABLES: 'plugin/global/list/global-variable',
130130
CONFIG_COMPARE_SECRET: 'config/compare/secret',
131131
CD_TRIGGER_POST: 'app/cd-pipeline/trigger',
132+
DELETE_RESOURCE: 'k8s/resource/delete',
133+
NODE_CAPACITY: 'k8s/capacity/node',
132134
}
133135

134136
export enum KEY_VALUE {

src/Pages/App/AppDetails/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './types'

src/Pages/App/AppDetails/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface OptionsBase {
2+
name: string
3+
isInitContainer?: boolean
4+
isEphemeralContainer?: boolean
5+
isExternal?: boolean
6+
}
7+
8+
export interface SelectedResourceType {
9+
clusterId: number
10+
group: string
11+
version: string
12+
kind: string
13+
namespace: string
14+
name: string
15+
containers: OptionsBase[]
16+
selectedContainer?: string
17+
clusterName?: string
18+
}

src/Pages/App/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './AppConfiguration'
22
export * from './Trigger'
3+
export * from './AppDetails'

src/Pages/ResourceBrowser/ResourceBrowser.Types.ts

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { NodeType, Nodes } from '@Shared/types'
18-
import { MutableRefObject, RefObject } from 'react'
18+
import { RefObject } from 'react'
1919

2020
export interface GVKType {
2121
Group: string
@@ -76,39 +76,6 @@ export interface BulkSelectionActionWidgetProps {
7676
showBulkRestartOption: boolean
7777
}
7878

79-
interface BulkOperationAdditionalKeysType {
80-
label: string
81-
value: string
82-
isSortable: boolean
83-
/**
84-
* width to be given in gridTemplateColumns
85-
*/
86-
width: string
87-
}
88-
89-
export interface BulkOperation {
90-
name: string
91-
/**
92-
* Would these keys beside the name
93-
*/
94-
additionalKeys?: BulkOperationAdditionalKeysType[]
95-
operation: (abortControllerRef: MutableRefObject<AbortController>, data?: unknown) => Promise<void>
96-
}
97-
98-
export type BulkOperationModalProps = {
99-
operationType: 'restart' | 'delete' | 'creation' | 'deploy'
100-
clusterName?: string
101-
operations: NonNullable<BulkOperation[]>
102-
handleModalClose: () => void
103-
resourceKind: string
104-
handleReloadDataAfterBulkOperation?: () => void
105-
hideResultsDrawer?: boolean
106-
shouldAllowForceOperation?: true
107-
shouldSkipConfirmation?: true
108-
}
109-
110-
export type BulkOperationModalState = BulkOperationModalProps['operationType'] | 'closed'
111-
11279
export interface CreateResourceRequestBodyType {
11380
appId: string
11481
clusterId: number
@@ -145,3 +112,30 @@ export interface CreateResourceDTO {
145112
isUpdate: boolean
146113
error: string
147114
}
115+
116+
export interface ResourceListPayloadType {
117+
clusterId: number
118+
k8sRequest: {
119+
resourceIdentifier: {
120+
groupVersionKind: GVKType
121+
namespace?: string
122+
name?: string
123+
}
124+
patch?: string
125+
forceDelete?: boolean
126+
}
127+
}
128+
129+
export interface ResourceType {
130+
kind: string
131+
name: string
132+
isUpdate: boolean
133+
error: string
134+
}
135+
136+
export interface NodeActionRequest {
137+
clusterId?: number
138+
name: string
139+
version: string
140+
kind: string
141+
}

src/Pages/ResourceBrowser/service.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import { post } from '@Common/Api'
1+
import { post, trash } from '@Common/Api'
22
import { ROUTES } from '@Common/Constants'
33
import { ResponseType } from '@Common/Types'
4+
import { MutableRefObject } from 'react'
45
import {
56
CreateResourceDTO,
67
CreateResourcePayload,
78
K8sResourceDetailType,
89
K8sResourceListPayloadType,
10+
NodeActionRequest,
11+
ResourceListPayloadType,
12+
ResourceType,
913
} from './ResourceBrowser.Types'
1014

1115
export const getK8sResourceList = (
@@ -19,3 +23,13 @@ export const getK8sResourceList = (
1923
export const createNewResource = (
2024
resourceListPayload: CreateResourcePayload,
2125
): Promise<ResponseType<CreateResourceDTO[]>> => post(ROUTES.K8S_RESOURCE_CREATE, resourceListPayload)
26+
27+
export const deleteResource = (
28+
resourceListPayload: ResourceListPayloadType,
29+
abortControllerRef?: MutableRefObject<AbortController>,
30+
): Promise<ResponseType<ResourceType[]>> => post(ROUTES.DELETE_RESOURCE, resourceListPayload, { abortControllerRef })
31+
32+
export const deleteNodeCapacity = (
33+
requestPayload: NodeActionRequest,
34+
abortControllerRef?: MutableRefObject<AbortController>,
35+
): Promise<ResponseType> => trash(ROUTES.NODE_CAPACITY, requestPayload, { abortControllerRef })
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { Prompt } from 'react-router-dom'
2+
import { useEffect, useRef, useState } from 'react'
3+
import { getIsRequestAborted } from '@Common/Api'
4+
import { showError } from '@Common/Helper'
5+
import { ServerErrors } from '@Common/ServerError'
6+
import { ApiQueuingWithBatch } from '@Shared/API'
7+
import { usePrompt } from '@Shared/Hooks'
8+
import { ConfirmationModal, ToastManager, ToastVariantType } from '@Shared/index'
9+
import { noop } from 'rxjs'
10+
import { OperationResultStore } from './OperationResultStore'
11+
import BulkOperationsResultModal from './BulkOperationsResultModal'
12+
import { OperationResultStoreType, BulkOperationModalProps } from './types'
13+
import './styles.scss'
14+
15+
const BulkOperations = ({
16+
handleModalClose: handleModalCloseProp,
17+
operations: userOperations = [],
18+
getResultsChartSummaryText,
19+
handleReloadDataAfterBulkOperation = noop,
20+
hideResultsDrawer = false,
21+
textConfig,
22+
shouldSkipConfirmation,
23+
confirmationModalConfig,
24+
data,
25+
}: BulkOperationModalProps) => {
26+
const [apiCallInProgress, setApiCallInProgress] = useState(!!shouldSkipConfirmation)
27+
const [, setLastUpdateTimeStamp] = useState(new Date().toISOString())
28+
const operationsRef = useRef<BulkOperationModalProps['operations']>(userOperations)
29+
const resultsStoreRef = useRef<OperationResultStoreType>(new OperationResultStore(operationsRef.current.length))
30+
const abortControllerRef = useRef<AbortController>(new AbortController())
31+
32+
const showResultsDrawer = !hideResultsDrawer && !!resultsStoreRef.current.getSize()
33+
34+
usePrompt({
35+
shouldPrompt: apiCallInProgress,
36+
})
37+
38+
const handleModalClose = () => {
39+
const hasAnyOperationSucceeded = resultsStoreRef.current.getHasAnyOperationSucceeded()
40+
41+
if (hasAnyOperationSucceeded || hideResultsDrawer) {
42+
handleReloadDataAfterBulkOperation()
43+
}
44+
45+
handleModalCloseProp()
46+
}
47+
48+
const handleBulkOperations = async () => {
49+
try {
50+
setApiCallInProgress(true)
51+
52+
let timeout = -1
53+
54+
const triggerUpdate = () => {
55+
if (timeout >= 0) {
56+
return
57+
}
58+
59+
timeout = setTimeout(() => {
60+
setLastUpdateTimeStamp(new Date().toISOString())
61+
62+
timeout = -1
63+
}, 10)
64+
}
65+
66+
const calls = operationsRef.current.map((op) => async () => {
67+
const { operation, name, additionalKeys } = op
68+
69+
if (abortControllerRef.current.signal.aborted) {
70+
throw new Error('bulk operations aborted')
71+
}
72+
73+
let id = -1
74+
75+
try {
76+
id = resultsStoreRef.current.addResult({
77+
name,
78+
additionalKeys,
79+
status: 'Progressing',
80+
message: '-',
81+
})
82+
83+
triggerUpdate()
84+
85+
await operation(abortControllerRef, data)
86+
87+
resultsStoreRef.current.updateResultStatus(id, { status: 'Completed' })
88+
89+
triggerUpdate()
90+
} catch (err) {
91+
if (getIsRequestAborted(err)) {
92+
resultsStoreRef.current.updateResultStatus(id, {
93+
status: 'Failed',
94+
message: 'Aborted by you',
95+
retryOperation: op,
96+
})
97+
98+
triggerUpdate()
99+
100+
return
101+
}
102+
103+
resultsStoreRef.current.updateResultStatus(id, {
104+
status: 'Failed',
105+
message:
106+
(err &&
107+
(err.message ||
108+
(err instanceof ServerErrors &&
109+
Array.isArray(err.errors) &&
110+
err.errors[0].userMessage))) ??
111+
'',
112+
retryOperation: op,
113+
})
114+
115+
triggerUpdate()
116+
}
117+
})
118+
119+
await ApiQueuingWithBatch(calls, true)
120+
121+
ToastManager.showToast({
122+
variant: ToastVariantType.info,
123+
description: 'Bulk action completed',
124+
})
125+
126+
if (hideResultsDrawer) {
127+
handleModalClose()
128+
}
129+
} catch (err) {
130+
showError(err)
131+
} finally {
132+
setApiCallInProgress(false)
133+
}
134+
}
135+
136+
// NOTE: this should really only be run at mount
137+
// therefore empty dependency array
138+
useEffect(() => {
139+
if (shouldSkipConfirmation) {
140+
handleBulkOperations().then(noop).catch(noop)
141+
}
142+
143+
return () => {
144+
abortControllerRef.current?.abort()
145+
}
146+
}, [])
147+
148+
const handleDrawerClose = apiCallInProgress ? noop : handleModalClose
149+
150+
const handleRetryFailedOperations = async () => {
151+
operationsRef.current = resultsStoreRef.current.getRetryOperations()
152+
resultsStoreRef.current = new OperationResultStore(operationsRef.current.length)
153+
154+
abortControllerRef.current = new AbortController()
155+
156+
await handleBulkOperations()
157+
}
158+
159+
const handleAbortBulkOperation = () => {
160+
abortControllerRef.current.abort()
161+
}
162+
163+
const renderConfirmationDialog = () => (
164+
<ConfirmationModal
165+
{...{
166+
...confirmationModalConfig,
167+
handleClose: handleModalClose,
168+
showConfirmationModal: true,
169+
buttonConfig: {
170+
primaryButtonConfig: {
171+
...confirmationModalConfig.buttonConfig.primaryButtonConfig,
172+
onClick: handleBulkOperations,
173+
disabled: apiCallInProgress,
174+
isLoading: apiCallInProgress,
175+
},
176+
secondaryButtonConfig: {
177+
...confirmationModalConfig.buttonConfig.secondaryButtonConfig,
178+
onClick: handleModalClose,
179+
disabled: apiCallInProgress,
180+
},
181+
},
182+
}}
183+
/>
184+
)
185+
186+
const renderResultsDrawer = () => (
187+
<BulkOperationsResultModal
188+
apiCallInProgress={apiCallInProgress}
189+
handleAbortBulkOperation={handleAbortBulkOperation}
190+
handleModalClose={handleDrawerClose}
191+
isOperationAborted={abortControllerRef.current?.signal.aborted ?? false}
192+
resultsStore={resultsStoreRef.current}
193+
handleRetryFailedOperations={handleRetryFailedOperations}
194+
getResultsChartSummaryText={getResultsChartSummaryText}
195+
resultsHeader={textConfig.resultsHeader}
196+
/>
197+
)
198+
199+
return (
200+
<div className="bulk-operations">
201+
<Prompt when={apiCallInProgress} message={textConfig.prompt} />
202+
{!shouldSkipConfirmation && !showResultsDrawer ? renderConfirmationDialog() : renderResultsDrawer()}
203+
</div>
204+
)
205+
}
206+
207+
export default BulkOperations

0 commit comments

Comments
 (0)