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

Create general purpose Table component #978

Merged
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
122 changes: 122 additions & 0 deletions frontend/src/components/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Pagination, Toolbar, ToolbarContent, ToolbarItem } from '@patternfly/react-core';
import {
TableComposable,
Thead,
Tr,
Th,
TableComposableProps,
Caption,
Tbody,
} from '@patternfly/react-table';
import React, { useEffect } from 'react';
import useTableColumnSort, { SortableData } from '~/utilities/useTableColumnSort';

type TableProps<DataType> = {
data: DataType[];
columns: SortableData<DataType>[];
rowRenderer: (data: DataType) => React.ReactNode;
enablePagination?: boolean;
minPageSize?: number;
toolbarContent?: React.ReactElement<typeof ToolbarItem>;
emptyTableView?: React.ReactElement<typeof Tr>;
caption?: string;
disableRowRenderSupport?: boolean;
} & Omit<TableComposableProps, 'ref' | 'data'>;

const Table = <T,>({
data: allData,
columns,
rowRenderer,
enablePagination,
minPageSize = 10,
toolbarContent,
emptyTableView,
caption,
disableRowRenderSupport,
...props
}: TableProps<T>): React.ReactElement => {
const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(minPageSize);

const data = enablePagination ? allData.slice(pageSize * (page - 1), pageSize * page) : allData;

// update page to 1 if data changes (common when filter is applied)
useEffect(() => {
if (data.length === 0) {
setPage(1);
}
}, [data.length]);

const sort = useTableColumnSort<T>(columns, 0);

const showPagination = enablePagination && allData.length > minPageSize;
const pagination = (pageDirection: 'up' | 'down') => (
<Pagination
dropDirection={pageDirection}
perPageComponent="button"
itemCount={allData.length}
perPage={pageSize}
page={page}
onSetPage={(e, newPage) => setPage(newPage)}
onPerPageSelect={(e, newSize, newPage) => {
setPageSize(newSize);
setPage(newPage);
}}
widgetId="table-pagination"
/>
);

return (
<>
{(toolbarContent || showPagination) && (
<Toolbar>
<ToolbarContent>
{toolbarContent}
{showPagination && (
<ToolbarItem variant="pagination" alignment={{ default: 'alignRight' }}>
{pagination('down')}
</ToolbarItem>
)}
</ToolbarContent>
</Toolbar>
)}
<TableComposable {...props}>
{caption && <Caption>{caption}</Caption>}
<Thead>
<Tr>
{columns.map((col, i) => (
<Th
key={col.field + i}
andrewballantyne marked this conversation as resolved.
Show resolved Hide resolved
sort={col.sortable ? sort.getColumnSort(i) : undefined}
width={col.width}
>
{col.label}
</Th>
))}
</Tr>
</Thead>
{disableRowRenderSupport ? (
sort.transformData(data).map((row) => rowRenderer(row))
) : (
<Tbody>{sort.transformData(data).map((row) => rowRenderer(row))}</Tbody>
)}
</TableComposable>
{emptyTableView && data.length === 0 && (
<div style={{ padding: 'var(--pf-global--spacer--2xl) 0', textAlign: 'center' }}>
{emptyTableView}
</div>
)}
{showPagination && (
<Toolbar>
<ToolbarContent>
<ToolbarItem variant="pagination" alignment={{ default: 'alignRight' }}>
{pagination('up')}
</ToolbarItem>
</ToolbarContent>
</Toolbar>
)}
</>
);
};

export default Table;
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import * as React from 'react';
import { Pagination, Toolbar, ToolbarContent, ToolbarItem } from '@patternfly/react-core';
import { ToolbarItem } from '@patternfly/react-core';
import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes';
import useTableColumnSort from '~/utilities/useTableColumnSort';
import SearchField, { SearchType } from '~/pages/projects/components/SearchField';
import { ModelServingContext } from '~/pages/modelServing/ModelServingContext';
import { getInferenceServiceDisplayName } from './utils';
import ServeModelButton from './ServeModelButton';
import { getGlobalInferenceServiceColumns } from './data';
import InferenceServiceTable from './InferenceServiceTable';

const MIN_PAGE_SIZE = 10;

