Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Force delete #3716

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions public/i18n/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ common:
discard: Discard
disconnect: Disconnect
edit: Edit
force-delete: Force delete
learn-more: Learn More
ok: OK
remove-all: Remove all
Expand Down Expand Up @@ -799,6 +800,7 @@ kyma-modules:
channel-overridden: Overridden
managed: Managed
associated-resources-warning: If there are resources left, remove them first to delete the module.
force-edit-info: Enable force delete of all connected resources
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
168 changes: 149 additions & 19 deletions src/components/KymaModules/KymaModulesList.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jsyaml from 'js-yaml';
import { ResourceDetails } from 'shared/components/ResourceDetails/ResourceDetails';
import {
Button,
CheckBox,
DynamicPageHeader,
FlexBox,
List,
Expand Down Expand Up @@ -51,6 +52,7 @@ import {
} from './kymaModulesQueries';
import { findModuleStatus } from './support';
import { UnmanagedModuleInfo } from './components/UnmanagedModuleInfo';
import { useDelete } from 'shared/hooks/BackendAPI/useMutation';

export default function KymaModulesList({
DeleteMessageBox,
Expand Down Expand Up @@ -79,6 +81,7 @@ export default function KymaModulesList({
const fetch = useSingleGet();
const getScope = useGetScope();
const navigate = useNavigate();
const deleteResourceMutation = useDelete();

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

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

const getNumberOfResources = async (kind, group, version) => {
const getCRResource = () => {
if (!chosenModuleIndex) {
return [];
}
const selectedModule = selectedModules[chosenModuleIndex];
const moduleChannel =
selectedModule?.channel || kymaResource?.spec?.channel;
const moduleVersion =
selectedModule?.version ||
findModuleStatus(kymaResource, selectedModule?.name)?.version;

const module = findModuleTemplate(
selectedModule?.name,
moduleChannel,
moduleVersion,
);

const resource = {};
if (module.spec.data) {
resource.group = module.spec.data.apiVersion.split('/')[0];
resource.version = module.spec.data.apiVersion.split('/')[1];
resource.kind = module.spec.data.kind;
}
return resource ? [resource] : [];
};

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 getResources = async (kind, group, version) => {
const url =
group === 'v1'
? '/api/v1'
Expand All @@ -419,45 +457,90 @@ export default function KymaModulesList({
try {
const response = await fetch(url);
const json = await response.json();
return json.items.length;
return json.items;
} 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);
const getUrlsByNamespace = resources => {
return resources.reduce((urls, resource) => {
const url = `/apis/${resource.apiVersion}/namespaces/${
resource.metadata.namespace
}/${pluralize(resource.kind.toLowerCase())}`;

navigate(link);
if (!urls.includes(url)) {
urls.push(url);
}
return urls;
}, []);
};

const fetchResourceCounts = async () => {
const resources = getAssociatedResources();
const generateAssociatedResourcesUrls = async resources => {
const allUrls = [];

for (const resource of resources) {
const isNamespaced = await getScope(
resource.group,
resource.version,
resource.kind,
);
let resources,
urls = [];
if (isNamespaced) {
resources = await getResources(
resource.kind,
resource.group,
resource.version,
);
urls = getUrlsByNamespace(resources);
} else {
urls = [
resource.group === 'v1'
? '/api/v1'
: `/apis/${resource.group}/${resource.version}/${pluralize(
resource.kind.toLowerCase(),
)}`,
];
}

allUrls.push(...urls);
}

return allUrls;
};

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

const [resourceCounts, setResourceCounts] = useState({});
const [forceDeleteUrls, setForceDeleteUrls] = useState([]);
const [crUrls, setCrUrls] = useState([]);
const [allowForceDelete, setAllowForceDelete] = useState(false);

useEffect(() => {
const fetchCounts = async () => {
const counts = await fetchResourceCounts();
const resources = getAssociatedResources();
const counts = await fetchResourceCounts(resources);
const urls = await generateAssociatedResourcesUrls(resources);
const crUResources = getCRResource();
const crUrl = await generateAssociatedResourcesUrls(crUResources);

setResourceCounts(counts);
setForceDeleteUrls(urls);
setCrUrls([crUrl]);
};

fetchCounts();
Expand All @@ -478,13 +561,45 @@ export default function KymaModulesList({
return false;
};

const deleteAssociatedResources = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could these functions be moved to a separate file?

try {
await Promise.all(
forceDeleteUrls.map(async url => {
return await deleteResourceMutation(url);
}),
);
} catch (e) {
console.warn(e);
return 'Error while deleting Associated Resources';
}
};

const deleteCrResources = async () => {
try {
await Promise.all(
crUrls.map(async url => {
return await deleteResourceMutation(url);
}),
);
} catch (e) {
console.warn(e);
return 'Error while deleting Custom Resource';
}
};

return (
<React.Fragment key="modules-list">
{!detailsOpen &&
createPortal(
<DeleteMessageBox
disableDeleteButton={checkIfAssociatedResourceLeft()}
disableDeleteButton={
checkIfAssociatedResourceLeft() ? !allowForceDelete : false
}
allowForceDelete={
checkIfAssociatedResourceLeft() ? allowForceDelete : false
}
Comment on lines 594 to +600
Copy link
Contributor

Choose a reason for hiding this comment

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

Will it be possible to move the whole DeleteMessageBox to a separate file with connected functions? Also if functions will provide a mess in separate files move them to support functions for Delete only?

cancelFn={() => {
setAllowForceDelete(false);
setChosenModuleIndex(null);
}}
additionalDeleteInfo={
Expand Down Expand Up @@ -532,12 +647,25 @@ export default function KymaModulesList({
);
})}
</List>
{checkIfAssociatedResourceLeft() && (
<CheckBox
checked={allowForceDelete}
onChange={() =>
setAllowForceDelete(!allowForceDelete)
}
accessibleName={t('kyma-modules.force-edit-info')}
text={t('kyma-modules.force-edit-info')}
/>
)}
</>
)}
</>
}
resourceTitle={selectedModules[chosenModuleIndex]?.name}
deleteFn={() => {
if (allowForceDelete && forceDeleteUrls.length > 0) {
deleteAssociatedResources();
}
selectedModules.splice(chosenModuleIndex, 1);
setKymaResourceState({
...kymaResource,
Expand All @@ -548,7 +676,9 @@ export default function KymaModulesList({
});
handleModuleUninstall();
setInitialUnchangedResource(cloneDeep(kymaResourceState));
setChosenModuleIndex(null);
if (allowForceDelete && forceDeleteUrls.length > 0) {
deleteCrResources();
}
}}
/>,
document.body,
Expand Down
15 changes: 9 additions & 6 deletions src/shared/hooks/useDeleteResource.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,6 @@ export function useDeleteResource({
}
};

const closeDeleteDialog = () => {
setShowDeleteDialog(false);
};

const handleResourceDelete = ({ resource, resourceUrl, deleteFn }) => {
if (dontConfirmDelete && !forceConfirmDelete) {
performDelete(resource, resourceUrl, deleteFn);
Expand All @@ -181,6 +177,7 @@ export function useDeleteResource({
cancelFn,
additionalDeleteInfo,
disableDeleteButton = false,
allowForceDelete = false,
Copy link
Contributor

Choose a reason for hiding this comment

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

In my opinion allowForceDelete is not the best way to do this, it's used only to adjust the text in the Delete Button, so maybe change it to customDeleteText and pass the string here

}) => {
return (
<MessageBox
Expand All @@ -207,19 +204,25 @@ export function useDeleteResource({
{t(
resourceIsCluster
? 'common.buttons.disconnect'
: allowForceDelete
? 'common.buttons.force-delete'
: 'common.buttons.delete',
)}
</Button>,
<Button
key="delete-cancel"
data-testid="delete-cancel"
design="Transparent"
onClick={() => performCancel(cancelFn)}
onClick={() => {
performCancel(cancelFn);
}}
>
{t('common.buttons.cancel')}
</Button>,
]}
onClose={closeDeleteDialog}
onClose={() => {
performCancel(cancelFn);
}}
>
<FlexBox
direction="Column"
Expand Down
Loading