Skip to content

Commit

Permalink
feat: support filter in flat run table (#9250)
Browse files Browse the repository at this point in the history
  • Loading branch information
keita-determined authored May 6, 2024
1 parent a76c549 commit fdaa015
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 52 deletions.
6 changes: 5 additions & 1 deletion webui/react/src/components/FilterForm/TableFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { V1ProjectColumn } from 'services/api-ts-sdk';

interface Props {
loadableColumns: Loadable<V1ProjectColumn[]>;
bannedFilterColumns: Set<string>;
formStore: FilterFormStore;
isMobile?: boolean;
isOpenFilter: boolean;
Expand All @@ -20,12 +21,15 @@ interface Props {

const TableFilter = ({
loadableColumns,
bannedFilterColumns,
formStore,
isMobile = false,
isOpenFilter,
onIsOpenFilterChange,
}: Props): JSX.Element => {
const columns: V1ProjectColumn[] = Loadable.getOrElse([], loadableColumns);
const columns: V1ProjectColumn[] = Loadable.getOrElse([], loadableColumns).filter(
(column) => !bannedFilterColumns.has(column.column),
);
const fieldCount = useObservable(formStore.fieldCount);
const formset = useObservable(formStore.formset);

Expand Down
2 changes: 2 additions & 0 deletions webui/react/src/components/TableActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import MultiSortMenu from 'components/MultiSortMenu';
import { OptionsMenu, RowHeight, TableViewMode } from 'components/OptionsMenu';
import useMobile from 'hooks/useMobile';
import usePermissions from 'hooks/usePermissions';
import { BANNED_FILTER_COLUMNS } from 'pages/F_ExpList/F_ExperimentList';
import {
activateExperiments,
archiveExperiments,
Expand Down Expand Up @@ -394,6 +395,7 @@ const TableActionBar: React.FC<Props> = ({
<Column>
<Row>
<TableFilter
bannedFilterColumns={BANNED_FILTER_COLUMNS}
formStore={formStore}
isMobile={isMobile}
isOpenFilter={isOpenFilter}
Expand Down
5 changes: 3 additions & 2 deletions webui/react/src/pages/F_ExpList/F_ExperimentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ type ExperimentWithIndex = { index: number; experiment: BulkExperimentItem };

const NO_PINS_WIDTH = 200;

export const BANNED_FILTER_COLUMNS = new Set(['searcherMetricsVal']);

const makeSortString = (sorts: ValidSort[]): string =>
sorts.map((s) => `${s.column}=${s.direction}`).join(',');

Expand Down Expand Up @@ -945,7 +947,6 @@ const F_ExperimentList: React.FC<Props> = ({ project }) => {

const filterCount = formStore.getFieldCount(column.column).get();

const BANNED_FILTER_COLUMNS = ['searcherMetricsVal'];
const loadableFormset = formStore.formset.get();
const filterMenuItemsForColumn = () => {
const isSpecialColumn = (SpecialColumnNames as ReadonlyArray<string>).includes(column.column);
Expand Down Expand Up @@ -1014,7 +1015,7 @@ const F_ExperimentList: React.FC<Props> = ({ project }) => {
},
},
{ type: 'divider' as const },
...(BANNED_FILTER_COLUMNS.includes(column.column)
...(BANNED_FILTER_COLUMNS.has(column.column)
? []
: [
...sortMenuItemsForColumn(column, sorts, handleSortChange),
Expand Down
23 changes: 13 additions & 10 deletions webui/react/src/pages/FlatRuns/FlatRuns.settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ import { DEFAULT_SELECTION, SelectionType } from 'pages/F_ExpList/F_ExperimentLi
import { defaultColumnWidths, defaultRunColumns } from './columns';

// have to intersect with an empty object bc of settings store type issue
export const FlatRunsSettings = t.type({
columns: t.array(t.string),
columnWidths: t.record(t.string, t.number),
compare: t.boolean,
filterset: t.string, // save FilterFormSet as string
pageLimit: t.number,
pinnedColumnsCount: t.number,
selection: SelectionType,
sortString: t.string,
});
export const FlatRunsSettings = t.intersection([
t.type({}),
t.partial({
columns: t.array(t.string),
columnWidths: t.record(t.string, t.number),
compare: t.boolean,
filterset: t.string, // save FilterFormSet as string
pageLimit: t.number,
pinnedColumnsCount: t.number,
selection: SelectionType,
sortString: t.string,
}),
]);
export type FlatRunsSettings = t.TypeOf<typeof FlatRunsSettings>;

export const defaultFlatRunsSettings: Required<FlatRunsSettings> = {
Expand Down
167 changes: 128 additions & 39 deletions webui/react/src/pages/FlatRuns/FlatRuns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,18 @@ import { Loadable, Loaded, NotLoaded } from 'hew/utils/loadable';
import { useObservable } from 'micro-observables';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid';

import { Error } from 'components/exceptions';
import { FilterFormStore } from 'components/FilterForm/components/FilterFormStore';
import { IOFilterFormSet } from 'components/FilterForm/components/type';
import { FilterFormStore, ROOT_ID } from 'components/FilterForm/components/FilterFormStore';
import {
AvailableOperators,
FormKind,
IOFilterFormSet,
Operator,
SpecialColumnNames,
} from 'components/FilterForm/components/type';
import TableFilter from 'components/FilterForm/TableFilter';
import { EMPTY_SORT, sortMenuItemsForColumn } from 'components/MultiSortMenu';
import { RowHeight } from 'components/OptionsMenu';
import {
Expand All @@ -58,6 +66,8 @@ import userStore from 'stores/users';
import userSettings from 'stores/userSettings';
import { DetailedUser, ExperimentAction, FlatRun, Project, ProjectColumn } from 'types';
import handleError from 'utils/error';
import { eagerSubscribe } from 'utils/observable';
import { pluralizer } from 'utils/string';

import { defaultColumnWidths, getColumnDefs, RunColumn, runColumns } from './columns';
import css from './FlatRuns.module.scss';
Expand All @@ -72,6 +82,8 @@ const INITIAL_LOADING_RUNS: Loadable<FlatRun>[] = new Array(PAGE_SIZE).fill(NotL

const STATIC_COLUMNS = [MULTISELECT];

const BANNED_FILTER_COLUMNS = new Set(['searcherMetricsVal']);

const formStore = new FilterFormStore();

interface Props {
Expand Down Expand Up @@ -119,6 +131,7 @@ const FlatRuns: React.FC<Props> = ({ project }) => {

const { settings: globalSettings } = useSettings<DataGridGlobalSettings>(settingsConfigGlobal);

const [isOpenFilter, setIsOpenFilter] = useState<boolean>(false);
const [runs, setRuns] = useState<Loadable<FlatRun>[]>(INITIAL_LOADING_RUNS);
const isPagedView = true;
const [page, setPage] = useState(() =>
Expand All @@ -133,6 +146,7 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
});
const sortString = useMemo(() => makeSortString(sorts.filter(validSort.is)), [sorts]);
const loadableFormset = useObservable(formStore.formset);
const filtersString = useObservable(formStore.asJsonString);
const [total, setTotal] = useState<Loadable<number>>(NotLoaded);
const isMobile = useMobile();
const [isLoading, setIsLoading] = useState(true);
Expand Down Expand Up @@ -202,6 +216,13 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
};
}, [loadedSelectedRunIds]);

const handleIsOpenFilterChange = useCallback((newOpen: boolean) => {
setIsOpenFilter(newOpen);
if (!newOpen) {
formStore.sweep();
}
}, []);

const colorMap = useGlasbey([...loadedSelectedRunIds.keys()]);

const columns: ColumnDef<FlatRun>[] = useMemo(() => {
Expand Down Expand Up @@ -328,7 +349,7 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
const tableOffset = Math.max((page - 0.5) * PAGE_SIZE, 0);
const response = await searchRuns(
{
//filter: filtersString,
filter: filtersString,
limit: isPagedView ? settings.pageLimit : 2 * PAGE_SIZE,
offset: isPagedView ? page * settings.pageLimit : tableOffset,
projectId: project.id,
Expand Down Expand Up @@ -360,6 +381,7 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
}
}, [
canceler.signal,
filtersString,
isLoadingSettings,
isPagedView,
loadableFormset,
Expand Down Expand Up @@ -403,19 +425,38 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
}, [page]);

useEffect(() => {
// useSettings load the default value first, and then load the data from DB
// use this useEffect to re-init the correct useSettings value when settings.filterset is changed
if (isLoadingSettings) return;
const formSetValidation = IOFilterFormSet.decode(JSON.parse(settings.filterset));
if (isLeft(formSetValidation)) {
handleError(formSetValidation.left, {
publicSubject: 'Unable to initialize filterset from settings',
});
} else {
const formset = formSetValidation.right;
formStore.init(formset);
}
}, [settings.filterset, isLoadingSettings]);
let cleanup: () => void;
// eagerSubscribe is like subscribe but it runs once before the observed value changes.
cleanup = eagerSubscribe(flatRunsSettingsObs, (ps, prevPs) => {
// init formset once from settings when loaded, then flip the sync
// direction -- when formset changes, update settings
if (!prevPs?.isLoaded) {
ps.forEach((s) => {
cleanup?.();
if (!s?.filterset) {
formStore.init();
} else {
const formSetValidation = IOFilterFormSet.decode(JSON.parse(s.filterset));
if (isLeft(formSetValidation)) {
handleError(formSetValidation.left, {
publicSubject: 'Unable to initialize filterset from settings',
});
} else {
formStore.init(formSetValidation.right);
}
}
cleanup = formStore.asJsonString.subscribe(() => {
resetPagination();
const loadableFormset = formStore.formset.get();
Loadable.forEach(loadableFormset, (formSet) =>
updateSettings({ filterset: JSON.stringify(formSet), selection: DEFAULT_SELECTION }),
);
});
});
}
});
return () => cleanup?.();
}, [flatRunsSettingsObs, resetPagination, updateSettings]);

const handleColumnWidthChange = useCallback(
(columnId: string, width: number) => {
Expand Down Expand Up @@ -554,6 +595,7 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
return items;
}

const column = Loadable.getOrElse([], projectColumns).find((c) => c.column === columnId);
const isPinned = colIdx <= settings.pinnedColumnsCount + STATIC_COLUMNS.length - 1;
const items: MenuItem[] = [
// Column is pinned if the index is inside of the frozen columns
Expand Down Expand Up @@ -586,20 +628,72 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
},
},
];
const column = Loadable.getOrElse([], projectColumns).find((c) => c.column === columnId);
if (!column) return items;

const BANNED_FILTER_COLUMNS = ['searcherMetricsVal'];
const sortOptions = sortMenuItemsForColumn(column, sorts, handleSortChange);
if (sortOptions.length > 0) {
items.push(
...(BANNED_FILTER_COLUMNS.includes(column.column)
if (!column) {
return items;
}

const filterMenuItemsForColumn = () => {
const isSpecialColumn = (SpecialColumnNames as ReadonlyArray<string>).includes(
column.column,
);
formStore.addChild(ROOT_ID, FormKind.Field, {
index: Loadable.match(loadableFormset, {
_: () => 0,
Loaded: (formset) => formset.filterGroup.children.length,
}),
item: {
columnName: column.column,
id: uuidv4(),
kind: FormKind.Field,
location: column.location,
operator: isSpecialColumn ? Operator.Eq : AvailableOperators[column.type][0],
type: column.type,
value: null,
},
});
handleIsOpenFilterChange?.(true);
};

const clearFilterForColumn = () => {
formStore.removeByField(column.column);
};

const filterCount = formStore.getFieldCount(column.column).get();

if (!BANNED_FILTER_COLUMNS.has(column.column)) {
const sortCount = sortMenuItemsForColumn(column, sorts, handleSortChange).length;
const sortMenuItems =
sortCount === 0
? []
: [
{ type: 'divider' as const },
...sortMenuItemsForColumn(column, sorts, handleSortChange),
]),
];

items.push(
...sortMenuItems,
{ type: 'divider' as const },
{
icon: <Icon decorative name="filter" />,
key: 'filter',
label: 'Add Filter',
onClick: () => {
setTimeout(filterMenuItemsForColumn, 5);
},
},
);

if (filterCount > 0) {
items.push({
icon: <Icon decorative name="filter" />,
key: 'filter-clear',
label: `Clear ${pluralizer(filterCount, 'Filter')} (${filterCount})`,
onClick: () => {
setTimeout(clearFilterForColumn, 5);
},
});
}
}
return items;
},
Expand All @@ -610,6 +704,8 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
handleSelectionChange,
handleSortChange,
isMobile,
loadableFormset,
handleIsOpenFilterChange,
projectColumns,
settings.pinnedColumnsCount,
sorts,
Expand All @@ -618,21 +714,6 @@ const FlatRuns: React.FC<Props> = ({ project }) => {
],
);

useEffect(
() =>
formStore.asJsonString.subscribe(() => {
resetPagination();
const loadableFormset = formStore.formset.get();
Loadable.forEach(loadableFormset, (formSet) =>
updateSettings({
filterset: JSON.stringify(formSet),
selection: DEFAULT_SELECTION,
}),
);
}),
[resetPagination, updateSettings],
);

useEffect(() => {
return () => {
canceler.abort();
Expand All @@ -642,6 +723,14 @@ const FlatRuns: React.FC<Props> = ({ project }) => {

return (
<div className={css.content} ref={contentRef}>
<TableFilter
bannedFilterColumns={BANNED_FILTER_COLUMNS}
formStore={formStore}
isMobile={isMobile}
isOpenFilter={isOpenFilter}
loadableColumns={projectColumns}
onIsOpenFilterChange={handleIsOpenFilterChange}
/>
{!isLoading && total.isLoaded && total.data === 0 ? (
numFilters === 0 ? (
<Message
Expand Down

0 comments on commit fdaa015

Please sign in to comment.