feat(ws): call delete Workspace API from the frontend (#383)

Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com>
This commit is contained in:
Guilherme Caponetto 2025-05-27 10:44:33 -03:00 committed by GitHub
parent c40a2e00e3
commit 3c8c8e0c2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 80 additions and 35 deletions

View File

@ -16,6 +16,7 @@
"@patternfly/react-styles": "^6.2.0",
"@patternfly/react-table": "^6.2.0",
"@types/js-yaml": "^4.0.9",
"date-fns": "^4.1.0",
"js-yaml": "^4.1.0",
"npm-run-all": "^4.1.5",
"react": "^18",
@ -8597,6 +8598,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",

View File

@ -104,6 +104,7 @@
"@patternfly/react-styles": "^6.2.0",
"@patternfly/react-table": "^6.2.0",
"@types/js-yaml": "^4.0.9",
"date-fns": "^4.1.0",
"js-yaml": "^4.1.0",
"npm-run-all": "^4.1.5",
"react": "^18",

View File

@ -34,6 +34,7 @@ import {
CodeIcon,
} from '@patternfly/react-icons';
import { useState } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes';
import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails';
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
@ -115,7 +116,6 @@ export const Workspaces: React.FunctionComponent = () => {
const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState<string[]>([]);
const [selectedWorkspace, setSelectedWorkspace] = React.useState<Workspace | null>(null);
const [workspaceToDelete, setWorkspaceToDelete] = React.useState<Workspace | null>(null);
const [isActionAlertModalOpen, setIsActionAlertModalOpen] = React.useState(false);
const [activeActionType, setActiveActionType] = React.useState<ActionType | null>(null);
const filterRef = React.useRef<FilterRef>(null);
@ -285,10 +285,21 @@ export const Workspaces: React.FunctionComponent = () => {
// setActiveActionType(ActionType.Edit);
// }, []);
const deleteAction = React.useCallback((workspace: Workspace) => {
setSelectedWorkspace(workspace);
setActiveActionType(ActionType.Delete);
}, []);
const deleteAction = React.useCallback(async () => {
if (!selectedWorkspace) {
return;
}
try {
await api.deleteWorkspace({}, selectedNamespace, selectedWorkspace.name);
// TODO: alert user about success
console.info(`Workspace '${selectedWorkspace.name}' deleted successfully`);
await initialWorkspacesRefresh();
} catch (err) {
// TODO: alert user about error
console.error(`Error deleting workspace '${selectedWorkspace.name}': ${err}`);
}
}, [api, initialWorkspacesRefresh, selectedNamespace, selectedWorkspace]);
const startRestartAction = React.useCallback((workspace: Workspace, action: ActionType) => {
setSelectedWorkspace(workspace);
@ -305,7 +316,8 @@ export const Workspaces: React.FunctionComponent = () => {
const handleDeleteClick = React.useCallback((workspace: Workspace) => {
const buttonElement = document.activeElement as HTMLElement;
buttonElement.blur(); // Remove focus from the currently focused button
setWorkspaceToDelete(workspace); // Open the modal and set workspace to delete
setSelectedWorkspace(workspace);
setActiveActionType(ActionType.Delete);
}, []);
const onCloseActionAlertDialog = () => {
@ -610,7 +622,9 @@ export const Workspaces: React.FunctionComponent = () => {
date={new Date(workspace.activity.lastActivity)}
tooltip={{ variant: TimestampTooltipVariant.default }}
>
1 hour ago
{formatDistanceToNow(new Date(workspace.activity.lastActivity), {
addSuffix: true,
})}
</Timestamp>
</Td>
<Td>
@ -641,14 +655,19 @@ export const Workspaces: React.FunctionComponent = () => {
)}
</Table>
{isActionAlertModalOpen && chooseAlertModal()}
<DeleteModal
isOpen={workspaceToDelete != null}
resourceName={workspaceToDelete?.name || ''}
namespace={workspaceToDelete?.namespace || ''}
title="Delete Workspace?"
onClose={() => setWorkspaceToDelete(null)}
onDelete={() => workspaceToDelete && deleteAction(workspaceToDelete)}
/>
{selectedWorkspace && (
<DeleteModal
isOpen={activeActionType === ActionType.Delete}
resourceName={selectedWorkspace.name}
namespace={selectedWorkspace.namespace}
title="Delete Workspace?"
onClose={() => {
setSelectedWorkspace(null);
setActiveActionType(null);
}}
onDelete={() => deleteAction()}
/>
)}
<Pagination
itemCount={333}
widgetId="bottom-example"

View File

@ -7,9 +7,9 @@ import {
ModalHeader,
TabTitleText,
} from '@patternfly/react-core';
import { Workspace } from '~/shared/api/backendApiTypes';
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
import { ActionButton } from '~/app/pages/Workspaces/workspaceActions/ActionButton';
import { Workspace } from '~/shared/api/backendApiTypes';
import { ActionButton } from '~/shared/components/ActionButton';
interface StartActionAlertProps {
onClose: () => void;

View File

@ -1,16 +1,16 @@
import * as React from 'react';
import {
Button,
Content,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
TabTitleText,
Content,
} from '@patternfly/react-core';
import { Workspace } from '~/shared/api/backendApiTypes';
import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView';
import { ActionButton } from '~/app/pages/Workspaces/workspaceActions/ActionButton';
import { Workspace } from '~/shared/api/backendApiTypes';
import { ActionButton } from '~/shared/components/ActionButton';
interface StopActionAlertProps {
onClose: () => void;

View File

@ -68,9 +68,7 @@ export const patchWorkspace: PatchWorkspaceAPI = (hostPath) => (opts, namespace,
);
export const deleteWorkspace: DeleteWorkspaceAPI = (hostPath) => (opts, namespace, workspace) =>
handleRestFailures(
restDELETE(hostPath, `/workspaces/${namespace}/${workspace}`, {}, {}, opts),
).then((response) => extractNotebookResponse<void>(response));
handleRestFailures(restDELETE(hostPath, `/workspaces/${namespace}/${workspace}`, {}, {}, opts));
export const pauseWorkspace: PauseWorkspaceAPI = (hostPath) => (opts, namespace, workspace) =>
handleRestFailures(

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import {
Modal,
ModalBody,
@ -14,13 +14,14 @@ import {
HelperTextItem,
} from '@patternfly/react-core';
import { default as ExclamationCircleIcon } from '@patternfly/react-icons/dist/esm/icons/exclamation-circle-icon';
import { ActionButton } from '~/shared/components/ActionButton';
interface DeleteModalProps {
isOpen: boolean;
resourceName: string;
namespace: string;
onClose: () => void;
onDelete: (resourceName: string) => void;
onDelete: (resourceName: string) => Promise<void>;
title: string;
}
@ -33,6 +34,7 @@ const DeleteModal: React.FC<DeleteModalProps> = ({
onDelete,
}) => {
const [inputValue, setInputValue] = useState('');
const [isDeleting, setIsDeleting] = React.useState(false);
useEffect(() => {
if (!isOpen) {
@ -40,14 +42,17 @@ const DeleteModal: React.FC<DeleteModalProps> = ({
}
}, [isOpen]);
const handleDelete = () => {
const handleDelete = useCallback(async () => {
if (inputValue === resourceName) {
onDelete(resourceName);
setIsDeleting(true);
await onDelete(resourceName);
setIsDeleting(false);
onClose();
} else {
alert('Resource name does not match.');
}
};
}, [inputValue, onClose, onDelete, resourceName]);
const handleInputChange = (event: React.FormEvent<HTMLInputElement>, value: string) => {
setInputValue(value);
@ -94,17 +99,21 @@ const DeleteModal: React.FC<DeleteModalProps> = ({
</ModalBody>
<ModalFooter>
<div style={{ marginTop: '1rem' }}>
<Button
<ActionButton
action="Delete"
titleOnLoading="Deleting ..."
onClick={handleDelete}
variant="danger"
isDisabled={inputValue !== resourceName}
aria-disabled={inputValue !== resourceName}
>
Delete
</Button>
<Button onClick={onClose} variant="link" style={{ marginLeft: '1rem' }}>
Cancel
</Button>
</ActionButton>
{!isDeleting && (
<Button onClick={onClose} variant="link" style={{ marginLeft: '1rem' }}>
Cancel
</Button>
)}
</div>
</ModalFooter>
</Modal>

View File

@ -103,8 +103,8 @@ export const buildMockWorkspace = (workspace?: Partial<Workspace>): Workspace =>
},
},
activity: {
lastActivity: 1746551485113,
lastUpdate: 1746551485113,
lastActivity: new Date().getTime(),
lastUpdate: new Date().getTime(),
},
pendingRestart: false,
services: [

View File

@ -61,6 +61,10 @@ export const mockWorkspace2: Workspace = buildMockWorkspace({
state: WorkspaceState.WorkspaceStatePaused,
paused: false,
deferUpdates: false,
activity: {
lastActivity: 1735603200000,
lastUpdate: 1735603200000,
},
podTemplate: {
podMetadata: {
labels: { labelKey1: 'labelValue1', labelKey2: 'labelValue2' },
@ -121,6 +125,10 @@ export const mockWorkspace3: Workspace = buildMockWorkspace({
workspaceKind: mockWorkspaceKindInfo1,
state: WorkspaceState.WorkspaceStateRunning,
pendingRestart: true,
activity: {
lastActivity: 1744857600000,
lastUpdate: 1744857600000,
},
});
export const mockWorkspace4 = buildMockWorkspace({