Skip to content

Commit

Permalink
Model Serving UI Improvements (opendatahub-io#937)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewballantyne authored and lucferbux committed Feb 13, 2023
1 parent 9b79c54 commit ba47d4e
Show file tree
Hide file tree
Showing 30 changed files with 596 additions and 403 deletions.
14 changes: 14 additions & 0 deletions frontend/src/components/CPUField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';
import ValueUnitField from './ValueUnitField';
import { CPU_UNITS } from '../utilities/valueUnits';

type CPUFieldProps = {
onChange: (newValue: string) => void;
value?: string;
};

const CPUField: React.FC<CPUFieldProps> = ({ onChange, value = '1' }) => {
return <ValueUnitField min={1} onChange={onChange} options={CPU_UNITS} value={value} />;
};

export default CPUField;
14 changes: 14 additions & 0 deletions frontend/src/components/MemoryField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react';
import ValueUnitField from './ValueUnitField';
import { MEMORY_UNITS } from '../utilities/valueUnits';

type MemoryFieldProps = {
onChange: (newValue: string) => void;
value?: string;
};

const MemoryField: React.FC<MemoryFieldProps> = ({ onChange, value = '1Gi' }) => {
return <ValueUnitField min={1} onChange={onChange} options={MEMORY_UNITS} value={value} />;
};

export default MemoryField;
27 changes: 27 additions & 0 deletions frontend/src/components/NumberInputWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { NumberInput } from '@patternfly/react-core';

type NumberInputWrapperProps = {
onChange: (newValue: number) => void;
value: number;
} & Omit<React.ComponentProps<typeof NumberInput>, 'onChange' | 'value' | 'onPlus' | 'onMinus'>;

const NumberInputWrapper: React.FC<NumberInputWrapperProps> = ({
onChange,
value,
...otherProps
}) => {
return (
<NumberInput
{...otherProps}
value={value}
onChange={(e) => {
onChange(parseInt(e.currentTarget.value));
}}
onPlus={() => onChange(value + 1)}
onMinus={() => onChange(value - 1)}
/>
);
};

export default NumberInputWrapper;
15 changes: 15 additions & 0 deletions frontend/src/components/ScrollViewOnMount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';

const ScrollViewOnMount: React.FC = () => {
return (
<div
ref={(elm) => {
if (elm) {
elm.scrollIntoView();
}
}}
/>
);
};

export default ScrollViewOnMount;
63 changes: 63 additions & 0 deletions frontend/src/components/ValueUnitField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';
import { Dropdown, DropdownItem, DropdownToggle, Split, SplitItem } from '@patternfly/react-core';
import NumberInputWrapper from './NumberInputWrapper';
import { splitValueUnit, UnitOption, ValueUnitString } from '../utilities/valueUnits';

type ValueUnitFieldProps = {
/** @defaults to unlimited pos/neg */
min?: number;
/** @defaults to unlimited pos/neg */
max?: number;
onChange: (newValue: string) => void;
options: UnitOption[];
value: ValueUnitString;
};

const ValueUnitField: React.FC<ValueUnitFieldProps> = ({
min,
max,
onChange,
options,
value: fullValue,
}) => {
const [open, setOpen] = React.useState(false);
const [currentValue, currentUnitOption] = splitValueUnit(fullValue, options);

return (
<Split hasGutter>
<SplitItem>
<NumberInputWrapper
min={min}
max={max}
value={currentValue}
onChange={(value) => {
onChange(`${value || min}${currentUnitOption.unit}`);
}}
/>
</SplitItem>
<SplitItem>
<Dropdown
toggle={
<DropdownToggle id="toggle-basic" onToggle={() => setOpen(!open)}>
{currentUnitOption.name}
</DropdownToggle>
}
isOpen={open}
dropdownItems={options.map((option) => (
<DropdownItem
key={option.unit}
onClick={() => {
onChange(`${currentValue}${option.unit}`);
setOpen(false);
}}
>
{option.name}
</DropdownItem>
))}
/>
</SplitItem>
</Split>
);
};

export default ValueUnitField;
11 changes: 9 additions & 2 deletions frontend/src/pages/modelServing/ModelServingContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import { ServingRuntimeKind, InferenceServiceKind } from '../../k8sTypes';
import { ServingRuntimeKind, InferenceServiceKind, ProjectKind } from '../../k8sTypes';
import { Outlet, useParams } from 'react-router-dom';
import {
Bullseye,
Expand All @@ -17,33 +17,39 @@ import useServingRuntimes from './useServingRuntimes';
import useInferenceServices from './useInferenceServices';
import { ContextResourceData } from '../../types';
import { useContextResourceData } from '../../utilities/useContextResourceData';
import useUserProjects from '../projects/screens/projects/useUserProjects';

type ModelServingContextType = {
refreshAllData: () => void;
servingRuntimes: ContextResourceData<ServingRuntimeKind>;
inferenceServices: ContextResourceData<InferenceServiceKind>;
projects: ContextResourceData<ProjectKind>;
};

export const ModelServingContext = React.createContext<ModelServingContextType>({
refreshAllData: () => undefined,
servingRuntimes: DEFAULT_CONTEXT_DATA,
inferenceServices: DEFAULT_CONTEXT_DATA,
projects: DEFAULT_CONTEXT_DATA,
});

const ModelServingContextProvider: React.FC = () => {
const navigate = useNavigate();
const { namespace } = useParams<{ namespace: string }>();
const projects = useContextResourceData<ProjectKind>(useUserProjects());
const servingRuntimes = useContextResourceData<ServingRuntimeKind>(useServingRuntimes(namespace));
const inferenceServices = useContextResourceData<InferenceServiceKind>(
useInferenceServices(namespace),
);

const projectRefresh = projects.refresh;
const servingRuntimeRefresh = servingRuntimes.refresh;
const inferenceServiceRefresh = inferenceServices.refresh;
const refreshAllData = React.useCallback(() => {
projectRefresh();
servingRuntimeRefresh();
inferenceServiceRefresh();
}, [servingRuntimeRefresh, inferenceServiceRefresh]);
}, [projectRefresh, servingRuntimeRefresh, inferenceServiceRefresh]);

if (servingRuntimes.error || inferenceServices.error) {
return (
Expand Down Expand Up @@ -75,6 +81,7 @@ const ModelServingContextProvider: React.FC = () => {
return (
<ModelServingContext.Provider
value={{
projects,
servingRuntimes,
inferenceServices,
refreshAllData,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { InferenceServiceKind, ServingRuntimeKind } from '../../../../k8sTypes';
import useTableColumnSort from '../../../../utilities/useTableColumnSort';
import { getInferenceServiceDisplayName } from './utils';
import ServeModelButton from './ServeModelButton';
import { inferenceServiceColumns } from './data';
import { getGlobalInferenceServiceColumns } from './data';
import SearchField, { SearchType } from '../../../projects/components/SearchField';
import InferenceServiceTable from './InferenceServiceTable';
import { ModelServingContext } from '../../ModelServingContext';
Expand All @@ -22,12 +22,16 @@ 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>(inferenceServiceColumns, 0);
const sortInferenceService = useTableColumnSort<InferenceServiceKind>(
getGlobalInferenceServiceColumns(projects),
0,
);
const filteredInferenceServices = sortInferenceService
.transformData(unfilteredInferenceServices)
.filter((project) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,29 @@ import * as React from 'react';
import { HelperText, HelperTextItem, Skeleton } from '@patternfly/react-core';
import { InferenceServiceKind } from '../../../../k8sTypes';
import { getProjectDisplayName } from '../../../projects/utils';
import useConnectedProject from './useConnectedProject';
import { ModelServingContext } from '../../ModelServingContext';

type InferenceServiceProjectProps = {
inferenceService: InferenceServiceKind;
};

const InferenceServiceProject: React.FC<InferenceServiceProjectProps> = ({ inferenceService }) => {
const [project, loaded, loadError] = useConnectedProject(inferenceService.metadata.namespace);
const {
projects: { data: projects, loaded, error },
} = React.useContext(ModelServingContext);
const project = projects.find(
({ metadata: { name } }) => name === inferenceService.metadata.namespace,
);

if (!loaded) {
return <Skeleton />;
}

if (loadError) {
if (error) {
return (
<HelperText>
<HelperTextItem variant="warning" hasIcon>
Failed to get project for this deployed model. {loadError.message}
Failed to get project for this deployed model. {error.message}
</HelperTextItem>
</HelperText>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { TableComposable, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-tab
import { Button } from '@patternfly/react-core';
import { GetColumnSort } from '../../../../utilities/useTableColumnSort';
import { InferenceServiceKind, ServingRuntimeKind } from '../../../../k8sTypes';
import { inferenceServiceColumns } from './data';
import { getGlobalInferenceServiceColumns, getProjectInferenceServiceColumns } from './data';
import InferenceServiceTableRow from './InferenceServiceTableRow';
import DeleteInferenceServiceModal from './DeleteInferenceServiceModal';
import ManageInferenceServiceModal from '../projects/InferenceServiceModal/ManageInferenceServiceModal';
import { ModelServingContext } from '../../ModelServingContext';

type InferenceServiceTableProps = {
clearFilters?: () => void;
Expand All @@ -23,13 +24,16 @@ const InferenceServiceTable: React.FC<InferenceServiceTableProps> = ({
getColumnSort,
refresh,
}) => {
const {
projects: { data: projects },
} = React.useContext(ModelServingContext);
const [deleteInferenceService, setDeleteInferenceService] =
React.useState<InferenceServiceKind>();
const [editInferenceService, setEditInferenceService] = React.useState<InferenceServiceKind>();
const isGlobal = !!clearFilters;
const mappedColumns = isGlobal
? inferenceServiceColumns
: inferenceServiceColumns.filter((column) => column.field !== 'project');
? getGlobalInferenceServiceColumns(projects)
: getProjectInferenceServiceColumns();
return (
<>
<TableComposable variant={isGlobal ? undefined : 'compact'}>
Expand All @@ -45,7 +49,7 @@ const InferenceServiceTable: React.FC<InferenceServiceTableProps> = ({
{isGlobal && inferenceServices.length === 0 && (
<Tbody>
<Tr>
<Td colSpan={inferenceServiceColumns.length} style={{ textAlign: 'center' }}>
<Td colSpan={mappedColumns.length} style={{ textAlign: 'center' }}>
No projects match your filters.{' '}
<Button variant="link" isInline onClick={clearFilters}>
Clear filters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ModelServingGlobal: React.FC = () => {

return (
<ApplicationsPage
title="Deployed models"
title="Model serving"
description="Manage and view the health and performance of your deployed models."
loaded // already checked this in the context provider so loaded is always true here
empty={servingRuntimes.length === 0 || inferenceServices.length === 0}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const ServeModelButton: React.FC = () => {
return (
<>
<Button variant="primary" onClick={() => setOpen(true)}>
Serve model
Deploy model
</Button>
<ManageInferenceServiceModal
isOpen={open}
Expand Down

0 comments on commit ba47d4e

Please sign in to comment.