From 3c8c8e0c2af4a9581eb49e6dd3a5877f5c62ea04 Mon Sep 17 00:00:00 2001 From: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> Date: Tue, 27 May 2025 10:44:33 -0300 Subject: [PATCH] feat(ws): call delete Workspace API from the frontend (#383) Signed-off-by: Guilherme Caponetto <638737+caponetto@users.noreply.github.com> --- workspaces/frontend/package-lock.json | 10 ++++ workspaces/frontend/package.json | 1 + .../src/app/pages/Workspaces/Workspaces.tsx | 49 +++++++++++++------ .../WorkspaceStartActionModal.tsx | 4 +- .../WorkspaceStopActionModal.tsx | 6 +-- .../src/shared/api/notebookService.ts | 4 +- .../components}/ActionButton.tsx | 0 .../src/shared/components/DeleteModal.tsx | 29 +++++++---- .../frontend/src/shared/mock/mockBuilder.ts | 4 +- .../shared/mock/mockNotebookServiceData.ts | 8 +++ 10 files changed, 80 insertions(+), 35 deletions(-) rename workspaces/frontend/src/{app/pages/Workspaces/workspaceActions => shared/components}/ActionButton.tsx (100%) diff --git a/workspaces/frontend/package-lock.json b/workspaces/frontend/package-lock.json index 97a1a67..68a58be 100644 --- a/workspaces/frontend/package-lock.json +++ b/workspaces/frontend/package-lock.json @@ -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", diff --git a/workspaces/frontend/package.json b/workspaces/frontend/package.json index 6af3601..c0dbdd1 100644 --- a/workspaces/frontend/package.json +++ b/workspaces/frontend/package.json @@ -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", diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index 2667120..7036486 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -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([]); const [expandedWorkspacesNames, setExpandedWorkspacesNames] = React.useState([]); const [selectedWorkspace, setSelectedWorkspace] = React.useState(null); - const [workspaceToDelete, setWorkspaceToDelete] = React.useState(null); const [isActionAlertModalOpen, setIsActionAlertModalOpen] = React.useState(false); const [activeActionType, setActiveActionType] = React.useState(null); const filterRef = React.useRef(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, + })} @@ -641,14 +655,19 @@ export const Workspaces: React.FunctionComponent = () => { )} {isActionAlertModalOpen && chooseAlertModal()} - setWorkspaceToDelete(null)} - onDelete={() => workspaceToDelete && deleteAction(workspaceToDelete)} - /> + {selectedWorkspace && ( + { + setSelectedWorkspace(null); + setActiveActionType(null); + }} + onDelete={() => deleteAction()} + /> + )} void; diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx index 0c8a29d..eb00e42 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx @@ -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; diff --git a/workspaces/frontend/src/shared/api/notebookService.ts b/workspaces/frontend/src/shared/api/notebookService.ts index bb85798..736f13d 100644 --- a/workspaces/frontend/src/shared/api/notebookService.ts +++ b/workspaces/frontend/src/shared/api/notebookService.ts @@ -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(response)); + handleRestFailures(restDELETE(hostPath, `/workspaces/${namespace}/${workspace}`, {}, {}, opts)); export const pauseWorkspace: PauseWorkspaceAPI = (hostPath) => (opts, namespace, workspace) => handleRestFailures( diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/ActionButton.tsx b/workspaces/frontend/src/shared/components/ActionButton.tsx similarity index 100% rename from workspaces/frontend/src/app/pages/Workspaces/workspaceActions/ActionButton.tsx rename to workspaces/frontend/src/shared/components/ActionButton.tsx diff --git a/workspaces/frontend/src/shared/components/DeleteModal.tsx b/workspaces/frontend/src/shared/components/DeleteModal.tsx index a4bf68a..fa9d87f 100644 --- a/workspaces/frontend/src/shared/components/DeleteModal.tsx +++ b/workspaces/frontend/src/shared/components/DeleteModal.tsx @@ -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; title: string; } @@ -33,6 +34,7 @@ const DeleteModal: React.FC = ({ onDelete, }) => { const [inputValue, setInputValue] = useState(''); + const [isDeleting, setIsDeleting] = React.useState(false); useEffect(() => { if (!isOpen) { @@ -40,14 +42,17 @@ const DeleteModal: React.FC = ({ } }, [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, value: string) => { setInputValue(value); @@ -94,17 +99,21 @@ const DeleteModal: React.FC = ({
- - + + {!isDeleting && ( + + )}
diff --git a/workspaces/frontend/src/shared/mock/mockBuilder.ts b/workspaces/frontend/src/shared/mock/mockBuilder.ts index 9187df2..fe228d2 100644 --- a/workspaces/frontend/src/shared/mock/mockBuilder.ts +++ b/workspaces/frontend/src/shared/mock/mockBuilder.ts @@ -103,8 +103,8 @@ export const buildMockWorkspace = (workspace?: Partial): Workspace => }, }, activity: { - lastActivity: 1746551485113, - lastUpdate: 1746551485113, + lastActivity: new Date().getTime(), + lastUpdate: new Date().getTime(), }, pendingRestart: false, services: [ diff --git a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts index 093835c..44518a8 100644 --- a/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts +++ b/workspaces/frontend/src/shared/mock/mockNotebookServiceData.ts @@ -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({