From 4cbc26eaf46e4936388e90845069ec6359badfce Mon Sep 17 00:00:00 2001 From: ElayAharoni <62550608+ElayAharoni@users.noreply.github.com> Date: Thu, 6 Feb 2025 20:32:41 +0200 Subject: [PATCH] feat(ws): Implement Start restart and stop workspace actions (#162) Signed-off-by: Elay Aharoni (EXT-Nokia) Co-authored-by: Elay Aharoni (EXT-Nokia) --- .../src/app/pages/Workspaces/Workspaces.tsx | 154 ++++++++++---- .../WorkspaceRedirectInformationView.tsx | 188 ++++++++++++++++++ .../WorkspaceRestartActionModal.tsx | 71 +++++++ .../WorkspaceStartActionModal.tsx | 57 ++++++ .../WorkspaceStopActionModal.tsx | 71 +++++++ 5 files changed, 501 insertions(+), 40 deletions(-) create mode 100644 workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView.tsx create mode 100644 workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx create mode 100644 workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx create mode 100644 workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index f1d20e14..b3ca85ed 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -11,8 +11,8 @@ import { Pagination, Button, Content, - Tooltip, Brand, + Tooltip, } from '@patternfly/react-core'; import { Table, @@ -34,9 +34,21 @@ import DeleteModal from '~/shared/components/DeleteModal'; import { buildKindLogoDictionary } from '~/app/actions/WorkspaceKindsActions'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; import { WorkspaceConnectAction } from '~/app/pages/Workspaces/WorkspaceConnectAction'; +import { WorkspaceStartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal'; +import { WorkspaceRestartActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal'; +import { WorkspaceStopActionModal } from '~/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal'; import Filter, { FilteredColumn } from 'shared/components/Filter'; import { formatRam } from 'shared/utilities/WorkspaceResources'; +export enum ActionType { + ViewDetails, + Edit, + Delete, + Start, + Restart, + Stop, +} + export const Workspaces: React.FunctionComponent = () => { /* Mocked workspaces, to be removed after fetching info from backend */ const mockWorkspaces: Workspace[] = [ @@ -85,7 +97,7 @@ export const Workspaces: React.FunctionComponent = () => { lastUpdate: 0, }, pauseTime: 0, - pendingRestart: false, + pendingRestart: true, podTemplateOptions: { imageConfig: { desired: '', @@ -170,7 +182,7 @@ export const Workspaces: React.FunctionComponent = () => { lastActivity: 'Last Activity', }; - const filterableColumns: WorkspacesColumnNames = { + const filterableColumns = { name: 'Name', kind: 'Kind', image: 'Image', @@ -182,10 +194,12 @@ export const Workspaces: React.FunctionComponent = () => { // change when fetch workspaces is implemented const initialWorkspaces = mockWorkspaces; - const [workspaces, setWorkspaces] = useState(initialWorkspaces); + const [workspaces, setWorkspaces] = useState(initialWorkspaces); 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 selectWorkspace = React.useCallback( (newSelectedWorkspace) => { @@ -197,6 +211,7 @@ export const Workspaces: React.FunctionComponent = () => { }, [selectedWorkspace], ); + const setWorkspaceExpanded = (workspace: Workspace, isExpanding = true) => setExpandedWorkspacesNames((prevExpanded) => { const newExpandedWorkspacesNames = prevExpanded.filter((wsName) => wsName !== workspace.name); @@ -300,20 +315,31 @@ export const Workspaces: React.FunctionComponent = () => { // Actions + const viewDetailsClick = React.useCallback((workspace: Workspace) => { + setSelectedWorkspace(workspace); + setActiveActionType(ActionType.ViewDetails); + }, []); + const editAction = React.useCallback((workspace: Workspace) => { - console.log(`Clicked on edit, on row ${workspace.name}`); + setSelectedWorkspace(workspace); + setActiveActionType(ActionType.Edit); }, []); const deleteAction = React.useCallback((workspace: Workspace) => { - console.log(`Clicked on delete, on row ${workspace.name}`); + setSelectedWorkspace(workspace); + setActiveActionType(ActionType.Delete); }, []); - const startRestartAction = React.useCallback((workspace: Workspace) => { - console.log(`Clicked on start/restart, on row ${workspace.name}`); + const startRestartAction = React.useCallback((workspace: Workspace, action: ActionType) => { + setSelectedWorkspace(workspace); + setActiveActionType(action); + setIsActionAlertModalOpen(true); }, []); const stopAction = React.useCallback((workspace: Workspace) => { - console.log(`Clicked on stop, on row ${workspace.name}`); + setSelectedWorkspace(workspace); + setActiveActionType(ActionType.Stop); + setIsActionAlertModalOpen(true); }, []); const handleDeleteClick = React.useCallback((workspace: Workspace) => { @@ -322,35 +348,79 @@ export const Workspaces: React.FunctionComponent = () => { setWorkspaceToDelete(workspace); // Open the modal and set workspace to delete }, []); - const defaultActions = React.useCallback( - (workspace: Workspace): IActions => - [ - { - title: 'View Details', - onClick: () => selectWorkspace(workspace), - }, - { - title: 'Edit', - onClick: () => editAction(workspace), - }, - { - title: 'Delete', - onClick: () => handleDeleteClick(workspace), - }, - { - isSeparator: true, - }, - { - title: 'Start/restart', - onClick: () => startRestartAction(workspace), - }, - { - title: 'Stop', - onClick: () => stopAction(workspace), - }, - ] as IActions, - [selectWorkspace, editAction, handleDeleteClick, startRestartAction, stopAction], - ); + const onCloseActionAlertDialog = () => { + setIsActionAlertModalOpen(false); + setSelectedWorkspace(null); + setActiveActionType(null); + }; + + const workspaceDefaultActions = (workspace: Workspace): IActions => { + const workspaceState = workspace.status.state; + const workspaceActions = [ + { + title: 'View Details', + onClick: () => viewDetailsClick(workspace), + }, + { + title: 'Edit', + onClick: () => editAction(workspace), + }, + { + title: 'Delete', + onClick: () => handleDeleteClick(workspace), + }, + { + isSeparator: true, + }, + workspaceState !== WorkspaceState.Running + ? { + title: 'Start', + onClick: () => startRestartAction(workspace, ActionType.Start), + } + : { + title: 'Restart', + onClick: () => startRestartAction(workspace, ActionType.Restart), + }, + ] as IActions; + + if (workspaceState === WorkspaceState.Running) { + workspaceActions.push({ + title: 'Stop', + onClick: () => stopAction(workspace), + }); + } + return workspaceActions; + }; + + const chooseAlertModal = () => { + switch (activeActionType) { + case ActionType.Start: + return ( + + ); + case ActionType.Restart: + return ( + + ); + case ActionType.Stop: + return ( + + ); + } + return undefined; + }; // States @@ -401,7 +471,10 @@ export const Workspaces: React.FunctionComponent = () => { ); return ( - + @@ -483,7 +556,7 @@ export const Workspaces: React.FunctionComponent = () => { ({ + items={workspaceDefaultActions(workspace).map((action) => ({ ...action, 'data-testid': `action-${typeof action.title === 'string' ? action.title.toLowerCase() : ''}`, }))} @@ -496,6 +569,7 @@ export const Workspaces: React.FunctionComponent = () => { ))} + {isActionAlertModalOpen && chooseAlertModal()} { + switch (level) { + case 'Info': + return ( + + + + ); + case 'Warning': + return ( + + + + ); + case 'Danger': + return ( + + + + ); + default: + return ( + + + + ); + } +}; + +export const WorkspaceRedirectInformationView: React.FC = () => { + const [activeKey, setActiveKey] = React.useState(0); + // change this to get from BE, and use the workspaceKinds API + const workspaceKind = mockedWorkspaceKind; + + const { imageConfig } = workspaceKind.podTemplate.options; + const { podConfig } = workspaceKind.podTemplate.options; + + const imageConfigRedirects = imageConfig.values.map((value) => ({ + src: value.id, + dest: value.redirect.to, + message: value.redirect.message.text, + level: value.redirect.message.level, + })); + const podConfigRedirects = podConfig.values.map((value) => ({ + src: value.id, + dest: value.redirect.to, + message: value.redirect.message.text, + level: value.redirect.message.level, + })); + + const getMaxLevel = ( + redirects: { dest: string; level: string; message: string; src: string }[], + ) => { + let maxLevel = redirects[0].level; + redirects.forEach((redirect) => { + if ( + (maxLevel === 'Info' && (redirect.level === 'Warning' || redirect.level === 'Danger')) || + (maxLevel === 'Warning' && redirect.level === 'Danger') + ) { + maxLevel = redirect.level; + } + }); + return maxLevel; + }; + + return ( + setActiveKey(eventKey)}> + {imageConfigRedirects.length > 0 && ( + + Image Config {getLevelIcon(getMaxLevel(imageConfigRedirects))} + + } + > + {imageConfigRedirects.map((redirect, index) => ( + + {getLevelIcon(redirect.level)} + ${redirect.dest}`}> + {redirect.message} + + + ))} + + )} + {podConfigRedirects.length > 0 && ( + Pod Config {getLevelIcon(getMaxLevel(podConfigRedirects))} + } + > + {podConfigRedirects.map((redirect, index) => ( + + {getLevelIcon(redirect.level)} + ${redirect.dest}`}> + {redirect.message} + + + ))} + + )} + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx new file mode 100644 index 00000000..8139b48c --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceRestartActionModal.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { + Button, + Content, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + TabTitleText, +} from '@patternfly/react-core'; +import { Workspace } from '~/shared/types'; +import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; + +interface RestartActionAlertProps { + onClose: () => void; + isOpen: boolean; + workspace: Workspace | null; +} + +export const WorkspaceRestartActionModal: React.FC = ({ + onClose, + isOpen, + workspace, +}) => { + const workspacePendingUpdate = workspace?.status.pendingRestart; + const handleClick = (isUpdate = false) => { + if (isUpdate) { + console.log(`Update ${workspace?.name}`); + } + console.log(`Restart ${workspace?.name}`); + onClose(); + }; + return ( + + + + {workspacePendingUpdate ? ( + <> + + There are pending redirect updates for that workspace. Are you sure you want to + proceed? + + + + ) : ( + Are you sure you want to restart the workspace? + )} + + + {workspacePendingUpdate && ( + + )} + + + + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx new file mode 100644 index 00000000..8e065e77 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStartActionModal.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { + Button, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + TabTitleText, +} from '@patternfly/react-core'; +import { Workspace } from '~/shared/types'; +import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; + +interface StartActionAlertProps { + onClose: () => void; + isOpen: boolean; + workspace: Workspace | null; +} + +export const WorkspaceStartActionModal: React.FC = ({ + onClose, + isOpen, + workspace, +}) => { + const handleClick = (isUpdate = false) => { + if (isUpdate) { + console.log(`Update ${workspace?.name}`); + } + console.log(`Start ${workspace?.name}`); + onClose(); + }; + return ( + + + + + There are pending redirect updates for that workspace. Are you sure you want to proceed? + + + + + + + + + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx new file mode 100644 index 00000000..992044f6 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/workspaceActions/WorkspaceStopActionModal.tsx @@ -0,0 +1,71 @@ +import * as React from 'react'; +import { + Button, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + TabTitleText, + Content, +} from '@patternfly/react-core'; +import { Workspace } from '~/shared/types'; +import { WorkspaceRedirectInformationView } from '~/app/pages/Workspaces/workspaceActions/WorkspaceRedirectInformationView'; + +interface StopActionAlertProps { + onClose: () => void; + isOpen: boolean; + workspace: Workspace | null; +} + +export const WorkspaceStopActionModal: React.FC = ({ + onClose, + isOpen, + workspace, +}) => { + const workspacePendingUpdate = workspace?.status.pendingRestart; + const handleClick = (isUpdate = false) => { + if (isUpdate) { + console.log(`Update ${workspace?.name}`); + } + console.log(`Stop ${workspace?.name}`); + onClose(); + }; + return ( + + + + {workspacePendingUpdate ? ( + <> + + There are pending redirect updates for that workspace. Are you sure you want to + proceed? + + + + ) : ( + Are you sure you want to stop the workspace? + )} + + + {workspacePendingUpdate && ( + + )} + + + + + ); +};