Skip to content

Commit 3646111

Browse files
authored
Share table sorting component & update sortable keys (conda-forge#2675)
1 parent 1c153f7 commit 3646111

File tree

4 files changed

+189
-63
lines changed

4 files changed

+189
-63
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from "react";
2+
import { useState } from "react";
3+
4+
/**
5+
* Custom hook for managing table sorting state with secondary sort support
6+
* @param {string} initialSortBy - Initial sort column
7+
* @param {string} initialOrder - Initial sort order ("ascending" or "descending")
8+
* @returns {{ sort: object, previousSort: object, resort: function }} - Sort state, previous sort state, and resort function
9+
*/
10+
export function useSorting(initialSortBy, initialOrder = "ascending") {
11+
const [sort, setSort] = useState({ by: initialSortBy, order: initialOrder });
12+
const [previousSort, setPreviousSort] = useState(null);
13+
14+
const resort = (by) => {
15+
setSort((prev) => {
16+
let order = "ascending";
17+
order = by === prev.by && order === prev.order ? "descending" : order;
18+
// Save previous sort state (but only if it's a different column)
19+
if (by !== prev.by) {
20+
setPreviousSort(prev);
21+
}
22+
return { by, order };
23+
});
24+
};
25+
26+
return { sort, previousSort, resort };
27+
}
28+
29+
/**
30+
* Sortable table header component
31+
* @param {string} sortKey - The key to sort by when clicked
32+
* @param {object} currentSort - Current sort state { by, order }
33+
* @param {function} onSort - Callback function when header is clicked
34+
* @param {object} styles - CSS module styles object
35+
* @param {object} style - Inline styles for the header
36+
* @param {React.ReactNode} children - Header content
37+
*/
38+
export function SortableHeader({ sortKey, currentSort, onSort, styles, style, children }) {
39+
const isActive = currentSort.by === sortKey;
40+
const className = isActive ? styles[currentSort.order] : undefined;
41+
42+
return (
43+
<th
44+
style={{ cursor: "pointer", userSelect: "none", ...style }}
45+
className={className}
46+
onClick={() => onSort(sortKey)}
47+
>
48+
{children}
49+
</th>
50+
);
51+
}

src/components/StatusDashboard/current_migrations.jsx

Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
44
import { measureProgress } from "@site/src/pages/status/migration";
55
import styles from "./styles.module.css";
66
import Link from "@docusaurus/Link";
7+
import { SortableHeader } from "@site/src/components/SortableTable";
78

89
const COLLAPSED_KEY = "migration-collapsed";
910
const SORT_KEY = "migration-sort";
@@ -135,54 +136,30 @@ function TableContent({ collapsed, name, resort, rows, select, sort, fetched })
135136
</th>
136137
</tr>
137138
<tr className={collapsed ? styles.collapsed : undefined}>
138-
<th
139-
onClick={() => resort("name")}
140-
className={sort.by === "name" ? styles[sort.order] : undefined}
141-
>
139+
<SortableHeader sortKey="name" currentSort={sort} onSort={resort} styles={styles}>
142140
Name
143-
</th>
144-
<th
145-
onClick={() => resort("status")}
146-
className={sort.by === "status" ? styles[sort.order] : undefined}
147-
>
141+
</SortableHeader>
142+
<SortableHeader sortKey="status" currentSort={sort} onSort={resort} styles={styles}>
148143
PRs made
149-
</th>
150-
<th
151-
onClick={() => resort("done")}
152-
className={sort.by === "done" ? styles[sort.order] : undefined}
153-
>
144+
</SortableHeader>
145+
<SortableHeader sortKey="done" currentSort={sort} onSort={resort} styles={styles}>
154146
Done
155-
</th>
156-
<th
157-
onClick={() => resort("in-pr")}
158-
className={sort.by === "in-pr" ? styles[sort.order] : undefined}
159-
>
147+
</SortableHeader>
148+
<SortableHeader sortKey="in-pr" currentSort={sort} onSort={resort} styles={styles}>
160149
In PR
161-
</th>
162-
<th
163-
onClick={() => resort("awaiting-pr")}
164-
className={sort.by === "awaiting-pr" ? styles[sort.order] : undefined}
165-
>
150+
</SortableHeader>
151+
<SortableHeader sortKey="awaiting-pr" currentSort={sort} onSort={resort} styles={styles}>
166152
Awaiting PR
167-
</th>
168-
<th
169-
onClick={() => resort("awaiting-parents")}
170-
className={sort.by === "awaiting-parents" ? styles[sort.order] : undefined}
171-
>
153+
</SortableHeader>
154+
<SortableHeader sortKey="awaiting-parents" currentSort={sort} onSort={resort} styles={styles}>
172155
Awaiting parents
173-
</th>
174-
<th
175-
onClick={() => resort("not-solvable")}
176-
className={sort.by === "not-solvable" ? styles[sort.order] : undefined}
177-
>
156+
</SortableHeader>
157+
<SortableHeader sortKey="not-solvable" currentSort={sort} onSort={resort} styles={styles}>
178158
Not solvable
179-
</th>
180-
<th
181-
onClick={() => resort("bot-error")}
182-
className={sort.by === "bot-error" ? styles[sort.order] : undefined}
183-
>
159+
</SortableHeader>
160+
<SortableHeader sortKey="bot-error" currentSort={sort} onSort={resort} styles={styles}>
184161
Bot error
185-
</th>
162+
</SortableHeader>
186163
</tr>
187164
</thead>
188165
<tbody className={collapsed ? styles.collapsed : undefined}>
@@ -230,20 +207,81 @@ function TableContent({ collapsed, name, resort, rows, select, sort, fetched })
230207
}
231208

