Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,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
135 changes: 132 additions & 3 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 @@ -60,7 +69,10 @@ export default function KymaModulesList({
] = useState(false);
const setLayoutColumn = useSetRecoilState(columnLayoutState);
const setIsFormOpen = useSetRecoilState(isFormOpenState);
const { clusterUrl } = useUrl();
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 @@ -383,11 +395,128 @@ export default function KymaModulesList({
);
};

const getAssciatedResources = () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a typo in the function name :)

Suggested change
const getAssciatedResources = () => {
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 = getAssciatedResources();
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 = getAssciatedResources();
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={
getAssciatedResources().length > 0 && (
<>
<MessageStrip design="Warning" hideCloseButton>
{t('kyma-modules.associated-resources-warning')}
</MessageStrip>
<List
headerText="AssociatedResources"
mode="None"
separators="All"
>
{getAssciatedResources().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.header.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];
}
Loading