Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add accelerator table #2069

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,39 @@ 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();
});

dpanshug marked this conversation as resolved.
Show resolved Hide resolved
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');

// Check for name
expect(page.getByText('TensorRT')).toBeTruthy();
expect(page.getByText('Test Accelerator')).toBeTruthy();

// Check for description
expect(
page.getByText('Lorem, ipsum dolor sit amet consectetur adipisicing elit. Saepe, quis'),
).toBeTruthy();
expect(page.getByText('Test description')).toBeTruthy();

// Check for identifier
expect(page.getByText('tensor.com/gpu')).toBeTruthy();
expect(page.getByText('nvidia.com/gpu')).toBeTruthy();

// Check column sorting by identifier
await page
.locator('th:has-text("Identifier")')
.getByRole('button', { name: 'Identifier', exact: true })
.click();
const tableBodyLocator = await page.locator('table#accelerator-profile-table tbody');
const firstRowIdentifier = await tableBodyLocator
.locator('tr')
.nth(0)
.locator('td:nth-child(2)')
.allInnerTexts();
await expect(firstRowIdentifier[0]).toBe('nvidia.com/gpu');
});
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,7 +23,24 @@ 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({
metadata: { name: 'some-other-gpu' },
spec: {
displayName: 'TensorRT',
enabled: false,
identifier: 'tensor.com/gpu',
description:
'Lorem, ipsum dolor sit amet consectetur adipisicing elit. Saepe, quis',
},
}),
]),
),
),
),
},
},
Expand All @@ -48,3 +66,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');
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { Switch } from '@patternfly/react-core';
import DisableAcceleratorProfileModal from '~/pages/acceleratorProfiles/screens/list/DisableAcceleratorProfileModal';
import { updateAcceleratorProfile } from '~/services/acceleratorProfileService';
import useNotification from '~/utilities/useNotification';

type AcceleratorProfileEnableToggleProps = {
enabled: boolean;
name: string;
};

const AcceleratorProfileEnableToggle: React.FC<AcceleratorProfileEnableToggleProps> = ({
dpanshug marked this conversation as resolved.
Show resolved Hide resolved
enabled,
name,
}) => {
const label = enabled ? 'enabled' : 'stopped';
const [isModalOpen, setIsModalOpen] = React.useState(false);
const [isEnabled, setEnabled] = React.useState(enabled);
const [isLoading, setLoading] = React.useState(false);
const notification = useNotification();

const handleChange = (checked: boolean) => {
setLoading(true);
updateAcceleratorProfile(name, {
enabled: checked,
})
.then(() => {
setEnabled(checked);
})
.catch((e) => {
notification.error(
`Error ${checked ? 'enable' : 'disable'} the accelerator profile`,
e.message,
);
setEnabled(!checked);
})
.finally(() => {
setLoading(false);
});
};

return (
<>
<Switch
aria-label={label}
id={`${name}-enable-switch`}
isChecked={isEnabled}
isDisabled={isLoading}
onChange={() => {
isEnabled ? setIsModalOpen(true) : handleChange(true);
}}
/>
<DisableAcceleratorProfileModal
isOpen={isModalOpen}
onClose={(confirmStatus) => {
if (confirmStatus) {
handleChange(false);
}
setIsModalOpen(false);
}}
/>
</>
);
};

