Skip to content

Commit

Permalink
Adds multiple model selection to to metrics page (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcreasy committed Aug 23, 2023
1 parent 4bd9b7d commit 1e5712e
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 6 deletions.
154 changes: 154 additions & 0 deletions src/app/components/ContextSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from 'react';
import {
MenuToggle,
MenuFooter,
MenuSearch,
MenuSearchInput,
Divider,
InputGroup,
InputGroupItem,
Button,
ButtonVariant,
SearchInput,
Dropdown,
DropdownList,
DropdownItem,
} from '@patternfly/react-core';
import SearchIcon from '@patternfly/react-icons/dist/esm/icons/search-icon';

type ItemData = {
text: string;
href?: string;
isDisabled?: boolean | undefined;
};

type ItemArrayType = (ItemData | string)[];

export enum ContextSelectorVariants {
DEFAULT = 'default',
PLAIN = 'plain',
PRIMARY = 'primary',
PLAIN_TEXT = 'plainText',
SECONDARY = 'secondary',
TYPEAHEAD = 'typeahead',
}

type ContextSelectorProps = {
items: ItemArrayType;
onSelect: (itemId: string) => void;
label?: string;
variant?: ContextSelectorVariants;
footer?: React.ReactNode;
};
const ContextSelector: React.FC<ContextSelectorProps> = ({
items,
onSelect: selectCallback,
label,
variant = ContextSelectorVariants.DEFAULT,
footer,
}) => {
const [isOpen, setIsOpen] = React.useState<boolean>(false);
const [selected, setSelected] = React.useState(typeof items[0] === 'string' ? items[0] : items[0].text);
const [filteredItems, setFilteredItems] = React.useState<ItemArrayType>(items);
const [searchInputValue, setSearchInputValue] = React.useState<string>('');
const menuRef = React.useRef<HTMLDivElement>(null);

const onToggleClick = () => {
setIsOpen(!isOpen);
};

const onSelect = (ev: React.MouseEvent<Element, MouseEvent> | undefined, itemId: string | number | undefined) => {
if (typeof itemId === 'number' || typeof itemId === 'undefined') {
return;
}
setSelected(itemId.toString());
selectCallback(itemId.toString());
setIsOpen(!isOpen);
};

const onSearchInputChange = (value: string) => {
setSearchInputValue(value);
};

const onSearchButtonClick = () => {
const filtered =
searchInputValue === ''
? items
: items.filter((item) => {
const str = typeof item === 'string' ? item : item.text;
return str.toLowerCase().indexOf(searchInputValue.toLowerCase()) !== -1;
});

setFilteredItems(filtered || []);
setIsOpen(true); // Keep menu open after search executed
};

const onEnterPressed = (event: React.KeyboardEvent) => {
if (event.key === 'Enter') {
onSearchButtonClick();
}
};

return (
<Dropdown
isOpen={isOpen}
onOpenChange={(isOpen) => setIsOpen(isOpen)}
onOpenChangeKeys={['Escape']}
toggle={(toggleRef) => (
<MenuToggle ref={toggleRef} onClick={onToggleClick} isExpanded={isOpen} variant={variant}>
{label && label + ': '} {selected}
</MenuToggle>
)}
ref={menuRef}
id="context-selector"
onSelect={onSelect}
isScrollable
>
<MenuSearch>
<MenuSearchInput>
<InputGroup>
<InputGroupItem isFill>
<SearchInput
value={searchInputValue}
placeholder="Search"
onChange={(_event, value) => onSearchInputChange(value)}
onKeyPress={onEnterPressed}
aria-labelledby="pf-v5-context-selector-search-button-id-1"
/>
</InputGroupItem>
<InputGroupItem>
<Button
variant={ButtonVariant.control}
aria-label="Search menu items"
id="pf-v5-context-selector-search-button-id-1"
onClick={onSearchButtonClick}
>
<SearchIcon aria-hidden="true" />
</Button>
</InputGroupItem>
</InputGroup>
</MenuSearchInput>
</MenuSearch>
<Divider />
<DropdownList>
{filteredItems.map((item, index) => {
const [itemText, isDisabled, href] =
typeof item === 'string' ? [item, null, null] : [item.text, item.isDisabled || null, item.href || null];
return (
<DropdownItem
itemId={itemText}
key={index}
isDisabled={isDisabled as boolean | undefined}
to={href as string | undefined}
>
{itemText}
</DropdownItem>
);
})}
</DropdownList>
{footer && <MenuFooter>{footer}</MenuFooter>}
</Dropdown>
);
};

export default ContextSelector;
68 changes: 62 additions & 6 deletions src/app/pages/Metrics/MetricsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,76 @@
import React from 'react';
import { Card, CardBody, CardTitle, Gallery, PageSection, Title } from '@patternfly/react-core';
import { Bullseye, Card, CardBody, CardTitle, Gallery, PageSection, Spinner, Title } from '@patternfly/react-core';
import PageLayout from '@app/pages/PageLayout';
import { useGetAllRequests, useGetModelMetadata } from '@app/integrations/trustyai-service/api/hooks';
import RequestsTable from '@app/pages/Metrics/RequestsTable';
import ContextSelector, { ContextSelectorVariants } from '@app/components/ContextSelector';
import { BaseMetricResponse, ModelMetaData } from '@app/integrations/trustyai-service/api/types';

const MetricsPage: React.FC = () => {
const MetricsPage = () => {
const [modelId, setModelId] = React.useState<string>();
const { data, loaded, error } = useGetModelMetadata();
const { data: requestData, loaded: requestDataLoaded } = useGetAllRequests();
const [model, setModel] = React.useState<ModelMetaData>();
const [models, setModels] = React.useState<string[]>([]);
const [requests, setRequests] = React.useState<BaseMetricResponse[]>([]);

const { data: requestData } = useGetAllRequests();
const getModelById = React.useCallback(
(id: string) => {
const modelVal = data.find((m) => m.data.modelId === id);
if (!modelVal) {
// This shouldn't happen, something is wrong if we're here.
throw new Error(`No such model with id: ${id}`);
}
return modelVal;
},
[data],
);

React.useEffect(() => {
if (loaded && requestDataLoaded) {
if (!modelId) {
setModelId(data[0]?.data?.modelId);
}

if (modelId) {
setModel(getModelById(modelId));
setRequests(requestData.requests.filter((r) => r.request.modelId === model?.data.modelId));
setModels(data.map((r) => r.data.modelId));
}
}
}, [data, getModelById, loaded, model?.data.modelId, modelId, requestData.requests, requestDataLoaded]);

const model = data[0];
if (!loaded || !requestDataLoaded) {
return (
<Bullseye>
<Spinner />
</Bullseye>
);
}

const requests = requestData.requests;
if (!modelId || models.length === 0) {
return (
<Bullseye>
<Title headingLevel="h1">Error - no model selected</Title>
</Bullseye>
);
}

return (
<PageLayout title="Metrics" loaded={loaded} error={error}>
<PageLayout
title={
<ContextSelector
label="Model"
variant={ContextSelectorVariants.PLAIN_TEXT}
items={models}
onSelect={(modelId) => {
setModelId(modelId);
}}
/>
}
loaded={loaded && requestDataLoaded}
error={error}
>
<PageSection>
<Gallery hasGutter style={{ '--pf-l-gallery--GridTemplateColumns--min': '260px' } as never}>
<Card>
Expand Down
8 changes: 8 additions & 0 deletions src/app/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const byId =
<T extends { id: string | number }, U extends T | T['id']>(arg: U) =>
(arg2: T) => {
if (typeof arg === 'object') {
return arg2.id === arg.id;
}
return arg2.id === arg;
};

0 comments on commit 1e5712e

Please sign in to comment.