232209
// Returns a comparator function for sorting table columns.
233-
function compare(by, order) {
210+
// Supports secondary sorting: when primary values are equal, falls back to previousSort
211+
export function compare(by, order, previousSort = null) {
212+
const secondaryComparator = previousSort ? compare(previousSort.by, previousSort.order, null) : null;
213+
214+
const applySecondarySort = (primaryResult, a, b) => {
215+
if (primaryResult !== 0 || !secondaryComparator) return primaryResult;
216+
return secondaryComparator(a, b);
217+
};
218+
234219
switch (by) {
235220
case "name":
236-
return order === "ascending"
237-
? (a, b) => a.name.localeCompare(b.name)
238-
: (a, b) => b.name.localeCompare(a.name);
221+
return (a, b) => {
222+
const result = order === "ascending"
223+
? a.name.localeCompare(b.name)
224+
: b.name.localeCompare(a.name);
225+
return applySecondarySort(result, a, b);
226+
};
239227
case "status":
240-
return order === "ascending"
241-
? (a, b) => a.progress.percentage - b.progress.percentage
242-
: (a, b) => b.progress.percentage - a.progress.percentage;
228+
return (a, b) => {
229+
const result = order === "ascending"
230+
? a.progress.percentage - b.progress.percentage
231+
: b.progress.percentage - a.progress.percentage;
232+
return applySecondarySort(result, a, b);
233+
};
234+
case "migration_status":
235+
return (a, b) => {
236+
const result = order === "ascending"
237+
? (a.migration_status_order ?? 999) - (b.migration_status_order ?? 999)
238+
: (b.migration_status_order ?? 999) - (a.migration_status_order ?? 999);
239+
return applySecondarySort(result, a, b);
240+
};
241+
case "ci_status":
242+
return (a, b) => {
243+
const aOrder = a.ci_status_order ?? 999;
244+
const bOrder = b.ci_status_order ?? 999;
245+
let result;
246+
// Always put items with no CI status (order >= 999) last
247+
if (aOrder >= 999 && bOrder < 999) result = 1;
248+
else if (aOrder < 999 && bOrder >= 999) result = -1;
249+
else if (aOrder >= 999 && bOrder >= 999) result = 0;
250+
else {
251+
// Normal sorting for items with CI status
252+
result = order === "ascending" ? aOrder - bOrder : bOrder - aOrder;
253+
}
254+
return applySecondarySort(result, a, b);
255+
};
256+
case "num_descendants":
257+
return (a, b) => {
258+
const result = order === "ascending"
259+
? (a.num_descendants ?? 0) - (b.num_descendants ?? 0)
260+
: (b.num_descendants ?? 0) - (a.num_descendants ?? 0);
261+
return applySecondarySort(result, a, b);
262+
};
263+
case "updated_at":
264+
return (a, b) => {
265+
const aTimestamp = a.updated_at_timestamp ?? 0;
266+
const bTimestamp = b.updated_at_timestamp ?? 0;
267+
let result;
268+
// Always put items with no timestamp (0) last
269+
if (aTimestamp === 0 && bTimestamp !== 0) result = 1;
270+
else if (aTimestamp !== 0 && bTimestamp === 0) result = -1;
271+
else if (aTimestamp === 0 && bTimestamp === 0) result = 0;
272+
else {
273+
// Normal sorting for items with timestamps
274+
result = order === "ascending" ? aTimestamp - bTimestamp : bTimestamp - aTimestamp;
275+
}
276+
return applySecondarySort(result, a, b);
277+
};
243278
default:
244-
return order === "ascending"
245-
? (a, b) => a.details[by].length - b.details[by].length
246-
: (a, b) => b.details[by].length - a.details[by].length;
279+
return (a, b) => {
280+
const result = order === "ascending"
281+
? a.details[by].length - b.details[by].length
282+
: b.details[by].length - a.details[by].length;
283+
return applySecondarySort(result, a, b);
284+
};
247285
}
248286
}
249287

src/pages/status/migration/index.jsx

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Tooltip } from "react-tooltip";
1010
import Tabs from '@theme/Tabs';
1111
import TabItem from '@theme/TabItem';
1212
import moment from 'moment';
13+
import { compare } from '@site/src/components/StatusDashboard/current_migrations';
14+
import { useSorting, SortableHeader } from '@site/src/components/SortableTable';
1315

