@d3lm/pr-stats 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/tui-app.mjs +656 -256
- package/package.json +1 -1
package/dist/tui-app.mjs
CHANGED
|
@@ -726,7 +726,19 @@ function wallParts(instantMs) {
|
|
|
726
726
|
function zonedStamp(date) {
|
|
727
727
|
const parts = wallParts(date.getTime());
|
|
728
728
|
const dayUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day);
|
|
729
|
-
return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour };
|
|
729
|
+
return { dayUtcMs, weekday: new Date(dayUtcMs).getUTCDay(), hour: parts.hour, minute: parts.minute };
|
|
730
|
+
}
|
|
731
|
+
function hasWorkWindows() {
|
|
732
|
+
const covered = timeMode.workWindows.reduce((sum, window) => sum + (window.endMin - window.startMin), 0);
|
|
733
|
+
return covered < 24 * 60;
|
|
734
|
+
}
|
|
735
|
+
function classifyInstant(date) {
|
|
736
|
+
const { weekday, hour, minute } = zonedStamp(date);
|
|
737
|
+
if (weekday === 0 || weekday === 6) {
|
|
738
|
+
return "weekend";
|
|
739
|
+
}
|
|
740
|
+
const minuteOfDay = hour * 60 + minute;
|
|
741
|
+
return timeMode.workWindows.some((window) => minuteOfDay >= window.startMin && minuteOfDay < window.endMin) ? "work" : "after";
|
|
730
742
|
}
|
|
731
743
|
function utcFromWall(wallTargetMs) {
|
|
732
744
|
let guess = wallTargetMs;
|
|
@@ -780,13 +792,32 @@ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Da
|
|
|
780
792
|
pr: result.pr,
|
|
781
793
|
requestedAt: result.requestedAt,
|
|
782
794
|
reviewedAt: result.reviewedAt,
|
|
783
|
-
hours: durationHours(result.requestedAt, result.reviewedAt)
|
|
795
|
+
hours: durationHours(result.requestedAt, result.reviewedAt),
|
|
796
|
+
verdict: result.verdict
|
|
784
797
|
});
|
|
785
798
|
} else if (result.kind === "pending" && result.pr.state === "open") {
|
|
786
799
|
pending.push({ pr: result.pr, requestedAt: result.requestedAt, hours: durationHours(result.requestedAt, now) });
|
|
787
800
|
}
|
|
788
801
|
}
|
|
789
802
|
pending.sort((a, b) => a.requestedAt.getTime() - b.requestedAt.getTime());
|
|
803
|
+
const pendingKeys = new Set(pending.map((entry) => `${entry.pr.repo}#${entry.pr.number}`));
|
|
804
|
+
const latestReviews = /* @__PURE__ */ new Map();
|
|
805
|
+
for (const result of results) {
|
|
806
|
+
if (result.kind !== "reviewed" && result.kind !== "unrequested" || result.pr.state !== "open") {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
810
|
+
if (pendingKeys.has(key)) {
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const latest = latestReviews.get(key);
|
|
814
|
+
if (latest === void 0 || result.reviewedAt > latest.reviewedAt) {
|
|
815
|
+
latestReviews.set(key, { pr: result.pr, reviewedAt: result.reviewedAt });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
const reviewing = [...latestReviews.values()].map(({ pr, reviewedAt }) => {
|
|
819
|
+
return { pr, reviewedAt, hours: durationHours(reviewedAt, now) };
|
|
820
|
+
}).toSorted((a, b) => a.reviewedAt.getTime() - b.reviewedAt.getTime());
|
|
790
821
|
const expired = results.filter((result) => result.kind === "pending" && result.pr.state !== "open");
|
|
791
822
|
const unrequested = results.filter((result) => result.kind === "unrequested");
|
|
792
823
|
const allHours = reviewed.map((result) => result.hours);
|
|
@@ -798,7 +829,22 @@ function computeReviewStats(results, { targetHours, now = /* @__PURE__ */ new Da
|
|
|
798
829
|
}
|
|
799
830
|
const byRepo = [...hoursByRepo.entries()].toSorted((a, b) => b[1].length - a[1].length);
|
|
800
831
|
const misses = targetHours === void 0 ? [] : reviewed.filter((result) => result.hours > targetHours).toSorted((a, b) => b.hours - a.hours);
|
|
801
|
-
|
|
832
|
+
const cyclesByPr = /* @__PURE__ */ new Map();
|
|
833
|
+
for (const result of reviewed) {
|
|
834
|
+
const key = `${result.pr.repo}#${result.pr.number}`;
|
|
835
|
+
cyclesByPr.set(key, (cyclesByPr.get(key) ?? 0) + 1);
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
reviewed,
|
|
839
|
+
pending,
|
|
840
|
+
reviewing,
|
|
841
|
+
expired,
|
|
842
|
+
unrequested,
|
|
843
|
+
allHours,
|
|
844
|
+
byRepo,
|
|
845
|
+
misses,
|
|
846
|
+
cycles: [...cyclesByPr.values()]
|
|
847
|
+
};
|
|
802
848
|
}
|
|
803
849
|
function computeSizeStats(sizes, { sizeTarget } = {}) {
|
|
804
850
|
const metrics = [
|
|
@@ -900,6 +946,13 @@ var FILE_BUCKETS = [
|
|
|
900
946
|
{ label: "21-50", max: 51 },
|
|
901
947
|
{ label: "> 50", max: Infinity }
|
|
902
948
|
];
|
|
949
|
+
var CYCLE_BUCKETS = [
|
|
950
|
+
{ label: "1", max: 2 },
|
|
951
|
+
{ label: "2", max: 3 },
|
|
952
|
+
{ label: "3", max: 4 },
|
|
953
|
+
{ label: "4-5", max: 6 },
|
|
954
|
+
{ label: "> 5", max: Infinity }
|
|
955
|
+
];
|
|
903
956
|
var COMMENT_BUCKETS = [
|
|
904
957
|
{ label: "0", max: 1 },
|
|
905
958
|
{ label: "1-2", max: 3 },
|
|
@@ -948,7 +1001,7 @@ function toPrRows(entries, leads) {
|
|
|
948
1001
|
|
|
949
1002
|
// src/tui/views/queue.ts
|
|
950
1003
|
function queueRows(view) {
|
|
951
|
-
return view.lists.flatMap((list) => list.rows);
|
|
1004
|
+
return view.sections.flatMap((section) => [...section.rows, ...section.lists.flatMap((list) => list.rows)]);
|
|
952
1005
|
}
|
|
953
1006
|
function groupedLists(entries, rowsOf2) {
|
|
954
1007
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -962,23 +1015,26 @@ function groupedLists(entries, rowsOf2) {
|
|
|
962
1015
|
});
|
|
963
1016
|
}
|
|
964
1017
|
function buildPendingReviewView(raw, repo = null, grouped = false) {
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
}
|
|
1018
|
+
const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
|
|
1019
|
+
const awaiting = repo === null ? stats.pending : stats.pending.filter((entry) => entry.pr.repo === repo);
|
|
1020
|
+
const reviewing = repo === null ? stats.reviewing : stats.reviewing.filter((entry) => entry.pr.repo === repo);
|
|
1021
|
+
if (awaiting.length === 0 && reviewing.length === 0) {
|
|
1022
|
+
return { empty: "No PRs are awaiting your review, and none you reviewed are still open.", sections: [] };
|
|
1023
|
+
}
|
|
1024
|
+
const split = repo === null && grouped;
|
|
1025
|
+
const sectionOf = (title, entries) => split ? { title, rows: [], lists: groupedLists(entries, rowsOf) } : { title, rows: rowsOf(entries), lists: [] };
|
|
973
1026
|
return {
|
|
974
1027
|
empty: null,
|
|
975
|
-
|
|
1028
|
+
sections: [
|
|
1029
|
+
...awaiting.length === 0 ? [] : [sectionOf(`Awaiting your review (n=${awaiting.length})`, awaiting)],
|
|
1030
|
+
...reviewing.length === 0 ? [] : [sectionOf(`Reviewing (n=${reviewing.length})`, reviewing)]
|
|
1031
|
+
]
|
|
976
1032
|
};
|
|
977
1033
|
}
|
|
978
1034
|
function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
979
1035
|
const open = raw.sizes.filter((entry) => entry.pr.state === "open" && (repo === null || entry.pr.repo === repo)).toSorted((a, b) => a.pr.createdAt.getTime() - b.pr.createdAt.getTime());
|
|
980
1036
|
if (open.length === 0) {
|
|
981
|
-
return { empty: "No open authored PRs found.",
|
|
1037
|
+
return { empty: "No open authored PRs found.", sections: [] };
|
|
982
1038
|
}
|
|
983
1039
|
const rowsOf2 = (group) => {
|
|
984
1040
|
const ages = group.map((entry) => durationLead({ hours: durationHours(entry.pr.createdAt, raw.fetchedAt) }));
|
|
@@ -990,13 +1046,11 @@ function buildOpenAuthoredView(raw, repo = null, grouped = false) {
|
|
|
990
1046
|
)
|
|
991
1047
|
);
|
|
992
1048
|
};
|
|
1049
|
+
const title = `Your open authored PRs (n=${open.length})`;
|
|
993
1050
|
if (repo === null && grouped) {
|
|
994
|
-
return { empty: null, lists: groupedLists(open, rowsOf2) };
|
|
1051
|
+
return { empty: null, sections: [{ title, rows: [], lists: groupedLists(open, rowsOf2) }] };
|
|
995
1052
|
}
|
|
996
|
-
return {
|
|
997
|
-
empty: null,
|
|
998
|
-
lists: [{ title: `Your open authored PRs (n=${open.length})`, rows: rowsOf2(open) }]
|
|
999
|
-
};
|
|
1053
|
+
return { empty: null, sections: [{ title, rows: rowsOf2(open), lists: [] }] };
|
|
1000
1054
|
}
|
|
1001
1055
|
function rowsOf(group) {
|
|
1002
1056
|
return toPrRows(
|
|
@@ -1075,18 +1129,18 @@ function ChartsPanel({
|
|
|
1075
1129
|
if (view.empty !== null) {
|
|
1076
1130
|
return /* @__PURE__ */ jsx4("box", { flexGrow: 1, alignItems: "center", justifyContent: "center", children: /* @__PURE__ */ jsx4("text", { fg: theme.muted, children: view.empty }) });
|
|
1077
1131
|
}
|
|
1078
|
-
const
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
return { key:
|
|
1132
|
+
const left = view.cards.filter((_, i) => i % 2 === 0);
|
|
1133
|
+
const right = view.cards.filter((_, i) => i % 2 === 1);
|
|
1134
|
+
const leftWidth = Math.max(0, ...left.map((card) => cardWidth(card)));
|
|
1135
|
+
const rightWidth = Math.max(0, ...right.map((card) => cardWidth(card)));
|
|
1136
|
+
const twoColumns = right.length > 0 && width - 4 >= leftWidth + COLUMN_GAP + rightWidth;
|
|
1137
|
+
const rows = left.map((card, i) => {
|
|
1138
|
+
return { key: card.title, left: card, right: right.at(i) };
|
|
1085
1139
|
});
|
|
1086
1140
|
const cards = twoColumns ? /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children: rows.map((row) => /* @__PURE__ */ jsxs3("box", { flexDirection: "row", alignItems: "flex-start", columnGap: COLUMN_GAP, children: [
|
|
1087
|
-
/* @__PURE__ */ jsx4("box", { flexDirection: "column", width: leftWidth, flexShrink: 0, children:
|
|
1141
|
+
/* @__PURE__ */ jsx4("box", { flexDirection: "column", width: leftWidth, flexShrink: 0, children: /* @__PURE__ */ jsx4(ChartCard, { card: row.left }) }),
|
|
1088
1142
|
/* @__PURE__ */ jsx4("box", { flexDirection: "column", width: rightWidth, flexShrink: 0, children: row.right !== void 0 && /* @__PURE__ */ jsx4(ChartCard, { card: row.right }) })
|
|
1089
|
-
] }, row.key)) }) : /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children:
|
|
1143
|
+
] }, row.key)) }) : /* @__PURE__ */ jsx4("box", { flexDirection: "column", rowGap: 1, children: view.cards.map((card) => /* @__PURE__ */ jsx4(ChartCard, { card }, card.title)) });
|
|
1090
1144
|
return /* @__PURE__ */ jsxs3("box", { flexGrow: 1, flexDirection: "column", children: [
|
|
1091
1145
|
/* @__PURE__ */ jsx4("box", { height: 1, children: /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.border, children: "\u2500".repeat(width) }) }),
|
|
1092
1146
|
/* @__PURE__ */ jsx4("box", { height: 1, paddingLeft: 1, paddingRight: 2, flexDirection: "row", justifyContent: "space-between", children: keyed(view.strip, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) }),
|
|
@@ -1127,7 +1181,7 @@ function ChartsPanel({
|
|
|
1127
1181
|
] }),
|
|
1128
1182
|
/* @__PURE__ */ jsx4("box", { flexDirection: "column", marginTop: 1, children: keyed(view.distribution.lines, lineKey).map(({ key, item }) => /* @__PURE__ */ jsx4(ChartLine, { line: item }, key)) })
|
|
1129
1183
|
] }),
|
|
1130
|
-
view.
|
|
1184
|
+
view.cards.length === 0 ? /* @__PURE__ */ jsx4("text", { wrapMode: "none", fg: theme.muted, marginTop: 1, children: view.noCharts }) : cards
|
|
1131
1185
|
]
|
|
1132
1186
|
}
|
|
1133
1187
|
)
|
|
@@ -1210,7 +1264,7 @@ function QueuePanel({
|
|
|
1210
1264
|
heading,
|
|
1211
1265
|
warning,
|
|
1212
1266
|
empty,
|
|
1213
|
-
|
|
1267
|
+
sections,
|
|
1214
1268
|
cursor,
|
|
1215
1269
|
onRefClick
|
|
1216
1270
|
}) {
|
|
@@ -1237,10 +1291,57 @@ function QueuePanel({
|
|
|
1237
1291
|
}
|
|
1238
1292
|
const offsets = [];
|
|
1239
1293
|
let total = 0;
|
|
1240
|
-
for (const
|
|
1241
|
-
|
|
1242
|
-
total +=
|
|
1243
|
-
|
|
1294
|
+
for (const section of sections) {
|
|
1295
|
+
const entry = { rows: total, lists: [] };
|
|
1296
|
+
total += section.rows.length;
|
|
1297
|
+
for (const list of section.lists) {
|
|
1298
|
+
entry.lists.push(total);
|
|
1299
|
+
total += list.rows.length;
|
|
1300
|
+
}
|
|
1301
|
+
offsets.push(entry);
|
|
1302
|
+
}
|
|
1303
|
+
const renderRow = (row, index, indent) => {
|
|
1304
|
+
const isSelected = index === cursor;
|
|
1305
|
+
const bg = isSelected ? theme.selectedBg : void 0;
|
|
1306
|
+
const refStart = indent.length + 2 + row.lead.length + 2;
|
|
1307
|
+
return /* @__PURE__ */ jsxs5(
|
|
1308
|
+
"text",
|
|
1309
|
+
{
|
|
1310
|
+
id: `queue-row-${index}`,
|
|
1311
|
+
wrapMode: "none",
|
|
1312
|
+
onMouseDown: onRefClick === null ? void 0 : (event) => {
|
|
1313
|
+
lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
|
|
1314
|
+
},
|
|
1315
|
+
onMouseUp: onRefClick === null ? void 0 : (event) => {
|
|
1316
|
+
const down = lastDown.current;
|
|
1317
|
+
lastDown.current = null;
|
|
1318
|
+
if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
|
|
1322
|
+
if (local >= refStart && local < refStart + row.ref.length) {
|
|
1323
|
+
onRefClick(row);
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1326
|
+
children: [
|
|
1327
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.accent, children: [
|
|
1328
|
+
indent,
|
|
1329
|
+
isSelected ? "\u25B8 " : " "
|
|
1330
|
+
] }),
|
|
1331
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1332
|
+
row.lead,
|
|
1333
|
+
" "
|
|
1334
|
+
] }),
|
|
1335
|
+
onRefClick === null ? /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }) : /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: row.ref }),
|
|
1336
|
+
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1337
|
+
" ",
|
|
1338
|
+
row.title
|
|
1339
|
+
] })
|
|
1340
|
+
]
|
|
1341
|
+
},
|
|
1342
|
+
row.url
|
|
1343
|
+
);
|
|
1344
|
+
};
|
|
1244
1345
|
return /* @__PURE__ */ jsxs5("box", { flexGrow: 1, flexDirection: "column", children: [
|
|
1245
1346
|
header,
|
|
1246
1347
|
/* @__PURE__ */ jsxs5(
|
|
@@ -1253,49 +1354,19 @@ function QueuePanel({
|
|
|
1253
1354
|
verticalScrollbarOptions: overlayScrollbar,
|
|
1254
1355
|
children: [
|
|
1255
1356
|
warning !== null && /* @__PURE__ */ jsx6("text", { wrapMode: "word", fg: theme.warn, marginTop: 1, children: warning }),
|
|
1256
|
-
|
|
1257
|
-
/* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.accent, children:
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
lastDown.current = event.button === 0 ? { x: event.x, y: event.y } : null;
|
|
1270
|
-
},
|
|
1271
|
-
onMouseUp: onRefClick === null ? void 0 : (event) => {
|
|
1272
|
-
const down = lastDown.current;
|
|
1273
|
-
lastDown.current = null;
|
|
1274
|
-
if (event.button !== 0 || down?.x !== event.x || down.y !== event.y) {
|
|
1275
|
-
return;
|
|
1276
|
-
}
|
|
1277
|
-
const local = event.currentTarget === null ? -1 : event.x - event.currentTarget.x;
|
|
1278
|
-
if (local >= refStart && local < refStart + row.ref.length) {
|
|
1279
|
-
onRefClick(row);
|
|
1280
|
-
}
|
|
1281
|
-
},
|
|
1282
|
-
children: [
|
|
1283
|
-
/* @__PURE__ */ jsx6("span", { fg: theme.accent, children: isSelected ? "\u25B8 " : " " }),
|
|
1284
|
-
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1285
|
-
row.lead,
|
|
1286
|
-
" "
|
|
1287
|
-
] }),
|
|
1288
|
-
onRefClick === null ? /* @__PURE__ */ jsx6("a", { href: row.url, fg: theme.accent, bg, children: row.ref }) : /* @__PURE__ */ jsx6("span", { fg: theme.accent, bg, children: row.ref }),
|
|
1289
|
-
/* @__PURE__ */ jsxs5("span", { fg: theme.text, bg, children: [
|
|
1290
|
-
" ",
|
|
1291
|
-
row.title
|
|
1292
|
-
] })
|
|
1293
|
-
]
|
|
1294
|
-
},
|
|
1295
|
-
row.url
|
|
1296
|
-
);
|
|
1297
|
-
})
|
|
1298
|
-
] }, list.title))
|
|
1357
|
+
sections.map((section, sectionIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: 1, children: [
|
|
1358
|
+
/* @__PURE__ */ jsx6("text", { wrapMode: "none", fg: theme.accent, children: section.title }),
|
|
1359
|
+
section.rows.map((row, rowIndex) => renderRow(row, offsets[sectionIndex].rows + rowIndex, "")),
|
|
1360
|
+
section.lists.map((list, listIndex) => /* @__PURE__ */ jsxs5("box", { flexDirection: "column", marginTop: listIndex > 0 ? 1 : 0, children: [
|
|
1361
|
+
/* @__PURE__ */ jsxs5("text", { wrapMode: "none", fg: theme.accent, children: [
|
|
1362
|
+
" ",
|
|
1363
|
+
list.title
|
|
1364
|
+
] }),
|
|
1365
|
+
list.rows.map(
|
|
1366
|
+
(row, rowIndex) => renderRow(row, offsets[sectionIndex].lists[listIndex] + rowIndex, " ")
|
|
1367
|
+
)
|
|
1368
|
+
] }, list.title))
|
|
1369
|
+
] }, section.title))
|
|
1299
1370
|
]
|
|
1300
1371
|
}
|
|
1301
1372
|
)
|
|
@@ -1482,7 +1553,7 @@ function QueueTab({
|
|
|
1482
1553
|
heading: repos.length > 0 ? scope.repo ?? (grouped ? "All repos \xB7 grouped by repo" : "All repos") : null,
|
|
1483
1554
|
warning,
|
|
1484
1555
|
empty: view.empty,
|
|
1485
|
-
|
|
1556
|
+
sections: view.sections,
|
|
1486
1557
|
cursor: Math.min(rowCursor, queueRows(view).length - 1),
|
|
1487
1558
|
onRefClick
|
|
1488
1559
|
}
|
|
@@ -2993,6 +3064,62 @@ function ColorRow({
|
|
|
2993
3064
|
] }) });
|
|
2994
3065
|
}
|
|
2995
3066
|
|
|
3067
|
+
// src/tui/components/Modals.tsx
|
|
3068
|
+
import { jsx as jsx14 } from "@opentui/react/jsx-runtime";
|
|
3069
|
+
function Modals({
|
|
3070
|
+
ui,
|
|
3071
|
+
options,
|
|
3072
|
+
saved: saved2,
|
|
3073
|
+
noCache: noCache2,
|
|
3074
|
+
copyLinks: copyLinks2,
|
|
3075
|
+
themeState,
|
|
3076
|
+
onDraft,
|
|
3077
|
+
onSubmitField,
|
|
3078
|
+
onSubmitThemeColor
|
|
3079
|
+
}) {
|
|
3080
|
+
if (ui.modal === "options") {
|
|
3081
|
+
return /* @__PURE__ */ jsx14(
|
|
3082
|
+
OptionsModal,
|
|
3083
|
+
{
|
|
3084
|
+
options,
|
|
3085
|
+
saved: saved2,
|
|
3086
|
+
selected: ui.selectedField,
|
|
3087
|
+
editing: ui.editing,
|
|
3088
|
+
fieldError: ui.fieldError,
|
|
3089
|
+
onDraft,
|
|
3090
|
+
onSubmit: onSubmitField
|
|
3091
|
+
}
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
if (ui.modal === "settings") {
|
|
3095
|
+
return /* @__PURE__ */ jsx14(
|
|
3096
|
+
SettingsModal,
|
|
3097
|
+
{
|
|
3098
|
+
selected: ui.selectedSetting,
|
|
3099
|
+
cacheAction: ui.cacheAction,
|
|
3100
|
+
noCache: noCache2,
|
|
3101
|
+
copyLinks: copyLinks2,
|
|
3102
|
+
preset: themeState.preset
|
|
3103
|
+
}
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3106
|
+
if (ui.modal === "theme") {
|
|
3107
|
+
return /* @__PURE__ */ jsx14(
|
|
3108
|
+
ThemeModal,
|
|
3109
|
+
{
|
|
3110
|
+
selected: ui.selectedThemeColor,
|
|
3111
|
+
editing: ui.editing,
|
|
3112
|
+
error: ui.themeColorError,
|
|
3113
|
+
cacheAction: ui.cacheAction,
|
|
3114
|
+
overrides: themeState.preset === "custom" ? themeState.overrides : {},
|
|
3115
|
+
onDraft,
|
|
3116
|
+
onSubmit: onSubmitThemeColor
|
|
3117
|
+
}
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
return null;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
2996
3123
|
// src/tui/hooks/useDeferredLoading.ts
|
|
2997
3124
|
import { useEffect as useEffect4, useRef as useRef3, useState as useState2 } from "react";
|
|
2998
3125
|
function useDeferredLoading(isLoading, { showDelay = 300, minDuration = 500 } = {}) {
|
|
@@ -3376,17 +3503,22 @@ function classifyPr(pr, details, user) {
|
|
|
3376
3503
|
(node) => node?.requestedReviewer?.login === user ? [new Date(node.createdAt)] : []
|
|
3377
3504
|
);
|
|
3378
3505
|
const reviews = details.reviews.nodes.flatMap(
|
|
3379
|
-
(node) => node?.author?.login === user && node.submittedAt ? [new Date(node.submittedAt)] : []
|
|
3506
|
+
(node) => node?.author?.login === user && node.submittedAt ? [{ at: new Date(node.submittedAt), state: node.state }] : []
|
|
3380
3507
|
);
|
|
3381
3508
|
if (requests.length === 0) {
|
|
3382
|
-
|
|
3509
|
+
if (reviews.length === 0) {
|
|
3510
|
+
return [{ kind: "inaccessible", pr }];
|
|
3511
|
+
}
|
|
3512
|
+
return [
|
|
3513
|
+
{ kind: "unrequested", pr, reviewedAt: new Date(Math.max(...reviews.map((review) => review.at.getTime()))) }
|
|
3514
|
+
];
|
|
3383
3515
|
}
|
|
3384
3516
|
const events = [
|
|
3385
3517
|
...requests.map((at) => {
|
|
3386
|
-
return { at, isRequest: true };
|
|
3518
|
+
return { at, isRequest: true, state: "" };
|
|
3387
3519
|
}),
|
|
3388
|
-
...reviews.map((at) => {
|
|
3389
|
-
return { at, isRequest: false };
|
|
3520
|
+
...reviews.map(({ at, state }) => {
|
|
3521
|
+
return { at, isRequest: false, state };
|
|
3390
3522
|
})
|
|
3391
3523
|
].toSorted((a, b) => a.at.getTime() - b.at.getTime() || Number(b.isRequest) - Number(a.isRequest));
|
|
3392
3524
|
const results = [];
|
|
@@ -3395,7 +3527,7 @@ function classifyPr(pr, details, user) {
|
|
|
3395
3527
|
if (event.isRequest) {
|
|
3396
3528
|
openedAt ??= event.at;
|
|
3397
3529
|
} else if (openedAt !== null) {
|
|
3398
|
-
results.push({ kind: "reviewed", pr, requestedAt: openedAt, reviewedAt: event.at });
|
|
3530
|
+
results.push({ kind: "reviewed", pr, requestedAt: openedAt, reviewedAt: event.at, verdict: event.state });
|
|
3399
3531
|
openedAt = null;
|
|
3400
3532
|
}
|
|
3401
3533
|
}
|
|
@@ -3492,6 +3624,9 @@ function reviveRawData(data) {
|
|
|
3492
3624
|
if (result.kind === "reviewed") {
|
|
3493
3625
|
return { ...result, pr, requestedAt: new Date(result.requestedAt), reviewedAt: new Date(result.reviewedAt) };
|
|
3494
3626
|
}
|
|
3627
|
+
if (result.kind === "unrequested") {
|
|
3628
|
+
return { ...result, pr, reviewedAt: new Date(result.reviewedAt) };
|
|
3629
|
+
}
|
|
3495
3630
|
return { ...result, pr };
|
|
3496
3631
|
}),
|
|
3497
3632
|
sizes: data.sizes.map((entry) => {
|
|
@@ -3518,6 +3653,15 @@ function loadSnapshot(options) {
|
|
|
3518
3653
|
return null;
|
|
3519
3654
|
}
|
|
3520
3655
|
const data = reviveRawData(stored.data);
|
|
3656
|
+
if (data.reviewResults.some((result) => result.kind === "unrequested" && Number.isNaN(result.reviewedAt.getTime()))) {
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
const hasMissingVerdict = data.reviewResults.some((result) => {
|
|
3660
|
+
return result.kind === "reviewed" && result.verdict === void 0;
|
|
3661
|
+
});
|
|
3662
|
+
if (hasMissingVerdict) {
|
|
3663
|
+
return null;
|
|
3664
|
+
}
|
|
3521
3665
|
if (sinceIso === data.sinceIso) {
|
|
3522
3666
|
return data;
|
|
3523
3667
|
}
|
|
@@ -3726,24 +3870,42 @@ function buildSizeRepoOptions(raw) {
|
|
|
3726
3870
|
})
|
|
3727
3871
|
];
|
|
3728
3872
|
}
|
|
3729
|
-
function pendingDetail(
|
|
3730
|
-
|
|
3873
|
+
function pendingDetail(counts) {
|
|
3874
|
+
const awaiting = `${counts.awaiting} ${counts.awaiting === 1 ? "PR" : "PRs"} awaiting your review`;
|
|
3875
|
+
return awaiting + (counts.reviewing > 0 ? `, ${counts.reviewing} reviewing` : "");
|
|
3731
3876
|
}
|
|
3732
3877
|
function buildPendingRepoOptions(raw) {
|
|
3733
|
-
const
|
|
3878
|
+
const countsByRepo = /* @__PURE__ */ new Map();
|
|
3879
|
+
const countsOf = (repo) => {
|
|
3880
|
+
const counts = countsByRepo.get(repo) ?? { awaiting: 0, reviewing: 0 };
|
|
3881
|
+
countsByRepo.set(repo, counts);
|
|
3882
|
+
return counts;
|
|
3883
|
+
};
|
|
3734
3884
|
for (const result of raw.reviewResults) {
|
|
3735
|
-
|
|
3736
|
-
countByRepo.set(result.pr.repo, (countByRepo.get(result.pr.repo) ?? 0) + pending);
|
|
3885
|
+
countsOf(result.pr.repo);
|
|
3737
3886
|
}
|
|
3738
|
-
if (
|
|
3887
|
+
if (countsByRepo.size < 2) {
|
|
3739
3888
|
return [];
|
|
3740
3889
|
}
|
|
3741
|
-
const
|
|
3742
|
-
const
|
|
3890
|
+
const stats = computeReviewStats(raw.reviewResults, { now: raw.fetchedAt });
|
|
3891
|
+
for (const entry of stats.pending) {
|
|
3892
|
+
countsOf(entry.pr.repo).awaiting += 1;
|
|
3893
|
+
}
|
|
3894
|
+
for (const entry of stats.reviewing) {
|
|
3895
|
+
countsOf(entry.pr.repo).reviewing += 1;
|
|
3896
|
+
}
|
|
3897
|
+
const entries = [...countsByRepo.entries()].toSorted(
|
|
3898
|
+
(a, b) => b[1].awaiting - a[1].awaiting || b[1].reviewing - a[1].reviewing || a[0].localeCompare(b[0])
|
|
3899
|
+
);
|
|
3900
|
+
const totals = { awaiting: 0, reviewing: 0 };
|
|
3901
|
+
for (const [, counts] of entries) {
|
|
3902
|
+
totals.awaiting += counts.awaiting;
|
|
3903
|
+
totals.reviewing += counts.reviewing;
|
|
3904
|
+
}
|
|
3743
3905
|
return [
|
|
3744
|
-
{ repo: null, label: "All repos", detail: pendingDetail(
|
|
3745
|
-
...entries.map(([repo,
|
|
3746
|
-
return { repo, label: repo, detail: pendingDetail(
|
|
3906
|
+
{ repo: null, label: "All repos", detail: pendingDetail(totals) },
|
|
3907
|
+
...entries.map(([repo, counts]) => {
|
|
3908
|
+
return { repo, label: repo, detail: pendingDetail(counts) };
|
|
3747
3909
|
})
|
|
3748
3910
|
];
|
|
3749
3911
|
}
|
|
@@ -3883,6 +4045,137 @@ function hbar(fraction, width, color) {
|
|
|
3883
4045
|
return line;
|
|
3884
4046
|
}
|
|
3885
4047
|
|
|
4048
|
+
// src/tui/views/charts/bars.ts
|
|
4049
|
+
var BAR_WIDTH2 = 24;
|
|
4050
|
+
var MAX_BARS = 8;
|
|
4051
|
+
function buildBarsCard({ title, subtitle, rows, format }) {
|
|
4052
|
+
const shown = rows.slice(0, MAX_BARS);
|
|
4053
|
+
const max = Math.max(...shown.map((row) => row.value), 0);
|
|
4054
|
+
const labelWidth = Math.max(...shown.map((row) => row.label.length));
|
|
4055
|
+
const valueWidth = Math.max(...shown.map((row) => format(row.value).length));
|
|
4056
|
+
const lines = shown.map((row) => {
|
|
4057
|
+
const line = [{ text: `${row.label.padEnd(labelWidth)} `, fg: theme.muted }];
|
|
4058
|
+
if (row.value <= 0 || max <= 0) {
|
|
4059
|
+
line.push({ text: " ".repeat(BAR_WIDTH2) });
|
|
4060
|
+
} else {
|
|
4061
|
+
line.push(...hbar(row.value / max, BAR_WIDTH2, row.value === max ? theme.accent : theme.chartBar));
|
|
4062
|
+
}
|
|
4063
|
+
line.push(
|
|
4064
|
+
{ text: ` ${format(row.value).padStart(valueWidth)}`, fg: theme.text },
|
|
4065
|
+
{ text: ` ${row.detail}`, fg: theme.dim }
|
|
4066
|
+
);
|
|
4067
|
+
return line;
|
|
4068
|
+
});
|
|
4069
|
+
if (rows.length > shown.length) {
|
|
4070
|
+
lines.push([{ text: `+ ${rows.length - shown.length} more`, fg: theme.dim }]);
|
|
4071
|
+
}
|
|
4072
|
+
return { title, subtitle, lines };
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
// src/tui/views/charts/weeks.ts
|
|
4076
|
+
var DAY_MS = 864e5;
|
|
4077
|
+
var WEEK_MS = 7 * DAY_MS;
|
|
4078
|
+
function mondayOf(dayUtcMs) {
|
|
4079
|
+
return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS;
|
|
4080
|
+
}
|
|
4081
|
+
function dateLabel(ms) {
|
|
4082
|
+
return new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
4083
|
+
}
|
|
4084
|
+
function weekAxisRow(width, prefix, points, columnOf, mondayOfPoint) {
|
|
4085
|
+
const cells = blankCells(width);
|
|
4086
|
+
const every = Math.ceil(points / 4);
|
|
4087
|
+
let lastEnd = -2;
|
|
4088
|
+
for (let point = 0; point < points; point += every) {
|
|
4089
|
+
const label = dateLabel(mondayOfPoint(point));
|
|
4090
|
+
const at = prefix + columnOf(point);
|
|
4091
|
+
if (at >= lastEnd + 2 && at + label.length <= width) {
|
|
4092
|
+
placeText(cells, at, label, theme.dim);
|
|
4093
|
+
lastEnd = at + label.length;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
return mergeCells(cells);
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// src/tui/views/charts/cumulative.ts
|
|
4100
|
+
var CUM_WIDTH = 36;
|
|
4101
|
+
var CUM_HEIGHT = 8;
|
|
4102
|
+
function weekOf(date) {
|
|
4103
|
+
return mondayOf(zonedStamp(date).dayUtcMs);
|
|
4104
|
+
}
|
|
4105
|
+
function buildCumulativeCard({ title, series, legend }) {
|
|
4106
|
+
const subtitle = series.flatMap((entry, i) => [
|
|
4107
|
+
{ text: `${i === 0 ? "" : " "}\u2500\u2500`, fg: entry.color },
|
|
4108
|
+
{ text: ` ${entry.label}`, fg: theme.muted }
|
|
4109
|
+
]);
|
|
4110
|
+
subtitle.push({ text: `, ${legend}`, fg: theme.muted });
|
|
4111
|
+
const mondays = series.flatMap((entry) => entry.dates.map((date) => weekOf(date)));
|
|
4112
|
+
const first = Math.min(...mondays);
|
|
4113
|
+
const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
|
|
4114
|
+
if (weekCount < 2) {
|
|
4115
|
+
return { title, subtitle, lines: [[{ text: "not enough weeks to draw a trend", fg: theme.muted }]] };
|
|
4116
|
+
}
|
|
4117
|
+
const totals = series.map((entry) => {
|
|
4118
|
+
const weekly = Array.from({ length: weekCount }, () => 0);
|
|
4119
|
+
for (const date of entry.dates) {
|
|
4120
|
+
weekly[(weekOf(date) - first) / WEEK_MS] += 1;
|
|
4121
|
+
}
|
|
4122
|
+
let running = 0;
|
|
4123
|
+
return weekly.map((count2) => running += count2);
|
|
4124
|
+
});
|
|
4125
|
+
const maxY = Math.max(...totals.map((cumulative) => cumulative.at(-1) ?? 0), 1);
|
|
4126
|
+
const grid = Array.from(
|
|
4127
|
+
{ length: CUM_HEIGHT },
|
|
4128
|
+
() => Array.from({ length: CUM_WIDTH }, () => null)
|
|
4129
|
+
);
|
|
4130
|
+
for (const [i, cumulative] of totals.entries()) {
|
|
4131
|
+
const rows = Array.from({ length: CUM_WIDTH }, (_, x) => {
|
|
4132
|
+
const weekPos = x / (CUM_WIDTH - 1) * (weekCount - 1);
|
|
4133
|
+
const week = Math.floor(weekPos);
|
|
4134
|
+
const nextWeek = Math.min(week + 1, weekCount - 1);
|
|
4135
|
+
const value2 = cumulative[week] + (cumulative[nextWeek] - cumulative[week]) * (weekPos - week);
|
|
4136
|
+
return CUM_HEIGHT - 1 - Math.round(value2 / maxY * (CUM_HEIGHT - 1));
|
|
4137
|
+
});
|
|
4138
|
+
const color = series[i].color;
|
|
4139
|
+
for (let x = 0; x < CUM_WIDTH; x++) {
|
|
4140
|
+
const here = rows[x];
|
|
4141
|
+
const next = rows[Math.min(x + 1, CUM_WIDTH - 1)];
|
|
4142
|
+
if (here === next) {
|
|
4143
|
+
grid[here][x] = { ch: "\u2500", fg: color };
|
|
4144
|
+
continue;
|
|
4145
|
+
}
|
|
4146
|
+
grid[next][x] = { ch: next < here ? "\u256D" : "\u2570", fg: color };
|
|
4147
|
+
grid[here][x] = { ch: next < here ? "\u256F" : "\u256E", fg: color };
|
|
4148
|
+
for (let row = Math.min(here, next) + 1; row < Math.max(here, next); row++) {
|
|
4149
|
+
grid[row][x] = { ch: "\u2502", fg: color };
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4153
|
+
const topLabel = String(maxY);
|
|
4154
|
+
const yWidth = topLabel.length;
|
|
4155
|
+
const lines = grid.map((row, i) => {
|
|
4156
|
+
const label = i === 0 ? topLabel : i === CUM_HEIGHT - 1 ? "0" : "";
|
|
4157
|
+
const line = [
|
|
4158
|
+
{ text: `${label.padStart(yWidth)} `, fg: theme.muted },
|
|
4159
|
+
{ text: label === "" ? "\u2502" : "\u2524", fg: theme.dim }
|
|
4160
|
+
];
|
|
4161
|
+
for (const cell of row) {
|
|
4162
|
+
line.push(cell === null ? { text: " " } : { text: cell.ch, fg: cell.fg });
|
|
4163
|
+
}
|
|
4164
|
+
return line;
|
|
4165
|
+
});
|
|
4166
|
+
lines.push(
|
|
4167
|
+
[{ text: `${" ".repeat(yWidth)} \u2514${"\u2500".repeat(CUM_WIDTH)}`, fg: theme.dim }],
|
|
4168
|
+
weekAxisRow(
|
|
4169
|
+
yWidth + 2 + CUM_WIDTH,
|
|
4170
|
+
yWidth + 2,
|
|
4171
|
+
weekCount,
|
|
4172
|
+
(week) => Math.round(week / (weekCount - 1) * (CUM_WIDTH - 1)),
|
|
4173
|
+
(week) => first + week * WEEK_MS
|
|
4174
|
+
)
|
|
4175
|
+
);
|
|
4176
|
+
return { title, subtitle, lines };
|
|
4177
|
+
}
|
|
4178
|
+
|
|
3886
4179
|
// src/tui/views/charts/distribution.ts
|
|
3887
4180
|
var DURATION_TICKS = [5 / 60, 0.25, 0.5, 1, 2, 4, 8, 24, 48, 96, 168, 336, 720, 2160];
|
|
3888
4181
|
var COUNT_TICKS = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1e3, 2500, 5e3, 1e4, 25e3, 5e4, 1e5];
|
|
@@ -4203,39 +4496,20 @@ function buildSpreadCard(title, metrics, format) {
|
|
|
4203
4496
|
|
|
4204
4497
|
// src/tui/views/charts/trend.ts
|
|
4205
4498
|
var asciichart = __toESM(require_asciichart(), 1);
|
|
4206
|
-
|
|
4207
|
-
// src/tui/views/charts/weeks.ts
|
|
4208
|
-
var DAY_MS = 864e5;
|
|
4209
|
-
var WEEK_MS = 7 * DAY_MS;
|
|
4210
|
-
function mondayOf(dayUtcMs) {
|
|
4211
|
-
return dayUtcMs - (new Date(dayUtcMs).getUTCDay() + 6) % 7 * DAY_MS;
|
|
4212
|
-
}
|
|
4213
|
-
function dateLabel(ms) {
|
|
4214
|
-
return new Date(ms).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
|
4215
|
-
}
|
|
4216
|
-
function weekAxisRow(width, prefix, points, columnOf, mondayOfPoint) {
|
|
4217
|
-
const cells = blankCells(width);
|
|
4218
|
-
const every = Math.ceil(points / 4);
|
|
4219
|
-
let lastEnd = -2;
|
|
4220
|
-
for (let point = 0; point < points; point += every) {
|
|
4221
|
-
const label = dateLabel(mondayOfPoint(point));
|
|
4222
|
-
const at = prefix + columnOf(point);
|
|
4223
|
-
if (at >= lastEnd + 2 && at + label.length <= width) {
|
|
4224
|
-
placeText(cells, at, label, theme.dim);
|
|
4225
|
-
lastEnd = at + label.length;
|
|
4226
|
-
}
|
|
4227
|
-
}
|
|
4228
|
-
return mergeCells(cells);
|
|
4229
|
-
}
|
|
4230
|
-
|
|
4231
|
-
// src/tui/views/charts/trend.ts
|
|
4232
4499
|
var TREND_POINTS = 40;
|
|
4233
4500
|
var TREND_HEIGHT = 6;
|
|
4234
4501
|
function axisSplit(line) {
|
|
4235
4502
|
const positions = [line.indexOf("\u2524"), line.indexOf("\u253C")].filter((at) => at >= 0);
|
|
4236
4503
|
return positions.length === 0 ? 0 : Math.min(...positions) + 1;
|
|
4237
4504
|
}
|
|
4238
|
-
function buildTrendCard({
|
|
4505
|
+
function buildTrendCard({
|
|
4506
|
+
title,
|
|
4507
|
+
entries,
|
|
4508
|
+
format,
|
|
4509
|
+
floor = 1,
|
|
4510
|
+
scale = "log",
|
|
4511
|
+
valueLabel = "median"
|
|
4512
|
+
}) {
|
|
4239
4513
|
const byWeek = /* @__PURE__ */ new Map();
|
|
4240
4514
|
for (const entry of entries) {
|
|
4241
4515
|
const monday = mondayOf(zonedStamp(entry.date).dayUtcMs);
|
|
@@ -4246,10 +4520,11 @@ function buildTrendCard({ title, entries, format, floor }) {
|
|
|
4246
4520
|
const mondays = [...byWeek.keys()];
|
|
4247
4521
|
const first = Math.min(...mondays);
|
|
4248
4522
|
const weekCount = (Math.max(...mondays) - first) / WEEK_MS + 1;
|
|
4523
|
+
const scaleSuffix = scale === "log" ? ", log scale" : "";
|
|
4249
4524
|
if (weekCount < 2) {
|
|
4250
4525
|
return {
|
|
4251
4526
|
title,
|
|
4252
|
-
subtitle:
|
|
4527
|
+
subtitle: `weekly ${valueLabel}${scaleSuffix}`,
|
|
4253
4528
|
lines: [[{ text: "not enough weeks to draw a trend", fg: theme.muted }]]
|
|
4254
4529
|
};
|
|
4255
4530
|
}
|
|
@@ -4267,9 +4542,11 @@ function buildTrendCard({ title, entries, format, floor }) {
|
|
|
4267
4542
|
medians.push(medians.at(-1) ?? 0);
|
|
4268
4543
|
}
|
|
4269
4544
|
}
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
medians[i]
|
|
4545
|
+
if (scale === "log") {
|
|
4546
|
+
const firstKnown = medians.find((value2) => value2 > 0) ?? 0;
|
|
4547
|
+
for (let i = 0; i < medians.length && medians[i] === 0; i++) {
|
|
4548
|
+
medians[i] = firstKnown;
|
|
4549
|
+
}
|
|
4273
4550
|
}
|
|
4274
4551
|
const chunk = Math.ceil(weekCount / TREND_POINTS);
|
|
4275
4552
|
const points = [];
|
|
@@ -4279,23 +4556,23 @@ function buildTrendCard({ title, entries, format, floor }) {
|
|
|
4279
4556
|
}
|
|
4280
4557
|
const stretch = Math.max(1, Math.floor(TREND_POINTS / points.length));
|
|
4281
4558
|
const series = points.flatMap((value2) => Array.from({ length: stretch }, () => value2));
|
|
4282
|
-
const
|
|
4283
|
-
let lo = Math.min(...
|
|
4284
|
-
let hi = Math.max(...
|
|
4559
|
+
const plotted = scale === "log" ? series.map((value2) => Math.log2(Math.max(value2, floor))) : series;
|
|
4560
|
+
let lo = Math.min(...plotted);
|
|
4561
|
+
let hi = Math.max(...plotted);
|
|
4285
4562
|
if (hi - lo < 1e-9) {
|
|
4286
4563
|
lo -= 1;
|
|
4287
4564
|
hi += 1;
|
|
4288
4565
|
}
|
|
4289
|
-
const chart = asciichart.plot(
|
|
4566
|
+
const chart = asciichart.plot(plotted, {
|
|
4290
4567
|
height: TREND_HEIGHT,
|
|
4291
4568
|
min: lo,
|
|
4292
4569
|
max: hi,
|
|
4293
|
-
format: (x) => format(2 ** x).padStart(6)
|
|
4570
|
+
format: (x) => format(scale === "log" ? 2 ** x : x).padStart(6)
|
|
4294
4571
|
});
|
|
4295
4572
|
const ratio = TREND_HEIGHT / (hi - lo);
|
|
4296
4573
|
const min2 = Math.round(lo * ratio);
|
|
4297
4574
|
const rows = Math.abs(Math.round(hi * ratio) - min2);
|
|
4298
|
-
const lastRow = Math.min(rows, Math.max(0, rows - (Math.round((
|
|
4575
|
+
const lastRow = Math.min(rows, Math.max(0, rows - (Math.round((plotted.at(-1) ?? 0) * ratio) - min2)));
|
|
4299
4576
|
const raw = chart.split("\n");
|
|
4300
4577
|
const lines = raw.map((line, i) => {
|
|
4301
4578
|
const split = axisSplit(line);
|
|
@@ -4318,7 +4595,7 @@ function buildTrendCard({ title, entries, format, floor }) {
|
|
|
4318
4595
|
(point) => first + point * chunk * WEEK_MS
|
|
4319
4596
|
)
|
|
4320
4597
|
);
|
|
4321
|
-
const subtitle = chunk === 1 ?
|
|
4598
|
+
const subtitle = chunk === 1 ? `weekly ${valueLabel}${scaleSuffix}` : `${valueLabel} per ${chunk} weeks${scaleSuffix}`;
|
|
4322
4599
|
return { title, subtitle, lines };
|
|
4323
4600
|
}
|
|
4324
4601
|
|
|
@@ -4386,6 +4663,79 @@ function countCell(count2, label, dimWhenZero = false) {
|
|
|
4386
4663
|
{ text: ` ${label}`, fg: dim2 ? theme.dim : theme.muted }
|
|
4387
4664
|
];
|
|
4388
4665
|
}
|
|
4666
|
+
function mondayNoon(monday) {
|
|
4667
|
+
return new Date(monday + 12 * 36e5);
|
|
4668
|
+
}
|
|
4669
|
+
function weeklySums(entries) {
|
|
4670
|
+
const byWeek = /* @__PURE__ */ new Map();
|
|
4671
|
+
for (const entry of entries) {
|
|
4672
|
+
const monday = mondayOf(zonedStamp(entry.date).dayUtcMs);
|
|
4673
|
+
byWeek.set(monday, (byWeek.get(monday) ?? 0) + entry.value);
|
|
4674
|
+
}
|
|
4675
|
+
const mondays = [...byWeek.keys()];
|
|
4676
|
+
const first = Math.min(...mondays);
|
|
4677
|
+
const last = Math.max(...mondays);
|
|
4678
|
+
const result = [];
|
|
4679
|
+
for (let monday = first; monday <= last; monday += WEEK_MS) {
|
|
4680
|
+
result.push({ date: mondayNoon(monday), value: byWeek.get(monday) ?? 0 });
|
|
4681
|
+
}
|
|
4682
|
+
return result;
|
|
4683
|
+
}
|
|
4684
|
+
function mergeRateEntries(stats) {
|
|
4685
|
+
const byWeek = /* @__PURE__ */ new Map();
|
|
4686
|
+
const add = (createdAt, merged) => {
|
|
4687
|
+
const monday = mondayOf(zonedStamp(createdAt).dayUtcMs);
|
|
4688
|
+
const counts = byWeek.get(monday) ?? { merged: 0, concluded: 0 };
|
|
4689
|
+
counts.concluded += 1;
|
|
4690
|
+
counts.merged += merged ? 1 : 0;
|
|
4691
|
+
byWeek.set(monday, counts);
|
|
4692
|
+
};
|
|
4693
|
+
for (const result of stats.merged) {
|
|
4694
|
+
add(result.entry.pr.createdAt, true);
|
|
4695
|
+
}
|
|
4696
|
+
for (const result of stats.closed) {
|
|
4697
|
+
add(result.entry.pr.createdAt, false);
|
|
4698
|
+
}
|
|
4699
|
+
return [...byWeek.entries()].map(([monday, counts]) => {
|
|
4700
|
+
return { date: mondayNoon(monday), value: counts.merged / counts.concluded * 100 };
|
|
4701
|
+
});
|
|
4702
|
+
}
|
|
4703
|
+
function buildOffHoursCard(subtitle, dates) {
|
|
4704
|
+
const counts = { work: 0, after: 0, weekend: 0 };
|
|
4705
|
+
for (const date of dates) {
|
|
4706
|
+
counts[classifyInstant(date)] += 1;
|
|
4707
|
+
}
|
|
4708
|
+
const rows = hasWorkWindows() ? [
|
|
4709
|
+
{ label: "work hours", count: counts.work, color: theme.accent },
|
|
4710
|
+
{ label: "after hours", count: counts.after, color: theme.warn },
|
|
4711
|
+
{ label: "weekend", count: counts.weekend, color: theme.chartDim }
|
|
4712
|
+
] : [
|
|
4713
|
+
{ label: "weekday", count: counts.work + counts.after, color: theme.accent },
|
|
4714
|
+
{ label: "weekend", count: counts.weekend, color: theme.chartDim }
|
|
4715
|
+
];
|
|
4716
|
+
return buildGaugeCard({ title: "Off-hours share", subtitle, rows });
|
|
4717
|
+
}
|
|
4718
|
+
function buildVerdictCard(reviewed) {
|
|
4719
|
+
const countOf = (state) => reviewed.filter((entry) => entry.verdict === state).length;
|
|
4720
|
+
const approved = countOf("APPROVED");
|
|
4721
|
+
const changes = countOf("CHANGES_REQUESTED");
|
|
4722
|
+
const commented = countOf("COMMENTED");
|
|
4723
|
+
const other = reviewed.length - approved - changes - commented;
|
|
4724
|
+
const counts = [
|
|
4725
|
+
{ label: "approved", count: approved },
|
|
4726
|
+
{ label: "changes requested", count: changes },
|
|
4727
|
+
{ label: "commented", count: commented },
|
|
4728
|
+
...other > 0 ? [{ label: "other", count: other }] : []
|
|
4729
|
+
];
|
|
4730
|
+
const max = Math.max(...counts.map((row) => row.count));
|
|
4731
|
+
return buildGaugeCard({
|
|
4732
|
+
title: "Review verdicts",
|
|
4733
|
+
subtitle: "how your requested reviews concluded",
|
|
4734
|
+
rows: counts.map((row) => {
|
|
4735
|
+
return { ...row, color: row.count === max ? theme.accent : theme.chartBar };
|
|
4736
|
+
})
|
|
4737
|
+
});
|
|
4738
|
+
}
|
|
4389
4739
|
function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100) {
|
|
4390
4740
|
const results = repo === null ? raw.reviewResults : raw.reviewResults.filter((result) => result.pr.repo === repo);
|
|
4391
4741
|
const stats = computeReviewStats(results, { targetHours, now: raw.fetchedAt });
|
|
@@ -4400,8 +4750,7 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4400
4750
|
headline: null,
|
|
4401
4751
|
distributionTitle: "Review time distribution",
|
|
4402
4752
|
noCharts: "No completed reviews to chart.",
|
|
4403
|
-
|
|
4404
|
-
right: [],
|
|
4753
|
+
cards: [],
|
|
4405
4754
|
distribution: null
|
|
4406
4755
|
};
|
|
4407
4756
|
if (results.length === 0) {
|
|
@@ -4414,8 +4763,15 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4414
4763
|
rows: toPrRows(stats.misses, stats.misses.map(durationLead))
|
|
4415
4764
|
});
|
|
4416
4765
|
}
|
|
4766
|
+
const pendingCard = stats.pending.length === 0 ? null : buildHistogramCard({
|
|
4767
|
+
title: "Pending request age",
|
|
4768
|
+
subtitle: "how long open requests have waited",
|
|
4769
|
+
values: stats.pending.map((entry) => entry.hours),
|
|
4770
|
+
buckets: currentBuckets(),
|
|
4771
|
+
format: formatDuration
|
|
4772
|
+
});
|
|
4417
4773
|
if (stats.reviewed.length === 0) {
|
|
4418
|
-
return { empty: null, ...base, lists };
|
|
4774
|
+
return { empty: null, ...base, cards: pendingCard === null ? [] : [pendingCard], lists };
|
|
4419
4775
|
}
|
|
4420
4776
|
const sorted = [...stats.allHours].toSorted((a, b) => a - b);
|
|
4421
4777
|
const total = raw.reviewResults.filter((result) => result.kind === "reviewed").length;
|
|
@@ -4428,7 +4784,39 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4428
4784
|
];
|
|
4429
4785
|
const requestDates = [...stats.reviewed, ...stats.pending].map((entry) => entry.requestedAt);
|
|
4430
4786
|
const reviewDates = stats.reviewed.map((entry) => entry.reviewedAt);
|
|
4431
|
-
const
|
|
4787
|
+
const requestAges = [...stats.reviewed, ...stats.pending].map(
|
|
4788
|
+
(entry) => durationHours(entry.pr.createdAt, entry.requestedAt)
|
|
4789
|
+
);
|
|
4790
|
+
let serviceCard = null;
|
|
4791
|
+
if (targetHours !== void 0 && targetLabel !== void 0) {
|
|
4792
|
+
const inside = stats.allHours.filter((value2) => value2 <= targetHours).length;
|
|
4793
|
+
const overdue = stats.pending.filter((entry) => entry.hours > targetHours).length;
|
|
4794
|
+
serviceCard = buildGaugeCard({
|
|
4795
|
+
title: "Service level",
|
|
4796
|
+
subtitle: `reviewed within ${targetLabel}`,
|
|
4797
|
+
rows: [
|
|
4798
|
+
{ label: `inside ${targetLabel}`, count: inside, color: theme.accent },
|
|
4799
|
+
{ label: `over ${targetLabel}`, count: stats.allHours.length - inside, color: theme.chartDim },
|
|
4800
|
+
...overdue > 0 ? [{ label: "awaiting and already over", count: overdue, color: theme.warn }] : []
|
|
4801
|
+
]
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
const byRepoCard = repo === null && stats.byRepo.length > 1 ? buildBarsCard({
|
|
4805
|
+
title: "Review time by repo",
|
|
4806
|
+
subtitle: "median review time, slowest first",
|
|
4807
|
+
rows: stats.byRepo.map(([name, hours]) => {
|
|
4808
|
+
return {
|
|
4809
|
+
label: name,
|
|
4810
|
+
value: percentile(
|
|
4811
|
+
hours.toSorted((a, b) => a - b),
|
|
4812
|
+
50
|
|
4813
|
+
),
|
|
4814
|
+
detail: `n=${hours.length}`
|
|
4815
|
+
};
|
|
4816
|
+
}).toSorted((a, b) => b.value - a.value),
|
|
4817
|
+
format: formatDuration
|
|
4818
|
+
}) : null;
|
|
4819
|
+
const cards = [
|
|
4432
4820
|
buildHistogramCard({
|
|
4433
4821
|
title: "Time to review",
|
|
4434
4822
|
subtitle: "elapsed time, request \u2192 review",
|
|
@@ -4436,6 +4824,14 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4436
4824
|
buckets: currentBuckets(),
|
|
4437
4825
|
format: formatDuration
|
|
4438
4826
|
}),
|
|
4827
|
+
buildTrendCard({
|
|
4828
|
+
title: "Review time trend",
|
|
4829
|
+
entries: stats.reviewed.map((entry) => {
|
|
4830
|
+
return { date: entry.reviewedAt, value: entry.hours };
|
|
4831
|
+
}),
|
|
4832
|
+
format: formatDuration,
|
|
4833
|
+
floor: 1 / 60
|
|
4834
|
+
}),
|
|
4439
4835
|
buildHeatmapCard({
|
|
4440
4836
|
title: "When you review",
|
|
4441
4837
|
subtitle: "reviews submitted, weekday \xD7 hour, local time",
|
|
@@ -4445,34 +4841,28 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4445
4841
|
{ label: "req", dates: requestDates, muted: true }
|
|
4446
4842
|
],
|
|
4447
4843
|
legend: "reviews in that hour"
|
|
4448
|
-
})
|
|
4449
|
-
];
|
|
4450
|
-
const right = [
|
|
4451
|
-
buildTrendCard({
|
|
4452
|
-
title: "Review time trend",
|
|
4453
|
-
entries: stats.reviewed.map((entry) => {
|
|
4454
|
-
return { date: entry.reviewedAt, value: entry.hours };
|
|
4455
|
-
}),
|
|
4456
|
-
format: formatDuration,
|
|
4457
|
-
floor: 1 / 60
|
|
4458
4844
|
}),
|
|
4459
|
-
buildVolumeCard("Reviews completed per week", reviewDates)
|
|
4845
|
+
buildVolumeCard("Reviews completed per week", reviewDates),
|
|
4846
|
+
buildHistogramCard({
|
|
4847
|
+
title: "Review cycles per PR",
|
|
4848
|
+
subtitle: "completed request \u2192 review rounds per PR",
|
|
4849
|
+
values: stats.cycles,
|
|
4850
|
+
buckets: CYCLE_BUCKETS,
|
|
4851
|
+
format: count
|
|
4852
|
+
}),
|
|
4853
|
+
buildHistogramCard({
|
|
4854
|
+
title: "PR age at request",
|
|
4855
|
+
subtitle: "elapsed time, PR created \u2192 review requested",
|
|
4856
|
+
values: requestAges,
|
|
4857
|
+
buckets: currentBuckets(),
|
|
4858
|
+
format: formatDuration
|
|
4859
|
+
}),
|
|
4860
|
+
buildVerdictCard(stats.reviewed),
|
|
4861
|
+
...pendingCard === null ? [] : [pendingCard],
|
|
4862
|
+
buildOffHoursCard("reviews submitted, local time", reviewDates),
|
|
4863
|
+
...serviceCard === null ? [] : [serviceCard],
|
|
4864
|
+
...byRepoCard === null ? [] : [byRepoCard]
|
|
4460
4865
|
];
|
|
4461
|
-
if (targetHours !== void 0 && targetLabel !== void 0) {
|
|
4462
|
-
const inside = stats.allHours.filter((value2) => value2 <= targetHours).length;
|
|
4463
|
-
const overdue = stats.pending.filter((entry) => entry.hours > targetHours).length;
|
|
4464
|
-
left.push(
|
|
4465
|
-
buildGaugeCard({
|
|
4466
|
-
title: "Service level",
|
|
4467
|
-
subtitle: `reviewed within ${targetLabel}`,
|
|
4468
|
-
rows: [
|
|
4469
|
-
{ label: `inside ${targetLabel}`, count: inside, color: theme.accent },
|
|
4470
|
-
{ label: `over ${targetLabel}`, count: stats.allHours.length - inside, color: theme.chartDim },
|
|
4471
|
-
...overdue > 0 ? [{ label: "awaiting and already over", count: overdue, color: theme.warn }] : []
|
|
4472
|
-
]
|
|
4473
|
-
})
|
|
4474
|
-
);
|
|
4475
|
-
}
|
|
4476
4866
|
const distribution = buildDistribution({
|
|
4477
4867
|
values: stats.allHours,
|
|
4478
4868
|
width: Math.max(width - 3, 40),
|
|
@@ -4480,7 +4870,7 @@ function buildReviewView(raw, targetHours, targetLabel, repo = null, width = 100
|
|
|
4480
4870
|
ticks: DURATION_TICKS,
|
|
4481
4871
|
flat: (count2, value2) => `all ${count2} ${count2 === 1 ? "review" : "reviews"} took ${value2}`
|
|
4482
4872
|
});
|
|
4483
|
-
return { empty: null, ...base, headline,
|
|
4873
|
+
return { empty: null, ...base, headline, cards, distribution, lists };
|
|
4484
4874
|
}
|
|
4485
4875
|
function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
4486
4876
|
const base = {
|
|
@@ -4488,8 +4878,7 @@ function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
|
4488
4878
|
headline: null,
|
|
4489
4879
|
distributionTitle: "PR size distribution",
|
|
4490
4880
|
noCharts: "No authored PRs to chart.",
|
|
4491
|
-
|
|
4492
|
-
right: [],
|
|
4881
|
+
cards: [],
|
|
4493
4882
|
distribution: null,
|
|
4494
4883
|
lists: []
|
|
4495
4884
|
};
|
|
@@ -4520,7 +4909,15 @@ function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
|
4520
4909
|
{ text: `${count(percentile(sorted, 90))} lines`, fg: theme.accent },
|
|
4521
4910
|
{ text: ` ${sizes.length} of ${raw.sizes.length} PRs`, fg: theme.muted }
|
|
4522
4911
|
];
|
|
4523
|
-
const
|
|
4912
|
+
const targetCard = stats.met !== void 0 && stats.targetLabel !== void 0 ? buildGaugeCard({
|
|
4913
|
+
title: "Size target",
|
|
4914
|
+
subtitle: `authored within ${stats.targetLabel}`,
|
|
4915
|
+
rows: [
|
|
4916
|
+
{ label: "inside target", count: stats.met, color: theme.accent },
|
|
4917
|
+
{ label: "over target", count: sizes.length - stats.met, color: theme.chartDim }
|
|
4918
|
+
]
|
|
4919
|
+
}) : null;
|
|
4920
|
+
const cards = [
|
|
4524
4921
|
buildHistogramCard({
|
|
4525
4922
|
title: "PR size",
|
|
4526
4923
|
subtitle: "total lines changed per authored PR",
|
|
@@ -4528,6 +4925,14 @@ function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
|
4528
4925
|
buckets: LINE_BUCKETS,
|
|
4529
4926
|
format: count
|
|
4530
4927
|
}),
|
|
4928
|
+
buildTrendCard({
|
|
4929
|
+
title: "PR size trend",
|
|
4930
|
+
entries: sizes.map((size) => {
|
|
4931
|
+
return { date: size.pr.createdAt, value: size.total };
|
|
4932
|
+
}),
|
|
4933
|
+
format: count,
|
|
4934
|
+
floor: 1
|
|
4935
|
+
}),
|
|
4531
4936
|
buildHistogramCard({
|
|
4532
4937
|
title: "Files touched",
|
|
4533
4938
|
subtitle: "files changed per authored PR",
|
|
@@ -4535,38 +4940,28 @@ function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
|
4535
4940
|
buckets: FILE_BUCKETS,
|
|
4536
4941
|
format: count
|
|
4537
4942
|
}),
|
|
4943
|
+
buildTrendCard({
|
|
4944
|
+
title: "Net lines trend",
|
|
4945
|
+
entries: weeklySums(
|
|
4946
|
+
sizes.map((size) => {
|
|
4947
|
+
return { date: size.pr.createdAt, value: size.additions - size.deletions };
|
|
4948
|
+
})
|
|
4949
|
+
),
|
|
4950
|
+
format: netCount,
|
|
4951
|
+
scale: "linear",
|
|
4952
|
+
valueLabel: "net lines"
|
|
4953
|
+
}),
|
|
4538
4954
|
buildHeatmapCard({
|
|
4539
4955
|
title: "When you open PRs",
|
|
4540
4956
|
subtitle: "PRs opened, weekday \xD7 hour, local time",
|
|
4541
4957
|
grid: created,
|
|
4542
4958
|
columns: [{ label: "opened", dates: created }],
|
|
4543
4959
|
legend: "PRs opened in that hour"
|
|
4544
|
-
})
|
|
4545
|
-
];
|
|
4546
|
-
const right = [
|
|
4547
|
-
buildTrendCard({
|
|
4548
|
-
title: "PR size trend",
|
|
4549
|
-
entries: sizes.map((size) => {
|
|
4550
|
-
return { date: size.pr.createdAt, value: size.total };
|
|
4551
|
-
}),
|
|
4552
|
-
format: count,
|
|
4553
|
-
floor: 1
|
|
4554
4960
|
}),
|
|
4555
4961
|
buildVolumeCard("PRs opened per week", created),
|
|
4962
|
+
...targetCard === null ? [] : [targetCard],
|
|
4556
4963
|
buildSpreadCard("Size spread", stats.metrics, count)
|
|
4557
4964
|
];
|
|
4558
|
-
if (stats.met !== void 0 && stats.targetLabel !== void 0) {
|
|
4559
|
-
left.push(
|
|
4560
|
-
buildGaugeCard({
|
|
4561
|
-
title: "Size target",
|
|
4562
|
-
subtitle: `authored within ${stats.targetLabel}`,
|
|
4563
|
-
rows: [
|
|
4564
|
-
{ label: "inside target", count: stats.met, color: theme.accent },
|
|
4565
|
-
{ label: "over target", count: sizes.length - stats.met, color: theme.chartDim }
|
|
4566
|
-
]
|
|
4567
|
-
})
|
|
4568
|
-
);
|
|
4569
|
-
}
|
|
4570
4965
|
const distribution = buildDistribution({
|
|
4571
4966
|
values: totals,
|
|
4572
4967
|
width: Math.max(width - 3, 40),
|
|
@@ -4584,7 +4979,7 @@ function buildSizeView(raw, sizeTarget, repo = null, width = 100) {
|
|
|
4584
4979
|
)
|
|
4585
4980
|
});
|
|
4586
4981
|
}
|
|
4587
|
-
return { empty: null, ...base, strip, headline,
|
|
4982
|
+
return { empty: null, ...base, strip, headline, cards, distribution, lists };
|
|
4588
4983
|
}
|
|
4589
4984
|
function buildCommentView(raw, repo = null, width = 100) {
|
|
4590
4985
|
const base = {
|
|
@@ -4592,8 +4987,7 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4592
4987
|
headline: null,
|
|
4593
4988
|
distributionTitle: "Comments per PR distribution",
|
|
4594
4989
|
noCharts: "No authored PRs to chart.",
|
|
4595
|
-
|
|
4596
|
-
right: [],
|
|
4990
|
+
cards: [],
|
|
4597
4991
|
distribution: null,
|
|
4598
4992
|
lists: []
|
|
4599
4993
|
};
|
|
@@ -4623,7 +5017,7 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4623
5017
|
{ text: `${count(percentile(sorted, 90))} comments`, fg: theme.accent },
|
|
4624
5018
|
{ text: ` ${sizes.length} of ${raw.sizes.length} PRs`, fg: theme.muted }
|
|
4625
5019
|
];
|
|
4626
|
-
const
|
|
5020
|
+
const cards = [
|
|
4627
5021
|
buildHistogramCard({
|
|
4628
5022
|
title: "Comments per PR",
|
|
4629
5023
|
subtitle: "discussion plus review comments per authored PR",
|
|
@@ -4631,6 +5025,14 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4631
5025
|
buckets: COMMENT_BUCKETS,
|
|
4632
5026
|
format: count
|
|
4633
5027
|
}),
|
|
5028
|
+
buildTrendCard({
|
|
5029
|
+
title: "Comment trend",
|
|
5030
|
+
entries: sizes.map((size) => {
|
|
5031
|
+
return { date: size.pr.createdAt, value: size.comments.total };
|
|
5032
|
+
}),
|
|
5033
|
+
format: count,
|
|
5034
|
+
floor: 1
|
|
5035
|
+
}),
|
|
4634
5036
|
buildScatterCard({
|
|
4635
5037
|
title: "Comments vs size",
|
|
4636
5038
|
subtitle: "comments against lines changed, log scale",
|
|
@@ -4640,18 +5042,8 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4640
5042
|
formatX: count,
|
|
4641
5043
|
formatY: count
|
|
4642
5044
|
}),
|
|
4643
|
-
buildSpreadCard("Comment spread", stats.metrics, count)
|
|
4644
|
-
];
|
|
4645
|
-
const right = [
|
|
4646
|
-
buildTrendCard({
|
|
4647
|
-
title: "Comment trend",
|
|
4648
|
-
entries: sizes.map((size) => {
|
|
4649
|
-
return { date: size.pr.createdAt, value: size.comments.total };
|
|
4650
|
-
}),
|
|
4651
|
-
format: count,
|
|
4652
|
-
floor: 1
|
|
4653
|
-
}),
|
|
4654
5045
|
buildVolumeCard("Comments received per week", created, stats.totals),
|
|
5046
|
+
buildSpreadCard("Comment spread", stats.metrics, count),
|
|
4655
5047
|
buildGaugeCard({
|
|
4656
5048
|
title: "Feedback rate",
|
|
4657
5049
|
subtitle: "authored PRs that received comments",
|
|
@@ -4681,7 +5073,7 @@ function buildCommentView(raw, repo = null, width = 100) {
|
|
|
4681
5073
|
)
|
|
4682
5074
|
});
|
|
4683
5075
|
}
|
|
4684
|
-
return { empty: null, ...base, strip, headline,
|
|
5076
|
+
return { empty: null, ...base, strip, headline, cards, distribution, lists };
|
|
4685
5077
|
}
|
|
4686
5078
|
function buildMergedView(raw, repo = null, width = 100) {
|
|
4687
5079
|
const base = {
|
|
@@ -4689,8 +5081,7 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
4689
5081
|
headline: null,
|
|
4690
5082
|
distributionTitle: "Time to merge distribution",
|
|
4691
5083
|
noCharts: "No merged PRs to chart.",
|
|
4692
|
-
|
|
4693
|
-
right: [],
|
|
5084
|
+
cards: [],
|
|
4694
5085
|
distribution: null,
|
|
4695
5086
|
lists: []
|
|
4696
5087
|
};
|
|
@@ -4745,7 +5136,7 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
4745
5136
|
];
|
|
4746
5137
|
const mergeDates = stats.merged.map((result) => result.mergedAt);
|
|
4747
5138
|
const created = sizes.map((size) => size.pr.createdAt);
|
|
4748
|
-
const
|
|
5139
|
+
const cards = [
|
|
4749
5140
|
buildHistogramCard({
|
|
4750
5141
|
title: "Time to merge",
|
|
4751
5142
|
subtitle: "elapsed time, created \u2192 merged",
|
|
@@ -4753,6 +5144,14 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
4753
5144
|
buckets: currentBuckets(),
|
|
4754
5145
|
format: formatDuration
|
|
4755
5146
|
}),
|
|
5147
|
+
buildTrendCard({
|
|
5148
|
+
title: "Time to merge trend",
|
|
5149
|
+
entries: stats.merged.map((result) => {
|
|
5150
|
+
return { date: result.mergedAt, value: result.hours };
|
|
5151
|
+
}),
|
|
5152
|
+
format: formatDuration,
|
|
5153
|
+
floor: 1 / 60
|
|
5154
|
+
}),
|
|
4756
5155
|
buildHeatmapCard({
|
|
4757
5156
|
title: "When your PRs merge",
|
|
4758
5157
|
subtitle: "PRs merged, weekday \xD7 hour, local time",
|
|
@@ -4763,6 +5162,38 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
4763
5162
|
],
|
|
4764
5163
|
legend: "PRs merged in that hour"
|
|
4765
5164
|
}),
|
|
5165
|
+
buildTrendCard({
|
|
5166
|
+
title: "Merge rate trend",
|
|
5167
|
+
entries: mergeRateEntries(stats),
|
|
5168
|
+
format: (value2) => `${Math.round(value2)}%`,
|
|
5169
|
+
scale: "linear",
|
|
5170
|
+
valueLabel: "merge rate"
|
|
5171
|
+
}),
|
|
5172
|
+
buildScatterCard({
|
|
5173
|
+
title: "Merge time vs size",
|
|
5174
|
+
subtitle: "time to merge against lines changed, log scale",
|
|
5175
|
+
points: stats.merged.map((result) => {
|
|
5176
|
+
return { x: result.entry.total, y: result.hours };
|
|
5177
|
+
}),
|
|
5178
|
+
formatX: count,
|
|
5179
|
+
formatY: formatDuration
|
|
5180
|
+
}),
|
|
5181
|
+
buildCumulativeCard({
|
|
5182
|
+
title: "Created vs merged",
|
|
5183
|
+
series: [
|
|
5184
|
+
/**
|
|
5185
|
+
* The created line stays a neutral gray because every theme preset
|
|
5186
|
+
* keeps chartLine and accent in one hue family, which made the two
|
|
5187
|
+
* lines indistinguishable. The gray also survives theme changes,
|
|
5188
|
+
* since the presets only rotate the hue-carrying colors.
|
|
5189
|
+
*/
|
|
5190
|
+
{ label: "created", dates: created, color: theme.muted },
|
|
5191
|
+
{ label: "merged", dates: mergeDates, color: theme.accent }
|
|
5192
|
+
],
|
|
5193
|
+
legend: "cumulative PRs by week"
|
|
5194
|
+
}),
|
|
5195
|
+
buildVolumeCard("PRs created per week", created),
|
|
5196
|
+
buildVolumeCard("PRs merged per week", mergeDates),
|
|
4766
5197
|
buildGaugeCard({
|
|
4767
5198
|
title: "Outcomes",
|
|
4768
5199
|
subtitle: "where your authored PRs ended up",
|
|
@@ -4773,18 +5204,6 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
4773
5204
|
]
|
|
4774
5205
|
})
|
|
4775
5206
|
];
|
|
4776
|
-
const right = [
|
|
4777
|
-
buildTrendCard({
|
|
4778
|
-
title: "Time to merge trend",
|
|
4779
|
-
entries: stats.merged.map((result) => {
|
|
4780
|
-
return { date: result.mergedAt, value: result.hours };
|
|
4781
|
-
}),
|
|
4782
|
-
format: formatDuration,
|
|
4783
|
-
floor: 1 / 60
|
|
4784
|
-
}),
|
|
4785
|
-
buildVolumeCard("PRs merged per week", mergeDates),
|
|
4786
|
-
buildVolumeCard("PRs created per week", created)
|
|
4787
|
-
];
|
|
4788
5207
|
const distribution = buildDistribution({
|
|
4789
5208
|
values: stats.allHours,
|
|
4790
5209
|
width: Math.max(width - 3, 40),
|
|
@@ -4792,11 +5211,14 @@ function buildMergedView(raw, repo = null, width = 100) {
|
|
|
4792
5211
|
ticks: DURATION_TICKS,
|
|
4793
5212
|
flat: (n, value2) => `all ${n} merged ${n === 1 ? "PR" : "PRs"} took ${value2}`
|
|
4794
5213
|
});
|
|
4795
|
-
return { empty: null, ...base, strip, headline,
|
|
5214
|
+
return { empty: null, ...base, strip, headline, cards, distribution, lists };
|
|
4796
5215
|
}
|
|
4797
5216
|
function count(value2) {
|
|
4798
5217
|
return formatCount(Math.round(value2));
|
|
4799
5218
|
}
|
|
5219
|
+
function netCount(value2) {
|
|
5220
|
+
return value2 < 0 ? `-${count(-value2)}` : `+${count(value2)}`;
|
|
5221
|
+
}
|
|
4800
5222
|
|
|
4801
5223
|
// src/tui/hooks/useViewModel.ts
|
|
4802
5224
|
function resolveScope(scope, repos) {
|
|
@@ -5351,7 +5773,7 @@ function createClipboardCopier(renderer2) {
|
|
|
5351
5773
|
}
|
|
5352
5774
|
|
|
5353
5775
|
// src/tui/App.tsx
|
|
5354
|
-
import { jsx as
|
|
5776
|
+
import { jsx as jsx15, jsxs as jsxs13 } from "@opentui/react/jsx-runtime";
|
|
5355
5777
|
function App({
|
|
5356
5778
|
initial: initial2,
|
|
5357
5779
|
initialSaved = null,
|
|
@@ -5473,9 +5895,9 @@ function App({
|
|
|
5473
5895
|
});
|
|
5474
5896
|
const capWarning = raw?.searchCapped ? "Warning, a search hit the 1000 result cap, so data may be incomplete. Narrow since or repos." : null;
|
|
5475
5897
|
return /* @__PURE__ */ jsxs13("box", { flexDirection: "column", width: "100%", height: "100%", backgroundColor: theme.bg, children: [
|
|
5476
|
-
/* @__PURE__ */
|
|
5477
|
-
/* @__PURE__ */
|
|
5478
|
-
/* @__PURE__ */
|
|
5898
|
+
/* @__PURE__ */ jsx15(Header, { options, raw, error, spinning: showLoad }),
|
|
5899
|
+
/* @__PURE__ */ jsx15(TabBar, { tab: browse.tab }),
|
|
5900
|
+
/* @__PURE__ */ jsx15(
|
|
5479
5901
|
MainPanel,
|
|
5480
5902
|
{
|
|
5481
5903
|
views,
|
|
@@ -5494,7 +5916,7 @@ function App({
|
|
|
5494
5916
|
onRefClick: copyLinks2 ? copyRow : null
|
|
5495
5917
|
}
|
|
5496
5918
|
),
|
|
5497
|
-
/* @__PURE__ */
|
|
5919
|
+
/* @__PURE__ */ jsx15(
|
|
5498
5920
|
Footer,
|
|
5499
5921
|
{
|
|
5500
5922
|
width,
|
|
@@ -5509,42 +5931,20 @@ function App({
|
|
|
5509
5931
|
stale
|
|
5510
5932
|
}
|
|
5511
5933
|
),
|
|
5512
|
-
|
|
5513
|
-
|
|
5934
|
+
/* @__PURE__ */ jsx15(
|
|
5935
|
+
Modals,
|
|
5514
5936
|
{
|
|
5937
|
+
ui,
|
|
5515
5938
|
options,
|
|
5516
5939
|
saved: saved2,
|
|
5517
|
-
selected: ui.selectedField,
|
|
5518
|
-
editing: ui.editing,
|
|
5519
|
-
fieldError: ui.fieldError,
|
|
5520
|
-
onDraft: (value2) => {
|
|
5521
|
-
draftRef.current = value2;
|
|
5522
|
-
},
|
|
5523
|
-
onSubmit: commitField
|
|
5524
|
-
}
|
|
5525
|
-
),
|
|
5526
|
-
ui.modal === "settings" && /* @__PURE__ */ jsx14(
|
|
5527
|
-
SettingsModal,
|
|
5528
|
-
{
|
|
5529
|
-
selected: ui.selectedSetting,
|
|
5530
|
-
cacheAction: ui.cacheAction,
|
|
5531
5940
|
noCache: noCache2,
|
|
5532
5941
|
copyLinks: copyLinks2,
|
|
5533
|
-
|
|
5534
|
-
}
|
|
5535
|
-
),
|
|
5536
|
-
ui.modal === "theme" && /* @__PURE__ */ jsx14(
|
|
5537
|
-
ThemeModal,
|
|
5538
|
-
{
|
|
5539
|
-
selected: ui.selectedThemeColor,
|
|
5540
|
-
editing: ui.editing,
|
|
5541
|
-
error: ui.themeColorError,
|
|
5542
|
-
cacheAction: ui.cacheAction,
|
|
5543
|
-
overrides: themeState.preset === "custom" ? themeState.overrides : {},
|
|
5942
|
+
themeState,
|
|
5544
5943
|
onDraft: (value2) => {
|
|
5545
5944
|
draftRef.current = value2;
|
|
5546
5945
|
},
|
|
5547
|
-
|
|
5946
|
+
onSubmitField: commitField,
|
|
5947
|
+
onSubmitThemeColor: commitThemeColor
|
|
5548
5948
|
}
|
|
5549
5949
|
)
|
|
5550
5950
|
] });
|
|
@@ -5590,7 +5990,7 @@ function bootstrap() {
|
|
|
5590
5990
|
}
|
|
5591
5991
|
|
|
5592
5992
|
// src/tui/main.tsx
|
|
5593
|
-
import { jsx as
|
|
5993
|
+
import { jsx as jsx16 } from "@opentui/react/jsx-runtime";
|
|
5594
5994
|
var { initial, saved, noCache, copyLinks, theme: theme2 } = bootstrap();
|
|
5595
5995
|
var exitSignals = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT", "SIGHUP", "SIGBREAK", "SIGBUS"];
|
|
5596
5996
|
var renderer = await createCliRenderer({ exitOnCtrlC: true, exitSignals });
|
|
@@ -5601,7 +6001,7 @@ for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
|
5601
6001
|
});
|
|
5602
6002
|
}
|
|
5603
6003
|
createRoot(renderer).render(
|
|
5604
|
-
/* @__PURE__ */
|
|
6004
|
+
/* @__PURE__ */ jsx16(
|
|
5605
6005
|
App,
|
|
5606
6006
|
{
|
|
5607
6007
|
initial,
|