Skip to content

Commit

Permalink
Update tables to use hook and fix selection logic
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilyBonar committed Oct 18, 2024
1 parent ce8ef3e commit 6f25deb
Show file tree
Hide file tree
Showing 10 changed files with 296 additions and 454 deletions.
123 changes: 14 additions & 109 deletions webui/react/src/components/Searches/Searches.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { CompactSelection, GridSelection } from '@glideapps/glide-data-grid';
import { isLeft } from 'fp-ts/lib/Either';
import Column from 'hew/Column';
import {
Expand All @@ -11,15 +10,7 @@ import {
MIN_COLUMN_WIDTH,
MULTISELECT,
} from 'hew/DataGrid/columns';
import DataGrid, {
DataGridHandle,
HandleSelectionChangeType,
RangelessSelectionType,
SelectionType,
Sort,
validSort,
ValidSort,
} from 'hew/DataGrid/DataGrid';
import DataGrid, { DataGridHandle, Sort, validSort, ValidSort } from 'hew/DataGrid/DataGrid';
import { MenuItem } from 'hew/Dropdown';
import Icon from 'hew/Icon';
import Link from 'hew/Link';
Expand Down Expand Up @@ -56,6 +47,7 @@ import { useDebouncedSettings } from 'hooks/useDebouncedSettings';
import { useGlasbey } from 'hooks/useGlasbey';
import useMobile from 'hooks/useMobile';
import usePolling from 'hooks/usePolling';
import useSelection from 'hooks/useSelection';
import { useSettings } from 'hooks/useSettings';
import { useTypedParams } from 'hooks/useTypedParams';
import { paths } from 'routes/utils';
Expand All @@ -75,7 +67,6 @@ import {
Project,
ProjectColumn,
RunState,
SelectionType as SelectionState,
} from 'types';
import handleError from 'utils/error';
import { getProjectExperimentForExperimentItem } from 'utils/experiment';
Expand Down Expand Up @@ -183,6 +174,13 @@ const Searches: React.FC<Props> = ({ project }) => {
const isMobile = useMobile();
const { openToast } = useToast();

const { selectionSize, dataGridSelection, handleSelectionChange, rowRangeToIds } = useSelection({
records: experiments.map((loadable) => loadable.map((exp) => exp.experiment)),
selection: settings.selection,
total,
updateSettings,
});

const handlePinnedColumnsCountChange = useCallback(
(newCount: number) => updateSettings({ pinnedColumnsCount: newCount }),
[updateSettings],
Expand Down Expand Up @@ -264,36 +262,6 @@ const Searches: React.FC<Props> = ({ project }) => {
return selectedMap;
}, [isLoadingSettings, allSelectedExperimentIds, experiments]);

const selection = useMemo<GridSelection>(() => {
let rows = CompactSelection.empty();
if (settings.selection.type === 'ONLY_IN') {
loadedSelectedExperimentIds.forEach((exp) => {
rows = rows.add(exp.index);
});
} else if (settings.selection.type === 'ALL_EXCEPT') {
rows = rows.add([0, total.getOrElse(1) - 1]);
settings.selection.exclusions.forEach((exc) => {
const excIndex = loadedSelectedExperimentIds.get(exc)?.index;
if (excIndex !== undefined) {
rows = rows.remove(excIndex);
}
});
}
return {
columns: CompactSelection.empty(),
rows,
};
}, [loadedSelectedExperimentIds, settings.selection, total]);

const selectionSize = useMemo(() => {
if (settings.selection.type === 'ONLY_IN') {
return settings.selection.selections.length;
} else if (settings.selection.type === 'ALL_EXCEPT') {
return total.getOrElse(0) - settings.selection.exclusions.length;
}
return 0;
}, [settings.selection, total]);

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

const experimentFilters = useMemo(() => {
Expand Down Expand Up @@ -418,71 +386,6 @@ const Searches: React.FC<Props> = ({ project }) => {
};
}, [canceler, stopPolling]);

const rowRangeToIds = useCallback(
(range: [number, number]) => {
const slice = experiments.slice(range[0], range[1]);
return Loadable.filterNotLoaded(slice).map(({ experiment }) => experiment.id);
},
[experiments],
);

const handleSelectionChange: HandleSelectionChangeType = useCallback(
(selectionType: SelectionType | RangelessSelectionType, range?: [number, number]) => {
let newSettings: SelectionState = { ...settings.selection };

switch (selectionType) {
case 'add':
if (!range) return;
if (newSettings.type === 'ALL_EXCEPT') {
const excludedSet = new Set(newSettings.exclusions);
rowRangeToIds(range).forEach((id) => excludedSet.delete(id));
newSettings.exclusions = Array.from(excludedSet);
} else {
const includedSet = new Set(newSettings.selections);
rowRangeToIds(range).forEach((id) => includedSet.add(id));
newSettings.selections = Array.from(includedSet);
}

break;
case 'add-all':
newSettings = {
exclusions: [],
type: 'ALL_EXCEPT' as const,
};

break;
case 'remove':
if (!range) return;
if (newSettings.type === 'ALL_EXCEPT') {
const excludedSet = new Set(newSettings.exclusions);
rowRangeToIds(range).forEach((id) => excludedSet.add(id));
newSettings.exclusions = Array.from(excludedSet);
} else {
const includedSet = new Set(newSettings.selections);
rowRangeToIds(range).forEach((id) => includedSet.delete(id));
newSettings.selections = Array.from(includedSet);
}

break;
case 'remove-all':
newSettings = DEFAULT_SELECTION;

break;
case 'set':
if (!range) return;
newSettings = {
...DEFAULT_SELECTION,
selections: Array.from(rowRangeToIds(range)),
};

break;
}

updateSettings({ selection: newSettings });
},
[rowRangeToIds, settings.selection, updateSettings],
);

const handleActionComplete = useCallback(async () => {
/**
* Deselect selected rows since their states may have changed where they
Expand Down Expand Up @@ -658,7 +561,7 @@ const Searches: React.FC<Props> = ({ project }) => {
const gridColumns = [...STATIC_COLUMNS, ...columnsIfLoaded]
.map((columnName) => {
if (columnName === MULTISELECT) {
return (columnDefs[columnName] = defaultSelectionColumn(selection.rows, false));
return (columnDefs[columnName] = defaultSelectionColumn(dataGridSelection.rows, false));
}

if (!Loadable.isLoaded(projectColumnsMap)) {
Expand Down Expand Up @@ -731,7 +634,7 @@ const Searches: React.FC<Props> = ({ project }) => {
columnsIfLoaded,
appTheme,
isDarkMode,
selection.rows,
dataGridSelection.rows,
users,
]);

Expand Down Expand Up @@ -904,8 +807,10 @@ const Searches: React.FC<Props> = ({ project }) => {
projectColumns={projectColumns}
rowHeight={globalSettings.rowHeight}
selectedExperimentIds={allSelectedExperimentIds}
selection={settings.selection}
selectionSize={selectionSize}
sorts={sorts}
tableFilterString={filtersString}
total={total}
onActionComplete={handleActionComplete}
onActionSuccess={handleActionSuccess}
Expand Down Expand Up @@ -965,7 +870,7 @@ const Searches: React.FC<Props> = ({ project }) => {
);
}}
rowHeight={rowHeightMap[globalSettings.rowHeight as RowHeight]}
selection={selection}
selection={dataGridSelection}
sorts={sorts}
staticColumns={STATIC_COLUMNS}
onColumnResize={handleColumnWidthChange}
Expand Down
85 changes: 55 additions & 30 deletions webui/react/src/components/TableActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,32 @@ import useMobile from 'hooks/useMobile';
import usePermissions from 'hooks/usePermissions';
import { defaultExperimentColumns } from 'pages/F_ExpList/expListColumns';
import {
activateExperiments,
archiveExperiments,
archiveSearches,
cancelExperiments,
deleteExperiments,
deleteSearches,
getExperiments,
killExperiments,
killSearches,
openOrCreateTensorBoard,
pauseExperiments,
unarchiveExperiments,
pauseSearches,
resumeSearches,
unarchiveSearches,
} from 'services/api';
import { V1LocationType } from 'services/api-ts-sdk';
import { SearchBulkActionParams } from 'services/types';
import {
BulkActionResult,
BulkExperimentItem,
ExperimentAction,
Project,
ProjectColumn,
ProjectExperiment,
SelectionType,
} from 'types';
import handleError, { ErrorLevel } from 'utils/error';
import {
canActionExperiment,
getActionsForExperimentsUnion,
getIdsFilter,
getProjectExperimentForExperimentItem,
} from 'utils/experiment';
import { capitalizeWord } from 'utils/string';
Expand Down Expand Up @@ -107,6 +110,7 @@ interface Props {
projectColumns: Loadable<ProjectColumn[]>;
rowHeight: RowHeight;
selectedExperimentIds: number[];
selection: SelectionType;
selectionSize: number;
sorts: Sort[];
pinnedColumnsCount?: number;
Expand All @@ -117,11 +121,13 @@ interface Props {
bannedFilterColumns?: Set<string>;
bannedSortColumns?: Set<string>;
entityCopy?: string;
tableFilterString: string;
}

const TableActionBar: React.FC<Props> = ({
compareViewOn,
formStore,
tableFilterString,
heatmapBtnVisible,
heatmapOn,
initialVisibleColumns,
Expand Down Expand Up @@ -152,6 +158,7 @@ const TableActionBar: React.FC<Props> = ({
bannedSortColumns,
entityCopy,
selectionSize,
selection,
}) => {
const permissions = usePermissions();
const [batchAction, setBatchAction] = useState<BatchAction>();
Expand Down Expand Up @@ -209,29 +216,41 @@ const TableActionBar: React.FC<Props> = ({
);

const availableBatchActions = useMemo(() => {
const experiments = selectedExperimentIds.map((id) => experimentMap[id]) ?? [];
return getActionsForExperimentsUnion(experiments, [...batchActions], permissions);
// Spreading batchActions is so TypeScript doesn't complain that it's readonly.
}, [selectedExperimentIds, experimentMap, permissions]);
if (selection.type === 'ONLY_IN') {
const experiments = selectedExperimentIds.map((id) => experimentMap[id]) ?? [];
return getActionsForExperimentsUnion(experiments, [...batchActions], permissions); // Spreading batchActions is so TypeScript doesn't complain that it's readonly.
} else if (selection.type === 'ALL_EXCEPT') {
return batchActions.filter(
(action) =>
action !== ExperimentAction.OpenTensorBoard && action !== ExperimentAction.Cancel,
);
}
return []; // should never be reached
}, [selection.type, selectedExperimentIds, permissions, experimentMap]);

const sendBatchActions = useCallback(
async (action: BatchAction): Promise<BulkActionResult | void> => {
const validExperimentIds = selectedExperiments
.filter((exp) => !exp.unmanaged && canActionExperiment(action, exp))
.map((exp) => exp.id);
const params = {
experimentIds: validExperimentIds,
projectId: project.id,
};
const params: SearchBulkActionParams = { projectId: project.id };
if (selection.type === 'ONLY_IN') {
const validSearchIds = selectedExperiments
.filter((exp) => !exp.unmanaged && canActionExperiment(action, exp))
.map((exp) => exp.id);
params.searchIds = validSearchIds;
} else if (selection.type === 'ALL_EXCEPT') {
const filters = JSON.parse(tableFilterString);
params.filter = JSON.stringify(getIdsFilter(filters, selection));
}

switch (action) {
case ExperimentAction.OpenTensorBoard: {
if (validExperimentIds.length !== selectedExperiments.length) {
if (params.searchIds === undefined) break;
if (params.searchIds.length !== selectedExperiments.length) {
// if unmanaged experiments are selected, open experimentTensorBoardModal
openExperimentTensorBoardModal();
} else {
openCommandResponse(
await openOrCreateTensorBoard({
experimentIds: params.experimentIds,
experimentIds: params.searchIds,
workspaceId: project?.workspaceId,
}),
);
Expand All @@ -243,27 +262,34 @@ const TableActionBar: React.FC<Props> = ({
case ExperimentAction.RetainLogs:
return ExperimentRetainLogsModal.open();
case ExperimentAction.Activate:
return await activateExperiments(params);
return await resumeSearches(params);
case ExperimentAction.Archive:
return await archiveExperiments(params);
return await archiveSearches(params);
case ExperimentAction.Cancel:
return await cancelExperiments(params);
if (params.searchIds === undefined) break;
return await cancelExperiments({
experimentIds: params.searchIds,
projectId: params.projectId,
});
case ExperimentAction.Kill:
return await killExperiments(params);
return await killSearches(params);
case ExperimentAction.Pause:
return await pauseExperiments(params);
return await pauseSearches(params);
case ExperimentAction.Unarchive:
return await unarchiveExperiments(params);
return await unarchiveSearches(params);
case ExperimentAction.Delete:
return await deleteExperiments(params);
return await deleteSearches(params);
}
},
[
project.id,
project?.workspaceId,
selection,
selectedExperiments,
tableFilterString,
ExperimentMoveModal,
ExperimentRetainLogsModal,
openExperimentTensorBoardModal,
project,
],
);

Expand Down Expand Up @@ -320,8 +346,7 @@ const TableActionBar: React.FC<Props> = ({
closeable: true,
description: `${action} succeeded for ${numSuccesses} out of ${
numFailures + numSuccesses
} eligible
${labelPlural.toLowerCase()}`,
} ${labelPlural.toLowerCase()}`,
severity: 'Warning',
title: `Partial ${action} Failure`,
});
Expand Down Expand Up @@ -419,7 +444,7 @@ const TableActionBar: React.FC<Props> = ({
onVisibleColumnChange={onVisibleColumnChange}
/>
<OptionsMenu rowHeight={rowHeight} onRowHeightChange={onRowHeightChange} />
{selectedExperimentIds.length > 0 && (
{selectionSize > 0 && (
<Dropdown menu={editMenuItems} onClick={handleBatchAction}>
<Button data-test="actionsDropdown" hideChildren={isMobile}>
Actions
Expand Down
Loading

0 comments on commit 6f25deb

Please sign in to comment.