1416
// GitHub GraphQL MergeStateStatus documentation
1517
// Reference: https://docs.github.com/en/graphql/reference/enums#mergestatestatus
@@ -337,15 +339,26 @@ function Graph(props) {
337339
function Table({ details }) {
338340
const defaultFilters = ORDERED.reduce((filters, [status, _, toggled]) => ({ ...filters, [status]: toggled }), {});
339341
const [filters, setState] = useState(defaultFilters);
342+
const { sort, previousSort, resort } = useSorting("num_descendants", "descending");
340343
const feedstock = details._feedstock_status;
344+
345+
const CI_STATUS_ORDER = { clean: 0, behind: 1, has_hooks: 2, unknown: 3, unstable: 4, blocked: 5, dirty: 6, draft: 7, "": 999 };
346+
347+
// Transform data to match expected structure for compare function
341348
const rows = ORDERED.reduce((rows, [status]) => (
342349
filters[status] ? rows :
343-
rows.concat((details[status]).map(name => ([name, status])))
344-
), []).sort((a, b) => (
345-
feedstock[b[0]]["num_descendants"] - feedstock[a[0]]["num_descendants"]
346-
|| ORDERED.findIndex(x => x[0] == a[1]) - ORDERED.findIndex(x => x[0] == b[1])
347-
|| a[0].localeCompare(b[0]))
348-
);
350+
rows.concat((details[status]).map(name => {
351+
const feedstockData = feedstock[name];
352+
return {
353+
name,
354+
status,
355+
migration_status_order: ORDERED.findIndex(x => x[0] == status),
356+
ci_status_order: CI_STATUS_ORDER[feedstockData["pr_status"] || ""] ?? 8,
357+
num_descendants: feedstockData["num_descendants"] ?? 0,
358+
updated_at_timestamp: feedstockData["updated_at"] ? new Date(feedstockData["updated_at"]).getTime() : 0,
359+
};
360+
}))
361+
), []).sort(compare(sort.by, sort.order, previousSort));
349362

350363
return (
351364
<>
@@ -357,17 +370,27 @@ function Table({ details }) {
357370
{rows.length > 0 && <table>
358371
<thead>
359372
<tr>
360-
<th style={{ width: 200 }}>Name</th>
361-
<th style={{ width: 115 }}>Migration Status</th>
362-
<th style={{ width: 115 }}>CI Status</th>
363-
<th style={{ width: 115 }}>Last Updated</th>
364-
<th style={{ width: 115 }}>Total number of children</th>
373+
<SortableHeader sortKey="name" currentSort={sort} onSort={resort} styles={styles} style={{ width: 200 }}>
374+
Name
375+
</SortableHeader>
376+
<SortableHeader sortKey="migration_status" currentSort={sort} onSort={resort} styles={styles} style={{ width: 115 }}>
377+
Migration Status
378+
</SortableHeader>
379+
<SortableHeader sortKey="ci_status" currentSort={sort} onSort={resort} styles={styles} style={{ width: 115 }}>
380+
CI Status
381+
</SortableHeader>
382+
<SortableHeader sortKey="updated_at" currentSort={sort} onSort={resort} styles={styles} style={{ width: 115 }}>
383+
Last Updated
384+
</SortableHeader>
385+
<SortableHeader sortKey="num_descendants" currentSort={sort} onSort={resort} styles={styles} style={{ width: 115 }}>
386+
Total number of children
387+
</SortableHeader>
365388
<th style={{ flex: 1 }}>Immediate children</th>
366389
</tr>
367390
</thead>
368391
<tbody>
369-
{rows.map(([name, status], i) =>
370-
<Row key={i}>{{ feedstock: feedstock[name], name, status }}</Row>
392+
{rows.map((row, i) =>
393+
<Row key={i}>{{ feedstock: feedstock[row.name], name: row.name, status: row.status }}</Row>
371394
)}
372395
</tbody>
373396
</table>}

src/pages/status/migration/styles.module.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,17 @@
193193
align-items: center;
194194
gap: 8px;
195195
}
196+
197+
.migration_details table th {
198+
position: relative;
199+
}
200+
201+
.migration_details table th.ascending::after {
202+
content: " △";
203+
font-size: smaller;
204+
}
205+
206+
.migration_details table th.descending::after {
207+
content: " ▽";
208+
font-size: smaller;
209+
}

0 commit comments

Comments
 (0)