type InferenceServiceListViewProps = {
inferenceServices: InferenceServiceKind[];
servingRuntimes: ServingRuntimeKind[];
Expand All @@ -22,102 +18,61 @@ const InferenceServiceListView: React.FC<InferenceServiceListViewProps> = ({
}) => {
const {
inferenceServices: { refresh },
projects: { data: projects },
} = React.useContext(ModelServingContext);
const [searchType, setSearchType] = React.useState<SearchType>(SearchType.NAME);
const [search, setSearch] = React.useState('');
const [page, setPage] = React.useState(1);
const [pageSize, setPageSize] = React.useState(MIN_PAGE_SIZE);
const sortInferenceService = useTableColumnSort<InferenceServiceKind>(
getGlobalInferenceServiceColumns(projects),
0,
);
const filteredInferenceServices = sortInferenceService
.transformData(unfilteredInferenceServices)
.filter((project) => {
if (!search) {
return true;
}

switch (searchType) {
case SearchType.NAME:
return getInferenceServiceDisplayName(project)
.toLowerCase()
.includes(search.toLowerCase());
default:
return true;
}
});
const filteredInferenceServices = unfilteredInferenceServices.filter((project) => {
if (!search) {
return true;
}

switch (searchType) {
case SearchType.NAME:
return getInferenceServiceDisplayName(project).toLowerCase().includes(search.toLowerCase());
default:
return true;
}
});

const resetFilters = () => {
setSearch('');
};

const showPagination = unfilteredInferenceServices.length > MIN_PAGE_SIZE;
const pagination = (pageDirection: 'up' | 'down') =>
showPagination && (
<Pagination
dropDirection={pageDirection}
perPageComponent="button"
itemCount={filteredInferenceServices.length}
perPage={pageSize}
page={page}
onSetPage={(e, newPage) => setPage(newPage)}
onPerPageSelect={(e, newSize, newPage) => {
setPageSize(newSize);
setPage(newPage);
}}
widgetId="table-pagination"
/>
);

const searchTypes = React.useMemo(
() => Object.keys(SearchType).filter((key) => SearchType[key] === SearchType.NAME),
[],
);

return (
<>
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<SearchField
types={searchTypes}
searchType={searchType}
searchValue={search}
onSearchTypeChange={(searchType) => {
setSearchType(searchType);
}}
onSearchValueChange={(searchValue) => {
setPage(1);
setSearch(searchValue);
}}
/>
</ToolbarItem>
<ToolbarItem>
<ServeModelButton />
</ToolbarItem>
<ToolbarItem variant="pagination" alignment={{ default: 'alignRight' }}>
{pagination('down')}
</ToolbarItem>
</ToolbarContent>
</Toolbar>
<InferenceServiceTable
clearFilters={resetFilters}
servingRuntimes={servingRuntimes}
inferenceServices={filteredInferenceServices.slice(pageSize * (page - 1), pageSize * page)}
getColumnSort={sortInferenceService.getColumnSort}
inferenceServices={filteredInferenceServices}
refresh={refresh}
/>
{showPagination && (
<Toolbar>
<ToolbarContent>
<ToolbarItem variant="pagination" alignment={{ default: 'alignRight' }}>
{pagination('up')}
enablePagination
toolbarContent={
<>
<ToolbarItem>
<SearchField
types={searchTypes}
searchType={searchType}
searchValue={search}
onSearchTypeChange={(searchType) => {
setSearchType(searchType);
}}
onSearchValueChange={(searchValue) => {
setSearch(searchValue);
}}
/>
</ToolbarItem>
</ToolbarContent>
</Toolbar>
)}
<ToolbarItem>
<ServeModelButton />
</ToolbarItem>
</>
}
/>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import * as React from 'react';
import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table';
import { Button } from '@patternfly/react-core';
import { GetColumnSort } from '~/utilities/useTableColumnSort';
import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes';
import ManageInferenceServiceModal from '~/pages/modelServing/screens/projects/InferenceServiceModal/ManageInferenceServiceModal';
import { ModelServingContext } from '~/pages/modelServing/ModelServingContext';
import { getGlobalInferenceServiceColumns, getProjectInferenceServiceColumns } from './data';
import Table from '~/components/Table';

import { InferenceServiceKind, ServingRuntimeKind } from '~/k8sTypes';
import InferenceServiceTableRow from './InferenceServiceTableRow';
import { getGlobalInferenceServiceColumns, getProjectInferenceServiceColumns } from './data';
import DeleteInferenceServiceModal from './DeleteInferenceServiceModal';

type InferenceServiceTableProps = {
andrewballantyne marked this conversation as resolved.
Show resolved Hide resolved
andrewballantyne marked this conversation as resolved.
Show resolved Hide resolved
clearFilters?: () => void;
inferenceServices: InferenceServiceKind[];
servingRuntimes: ServingRuntimeKind[];
getColumnSort: GetColumnSort;
refresh: () => void;
};
} & Partial<Pick<React.ComponentProps<typeof Table>, 'enablePagination' | 'toolbarContent'>>;

const InferenceServiceTable: React.FC<InferenceServiceTableProps> = ({
clearFilters,
inferenceServices,
servingRuntimes,
getColumnSort,
refresh,
enablePagination,
toolbarContent,
}) => {
const {
projects: { data: projects },
Expand All @@ -36,29 +36,23 @@ const InferenceServiceTable: React.FC<InferenceServiceTableProps> = ({
: getProjectInferenceServiceColumns();
return (
<>
<TableComposable variant={isGlobal ? undefined : 'compact'}>
<Thead>
<Tr>
{mappedColumns.map((col, i) => (
<Th key={col.field} sort={getColumnSort(i)} width={col.width}>
{col.label}
</Th>
))}
</Tr>
</Thead>
{isGlobal && inferenceServices.length === 0 && (
<Tbody>
<Tr>
<Td colSpan={mappedColumns.length} style={{ textAlign: 'center' }}>
No projects match your filters.{' '}
<Button variant="link" isInline onClick={clearFilters}>
Clear filters
</Button>
</Td>
</Tr>
</Tbody>
)}
{inferenceServices.map((is) => (
<Table
data={inferenceServices}
columns={mappedColumns}
variant={isGlobal ? undefined : 'compact'}
toolbarContent={toolbarContent}
enablePagination={enablePagination}
emptyTableView={
isGlobal ? (
<>
No projects match your filters.{' '}
<Button variant="link" isInline onClick={clearFilters}>
Clear filters
</Button>
</>
) : undefined
}
rowRenderer={(is) => (
<InferenceServiceTableRow
key={is.metadata.uid}
obj={is}
Expand All @@ -69,8 +63,8 @@ const InferenceServiceTable: React.FC<InferenceServiceTableProps> = ({
onDeleteInferenceService={setDeleteInferenceService}
onEditInferenceService={setEditInferenceService}
/>
))}
</TableComposable>
)}
/>
<DeleteInferenceServiceModal
inferenceService={deleteInferenceService}
onClose={(deleted) => {
Expand Down
Loading