Skip to content

Commit

Permalink
feat: support filter by metadata with string type (#9810)
Browse files Browse the repository at this point in the history
  • Loading branch information
keita-determined authored Aug 13, 2024
1 parent 9da5620 commit 92a7ff5
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 21 deletions.
9 changes: 8 additions & 1 deletion webui/react/src/components/FilterForm/TableFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface Props {
formStore: FilterFormStore;
isMobile?: boolean;
isOpenFilter: boolean;
projectId?: number;
onIsOpenFilterChange?: (value: boolean) => void;
}

Expand All @@ -25,6 +26,7 @@ const TableFilter = ({
formStore,
isMobile = false,
isOpenFilter,
projectId,
onIsOpenFilterChange,
}: Props): JSX.Element => {
const columns: V1ProjectColumn[] = Loadable.getOrElse([], loadableColumns).filter(
Expand Down Expand Up @@ -66,7 +68,12 @@ const TableFilter = ({
}
}}
onMouseMove={(e) => e.stopPropagation()}>
<FilterForm columns={columns} formStore={formStore} onHidePopOver={onHidePopOver} />
<FilterForm
columns={columns}
formStore={formStore}
projectId={projectId}
onHidePopOver={onHidePopOver}
/>
</div>
}
open={isOpenFilter}
Expand Down
99 changes: 80 additions & 19 deletions webui/react/src/components/FilterForm/components/FilterField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import DatePicker, { DatePickerProps } from 'hew/DatePicker';
import Icon from 'hew/Icon';
import Input from 'hew/Input';
import InputNumber from 'hew/InputNumber';
import InputSelect from 'hew/InputSelect';
import Select, { SelectProps, SelectValue } from 'hew/Select';
import { Loadable } from 'hew/utils/loadable';
import { Loadable, NotLoaded } from 'hew/utils/loadable';
import { Observable, useObservable } from 'micro-observables';
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useDrag, useDrop } from 'react-dnd';
import { debounce } from 'throttle-debounce';

Expand All @@ -26,7 +27,9 @@ import {
SEARCHER_TYPE,
SpecialColumnNames,
} from 'components/FilterForm/components/type';
import { V1ColumnType, V1ProjectColumn } from 'services/api-ts-sdk';
import { useAsync } from 'hooks/useAsync';
import { getMetadataValues } from 'services/api';
import { V1ColumnType, V1LocationType, V1ProjectColumn } from 'services/api-ts-sdk';
import clusterStore from 'stores/cluster';
import userStore from 'stores/users';
import { alphaNumericSorter } from 'utils/sort';
Expand All @@ -37,6 +40,12 @@ const debounceFunc = debounce(1000, (func: () => void) => {
func();
});

const COLUMN_TYPE = {
NormalColumnType: 'NormalColumnType',
SpecialColumnType: 'SpecialColumnType',
StringMetadataColumnType: 'StringMetadataColumn',
} as const;

