fix(ws): Updates to Table Columns, Expandable Rows, and Theming (#432)
Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> add icon to workspaceKindsColumns interface fix(ws): Update table with expandable variant and fix styles fix secondary border in menu toggle fix menu toggle expanded text color and update icon to use status prop remove unused files add cluster storage description list group Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> Add title and packages revert form label styling, revert homeVol column fix linting fix lint Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> Add PR code suggestions, remove unused interfaces Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> remove unused import Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> fix filterWorkspacesTest Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> fix(ws): apply feedback to fix Cypress tests Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> Update tests, add width to defineDataFields, remove duplicate WorkspaceTableColumnKeys type Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> fix wrapping behavior Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> Replace Th values with mapped instance Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> revert column order Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> remove hardcoded package label instances Signed-off-by: Jenny <32821331+jenny-s51@users.noreply.github.com> delete cursor rule
This commit is contained in:
parent
da615f5f7e
commit
9607fabd93
|
@ -11,7 +11,12 @@ describe('WorkspaceDetailsActivity Component', () => {
|
|||
|
||||
// This tests depends on the mocked workspaces data at home page, needs revisit once workspace data fetched from BE
|
||||
it('open workspace details, open activity tab, check all fields match', () => {
|
||||
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
|
||||
cy.findAllByTestId('table-body')
|
||||
.first()
|
||||
.findByTestId('action-column')
|
||||
.find('button')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
// Extract first workspace from mock data
|
||||
cy.wait('@getWorkspaces').then((interception) => {
|
||||
if (!interception.response || !interception.response.body) {
|
||||
|
|
|
@ -119,7 +119,12 @@ describe('Workspaces Component', () => {
|
|||
});
|
||||
|
||||
function openDeleteModal() {
|
||||
cy.findAllByTestId('table-body').first().findByTestId('action-column').click();
|
||||
cy.findAllByTestId('table-body')
|
||||
.first()
|
||||
.findByTestId('action-column')
|
||||
.find('button')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
cy.findByTestId('action-delete').click();
|
||||
cy.findByTestId('delete-modal-input').should('have.value', '');
|
||||
}
|
||||
|
|
|
@ -31,20 +31,36 @@ describe('Application', () => {
|
|||
|
||||
it('filter rows with multiple filters', () => {
|
||||
home.visit();
|
||||
// First filter by name
|
||||
useFilter('name', 'Name', 'My');
|
||||
useFilter('podConfig', 'Pod Config', 'Tiny');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
|
||||
|
||||
// Add second filter by image
|
||||
useFilter('image', 'Image', 'jupyter');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Name');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Image');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
|
||||
});
|
||||
|
||||
it('filter rows with multiple filters and remove one', () => {
|
||||
home.visit();
|
||||
// Add name filter
|
||||
useFilter('name', 'Name', 'My');
|
||||
useFilter('podConfig', 'Pod Config', 'Tiny');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
|
||||
cy.get("[class$='pf-v6-c-label-group__close']").eq(1).click();
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
|
||||
|
||||
// Add image filter
|
||||
useFilter('image', 'Image', 'jupyter');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Name');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Image');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
|
||||
// Remove one filter (the first one)
|
||||
cy.get("[class$='pf-v6-c-label-group__close']").first().click();
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Name');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Image');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
|
||||
cy.get("[id$='workspaces-table-row-2']").contains('My Second Jupyter Notebook');
|
||||
|
@ -52,13 +68,20 @@ describe('Application', () => {
|
|||
|
||||
it('filter rows with multiple filters and remove all', () => {
|
||||
home.visit();
|
||||
// Add name filter
|
||||
useFilter('name', 'Name', 'My');
|
||||
useFilter('podConfig', 'Pod Config', 'Tiny');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 1);
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
cy.get("[id$='workspaces-table-row-1']").contains('My First Jupyter Notebook');
|
||||
|
||||
// Add image filter
|
||||
useFilter('image', 'Image', 'jupyter');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Name');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").contains('Image');
|
||||
|
||||
// Clear all filters
|
||||
cy.get('*').contains('Clear all filters').click();
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Pod Config');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Name');
|
||||
cy.get("[class$='pf-v6-c-toolbar__group']").should('not.contain', 'Image');
|
||||
cy.get("[id$='workspaces-table-content']").find('tr').should('have.length', 2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
Tooltip,
|
||||
Bullseye,
|
||||
Button,
|
||||
Icon,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Table,
|
||||
|
@ -36,7 +37,6 @@ import {
|
|||
FilterableDataFieldKey,
|
||||
SortableDataFieldKey,
|
||||
} from '~/app/filterableDataHelper';
|
||||
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
|
||||
import { useTypedNavigate } from '~/app/routerHelper';
|
||||
import {
|
||||
buildKindLogoDictionary,
|
||||
|
@ -47,11 +47,12 @@ import { WorkspaceConnectAction } from '~/app/pages/Workspaces/WorkspaceConnectA
|
|||
import CustomEmptyState from '~/shared/components/CustomEmptyState';
|
||||
import Filter, { FilteredColumn, FilterRef } from '~/shared/components/Filter';
|
||||
import WithValidImage from '~/shared/components/WithValidImage';
|
||||
import ImageFallback from '~/shared/components/ImageFallback';
|
||||
import {
|
||||
formatResourceFromWorkspace,
|
||||
formatWorkspaceIdleState,
|
||||
} from '~/shared/utilities/WorkspaceUtils';
|
||||
import ImageFallback from '~/shared/components/ImageFallback';
|
||||
import { ExpandedWorkspaceRow } from '~/app/pages/Workspaces/ExpandedWorkspaceRow';
|
||||
|
||||
const {
|
||||
fields: wsTableColumns,
|
||||
|
@ -59,21 +60,16 @@ const {
|
|||
sortableKeyArray: sortableWsTableColumnKeyArray,
|
||||
filterableKeyArray: filterableWsTableColumnKeyArray,
|
||||
} = defineDataFields({
|
||||
redirectStatus: { label: 'Redirect Status', isFilterable: false, isSortable: false },
|
||||
name: { label: 'Name', isFilterable: true, isSortable: true },
|
||||
kind: { label: 'Kind', isFilterable: true, isSortable: true },
|
||||
namespace: { label: 'Namespace', isFilterable: true, isSortable: true },
|
||||
image: { label: 'Image', isFilterable: true, isSortable: true },
|
||||
podConfig: { label: 'Pod Config', isFilterable: true, isSortable: true },
|
||||
state: { label: 'State', isFilterable: true, isSortable: true },
|
||||
homeVol: { label: 'Home Vol', isFilterable: true, isSortable: true },
|
||||
cpu: { label: 'CPU', isFilterable: false, isSortable: true },
|
||||
ram: { label: 'Memory', isFilterable: false, isSortable: true },
|
||||
gpu: { label: 'GPU', isFilterable: true, isSortable: true },
|
||||
idleGpu: { label: 'Idle GPU', isFilterable: true, isSortable: true },
|
||||
lastActivity: { label: 'Last Activity', isFilterable: false, isSortable: true },
|
||||
connect: { label: '', isFilterable: false, isSortable: false },
|
||||
actions: { label: '', isFilterable: false, isSortable: false },
|
||||
name: { label: 'Name', isFilterable: true, isSortable: true, width: 35 },
|
||||
image: { label: 'Image', isFilterable: true, isSortable: true, width: 25 },
|
||||
kind: { label: 'Kind', isFilterable: true, isSortable: true, width: 15 },
|
||||
namespace: { label: 'Namespace', isFilterable: true, isSortable: true, width: 15 },
|
||||
state: { label: 'State', isFilterable: true, isSortable: true, width: 15 },
|
||||
gpu: { label: 'GPU', isFilterable: true, isSortable: true, width: 15 },
|
||||
idleGpu: { label: 'Idle GPU', isFilterable: true, isSortable: true, width: 15 },
|
||||
lastActivity: { label: 'Last activity', isFilterable: false, isSortable: true, width: 15 },
|
||||
connect: { label: '', isFilterable: false, isSortable: false, width: 25 },
|
||||
actions: { label: '', isFilterable: false, isSortable: false, width: 10 },
|
||||
});
|
||||
|
||||
export type WorkspaceTableColumnKeys = DataFieldKey<typeof wsTableColumns>;
|
||||
|
@ -202,16 +198,12 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
return ws.namespace.match(searchValueInput);
|
||||
case 'image':
|
||||
return ws.podTemplate.options.imageConfig.current.displayName.match(searchValueInput);
|
||||
case 'podConfig':
|
||||
return ws.podTemplate.options.podConfig.current.displayName.match(searchValueInput);
|
||||
case 'state':
|
||||
return ws.state.match(searchValueInput);
|
||||
case 'gpu':
|
||||
return formatResourceFromWorkspace(ws, 'gpu').match(searchValueInput);
|
||||
case 'idleGpu':
|
||||
return formatWorkspaceIdleState(ws).match(searchValueInput);
|
||||
case 'homeVol':
|
||||
return ws.podTemplate.volumes.home?.mountPath.match(searchValueInput);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
|
@ -228,11 +220,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
kind: workspace.workspaceKind.name,
|
||||
namespace: workspace.namespace,
|
||||
image: workspace.podTemplate.options.imageConfig.current.displayName,
|
||||
podConfig: workspace.podTemplate.options.podConfig.current.displayName,
|
||||
state: workspace.state,
|
||||
homeVol: workspace.podTemplate.volumes.home?.pvcName ?? '',
|
||||
cpu: formatResourceFromWorkspace(workspace, 'cpu'),
|
||||
ram: formatResourceFromWorkspace(workspace, 'memory'),
|
||||
gpu: formatResourceFromWorkspace(workspace, 'gpu'),
|
||||
idleGpu: formatWorkspaceIdleState(workspace),
|
||||
lastActivity: workspace.activity.lastActivity,
|
||||
|
@ -280,6 +268,37 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
};
|
||||
};
|
||||
|
||||
// Column-specific modifiers and special properties
|
||||
const getColumnModifier = (
|
||||
columnKey: WorkspaceTableColumnKeys,
|
||||
): 'wrap' | 'nowrap' | undefined => {
|
||||
switch (columnKey) {
|
||||
case 'name':
|
||||
case 'kind':
|
||||
return 'nowrap';
|
||||
case 'image':
|
||||
case 'namespace':
|
||||
case 'state':
|
||||
case 'gpu':
|
||||
return 'wrap';
|
||||
case 'lastActivity':
|
||||
return 'nowrap';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const getSpecialColumnProps = (columnKey: WorkspaceTableColumnKeys) => {
|
||||
switch (columnKey) {
|
||||
case 'connect':
|
||||
return { screenReaderText: 'Connect action', hasContent: false };
|
||||
case 'actions':
|
||||
return { screenReaderText: 'Primary action', hasContent: false };
|
||||
default:
|
||||
return { hasContent: true };
|
||||
}
|
||||
};
|
||||
|
||||
const extractStateColor = (state: WorkspaceState) => {
|
||||
switch (state) {
|
||||
case WorkspaceState.WorkspaceStateRunning:
|
||||
|
@ -305,31 +324,41 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
case 'Info':
|
||||
return (
|
||||
<Tooltip content={message}>
|
||||
<InfoCircleIcon color="blue" aria-hidden="true" />
|
||||
<Icon status="info" isInline>
|
||||
<InfoCircleIcon aria-hidden="true" />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'Warning':
|
||||
return (
|
||||
<Tooltip content={message}>
|
||||
<ExclamationTriangleIcon color="orange" aria-hidden="true" />
|
||||
<Icon isInline>
|
||||
<ExclamationTriangleIcon color="orange" aria-hidden="true" />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
);
|
||||
case 'Danger':
|
||||
return (
|
||||
<Tooltip content={message}>
|
||||
<TimesCircleIcon color="red" aria-hidden="true" />
|
||||
<Icon isInline>
|
||||
<TimesCircleIcon color="red" aria-hidden="true" />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
);
|
||||
case undefined:
|
||||
return (
|
||||
<Tooltip content={message}>
|
||||
<QuestionCircleIcon color="gray" aria-hidden="true" />
|
||||
<Icon isInline>
|
||||
<QuestionCircleIcon color="gray" aria-hidden="true" />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Tooltip content={`Invalid level: ${level}`}>
|
||||
<QuestionCircleIcon color="gray" aria-hidden="true" />
|
||||
<Icon isInline>
|
||||
<QuestionCircleIcon color="gray" aria-hidden="true" />
|
||||
</Icon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
@ -371,19 +400,32 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
}
|
||||
/>
|
||||
</Content>
|
||||
<Table data-testid="workspaces-table" aria-label="Sortable table" ouiaId="SortableTable">
|
||||
<Table
|
||||
data-testid="workspaces-table"
|
||||
aria-label="Sortable table"
|
||||
ouiaId="SortableTable"
|
||||
style={{ tableLayout: 'fixed' }}
|
||||
>
|
||||
<Thead>
|
||||
<Tr>
|
||||
{canExpandRows && <Th screenReaderText="expand-action" />}
|
||||
{visibleColumnKeys.map((columnKey) => (
|
||||
<Th
|
||||
key={`workspace-table-column-${columnKey}`}
|
||||
sort={getSortParams(columnKey)}
|
||||
aria-label={columnKey}
|
||||
>
|
||||
{wsTableColumns[columnKey].label}
|
||||
</Th>
|
||||
))}
|
||||
{canExpandRows && <Th width={10} screenReaderText="expand-action" />}
|
||||
{visibleColumnKeys.map((columnKey) => {
|
||||
const specialProps = getSpecialColumnProps(columnKey);
|
||||
const modifier = getColumnModifier(columnKey);
|
||||
|
||||
return (
|
||||
<Th
|
||||
width={wsTableColumns[columnKey].width}
|
||||
key={`workspace-table-column-${columnKey}`}
|
||||
sort={specialProps.hasContent ? getSortParams(columnKey) : undefined}
|
||||
aria-label={specialProps.hasContent ? columnKey : undefined}
|
||||
modifier={modifier}
|
||||
screenReaderText={specialProps.screenReaderText}
|
||||
>
|
||||
{specialProps.hasContent ? wsTableColumns[columnKey].label : undefined}
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
</Thead>
|
||||
{sortedWorkspaces.length > 0 &&
|
||||
|
@ -397,6 +439,7 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
<Tr
|
||||
id={`workspaces-table-row-${rowIndex + 1}`}
|
||||
data-testid={`workspace-row-${rowIndex}`}
|
||||
isStriped={rowIndex % 2 === 0}
|
||||
>
|
||||
{canExpandRows && (
|
||||
<Td
|
||||
|
@ -409,10 +452,43 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
/>
|
||||
)}
|
||||
{visibleColumnKeys.map((columnKey) => {
|
||||
switch (columnKey) {
|
||||
case 'redirectStatus':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
if (columnKey === 'connect') {
|
||||
return (
|
||||
<Td key="connect" isActionCell>
|
||||
<WorkspaceConnectAction workspace={workspace} />
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
if (columnKey === 'actions') {
|
||||
return (
|
||||
<Td key="actions" isActionCell data-testid="action-column">
|
||||
<ActionsColumn
|
||||
items={rowActions(workspace).map((action) => ({
|
||||
...action,
|
||||
'data-testid': `action-${action.id || ''}`,
|
||||
}))}
|
||||
/>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Td
|
||||
key={columnKey}
|
||||
data-testid={
|
||||
columnKey === 'name'
|
||||
? 'workspace-name'
|
||||
: columnKey === 'state'
|
||||
? 'state-label'
|
||||
: `workspace-${columnKey}`
|
||||
}
|
||||
dataLabel={wsTableColumns[columnKey].label}
|
||||
>
|
||||
{columnKey === 'name' && workspace.name}
|
||||
{columnKey === 'image' && (
|
||||
<Content>
|
||||
{workspace.podTemplate.options.imageConfig.current.displayName}{' '}
|
||||
{workspaceRedirectStatus[workspace.workspaceKind.name]
|
||||
? getRedirectStatusIcon(
|
||||
workspaceRedirectStatus[workspace.workspaceKind.name]?.message
|
||||
|
@ -421,143 +497,57 @@ const WorkspaceTable = React.forwardRef<WorkspaceTableRef, WorkspaceTableProps>(
|
|||
?.text || 'No API response available',
|
||||
)
|
||||
: getRedirectStatusIcon(undefined, 'No API response available')}
|
||||
</Td>
|
||||
);
|
||||
case 'name':
|
||||
return (
|
||||
<Td
|
||||
key={columnKey}
|
||||
data-testid="workspace-name"
|
||||
dataLabel={wsTableColumns[columnKey].label}
|
||||
</Content>
|
||||
)}
|
||||
{columnKey === 'kind' && (
|
||||
<WithValidImage
|
||||
imageSrc={kindLogoDict[workspace.workspaceKind.name]}
|
||||
skeletonWidth="20px"
|
||||
fallback={
|
||||
<ImageFallback
|
||||
imageSrc={kindLogoDict[workspace.workspaceKind.name]}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{workspace.name}
|
||||
</Td>
|
||||
);
|
||||
case 'kind':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
<WithValidImage
|
||||
imageSrc={kindLogoDict[workspace.workspaceKind.name]}
|
||||
skeletonWidth="20px"
|
||||
fallback={
|
||||
<ImageFallback
|
||||
imageSrc={kindLogoDict[workspace.workspaceKind.name]}
|
||||
{(validSrc) => (
|
||||
<Tooltip content={workspace.workspaceKind.name}>
|
||||
<img
|
||||
src={validSrc}
|
||||
alt={workspace.workspaceKind.name}
|
||||
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{(validSrc) => (
|
||||
<Tooltip content={workspace.workspaceKind.name}>
|
||||
<img
|
||||
src={validSrc}
|
||||
alt={workspace.workspaceKind.name}
|
||||
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</WithValidImage>
|
||||
</Td>
|
||||
);
|
||||
case 'namespace':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{workspace.namespace}
|
||||
</Td>
|
||||
);
|
||||
case 'image':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{workspace.podTemplate.options.imageConfig.current.displayName}
|
||||
</Td>
|
||||
);
|
||||
case 'podConfig':
|
||||
return (
|
||||
<Td
|
||||
key={columnKey}
|
||||
data-testid="pod-config"
|
||||
dataLabel={wsTableColumns[columnKey].label}
|
||||
</Tooltip>
|
||||
)}
|
||||
</WithValidImage>
|
||||
)}
|
||||
{columnKey === 'namespace' && workspace.namespace}
|
||||
{columnKey === 'state' && (
|
||||
<Label color={extractStateColor(workspace.state)}>
|
||||
{workspace.state}
|
||||
</Label>
|
||||
)}
|
||||
{columnKey === 'gpu' && formatResourceFromWorkspace(workspace, 'gpu')}
|
||||
{columnKey === 'idleGpu' && formatWorkspaceIdleState(workspace)}
|
||||
{columnKey === 'lastActivity' && (
|
||||
<Timestamp
|
||||
date={new Date(workspace.activity.lastActivity)}
|
||||
tooltip={{ variant: TimestampTooltipVariant.default }}
|
||||
>
|
||||
{workspace.podTemplate.options.podConfig.current.displayName}
|
||||
</Td>
|
||||
);
|
||||
case 'state':
|
||||
return (
|
||||
<Td
|
||||
key={columnKey}
|
||||
data-testid="state-label"
|
||||
dataLabel={wsTableColumns[columnKey].label}
|
||||
>
|
||||
<Label color={extractStateColor(workspace.state)}>
|
||||
{workspace.state}
|
||||
</Label>
|
||||
</Td>
|
||||
);
|
||||
case 'homeVol':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{workspace.podTemplate.volumes.home?.pvcName ?? ''}
|
||||
</Td>
|
||||
);
|
||||
case 'cpu':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{formatResourceFromWorkspace(workspace, 'cpu')}
|
||||
</Td>
|
||||
);
|
||||
case 'ram':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{formatResourceFromWorkspace(workspace, 'memory')}
|
||||
</Td>
|
||||
);
|
||||
case 'gpu':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{formatResourceFromWorkspace(workspace, 'gpu')}
|
||||
</Td>
|
||||
);
|
||||
case 'idleGpu':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
{formatWorkspaceIdleState(workspace)}
|
||||
</Td>
|
||||
);
|
||||
case 'lastActivity':
|
||||
return (
|
||||
<Td key={columnKey} dataLabel={wsTableColumns[columnKey].label}>
|
||||
<Timestamp
|
||||
date={new Date(workspace.activity.lastActivity)}
|
||||
tooltip={{ variant: TimestampTooltipVariant.default }}
|
||||
>
|
||||
{formatDistanceToNow(new Date(workspace.activity.lastActivity), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Timestamp>
|
||||
</Td>
|
||||
);
|
||||
case 'connect':
|
||||
return (
|
||||
<Td key={columnKey} isActionCell>
|
||||
<WorkspaceConnectAction workspace={workspace} />
|
||||
</Td>
|
||||
);
|
||||
case 'actions':
|
||||
return (
|
||||
<Td key={columnKey} isActionCell data-testid="action-column">
|
||||
<ActionsColumn
|
||||
items={rowActions(workspace).map((action) => ({
|
||||
...action,
|
||||
'data-testid': `action-${action.id || ''}`,
|
||||
}))}
|
||||
/>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
{formatDistanceToNow(new Date(workspace.activity.lastActivity), {
|
||||
addSuffix: true,
|
||||
})}
|
||||
</Timestamp>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
{isWorkspaceExpanded(workspace) && (
|
||||
<ExpandedWorkspaceRow workspace={workspace} columnKeys={visibleColumnKeys} />
|
||||
<ExpandedWorkspaceRow
|
||||
workspace={workspace}
|
||||
visibleColumnKeys={visibleColumnKeys}
|
||||
canExpandRows={canExpandRows}
|
||||
/>
|
||||
)}
|
||||
</Tbody>
|
||||
))}
|
||||
|
|
|
@ -2,6 +2,7 @@ export interface DataFieldDefinition {
|
|||
label: string;
|
||||
isSortable: boolean;
|
||||
isFilterable: boolean;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export type FilterableDataFieldKey<T extends Record<string, DataFieldDefinition>> = {
|
||||
|
|
|
@ -92,7 +92,7 @@ const WorkspaceKindSummary: React.FC = () => {
|
|||
ref={workspaceTableRef}
|
||||
workspaces={workspaces}
|
||||
canCreateWorkspaces={false}
|
||||
hiddenColumns={['connect', 'kind', 'homeVol']}
|
||||
hiddenColumns={['connect', 'kind']}
|
||||
rowActions={tableRowActions}
|
||||
/>
|
||||
</StackItem>
|
||||
|
|
|
@ -3,12 +3,14 @@ import {
|
|||
ClipboardCopy,
|
||||
ClipboardCopyVariant,
|
||||
Content,
|
||||
DescriptionList,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
DescriptionListDescription,
|
||||
Flex,
|
||||
FlexItem,
|
||||
List,
|
||||
ListItem,
|
||||
Stack,
|
||||
StackItem,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import { DatabaseIcon, LockedIcon } from '@patternfly/react-icons';
|
||||
|
@ -43,32 +45,32 @@ export const DataVolumesList: React.FC<DataVolumesListProps> = ({ workspace }) =
|
|||
</Tooltip>
|
||||
)}
|
||||
</Content>
|
||||
<Stack>
|
||||
<StackItem>
|
||||
<Content component="small">
|
||||
Mount path:{' '}
|
||||
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact}>
|
||||
{data.mountPath}
|
||||
</ClipboardCopy>
|
||||
</Content>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<Flex gap={{ default: 'gapSm' }} flexWrap={{ default: 'wrap' }}>
|
||||
<FlexItem>Mount path:</FlexItem>
|
||||
<FlexItem>
|
||||
<ClipboardCopy variant={ClipboardCopyVariant.inlineCompact} isCode>
|
||||
{data.mountPath}
|
||||
</ClipboardCopy>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack hasGutter>
|
||||
<StackItem>
|
||||
<strong data-testid="notebook-storage-bar-title">Cluster storage</strong>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<List isPlain>
|
||||
{workspaceDataVol.map((data, index) => (
|
||||
<ListItem key={`data-vol-${index}`}>{singleDataVolRenderer(data)}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm data-testid="notebook-storage-bar-title">
|
||||
Cluster storage
|
||||
</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
<List isPlain>
|
||||
{workspaceDataVol.map((data, index) => (
|
||||
<ListItem key={`data-vol-${index}`}>{singleDataVolRenderer(data)}</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,38 +1,74 @@
|
|||
import React from 'react';
|
||||
import { ExpandableRowContent, Td, Tr } from '@patternfly/react-table';
|
||||
import { Tr, Td, ExpandableRowContent } from '@patternfly/react-table';
|
||||
import { Workspace } from '~/shared/api/backendApiTypes';
|
||||
import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList';
|
||||
import { WorkspaceTableColumnKeys } from '~/app/components/WorkspaceTable';
|
||||
import { WorkspaceStorage } from './WorkspaceStorage';
|
||||
import { WorkspacePackageDetails } from './WorkspacePackageDetails';
|
||||
import { WorkspaceConfigDetails } from './WorkspaceConfigDetails';
|
||||
|
||||
interface ExpandedWorkspaceRowProps {
|
||||
workspace: Workspace;
|
||||
columnKeys: WorkspaceTableColumnKeys[];
|
||||
visibleColumnKeys: WorkspaceTableColumnKeys[];
|
||||
canExpandRows: boolean;
|
||||
}
|
||||
|
||||
export const ExpandedWorkspaceRow: React.FC<ExpandedWorkspaceRowProps> = ({
|
||||
workspace,
|
||||
columnKeys,
|
||||
visibleColumnKeys,
|
||||
canExpandRows,
|
||||
}) => {
|
||||
const renderExpandedData = () =>
|
||||
columnKeys.map((colKey, index) => {
|
||||
switch (colKey) {
|
||||
case 'name':
|
||||
// Calculate total number of columns (including expand column if present)
|
||||
const totalColumns = visibleColumnKeys.length + (canExpandRows ? 1 : 0);
|
||||
|
||||
// Find the positions where we want to show our content
|
||||
// We'll show storage in the first content column, package details in the second,
|
||||
// and config details in the third
|
||||
const getColumnIndex = (columnKey: WorkspaceTableColumnKeys) => {
|
||||
const baseIndex = canExpandRows ? 1 : 0; // Account for expand column
|
||||
return baseIndex + visibleColumnKeys.indexOf(columnKey);
|
||||
};
|
||||
|
||||
const storageColumnIndex = visibleColumnKeys.includes('name') ? getColumnIndex('name') : 1;
|
||||
const packageColumnIndex = visibleColumnKeys.includes('image') ? getColumnIndex('image') : 2;
|
||||
const configColumnIndex = visibleColumnKeys.includes('kind') ? getColumnIndex('kind') : 3;
|
||||
|
||||
return (
|
||||
<Tr isExpanded>
|
||||
{/* Render cells for each column */}
|
||||
{Array.from({ length: totalColumns }, (_, index) => {
|
||||
if (index === storageColumnIndex) {
|
||||
return (
|
||||
<Td noPadding colSpan={1} key={colKey}>
|
||||
<Td key={`storage-${index}`} dataLabel="Storage" modifier="nowrap">
|
||||
<ExpandableRowContent>
|
||||
<DataVolumesList workspace={workspace} />
|
||||
<WorkspaceStorage workspace={workspace} />
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
);
|
||||
default:
|
||||
return <Td key={index} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td />
|
||||
{renderExpandedData()}
|
||||
if (index === packageColumnIndex) {
|
||||
return (
|
||||
<Td key={`package-${index}`} modifier="nowrap">
|
||||
<ExpandableRowContent>
|
||||
<WorkspacePackageDetails workspace={workspace} />
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
if (index === configColumnIndex) {
|
||||
return (
|
||||
<Td key={`config-${index}`} modifier="nowrap">
|
||||
<ExpandableRowContent>
|
||||
<WorkspaceConfigDetails workspace={workspace} />
|
||||
</ExpandableRowContent>
|
||||
</Td>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty cell for all other columns
|
||||
return <Td key={`empty-${index}`} />;
|
||||
})}
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
} from '@patternfly/react-core';
|
||||
import { Workspace } from '~/shared/api/backendApiTypes';
|
||||
import { formatResourceFromWorkspace } from '~/shared/utilities/WorkspaceUtils';
|
||||
|
||||
interface WorkspaceConfigDetailsProps {
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export const WorkspaceConfigDetails: React.FC<WorkspaceConfigDetailsProps> = ({ workspace }) => (
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Pod config</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{workspace.podTemplate.options.podConfig.current.displayName}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>CPU</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{formatResourceFromWorkspace(workspace, 'cpu')}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Memory</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{formatResourceFromWorkspace(workspace, 'memory')}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
);
|
|
@ -52,6 +52,7 @@ export const WorkspaceConnectAction: React.FunctionComponent<WorkspaceConnectAct
|
|||
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
|
||||
<MenuToggle
|
||||
ref={toggleRef}
|
||||
variant="secondary"
|
||||
onClick={onToggleClick}
|
||||
isExpanded={open}
|
||||
isDisabled={workspace.state !== WorkspaceState.WorkspaceStateRunning}
|
||||
|
@ -60,6 +61,7 @@ export const WorkspaceConnectAction: React.FunctionComponent<WorkspaceConnectAct
|
|||
id="connect-endpoint-button"
|
||||
key="connect-endpoint-button"
|
||||
onClick={onClickConnect}
|
||||
className="connect-button-no-wrap"
|
||||
>
|
||||
Connect
|
||||
</MenuToggleAction>,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListDescription,
|
||||
ListItem,
|
||||
List,
|
||||
DescriptionListGroup,
|
||||
} from '@patternfly/react-core';
|
||||
import { Workspace } from '~/shared/api/backendApiTypes';
|
||||
import { extractPackageLabels, formatLabelKey } from '~/shared/utilities/WorkspaceUtils';
|
||||
|
||||
interface WorkspacePackageDetailsProps {
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export const WorkspacePackageDetails: React.FC<WorkspacePackageDetailsProps> = ({ workspace }) => {
|
||||
const packageLabels = extractPackageLabels(workspace);
|
||||
|
||||
const renderedItems = packageLabels.map((label) => (
|
||||
<ListItem key={label.key}>{`${formatLabelKey(label.key)} v${label.value}`}</ListItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Packages</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{renderedItems.length > 0 ? (
|
||||
<List isPlain>{renderedItems}</List>
|
||||
) : (
|
||||
<span>No package information available</span>
|
||||
)}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListTerm,
|
||||
DescriptionListGroup,
|
||||
DescriptionListDescription,
|
||||
} from '@patternfly/react-core';
|
||||
import { Workspace } from '~/shared/api/backendApiTypes';
|
||||
import { DataVolumesList } from '~/app/pages/Workspaces/DataVolumesList';
|
||||
|
||||
interface WorkspaceStorageProps {
|
||||
workspace: Workspace;
|
||||
}
|
||||
|
||||
export const WorkspaceStorage: React.FC<WorkspaceStorageProps> = ({ workspace }) => (
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>Home volume</DescriptionListTerm>
|
||||
<DescriptionListDescription>
|
||||
{workspace.podTemplate.volumes.home?.pvcName ?? 'None'}
|
||||
</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
<DescriptionListGroup>
|
||||
<DataVolumesList workspace={workspace} />
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
);
|
|
@ -81,6 +81,10 @@ export const buildMockWorkspace = (workspace?: Partial<Workspace>): Workspace =>
|
|||
key: 'pythonVersion',
|
||||
value: '3.11',
|
||||
},
|
||||
{
|
||||
key: 'jupyterlabVersion',
|
||||
value: '1.9.0',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -290,9 +294,30 @@ export const buildMockWorkspaceList = (args: {
|
|||
}): Workspace[] => {
|
||||
const states = Object.values(WorkspaceState);
|
||||
const imageConfigs = [
|
||||
{ id: 'jupyterlab_scipy_190', displayName: `jupyter-scipy:v1.9.0` },
|
||||
{ id: 'jupyterlab_scipy_200', displayName: `jupyter-scipy:v2.0.0` },
|
||||
{ id: 'jupyterlab_scipy_210', displayName: `jupyter-scipy:v2.1.0` },
|
||||
{
|
||||
id: 'jupyterlab_scipy_190',
|
||||
displayName: `jupyter-scipy:v1.9.0`,
|
||||
labels: [
|
||||
{ key: 'pythonVersion', value: '3.12' },
|
||||
{ key: 'jupyterlabVersion', value: '1.9.0' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'jupyterlab_scipy_200',
|
||||
displayName: `jupyter-scipy:v2.0.0`,
|
||||
labels: [
|
||||
{ key: 'pythonVersion', value: '3.12' },
|
||||
{ key: 'jupyterlabVersion', value: '2.0.0' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'jupyterlab_scipy_210',
|
||||
displayName: `jupyter-scipy:v2.1.0`,
|
||||
labels: [
|
||||
{ key: 'pythonVersion', value: '3.13' },
|
||||
{ key: 'jupyterlabVersion', value: '2.1.0' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const podConfigs = [
|
||||
{ id: 'tiny_cpu', displayName: 'Tiny CPU' },
|
||||
|
@ -353,12 +378,7 @@ export const buildMockWorkspaceList = (args: {
|
|||
id: imageConfig.id,
|
||||
displayName: imageConfig.displayName,
|
||||
description: 'JupyterLab, with SciPy Packages',
|
||||
labels: [
|
||||
{
|
||||
key: 'pythonVersion',
|
||||
value: '3.11',
|
||||
},
|
||||
],
|
||||
labels: imageConfig.labels,
|
||||
},
|
||||
},
|
||||
podConfig: {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
// Button
|
||||
--mui-button-font-weight: 500;
|
||||
--mui-button--BorderWidth: 1px;
|
||||
--mui-button--hover--BorderWidth: 1px;
|
||||
--mui-button--PaddingBlockStart: 6px;
|
||||
--mui-button--PaddingBlockEnd: 6px;
|
||||
|
@ -76,7 +77,7 @@
|
|||
--mui-table--cell--PaddingInlineEnd: 16px;
|
||||
--mui-table--cell--PaddingBlockStart: 16px;
|
||||
--mui-table--cell--PaddingBlockEnd: 16px;
|
||||
--mui-table--cell--first-last-child--PaddingInline: 0px;
|
||||
--mui-table--cell--first-last-child--PaddingInline: 8px;
|
||||
--mui-table__thead--cell--FontSize: 14px;
|
||||
--mui-table__sort-indicator--MarginInlineStart: 4px;
|
||||
|
||||
|
@ -116,7 +117,6 @@
|
|||
}
|
||||
|
||||
.mui-theme .pf-v6-c-action-list__item .pf-v6-c-button {
|
||||
--pf-v6-c-button--BorderRadius: 50%;
|
||||
--pf-v6-c-button--PaddingInlineStart: none;
|
||||
--pf-v6-c-button--PaddingInlineEnd: none;
|
||||
}
|
||||
|
@ -152,7 +152,6 @@
|
|||
--pf-v6-c-button--PaddingInlineStart: var(--mui-button--PaddingInlineStart);
|
||||
--pf-v6-c-button--PaddingInlineEnd: var(--mui-button--PaddingInlineEnd);
|
||||
--pf-v6-c-button--LineHeight: var(--mui-button--LineHeight);
|
||||
--pf-v6-c-button--m-plain--BorderRadius: 50%;
|
||||
|
||||
text-transform: var(--mui-text-transform);
|
||||
letter-spacing: 0.02857em;
|
||||
|
@ -197,10 +196,6 @@
|
|||
--pf-v6-c-card--m-selectable--m-selected--BorderColor: none;
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-description-list {
|
||||
--pf-v6-c-description-list--RowGap: var(--pf-t--global--spacer--gap--group-to-group--horizontal--compact);
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-description-list__term {
|
||||
font: var(--mui-font-subtitle2);
|
||||
}
|
||||
|
@ -237,20 +232,26 @@
|
|||
--pf-v6-c-form__section-title--MarginInlineEnd: 0px;
|
||||
}
|
||||
|
||||
// Base form label styles
|
||||
.mui-theme .pf-v6-c-form__label {
|
||||
color: var(--mui-palette-grey-600);
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
--pf-v6-c-form__label-required--Color: currentColor;
|
||||
}
|
||||
|
||||
// Text input labels (text fields, textareas, selects)
|
||||
.mui-theme .pf-v6-c-form__group:has(.pf-v6-c-form-control) .pf-v6-c-form__label,
|
||||
.mui-theme .pf-v6-c-form__group:has(.pf-v6-c-text-input-group) .pf-v6-c-form__label {
|
||||
position: absolute;
|
||||
top: 35%;
|
||||
left: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--mui-palette-grey-600);
|
||||
pointer-events: none;
|
||||
transition: all 0.2s ease;
|
||||
transform-origin: left center;
|
||||
transform: translateY(-50%) scale(0.75);
|
||||
background-color: var(--mui-palette-common-white);
|
||||
padding: 0 4px;
|
||||
z-index: 1;
|
||||
--pf-v6-c-form__label-required--Color: currentColor;
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-form-control input::placeholder {
|
||||
|
@ -514,10 +515,6 @@
|
|||
align-self: stretch;
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-menu-toggle.pf-m-plain {
|
||||
--pf-v6-c-menu-toggle--BorderRadius: 50%;
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-menu-toggle.pf-m-primary {
|
||||
--pf-v6-c-menu-toggle--expanded--Color: var(--mui-palette-common-white);
|
||||
--pf-v6-c-menu-toggle--expanded--BackgroundColor: var(--mui-palette-primary-main);
|
||||
|
@ -532,6 +529,13 @@
|
|||
--pf-v6-c-menu-toggle--m-split-button--m-action--m-primary--child--BackgroundColor: var(--mui-palette-primary-dark);
|
||||
}
|
||||
|
||||
.pf-v6-c-menu-toggle.pf-m-secondary.pf-m-split-button {
|
||||
--pf-v6-c-menu-toggle--BorderColor: var(--mui-palette-primary-main);
|
||||
--pf-v6-c-menu-toggle--BorderWidth: var(--mui-button--BorderWidth);
|
||||
--pf-v6-c-menu-toggle--BorderStyle: solid;
|
||||
--pf-v6-c-menu-toggle--expanded--Color: var(--mui-palette-primary-dark);
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-menu-toggle__button:has(.pf-v6-c-menu-toggle__toggle-icon) {
|
||||
--pf-v6-c-menu-toggle--PaddingBlockStart: var(--mui-spacing-4);
|
||||
--pf-v6-c-menu-toggle--PaddingBlockEnd: var(--mui-spacing-4);
|
||||
|
@ -725,6 +729,12 @@
|
|||
align-content: center;
|
||||
}
|
||||
|
||||
// Updates the expand button to be 36.5px wide to match menu toggle button width
|
||||
.mui-theme .pf-v6-c-table__td.pf-v6-c-table__toggle .pf-v6-c-button.pf-m-plain {
|
||||
--pf-v6-c-button--PaddingInlineStart: 6px;
|
||||
--pf-v6-c-button--PaddingInlineEnd: 6px;
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-label {
|
||||
--pf-v6-c-label--BorderRadius: 16px;
|
||||
--pf-v6-c-label--FontSize: 0.8125rem;
|
||||
|
@ -775,10 +785,6 @@
|
|||
--pf-v6-c-modal-box--BoxShadow: var(--mui-shadows-24);
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-button.pf-m-plain {
|
||||
--pf-v6-c-button--BorderRadius: 50%;
|
||||
}
|
||||
|
||||
.mui-theme .pf-v6-c-page__main-container {
|
||||
--pf-v6-c-page__main-container--BorderWidth: 0px;
|
||||
--pf-v6-c-page__main-container--BorderRadius: var(--mui-shape-borderRadius);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Workspace, WorkspaceState } from '~/shared/api/backendApiTypes';
|
||||
import { Workspace, WorkspaceState, WorkspaceOptionLabel } from '~/shared/api/backendApiTypes';
|
||||
import {
|
||||
CPU_UNITS,
|
||||
MEMORY_UNITS_FOR_PARSING,
|
||||
|
@ -102,3 +102,24 @@ export const countGpusFromWorkspaces = (workspaces: Workspace[]): number =>
|
|||
const [gpuValue] = splitValueUnit(extractResourceValue(workspace, 'gpu') || '0', OTHER);
|
||||
return total + (gpuValue ?? 0);
|
||||
}, 0);
|
||||
|
||||
// Helper function to format label keys into human-readable names
|
||||
export const formatLabelKey = (key: string): string => {
|
||||
// Handle camelCase version labels (e.g., pythonVersion -> Python)
|
||||
if (key.endsWith('Version')) {
|
||||
const baseName = key.slice(0, -7); // Remove 'Version' suffix
|
||||
return baseName.charAt(0).toUpperCase() + baseName.slice(1);
|
||||
}
|
||||
|
||||
// Otherwise just capitalize the first letter
|
||||
return key.charAt(0).toUpperCase() + key.slice(1);
|
||||
};
|
||||
|
||||
// Check if a label represents version/package information
|
||||
export const isPackageLabel = (key: string): boolean => key.endsWith('Version');
|
||||
|
||||
// Extract package labels from workspace image config
|
||||
export const extractPackageLabels = (workspace: Workspace): WorkspaceOptionLabel[] =>
|
||||
workspace.podTemplate.options.imageConfig.current.labels.filter((label) =>
|
||||
isPackageLabel(label.key),
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue