Skip to content

Commit

Permalink
feat: Information about associated resources in delete modal for modu…
Browse files Browse the repository at this point in the history
…les (#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
  • Loading branch information
mrCherry97 authored Jan 15, 2025
1 parent ad8c021 commit 9f36ee5
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 69 deletions.
2 changes: 2 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ kubeconfig-id:
error: "Couldn't load kubeconfig ID; configuration not changed (Error: ${{error}})"
must-be-an-object: Kubeconfig must be a JSON or YAML object.
kyma-modules:
associated-resources: Associated Resources
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.
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?
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.
Expand All @@ -775,6 +776,7 @@ kyma-modules:
no-version: No version available
channel-overridden: Overridden
managed: Managed
associated-resources-warning: If there are resources left, remove them first to delete the module.
beta: Beta
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."
change: Change
Expand Down
133 changes: 131 additions & 2 deletions src/components/KymaModules/KymaModulesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,21 @@ import {
FlexBox,
Text,
Badge,
List,
StandardListItem,
MessageStrip,
} from '@ui5/webcomponents-react';

import { HintButton } from 'shared/components/DescriptionHint/DescriptionHint';
import { spacing } from '@ui5/webcomponents-react-base';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { GenericList } from 'shared/components/GenericList/GenericList';
import { useGet, useGetList } from 'shared/hooks/BackendAPI/useGet';
import {
useGet,
useGetList,
useGetScope,
useSingleGet,
} from 'shared/hooks/BackendAPI/useGet';
import { ExternalLink } from 'shared/components/ExternalLink/ExternalLink';
import { EMPTY_TEXT_PLACEHOLDER } from 'shared/constants';
import KymaModulesCreate from './KymaModulesCreate';
Expand All @@ -36,6 +44,7 @@ import { isFormOpenState } from 'state/formOpenAtom';
import { ModuleStatus } from './components/ModuleStatus';
import { cloneDeep } from 'lodash';
import { StatusBadge } from 'shared/components/StatusBadge/StatusBadge';
import { useNavigate } from 'react-router-dom';

export default function KymaModulesList({
DeleteMessageBox,
Expand All @@ -62,6 +71,9 @@ export default function KymaModulesList({
const setLayoutColumn = useSetRecoilState(columnLayoutState);
const setIsFormOpen = useSetRecoilState(isFormOpenState);
const { clusterUrl, namespaceUrl } = useUrl();
const fetch = useSingleGet();
const getScope = useGetScope();
const navigate = useNavigate();

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

const getAssociatedResources = () => {
const module = findModule(
selectedModules[chosenModuleIndex]?.name,
selectedModules[chosenModuleIndex]?.channel ||
kymaResource?.spec?.channel,
selectedModules[chosenModuleIndex]?.version ||
findStatus(selectedModules[chosenModuleIndex]?.name)?.version,
);

return module?.spec?.associatedResources || [];
};

const getNumberOfResources = async (kind, group, version) => {
const url =
group === 'v1'
? '/api/v1'
: `/apis/${group}/${version}/${pluralize(kind.toLowerCase())}`;

try {
const response = await fetch(url);
const json = await response.json();
return json.items.length;
} catch (e) {
console.warn(e);
return 'Error';
}
};

const handleItemClick = async (kind, group, version) => {
const isNamespaced = await getScope(group, version, kind);
const path = `${pluralize(kind.toLowerCase())}`;
const link = isNamespaced
? namespaceUrl(path, { namespace: '-all-' })
: clusterUrl(path);

navigate(link);
};

const fetchResourceCounts = async () => {
const resources = getAssociatedResources();
const counts = {};
for (const resource of resources) {
const count = await getNumberOfResources(
resource.kind,
resource.group,
resource.version,
);
counts[
`${resource.kind}-${resource.group}-${resource.version}`
] = count;
}
return counts;
};

const [resourceCounts, setResourceCounts] = useState({});

useEffect(() => {
const fetchCounts = async () => {
const counts = await fetchResourceCounts();
setResourceCounts(counts);
};

fetchCounts();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chosenModuleIndex]);

const checkIfAssociatedResourceLeft = () => {
const resources = getAssociatedResources();
for (const resource of resources) {
if (
resourceCounts[
`${resource.kind}-${resource.group}-${resource.version}`
] > 0
) {
return true;
}
}
return false;
};

return (
<>
{!detailsOpen &&
createPortal(
<DeleteMessageBox
disableDeleteButton={checkIfAssociatedResourceLeft()}
additionalDeleteInfo={
getAssociatedResources().length > 0 && (
<>
<MessageStrip design="Warning" hideCloseButton>
{t('kyma-modules.associated-resources-warning')}
</MessageStrip>
<List
headerText={t('kyma-modules.associated-resources')}
mode="None"
separators="All"
>
{getAssociatedResources().map(assResource => (
<StandardListItem
onClick={e => {
e.preventDefault();
handleItemClick(
assResource.kind,
assResource.group,
assResource.version,
);
}}
type="Active"
key={`${assResource.kind}-${assResource.group}-${assResource.version}`}
additionalText={
resourceCounts[
`${assResource.kind}-${assResource.group}-${assResource.version}`
] || t('common.headers.loading')
}
>
{pluralize(assResource?.kind)}
</StandardListItem>
))}
</List>
</>
)
}
resourceTitle={selectedModules[chosenModuleIndex]?.name}
deleteFn={() => {
selectedModules.splice(chosenModuleIndex, 1);
Expand Down
12 changes: 12 additions & 0 deletions src/shared/hooks/BackendAPI/useGet.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,15 @@ export const useSingleGet = () => {
const fetch = useFetch();
return url => fetch({ relativeUrl: url });
};

export const useGetScope = () => {
const fetch = useFetch();
return async (group, version, kind) => {
const response = await fetch({
relativeUrl: `/apis/${group}/${version}`,
});
const openApiSpec = await response.json();

return openApiSpec.resources.find(r => r.kind === kind).namespaced;
};
};
142 changes: 75 additions & 67 deletions src/shared/hooks/useDeleteResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,76 +173,84 @@ export function useDeleteResource({
resourceIsCluster = false,
resourceUrl,
deleteFn,
}) => (
<MessageBox
type="Warning"
titleText={t(
resourceIsCluster
? 'common.delete-dialog.disconnect-title'
: 'common.delete-dialog.delete-title',
{
type: prettifiedResourceName,
},
)}
open={showDeleteDialog}
className="ui5-content-density-compact"
actions={[
<Button
key="delete-confirmation"
data-testid="delete-confirmation"
design="Emphasized"
onClick={() => performDelete(resource, resourceUrl, deleteFn)}
additionalDeleteInfo,
disableDeleteButton = false,
}) => {
return (
<MessageBox
type="Warning"
titleText={t(
resourceIsCluster
? 'common.delete-dialog.disconnect-title'
: 'common.delete-dialog.delete-title',
{
type: prettifiedResourceName,
},
)}
open={showDeleteDialog}
className="ui5-content-density-compact"
actions={[
<Button
key="delete-confirmation"
data-testid="delete-confirmation"
design="Emphasized"
onClick={() => performDelete(resource, resourceUrl, deleteFn)}
disabled={disableDeleteButton}
>
{t(
resourceIsCluster
? 'common.buttons.disconnect'
: 'common.buttons.delete',
)}
</Button>,
<Button
key="delete-cancel"
data-testid="delete-cancel"
design="Transparent"
onClick={() => setShowDeleteDialog(false)}
>
{t('common.buttons.cancel')}
</Button>,
]}
onClose={closeDeleteDialog}
>
<FlexBox
direction="Column"
style={{
gap: '10px',
padding: '15px 25px',
}}
>
{t(
resourceIsCluster
? 'common.buttons.disconnect'
: 'common.buttons.delete',
<Text style={{ paddingLeft: '7.5px' }}>
{t(
resourceIsCluster
? 'common.delete-dialog.disconnect-message'
: 'common.delete-dialog.delete-message',
{
type: prettifiedResourceName,
name: resourceTitle || resource?.metadata?.name,
},
)}
</Text>
{additionalDeleteInfo && (
<Text style={{ paddingLeft: '7.5px' }}>{additionalDeleteInfo}</Text>
)}
</Button>,
<Button
key="delete-cancel"
data-testid="delete-cancel"
design="Transparent"
onClick={() => setShowDeleteDialog(false)}
>
{t('common.buttons.cancel')}
</Button>,
]}
onClose={closeDeleteDialog}
>
<FlexBox
direction="Column"
style={{
gap: '10px',
padding: '15px 25px',
}}
>
<Text style={{ paddingLeft: '7.5px' }}>
{t(
resourceIsCluster
? 'common.delete-dialog.disconnect-message'
: 'common.delete-dialog.delete-message',
{
type: prettifiedResourceName,
name: resourceTitle || resource?.metadata?.name,
},
{!forceConfirmDelete && (
<CheckBox
checked={dontConfirmDelete}
onChange={() => setDontConfirmDelete(prevState => !prevState)}
text={t('common.delete-dialog.delete-confirm')}
/>
)}
</Text>
{!forceConfirmDelete && (
<CheckBox
checked={dontConfirmDelete}
onChange={() => setDontConfirmDelete(prevState => !prevState)}
text={t('common.delete-dialog.delete-confirm')}
/>
)}
{dontConfirmDelete && !forceConfirmDelete && (
<MessageStrip design="Information" hideCloseButton>
{t('common.delete-dialog.information')}
</MessageStrip>
)}
</FlexBox>
</MessageBox>
);
{dontConfirmDelete && !forceConfirmDelete && (
<MessageStrip design="Information" hideCloseButton>
{t('common.delete-dialog.information')}
</MessageStrip>
)}
</FlexBox>
</MessageBox>
);
};

return [DeleteMessageBox, handleResourceDelete];
}

0 comments on commit 9f36ee5

Please sign in to comment.