Skip to content

Commit

Permalink
[OPIK-863] Allow to quickly select multiple rows with Shift+Click (#1200
Browse files Browse the repository at this point in the history
)

* [OPIK-863] Allow to quickly select multiple rows with Shift+Click

* - add comment with explanation of logic
  • Loading branch information
andriidudar authored Feb 4, 2025
1 parent eb1dd56 commit 4b5e746
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
generateGroupedCellDef,
getIsCustomRow,
getRowId,
getSharedShiftCheckboxClickHandler,
GROUPING_CONFIG,
renderCustomRow,
} from "@/components/pages/ExperimentsShared/table";
Expand Down Expand Up @@ -142,6 +143,11 @@ const ExperimentsPage: React.FunctionComponent = () => {
);

const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const { checkboxClickHandler } = useMemo(() => {
return {
checkboxClickHandler: getSharedShiftCheckboxClickHandler(),
};
}, []);

const { data, isPending, refetch } = useGroupedExperimentsList({
workspaceName,
Expand Down Expand Up @@ -242,18 +248,21 @@ const ExperimentsPage: React.FunctionComponent = () => {

const columns = useMemo(() => {
return [
generateExperimentNameColumDef<GroupedExperiment>(),
generateGroupedCellDef<GroupedExperiment, unknown>({
id: GROUPING_COLUMN,
label: "Dataset",
type: COLUMN_TYPE.string,
cell: ResourceCell as never,
customMeta: {
nameKey: "dataset_name",
idKey: "dataset_id",
resource: RESOURCE_TYPE.dataset,
generateExperimentNameColumDef<GroupedExperiment>(checkboxClickHandler),
generateGroupedCellDef<GroupedExperiment, unknown>(
{
id: GROUPING_COLUMN,
label: "Dataset",
type: COLUMN_TYPE.string,
cell: ResourceCell as never,
customMeta: {
nameKey: "dataset_name",
idKey: "dataset_id",
resource: RESOURCE_TYPE.dataset,
},
},
}),
checkboxClickHandler,
),
...convertColumnDataToColumn<GroupedExperiment, GroupedExperiment>(
DEFAULT_COLUMNS,
{
Expand All @@ -272,7 +281,13 @@ const ExperimentsPage: React.FunctionComponent = () => {
cell: ExperimentRowActionsCell,
}),
];
}, [selectedColumns, columnsOrder, scoresColumnsOrder, scoresColumnsData]);
}, [
selectedColumns,
columnsOrder,
checkboxClickHandler,
scoresColumnsOrder,
scoresColumnsData,
]);

const resizeConfig = useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { TableCell, TableRow } from "@/components/ui/table";
import {
getCommonPinningClasses,
getCommonPinningStyles,
shiftCheckboxClickHandler,
} from "@/components/shared/DataTable/utils";

export const GROUPING_CONFIG = {
Expand All @@ -37,7 +38,28 @@ export const getRowId = (e: GroupedExperiment) => e.id;
export const getIsMoreRow = (row: Row<GroupedExperiment>) =>
checkIsMoreRowId(row?.original?.id || "");

export const generateExperimentNameColumDef = <TData,>() => {
// The goal to use shared handler between two different helpers to draw cells in Experiments table,
// to share in closure the previousSelectedRowID between two helpers
// generateExperimentNameColumDef and generateGroupedCellDef
export const getSharedShiftCheckboxClickHandler = () => {
let previousSelectedRowID = "";

return <TData,>(
event: React.MouseEvent<HTMLButtonElement>,
context: CellContext<TData, unknown>,
) => {
event.stopPropagation();
shiftCheckboxClickHandler(event, context, previousSelectedRowID);
previousSelectedRowID = context.row.id;
};
};

export const generateExperimentNameColumDef = <TData,>(
checkboxClickHandler: (
event: React.MouseEvent<HTMLButtonElement>,
context: CellContext<TData, unknown>,
) => void,
) => {
return {
accessorKey: COLUMN_NAME_ID,
header: (context) => (
Expand Down Expand Up @@ -70,10 +92,10 @@ export const generateExperimentNameColumDef = <TData,>() => {
style={{
marginLeft: `${context.row.depth * 28}px`,
}}
onClick={(event) => event.stopPropagation()}
checked={context.row.getIsSelected()}
disabled={!context.row.getCanSelect()}
onCheckedChange={(value) => context.row.toggleSelected(!!value)}
onClick={(event) => checkboxClickHandler(event, context)}
aria-label="Select row"
/>
<div className="ml-3 min-w-1 max-w-full">
Expand All @@ -98,6 +120,10 @@ export const generateExperimentNameColumDef = <TData,>() => {

export const generateGroupedCellDef = <TData, TValue>(
columnData: ColumnData<TData>,
checkboxClickHandler: (
event: React.MouseEvent<HTMLButtonElement>,
context: CellContext<TData, unknown>,
) => void,
) => {
return {
...mapColumnDataFields(columnData),
Expand All @@ -108,13 +134,13 @@ export const generateGroupedCellDef = <TData, TValue>(
<div className="flex size-full h-11 items-center">
<div className="flex shrink-0 items-center pl-5">
<Checkbox
onClick={(event) => event.stopPropagation()}
checked={
row.getIsAllSubRowsSelected() ||
(row.getIsSomeSelected() && "indeterminate")
}
disabled={!row.getCanSelect()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
onClick={(event) => checkboxClickHandler(event, context)}
aria-label="Select row"
/>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
generateGroupedCellDef,
getIsCustomRow,
getRowId,
getSharedShiftCheckboxClickHandler,
GROUPING_CONFIG,
renderCustomRow,
} from "@/components/pages/ExperimentsShared/table";
Expand Down Expand Up @@ -100,8 +101,13 @@ const ExperimentsTab: React.FC<ExperimentsTabProps> = ({ promptId }) => {
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [datasetId, setDatasetId] = useState("");
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [groupLimit, setGroupLimit] = useState<Record<string, number>>({});
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const { checkboxClickHandler } = useMemo(() => {
return {
checkboxClickHandler: getSharedShiftCheckboxClickHandler(),
};
}, []);

const { data, isPending } = useGroupedExperimentsList({
workspaceName,
Expand Down Expand Up @@ -202,18 +208,21 @@ const ExperimentsTab: React.FC<ExperimentsTabProps> = ({ promptId }) => {

const columns = useMemo(() => {
return [
generateExperimentNameColumDef<GroupedExperiment>(),
generateGroupedCellDef<GroupedExperiment, unknown>({
id: GROUPING_COLUMN,
label: "Dataset",
type: COLUMN_TYPE.string,
cell: ResourceCell as never,
customMeta: {
nameKey: "dataset_name",
idKey: "dataset_id",
resource: RESOURCE_TYPE.dataset,
generateExperimentNameColumDef<GroupedExperiment>(checkboxClickHandler),
generateGroupedCellDef<GroupedExperiment, unknown>(
{
id: GROUPING_COLUMN,
label: "Dataset",
type: COLUMN_TYPE.string,
cell: ResourceCell as never,
customMeta: {
nameKey: "dataset_name",
idKey: "dataset_id",
resource: RESOURCE_TYPE.dataset,
},
},
}),
checkboxClickHandler,
),
...convertColumnDataToColumn<GroupedExperiment, GroupedExperiment>(
DEFAULT_COLUMNS,
{
Expand All @@ -229,7 +238,13 @@ const ExperimentsTab: React.FC<ExperimentsTabProps> = ({ promptId }) => {
},
),
];
}, [selectedColumns, columnsOrder, scoresColumnsOrder, scoresColumnsData]);
}, [
selectedColumns,
columnsOrder,
checkboxClickHandler,
scoresColumnsOrder,
scoresColumnsData,
]);

const resizeConfig = useMemo(
() => ({
Expand Down
65 changes: 64 additions & 1 deletion apps/opik-frontend/src/components/shared/DataTable/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Column,
ColumnDef,
ColumnDefTemplate,
Row,
} from "@tanstack/react-table";
import {
ROW_HEIGHT_MAP,
Expand Down Expand Up @@ -57,7 +58,65 @@ export const getCommonPinningClasses = <TData,>(
return isPinned ? (isHeader ? "bg-[#FBFCFD]" : "bg-white") : "";
};

const getRowRange = <TData,>(
rows: Array<Row<TData>>,
clickedRowID: string,
previousClickedRowID: string,
) => {
const range: Array<Row<TData>> = [];
const processedRowsMap = {
[clickedRowID]: false,
[previousClickedRowID]: false,
};
for (const row of rows) {
if (row.id === clickedRowID || row.id === previousClickedRowID) {
if ("" === previousClickedRowID) {
range.push(row);
break;
}

processedRowsMap[row.id] = true;
}
if (
(processedRowsMap[clickedRowID] ||
processedRowsMap[previousClickedRowID]) &&
!row.getIsGrouped()
) {
range.push(row);
}
if (
processedRowsMap[clickedRowID] &&
processedRowsMap[previousClickedRowID]
) {
break;
}
}

return range;
};

export const shiftCheckboxClickHandler = <TData,>(
event: React.MouseEvent<HTMLButtonElement>,
context: CellContext<TData, unknown>,
previousClickedRowID: string,
) => {
if (event.shiftKey) {
const { rows, rowsById: rowsMap } = context.table.getRowModel();
const rowsToToggle = getRowRange(
rows,
context.row.id,
rows.map((r) => r.id).includes(previousClickedRowID)
? previousClickedRowID
: "",
);
const isLastSelected = !rowsMap[context.row.id]?.getIsSelected() || false;
rowsToToggle.forEach((row) => row.toggleSelected(isLastSelected));
}
};

export const generateSelectColumDef = <TData,>() => {
let previousSelectedRowID = "";

return {
accessorKey: COLUMN_SELECT_ID,
header: (context) => (
Expand Down Expand Up @@ -86,10 +145,14 @@ export const generateSelectColumDef = <TData,>() => {
className="py-3.5"
>
<Checkbox
onClick={(event) => event.stopPropagation()}
checked={context.row.getIsSelected()}
disabled={!context.row.getCanSelect()}
onCheckedChange={(value) => context.row.toggleSelected(!!value)}
onClick={(event) => {
event.stopPropagation();
shiftCheckboxClickHandler(event, context, previousSelectedRowID);
previousSelectedRowID = context.row.id;
}}
aria-label="Select row"
/>
</CellWrapper>
Expand Down

0 comments on commit 4b5e746

Please sign in to comment.