diff --git a/workspaces/frontend/src/app/App.tsx b/workspaces/frontend/src/app/App.tsx index dabd3b3..283a3ee 100644 --- a/workspaces/frontend/src/app/App.tsx +++ b/workspaces/frontend/src/app/App.tsx @@ -43,6 +43,7 @@ const App: React.FC = () => { } > diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx new file mode 100644 index 0000000..8afad9b --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetails.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { + DrawerActions, + DrawerCloseButton, + DrawerHead, + DrawerPanelBody, + DrawerPanelContent, + Tabs, + Tab, + TabTitleText, + Title, +} from '@patternfly/react-core'; +import { Workspace } from '~/shared/types'; +import { WorkspaceDetailsOverview } from '~/app/pages/Workspaces/Details/WorkspaceDetailsOverview'; +import { WorkspaceDetailsActions } from '~/app/pages/Workspaces/Details/WorkspaceDetailsActions'; + +type WorkspaceDetailsProps = { + workspace: Workspace; + onCloseClick: React.MouseEventHandler; + onEditClick: React.MouseEventHandler; + onDeleteClick: React.MouseEventHandler; +}; + +export const WorkspaceDetails: React.FunctionComponent = ({ + workspace, + onCloseClick, + onEditClick, + onDeleteClick, +}) => { + const [activeTabKey, setActiveTabKey] = React.useState(0); + + const handleTabClick = ( + event: React.MouseEvent | React.KeyboardEvent | MouseEvent, + tabIndex: string | number, + ) => { + setActiveTabKey(tabIndex); + }; + + return ( + + + {workspace.name} + + + + + + + + Overview} aria-label="Overview"> + + + Activity} aria-label="Activity"> + Activity + + Logs} aria-label="Logs"> + Logs + + Pod template} + aria-label="Pod template" + > + Pod template + + + + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActions.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActions.tsx new file mode 100644 index 0000000..4cb7f1f --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsActions.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { + Dropdown, + DropdownList, + MenuToggle, + DropdownItem, + Flex, + FlexItem, +} from '@patternfly/react-core'; + +interface WorkspaceDetailsActionsProps { + onEditClick: React.MouseEventHandler; + onDeleteClick: React.MouseEventHandler; +} + +export const WorkspaceDetailsActions: React.FC = ({ + onEditClick, + onDeleteClick, +}) => { + const [isOpen, setOpen] = React.useState(false); + + return ( + + + setOpen(false)} + onOpenChange={(open) => setOpen(open)} + popperProps={{ position: 'end' }} + toggle={(toggleRef) => ( + setOpen(!isOpen)} + isExpanded={isOpen} + aria-label="Workspace details action toggle" + data-testid="workspace-details-action-toggle" + > + Actions + + )} + > + + + Edit + + + Delete + + + + + + ); +}; diff --git a/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx new file mode 100644 index 0000000..67e7f03 --- /dev/null +++ b/workspaces/frontend/src/app/pages/Workspaces/Details/WorkspaceDetailsOverview.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { + DescriptionList, + DescriptionListTerm, + DescriptionListGroup, + DescriptionListDescription, +} from '@patternfly/react-core'; +import { Workspace } from '~/shared/types'; + +type WorkspaceDetailsOverviewProps = { + workspace: Workspace; +}; + +export const WorkspaceDetailsOverview: React.FunctionComponent = ({ + workspace, +}) => ( + + + Name + {workspace.name} + + + Kind + {workspace.kind} + + + Labels + + {workspace.podTemplate.podMetadata.labels.join(', ')} + + + + Pod config + {workspace.options.podConfig} + + +); diff --git a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx index b3734e9..031e5da 100644 --- a/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx +++ b/workspaces/frontend/src/app/pages/Workspaces/Workspaces.tsx @@ -1,5 +1,8 @@ import * as React from 'react'; import { + Drawer, + DrawerContent, + DrawerContentBody, PageSection, MenuToggle, TimestampTooltipVariant, @@ -36,7 +39,7 @@ import { } from '@patternfly/react-table'; import { FilterIcon } from '@patternfly/react-icons'; import { Workspace, WorkspacesColumnNames, WorkspaceState } from '~/shared/types'; - +import { WorkspaceDetails } from '~/app/pages/Workspaces/Details/WorkspaceDetails'; import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow'; import { formatRam } from 'shared/utilities/WorkspaceResources'; @@ -52,6 +55,10 @@ export const Workspaces: React.FunctionComponent = () => { cpu: 3, ram: 500, podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, volumes: { home: '/home', data: [ @@ -98,6 +105,10 @@ export const Workspaces: React.FunctionComponent = () => { cpu: 1, ram: 12540, podTemplate: { + podMetadata: { + labels: ['label1', 'label2'], + annotations: ['annotation1', 'annotation2'], + }, volumes: { home: '/home', data: [ @@ -145,6 +156,20 @@ export const Workspaces: React.FunctionComponent = () => { lastActivity: 'Last Activity', }; + // Selected workspace + const [selectedWorkspace, setSelectedWorkspace] = React.useState(null); + + const selectWorkspace = React.useCallback( + (newSelectedWorkspace) => { + if (selectedWorkspace?.name === newSelectedWorkspace?.name) { + setSelectedWorkspace(null); + } else { + setSelectedWorkspace(newSelectedWorkspace); + } + }, + [selectedWorkspace], + ); + // Filter const [activeAttributeMenu, setActiveAttributeMenu] = React.useState(columnNames.name); const [isAttributeMenuOpen, setIsAttributeMenuOpen] = React.useState(false); @@ -389,28 +414,51 @@ export const Workspaces: React.FunctionComponent = () => { // Actions - const defaultActions = (workspace: Workspace): IActions => - [ - { - title: 'Edit', - onClick: () => console.log(`Clicked on edit, on row ${workspace.name}`), - }, - { - title: 'Delete', - onClick: () => console.log(`Clicked on delete, on row ${workspace.name}`), - }, - { - isSeparator: true, - }, - { - title: 'Start/restart', - onClick: () => console.log(`Clicked on start/restart, on row ${workspace.name}`), - }, - { - title: 'Stop', - onClick: () => console.log(`Clicked on stop, on row ${workspace.name}`), - }, - ] as IActions; + const editAction = React.useCallback((workspace: Workspace) => { + console.log(`Clicked on edit, on row ${workspace.name}`); + }, []); + + const deleteAction = React.useCallback((workspace: Workspace) => { + console.log(`Clicked on delete, on row ${workspace.name}`); + }, []); + + const startRestartAction = React.useCallback((workspace: Workspace) => { + console.log(`Clicked on start/restart, on row ${workspace.name}`); + }, []); + + const stopAction = React.useCallback((workspace: Workspace) => { + console.log(`Clicked on stop, on row ${workspace.name}`); + }, []); + + const defaultActions = React.useCallback( + (workspace: Workspace): IActions => + [ + { + title: 'View Details', + onClick: () => selectWorkspace(workspace), + }, + { + title: 'Edit', + onClick: () => editAction(workspace), + }, + { + title: 'Delete', + onClick: () => deleteAction(workspace), + }, + { + isSeparator: true, + }, + { + title: 'Start/restart', + onClick: () => startRestartAction(workspace), + }, + { + title: 'Stop', + onClick: () => stopAction(workspace), + }, + ] as IActions, + [selectWorkspace, editAction, deleteAction, startRestartAction, stopAction], + ); // States @@ -447,80 +495,100 @@ export const Workspaces: React.FunctionComponent = () => { setPage(newPage); }; + const workspaceDetailsContent = ( + <> + {selectedWorkspace && ( + selectWorkspace(null)} + onEditClick={() => editAction(selectedWorkspace)} + onDeleteClick={() => deleteAction(selectedWorkspace)} + /> + )} + + ); + return ( - - Kubeflow Workspaces -

View your existing workspaces or create new workspaces.

- {toolbar} - - - - - - - - - - - - - - - {sortedWorkspaces.map((workspace, rowIndex) => ( - - - - - - - - - - - - - - {isWorkspaceExpanded(workspace) && ( - - )} - - ))} -
- {columnNames.name}{columnNames.kind}{columnNames.image}{columnNames.podConfig}{columnNames.state}{columnNames.homeVol} - {columnNames.cpu} - - {columnNames.ram} - {columnNames.lastActivity} -
setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)), - }} - /> - {workspace.name}{workspace.kind}{workspace.options.imageConfig}{workspace.options.podConfig} - - {workspace.podTemplate.volumes.home}{`${workspace.cpu}%`}{formatRam(workspace.ram)} - - 1 hour ago - - - -
- -
+ + + + + Kubeflow Workspaces +

View your existing workspaces or create new workspaces.

+ {toolbar} + + + + + + + + + + + + + + + {sortedWorkspaces.map((workspace, rowIndex) => ( + + + + + + + + + + + + + + {isWorkspaceExpanded(workspace) && ( + + )} + + ))} +
+ {columnNames.name}{columnNames.kind}{columnNames.image}{columnNames.podConfig}{columnNames.state}{columnNames.homeVol} + {columnNames.cpu} + + {columnNames.ram} + {columnNames.lastActivity} +
+ setWorkspaceExpanded(workspace, !isWorkspaceExpanded(workspace)), + }} + /> + {workspace.name}{workspace.kind}{workspace.options.imageConfig}{workspace.options.podConfig} + + {workspace.podTemplate.volumes.home}{`${workspace.cpu}%`}{formatRam(workspace.ram)} + + 1 hour ago + + + +
+ +
+
+
+
); }; diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index 8893810..577c371 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -55,6 +55,10 @@ export interface Workspace { cpu: number; ram: number; podTemplate: { + podMetadata: { + labels: string[]; + annotations: string[]; + }; volumes: { home: string; data: {