export default AcceleratorProfileEnableToggle;
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ 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 { useNavigate } from 'react-router-dom';
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`;

Expand Down Expand Up @@ -60,12 +58,9 @@ const AcceleratorProfiles: React.FC = () => {
loadError={loadError}
errorMessage="Unable to load accelerator profiles."
emptyStatePage={noAcceleratorProfilePageSection}
provideChildrenPadding
>
<PageSection variant={PageSectionVariants.light} padding={{ default: 'noPadding' }}>
<Flex direction={{ default: 'column' }}>
<FlexItem>{/* Todo: Create accelerator table */}</FlexItem>
</Flex>
</PageSection>
<AcceleratorProfilesTable accelerators={accelerators} />
</ApplicationsPage>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as React from 'react';
import { Button, ToolbarItem } from '@patternfly/react-core';
import { useNavigate } from 'react-router-dom';
import { Table } from '~/components/table';
import AcceleratorProfilesTableRow from '~/pages/acceleratorProfiles/screens/list/AcceleratorProfilesTableRow';
import { AcceleratorKind } from '~/k8sTypes';
import { columns } from '~/pages/acceleratorProfiles/screens/list/const';

type AcceleratorProfilesTableProps = {
accelerators: AcceleratorKind[];
};

const AcceleratorProfilesTable: React.FC<AcceleratorProfilesTableProps> = ({ accelerators }) => {
const navigate = useNavigate();

return (
<Table
id="accelerator-profile-table"
enablePagination
data={accelerators}
columns={columns}
emptyTableView={'No projects match your filters.'}
rowRenderer={(accelerator) => (
<AcceleratorProfilesTableRow key={accelerator.metadata.name} accelerator={accelerator} />
)}
toolbarContent={
<ToolbarItem>
<Button
data-id="create-accelerator-profile"
onClick={() => navigate(`/acceleratorProfiles/create`)}
>
Create accelerator profile
</Button>
</ToolbarItem>
}
/>
);
};

export default AcceleratorProfilesTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react';
import {
Text,
TextContent,
TextVariants,
Timestamp,
TimestampTooltipVariant,
Truncate,
} from '@patternfly/react-core';
import { ActionsColumn, Td, Tr } from '@patternfly/react-table';
import { useNavigate } from 'react-router-dom';
import { AcceleratorKind } from '~/k8sTypes';
import AcceleratorProfileEnableToggle from '~/pages/acceleratorProfiles/screens/list/AcceleratorProfileEnableToggle';
import { relativeTime } from '~/utilities/time';

type AcceleratorProfilesTableRow = {
accelerator: AcceleratorKind;
};

const AcceleratorProfilesTableRow: React.FC<AcceleratorProfilesTableRow> = ({ accelerator }) => {
const navigate = useNavigate();
const modifiedDate = accelerator.metadata.annotations?.['opendatahub.io/modified-date'];

return (
<Tr>
<Td dataLabel="Name">
<TextContent>
<Text>
<Truncate content={accelerator.spec.displayName} />
</Text>
{accelerator.spec.description && (
<Text component={TextVariants.small}>
<Truncate content={accelerator.spec.description} />
</Text>
)}
</TextContent>
</Td>
<Td dataLabel="Identifier">{accelerator.spec.identifier}</Td>
<Td dataLabel="Enable">
<AcceleratorProfileEnableToggle
enabled={accelerator.spec.enabled}
name={accelerator.metadata.name}
/>
</Td>
<Td dataLabel="Last modified">
{modifiedDate && !isNaN(new Date(modifiedDate).getTime()) ? (
<Timestamp
date={new Date(modifiedDate)}
tooltip={{
variant: TimestampTooltipVariant.default,
}}
>
{relativeTime(Date.now(), new Date(modifiedDate).getTime())}
</Timestamp>
) : (
'--'
)}
</Td>
<Td isActionCell>
<ActionsColumn
items={[
{
title: 'Edit',
onClick: () => navigate(`/acceleratorProfiles/edit/${accelerator.metadata.name}`),
},
{
title: 'Delete',
onClick: () => undefined,
},
]}
/>
</Td>
</Tr>
);
};

export default AcceleratorProfilesTableRow;
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { Button, Modal } from '@patternfly/react-core';

type DisableAcceleratorProfileModal = {
isOpen: boolean;
onClose: (confirmStatus: boolean) => void;
};

const DisableAcceleratorProfileModal: React.FC<DisableAcceleratorProfileModal> = ({
isOpen,
onClose,
}) => (
<Modal
variant="small"
title="Disable accelerator"
isOpen={isOpen}
onClose={() => onClose(false)}
actions={[
<Button key="confirm-disable" variant="primary" onClick={() => onClose(true)}>
Disable
</Button>,
<Button key="cancel" variant="secondary" onClick={() => onClose(false)}>
Cancel
</Button>,
]}
>
Disable this will disable accelerators for existing images and runtimes that want to use it.
</Modal>
);

export default DisableAcceleratorProfileModal;
47 changes: 47 additions & 0 deletions frontend/src/pages/acceleratorProfiles/screens/list/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { SortableData } from '~/components/table';
import { AcceleratorKind } from '~/k8sTypes';

export const columns: SortableData<AcceleratorKind>[] = [
{
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: {
popover: 'The resource identifier of the accelerator device.',
popoverProps: {
showClose: false,
},
},
},
{
field: 'enablement',
label: 'Enable',
sortable: (a, b) => Number(a.spec.enabled ?? false) - Number(b.spec.enabled ?? false),
info: {
popover: 'Indicates whether the accelerator profile is available for new resources.',
popoverProps: {
showClose: false,
},
},
},
{
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,
},
];
Loading