Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/components/TestRunList/BulkOperation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ export const BulkOperation: React.FunctionComponent<{

const [isProcessing, setIsProcessing] = React.useState(false);

// thousands of rows against thousands of selected ids, on every render
const selectedIdSet = React.useMemo(
() => new Set(selectedIds),
[selectedIds],
);

const isMerge: boolean = React.useMemo(
() => !!rows.find((row) => selectedIds.includes(row.id))?.merge,
[selectedIds, rows],
() => !!rows.find((row) => selectedIdSet.has(row.id))?.merge,
[selectedIdSet, rows],
);

// The ids of those rows that have status "new" or "resolved"
Expand All @@ -43,11 +49,11 @@ export const BulkOperation: React.FunctionComponent<{
rows
.filter(
(row) =>
selectedIds.includes(row.id) &&
selectedIdSet.has(row.id) &&
[TestStatus.new, TestStatus.unresolved].includes(row.status),
)
.map((row) => row.id),
[selectedIds, rows],
[selectedIdSet, rows],
);

const toggleApproveDialogOpen = () => {
Expand Down Expand Up @@ -131,7 +137,7 @@ export const BulkOperation: React.FunctionComponent<{

if (downloadDialogOpen) {
const images = testRuns
.filter((testRun) => selectedIds.includes(testRun.id))
.filter((testRun) => selectedIdSet.has(testRun.id))
.map((item) => ({
filename: item.name,
name: item.imageName,
Expand Down
8 changes: 5 additions & 3 deletions src/components/TestRunList/TestRunGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ const clipToLines = (lines: number) => ({

export const TestRunGrid: React.FunctionComponent<{
groups: TestRunGroup[];
selectedIds: string[];
// a set rather than the array: a card looks its runs up on every render, and
// a build of thousands can have every one of them selected
selectedIdSet: Set<string>;
density: TestRunDensity;
showDiff: boolean;
activeTags: string[];
Expand All @@ -72,7 +74,7 @@ export const TestRunGrid: React.FunctionComponent<{
onOpen: (id: string) => void;
}> = ({
groups,
selectedIds,
selectedIdSet,
density,
showDiff,
activeTags,
Expand All @@ -90,7 +92,7 @@ export const TestRunGrid: React.FunctionComponent<{
>
{groups.map(({ key, runs, representative }) => {
const ids = runs.map((run) => run.id);
const selectedCount = ids.filter((id) => selectedIds.includes(id)).length;
const selectedCount = ids.filter((id) => selectedIdSet.has(id)).length;
const tags = tagValuesOf(representative, tagFieldsFor(runs.length));

return (
Expand Down
48 changes: 33 additions & 15 deletions src/components/TestRunList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,23 +228,34 @@ const TestRunList: React.FunctionComponent = () => {
enabled: view === "grid" && !selectedTestRun,
});

// A build holds thousands of runs and selecting them all is one click, so
// every membership test below reads a set rather than scanning the array.
const selectedIdSet = React.useMemo(
() => new Set(selectedIds),
[selectedIds],
);

// every filtered run, not just the page: the table's header box does the same
const toggleAll = React.useCallback(
(ids: string[]) =>
setSelectedIds((prev) =>
ids.every((id) => prev.includes(id)) ? [] : ids,
),
setSelectedIds((prev) => {
const previous = new Set(prev);
return ids.every((id) => previous.has(id)) ? [] : ids;
}),
[],
);

// a group is selected or cleared as a whole
const toggleGroup = React.useCallback(
(ids: string[]) =>
setSelectedIds((prev) =>
ids.every((id) => prev.includes(id))
? prev.filter((id) => !ids.includes(id))
: Array.from(new Set([...prev, ...ids])),
),
setSelectedIds((prev) => {
const previous = new Set(prev);
if (ids.every((id) => previous.has(id))) {
const dropped = new Set(ids);
return prev.filter((id) => !dropped.has(id));
}
return Array.from(new Set([...prev, ...ids]));
}),
[],
);

Expand Down Expand Up @@ -376,11 +387,11 @@ const TestRunList: React.FunctionComponent = () => {
groupVariations
? groups
.filter((group) =>
group.runs.every((run) => selectedIds.includes(run.id)),
group.runs.every((run) => selectedIdSet.has(run.id)),
)
.map((group) => group.representative.id)
: selectedIds,
[groupVariations, groups, selectedIds],
[groupVariations, groups, selectedIds, selectedIdSet],
);

// the dialog walks the grid's runs in the grid's order, groups expanded, so
Expand All @@ -397,9 +408,9 @@ const TestRunList: React.FunctionComponent = () => {
const selectedCardCount = React.useMemo(
() =>
groups.filter((group) =>
group.runs.some((run) => selectedIds.includes(run.id)),
group.runs.some((run) => selectedIdSet.has(run.id)),
).length,
[groups, selectedIds],
[groups, selectedIdSet],
);

// clamped rather than corrected in state: turning grouping off and on again
Expand Down Expand Up @@ -430,7 +441,14 @@ const TestRunList: React.FunctionComponent = () => {
const present = new Set<string>(tagFilter);
testRuns.forEach((run) => {
if (matchesNameQuery(run, query) && matchesStatuses(run, statusFilter)) {
runTagValues(run).forEach((value) => present.add(value));
// read the fields straight off the run: an array per run is thousands
// of throwaway allocations on every keystroke
TAG_FIELDS.forEach((field) => {
const value = run[field];
if (typeof value === "string" && value) {
present.add(value);
}
});
}
});
const fieldIndex = (value: string): number => {
Expand Down Expand Up @@ -648,7 +666,7 @@ const TestRunList: React.FunctionComponent = () => {
onSortChange={setGridSort}
density={density}
selectedCount={
groupedRunIds.filter((id) => selectedIds.includes(id)).length
groupedRunIds.filter((id) => selectedIdSet.has(id)).length
}
totalCount={groupedRunIds.length}
onToggleAll={() => toggleAll(groupedRunIds)}
Expand All @@ -668,7 +686,7 @@ const TestRunList: React.FunctionComponent = () => {
) : (
<TestRunGrid
groups={pagedGroups}
selectedIds={selectedIds}
selectedIdSet={selectedIdSet}
density={density}
showDiff={showDiff}
activeTags={tagFilter}
Expand Down
19 changes: 19 additions & 0 deletions src/contexts/testRun.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ const initialState: State = {
loading: false,
};

const sameOrder = (
previous: GridRowId[] | undefined,
next: GridRowId[] | undefined,
): boolean => {
if (previous === next) {
return true;
}
if (!previous || !next || previous.length !== next.length) {
return false;
}
return previous.every((id, index) => id === next[index]);
};

function testRunReducer(state: State, action: IAction): State {
switch (action.type) {
case "select":
Expand All @@ -98,6 +111,12 @@ function testRunReducer(state: State, action: IAction): State {
};

case "filterSort":
// The list republishes its order on every grid state change, and with
// thousands of runs a fresh array re-renders everything reading this
// context for nothing. Keep the previous one when the order is the same.
if (sameOrder(state.filteredSortedTestRunIds, action.payload)) {
return state;
}
return {
...state,
filteredSortedTestRunIds: action.payload,
Expand Down
Loading