co-maintainer 0.5.0 → 0.5.1-beta.2
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/LICENSE +21 -21
- package/README.md +5 -3
- package/dist/package.json +1 -1
- package/dist/src/ai/anthropic.d.ts +55 -0
- package/dist/src/ai/anthropic.js +262 -0
- package/dist/src/ai/batch.d.ts +2 -2
- package/dist/src/ai/batch.js +1 -1
- package/dist/src/ai/estimate.d.ts +4 -0
- package/dist/src/ai/estimate.js +17 -2
- package/dist/src/ai/mermaid_loop.js +7 -4
- package/dist/src/ai/openai.d.ts +45 -0
- package/dist/src/ai/openai.js +206 -0
- package/dist/src/ai/opencode.d.ts +10 -0
- package/dist/src/ai/opencode.js +76 -0
- package/dist/src/ai/openrouter.d.ts +23 -7
- package/dist/src/ai/openrouter.js +39 -23
- package/dist/src/ai/pricing.js +1 -1
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +42 -3
- package/dist/src/ai/verify.d.ts +9 -1
- package/dist/src/ai/verify.js +81 -3
- package/dist/src/cli/args.d.ts +1 -2
- package/dist/src/cli/args.js +38 -26
- package/dist/src/cli/commands/config.d.ts +1 -1
- package/dist/src/cli/commands/config.js +10 -4
- package/dist/src/cli/commands/probe.js +3 -11
- package/dist/src/cli/commands/registry.d.ts +1 -9
- package/dist/src/cli/commands/registry.js +75 -10
- package/dist/src/cli/commands/review.js +18 -13
- package/dist/src/cli/commands/rollback.d.ts +1 -0
- package/dist/src/cli/commands/rollback.js +42 -0
- package/dist/src/cli/commands/serve.js +2 -3
- package/dist/src/cli/commands/set.js +71 -7
- package/dist/src/cli/commands/view.js +10 -9
- package/dist/src/cli/error.d.ts +4 -4
- package/dist/src/cli/error.js +4 -4
- package/dist/src/cli/main.d.ts +4 -4
- package/dist/src/cli/main.js +13 -8
- package/dist/src/cli/prompt.js +1 -1
- package/dist/src/cli/review_args.d.ts +9 -3
- package/dist/src/cli/review_args.js +28 -5
- package/dist/src/cli/review_github.d.ts +31 -0
- package/dist/src/cli/review_github.js +128 -0
- package/dist/src/cli/review_result.d.ts +27 -16
- package/dist/src/cli/review_result.js +7 -7
- package/dist/src/config.d.ts +28 -6
- package/dist/src/config.js +15 -1
- package/dist/src/git/default_branch.d.ts +1 -1
- package/dist/src/git/default_branch.js +1 -1
- package/dist/src/github/app.js +2 -2
- package/dist/src/github/app_manifest.d.ts +1 -7
- package/dist/src/github/app_manifest.js +2 -7
- package/dist/src/github/client.d.ts +4 -1
- package/dist/src/github/client.js +14 -2
- package/dist/src/github/gh.js +10 -7
- package/dist/src/github/pat.js +4 -4
- package/dist/src/github/permissions.d.ts +36 -0
- package/dist/src/github/permissions.js +334 -0
- package/dist/src/knowledge/facts.js +1 -1
- package/dist/src/knowledge/guide.d.ts +3 -3
- package/dist/src/knowledge/guide.js +5 -5
- package/dist/src/knowledge/probe.js +1 -2
- package/dist/src/knowledge/sections.d.ts +3 -3
- package/dist/src/knowledge/sections.js +3 -3
- package/dist/src/knowledge/skill.d.ts +1 -1
- package/dist/src/knowledge/skill.js +4 -4
- package/dist/src/knowledge/synthesis.d.ts +1 -1
- package/dist/src/knowledge/synthesis.js +5 -5
- package/dist/src/knowledge/types.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.js +1 -1
- package/dist/src/local/codegraph_prepare.js +1 -1
- package/dist/src/local/git_ops.d.ts +1 -1
- package/dist/src/local/git_ops.js +1 -1
- package/dist/src/local/git_parse.d.ts +2 -2
- package/dist/src/local/git_parse.js +2 -2
- package/dist/src/local/git_revision.d.ts +1 -1
- package/dist/src/local/git_revision.js +1 -1
- package/dist/src/local/git_untracked.d.ts +1 -1
- package/dist/src/local/git_untracked.js +1 -1
- package/dist/src/local/review_local.js +17 -14
- package/dist/src/local/review_lock.d.ts +1 -1
- package/dist/src/local/review_lock.js +1 -1
- package/dist/src/pr/checkout.js +2 -2
- package/dist/src/pr/findings.js +1 -1
- package/dist/src/pr/findings_json.d.ts +2 -2
- package/dist/src/pr/findings_json.js +1 -1
- package/dist/src/pr/github_revision.js +1 -1
- package/dist/src/pr/review_copy.d.ts +2 -2
- package/dist/src/pr/review_copy.js +2 -2
- package/dist/src/pr/reviewer.d.ts +6 -0
- package/dist/src/pr/reviewer.js +49 -39
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +84 -23
- package/dist/src/remote/fixtures.js +1 -1
- package/dist/src/remote/http.d.ts +10 -1
- package/dist/src/remote/http.js +15 -3
- package/dist/src/remote/schema.d.ts +3 -3
- package/dist/src/remote/schema.js +3 -3
- package/dist/src/remote/server/body.d.ts +1 -1
- package/dist/src/remote/server/body.js +1 -1
- package/dist/src/remote/server/byok_keys.d.ts +15 -0
- package/dist/src/remote/server/byok_keys.js +40 -0
- package/dist/src/remote/server/guides.js +2 -2
- package/dist/src/remote/server/routes.js +8 -1
- package/dist/src/remote/validate.js +15 -1
- package/dist/src/review/blocking.d.ts +2 -2
- package/dist/src/review/blocking.js +2 -2
- package/dist/src/review/carry_over.d.ts +2 -2
- package/dist/src/review/carry_over.js +2 -2
- package/dist/src/review/guides.js +1 -1
- package/dist/src/server/api/remote_tokens.js +2 -0
- package/dist/src/server/api/repos.js +51 -0
- package/dist/src/server/api/settings.js +10 -0
- package/dist/src/server/errors.d.ts +1 -1
- package/dist/src/server/http.js +1 -1
- package/dist/src/server/pages/activity.d.ts +2 -0
- package/dist/src/server/pages/activity.js +18 -4
- package/dist/src/server/pages/add_repo.js +61 -1
- package/dist/src/server/pages/analytics.js +18 -8
- package/dist/src/server/pages/cost.d.ts +21 -0
- package/dist/src/server/pages/cost.js +72 -0
- package/dist/src/server/pages/pr.js +5 -4
- package/dist/src/server/pages/repo.js +3 -2
- package/dist/src/server/pages/repo_prs.js +3 -2
- package/dist/src/server/pages/repo_remote.js +3 -2
- package/dist/src/server/pages/repo_settings.js +326 -12
- package/dist/src/server/pages/router.js +3 -2
- package/dist/src/server/pages/settings.js +50 -6
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/credentials.js +8 -11
- package/dist/src/services/dashboard.d.ts +22 -9
- package/dist/src/services/dashboard.js +16 -2
- package/dist/src/services/probe.js +1 -1
- package/dist/src/services/remote_review.js +142 -35
- package/dist/src/services/remote_review_abort.d.ts +5 -1
- package/dist/src/services/remote_review_abort.js +9 -1
- package/dist/src/services/review.d.ts +1 -1
- package/dist/src/services/review.js +19 -5
- package/dist/src/services/review_policy.d.ts +116 -0
- package/dist/src/services/review_policy.js +489 -0
- package/dist/src/services/setup.d.ts +4 -3
- package/dist/src/services/setup.js +18 -16
- package/dist/src/services/setup_checklist.js +2 -2
- package/dist/src/services/webhook.d.ts +23 -0
- package/dist/src/services/webhook.js +141 -34
- package/dist/src/store/app_db.d.ts +5 -0
- package/dist/src/store/app_db.js +69 -13
- package/dist/src/store/backup.d.ts +31 -0
- package/dist/src/store/backup.js +158 -0
- package/dist/src/store/migrations.d.ts +1 -1
- package/dist/src/store/migrations.js +21 -1
- package/dist/src/store/remote_review_inputs.d.ts +3 -0
- package/dist/src/store/remote_review_inputs.js +3 -3
- package/dist/src/store/remote_tokens.d.ts +1 -1
- package/dist/src/store/remote_tokens.js +1 -1
- package/dist/src/store/repos.d.ts +2 -2
- package/dist/src/store/repos.js +1 -1
- package/dist/src/store/reviews.d.ts +21 -13
- package/dist/src/store/reviews.js +60 -15
- package/dist/src/store/rows.d.ts +10 -0
- package/dist/src/tools/codegraph.d.ts +4 -4
- package/dist/src/tools/codegraph.js +5 -5
- package/dist/src/tools/codegraph_exec.d.ts +1 -1
- package/dist/src/tools/codegraph_exec.js +1 -1
- package/dist/src/types.d.ts +12 -6
- package/dist/src/util/cost.d.ts +52 -0
- package/dist/src/util/cost.js +70 -0
- package/dist/src/util/log.d.ts +1 -1
- package/dist/src/util/log.js +2 -2
- package/dist/src/util/redact.js +2 -0
- package/dist/src/util/run_summary.d.ts +7 -2
- package/dist/src/util/run_summary.js +28 -2
- package/dist/src/util/time.d.ts +1 -1
- package/dist/src/util/time.js +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { bars, count, delta, duration, empty, html, layout, money, text, } from "./layout.js";
|
|
2
|
+
import { formatCostTotal } from "./cost.js";
|
|
2
3
|
import { listJobs } from "../../store/jobs.js";
|
|
3
4
|
export function renderAnalytics(username, range, data) {
|
|
4
5
|
const failed = listJobs({ status: "failed" }).slice(0, 5);
|
|
@@ -19,7 +20,7 @@ export function renderAnalytics(username, range, data) {
|
|
|
19
20
|
: "0"}</td>
|
|
20
21
|
<td class="num">${count(row.tokensIn + row.tokensOut)}</td>
|
|
21
22
|
<td class="num">${duration(row.avgDurationMs)}</td>
|
|
22
|
-
<td class="num">${
|
|
23
|
+
<td class="num">${formatCostTotal(row)}</td></tr>`)
|
|
23
24
|
.join("")}
|
|
24
25
|
</tbody>
|
|
25
26
|
</table>`;
|
|
@@ -36,8 +37,12 @@ export function renderAnalytics(username, range, data) {
|
|
|
36
37
|
<td class="num">${count(row.tokensIn)}</td>
|
|
37
38
|
<td class="num">${count(row.tokensOut)}</td>
|
|
38
39
|
<td class="num">${duration(row.avgDurationMs)}</td>
|
|
39
|
-
<td class="num">${
|
|
40
|
-
|
|
40
|
+
<td class="num">${row.knownCount
|
|
41
|
+
? money(row.cost / row.knownCount)
|
|
42
|
+
: row.unknownCount
|
|
43
|
+
? "unknown"
|
|
44
|
+
: money(0)}</td>
|
|
45
|
+
<td class="num">${formatCostTotal(row)}</td></tr>`)
|
|
41
46
|
.join("")}
|
|
42
47
|
</tbody>
|
|
43
48
|
</table>`;
|
|
@@ -51,7 +56,7 @@ export function renderAnalytics(username, range, data) {
|
|
|
51
56
|
.map((row) => `<tr><td>${text(row.tokenName)}</td>
|
|
52
57
|
<td class="num">${row.reviews}</td>
|
|
53
58
|
<td class="num">${row.findings}</td>
|
|
54
|
-
<td class="num">${
|
|
59
|
+
<td class="num">${formatCostTotal(row)}</td></tr>`)
|
|
55
60
|
.join("")}
|
|
56
61
|
</tbody>
|
|
57
62
|
</table>`;
|
|
@@ -77,6 +82,11 @@ export function renderAnalytics(username, range, data) {
|
|
|
77
82
|
cost: 0,
|
|
78
83
|
reviews: 0,
|
|
79
84
|
findings: 0,
|
|
85
|
+
knownCount: 0,
|
|
86
|
+
unknownCount: 0,
|
|
87
|
+
byokUsd: 0,
|
|
88
|
+
byokKnownCount: 0,
|
|
89
|
+
byokUnknownCount: 0,
|
|
80
90
|
});
|
|
81
91
|
const looks = failed.length === 0
|
|
82
92
|
? empty("Nothing stands out", "Failed jobs will be listed here.")
|
|
@@ -107,7 +117,7 @@ export function renderAnalytics(username, range, data) {
|
|
|
107
117
|
<div class="kfig wrap4">
|
|
108
118
|
<div><div class="k">Pull requests reviewed</div><div class="big">${data.totals.pullRequests}</div>${delta(data.change.pullRequests, data.days)}</div>
|
|
109
119
|
<div><div class="k">Findings</div><div class="big">${data.totals.findings}</div>${delta(data.change.findings, data.days)}</div>
|
|
110
|
-
<div><div class="k">Cost</div><div class="big">${
|
|
120
|
+
<div><div class="k">Cost</div><div class="big">${formatCostTotal(data.totals)}</div>${delta(data.change.cost, data.days, true)}</div>
|
|
111
121
|
<div><div class="k">Tokens</div><div class="big">${count(data.totals.tokensIn + data.totals.tokensOut)}</div><div class="trend">${count(data.totals.tokensIn)} in and ${count(data.totals.tokensOut)} out</div></div>
|
|
112
122
|
<div><div class="k">Average review</div><div class="big">${duration(data.totals.avgDurationMs)}</div></div>
|
|
113
123
|
<div><div class="k">Rounds per pull request</div><div class="big">${data.totals.pullRequests
|
|
@@ -121,11 +131,11 @@ export function renderAnalytics(username, range, data) {
|
|
|
121
131
|
<div class="hd"><h2>Cost per day</h2></div>
|
|
122
132
|
<div class="bd">
|
|
123
133
|
${bars(data.byDay.map((row) => ({
|
|
124
|
-
label: `${row.day} ${
|
|
134
|
+
label: `${row.day} ${formatCostTotal(row)} over ${row.reviews} reviews`,
|
|
125
135
|
value: row.cost,
|
|
126
136
|
})))}
|
|
127
|
-
<p class="muted" style="margin:10px 0 0">${busiest.cost
|
|
128
|
-
? `Busiest day was ${text(busiest.day)} at ${
|
|
137
|
+
<p class="muted" style="margin:10px 0 0">${busiest.cost > 0 || busiest.unknownCount > 0
|
|
138
|
+
? `Busiest day was ${text(busiest.day)} at ${formatCostTotal(busiest)}`
|
|
129
139
|
: "No spend in this range"}</p>
|
|
130
140
|
</div>
|
|
131
141
|
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CostTotals } from "../../store/reviews.ts";
|
|
2
|
+
export type CostRow = {
|
|
3
|
+
cost: number | null;
|
|
4
|
+
/** `null` on a row written before this column existed. */
|
|
5
|
+
cost_status?: "known" | "unknown" | null;
|
|
6
|
+
cost_note?: string | null;
|
|
7
|
+
billed_to?: "server" | "byok" | null;
|
|
8
|
+
};
|
|
9
|
+
/** One review's cost cell: the dollar amount, or "unknown" with the reason
|
|
10
|
+
* as a tooltip. A row from before `cost_status` existed falls back to the
|
|
11
|
+
* same rule the SQL aggregates use: a recorded cost is known, a missing one
|
|
12
|
+
* is not. */
|
|
13
|
+
export declare function formatCost(row: CostRow): string;
|
|
14
|
+
/** A figure summed from several reviews, as `$1.23 · 4 reviews with unknown
|
|
15
|
+
* cost`. Plain text, safe wherever HTML cannot go (a `title` attribute, the
|
|
16
|
+
* settings page's own JSON). BYOK never sits inside the main amount. */
|
|
17
|
+
export declare function formatCostTotal(totals: CostTotals): string;
|
|
18
|
+
/** Folds a handful of reviews (a pull request's rounds) into the same shape
|
|
19
|
+
* `reviewStats` returns, so one review list and one SQL aggregate render
|
|
20
|
+
* through the same `formatCostTotal`. */
|
|
21
|
+
export declare function totalOfRows(rows: CostRow[]): CostTotals;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** How a review's cost renders on the dashboard. A missing cost
|
|
2
|
+
* is never a silent `$0.00`: a single review says why in a tooltip, a total
|
|
3
|
+
* names how many of its reviews are missing one. */
|
|
4
|
+
import { costReasonText } from "../../util/cost.js";
|
|
5
|
+
import { escapeHtml, money } from "./layout.js";
|
|
6
|
+
function statusOf(row) {
|
|
7
|
+
return row.cost_status ?? (row.cost !== null ? "known" : "unknown");
|
|
8
|
+
}
|
|
9
|
+
/** One review's cost cell: the dollar amount, or "unknown" with the reason
|
|
10
|
+
* as a tooltip. A row from before `cost_status` existed falls back to the
|
|
11
|
+
* same rule the SQL aggregates use: a recorded cost is known, a missing one
|
|
12
|
+
* is not. */
|
|
13
|
+
export function formatCost(row) {
|
|
14
|
+
if (statusOf(row) === "known")
|
|
15
|
+
return money(row.cost ?? 0);
|
|
16
|
+
const reason = row.cost_note ?? "recorded_before_0_5_1";
|
|
17
|
+
return `<span class="cost-unknown" tabindex="0" title="${escapeHtml(costReasonText(reason))}">unknown</span>`;
|
|
18
|
+
}
|
|
19
|
+
function reviewsWord(n) {
|
|
20
|
+
return `${n} review${n === 1 ? "" : "s"} with unknown cost`;
|
|
21
|
+
}
|
|
22
|
+
/** A figure summed from several reviews, as `$1.23 · 4 reviews with unknown
|
|
23
|
+
* cost`. Plain text, safe wherever HTML cannot go (a `title` attribute, the
|
|
24
|
+
* settings page's own JSON). BYOK never sits inside the main amount. */
|
|
25
|
+
export function formatCostTotal(totals) {
|
|
26
|
+
let main;
|
|
27
|
+
if (totals.knownCount === 0 && totals.unknownCount > 0) {
|
|
28
|
+
main = reviewsWord(totals.unknownCount);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
main = money(totals.cost);
|
|
32
|
+
if (totals.unknownCount > 0)
|
|
33
|
+
main += ` · ${reviewsWord(totals.unknownCount)}`;
|
|
34
|
+
}
|
|
35
|
+
if (totals.byokKnownCount === 0 && totals.byokUnknownCount === 0)
|
|
36
|
+
return main;
|
|
37
|
+
const byok = totals.byokKnownCount === 0
|
|
38
|
+
? reviewsWord(totals.byokUnknownCount)
|
|
39
|
+
: money(totals.byokUsd);
|
|
40
|
+
return `${main} + ${byok} BYOK`;
|
|
41
|
+
}
|
|
42
|
+
/** Folds a handful of reviews (a pull request's rounds) into the same shape
|
|
43
|
+
* `reviewStats` returns, so one review list and one SQL aggregate render
|
|
44
|
+
* through the same `formatCostTotal`. */
|
|
45
|
+
export function totalOfRows(rows) {
|
|
46
|
+
const totals = {
|
|
47
|
+
cost: 0,
|
|
48
|
+
knownCount: 0,
|
|
49
|
+
unknownCount: 0,
|
|
50
|
+
byokUsd: 0,
|
|
51
|
+
byokKnownCount: 0,
|
|
52
|
+
byokUnknownCount: 0,
|
|
53
|
+
};
|
|
54
|
+
for (const row of rows) {
|
|
55
|
+
const known = statusOf(row) === "known";
|
|
56
|
+
if (row.billed_to === "byok") {
|
|
57
|
+
if (known) {
|
|
58
|
+
totals.byokUsd += row.cost ?? 0;
|
|
59
|
+
totals.byokKnownCount++;
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
totals.byokUnknownCount++;
|
|
63
|
+
}
|
|
64
|
+
else if (known) {
|
|
65
|
+
totals.cost += row.cost ?? 0;
|
|
66
|
+
totals.knownCount++;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
totals.unknownCount++;
|
|
70
|
+
}
|
|
71
|
+
return totals;
|
|
72
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { empty, html, layout, markdown,
|
|
1
|
+
import { empty, html, layout, markdown, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
|
+
import { formatCost, formatCostTotal, totalOfRows } from "./cost.js";
|
|
2
3
|
export function renderPr(username, fullName, data) {
|
|
3
4
|
const n = data.prNumber;
|
|
4
|
-
const totalCost = data.reviews
|
|
5
|
+
const totalCost = totalOfRows(data.reviews);
|
|
5
6
|
const stateLabel = (state) => {
|
|
6
7
|
if (state === "closed")
|
|
7
8
|
return "Closed";
|
|
@@ -64,7 +65,7 @@ export function renderPr(username, fullName, data) {
|
|
|
64
65
|
? "Changes since the last review"
|
|
65
66
|
: "Whole pull request")}</td>
|
|
66
67
|
<td class="num">${review.findings_count}</td>
|
|
67
|
-
<td class="num">${
|
|
68
|
+
<td class="num">${formatCost(review)}</td></tr>`)
|
|
68
69
|
.join("")}
|
|
69
70
|
</tbody>
|
|
70
71
|
</table>`;
|
|
@@ -95,7 +96,7 @@ export function renderPr(username, fullName, data) {
|
|
|
95
96
|
</div>
|
|
96
97
|
<div class="card">
|
|
97
98
|
<div class="hd"><h2>Reviews</h2>
|
|
98
|
-
<span class="muted" style="margin-left:auto;font-size:13px">${
|
|
99
|
+
<span class="muted" style="margin-left:auto;font-size:13px">${formatCostTotal(totalCost)} total</span></div>
|
|
99
100
|
<div class="bd flush">${reviewsTable}</div>
|
|
100
101
|
</div>
|
|
101
102
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { capped, empty, html, layout,
|
|
1
|
+
import { capped, empty, html, layout, repoNav, skSlot, text, when, } from "./layout.js";
|
|
2
|
+
import { formatCostTotal } from "./cost.js";
|
|
2
3
|
import { COMPARE_FILE_CAP, MAX_COMMITS_COUNTED } from "../../services/drift.js";
|
|
3
4
|
import { webhookReachabilityProblem } from "../../util/webhook_reachability.js";
|
|
4
5
|
import { lastDeliveryForRepo } from "../../store/deliveries.js";
|
|
@@ -72,7 +73,7 @@ export function renderRepo(username, data, webhookUrl) {
|
|
|
72
73
|
<div class="kfig">
|
|
73
74
|
<div><div class="k">Pull requests reviewed</div><div class="big">${stats.pullRequests}</div></div>
|
|
74
75
|
<div><div class="k">Findings</div><div class="big">${stats.findings}</div></div>
|
|
75
|
-
<div><div class="k">Cost</div><div class="big">${
|
|
76
|
+
<div><div class="k">Cost</div><div class="big">${formatCostTotal(stats)}</div></div>
|
|
76
77
|
</div>
|
|
77
78
|
</div>
|
|
78
79
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { empty, html, layout,
|
|
1
|
+
import { empty, html, layout, repoNav, skSlot, text, when } from "./layout.js";
|
|
2
|
+
import { formatCostTotal } from "./cost.js";
|
|
2
3
|
export function renderRepoPulls(username, fullName, data, open) {
|
|
3
4
|
const start = (data.page - 1) * data.per + 1;
|
|
4
5
|
const end = Math.min(data.page * data.per, data.total);
|
|
@@ -24,7 +25,7 @@ export function renderRepoPulls(username, fullName, data, open) {
|
|
|
24
25
|
.map((item) => `<tr><td><a href="/repos/${text(fullName)}/pulls/${item.pr_number}">#${item.pr_number}</a></td>
|
|
25
26
|
<td class="muted">${when(item.last_reviewed)}</td>
|
|
26
27
|
<td class="num">${item.findings}</td>
|
|
27
|
-
<td class="num">${
|
|
28
|
+
<td class="num">${formatCostTotal(item)}</td></tr>`)
|
|
28
29
|
.join("")}
|
|
29
30
|
</tbody>
|
|
30
31
|
</table>${pager}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { empty, html, layout,
|
|
1
|
+
import { empty, html, layout, repoNav, text, when } from "./layout.js";
|
|
2
|
+
import { formatCost } from "./cost.js";
|
|
2
3
|
export function renderRepoRemote(username, fullName, data) {
|
|
3
4
|
const rows = data.items.length === 0
|
|
4
5
|
? empty("No remote reviews yet", "Developers run co-maintainer review --remote with a token from Settings.")
|
|
@@ -12,7 +13,7 @@ export function renderRepoRemote(username, fullName, data) {
|
|
|
12
13
|
<td>${text(review.token_name ?? "none")}</td>
|
|
13
14
|
<td>${text(review.status)}</td>
|
|
14
15
|
<td class="num">${review.findings_count ?? 0}</td>
|
|
15
|
-
<td class="num">${
|
|
16
|
+
<td class="num">${formatCost(review)}</td>
|
|
16
17
|
<td class="muted">${when(review.created_at)}</td>
|
|
17
18
|
</tr>`)
|
|
18
19
|
.join("")}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { html, layout, repoNav, skSlot, text } from "./layout.js";
|
|
2
2
|
import { parseRemakeCron } from "../../services/remake_cron.js";
|
|
3
3
|
import { nextCronRun } from "../../util/cron.js";
|
|
4
|
+
import { policySource } from "../../services/webhook.js";
|
|
5
|
+
import { ASSOCIATIONS, TEMPLATES, TEMPLATE_INFO, storedPolicyValue, withDefaults, } from "../../services/review_policy.js";
|
|
6
|
+
const ASSOCIATION_LABEL = {
|
|
7
|
+
OWNER: "Owner",
|
|
8
|
+
MEMBER: "Member",
|
|
9
|
+
COLLABORATOR: "Collaborator",
|
|
10
|
+
CONTRIBUTOR: "Earlier contributor",
|
|
11
|
+
FIRST_TIME_CONTRIBUTOR: "First time contributor",
|
|
12
|
+
FIRST_TIMER: "First timer",
|
|
13
|
+
NONE: "No relation to the repository",
|
|
14
|
+
};
|
|
15
|
+
/** JSON for an inline script: `<` is escaped so a rule name can never close
|
|
16
|
+
* the script tag. */
|
|
17
|
+
function scriptJson(value) {
|
|
18
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
19
|
+
}
|
|
4
20
|
function cronHint(repo, expression) {
|
|
5
21
|
const notBuilt = repo.knowledge_built_at
|
|
6
22
|
? ""
|
|
@@ -21,6 +37,30 @@ function cronHint(repo, expression) {
|
|
|
21
37
|
}
|
|
22
38
|
export function renderRepoSettings(username, repo, config) {
|
|
23
39
|
const name = repo.full_name;
|
|
40
|
+
const source = policySource(repo);
|
|
41
|
+
const stored = storedPolicyValue(source.policy);
|
|
42
|
+
const current = source.legacy
|
|
43
|
+
? "legacy"
|
|
44
|
+
: typeof stored === "string"
|
|
45
|
+
? stored
|
|
46
|
+
: "custom";
|
|
47
|
+
const templates = Object.fromEntries(Object.keys(TEMPLATES).map((key) => [key, withDefaults(TEMPLATES[key])]));
|
|
48
|
+
const policyData = {
|
|
49
|
+
current,
|
|
50
|
+
legacy: source.legacy,
|
|
51
|
+
policy: source.legacy ? templates.everyone : source.policy,
|
|
52
|
+
templates,
|
|
53
|
+
info: TEMPLATE_INFO,
|
|
54
|
+
associations: ASSOCIATIONS.map((value) => ({
|
|
55
|
+
value,
|
|
56
|
+
label: ASSOCIATION_LABEL[value],
|
|
57
|
+
})),
|
|
58
|
+
};
|
|
59
|
+
const policyOptions = [
|
|
60
|
+
...TEMPLATE_INFO.map((item) => `<option value="${text(item.name)}"${current === item.name ? " selected" : ""}>${text(item.label)}</option>`),
|
|
61
|
+
`<option value="custom"${current === "custom" ? " selected" : ""}>Custom rules</option>`,
|
|
62
|
+
`<option value="legacy"${current === "legacy" ? " selected" : ""}>Simple switches</option>`,
|
|
63
|
+
].join("");
|
|
24
64
|
const skip = repo.skip_drafts && repo.skip_bots
|
|
25
65
|
? "both"
|
|
26
66
|
: repo.skip_drafts
|
|
@@ -37,11 +77,57 @@ export function renderRepoSettings(username, repo, config) {
|
|
|
37
77
|
<div class="crumbs"><a href="/">Repositories</a> /
|
|
38
78
|
<a href="/repos/${text(name)}">${text(name)}</a> / Settings</div>
|
|
39
79
|
<div class="pagehead"><div><h1>Settings</h1></div></div>
|
|
80
|
+
<div class="card" data-async id="policy-card">
|
|
81
|
+
${skSlot()}
|
|
82
|
+
<div class="hd"><h2>Who gets a review</h2></div>
|
|
83
|
+
<div class="bd">
|
|
84
|
+
<div class="field wide">
|
|
85
|
+
<label for="policy-template">Policy</label>
|
|
86
|
+
<select id="policy-template">${policyOptions}</select>
|
|
87
|
+
<div class="hint" id="policy-desc"></div>
|
|
88
|
+
</div>
|
|
89
|
+
<div id="policy-body">
|
|
90
|
+
<div class="notice info"><div class="txt"><b>In short</b>
|
|
91
|
+
<ul id="policy-summary" aria-live="polite" style="margin:6px 0 0 18px;padding:0"></ul></div></div>
|
|
92
|
+
<p id="policy-problem" class="muted" role="alert" hidden></p>
|
|
93
|
+
<details id="policy-details" style="margin-top:14px">
|
|
94
|
+
<summary>Edit the rules</summary>
|
|
95
|
+
<p class="muted">The first rule that matches a pull request decides. If none matches, the last setting below decides.</p>
|
|
96
|
+
<div id="rules"></div>
|
|
97
|
+
<button class="btn sm" type="button" id="add-rule">Add rule</button>
|
|
98
|
+
<div class="field" style="margin-top:14px">
|
|
99
|
+
<label for="p-default">Any other pull request</label>
|
|
100
|
+
<select id="p-default">
|
|
101
|
+
<option value="review">Is reviewed automatically</option>
|
|
102
|
+
<option value="on-request">Waits for a maintainer request</option>
|
|
103
|
+
<option value="skip">Is not reviewed</option>
|
|
104
|
+
</select>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="two">
|
|
107
|
+
<div class="field"><label for="p-label">Request label</label><input id="p-label"></div>
|
|
108
|
+
<div class="field"><label for="p-command">Request comment</label><input id="p-command"></div>
|
|
109
|
+
<div class="field"><label for="p-scope">A request covers</label>
|
|
110
|
+
<select id="p-scope">
|
|
111
|
+
<option value="head">Only the commit it was made on</option>
|
|
112
|
+
<option value="pull-request">Every later push to the pull request</option>
|
|
113
|
+
</select></div>
|
|
114
|
+
<div class="field"><label for="p-max">Reviews per pull request</label>
|
|
115
|
+
<input id="p-max" placeholder="No limit"></div>
|
|
116
|
+
</div>
|
|
117
|
+
<fieldset id="p-requesters" class="ticks">
|
|
118
|
+
<legend>Who can ask for a review</legend>
|
|
119
|
+
</fieldset>
|
|
120
|
+
</details>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="ft"><button class="primary" id="save-policy">Save</button></div>
|
|
124
|
+
</div>
|
|
40
125
|
<div class="card" data-async>
|
|
41
126
|
${skSlot()}
|
|
42
|
-
<div class="hd"><h2
|
|
127
|
+
<div class="hd"><h2>${source.legacy ? "Automatic review" : "How reviews run"}</h2></div>
|
|
43
128
|
<div class="bd">
|
|
44
|
-
|
|
129
|
+
${source.legacy
|
|
130
|
+
? `<div id="switches"><div style="display:flex;align-items:center;gap:14px;margin-bottom:22px">
|
|
45
131
|
<button class="tg${repo.auto_review === 1 ? " on" : ""}" id="auto"></button>
|
|
46
132
|
<div>Review pull requests as they are pushed</div>
|
|
47
133
|
</div>
|
|
@@ -53,7 +139,8 @@ export function renderRepoSettings(username, repo, config) {
|
|
|
53
139
|
<option value="bots"${skip === "bots" ? " selected" : ""}>Bots only</option>
|
|
54
140
|
<option value="none"${skip === "none" ? " selected" : ""}>Nothing</option>
|
|
55
141
|
</select>
|
|
56
|
-
</div
|
|
142
|
+
</div></div>`
|
|
143
|
+
: ""}
|
|
57
144
|
<div class="field">
|
|
58
145
|
<label>Review</label>
|
|
59
146
|
<select id="scope">
|
|
@@ -111,6 +198,228 @@ export function renderRepoSettings(username, repo, config) {
|
|
|
111
198
|
</div>
|
|
112
199
|
<script>
|
|
113
200
|
var repo = ${JSON.stringify(name)};
|
|
201
|
+
var P = ${scriptJson(policyData)};
|
|
202
|
+
var state = null;
|
|
203
|
+
var previewTimer = null;
|
|
204
|
+
function el(tag, props, kids) {
|
|
205
|
+
var node = document.createElement(tag);
|
|
206
|
+
Object.keys(props || {}).forEach(function(key) {
|
|
207
|
+
if (key === "text") node.textContent = props[key];
|
|
208
|
+
else node.setAttribute(key, props[key]);
|
|
209
|
+
});
|
|
210
|
+
(kids || []).forEach(function(kid) { node.appendChild(kid); });
|
|
211
|
+
return node;
|
|
212
|
+
}
|
|
213
|
+
function tri(v) { return v === true ? "yes" : v === false ? "no" : "any"; }
|
|
214
|
+
function unTri(v) { return v === "yes" ? true : v === "no" ? false : undefined; }
|
|
215
|
+
function csv(text) {
|
|
216
|
+
return text.split(",").map(function(x) { return x.trim(); }).filter(Boolean);
|
|
217
|
+
}
|
|
218
|
+
function fromPolicy(policy) {
|
|
219
|
+
return {
|
|
220
|
+
rules: (policy.rules || []).map(function(rule) {
|
|
221
|
+
var w = rule.when || {};
|
|
222
|
+
var lines = w.changedLines || {};
|
|
223
|
+
return {
|
|
224
|
+
name: rule.name || "", action: rule.action,
|
|
225
|
+
association: (w.association || []).slice(),
|
|
226
|
+
fork: tri(w.fork), draft: tri(w.draft), bot: tri(w.bot),
|
|
227
|
+
labels: (w.labels || []).join(", "),
|
|
228
|
+
branches: (w.targetBranch || []).join(", "),
|
|
229
|
+
min: lines.min === undefined ? "" : String(lines.min),
|
|
230
|
+
max: lines.max === undefined ? "" : String(lines.max)
|
|
231
|
+
};
|
|
232
|
+
}),
|
|
233
|
+
def: policy.default, label: policy.requestLabel,
|
|
234
|
+
command: policy.requestCommand,
|
|
235
|
+
requesters: policy.requesters.slice(), scope: policy.approvalScope,
|
|
236
|
+
max: policy.maxRounds === undefined ? "" : String(policy.maxRounds)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function toPolicy() {
|
|
240
|
+
return {
|
|
241
|
+
rules: state.rules.map(function(r) {
|
|
242
|
+
var when = {};
|
|
243
|
+
if (r.association.length) when.association = r.association;
|
|
244
|
+
if (unTri(r.fork) !== undefined) when.fork = unTri(r.fork);
|
|
245
|
+
if (unTri(r.draft) !== undefined) when.draft = unTri(r.draft);
|
|
246
|
+
if (unTri(r.bot) !== undefined) when.bot = unTri(r.bot);
|
|
247
|
+
if (csv(r.labels).length) when.labels = csv(r.labels);
|
|
248
|
+
if (csv(r.branches).length) when.targetBranch = csv(r.branches);
|
|
249
|
+
if (r.min.trim() !== "" || r.max.trim() !== "") {
|
|
250
|
+
when.changedLines = {};
|
|
251
|
+
if (r.min.trim() !== "") when.changedLines.min = Number(r.min);
|
|
252
|
+
if (r.max.trim() !== "") when.changedLines.max = Number(r.max);
|
|
253
|
+
}
|
|
254
|
+
var out = { when: when, action: r.action };
|
|
255
|
+
if (r.name.trim()) out.name = r.name.trim();
|
|
256
|
+
return out;
|
|
257
|
+
}),
|
|
258
|
+
default: state.def, requestLabel: state.label,
|
|
259
|
+
requestCommand: state.command, requesters: state.requesters,
|
|
260
|
+
approvalScope: state.scope,
|
|
261
|
+
maxRounds: state.max.trim() === "" ? undefined : Number(state.max)
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function markCustom() {
|
|
265
|
+
var select = document.getElementById("policy-template");
|
|
266
|
+
if (select.value !== "legacy") select.value = "custom";
|
|
267
|
+
describe();
|
|
268
|
+
queuePreview();
|
|
269
|
+
}
|
|
270
|
+
function describe() {
|
|
271
|
+
var value = document.getElementById("policy-template").value;
|
|
272
|
+
var text = "";
|
|
273
|
+
if (value === "legacy") text = "The three switches below decide: automatic review on or off, and skipping drafts and bots.";
|
|
274
|
+
else if (value === "custom") text = "Your own rules, edited below.";
|
|
275
|
+
else P.info.forEach(function(item) { if (item.name === value) text = item.description; });
|
|
276
|
+
document.getElementById("policy-desc").textContent = text;
|
|
277
|
+
document.getElementById("policy-body").hidden = value === "legacy";
|
|
278
|
+
}
|
|
279
|
+
function queuePreview() {
|
|
280
|
+
clearTimeout(previewTimer);
|
|
281
|
+
previewTimer = setTimeout(preview, 250);
|
|
282
|
+
}
|
|
283
|
+
async function preview() {
|
|
284
|
+
var list = document.getElementById("policy-summary");
|
|
285
|
+
var problem = document.getElementById("policy-problem");
|
|
286
|
+
var saveBtn = document.getElementById("save-policy");
|
|
287
|
+
if (document.getElementById("policy-template").value === "legacy") return;
|
|
288
|
+
try {
|
|
289
|
+
var data = await api("POST", "/api/repos/policy-preview", { policy: toPolicy() });
|
|
290
|
+
list.replaceChildren();
|
|
291
|
+
data.summary.forEach(function(line) {
|
|
292
|
+
list.appendChild(el("li", { text: line }));
|
|
293
|
+
});
|
|
294
|
+
problem.hidden = true;
|
|
295
|
+
saveBtn.disabled = false;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
problem.textContent = "This policy cannot be saved yet: " + error.message;
|
|
298
|
+
problem.hidden = false;
|
|
299
|
+
saveBtn.disabled = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function field(id, labelText, control) {
|
|
303
|
+
return el("div", { "class": "field" }, [el("label", { "for": id, text: labelText }), control]);
|
|
304
|
+
}
|
|
305
|
+
function tickList(container, name, values, onChange) {
|
|
306
|
+
P.associations.forEach(function(a) {
|
|
307
|
+
var id = name + a.value;
|
|
308
|
+
var box = el("input", { type: "checkbox", id: id });
|
|
309
|
+
box.checked = values().indexOf(a.value) !== -1;
|
|
310
|
+
box.addEventListener("change", function() {
|
|
311
|
+
var next = P.associations.map(function(x) { return x.value; }).filter(function(v) {
|
|
312
|
+
return v === a.value ? box.checked : values().indexOf(v) !== -1;
|
|
313
|
+
});
|
|
314
|
+
onChange(next);
|
|
315
|
+
markCustom();
|
|
316
|
+
});
|
|
317
|
+
container.appendChild(el("label", { "for": id }, [box, el("span", { text: a.label })]));
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function renderRules() {
|
|
321
|
+
var box = document.getElementById("rules");
|
|
322
|
+
box.replaceChildren();
|
|
323
|
+
state.rules.forEach(function(rule, i) {
|
|
324
|
+
var pre = "r" + i + "-";
|
|
325
|
+
var bind = function(node, key) {
|
|
326
|
+
node.addEventListener("input", function() { rule[key] = node.value; markCustom(); });
|
|
327
|
+
return node;
|
|
328
|
+
};
|
|
329
|
+
var pick = function(id, key, options) {
|
|
330
|
+
var node = el("select", { id: id }, options.map(function(o) {
|
|
331
|
+
return el("option", { value: o[0], text: o[1] });
|
|
332
|
+
}));
|
|
333
|
+
node.value = rule[key];
|
|
334
|
+
node.addEventListener("change", function() { rule[key] = node.value; markCustom(); });
|
|
335
|
+
return node;
|
|
336
|
+
};
|
|
337
|
+
var move = function(text, label, to, disabled) {
|
|
338
|
+
var btn = el("button", { "class": "btn sm", type: "button", text: text, "aria-label": label });
|
|
339
|
+
btn.disabled = disabled;
|
|
340
|
+
btn.addEventListener("click", function() {
|
|
341
|
+
state.rules.splice(to, 0, state.rules.splice(i, 1)[0]);
|
|
342
|
+
renderRules(); markCustom();
|
|
343
|
+
});
|
|
344
|
+
return btn;
|
|
345
|
+
};
|
|
346
|
+
var del = el("button", { "class": "btn sm", type: "button", text: "Remove", "aria-label": "Remove rule " + (i + 1) });
|
|
347
|
+
del.addEventListener("click", function() {
|
|
348
|
+
state.rules.splice(i, 1);
|
|
349
|
+
renderRules(); markCustom();
|
|
350
|
+
});
|
|
351
|
+
var head = el("div", { style: "display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap" }, [
|
|
352
|
+
el("strong", { text: "Rule " + (i + 1), style: "align-self:center" }),
|
|
353
|
+
field(pre + "name", "Name (optional)", bind(el("input", { id: pre + "name", value: rule.name }), "name")),
|
|
354
|
+
field(pre + "action", "Then", pick(pre + "action", "action", [
|
|
355
|
+
["review", "Review it"], ["on-request", "Wait for a request"], ["skip", "Do not review it"]
|
|
356
|
+
]))
|
|
357
|
+
]);
|
|
358
|
+
var authors = el("fieldset", { "class": "ticks" }, [
|
|
359
|
+
el("legend", { text: "When the author is (none ticked means anyone)" })
|
|
360
|
+
]);
|
|
361
|
+
tickList(authors, pre + "a-", function() { return rule.association; }, function(next) { rule.association = next; });
|
|
362
|
+
var yesNo = [["any", "Either"], ["yes", "Yes"], ["no", "No"]];
|
|
363
|
+
var flags = el("div", { "class": "two" }, [
|
|
364
|
+
field(pre + "fork", "Opened from a fork", pick(pre + "fork", "fork", yesNo)),
|
|
365
|
+
field(pre + "draft", "Is a draft", pick(pre + "draft", "draft", yesNo)),
|
|
366
|
+
field(pre + "bot", "Is by a bot", pick(pre + "bot", "bot", yesNo)),
|
|
367
|
+
field(pre + "labels", "Has one of these labels (comma separated)", bind(el("input", { id: pre + "labels", value: rule.labels }), "labels")),
|
|
368
|
+
field(pre + "branches", "Targets one of these branches (comma separated)", bind(el("input", { id: pre + "branches", value: rule.branches }), "branches")),
|
|
369
|
+
field(pre + "min", "Changes at least this many lines", bind(el("input", { id: pre + "min", value: rule.min }), "min")),
|
|
370
|
+
field(pre + "max", "Changes at most this many lines", bind(el("input", { id: pre + "max", value: rule.max }), "max"))
|
|
371
|
+
]);
|
|
372
|
+
box.appendChild(el("div", { "class": "rule" }, [
|
|
373
|
+
head, authors, flags,
|
|
374
|
+
el("div", { style: "display:flex;gap:8px;margin-top:8px" }, [
|
|
375
|
+
move("Move up", "Move rule " + (i + 1) + " up", i - 1, i === 0),
|
|
376
|
+
move("Move down", "Move rule " + (i + 1) + " down", i + 1, i === state.rules.length - 1),
|
|
377
|
+
del
|
|
378
|
+
])
|
|
379
|
+
]));
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
function renderSettings() {
|
|
383
|
+
document.getElementById("p-default").value = state.def;
|
|
384
|
+
document.getElementById("p-label").value = state.label;
|
|
385
|
+
document.getElementById("p-command").value = state.command;
|
|
386
|
+
document.getElementById("p-scope").value = state.scope;
|
|
387
|
+
document.getElementById("p-max").value = state.max;
|
|
388
|
+
var box = document.getElementById("p-requesters");
|
|
389
|
+
box.querySelectorAll("label").forEach(function(n) { n.remove(); });
|
|
390
|
+
tickList(box, "p-req-", function() { return state.requesters; }, function(next) { state.requesters = next; });
|
|
391
|
+
}
|
|
392
|
+
function loadState(policy) {
|
|
393
|
+
state = fromPolicy(policy);
|
|
394
|
+
renderRules();
|
|
395
|
+
renderSettings();
|
|
396
|
+
}
|
|
397
|
+
[["p-default", "def"], ["p-label", "label"], ["p-command", "command"], ["p-scope", "scope"], ["p-max", "max"]].forEach(function(pair) {
|
|
398
|
+
var node = document.getElementById(pair[0]);
|
|
399
|
+
node.addEventListener(node.tagName === "SELECT" ? "change" : "input", function() {
|
|
400
|
+
state[pair[1]] = node.value;
|
|
401
|
+
markCustom();
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
document.getElementById("add-rule").addEventListener("click", function() {
|
|
405
|
+
state.rules.push({ name: "", action: "skip", association: [], fork: "any", draft: "any", bot: "any", labels: "", branches: "", min: "", max: "" });
|
|
406
|
+
renderRules(); markCustom();
|
|
407
|
+
var last = document.getElementById("r" + (state.rules.length - 1) + "-name");
|
|
408
|
+
if (last) last.focus();
|
|
409
|
+
});
|
|
410
|
+
document.getElementById("policy-template").addEventListener("change", function() {
|
|
411
|
+
if (P.templates[this.value]) loadState(P.templates[this.value]);
|
|
412
|
+
describe();
|
|
413
|
+
queuePreview();
|
|
414
|
+
});
|
|
415
|
+
document.getElementById("save-policy").addEventListener("click", function() {
|
|
416
|
+
var value = document.getElementById("policy-template").value;
|
|
417
|
+
save(this, { reviewPolicy: value === "legacy" ? "legacy" : toPolicy() });
|
|
418
|
+
});
|
|
419
|
+
loadState(P.policy);
|
|
420
|
+
describe();
|
|
421
|
+
if (P.current !== "legacy") queuePreview();
|
|
422
|
+
|
|
114
423
|
function save(btn, body) {
|
|
115
424
|
run(btn, btn.closest("[data-async]"), async function() {
|
|
116
425
|
await api("PATCH", "/api/repos/" + repo, body);
|
|
@@ -118,18 +427,23 @@ function save(btn, body) {
|
|
|
118
427
|
});
|
|
119
428
|
}
|
|
120
429
|
document.getElementById("save-auto").addEventListener("click", function() {
|
|
121
|
-
var
|
|
122
|
-
save(this, {
|
|
123
|
-
autoReview: document.getElementById("auto").classList.contains("on"),
|
|
124
|
-
skipDrafts: skip === "both" || skip === "drafts",
|
|
125
|
-
skipBots: skip === "both" || skip === "bots",
|
|
430
|
+
var body = {
|
|
126
431
|
reviewScope: document.getElementById("scope").value,
|
|
127
432
|
useCodegraph: document.getElementById("codegraph").classList.contains("on")
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
document.getElementById("
|
|
131
|
-
|
|
433
|
+
};
|
|
434
|
+
if (document.getElementById("auto")) {
|
|
435
|
+
var skip = document.getElementById("skip").value;
|
|
436
|
+
body.autoReview = document.getElementById("auto").classList.contains("on");
|
|
437
|
+
body.skipDrafts = skip === "both" || skip === "drafts";
|
|
438
|
+
body.skipBots = skip === "both" || skip === "bots";
|
|
439
|
+
}
|
|
440
|
+
save(this, body);
|
|
132
441
|
});
|
|
442
|
+
if (document.getElementById("auto")) {
|
|
443
|
+
document.getElementById("auto").addEventListener("click", function() {
|
|
444
|
+
this.classList.toggle("on");
|
|
445
|
+
});
|
|
446
|
+
}
|
|
133
447
|
document.getElementById("codegraph").addEventListener("click", function() {
|
|
134
448
|
this.classList.toggle("on");
|
|
135
449
|
});
|
|
@@ -2,7 +2,8 @@ import { clearOauthStateCookieHeader, clearSessionCookieHeader, createSession, l
|
|
|
2
2
|
import { githubAuthorizeUrl, githubLoginFromCode } from "../../github/oauth.js";
|
|
3
3
|
import { handleClient, handleLogo, handleStyles } from "../assets.js";
|
|
4
4
|
import { activityFeed, listReposForHome, prDetail, repoKnowledge, RepoNotFound, repoOverview, repoPulls, repoRemoteReviews, runningJobs, skippedDeliveries, statsForRange, } from "../../services/dashboard.js";
|
|
5
|
-
import { escapeHtml, html
|
|
5
|
+
import { escapeHtml, html } from "./layout.js";
|
|
6
|
+
import { formatCostTotal } from "./cost.js";
|
|
6
7
|
import { readConfig, resolveAppPrivateKey, writeUserConfig, } from "../../config.js";
|
|
7
8
|
import { appNameProblem, beginManifestState, buildAppManifest, consumeManifestState, convertManifest, } from "../../github/app_manifest.js";
|
|
8
9
|
import { renderHome, renderLogin } from "./home.js";
|
|
@@ -210,7 +211,7 @@ function toHomeRow(item) {
|
|
|
210
211
|
knowledgeAt,
|
|
211
212
|
autoOn: item.repo.auto_review === 1,
|
|
212
213
|
reviews: String(item.stats.reviews),
|
|
213
|
-
cost:
|
|
214
|
+
cost: formatCostTotal(item.stats),
|
|
214
215
|
};
|
|
215
216
|
}
|
|
216
217
|
function requireRepo(fullName) {
|