Skip to content

Commit

Permalink
feat: add actual select all to glide tables [ET-238] (#10081)
Browse files Browse the repository at this point in the history
  • Loading branch information
EmilyBonar authored Oct 25, 2024
1 parent c7e0fb5 commit 834eeda
Show file tree
Hide file tree
Showing 31 changed files with 936 additions and 671 deletions.
7 changes: 7 additions & 0 deletions webui/react/src/components/ComparisonView.test.mock.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useObservable } from 'micro-observables';
import React from 'react';

import { useGlasbey } from 'hooks/useGlasbey';
import { RunMetricData } from 'hooks/useMetrics';
import { V1LocationType } from 'services/api-ts-sdk';
import { ExperimentWithTrial, Scale } from 'types';
import { generateTestRunData } from 'utils/tests/generateTestData';

import ComparisonView from './ComparisonView';
import { FilterFormStore } from './FilterForm/components/FilterFormStore';

export const METRIC_DATA: RunMetricData = {
data: {
Expand Down Expand Up @@ -245,6 +248,7 @@ export const ExperimentComparisonViewWithMocks: React.FC<Props> = ({
onWidthChange,
open,
}: Props): JSX.Element => {
const tableFilters = useObservable(new FilterFormStore(V1LocationType.EXPERIMENT).asJsonString);
const colorMap = useGlasbey(SELECTED_EXPERIMENTS.map((exp) => exp.experiment.id));
return (
<ComparisonView
Expand All @@ -258,6 +262,7 @@ export const ExperimentComparisonViewWithMocks: React.FC<Props> = ({
initialWidth={200}
open={open}
projectId={1}
tableFilters={tableFilters}
onWidthChange={onWidthChange}>
{children}
</ComparisonView>
Expand All @@ -270,6 +275,7 @@ export const RunComparisonViewWithMocks: React.FC<Props> = ({
onWidthChange,
open,
}: Props): JSX.Element => {
const tableFilters = useObservable(new FilterFormStore(V1LocationType.RUN).asJsonString);
const colorMap = useGlasbey(SELECTED_RUNS.map((run) => run.id));
return (
<ComparisonView
Expand All @@ -283,6 +289,7 @@ export const RunComparisonViewWithMocks: React.FC<Props> = ({
? { selections: [], type: 'ONLY_IN' }
: { selections: SELECTED_RUNS.map((run) => run.id), type: 'ONLY_IN' }
}
tableFilters={tableFilters}
onWidthChange={onWidthChange}>
{children}
</ComparisonView>
Expand Down
34 changes: 30 additions & 4 deletions webui/react/src/components/ComparisonView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@ import useMobile from 'hooks/useMobile';
import useScrollbarWidth from 'hooks/useScrollbarWidth';
import { TrialsComparisonTable } from 'pages/ExperimentDetails/TrialsComparisonModal';
import { searchExperiments, searchRuns } from 'services/api';
import { V1ColumnType, V1LocationType } from 'services/api-ts-sdk';
import { ExperimentWithTrial, FlatRun, SelectionType, XOR } from 'types';
import handleError from 'utils/error';
import { getIdsFilter as getExperimentIdsFilter } from 'utils/experiment';
import { combine } from 'utils/filterFormSet';
import { getIdsFilter as getRunIdsFilter } from 'utils/flatRun';

import CompareMetrics from './CompareMetrics';
import { INIT_FORMSET } from './FilterForm/components/FilterFormStore';
import { FilterFormSet, Operator } from './FilterForm/components/type';

export const EMPTY_MESSAGE = 'No items selected.';

Expand All @@ -33,6 +36,8 @@ interface BaseProps {
onWidthChange: (width: number) => void;
fixedColumnsCount: number;
projectId: number;
searchId?: number;
tableFilters: string;
}

type Props = XOR<{ experimentSelection: SelectionType }, { runSelection: SelectionType }> &
Expand Down Expand Up @@ -132,6 +137,8 @@ const ComparisonView: React.FC<Props> = ({
projectId,
experimentSelection,
runSelection,
searchId,
tableFilters,
}) => {
const scrollbarWidth = useScrollbarWidth();
const hasPinnedColumns = fixedColumnsCount > 1;
Expand All @@ -148,7 +155,10 @@ const ComparisonView: React.FC<Props> = ({
return NotLoaded;
}
try {
const filterFormSet = INIT_FORMSET;
const filterFormSet =
experimentSelection.type === 'ALL_EXCEPT'
? (JSON.parse(tableFilters) as FilterFormSet)
: INIT_FORMSET;
const filter = getExperimentIdsFilter(filterFormSet, experimentSelection);
const response = await searchExperiments({
filter: JSON.stringify(filter),
Expand All @@ -162,7 +172,7 @@ const ComparisonView: React.FC<Props> = ({
handleError(e, { publicSubject: 'Unable to fetch experiments for comparison' });
return NotLoaded;
}
}, [experimentSelection, open]);
}, [experimentSelection, open, tableFilters]);

const loadableSelectedRuns = useAsync(async () => {
if (
Expand All @@ -172,12 +182,28 @@ const ComparisonView: React.FC<Props> = ({
) {
return NotLoaded;
}
const filterFormSet = INIT_FORMSET;
const filterFormSet =
runSelection.type === 'ALL_EXCEPT'
? (JSON.parse(tableFilters) as FilterFormSet)
: INIT_FORMSET;
try {
const filter = getRunIdsFilter(filterFormSet, runSelection);
if (searchId) {
// only display trials for search
const searchFilter = {
columnName: 'experimentId',
kind: 'field' as const,
location: V1LocationType.RUN,
operator: Operator.Eq,
type: V1ColumnType.NUMBER,
value: searchId,
};
filter.filterGroup = combine(filter.filterGroup, 'and', searchFilter);
}
const response = await searchRuns({
filter: JSON.stringify(filter),
limit: SELECTION_LIMIT,
projectId,
});
setIsSelectionLimitReached(
!!response?.pagination?.total && response?.pagination?.total > SELECTION_LIMIT,
Expand All @@ -187,7 +213,7 @@ const ComparisonView: React.FC<Props> = ({
handleError(e, { publicSubject: 'Unable to fetch runs for comparison' });
return NotLoaded;
}
}, [open, runSelection]);
}, [open, projectId, runSelection, searchId, tableFilters]);

const minWidths: [number, number] = useMemo(() => {
return [fixedColumnsCount * MIN_COLUMN_WIDTH + scrollbarWidth, 100];
Expand Down
1 change: 1 addition & 0 deletions webui/react/src/components/ExperimentActionDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ const ExperimentActionDropdown: React.FC<Props> = ({
/>
<ExperimentMoveModal.Component
experimentIds={[experiment.id]}
selectionSize={1}
sourceProjectId={experiment.projectId}
sourceWorkspaceId={experiment.workspaceId}
onSubmit={handleMoveComplete}
Expand Down
5 changes: 4 additions & 1 deletion webui/react/src/components/ExperimentCreateModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React from 'react';

import ExperimentCreateModalComponent, {
CreateExperimentType,
RunActionCopyMap,
} from 'components/ExperimentCreateModal';
import { ThemeProvider } from 'components/ThemeProvider';
import { createExperiment as mockCreateExperiment } from 'services/api';
Expand Down Expand Up @@ -66,7 +67,9 @@ describe('Create Experiment Modal', () => {
it('submits a valid create experiment request', async () => {
await setup();

await user.click(screen.getByRole('button', { name: CreateExperimentType.Fork }));
await user.click(
screen.getByRole('button', { name: RunActionCopyMap[CreateExperimentType.Fork] }),
);
expect(mockCreateExperiment).toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions webui/react/src/components/ExperimentCreateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const ExperimentEntityCopyMap = {
trial: 'trial',
};

const RunActionCopyMap = {
export const RunActionCopyMap = {
[CreateExperimentType.ContinueTrial]: 'Continue Run',
[CreateExperimentType.Fork]: 'Fork',
};
Expand Down Expand Up @@ -361,7 +361,7 @@ const ExperimentCreateModalComponent = ({
form: idPrefix + FORM_ID,
handleError,
handler: handleSubmit,
text: type,
text: ExperimentActionCopyMap[type],
}}
title={titleLabel}
onClose={handleModalClose}>
Expand Down
76 changes: 43 additions & 33 deletions webui/react/src/components/ExperimentMoveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,40 @@ import Link from 'components/Link';
import useFeature from 'hooks/useFeature';
import usePermissions from 'hooks/usePermissions';
import { paths } from 'routes/utils';
import { moveExperiments } from 'services/api';
import { V1BulkExperimentFilters } from 'services/api-ts-sdk';
import { moveSearches } from 'services/api';
import { V1MoveSearchesRequest } from 'services/api-ts-sdk';
import projectStore from 'stores/projects';
import workspaceStore from 'stores/workspaces';
import { Project } from 'types';
import { Project, SelectionType, XOR } from 'types';
import handleError from 'utils/error';
import { getIdsFilter as getExperimentIdsFilter } from 'utils/experiment';
import { capitalize, pluralizer } from 'utils/string';

import { INIT_FORMSET } from './FilterForm/components/FilterFormStore';
import { FilterFormSet } from './FilterForm/components/type';

const FORM_ID = 'move-experiment-form';

type FormInputs = {
projectId?: number;
workspaceId?: number;
};

interface Props {
excludedExperimentIds?: Map<number, unknown>;
experimentIds: number[];
filters?: V1BulkExperimentFilters;
interface BaseProps {
onSubmit?: (successfulIds?: number[]) => void;
selectionSize: number;
sourceProjectId: number;
sourceWorkspaceId?: number;
}

type Props = BaseProps &
XOR<{ experimentIds: number[] }, { selection: SelectionType; tableFilters: string }>;

const ExperimentMoveModalComponent: React.FC<Props> = ({
excludedExperimentIds,
experimentIds,
filters,
selection,
selectionSize,
tableFilters,
onSubmit,
sourceProjectId,
sourceWorkspaceId,
Expand All @@ -54,8 +60,6 @@ const ExperimentMoveModalComponent: React.FC<Props> = ({
const projectId = Form.useWatch('projectId', form);
const f_flat_runs = useFeature().isOn('flat_runs');

const entityName = f_flat_runs ? 'searches' : 'experiments';

useEffect(() => {
setDisabled(workspaceId !== 1 && !projectId);
}, [workspaceId, projectId, sourceProjectId, sourceWorkspaceId]);
Expand All @@ -76,6 +80,14 @@ const ExperimentMoveModalComponent: React.FC<Props> = ({
}
}, [workspaceId]);

// use plurals for indeterminate case
const pluralizerArgs = f_flat_runs
? (['search', 'searches'] as const)
: (['experiment'] as const);
// we use apply instead of a direct call here because typescript errors when you spread a tuple into arguments
const plural = pluralizer.apply(null, [selectionSize, ...pluralizerArgs]);
const actionCopy = `Move ${capitalize(plural)}`;

const handleSubmit = async () => {
if (workspaceId === sourceWorkspaceId && projectId === sourceProjectId) {
openToast({ title: 'No changes to save.' });
Expand All @@ -84,16 +96,23 @@ const ExperimentMoveModalComponent: React.FC<Props> = ({
const values = await form.validateFields();
const projId = values.projectId ?? 1;

if (excludedExperimentIds?.size) {
filters = { ...filters, excludedExperimentIds: Array.from(excludedExperimentIds.keys()) };
const moveSearchesArgs: V1MoveSearchesRequest = {
destinationProjectId: projId,
sourceProjectId,
};

if (tableFilters !== undefined) {
const filterFormSet =
selection.type === 'ALL_EXCEPT'
? (JSON.parse(tableFilters) as FilterFormSet)
: INIT_FORMSET;
const filter = getExperimentIdsFilter(filterFormSet, selection);
moveSearchesArgs.filter = JSON.stringify(filter);
} else {
moveSearchesArgs.searchIds = experimentIds;
}

const results = await moveExperiments({
destinationProjectId: projId,
experimentIds,
filters,
projectId: sourceProjectId,
});
const results = await moveSearches(moveSearchesArgs);

onSubmit?.(results.successful);

Expand All @@ -106,19 +125,19 @@ const ExperimentMoveModalComponent: React.FC<Props> = ({

if (numSuccesses === 0 && numFailures === 0) {
openToast({
description: `No selected ${entityName} were eligible for moving`,
title: `No eligible ${entityName}`,
description: `No selected ${plural} were eligible for moving`,
title: `No eligible ${plural}`,
});
} else if (numFailures === 0) {
openToast({
closeable: true,
description: `${results.successful.length} ${entityName} moved to project ${destinationProjectName}`,
description: `${results.successful.length} ${pluralizer.apply(null, [results.successful.length, ...pluralizerArgs])} moved to project ${destinationProjectName}`,
link: <Link path={paths.projectDetails(projId)}>View Project</Link>,
title: 'Move Success',
});
} else if (numSuccesses === 0) {
openToast({
description: `Unable to move ${numFailures} ${entityName}`,
description: `Unable to move ${numFailures} ${pluralizer.apply(null, [numFailures, ...pluralizerArgs])}`,
severity: 'Warning',
title: 'Move Failure',
});
Expand All @@ -127,7 +146,7 @@ const ExperimentMoveModalComponent: React.FC<Props> = ({
closeable: true,
description: `${numFailures} out of ${
numFailures + numSuccesses
} eligible ${entityName} failed to move
} eligible ${plural} failed to move
to project ${destinationProjectName}`,
link: <Link path={paths.projectDetails(projId)}>View Project</Link>,
severity: 'Warning',
Expand All @@ -142,15 +161,6 @@ const ExperimentMoveModalComponent: React.FC<Props> = ({
form.setFieldValue('workspaceId', sourceWorkspaceId ?? 1);
}, [form, sourceProjectId, sourceWorkspaceId]);

// use plurals for indeterminate case
const entityCount = filters !== undefined ? 2 : experimentIds.length;
const pluralizerArgs = f_flat_runs
? (['search', 'searches'] as const)
: (['experiment'] as const);
// we use apply instead of a direct call here because typescript errors when you spread a tuple into arguments
const plural = pluralizer.apply(null, [entityCount, ...pluralizerArgs]);
const actionCopy = `Move ${capitalize(plural)}`;

return (
<Modal
cancel
Expand Down
Loading

0 comments on commit 834eeda

Please sign in to comment.