Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
20 changes: 17 additions & 3 deletions public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"tableHeaderReady": "Ready",
"tableHeaderDelete": "Delete",
"deleteAction": "Delete resource",
"editAction": "Edit resource",
"deleteDialogTitle": "Delete resource",
"advancedOptions": "Advanced options",
"forceDeletion": "Force deletion",
"forceWarningLine": "Force deletion removes finalizers. Related resources may not be deleted and cleanup may be skipped.",
"deleteStarted": "Deleting {{resourceName}} initialized",
"patchStarted": "Updating {{resourceName}} initialized",
"patchSuccess": "Updated {{resourceName}}",
"patchError": "Failed to update {{resourceName}}",
"actionColumnHeader": " "
},
"ProvidersConfig": {
Expand Down Expand Up @@ -373,7 +377,8 @@
"installError": "Install error",
"syncError": "Sync error",
"error": "Error",
"notHealthy": "Not healthy"
"notHealthy": "Not healthy",
"notReady": "Not ready"
},
"buttons": {
"viewResource": "View resource",
Expand All @@ -384,11 +389,20 @@
"close": "Close",
"back": "Back",
"cancel": "Cancel",
"update": "Update"
"update": "Update",
"applyChanges": "Apply changes"
},
"yaml": {
"YAML": "File",
"showOnlyImportant": "Show only important fields"
"showOnlyImportant": "Show only important fields",
"panelTitle": "YAML",
"editorTitle": "YAML Editor",
"applySuccess": "Changes applied successfully",
"applySuccess2": "Your resource update was submitted.",
"diffConfirmTitle": "Review changes",
"diffConfirmMessage": "Are you sure that you want to apply these changes?",
"diffNo": "No, go back",
"diffYes": "Yes"
},
"createMCP": {
"dialogTitle": "Create Managed Control Plane",
Expand Down
229 changes: 140 additions & 89 deletions src/components/ControlPlane/ManagedResources.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import { Fragment, useMemo, useState, useContext, useRef, useCallback } from 'react';
import {
AnalyticalTable,
AnalyticalTableColumnDefinition,
Expand All @@ -15,7 +16,6 @@ import IllustratedError from '../Shared/IllustratedError';
import { resourcesInterval } from '../../lib/shared/constants';

import { YamlViewButton } from '../Yaml/YamlViewButton.tsx';
import { useMemo, useState } from 'react';
import StatusFilter from '../Shared/StatusFilter/StatusFilter.tsx';
import { ResourceStatusCell } from '../Shared/ResourceStatusCell.tsx';
import { Resource } from '../../utils/removeManagedFieldsAndFilterData.ts';
Expand All @@ -30,14 +30,21 @@ import {
PatchResourceForForceDeletionBody,
} from '../../lib/api/types/crate/deleteResource';
import { useResourcePluralNames } from '../../hooks/useResourcePluralNames';
import { useSplitter } from '../Splitter/SplitterContext.tsx';
import { YamlSidePanel } from '../Yaml/YamlSidePanel.tsx';

interface CellData<T> {
cell: {
value: T | null; // null for grouping rows
row: {
original?: ResourceRow; // missing for grouping rows
};
};
import { ApiConfigContext } from '../Shared/k8s';
import { ErrorDialog, ErrorDialogHandle } from '../Shared/ErrorMessageBox.tsx';
import { APIError } from '../../lib/api/error.ts';

import { handleResourcePatch } from '../../lib/api/types/crossplane/handleResourcePatch.ts';

interface StatusFilterColumn {
filterValue?: string;
setFilter?: (value?: string) => void;
}
interface CellRow<T> {
original: T;
}

type ResourceRow = {
Expand All @@ -48,15 +55,18 @@ type ResourceRow = {
syncedTransitionTime: string;
ready: boolean;
readyTransitionTime: string;
item: unknown;
item: ManagedResourceItem;
conditionReadyMessage: string;
conditionSyncedMessage: string;
};

export function ManagedResources() {
const { t } = useTranslation();
const toast = useToast();
const { openInAside } = useSplitter();
const apiConfig = useContext(ApiConfigContext);
const [pendingDeleteItem, setPendingDeleteItem] = useState<ManagedResourceItem | null>(null);
const errorDialogRef = useRef<ErrorDialogHandle>(null);

const {
data: managedResources,
Expand All @@ -81,81 +91,116 @@ export function ManagedResources() {
PatchResourceForForceDeletion(apiVersion, pluralKind, resourceName, namespace),
);

const columns: AnalyticalTableColumnDefinition[] = useMemo(
() => [
{
Header: t('ManagedResources.tableHeaderKind'),
accessor: 'kind',
},
{
Header: t('ManagedResources.tableHeaderName'),
accessor: 'name',
},
{
Header: t('ManagedResources.tableHeaderCreated'),
accessor: 'created',
},
{
Header: t('ManagedResources.tableHeaderSynced'),
accessor: 'synced',
hAlign: 'Center',
width: 125,
Filter: ({ column }) => <StatusFilter column={column} />,
Cell: (cellData: CellData<ResourceRow['synced']>) =>
cellData.cell.row.original?.synced != null ? (
<ResourceStatusCell
isOk={cellData.cell.row.original?.synced}
transitionTime={cellData.cell.row.original?.syncedTransitionTime}
positiveText={t('common.synced')}
negativeText={t('errors.syncError')}
message={cellData.cell.row.original?.conditionSyncedMessage}
/>
) : null,
},
{
Header: t('ManagedResources.tableHeaderReady'),
accessor: 'ready',
hAlign: 'Center',
width: 125,
Filter: ({ column }) => <StatusFilter column={column} />,
Cell: (cellData: CellData<ResourceRow['ready']>) =>
cellData.cell.row.original?.ready != null ? (
<ResourceStatusCell
isOk={cellData.cell.row.original?.ready}
transitionTime={cellData.cell.row.original?.readyTransitionTime}
positiveText={t('common.ready')}
negativeText={'Not ready'}
message={cellData.cell.row.original?.conditionReadyMessage}
/>
) : null,
},
{
Header: t('yaml.YAML'),
hAlign: 'Center',
width: 75,
accessor: 'yaml',
disableFilters: true,
Cell: (cellData: CellData<ResourceRow>) => {
return cellData.cell.row.original?.item ? (
<YamlViewButton variant="resource" resource={cellData.cell.row.original?.item as Resource} />
) : undefined;
const openDeleteDialog = useCallback((item: ManagedResourceItem) => {
setPendingDeleteItem(item);
}, []);

const openEditPanel = useCallback(
(item: ManagedResourceItem) => {
const identityKey = `${item.kind}:${item.metadata.namespace ?? ''}:${item.metadata.name}`;
openInAside(
<Fragment key={identityKey}>
<YamlSidePanel
isEdit={true}
resource={item as unknown as Resource}
filename={`${item.kind}_${item.metadata.name}`}
onApply={async (parsed) =>
await handleResourcePatch({
item,
parsed,
getPluralKind,
apiConfig,
t,
toast,
errorDialogRef,
})
}
/>
</Fragment>,
);
},
[openInAside, getPluralKind, apiConfig, t, toast, errorDialogRef],
);

const columns = useMemo<AnalyticalTableColumnDefinition[]>(
() =>
[
{
Header: t('ManagedResources.tableHeaderKind'),
accessor: 'kind',
},
},
{
Header: t('ManagedResources.actionColumnHeader'),
hAlign: 'Center',
width: 60,
disableFilters: true,
Cell: (cellData: CellData<ResourceRow>) => {
const item = cellData.cell.row.original?.item as ManagedResourceItem;

return cellData.cell.row.original?.item ? (
<RowActionsMenu item={item} onOpen={openDeleteDialog} />
) : undefined;
{
Header: t('ManagedResources.tableHeaderName'),
accessor: 'name',
},
},
],
[t],
{
Header: t('ManagedResources.tableHeaderCreated'),
accessor: 'created',
},
{
Header: t('ManagedResources.tableHeaderSynced'),
accessor: 'synced',
hAlign: 'Center',
width: 125,
Filter: ({ column }: { column: StatusFilterColumn }) => <StatusFilter column={column} />,
Cell: ({ row }: { row: CellRow<ResourceRow> }) => {
const { original } = row;
return original?.synced != null ? (
<ResourceStatusCell
isOk={original.synced}
transitionTime={original.syncedTransitionTime}
positiveText={t('common.synced')}
negativeText={t('errors.syncError')}
message={original.conditionSyncedMessage}
/>
) : null;
},
},
{
Header: t('ManagedResources.tableHeaderReady'),
accessor: 'ready',
hAlign: 'Center',
width: 125,
Filter: ({ column }: { column: StatusFilterColumn }) => <StatusFilter column={column} />,
Cell: ({ row }: { row: CellRow<ResourceRow> }) => {
const { original } = row;
return original?.ready != null ? (
<ResourceStatusCell
isOk={original.ready}
transitionTime={original.readyTransitionTime}
positiveText={t('common.ready')}
negativeText={t('errors.notReady')}
message={original.conditionReadyMessage}
/>
) : null;
},
},
{
Header: t('yaml.YAML'),
hAlign: 'Center',
width: 75,
accessor: 'yaml',
disableFilters: true,
Cell: ({ row }: { row: CellRow<ResourceRow> }) => {
const { original } = row;
return original?.item ? (
<YamlViewButton variant="resource" resource={original.item as unknown as Resource} />
) : undefined;
},
},
{
Header: t('ManagedResources.actionColumnHeader'),
hAlign: 'Center',
width: 60,
disableFilters: true,
Cell: ({ row }: { row: CellRow<ResourceRow> }) => {
const { original } = row;
const item = original?.item;
return item ? <RowActionsMenu item={item} onOpen={openDeleteDialog} onEdit={openEditPanel} /> : undefined;
},
},
] as AnalyticalTableColumnDefinition[],
[t, openEditPanel, openDeleteDialog],
);

const rows: ResourceRow[] =
Expand All @@ -181,21 +226,26 @@ export function ManagedResources() {
}),
) ?? [];

const openDeleteDialog = (item: ManagedResourceItem) => {
setPendingDeleteItem(item);
};

const handleDeletionConfirmed = async (item: ManagedResourceItem, force: boolean) => {
toast.show(t('ManagedResources.deleteStarted', { resourceName: item.metadata.name }));

try {
await deleteTrigger();

if (force) {
await patchTrigger(PatchResourceForForceDeletionBody);
try {
await patchTrigger(PatchResourceForForceDeletionBody);
} catch (e) {
if (e instanceof APIError && errorDialogRef.current) {
errorDialogRef.current.showErrorDialog(`${e.message}: ${JSON.stringify(e.info)}`);
}
// already handled
}
}
} catch (e) {
if (e instanceof APIError && errorDialogRef.current) {
errorDialogRef.current.showErrorDialog(`${e.message}: ${JSON.stringify(e.info)}`);
}
} catch (_) {
// Ignore errors - will be handled by the mutation hook
} finally {
setPendingDeleteItem(null);
}
Expand Down Expand Up @@ -247,6 +297,7 @@ export function ManagedResources() {
onClose={() => setPendingDeleteItem(null)}
onDeletionConfirmed={handleDeletionConfirmed}
/>
<ErrorDialog ref={errorDialogRef} />
</>
</Panel>
)}
Expand Down
6 changes: 5 additions & 1 deletion src/components/ControlPlane/ManagedResourcesActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import type { Ui5CustomEvent, ButtonDomRef } from '@ui5/webcomponents-react';
interface RowActionsMenuProps {
item: ManagedResourceItem;
onOpen: (item: ManagedResourceItem) => void;
onEdit: (item: ManagedResourceItem) => void;
}

export const RowActionsMenu: FC<RowActionsMenuProps> = ({ item, onOpen }) => {
export const RowActionsMenu: FC<RowActionsMenuProps> = ({ item, onOpen, onEdit }) => {
const { t } = useTranslation();
const popoverRef = useRef<MenuDomRef>(null);
const [open, setOpen] = useState(false);
Expand All @@ -33,10 +34,13 @@ export const RowActionsMenu: FC<RowActionsMenuProps> = ({ item, onOpen }) => {
const action = element.dataset.action;
if (action === 'delete') {
onOpen(item);
} else if (action === 'edit') {
onEdit(item);
}
setOpen(false);
}}
>
<MenuItem text={t('ManagedResources.editAction', 'Edit')} icon="edit" data-action="edit" />
<MenuItem text={t('ManagedResources.deleteAction')} icon="delete" data-action="delete" />
</Menu>
</>
Expand Down
Loading
Loading