diff --git a/webui/react/src/components/ColumnPickerMenu.tsx b/webui/react/src/components/ColumnPickerMenu.tsx index bce59645c52..ec0735cf07f 100644 --- a/webui/react/src/components/ColumnPickerMenu.tsx +++ b/webui/react/src/components/ColumnPickerMenu.tsx @@ -1,3 +1,4 @@ +import Badge from 'hew/Badge'; import Button from 'hew/Button'; import Checkbox, { CheckboxChangeEvent } from 'hew/Checkbox'; import Dropdown from 'hew/Dropdown'; @@ -10,9 +11,10 @@ import { Loadable } from 'hew/utils/loadable'; import React, { ChangeEvent, useCallback, useMemo, useState } from 'react'; import { FixedSizeList as List } from 'react-window'; -import { V1LocationType } from 'services/api-ts-sdk'; +import { V1ColumnType, V1LocationType } from 'services/api-ts-sdk'; import { ProjectColumn } from 'types'; import { ensureArray } from 'utils/data'; +import { formatColumnKey, removeColumnTypePrefix } from 'utils/flatRun'; import css from './ColumnPickerMenu.module.scss'; @@ -63,6 +65,8 @@ interface ColumnTabProps { onHeatmapSelectionRemove?: (id: string) => void; } +const KNOWN_BOOLEAN_COLUMNS = ['archived', 'isExpMultitrial', 'parentArchived']; + const ColumnPickerTab: React.FC = ({ columnState, compare, @@ -75,7 +79,7 @@ const ColumnPickerTab: React.FC = ({ onVisibleColumnChange, onHeatmapSelectionRemove, }) => { - const checkedColumns = useMemo( + const checkedColumnNames = useMemo( () => (compare ? new Set(columnState.slice(0, pinnedColumnsCount)) : new Set(columnState)), [columnState, compare, pinnedColumnsCount], ); @@ -95,18 +99,21 @@ const ColumnPickerTab: React.FC = ({ }, [searchString, totalColumns, tab]); const allFilteredColumnsChecked = useMemo(() => { - return filteredColumns.every((col) => columnState.includes(col.column)); + return filteredColumns.every((col) => columnState.includes(formatColumnKey(col))); }, [columnState, filteredColumns]); const handleShowHideAll = useCallback(() => { const filteredColumnMap: Record = filteredColumns.reduce( - (acc, col) => ({ ...acc, [col.column]: columnState.includes(col.column) }), + (acc, col) => ({ + ...acc, + [formatColumnKey(col)]: columnState.includes(formatColumnKey(col)), + }), {}, ); const newColumns = allFilteredColumnsChecked ? columnState.filter((col) => !filteredColumnMap[col]) - : [...new Set([...columnState, ...filteredColumns.map((col) => col.column)])]; + : [...new Set([...columnState, ...filteredColumns.map((col) => formatColumnKey(col))])]; const pinnedCount = allFilteredColumnsChecked ? // If uncheck something pinned, reduce the pinnedColumnsCount newColumns.filter((col) => columnState.indexOf(col) < pinnedColumnsCount).length @@ -123,32 +130,34 @@ const ColumnPickerTab: React.FC = ({ const handleColumnChange = useCallback( (event: CheckboxChangeEvent) => { - const { id, checked } = event.target; - if (id === undefined) return; + const { id: targetCol, checked } = event.target; + + if (targetCol === undefined) return; + if (compare) { // pin or unpin column - const newColumns = columnState.filter((c) => c !== id); + const newColumns = columnState.filter((c) => c !== targetCol); let pinnedCount = pinnedColumnsCount; if (checked) { - newColumns.splice(pinnedColumnsCount, 0, id); + newColumns.splice(pinnedColumnsCount, 0, targetCol); pinnedCount = Math.max(pinnedColumnsCount + 1, 0); } else { - newColumns.splice(pinnedColumnsCount - 1, 0, id); + newColumns.splice(pinnedColumnsCount - 1, 0, targetCol); pinnedCount = Math.max(pinnedColumnsCount - 1, 0); } onVisibleColumnChange?.(newColumns, pinnedCount); } else { let pinnedCount = pinnedColumnsCount; // If uncheck something pinned, reduce the pinnedColumnsCount - if (!checked && columnState.indexOf(id) < pinnedColumnsCount) { + if (!checked && columnState.indexOf(targetCol) < pinnedColumnsCount) { pinnedCount = Math.max(pinnedColumnsCount - 1, 0); } // If uncheck something had heatmap skipped, reset to heatmap visible if (!checked) { - onHeatmapSelectionRemove?.(id); + onHeatmapSelectionRemove?.(targetCol); } const newColumnSet = new Set(columnState); - checked ? newColumnSet.add(id) : newColumnSet.delete(id); + checked ? newColumnSet.add(targetCol) : newColumnSet.delete(targetCol); onVisibleColumnChange?.([...newColumnSet], pinnedCount); } }, @@ -165,24 +174,47 @@ const ColumnPickerTab: React.FC = ({ const rows = useCallback( ({ index, style }: { index: number; style: React.CSSProperties }) => { const col = filteredColumns[index]; + const colType = + KNOWN_BOOLEAN_COLUMNS.includes(col.column) && col.type === V1ColumnType.UNSPECIFIED + ? 'BOOLEAN' + : removeColumnTypePrefix(col.type); + const getColDisplayName = (col: ProjectColumn) => { + return ( + <> + {col.displayName || col.column} + + ); + }; + const getId = () => { + if (col.location === V1LocationType.RUNMETADATA) return formatColumnKey(col); + + return col.column; + }; + const getChecked = () => { + if (col.location === V1LocationType.RUNMETADATA) + return checkedColumnNames.has(formatColumnKey(col)); + + return checkedColumnNames.has(col.column); + }; + return (
- {col.displayName || col.column} + {getColDisplayName(col)}
); }, - [filteredColumns, checkedColumns, handleColumnChange], + [filteredColumns, checkedColumnNames, handleColumnChange], ); return ( diff --git a/webui/react/src/components/FilterForm/TableFilter.tsx b/webui/react/src/components/FilterForm/TableFilter.tsx index 62a91c42109..6fd3f42ef74 100644 --- a/webui/react/src/components/FilterForm/TableFilter.tsx +++ b/webui/react/src/components/FilterForm/TableFilter.tsx @@ -9,6 +9,7 @@ import FilterForm from 'components/FilterForm/components/FilterForm'; import { FilterFormStore } from 'components/FilterForm/components/FilterFormStore'; import { FormKind } from 'components/FilterForm/components/type'; import { V1ProjectColumn } from 'services/api-ts-sdk'; +import { formatColumnKey } from 'utils/flatRun'; interface Props { loadableColumns: Loadable; @@ -32,7 +33,7 @@ const TableFilter = ({ onIsOpenFilterChange, }: Props): JSX.Element => { const columns: V1ProjectColumn[] = Loadable.getOrElse([], loadableColumns).filter( - (column) => !bannedFilterColumns?.has(column.column), + (column) => !bannedFilterColumns?.has(formatColumnKey(column)), ); const fieldCount = useObservable(formStore.fieldCount); const formset = useObservable(formStore.formset); diff --git a/webui/react/src/components/FilterForm/components/FilterField.tsx b/webui/react/src/components/FilterForm/components/FilterField.tsx index eda8536a474..5512e925781 100644 --- a/webui/react/src/components/FilterForm/components/FilterField.tsx +++ b/webui/react/src/components/FilterForm/components/FilterField.tsx @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import Badge from 'hew/Badge'; import Button from 'hew/Button'; import DatePicker, { DatePickerProps } from 'hew/DatePicker'; import Icon from 'hew/Icon'; @@ -32,6 +33,7 @@ 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 { formatColumnKey, METADATA_SEPARATOR, removeColumnTypePrefix } from 'utils/flatRun'; import { alphaNumericSorter } from 'utils/sort'; import css from './FilterField.module.scss'; @@ -69,7 +71,10 @@ const FilterField = ({ }: 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 currentColumn = useMemo( + () => columns.find((c) => c.type === field.type && c.column === field.columnName), + [columns, field.columnName, field.type], + ); const columnType = useMemo(() => { if (field.location === V1LocationType.RUNMETADATA && field.type === V1ColumnType.TEXT) { @@ -96,19 +101,18 @@ const FilterField = ({ }; const onChangeColumnName = (value: SelectValue) => { - const prevType = currentColumn?.type; - const newColName = value?.toString() ?? ''; - const newCol = columns.find((c) => c.column === newColName); + const prevType = field.type; + const [type, newColName] = (value?.toString() ?? '').split(METADATA_SEPARATOR, 2); + const newCol = columns.find((c) => c.column === newColName && type === c.type); if (newCol) { Observable.batch(() => { formStore.setFieldColumnName(field.id, newCol); - if ((SpecialColumnNames as ReadonlyArray).includes(newColName)) { formStore.setFieldOperator(field.id, Operator.Eq); updateFieldValue(field.id, null); - } else if (prevType !== newCol?.type) { + } else if (prevType !== newCol.type) { const defaultOperator: Operator = - AvailableOperators[newCol?.type ?? V1ColumnType.UNSPECIFIED][0]; + AvailableOperators[newCol.type ?? V1ColumnType.UNSPECIFIED][0]; formStore.setFieldOperator(field.id, defaultOperator); updateFieldValue(field.id, null); } @@ -218,6 +222,16 @@ const FilterField = ({ [columnType, field.type, formStore, index, inputOpen, parentId], ); + const getColDisplayName = (col: V1ProjectColumn) => { + const colType = removeColumnTypePrefix(col.type); + + return ( + <> + {col.displayName || col.column} + + ); + }; + return (
drop(node)}> ({ - key: `${col.column} ${idx}`, - label: col.displayName || col.column, - value: col.column, + options={columns.map((col) => ({ + key: formatColumnKey(col, true), + label: getColDisplayName(col), + title: col.displayName || col.column, + value: formatColumnKey(col, true), }))} - value={field.columnName} + value={`${field.type}${METADATA_SEPARATOR}${field.columnName}`} width={'100%'} onChange={onChangeColumnName} /> diff --git a/webui/react/src/components/FilterForm/components/FilterFormStore.ts b/webui/react/src/components/FilterForm/components/FilterFormStore.ts index 9bb61000a04..880c1421701 100644 --- a/webui/react/src/components/FilterForm/components/FilterFormStore.ts +++ b/webui/react/src/components/FilterForm/components/FilterFormStore.ts @@ -242,7 +242,8 @@ export class FilterFormStore { col: Pick, ): void { return this.#updateField(id, (form) => { - if (form.columnName === col.column && form.location === col.location) { + const isSameColumn = form.columnName === col.column && form.type === col.type; + if (isSameColumn && form.location === col.location) { return form; } return { diff --git a/webui/react/src/components/MultiSortMenu.tsx b/webui/react/src/components/MultiSortMenu.tsx index 68916cb3fd6..1b677894876 100644 --- a/webui/react/src/components/MultiSortMenu.tsx +++ b/webui/react/src/components/MultiSortMenu.tsx @@ -1,12 +1,17 @@ +import Badge from 'hew/Badge'; import Button from 'hew/Button'; import { DirectionType, Sort, validSort } from 'hew/DataGrid/DataGrid'; import Dropdown, { MenuItem } from 'hew/Dropdown'; import Icon from 'hew/Icon'; import Select, { Option } from 'hew/Select'; import { Loadable } from 'hew/utils/loadable'; +import { groupBy, mapValues } from 'lodash'; +import { Fragment, useMemo } from 'react'; +import { runColumns } from 'pages/FlatRuns/columns'; import { V1ColumnType } from 'services/api-ts-sdk'; import { ProjectColumn } from 'types'; +import { removeColumnTypePrefix } from 'utils/flatRun'; import css from './MultiSortMenu.module.scss'; @@ -25,6 +30,7 @@ interface MultiSortRowProps { onChange?: (sort: Sort) => void; onRemove?: () => void; bannedSortColumns: Set; + typeMap: Record; } interface DirectionOptionsProps { onChange?: (direction: DirectionType) => void; @@ -36,6 +42,7 @@ interface ColumnOptionsProps { onChange?: (column: string) => void; value?: string; bannedSortColumns: Set; + typeMap: Record; } export const optionsByColumnType = { @@ -153,6 +160,7 @@ const ColumnOptions: React.FC = ({ columns, value, bannedSortColumns, + typeMap, }) => (