Skip to content

Commit 9f36ee5

Browse files
authored
feat: Information about associated resources in delete modal for modules (#3576)
* add list items * somehow it works * remove unused import * add navigating on the associated resources list * diable delete is some assosiated resource exist * add warning about associated resources * move info to translations * adjust warning message * fix path to loading translation * move list title to translations' * fix typo
1 parent ad8c021 commit 9f36ee5

File tree

4 files changed

+220
-69
lines changed

4 files changed

+220
-69
lines changed

public/i18n/en.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,7 @@ kubeconfig-id:
749749
error: "Couldn't load kubeconfig ID; configuration not changed (Error: ${{error}})"
750750
must-be-an-object: Kubeconfig must be a JSON or YAML object.
751751
kyma-modules:
752+
associated-resources: Associated Resources
752753
unmanaged-modules-info: One of the modules is not managed and may not work properly. We cannot guarantee any service level agreement (SLA) or provide updates and maintenance for the module.
753754
unmanaged-modules-save-warning: Before proceeding, be aware that disabling module management may impact the stability and data integrity of your cluster. Once the management is disabled, reverting back may not be possible. Are you sure you want to continue?
754755
unmanaged-modules-warning: Disabling this option can lead to potential instability and loss of data within your cluster. Proceed with caution. Once disabled, it may not be possible to revert back.
@@ -775,6 +776,7 @@ kyma-modules:
775776
no-version: No version available
776777
channel-overridden: Overridden
777778
managed: Managed
779+
associated-resources-warning: If there are resources left, remove them first to delete the module.
778780
beta: Beta
779781
beta-alert: "CAUTION: The Service Level Agreements (SLAs) and Support obligations do not apply to Beta modules and functionalities. If Beta modules or functionalities directly or indirectly affect other modules, the Service Level Agreements and Support for these modules are limited to priority levels P3 (Medium) or P4 (Low). Thus, Beta releases are not intended for use in customer production environments."
780782
change: Change

src/components/KymaModules/KymaModulesList.js

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,21 @@ import {
99
FlexBox,
1010
Text,
1111
Badge,
12+
List,
13+
StandardListItem,
14+
MessageStrip,
1215
} from '@ui5/webcomponents-react';
1316

1417
import { HintButton } from 'shared/components/DescriptionHint/DescriptionHint';
1518
import { spacing } from '@ui5/webcomponents-react-base';
16-
import { useState } from 'react';
19+
import { useEffect, useState } from 'react';
1720
import { GenericList } from 'shared/components/GenericList/GenericList';
18-
import { useGet, useGetList } from 'shared/hooks/BackendAPI/useGet';
21+
import {
22+
useGet,
23+
useGetList,
24+
useGetScope,
25+
useSingleGet,
26+
} from 'shared/hooks/BackendAPI/useGet';
1927
import { ExternalLink } from 'shared/components/ExternalLink/ExternalLink';
2028
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';
2129
import KymaModulesCreate from './KymaModulesCreate';
@@ -36,6 +44,7 @@ import { isFormOpenState } from 'state/formOpenAtom';
3644
import { ModuleStatus } from './components/ModuleStatus';
3745
import { cloneDeep } from 'lodash';
3846
import { StatusBadge } from 'shared/components/StatusBadge/StatusBadge';
47+
import { useNavigate } from 'react-router-dom';
3948

4049
export default function KymaModulesList({
4150
DeleteMessageBox,
@@ -62,6 +71,9 @@ export default function KymaModulesList({
6271
const setLayoutColumn = useSetRecoilState(columnLayoutState);
6372
const setIsFormOpen = useSetRecoilState(isFormOpenState);
6473
const { clusterUrl, namespaceUrl } = useUrl();
74+
const fetch = useSingleGet();
75+
const getScope = useGetScope();
76+
const navigate = useNavigate();
6577

6678
const { data: kymaExt } = useGetList(
6779
ext => ext.metadata.labels['app.kubernetes.io/part-of'] === 'Kyma',
@@ -378,11 +390,128 @@ export default function KymaModulesList({
378390
);
379391
};
380392

393+
const getAssociatedResources = () => {
394+
const module = findModule(
395+
selectedModules[chosenModuleIndex]?.name,
396+
selectedModules[chosenModuleIndex]?.channel ||
397+
kymaResource?.spec?.channel,
398+
selectedModules[chosenModuleIndex]?.version ||
399+
findStatus(selectedModules[chosenModuleIndex]?.name)?.version,
400+
);
401+
402+
return module?.spec?.associatedResources || [];
403+
};
404+
405+
const getNumberOfResources = async (kind, group, version) => {
406+
const url =
407+
group === 'v1'
408+
? '/api/v1'
409+
: `/apis/${group}/${version}/${pluralize(kind.toLowerCase())}`;
410+
411+
try {
412+
const response = await fetch(url);
413+
const json = await response.json();
414+
return json.items.length;
415+
} catch (e) {
416+
console.warn(e);
417+
return 'Error';
418+
}
419+
};
420+
421+
const handleItemClick = async (kind, group, version) => {
422+
const isNamespaced = await getScope(group, version, kind);
423+
const path = `${pluralize(kind.toLowerCase())}`;
424+
const link = isNamespaced
425+
? namespaceUrl(path, { namespace: '-all-' })
426+
: clusterUrl(path);
427+
428+
navigate(link);
429+
};
430+
431+
const fetchResourceCounts = async () => {
432+
const resources = getAssociatedResources();
433+
const counts = {};
434+
for (const resource of resources) {
435+
const count = await getNumberOfResources(
436+
resource.kind,
437+
resource.group,
438+
resource.version,
439+
);
440+
counts[
441+
`${resource.kind}-${resource.group}-${resource.version}`
442+
] = count;
443+
}
444+
return counts;
445+
};
446+
447+
const [resourceCounts, setResourceCounts] = useState({});
448+
449+
useEffect(() => {
450+
const fetchCounts = async () => {
451+
const counts = await fetchResourceCounts();
452+
setResourceCounts(counts);
453+
};
454+
455+
fetchCounts();
456+
// eslint-disable-next-line react-hooks/exhaustive-deps
457+
}, [chosenModuleIndex]);
458+
459+
const checkIfAssociatedResourceLeft = () => {
460+
const resources = getAssociatedResources();
461+
for (const resource of resources) {
462+
if (
463+
resourceCounts[
464+
`${resource.kind}-${resource.group}-${resource.version}`
465+
] > 0
466+
) {
467+
return true;
468+
}
469+
}
470+
return false;
471+
};
472+
381473
return (
382474
<>
383475
{!detailsOpen &&
384476
createPortal(
385477
<DeleteMessageBox
478+
disableDeleteButton={checkIfAssociatedResourceLeft()}
479+
additionalDeleteInfo={
480+
getAssociatedResources().length > 0 && (
481+
<>
482+
<MessageStrip design="Warning" hideCloseButton>
483+
{t('kyma-modules.associated-resources-warning')}
484+
</MessageStrip>
485+
<List
486+
headerText={t('kyma-modules.associated-resources')}
487+
mode="None"
488+
separators="All"
489+
>
490+
{getAssociatedResources().map(assResource => (
491+
<StandardListItem
492+
onClick={e => {
493+
e.preventDefault();
494+
handleItemClick(
495+
assResource.kind,
496+
assResource.group,
497+
assResource.version,
498+
);
499+
}}
500+
type="Active"
501+
key={`${assResource.kind}-${assResource.group}-${assResource.version}`}
502+
additionalText={
503+
resourceCounts[
504+
`${assResource.kind}-${assResource.group}-${assResource.version}`
505+
] || t('common.headers.loading')
506+
}
507+
>
508+
{pluralize(assResource?.kind)}
509+
</StandardListItem>
510+
))}
511+
</List>
512+
</>
513+
)
514+
}
386515
resourceTitle={selectedModules[chosenModuleIndex]?.name}
387516
deleteFn={() => {
388517
selectedModules.splice(chosenModuleIndex, 1);

src/shared/hooks/BackendAPI/useGet.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,3 +358,15 @@ export const useSingleGet = () => {
358358
const fetch = useFetch();
359359
return url => fetch({ relativeUrl: url });
360360
};
361+
362+
export const useGetScope = () => {
363+
const fetch = useFetch();
364+
return async (group, version, kind) => {
365+
const response = await fetch({
366+
relativeUrl: `/apis/${group}/${version}`,
367+
});
368+
const openApiSpec = await response.json();
369+
370+
return openApiSpec.resources.find(r => r.kind === kind).namespaced;
371+
};
372+
};

src/shared/hooks/useDeleteResource.js

Lines changed: 75 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -173,76 +173,84 @@ export function useDeleteResource({
173173
resourceIsCluster = false,
174174
resourceUrl,
175175
deleteFn,
176-
}) => (
177-
<MessageBox
178-
type="Warning"
179-
titleText={t(
180-
resourceIsCluster
181-
? 'common.delete-dialog.disconnect-title'
182-
: 'common.delete-dialog.delete-title',
183-
{
184-
type: prettifiedResourceName,
185-
},
186-
)}
187-
open={showDeleteDialog}
188-
className="ui5-content-density-compact"
189-
actions={[
190-
<Button
191-
key="delete-confirmation"
192-
data-testid="delete-confirmation"
193-
design="Emphasized"
194-
onClick={() => performDelete(resource, resourceUrl, deleteFn)}
176+
additionalDeleteInfo,
177+
disableDeleteButton = false,
178+
}) => {
179+
return (
180+
<MessageBox
181+
type="Warning"
182+
titleText={t(
183+
resourceIsCluster
184+
? 'common.delete-dialog.disconnect-title'
185+
: 'common.delete-dialog.delete-title',
186+
{
187+
type: prettifiedResourceName,
188+
},
189+
)}
190+
open={showDeleteDialog}
191+
className="ui5-content-density-compact"
192+
actions={[
193+
<Button
194+
key="delete-confirmation"
195+
data-testid="delete-confirmation"
196+
design="Emphasized"
197+
onClick={() => performDelete(resource, resourceUrl, deleteFn)}
198+
disabled={disableDeleteButton}
199+
>
200+
{t(
201+
resourceIsCluster
202+
? 'common.buttons.disconnect'
203+
: 'common.buttons.delete',
204+
)}
205+
</Button>,
206+
<Button
207+
key="delete-cancel"
208+
data-testid="delete-cancel"
209+
design="Transparent"
210+
onClick={() => setShowDeleteDialog(false)}
211+
>
212+
{t('common.buttons.cancel')}
213+
</Button>,
214+
]}
215+
onClose={closeDeleteDialog}
216+
>
217+
<FlexBox
218+
direction="Column"
219+
style={{
220+
gap: '10px',
221+
padding: '15px 25px',
222+
}}
195223
>
196-
{t(
197-
resourceIsCluster
198-
? 'common.buttons.disconnect'
199-
: 'common.buttons.delete',
224+
<Text style={{ paddingLeft: '7.5px' }}>
225+
{t(
226+
resourceIsCluster
227+
? 'common.delete-dialog.disconnect-message'
228+
: 'common.delete-dialog.delete-message',
229+
{
230+
type: prettifiedResourceName,
231+
name: resourceTitle || resource?.metadata?.name,
232+
},
233+
)}
234+
</Text>
235+
{additionalDeleteInfo && (
236+
<Text style={{ paddingLeft: '7.5px' }}>{additionalDeleteInfo}</Text>
200237
)}
201-
</Button>,
202-
<Button
203-
key="delete-cancel"
204-
data-testid="delete-cancel"
205-
design="Transparent"
206-
onClick={() => setShowDeleteDialog(false)}
207-
>
208-
{t('common.buttons.cancel')}
209-
</Button>,
210-
]}
211-
onClose={closeDeleteDialog}
212-
>
213-
<FlexBox
214-
direction="Column"
215-
style={{
216-
gap: '10px',
217-
padding: '15px 25px',
218-
}}
219-
>
220-
<Text style={{ paddingLeft: '7.5px' }}>
221-
{t(
222-
resourceIsCluster
223-
? 'common.delete-dialog.disconnect-message'
224-
: 'common.delete-dialog.delete-message',
225-
{
226-
type: prettifiedResourceName,
227-
name: resourceTitle || resource?.metadata?.name,
228-
},
238+
{!forceConfirmDelete && (
239+
<CheckBox
240+
checked={dontConfirmDelete}
241+
onChange={() => setDontConfirmDelete(prevState => !prevState)}
242+
text={t('common.delete-dialog.delete-confirm')}
243+
/>
229244
)}
230-
</Text>
231-
{!forceConfirmDelete && (
232-
<CheckBox
233-
checked={dontConfirmDelete}
234-
onChange={() => setDontConfirmDelete(prevState => !prevState)}
235-
text={t('common.delete-dialog.delete-confirm')}
236-
/>
237-
)}
238-
{dontConfirmDelete && !forceConfirmDelete && (
239-
<MessageStrip design="Information" hideCloseButton>
240-
{t('common.delete-dialog.information')}
241-
</MessageStrip>
242-
)}
243-
</FlexBox>
244-
</MessageBox>
245-
);
245+
{dontConfirmDelete && !forceConfirmDelete && (
246+
<MessageStrip design="Information" hideCloseButton>
247+
{t('common.delete-dialog.information')}
248+
</MessageStrip>
249+
)}
250+
</FlexBox>
251+
</MessageBox>
252+
);
253+
};
246254

247255
return [DeleteMessageBox, handleResourceDelete];
248256
}

0 commit comments

Comments
 (0)