From 08852d47a42730b1fa7d6c54c48f2f05078d92af Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Tue, 25 Mar 2025 14:23:01 +0200 Subject: [PATCH 1/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup #203 Signed-off-by: Liav Weiss (EXT-Nokia) --- workspaces/frontend/src/app/AppRoutes.tsx | 10 + .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 279 ++++++++++++++++++ workspaces/frontend/src/shared/types.ts | 8 + 3 files changed, 297 insertions(+) create mode 100644 workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx diff --git a/workspaces/frontend/src/app/AppRoutes.tsx b/workspaces/frontend/src/app/AppRoutes.tsx index 1bd1258f5..8401fae69 100644 --- a/workspaces/frontend/src/app/AppRoutes.tsx +++ b/workspaces/frontend/src/app/AppRoutes.tsx @@ -5,6 +5,7 @@ import { Debug } from './pages/Debug/Debug'; import { Workspaces } from './pages/Workspaces/Workspaces'; import { WorkspaceCreation } from './pages/Workspaces/Creation/WorkspaceCreation'; import '~/shared/style/MUI-theme.scss'; +import { WorkspaceKinds } from './pages/WorkspaceKinds/WorkspaceKinds'; export const isNavDataGroup = (navItem: NavDataItem): navItem is NavDataGroup => 'children' in navItem; @@ -38,6 +39,14 @@ export const useAdminDebugSettings = (): NavDataItem[] => { label: 'Debug', children: [{ label: 'Notebooks', path: '/notebookDebugSettings' }], }, + { + label: 'Workspaces', + path: '/', + }, + { + label: 'WorkspaceKinds', + path: '/workspacekinds', + }, ]; }; @@ -55,6 +64,7 @@ const AppRoutes: React.FC = () => { return ( } /> + } /> } /> } /> } /> diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx new file mode 100644 index 000000000..219282faf --- /dev/null +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -0,0 +1,279 @@ +import * as React from 'react'; +import { + Drawer, + DrawerContent, + DrawerContentBody, + PageSection, + Content, + Brand, + Tooltip, + Label, +} from '@patternfly/react-core'; +import { + Table, + Thead, + Tr, + Th, + Tbody, + Td, + ThProps, + ActionsColumn, + IActions, +} from '@patternfly/react-table'; +import { + CodeIcon, +} from '@patternfly/react-icons'; +import { useState } from 'react'; +import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types'; +import Filter, { FilteredColumn } from 'shared/components/Filter'; +import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; +export enum ActionType { + ViewDetails, +} + +export const WorkspaceKinds: React.FunctionComponent = () => { + + const mockNumberOfWorkspaces = 1 // Todo: create a function to calculate number of workspaces for each workspace kind. + + // Table columns + const columnNames: WorkspaceKindsColumnNames = { + icon: '', + name: 'Name', + description: 'Description', + deprecated: 'Status', + numberOfWorkspaces: 'Number Of Workspaces', + + }; + + const filterableColumns = { + name: 'Name', + deprecated: 'Status', + }; + + const [initialWorkspaceKinds] = useWorkspaceKinds();; + const [workspaceKinds, setWorkspaceKinds] = useState(initialWorkspaceKinds); + const [expandedWorkspaceKindsNames, setExpandedWorkspaceKindsNames] = React.useState([]); + const [selectedWorkspaceKind, setSelectedWorkspacekind] = React.useState(null); + const [activeActionType, setActiveActionType] = React.useState(null); + + const setWorkspaceKindsExpanded = (workspaceKind: WorkspaceKind, isExpanding = true) => + setExpandedWorkspaceKindsNames((prevExpanded) => { + const newExpandedWorkspaceKindsNames = prevExpanded.filter((wsName) => wsName !== workspaceKind.name); + return isExpanding + ? [...newExpandedWorkspaceKindsNames, workspaceKind.name] + : newExpandedWorkspaceKindsNames; + }); + + const isWorkspaceKindExpanded = (workspaceKind: WorkspaceKind) => + expandedWorkspaceKindsNames.includes(workspaceKind.name); + + // filter function to pass to the filter component + const onFilter = (filters: FilteredColumn[]) => { + // Search name with search value + let filteredWorkspaceKinds = initialWorkspaceKinds; + filters.forEach((filter) => { + let searchValueInput: RegExp; + try { + searchValueInput = new RegExp(filter.value, 'i'); + } catch { + searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + } + + filteredWorkspaceKinds = filteredWorkspaceKinds.filter((workspaceKind) => { + if (filter.value === '') { + return true; + } + switch (filter.columnName) { + case columnNames.name: + return workspaceKind.name.search(searchValueInput) >= 0; + case columnNames.deprecated: + if (filter.value.toUpperCase() === "ACTIVE") { + return workspaceKind.deprecated === false; + } else if (filter.value.toUpperCase() === "DEPRECATED") { + return workspaceKind.deprecated === true; + } + return true + default: + return true; + } + }); + }); + setWorkspaceKinds(filteredWorkspaceKinds); + }; + + // Column sorting + + const [activeSortIndex, setActiveSortIndex] = React.useState(null); + const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null); + + const getSortableRowValues = (workspaceKind: WorkspaceKind): (string | boolean | number)[] => { + const { icon, name, description, deprecated, numOfWrokspaces} = + { + icon: "", + name: workspaceKind.name, + description: workspaceKind.description, + deprecated: workspaceKind.deprecated, + numOfWrokspaces: mockNumberOfWorkspaces, + }; + return [icon, name, description, deprecated, numOfWrokspaces]; + }; + + let sortedWorkspaceKinds = workspaceKinds; + if (activeSortIndex !== null) { + sortedWorkspaceKinds = workspaceKinds.sort((a, b) => { + const aValue = getSortableRowValues(a)[activeSortIndex]; + const bValue = getSortableRowValues(b)[activeSortIndex]; + if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { + // Convert boolean to number (true -> 1, false -> 0) for sorting + return activeSortDirection === 'asc' + ? Number(aValue) - Number(bValue) + : Number(bValue) - Number(aValue); + } + // String sort + if (activeSortDirection === 'asc') { + return (aValue as string).localeCompare(bValue as string); + } + return (bValue as string).localeCompare(aValue as string); + }); + } + + const getSortParams = (columnIndex: number): ThProps['sort'] => ({ + sortBy: { + index: activeSortIndex || 0, + direction: activeSortDirection || 'asc', + defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' + }, + onSort: (_event, index, direction) => { + setActiveSortIndex(index); + setActiveSortDirection(direction); + }, + columnIndex, + }); + + // Actions + + const viewDetailsClick = React.useCallback((workspaceKind: WorkspaceKind) => { + setSelectedWorkspacekind(workspaceKind); + setActiveActionType(ActionType.ViewDetails); + }, []); + + const workspaceKindsDefaultActions = (workspaceKind: WorkspaceKind): IActions => { + const workspaceKindsActions = [ + { + id: 'view-details', + title: 'View Details', + onClick: () => viewDetailsClick(workspaceKind), + }, + { + isSeparator: true, + }, + + ] as IActions; + + return workspaceKindsActions; + }; + + const workspaceDetailsContent = null // Todo: Detail need to be implemented. + + const DESCRIPTION_CHAR_LIMIT = 50; + + return ( + + + + + +

Kubeflow Workspace Kinds

+

View your existing workspace kinds.

+
+
+ + + {/* */} + + + + + + ))} + + + {sortedWorkspaceKinds.map((workspaceKind, rowIndex) => ( + + + + + + + + + + + + ))} +
+ {Object.values(columnNames).map((columnName, index) => ( + + {columnName} + +
+ setWorkspaceKindsExpanded(workspaceKind, !isWorkspaceKindExpanded(workspaceKind)), + }} + /> + {workspaceKind.icon.url ? ( + + ) : ( + + )} + + {workspaceKind.name} + + + + {workspaceKind.description.length > DESCRIPTION_CHAR_LIMIT ? + `${workspaceKind.description.slice(0, DESCRIPTION_CHAR_LIMIT)}...` : workspaceKind.description} + + + + {workspaceKind.deprecated ? ( + + ) : ( + + + + )} + {mockNumberOfWorkspaces} + ({ + ...action, + 'data-testid': `action-${action.id || ''}`, + }))} + /> +
+
+
+
+
+ ); +}; diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index a88fe8ad0..c2087bbe9 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -142,3 +142,11 @@ export type WorkspacesColumnNames = { lastActivity: string; redirectStatus: string; }; + +export type WorkspaceKindsColumnNames = { + icon: string; + name: string; + description: string; + deprecated: string; + numberOfWorkspaces: string; +}; \ No newline at end of file From de04306a4bc9358b77742868f689ad8bd5b194b4 Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Tue, 25 Mar 2025 15:56:41 +0200 Subject: [PATCH 2/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup #203 Signed-off-by: Liav Weiss (EXT-Nokia) --- .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 124 ++++++++++-------- workspaces/frontend/src/shared/types.ts | 2 +- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index 219282faf..3ae05aeb5 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -20,20 +20,18 @@ import { ActionsColumn, IActions, } from '@patternfly/react-table'; -import { - CodeIcon, -} from '@patternfly/react-icons'; +import { CodeIcon } from '@patternfly/react-icons'; import { useState } from 'react'; import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types'; -import Filter, { FilteredColumn } from 'shared/components/Filter'; import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; +import Filter, { FilteredColumn } from 'shared/components/Filter'; + export enum ActionType { ViewDetails, } export const WorkspaceKinds: React.FunctionComponent = () => { - - const mockNumberOfWorkspaces = 1 // Todo: create a function to calculate number of workspaces for each workspace kind. + const mockNumberOfWorkspaces = 1; // Todo: create a function to calculate number of workspaces for each workspace kind. // Table columns const columnNames: WorkspaceKindsColumnNames = { @@ -42,23 +40,28 @@ export const WorkspaceKinds: React.FunctionComponent = () => { description: 'Description', deprecated: 'Status', numberOfWorkspaces: 'Number Of Workspaces', - }; const filterableColumns = { name: 'Name', deprecated: 'Status', }; - - const [initialWorkspaceKinds] = useWorkspaceKinds();; + + const [initialWorkspaceKinds] = useWorkspaceKinds(); const [workspaceKinds, setWorkspaceKinds] = useState(initialWorkspaceKinds); - const [expandedWorkspaceKindsNames, setExpandedWorkspaceKindsNames] = React.useState([]); - const [selectedWorkspaceKind, setSelectedWorkspacekind] = React.useState(null); + const [expandedWorkspaceKindsNames, setExpandedWorkspaceKindsNames] = React.useState( + [], + ); + const [selectedWorkspaceKind, setSelectedWorkspacekind] = React.useState( + null, + ); const [activeActionType, setActiveActionType] = React.useState(null); const setWorkspaceKindsExpanded = (workspaceKind: WorkspaceKind, isExpanding = true) => setExpandedWorkspaceKindsNames((prevExpanded) => { - const newExpandedWorkspaceKindsNames = prevExpanded.filter((wsName) => wsName !== workspaceKind.name); + const newExpandedWorkspaceKindsNames = prevExpanded.filter( + (wsName) => wsName !== workspaceKind.name, + ); return isExpanding ? [...newExpandedWorkspaceKindsNames, workspaceKind.name] : newExpandedWorkspaceKindsNames; @@ -87,12 +90,13 @@ export const WorkspaceKinds: React.FunctionComponent = () => { case columnNames.name: return workspaceKind.name.search(searchValueInput) >= 0; case columnNames.deprecated: - if (filter.value.toUpperCase() === "ACTIVE") { + if (filter.value.toUpperCase() === 'ACTIVE') { return workspaceKind.deprecated === false; - } else if (filter.value.toUpperCase() === "DEPRECATED") { + } + if (filter.value.toUpperCase() === 'DEPRECATED') { return workspaceKind.deprecated === true; } - return true + return true; default: return true; } @@ -107,14 +111,13 @@ export const WorkspaceKinds: React.FunctionComponent = () => { const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null); const getSortableRowValues = (workspaceKind: WorkspaceKind): (string | boolean | number)[] => { - const { icon, name, description, deprecated, numOfWrokspaces} = - { - icon: "", - name: workspaceKind.name, - description: workspaceKind.description, - deprecated: workspaceKind.deprecated, - numOfWrokspaces: mockNumberOfWorkspaces, - }; + const { icon, name, description, deprecated, numOfWrokspaces } = { + icon: '', + name: workspaceKind.name, + description: workspaceKind.description, + deprecated: workspaceKind.deprecated, + numOfWrokspaces: mockNumberOfWorkspaces, + }; return [icon, name, description, deprecated, numOfWrokspaces]; }; @@ -138,17 +141,17 @@ export const WorkspaceKinds: React.FunctionComponent = () => { } const getSortParams = (columnIndex: number): ThProps['sort'] => ({ - sortBy: { - index: activeSortIndex || 0, - direction: activeSortDirection || 'asc', - defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' - }, - onSort: (_event, index, direction) => { - setActiveSortIndex(index); - setActiveSortDirection(direction); - }, - columnIndex, - }); + sortBy: { + index: activeSortIndex || 0, + direction: activeSortDirection || 'asc', + defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' + }, + onSort: (_event, index, direction) => { + setActiveSortIndex(index); + setActiveSortDirection(direction); + }, + columnIndex, + }); // Actions @@ -167,13 +170,12 @@ export const WorkspaceKinds: React.FunctionComponent = () => { { isSeparator: true, }, - ] as IActions; return workspaceKindsActions; }; - const workspaceDetailsContent = null // Todo: Detail need to be implemented. + const workspaceDetailsContent = null; // Todo: Detail need to be implemented. const DESCRIPTION_CHAR_LIMIT = 50; @@ -191,10 +193,14 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
- + {/* */} + */} @@ -203,7 +209,11 @@ export const WorkspaceKinds: React.FunctionComponent = () => { {Object.values(columnNames).map((columnName, index) => ( @@ -224,10 +234,14 @@ export const WorkspaceKinds: React.FunctionComponent = () => { rowIndex, isExpanded: isWorkspaceKindExpanded(workspaceKind), onToggle: () => - setWorkspaceKindsExpanded(workspaceKind, !isWorkspaceKindExpanded(workspaceKind)), + setWorkspaceKindsExpanded( + workspaceKind, + !isWorkspaceKindExpanded(workspaceKind), + ), }} /> - - - + diff --git a/workspaces/frontend/src/shared/types.ts b/workspaces/frontend/src/shared/types.ts index c2087bbe9..8adc53a8c 100644 --- a/workspaces/frontend/src/shared/types.ts +++ b/workspaces/frontend/src/shared/types.ts @@ -149,4 +149,4 @@ export type WorkspaceKindsColumnNames = { description: string; deprecated: string; numberOfWorkspaces: string; -}; \ No newline at end of file +}; From 3601fb830f3e5b55d7a2decf97a15f5dcfa0a196 Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Sun, 6 Apr 2025 11:44:40 +0300 Subject: [PATCH 3/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup #203 Signed-off-by: Liav Weiss (EXT-Nokia) --- workspaces/frontend/src/app/AppRoutes.tsx | 2 +- .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 511 ++++++++++++++---- 2 files changed, 415 insertions(+), 98 deletions(-) diff --git a/workspaces/frontend/src/app/AppRoutes.tsx b/workspaces/frontend/src/app/AppRoutes.tsx index 8401fae69..b69181411 100644 --- a/workspaces/frontend/src/app/AppRoutes.tsx +++ b/workspaces/frontend/src/app/AppRoutes.tsx @@ -44,7 +44,7 @@ export const useAdminDebugSettings = (): NavDataItem[] => { path: '/', }, { - label: 'WorkspaceKinds', + label: 'Workspace Kinds', path: '/workspacekinds', }, ]; diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index 3ae05aeb5..57e1daffc 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -8,6 +8,19 @@ import { Brand, Tooltip, Label, + SearchInput, + Toolbar, + ToolbarContent, + ToolbarItem, + Menu, + MenuContent, + MenuList, + MenuItem, + MenuToggle, + Popper, + ToolbarGroup, + ToolbarFilter, + ToolbarToggleGroup, } from '@patternfly/react-core'; import { Table, @@ -20,18 +33,84 @@ import { ActionsColumn, IActions, } from '@patternfly/react-table'; -import { CodeIcon } from '@patternfly/react-icons'; -import { useState } from 'react'; +import { CodeIcon, FilterIcon } from '@patternfly/react-icons'; import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types'; -import useWorkspaceKinds from '~/app/hooks/useWorkspaceKinds'; -import Filter, { FilteredColumn } from 'shared/components/Filter'; export enum ActionType { ViewDetails, } export const WorkspaceKinds: React.FunctionComponent = () => { - const mockNumberOfWorkspaces = 1; // Todo: create a function to calculate number of workspaces for each workspace kind. + // Todo: Remove mock and use useWorkspaceKinds API instead. + const mockWorkspaceKinds: WorkspaceKind[] = [ + { + name: 'jupyterlab', + displayName: 'JupyterLab Notebook', + description: + 'Example of a description for JupyterLab a Workspace which runs JupyterLab in a Pod.', + deprecated: true, + deprecationMessage: + 'This WorkspaceKind was removed on 20XX-XX-XX, please use another WorkspaceKind.', + hidden: false, + icon: { + url: 'https://jupyter.org/assets/favicons/apple-touch-icon-152x152.png', + }, + logo: { + url: 'https://upload.wikimedia.org/wikipedia/commons/3/38/Jupyter_logo.svg', + }, + podTemplate: { + podMetadata: { + labels: { + myWorkspaceKindLabel: 'my-value', + }, + annotations: { + myWorkspaceKindAnnotation: 'my-value', + }, + }, + volumeMounts: { + home: '/home/jovyan', + }, + options: { + imageConfig: { + default: 'jupyterlab_scipy_190', + values: [ + { + id: 'jupyterlab_scipy_180', + displayName: 'jupyter-scipy:v1.8.0', + labels: { + pythonVersion: '3.11', + }, + hidden: true, + redirect: { + to: 'jupyterlab_scipy_190', + message: { + text: 'This update will change...', + level: 'Info', + }, + }, + }, + ], + }, + podConfig: { + default: 'tiny_cpu', + values: [ + { + id: 'tiny_cpu', + displayName: 'Tiny CPU', + description: 'Pod with 0.1 CPU, 128 Mb RAM', + labels: { + cpu: '100m', + memory: '128Mi', + }, + }, + ], + }, + }, + }, + }, + ]; + + const mockNumberOfWorkspaces = 1; // Todo: Create a function to calculate number of workspaces for each workspace kind. // Table columns const columnNames: WorkspaceKindsColumnNames = { @@ -42,71 +121,13 @@ export const WorkspaceKinds: React.FunctionComponent = () => { numberOfWorkspaces: 'Number Of Workspaces', }; - const filterableColumns = { - name: 'Name', - deprecated: 'Status', - }; - - const [initialWorkspaceKinds] = useWorkspaceKinds(); - const [workspaceKinds, setWorkspaceKinds] = useState(initialWorkspaceKinds); - const [expandedWorkspaceKindsNames, setExpandedWorkspaceKindsNames] = React.useState( - [], - ); - const [selectedWorkspaceKind, setSelectedWorkspacekind] = React.useState( + const initialWorkspaceKinds = mockWorkspaceKinds; + const [selectedWorkspaceKind, setSelectedWorkspaceKind] = React.useState( null, ); const [activeActionType, setActiveActionType] = React.useState(null); - const setWorkspaceKindsExpanded = (workspaceKind: WorkspaceKind, isExpanding = true) => - setExpandedWorkspaceKindsNames((prevExpanded) => { - const newExpandedWorkspaceKindsNames = prevExpanded.filter( - (wsName) => wsName !== workspaceKind.name, - ); - return isExpanding - ? [...newExpandedWorkspaceKindsNames, workspaceKind.name] - : newExpandedWorkspaceKindsNames; - }); - - const isWorkspaceKindExpanded = (workspaceKind: WorkspaceKind) => - expandedWorkspaceKindsNames.includes(workspaceKind.name); - - // filter function to pass to the filter component - const onFilter = (filters: FilteredColumn[]) => { - // Search name with search value - let filteredWorkspaceKinds = initialWorkspaceKinds; - filters.forEach((filter) => { - let searchValueInput: RegExp; - try { - searchValueInput = new RegExp(filter.value, 'i'); - } catch { - searchValueInput = new RegExp(filter.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); - } - - filteredWorkspaceKinds = filteredWorkspaceKinds.filter((workspaceKind) => { - if (filter.value === '') { - return true; - } - switch (filter.columnName) { - case columnNames.name: - return workspaceKind.name.search(searchValueInput) >= 0; - case columnNames.deprecated: - if (filter.value.toUpperCase() === 'ACTIVE') { - return workspaceKind.deprecated === false; - } - if (filter.value.toUpperCase() === 'DEPRECATED') { - return workspaceKind.deprecated === true; - } - return true; - default: - return true; - } - }); - }); - setWorkspaceKinds(filteredWorkspaceKinds); - }; - // Column sorting - const [activeSortIndex, setActiveSortIndex] = React.useState(null); const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null); @@ -121,9 +142,9 @@ export const WorkspaceKinds: React.FunctionComponent = () => { return [icon, name, description, deprecated, numOfWrokspaces]; }; - let sortedWorkspaceKinds = workspaceKinds; + let sortedWorkspaceKinds = initialWorkspaceKinds; if (activeSortIndex !== null) { - sortedWorkspaceKinds = workspaceKinds.sort((a, b) => { + sortedWorkspaceKinds = initialWorkspaceKinds.sort((a, b) => { const aValue = getSortableRowValues(a)[activeSortIndex]; const bValue = getSortableRowValues(b)[activeSortIndex]; if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { @@ -153,10 +174,282 @@ export const WorkspaceKinds: React.FunctionComponent = () => { columnIndex, }); + // Set up filter - Attribute search. + const [searchNameValue, setSearchNameValue] = React.useState(''); + const [searchDescriptionValue, setSearchDescriptionValue] = React.useState(''); + const [statusSelection, setStatusSelection] = React.useState(''); + + const onSearchNameChange = (value: string) => { + setSearchNameValue(value); + }; + + const onSearchDescriptionChange = (value: string) => { + setSearchDescriptionValue(value); + }; + + const onFilter = (workspaceKind: WorkspaceKind) => { + let nameRegex: RegExp; + let descriptionRegex: RegExp; + + try { + nameRegex = new RegExp(searchNameValue, 'i'); + descriptionRegex = new RegExp(searchDescriptionValue, 'i'); + } catch { + // Escape any regex special characters in search inputs + nameRegex = new RegExp(searchNameValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + descriptionRegex = new RegExp( + searchDescriptionValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + 'i', + ); + } + + const matchesNameSearch = searchNameValue === '' || nameRegex.test(workspaceKind.name); + const matchesDescriptionSearch = + searchDescriptionValue === '' || descriptionRegex.test(workspaceKind.description); + + let matchesStatus = false; + if (statusSelection === 'Deprecated') { + matchesStatus = workspaceKind.deprecated === true; + } + if (statusSelection === 'Active') { + matchesStatus = workspaceKind.deprecated === false; + } + + return ( + matchesNameSearch && matchesDescriptionSearch && (statusSelection === '' || matchesStatus) + ); + }; + + const filteredWorkspaceKinds = sortedWorkspaceKinds.filter(onFilter); + + // Set up name search input + const searchNameInput = ( + onSearchNameChange(value)} + onClear={() => onSearchNameChange('')} + /> + ); + + // Set up description search input + const searchDescriptionInput = ( + onSearchDescriptionChange(value)} + onClear={() => onSearchDescriptionChange('')} + /> + ); + + // Set up status single select + const [isStatusMenuOpen, setIsStatusMenuOpen] = React.useState(false); + const statusToggleRef = React.useRef(null); + const statusMenuRef = React.useRef(null); + const statusContainerRef = React.useRef(null); + + const handleStatusMenuKeys = React.useCallback( + (event: KeyboardEvent) => { + if (isStatusMenuOpen && statusMenuRef.current?.contains(event.target as Node)) { + if (event.key === 'Escape' || event.key === 'Tab') { + setIsStatusMenuOpen(!isStatusMenuOpen); + statusToggleRef.current?.focus(); + } + } + }, + [isStatusMenuOpen], + ); + + const handleStatusClickOutside = React.useCallback( + (event: MouseEvent) => { + if (isStatusMenuOpen && !statusMenuRef.current?.contains(event.target as Node)) { + setIsStatusMenuOpen(false); + } + }, + [isStatusMenuOpen], + ); + + React.useEffect(() => { + window.addEventListener('keydown', handleStatusMenuKeys); + window.addEventListener('click', handleStatusClickOutside); + return () => { + window.removeEventListener('keydown', handleStatusMenuKeys); + window.removeEventListener('click', handleStatusClickOutside); + }; + }, [isStatusMenuOpen, statusMenuRef, handleStatusClickOutside, handleStatusMenuKeys]); + + const onStatusToggleClick = (ev: React.MouseEvent) => { + ev.stopPropagation(); // Stop handleClickOutside from handling + setTimeout(() => { + if (statusMenuRef.current) { + const firstElement = statusMenuRef.current.querySelector('li > button:not(:disabled)'); + if (firstElement) { + (firstElement as HTMLElement).focus(); + } + } + }, 0); + setIsStatusMenuOpen(!isStatusMenuOpen); + }; + + function onStatusSelect( + event: React.MouseEvent | undefined, + itemId: string | number | undefined, + ) { + if (typeof itemId === 'undefined') { + return; + } + + setStatusSelection(itemId.toString()); + setIsStatusMenuOpen(!isStatusMenuOpen); + } + + const statusToggle = ( + + Filter by status + + ); + + const statusMenu = ( + + + + Deprecated + Active + + + + ); + + const statusSelect = ( +
+ +
+ ); + + // Set up attribute selector + const [activeAttributeMenu, setActiveAttributeMenu] = React.useState< + 'Name' | 'Description' | 'Status' + >('Name'); + const [isAttributeMenuOpen, setIsAttributeMenuOpen] = React.useState(false); + const attributeToggleRef = React.useRef(null); + const attributeMenuRef = React.useRef(null); + const attributeContainerRef = React.useRef(null); + + const handleAttributeMenuKeys = React.useCallback( + (event: KeyboardEvent) => { + if (!isAttributeMenuOpen) { + return; + } + if ( + attributeMenuRef.current?.contains(event.target as Node) || + attributeToggleRef.current?.contains(event.target as Node) + ) { + if (event.key === 'Escape' || event.key === 'Tab') { + setIsAttributeMenuOpen(!isAttributeMenuOpen); + attributeToggleRef.current?.focus(); + } + } + }, + [isAttributeMenuOpen], + ); + + const handleAttributeClickOutside = React.useCallback( + (event: MouseEvent) => { + if (isAttributeMenuOpen && !attributeMenuRef.current?.contains(event.target as Node)) { + setIsAttributeMenuOpen(false); + } + }, + [isAttributeMenuOpen], + ); + + React.useEffect(() => { + window.addEventListener('keydown', handleAttributeMenuKeys); + window.addEventListener('click', handleAttributeClickOutside); + return () => { + window.removeEventListener('keydown', handleAttributeMenuKeys); + window.removeEventListener('click', handleAttributeClickOutside); + }; + }, [isAttributeMenuOpen, attributeMenuRef, handleAttributeMenuKeys, handleAttributeClickOutside]); + + const onAttributeToggleClick = (ev: React.MouseEvent) => { + ev.stopPropagation(); // Stop handleClickOutside from handling + setTimeout(() => { + if (attributeMenuRef.current) { + const firstElement = attributeMenuRef.current.querySelector('li > button:not(:disabled)'); + if (firstElement) { + (firstElement as HTMLElement).focus(); + } + } + }, 0); + setIsAttributeMenuOpen(!isAttributeMenuOpen); + }; + + const attributeToggle = ( + } + > + {activeAttributeMenu} + + ); + const attributeMenu = ( + { + setActiveAttributeMenu(itemId?.toString() as 'Name' | 'Description' | 'Status'); + setIsAttributeMenuOpen(!isAttributeMenuOpen); + }} + > + + + Name + Description + Status + + + + ); + + const attributeDropdown = ( +
+ +
+ ); + // Actions const viewDetailsClick = React.useCallback((workspaceKind: WorkspaceKind) => { - setSelectedWorkspacekind(workspaceKind); + setSelectedWorkspaceKind(workspaceKind); setActiveActionType(ActionType.ViewDetails); }, []); @@ -167,9 +460,6 @@ export const WorkspaceKinds: React.FunctionComponent = () => { title: 'View Details', onClick: () => viewDetailsClick(workspaceKind), }, - { - isSeparator: true, - }, ] as IActions; return workspaceKindsActions; @@ -193,11 +483,53 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
- + { + setSearchNameValue(''); + setStatusSelection(''); + setSearchDescriptionValue(''); + }} + > + + } breakpoint="xl"> + + {attributeDropdown} + setSearchNameValue('')} + deleteLabelGroup={() => setSearchNameValue('')} + categoryName="Name" + showToolbarItem={activeAttributeMenu === 'Name'} + > + {searchNameInput} + + setSearchDescriptionValue('')} + deleteLabelGroup={() => setSearchDescriptionValue('')} + categoryName="Description" + showToolbarItem={activeAttributeMenu === 'Description'} + > + {searchDescriptionInput} + + setStatusSelection('')} + deleteLabelGroup={() => setStatusSelection('')} + categoryName="Status" + showToolbarItem={activeAttributeMenu === 'Status'} + > + {statusSelect} + + + + + {/* */} @@ -221,25 +553,10 @@ export const WorkspaceKinds: React.FunctionComponent = () => {
- {sortedWorkspaceKinds.map((workspaceKind, rowIndex) => ( - + {filteredWorkspaceKinds.map((workspaceKind, rowIndex) => ( + - From c273d86af78fc2f57a76be7e204d7ac3309493bc Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Tue, 15 Apr 2025 12:20:57 +0300 Subject: [PATCH 4/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup #203 Signed-off-by: Liav Weiss (EXT-Nokia) --- .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 260 +++++++++++------- 1 file changed, 155 insertions(+), 105 deletions(-) diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index 57e1daffc..f0dec78c6 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -21,6 +21,12 @@ import { ToolbarGroup, ToolbarFilter, ToolbarToggleGroup, + EmptyStateActions, + EmptyState, + EmptyStateFooter, + EmptyStateBody, + Button, + Bullseye, } from '@patternfly/react-core'; import { Table, @@ -33,7 +39,7 @@ import { ActionsColumn, IActions, } from '@patternfly/react-table'; -import { CodeIcon, FilterIcon } from '@patternfly/react-icons'; +import { CodeIcon, FilterIcon, SearchIcon } from '@patternfly/react-icons'; import { WorkspaceKind, WorkspaceKindsColumnNames } from '~/shared/types'; export enum ActionType { @@ -118,7 +124,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => { name: 'Name', description: 'Description', deprecated: 'Status', - numberOfWorkspaces: 'Number Of Workspaces', + numberOfWorkspaces: 'Number of workspaces', }; const initialWorkspaceKinds = mockWorkspaceKinds; @@ -131,35 +137,44 @@ export const WorkspaceKinds: React.FunctionComponent = () => { const [activeSortIndex, setActiveSortIndex] = React.useState(null); const [activeSortDirection, setActiveSortDirection] = React.useState<'asc' | 'desc' | null>(null); - const getSortableRowValues = (workspaceKind: WorkspaceKind): (string | boolean | number)[] => { - const { icon, name, description, deprecated, numOfWrokspaces } = { - icon: '', - name: workspaceKind.name, - description: workspaceKind.description, - deprecated: workspaceKind.deprecated, - numOfWrokspaces: mockNumberOfWorkspaces, - }; - return [icon, name, description, deprecated, numOfWrokspaces]; - }; + const getSortableRowValues = React.useCallback( + (workspaceKind: WorkspaceKind): (string | boolean | number)[] => { + const { + icon, + name, + description, + deprecated, + numOfWrokspaces: numberOfWrokspaces, + } = { + icon: '', + name: workspaceKind.name, + description: workspaceKind.description, + deprecated: workspaceKind.deprecated, + numOfWrokspaces: mockNumberOfWorkspaces, + }; + return [icon, name, description, deprecated, numberOfWrokspaces]; + }, + [], + ); - let sortedWorkspaceKinds = initialWorkspaceKinds; - if (activeSortIndex !== null) { - sortedWorkspaceKinds = initialWorkspaceKinds.sort((a, b) => { + const sortedWorkspaceKinds = React.useMemo(() => { + if (activeSortIndex === null) { + return initialWorkspaceKinds; + } + + return [...initialWorkspaceKinds].sort((a, b) => { const aValue = getSortableRowValues(a)[activeSortIndex]; const bValue = getSortableRowValues(b)[activeSortIndex]; if (typeof aValue === 'boolean' && typeof bValue === 'boolean') { - // Convert boolean to number (true -> 1, false -> 0) for sorting return activeSortDirection === 'asc' ? Number(aValue) - Number(bValue) : Number(bValue) - Number(aValue); } - // String sort - if (activeSortDirection === 'asc') { - return (aValue as string).localeCompare(bValue as string); - } - return (bValue as string).localeCompare(aValue as string); + return activeSortDirection === 'asc' + ? (aValue as string).localeCompare(bValue as string) + : (bValue as string).localeCompare(aValue as string); }); - } + }, [initialWorkspaceKinds, activeSortIndex, activeSortDirection, getSortableRowValues]); const getSortParams = (columnIndex: number): ThProps['sort'] => ({ sortBy: { @@ -179,48 +194,53 @@ export const WorkspaceKinds: React.FunctionComponent = () => { const [searchDescriptionValue, setSearchDescriptionValue] = React.useState(''); const [statusSelection, setStatusSelection] = React.useState(''); - const onSearchNameChange = (value: string) => { + const onSearchNameChange = React.useCallback((value: string) => { setSearchNameValue(value); - }; + }, []); - const onSearchDescriptionChange = (value: string) => { + const onSearchDescriptionChange = React.useCallback((value: string) => { setSearchDescriptionValue(value); - }; + }, []); - const onFilter = (workspaceKind: WorkspaceKind) => { - let nameRegex: RegExp; - let descriptionRegex: RegExp; - - try { - nameRegex = new RegExp(searchNameValue, 'i'); - descriptionRegex = new RegExp(searchDescriptionValue, 'i'); - } catch { - // Escape any regex special characters in search inputs - nameRegex = new RegExp(searchNameValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); - descriptionRegex = new RegExp( - searchDescriptionValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), - 'i', - ); - } + const onFilter = React.useCallback( + (workspaceKind: WorkspaceKind) => { + let nameRegex: RegExp; + let descriptionRegex: RegExp; + + try { + nameRegex = new RegExp(searchNameValue, 'i'); + descriptionRegex = new RegExp(searchDescriptionValue, 'i'); + } catch { + nameRegex = new RegExp(searchNameValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'); + descriptionRegex = new RegExp( + searchDescriptionValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + 'i', + ); + } - const matchesNameSearch = searchNameValue === '' || nameRegex.test(workspaceKind.name); - const matchesDescriptionSearch = - searchDescriptionValue === '' || descriptionRegex.test(workspaceKind.description); + const matchesNameSearch = searchNameValue === '' || nameRegex.test(workspaceKind.name); + const matchesDescriptionSearch = + searchDescriptionValue === '' || descriptionRegex.test(workspaceKind.description); - let matchesStatus = false; - if (statusSelection === 'Deprecated') { - matchesStatus = workspaceKind.deprecated === true; - } - if (statusSelection === 'Active') { - matchesStatus = workspaceKind.deprecated === false; - } + let matchesStatus = false; + if (statusSelection === 'Deprecated') { + matchesStatus = workspaceKind.deprecated === true; + } + if (statusSelection === 'Active') { + matchesStatus = workspaceKind.deprecated === false; + } - return ( - matchesNameSearch && matchesDescriptionSearch && (statusSelection === '' || matchesStatus) - ); - }; + return ( + matchesNameSearch && matchesDescriptionSearch && (statusSelection === '' || matchesStatus) + ); + }, + [searchNameValue, searchDescriptionValue, statusSelection], + ); - const filteredWorkspaceKinds = sortedWorkspaceKinds.filter(onFilter); + const filteredWorkspaceKinds = React.useMemo( + () => sortedWorkspaceKinds.filter(onFilter), + [sortedWorkspaceKinds, onFilter], + ); // Set up name search input const searchNameInput = ( @@ -446,6 +466,28 @@ export const WorkspaceKinds: React.FunctionComponent = () => { ); + const emptyState = ( + + + No results match the filter criteria. Clear all filters and try again. + + + + + + + + ); + // Actions const viewDetailsClick = React.useCallback((workspaceKind: WorkspaceKind) => { @@ -553,56 +595,64 @@ export const WorkspaceKinds: React.FunctionComponent = () => { - {filteredWorkspaceKinds.map((workspaceKind, rowIndex) => ( - - - - - - + + + + - - - - - - ))} + + + + + + + + ))} + {filteredWorkspaceKinds.length === 0 && ( + + + + )}
{columnName} {workspaceKind.icon.url ? ( + + {workspaceKind.icon.url ? ( { )} - {workspaceKind.name} - + {workspaceKind.name} - {workspaceKind.description.length > DESCRIPTION_CHAR_LIMIT ? - `${workspaceKind.description.slice(0, DESCRIPTION_CHAR_LIMIT)}...` : workspaceKind.description} + {workspaceKind.description.length > DESCRIPTION_CHAR_LIMIT + ? `${workspaceKind.description.slice(0, DESCRIPTION_CHAR_LIMIT)}...` + : workspaceKind.description} - {workspaceKind.deprecated ? ( - - ) : ( - - - - )} + {workspaceKind.deprecated ? ( + + ) : ( + + + + )} {mockNumberOfWorkspaces}
- setWorkspaceKindsExpanded( - workspaceKind, - !isWorkspaceKindExpanded(workspaceKind), - ), - }} - /> + {workspaceKind.icon.url ? ( { {workspaceKind.deprecated ? ( - - ) : ( - + + ) : ( + )} {mockNumberOfWorkspaces}
- - {workspaceKind.icon.url ? ( - - ) : ( - - )} - {workspaceKind.name} - - - {workspaceKind.description.length > DESCRIPTION_CHAR_LIMIT - ? `${workspaceKind.description.slice(0, DESCRIPTION_CHAR_LIMIT)}...` - : workspaceKind.description} - - - - {workspaceKind.deprecated ? ( - - + {filteredWorkspaceKinds.length > 0 && + filteredWorkspaceKinds.map((workspaceKind, rowIndex) => ( +
+ + {workspaceKind.icon.url ? ( + + ) : ( + + )} + {workspaceKind.name} + + + {workspaceKind.description.length > DESCRIPTION_CHAR_LIMIT + ? `${workspaceKind.description.slice(0, DESCRIPTION_CHAR_LIMIT)}...` + : workspaceKind.description} + - ) : ( - - )} - {mockNumberOfWorkspaces} - ({ - ...action, - 'data-testid': `action-${action.id || ''}`, - }))} - /> -
+ {workspaceKind.deprecated ? ( + + + + ) : ( + + )} + {mockNumberOfWorkspaces} + ({ + ...action, + 'data-testid': `action-${action.id || ''}`, + }))} + /> +
+ {emptyState} +
From ef198b64108a185ac69c7b6efde98ea5481b1a06 Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Wed, 16 Apr 2025 10:42:41 +0300 Subject: [PATCH 5/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup #203 Signed-off-by: Liav Weiss (EXT-Nokia) --- .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 113 ++++++++---------- 1 file changed, 52 insertions(+), 61 deletions(-) diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index f0dec78c6..d9b8c0cb9 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -176,18 +176,18 @@ export const WorkspaceKinds: React.FunctionComponent = () => { }); }, [initialWorkspaceKinds, activeSortIndex, activeSortDirection, getSortableRowValues]); - const getSortParams = (columnIndex: number): ThProps['sort'] => ({ + const getSortParams = React.useCallback((columnIndex: number): ThProps['sort'] => ({ sortBy: { index: activeSortIndex || 0, direction: activeSortDirection || 'asc', - defaultDirection: 'asc', // starting sort direction when first sorting a column. Defaults to 'asc' + defaultDirection: 'asc', }, onSort: (_event, index, direction) => { setActiveSortIndex(index); setActiveSortDirection(direction); }, columnIndex, - }); + }), [activeSortIndex, activeSortDirection]); // Set up filter - Attribute search. const [searchNameValue, setSearchNameValue] = React.useState(''); @@ -243,24 +243,24 @@ export const WorkspaceKinds: React.FunctionComponent = () => { ); // Set up name search input - const searchNameInput = ( + const searchNameInput = React.useMemo(() => ( onSearchNameChange(value)} onClear={() => onSearchNameChange('')} /> - ); + ), [searchNameValue, onSearchNameChange]); // Set up description search input - const searchDescriptionInput = ( + const searchDescriptionInput = React.useMemo(() => ( onSearchDescriptionChange(value)} onClear={() => onSearchDescriptionChange('')} /> - ); + ), [searchDescriptionValue, onSearchDescriptionChange]); // Set up status single select const [isStatusMenuOpen, setIsStatusMenuOpen] = React.useState(false); @@ -298,47 +298,41 @@ export const WorkspaceKinds: React.FunctionComponent = () => { }; }, [isStatusMenuOpen, statusMenuRef, handleStatusClickOutside, handleStatusMenuKeys]); - const onStatusToggleClick = (ev: React.MouseEvent) => { - ev.stopPropagation(); // Stop handleClickOutside from handling + const onStatusToggleClick = React.useCallback((ev: React.MouseEvent) => { + ev.stopPropagation(); setTimeout(() => { - if (statusMenuRef.current) { - const firstElement = statusMenuRef.current.querySelector('li > button:not(:disabled)'); - if (firstElement) { - (firstElement as HTMLElement).focus(); - } + const firstElement = statusMenuRef.current?.querySelector('li > button:not(:disabled)'); + if (firstElement) { + (firstElement as HTMLElement).focus(); } }, 0); - setIsStatusMenuOpen(!isStatusMenuOpen); - }; + setIsStatusMenuOpen(prev => !prev); + }, []); - function onStatusSelect( + const onStatusSelect = React.useCallback(( event: React.MouseEvent | undefined, itemId: string | number | undefined, - ) { + ) => { if (typeof itemId === 'undefined') { return; } - + setStatusSelection(itemId.toString()); - setIsStatusMenuOpen(!isStatusMenuOpen); - } + setIsStatusMenuOpen(prev => !prev); + }, []); - const statusToggle = ( + const statusToggle = React.useMemo(() => ( Filter by status - ); + ), [isStatusMenuOpen, onStatusToggleClick]); - const statusMenu = ( + const statusMenu = React.useMemo(() => ( { - ); + ), [statusSelection, onStatusSelect]); - const statusSelect = ( + const statusSelect = React.useMemo(() => (
{ isVisible={isStatusMenuOpen} />
- ); + ), [statusToggle, statusMenu, isStatusMenuOpen]); // Set up attribute selector const [activeAttributeMenu, setActiveAttributeMenu] = React.useState< @@ -412,20 +406,20 @@ export const WorkspaceKinds: React.FunctionComponent = () => { }; }, [isAttributeMenuOpen, attributeMenuRef, handleAttributeMenuKeys, handleAttributeClickOutside]); - const onAttributeToggleClick = (ev: React.MouseEvent) => { - ev.stopPropagation(); // Stop handleClickOutside from handling + const onAttributeToggleClick = React.useCallback((ev: React.MouseEvent) => { + ev.stopPropagation(); + setTimeout(() => { - if (attributeMenuRef.current) { - const firstElement = attributeMenuRef.current.querySelector('li > button:not(:disabled)'); - if (firstElement) { - (firstElement as HTMLElement).focus(); - } + const firstElement = attributeMenuRef.current?.querySelector('li > button:not(:disabled)'); + if (firstElement) { + (firstElement as HTMLElement).focus(); } }, 0); - setIsAttributeMenuOpen(!isAttributeMenuOpen); - }; + + setIsAttributeMenuOpen(prev => !prev); + }, []); - const attributeToggle = ( + const attributeToggle = React.useMemo(() => ( { > {activeAttributeMenu} - ); - const attributeMenu = ( + ), [isAttributeMenuOpen, onAttributeToggleClick, activeAttributeMenu]); + + const attributeMenu = React.useMemo(() => ( { setActiveAttributeMenu(itemId?.toString() as 'Name' | 'Description' | 'Status'); - setIsAttributeMenuOpen(!isAttributeMenuOpen); + setIsAttributeMenuOpen(prev => !prev); }} > @@ -451,9 +446,9 @@ export const WorkspaceKinds: React.FunctionComponent = () => { - ); + ), []); - const attributeDropdown = ( + const attributeDropdown = React.useMemo(() => (
{ isVisible={isAttributeMenuOpen} />
- ); + ), [attributeToggle, attributeMenu, isAttributeMenuOpen]); - const emptyState = ( + const emptyState = React.useMemo(() => ( No results match the filter criteria. Clear all filters and try again. @@ -486,7 +481,7 @@ export const WorkspaceKinds: React.FunctionComponent = () => { - ); + ), []); // Actions @@ -495,17 +490,13 @@ export const WorkspaceKinds: React.FunctionComponent = () => { setActiveActionType(ActionType.ViewDetails); }, []); - const workspaceKindsDefaultActions = (workspaceKind: WorkspaceKind): IActions => { - const workspaceKindsActions = [ - { - id: 'view-details', - title: 'View Details', - onClick: () => viewDetailsClick(workspaceKind), - }, - ] as IActions; - - return workspaceKindsActions; - }; + const workspaceKindsDefaultActions = React.useCallback((workspaceKind: WorkspaceKind): IActions => ([ + { + id: 'view-details', + title: 'View Details', + onClick: () => viewDetailsClick(workspaceKind), + }, + ]), [viewDetailsClick]); const workspaceDetailsContent = null; // Todo: Detail need to be implemented. From 641a0a57aca694cfe4875d8952c26e21411ffbd0 Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Wed, 16 Apr 2025 15:51:27 +0300 Subject: [PATCH 6/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup #203 Signed-off-by: Liav Weiss (EXT-Nokia) --- .../pages/WorkspaceKinds/WorkspaceKinds.tsx | 343 ++++++++++-------- 1 file changed, 188 insertions(+), 155 deletions(-) diff --git a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx index d9b8c0cb9..9becc15e6 100644 --- a/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx +++ b/workspaces/frontend/src/app/pages/WorkspaceKinds/WorkspaceKinds.tsx @@ -144,15 +144,15 @@ export const WorkspaceKinds: React.FunctionComponent = () => { name, description, deprecated, - numOfWrokspaces: numberOfWrokspaces, + numOfWorkspaces: numberOfWorkspaces, } = { icon: '', name: workspaceKind.name, description: workspaceKind.description, deprecated: workspaceKind.deprecated, - numOfWrokspaces: mockNumberOfWorkspaces, + numOfWorkspaces: mockNumberOfWorkspaces, }; - return [icon, name, description, deprecated, numberOfWrokspaces]; + return [icon, name, description, deprecated, numberOfWorkspaces]; }, [], ); @@ -176,18 +176,21 @@ export const WorkspaceKinds: React.FunctionComponent = () => { }); }, [initialWorkspaceKinds, activeSortIndex, activeSortDirection, getSortableRowValues]); - const getSortParams = React.useCallback((columnIndex: number): ThProps['sort'] => ({ - sortBy: { - index: activeSortIndex || 0, - direction: activeSortDirection || 'asc', - defaultDirection: 'asc', - }, - onSort: (_event, index, direction) => { - setActiveSortIndex(index); - setActiveSortDirection(direction); - }, - columnIndex, - }), [activeSortIndex, activeSortDirection]); + const getSortParams = React.useCallback( + (columnIndex: number): ThProps['sort'] => ({ + sortBy: { + index: activeSortIndex || 0, + direction: activeSortDirection || 'asc', + defaultDirection: 'asc', + }, + onSort: (_event, index, direction) => { + setActiveSortIndex(index); + setActiveSortDirection(direction); + }, + columnIndex, + }), + [activeSortIndex, activeSortDirection], + ); // Set up filter - Attribute search. const [searchNameValue, setSearchNameValue] = React.useState(''); @@ -243,24 +246,30 @@ export const WorkspaceKinds: React.FunctionComponent = () => { ); // Set up name search input - const searchNameInput = React.useMemo(() => ( - onSearchNameChange(value)} - onClear={() => onSearchNameChange('')} - /> - ), [searchNameValue, onSearchNameChange]); + const searchNameInput = React.useMemo( + () => ( + onSearchNameChange(value)} + onClear={() => onSearchNameChange('')} + /> + ), + [searchNameValue, onSearchNameChange], + ); // Set up description search input - const searchDescriptionInput = React.useMemo(() => ( - onSearchDescriptionChange(value)} - onClear={() => onSearchDescriptionChange('')} - /> - ), [searchDescriptionValue, onSearchDescriptionChange]); + const searchDescriptionInput = React.useMemo( + () => ( + onSearchDescriptionChange(value)} + onClear={() => onSearchDescriptionChange('')} + /> + ), + [searchDescriptionValue, onSearchDescriptionChange], + ); // Set up status single select const [isStatusMenuOpen, setIsStatusMenuOpen] = React.useState(false); @@ -306,60 +315,69 @@ export const WorkspaceKinds: React.FunctionComponent = () => { (firstElement as HTMLElement).focus(); } }, 0); - setIsStatusMenuOpen(prev => !prev); + setIsStatusMenuOpen((prev) => !prev); }, []); - const onStatusSelect = React.useCallback(( - event: React.MouseEvent | undefined, - itemId: string | number | undefined, - ) => { - if (typeof itemId === 'undefined') { - return; - } - - setStatusSelection(itemId.toString()); - setIsStatusMenuOpen(prev => !prev); - }, []); + const onStatusSelect = React.useCallback( + (event: React.MouseEvent | undefined, itemId: string | number | undefined) => { + if (typeof itemId === 'undefined') { + return; + } - const statusToggle = React.useMemo(() => ( - - Filter by status - - ), [isStatusMenuOpen, onStatusToggleClick]); - - const statusMenu = React.useMemo(() => ( - - - - Deprecated - Active - - - - ), [statusSelection, onStatusSelect]); - - const statusSelect = React.useMemo(() => ( -
- -
- ), [statusToggle, statusMenu, isStatusMenuOpen]); + setStatusSelection(itemId.toString()); + setIsStatusMenuOpen((prev) => !prev); + }, + [], + ); + + const statusToggle = React.useMemo( + () => ( + + Filter by status + + ), + [isStatusMenuOpen, onStatusToggleClick], + ); + + const statusMenu = React.useMemo( + () => ( + + + + Deprecated + Active + + + + ), + [statusSelection, onStatusSelect], + ); + + const statusSelect = React.useMemo( + () => ( +
+ +
+ ), + [statusToggle, statusMenu, isStatusMenuOpen], + ); // Set up attribute selector const [activeAttributeMenu, setActiveAttributeMenu] = React.useState< @@ -408,80 +426,92 @@ export const WorkspaceKinds: React.FunctionComponent = () => { const onAttributeToggleClick = React.useCallback((ev: React.MouseEvent) => { ev.stopPropagation(); - + setTimeout(() => { const firstElement = attributeMenuRef.current?.querySelector('li > button:not(:disabled)'); if (firstElement) { (firstElement as HTMLElement).focus(); } }, 0); - - setIsAttributeMenuOpen(prev => !prev); + + setIsAttributeMenuOpen((prev) => !prev); }, []); - const attributeToggle = React.useMemo(() => ( - } - > - {activeAttributeMenu} - - ), [isAttributeMenuOpen, onAttributeToggleClick, activeAttributeMenu]); - - const attributeMenu = React.useMemo(() => ( - { - setActiveAttributeMenu(itemId?.toString() as 'Name' | 'Description' | 'Status'); - setIsAttributeMenuOpen(prev => !prev); - }} - > - - - Name - Description - Status - - - - ), []); - - const attributeDropdown = React.useMemo(() => ( -
- -
- ), [attributeToggle, attributeMenu, isAttributeMenuOpen]); - - const emptyState = React.useMemo(() => ( - - - No results match the filter criteria. Clear all filters and try again. - - - - - - - - ), []); + const attributeToggle = React.useMemo( + () => ( + } + > + {activeAttributeMenu} + + ), + [isAttributeMenuOpen, onAttributeToggleClick, activeAttributeMenu], + ); + + const attributeMenu = React.useMemo( + () => ( + { + setActiveAttributeMenu(itemId?.toString() as 'Name' | 'Description' | 'Status'); + setIsAttributeMenuOpen((prev) => !prev); + }} + > + + + Name + Description + Status + + + + ), + [], + ); + + const attributeDropdown = React.useMemo( + () => ( +
+ +
+ ), + [attributeToggle, attributeMenu, isAttributeMenuOpen], + ); + + const emptyState = React.useMemo( + () => ( + + + No results match the filter criteria. Clear all filters and try again. + + + + + + + + ), + [], + ); // Actions @@ -490,13 +520,16 @@ export const WorkspaceKinds: React.FunctionComponent = () => { setActiveActionType(ActionType.ViewDetails); }, []); - const workspaceKindsDefaultActions = React.useCallback((workspaceKind: WorkspaceKind): IActions => ([ - { - id: 'view-details', - title: 'View Details', - onClick: () => viewDetailsClick(workspaceKind), - }, - ]), [viewDetailsClick]); + const workspaceKindsDefaultActions = React.useCallback( + (workspaceKind: WorkspaceKind): IActions => [ + { + id: 'view-details', + title: 'View Details', + onClick: () => viewDetailsClick(workspaceKind), + }, + ], + [viewDetailsClick], + ); const workspaceDetailsContent = null; // Todo: Detail need to be implemented. From fbdbf2b0fa138a7e061a9312e239490820b33845 Mon Sep 17 00:00:00 2001 From: "Liav Weiss (EXT-Nokia)" Date: Tue, 22 Apr 2025 12:02:48 +0300 Subject: [PATCH 7/7] feat(ws): Notebooks 2.0 // Frontend // Workspace Kind Viewer // Live mockup kubeflow#203 Signed-off-by: Liav Weiss (EXT-Nokia) --- workspaces/frontend/src/app/AppRoutes.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/workspaces/frontend/src/app/AppRoutes.tsx b/workspaces/frontend/src/app/AppRoutes.tsx index b69181411..8c13559db 100644 --- a/workspaces/frontend/src/app/AppRoutes.tsx +++ b/workspaces/frontend/src/app/AppRoutes.tsx @@ -39,10 +39,6 @@ export const useAdminDebugSettings = (): NavDataItem[] => { label: 'Debug', children: [{ label: 'Notebooks', path: '/notebookDebugSettings' }], }, - { - label: 'Workspaces', - path: '/', - }, { label: 'Workspace Kinds', path: '/workspacekinds',