interface Props {
index: number; // start from 0
field: FormField;
Expand All @@ -45,6 +54,7 @@ interface Props {
formStore: FilterFormStore;
level: number; // start from 0
columns: V1ProjectColumn[];
projectId?: number;
}

const FilterField = ({
Expand All @@ -55,12 +65,20 @@ const FilterField = ({
parentId,
level,
columns,
projectId,
}: Props): JSX.Element => {
const users = Loadable.getOrElse([], useObservable(userStore.getUsers()));
const resourcePools = Loadable.getOrElse([], useObservable(clusterStore.resourcePools));

const currentColumn = columns.find((c) => c.column === field.columnName);
const isSpecialColumn = (SpecialColumnNames as ReadonlyArray<string>).includes(field.columnName);

const columnType = useMemo(() => {
if (field.location === V1LocationType.RUNMETADATA && field.type === V1ColumnType.TEXT) {
return COLUMN_TYPE.StringMetadataColumnType;
} else if ((SpecialColumnNames as ReadonlyArray<string>).includes(field.columnName)) {
return COLUMN_TYPE.SpecialColumnType;
}
return COLUMN_TYPE.NormalColumnType;
}, [field.columnName, field.location, field.type]);

const [inputOpen, setInputOpen] = useState(false);
const [fieldValue, setFieldValue] = useState<FormFieldValue>(field.value);
Expand Down Expand Up @@ -98,6 +116,21 @@ const FilterField = ({
}
};

const metadataValues = useAsync(async () => {
try {
if (projectId !== undefined && columnType === COLUMN_TYPE.StringMetadataColumnType) {
const metadataValues = await getMetadataValues({
key: field.columnName.replace(/^metadata\./, ''),
projectId,
});
return metadataValues;
}
return [];
} catch (e) {
return NotLoaded;
}
}, [columnType, field.columnName, projectId]);

const getSpecialOptions = (columnName: SpecialColumnNames): SelectProps['options'] => {
switch (columnName) {
case 'resourcePool':
Expand Down Expand Up @@ -170,12 +203,16 @@ const FilterField = ({
if (e.key === 'Enter' && !inputOpen && !e.nativeEvent.isComposing && e.keyCode !== 229) {
formStore.addChild(parentId, FormKind.Field, { index: index + 1, item: getInitField() });
// stop panel flashing for selects and dates
if (field.type === 'COLUMN_TYPE_DATE' || isSpecialColumn) {
if (
field.type === 'COLUMN_TYPE_DATE' ||
columnType === COLUMN_TYPE.SpecialColumnType ||
columnType === COLUMN_TYPE.StringMetadataColumnType
) {
e.stopPropagation();
}
}
},
[field.type, formStore, index, inputOpen, isSpecialColumn, parentId],
[columnType, field.type, formStore, index, inputOpen, parentId],
);

return (
Expand Down Expand Up @@ -203,10 +240,16 @@ const FilterField = ({
/>
<Select
data-test="operator"
options={(isSpecialColumn
? [Operator.Eq, Operator.NotEq] // just Eq and NotEq for Special column
: AvailableOperators[currentColumn?.type ?? V1ColumnType.UNSPECIFIED]
).map((op) => ({
options={{
[COLUMN_TYPE.StringMetadataColumnType]: [
Operator.Contains,
Operator.Eq,
Operator.NotEq,
],
[COLUMN_TYPE.SpecialColumnType]: [Operator.Eq, Operator.NotEq],
[COLUMN_TYPE.NormalColumnType]:
AvailableOperators[currentColumn?.type ?? V1ColumnType.UNSPECIFIED],
}[columnType].map((op) => ({
label: ReadableOperator[field.type][op],
value: op,
}))}
Expand All @@ -222,20 +265,38 @@ const FilterField = ({
});
}}
/>
{isSpecialColumn ? (
<div onKeyDownCapture={captureEnterKeyDown}>
<Select
{columnType !== COLUMN_TYPE.NormalColumnType ? (
columnType === COLUMN_TYPE.StringMetadataColumnType ? (
// StringMetadataColumnType
<InputSelect
customFilter={(options, filterValue) =>
options.filter((opt) => opt.includes(filterValue))
}
data-test="special"
options={getSpecialOptions(field.columnName as SpecialColumnNames)}
value={fieldValue ?? undefined}
options={metadataValues.getOrElse([])}
value={typeof fieldValue === 'string' ? fieldValue : undefined}
width={'100%'}
onChange={(value) => {
const val = value?.toString() ?? null;
updateFieldValue(field.id, val);
updateFieldValue(field.id, value);
}}
onDropdownVisibleChange={setInputOpen}
/>
</div>
) : (
// SpecialColumnType
<div onKeyDownCapture={captureEnterKeyDown}>
<Select
data-test="special"
options={getSpecialOptions(field.columnName as SpecialColumnNames)}
value={fieldValue ?? undefined}
width={'100%'}
onChange={(value) => {
const val = value?.toString() ?? null;
updateFieldValue(field.id, val);
}}
onDropdownVisibleChange={setInputOpen}
/>
</div>
)
) : (
<>
{(currentColumn?.type === V1ColumnType.TEXT ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import css from './FilterForm.module.scss';
interface Props {
formStore: FilterFormStore;
columns: V1ProjectColumn[];
projectId?: number;
onHidePopOver: () => void;
}

const FilterForm = ({ formStore, columns, onHidePopOver }: Props): JSX.Element => {
const FilterForm = ({ formStore, columns, projectId, onHidePopOver }: Props): JSX.Element => {
const scrollBottomRef = useRef<HTMLDivElement>(null);
const loadableData = useObservable(formStore.formset);
const isButtonDisabled = Loadable.match(loadableData, {
Expand Down Expand Up @@ -58,6 +59,7 @@ const FilterForm = ({ formStore, columns, onHidePopOver }: Props): JSX.Element =
index={0}
level={0}
parentId={data.filterGroup.id}
projectId={projectId}
/>
<div ref={scrollBottomRef} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Props {
level: number; // start from 0
formStore: FilterFormStore;
columns: V1ProjectColumn[];
projectId?: number;
}

const FilterGroup = ({
Expand All @@ -30,6 +31,7 @@ const FilterGroup = ({
formStore,
parentId,
columns,
projectId,
}: Props): JSX.Element => {
const scrollBottomRef = useRef<HTMLDivElement>(null);
const [, drag, preview] = useDrag<{ form: FormGroup; index: number }, unknown, unknown>(
Expand Down Expand Up @@ -202,6 +204,7 @@ const FilterGroup = ({
key={child.id}
level={level + 1}
parentId={group.id}
projectId={projectId}
/>
);
}
Expand Down
1 change: 1 addition & 0 deletions webui/react/src/pages/FlatRuns/FlatRuns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ const FlatRuns: React.FC<Props> = ({ projectId, workspaceId, searchId }) => {
isMobile={isMobile}
isOpenFilter={isOpenFilter}
loadableColumns={projectColumns}
projectId={projectId}
onIsOpenFilterChange={handleIsOpenFilterChange}
/>
<MultiSortMenu
Expand Down
6 changes: 6 additions & 0 deletions webui/react/src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,12 @@ export const getProjectNumericMetricsRange = generateDetApi<
Type.ProjectMetricsRange[]
>(Config.getProjectNumericMetricsRange);

export const getMetadataValues = generateDetApi<
Service.GetMetadataValuesParams,
Api.V1GetMetadataValuesResponse,
string[]
>(Config.getMetadataValues);

/* Runs */

export const searchRuns = generateDetApi<
Expand Down
10 changes: 10 additions & 0 deletions webui/react/src/services/apiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,16 @@ export const getProjectNumericMetricsRange: DetApi<
request: (params) => detApi.Internal.getProjectNumericMetricsRange(params.id),
};

export const getMetadataValues: DetApi<
Service.GetMetadataValuesParams,
Api.V1GetMetadataValuesResponse,
string[]
> = {
name: 'getMetadataValues',
postProcess: (response) => response.values ?? [],
request: (params) => detApi.Internal.getMetadataValues(params.projectId, params.key),
};

/* Tasks */

const TASK_LIMIT = 1000;
Expand Down
5 changes: 5 additions & 0 deletions webui/react/src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,11 @@ export interface GetProjectNumericMetricsRangeParams {
id: number;
}

export interface GetMetadataValuesParams {
projectId: number;
key: string;
}

export interface ActionWorkspaceParams {
workspaceId: number;
}
Expand Down

0 comments on commit 92a7ff5

Please sign in to comment.