diff --git a/frontend/src/__mocks__/mockAcceleratorProfile.ts b/frontend/src/__mocks__/mockAcceleratorProfile.ts index 4679cbe811..782a025306 100644 --- a/frontend/src/__mocks__/mockAcceleratorProfile.ts +++ b/frontend/src/__mocks__/mockAcceleratorProfile.ts @@ -1,19 +1,29 @@ import { AcceleratorKind } from '~/k8sTypes'; -export const mockAcceleratorProfile = (): AcceleratorKind => ({ +type MockAcceleratorProfileType = { + name?: string; + enabled?: boolean; + identifier?: string; +}; + +export const mockAcceleratorProfile = ({ + name = 'Test Accelerator', + enabled = true, + identifier = 'nvidia.com/gpu', +}: MockAcceleratorProfileType): AcceleratorKind => ({ apiVersion: 'dashboard.opendatahub.io/v1', kind: 'AcceleratorProfile', metadata: { - name: 'test-accelerator', + name: name, }, spec: { - displayName: 'test-accelerator', - enabled: true, - identifier: 'nvidia.com/gpu', + displayName: name, + enabled: enabled, + identifier: identifier, description: 'Test description', tolerations: [ { - key: 'nvidia.com/gpu', + key: identifier, operator: 'Exists', effect: 'NoSchedule', }, diff --git a/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts index 98b9924715..5344c43628 100644 --- a/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts +++ b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.spec.ts @@ -15,3 +15,16 @@ test('Empty State no Accelerator profile', async ({ page }) => { // Test that the button is enabled await expect(page.getByRole('button', { name: 'Add new accelerator profile' })).toBeEnabled(); }); + +test('List Accelerator profiles', async ({ page }) => { + await page.goto( + navigateToStory('pages-acceleratorprofiles-acceleratorprofiles', 'list-accelerator-profiles'), + ); + + // wait for page to load + await page.waitForSelector('text=Accelerator profiles'); + + expect(page.getByText('TensorRT')).toBeTruthy(); + expect(page.getByText('Test Accelerator')).toBeTruthy(); + expect(page.getByText('nvidia.com/gpu ')).toBeTruthy(); +}); diff --git a/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx index 27001e575b..56ca822415 100644 --- a/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx +++ b/frontend/src/__tests__/integration/pages/acceleratorProfiles/AcceleratorProfiles.stories.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { StoryFn, Meta, StoryObj } from '@storybook/react'; +import { within } from '@storybook/testing-library'; import { rest } from 'msw'; import { mockAcceleratorProfile } from '~/__mocks__/mockAcceleratorProfile'; import { mockProjectK8sResource } from '~/__mocks__/mockProjectK8sResource'; @@ -7,7 +8,7 @@ import { mockK8sResourceList } from '~/__mocks__/mockK8sResourceList'; import useDetectUser from '~/utilities/useDetectUser'; import { mockStatus } from '~/__mocks__/mockStatus'; -import AcceleratorProfiles from '~/pages/acceleratorProfiles/AcceleratorProfiles'; +import AcceleratorProfiles from '~/pages/acceleratorProfiles/screens/list/AcceleratorProfiles'; export default { component: AcceleratorProfiles, @@ -22,7 +23,18 @@ export default { ], accelerators: rest.get( '/api/k8s/apis/dashboard.opendatahub.io/v1/namespaces/opendatahub/acceleratorprofiles', - (req, res, ctx) => res(ctx.json(mockK8sResourceList([mockAcceleratorProfile()]))), + (req, res, ctx) => + res( + ctx.json( + mockK8sResourceList([ + mockAcceleratorProfile({}), + mockAcceleratorProfile({ + name: 'TensorRT', + enabled: false, + }), + ]), + ), + ), ), }, }, @@ -48,3 +60,14 @@ export const EmptyStateNoAcceleratorProfile: StoryObj = { }, }, }; + +export const ListAcceleratorProfiles: StoryObj = { + render: Template, + + play: async ({ canvasElement }) => { + // load page and wait until settled + const canvas = within(canvasElement); + await canvas.findByText('Test Accelerator', undefined, { timeout: 5000 }); + await canvas.findByText('TensorRT', undefined, { timeout: 5000 }); + }, +}; diff --git a/frontend/src/app/AppRoutes.tsx b/frontend/src/app/AppRoutes.tsx index 88ecd597ae..5bc072100d 100644 --- a/frontend/src/app/AppRoutes.tsx +++ b/frontend/src/app/AppRoutes.tsx @@ -38,7 +38,7 @@ const DependencyMissingPage = React.lazy( ); const AcceleratorProfiles = React.lazy( - () => import('../pages/acceleratorProfiles/AcceleratorProfiles'), + () => import('../pages/acceleratorProfiles/screens/list/AcceleratorProfiles'), ); const AppRoutes: React.FC = () => { diff --git a/frontend/src/pages/acceleratorProfiles/components/AcceleratorProfileEnableToggle.tsx b/frontend/src/pages/acceleratorProfiles/components/AcceleratorProfileEnableToggle.tsx new file mode 100644 index 0000000000..aeed794ea9 --- /dev/null +++ b/frontend/src/pages/acceleratorProfiles/components/AcceleratorProfileEnableToggle.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import { Switch } from '@patternfly/react-core'; + +type AcceleratorProfileEnableToggleProps = { + enabled: boolean; + name: string; +}; + +const AcceleratorProfileEnableToggle: React.FC = ({ + enabled, + name, +}) => { + const label = enabled ? 'enabled' : 'stopped'; + + return ( + undefined} + /> + ); +}; + +export default AcceleratorProfileEnableToggle; diff --git a/frontend/src/pages/acceleratorProfiles/components/SearchField.tsx b/frontend/src/pages/acceleratorProfiles/components/SearchField.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/pages/acceleratorProfiles/components/TableData.tsx b/frontend/src/pages/acceleratorProfiles/components/TableData.tsx new file mode 100644 index 0000000000..5d6e081526 --- /dev/null +++ b/frontend/src/pages/acceleratorProfiles/components/TableData.tsx @@ -0,0 +1,41 @@ +import { SortableData } from '~/components/table'; +import { AcceleratorKind } from '~/k8sTypes'; + +export const columns: SortableData[] = [ + { + field: 'name', + label: 'Name', + sortable: (a, b) => a.spec.displayName.localeCompare(b.spec.displayName), + width: 30, + }, + { + field: 'identifier', + label: 'Identifier', + sortable: (a, b) => a.spec.identifier.localeCompare(b.spec.identifier), + info: { + tooltip: 'The resource identifier of the accelerator device.', + }, + }, + { + field: 'enablement', + label: 'Enable', + sortable: (a, b) => Number(a.spec.enabled ?? false) - Number(b.spec.enabled ?? false), + info: { + tooltip: 'Indicates whether the accelerator profile is available for new resources.', + }, + }, + { + field: 'last_modified', + label: 'Last modified', + sortable: (a, b): number => { + const first = a.metadata.annotations?.['opendatahub.io/modified-date']; + const second = b.metadata.annotations?.['opendatahub.io/modified-date']; + return new Date(first ?? 0).getTime() - new Date(second ?? 0).getTime(); + }, + }, + { + field: 'kebab', + label: '', + sortable: false, + }, +]; diff --git a/frontend/src/pages/acceleratorProfiles/AcceleratorProfiles.tsx b/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfiles.tsx similarity index 87% rename from frontend/src/pages/acceleratorProfiles/AcceleratorProfiles.tsx rename to frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfiles.tsx index 8cd2938307..0582baf1c3 100644 --- a/frontend/src/pages/acceleratorProfiles/AcceleratorProfiles.tsx +++ b/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfiles.tsx @@ -2,20 +2,18 @@ import * as React from 'react'; import { Button, ButtonVariant, - Flex, - FlexItem, EmptyState, EmptyStateIcon, EmptyStateVariant, EmptyStateBody, PageSection, - PageSectionVariants, Title, } from '@patternfly/react-core'; import { PlusCircleIcon } from '@patternfly/react-icons'; import ApplicationsPage from '~/pages/ApplicationsPage'; import useAccelerators from '~/pages/notebookController/screens/server/useAccelerators'; import { useDashboardNamespace } from '~/redux/selectors'; +import AcceleratorProfilesTable from '~/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTable'; const description = `Manage accelerator profile settings for users in your organization`; @@ -57,12 +55,9 @@ const AcceleratorProfiles: React.FC = () => { loadError={loadError} errorMessage="Unable to load accelerator profiles." emptyStatePage={noAcceleratorProfilePageSection} + provideChildrenPadding > - - - {/* Todo: Create accelerator table */} - - + ); }; diff --git a/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTable.tsx b/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTable.tsx new file mode 100644 index 0000000000..51ad9ad83a --- /dev/null +++ b/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTable.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Table } from '~/components/table'; +import { columns } from '~/pages/acceleratorProfiles/components/TableData'; +import AcceleratorProfilesTableRow from '~/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTableRow'; +import { AcceleratorKind } from '~/k8sTypes'; + +type AcceleratorProfilesTableProps = { + accelerators: AcceleratorKind[]; +}; + +const AcceleratorProfilesTable: React.FC = ({ accelerators }) => ( + ( + + )} + /> +); + +export default AcceleratorProfilesTable; diff --git a/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTableRow.tsx b/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTableRow.tsx new file mode 100644 index 0000000000..548b9ba4c2 --- /dev/null +++ b/frontend/src/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTableRow.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Text, TextContent, TextVariants, Timestamp, Truncate } from '@patternfly/react-core'; +import { ActionsColumn, Td, Tr } from '@patternfly/react-table'; +import { AcceleratorKind } from '~/k8sTypes'; +import AcceleratorProfileEnableToggle from '~/pages/acceleratorProfiles/components/AcceleratorProfileEnableToggle'; + +type AcceleratorProfilesTableRow = { + accelerator: AcceleratorKind; +}; + +const AcceleratorProfilesTableRow: React.FC = ({ accelerator }) => ( + + + + + + + +); + +export default AcceleratorProfilesTableRow;
+ + {accelerator.spec.displayName} + {accelerator.spec.description && ( + + + + )} + + {accelerator.spec.identifier} + + + {accelerator.metadata.annotations?.['opendatahub.io/modified-date'] ? ( + + ) : ( + '--' + )} + + undefined, + }, + { + title: 'Delete', + onClick: () => undefined, + }, + ]} + /> +