rvlib-shipkit 1.1.1 → 1.3.0
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/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// THE PAGES OF THE TEST GROUPS: `tests` itself,
|
|
1
|
+
// THE PAGES OF THE TEST GROUPS: `tests` itself, every test file of the repo
|
|
2
|
+
// slowest first then one line per test check, and
|
|
2
3
|
// each test check (`test`, `test:<unit>`), which answers one question: where
|
|
3
4
|
// does its time go. The check's own facts (its sentence, its command, its
|
|
4
5
|
// last wall time and its trend) are the check page's pieces, read from the
|
|
5
6
|
// resource the tree already holds. The breakdown is the one fetch the page
|
|
6
7
|
// makes (tests-svc.ts), shaped on the server (collections/test-time.ts).
|
|
7
8
|
import { observer } from 'mobx-react-lite'
|
|
8
|
-
import type { FailingFile, FileTime, LastRun, Phases, Shape, TestCheckRow, TestCheckView, TestsRootView } from '../../collections/test-time.ts'
|
|
9
|
+
import type { FailingFile, FileTime, LastRun, Phases, Shape, TestCheckRow, TestCheckView, TestFileRow, TestsRootView } from '../../collections/test-time.ts'
|
|
9
10
|
import { CommandLine, LastRunValue, Row, Says, Section, Timings } from './check-view.tsx'
|
|
10
11
|
import { durationTone, fmtMs } from './dur.ts'
|
|
11
|
-
import type {
|
|
12
|
+
import type { RepoSt } from './repo-st.ts'
|
|
12
13
|
import { TimeAgo } from './time-ago.tsx'
|
|
13
14
|
|
|
14
15
|
const pct = (n: number | null): string => (n == null ? '' : `${Math.round(n * 100)}%`)
|
|
@@ -22,13 +23,13 @@ const TREND_TONE = { up: 'bad', down: 'fast', flat: 'dim' } as const
|
|
|
22
23
|
|
|
23
24
|
export const SHAPE_WORD: Record<Shape, string> = { tail: 'long tail', volume: 'volume', overhead: 'overhead' }
|
|
24
25
|
|
|
25
|
-
const FileLink = ({ app, file, name }: { app:
|
|
26
|
+
const FileLink = ({ app, file, name }: { app: RepoSt; file: string; name: string }) => (
|
|
26
27
|
<a href="#" title={`${file}: its own page`} onClick={(e) => { e.preventDefault(); e.stopPropagation(); app.tests.openFile(file) }}>{name}</a>
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
// ── failing now ─────────────────────────────────────────────────────────
|
|
30
31
|
|
|
31
|
-
const Failing = observer(({ app, rows, showCheck }: { app:
|
|
32
|
+
const Failing = observer(({ app, rows, showCheck }: { app: RepoSt; rows: FailingFile[]; showCheck: boolean }) => (
|
|
32
33
|
<Section title={`failing now (${rows.length})`} help="the files whose last recorded run failed: the same verdict that colours them red in the tree and lists them on the runs page tab">
|
|
33
34
|
{rows.length === 0
|
|
34
35
|
? <div className="dim">nothing: every file passed its last recorded run</div>
|
|
@@ -50,7 +51,7 @@ const Failing = observer(({ app, rows, showCheck }: { app: HubSt; rows: FailingF
|
|
|
50
51
|
|
|
51
52
|
// ── where the time goes ─────────────────────────────────────────────────
|
|
52
53
|
|
|
53
|
-
const Summary = observer(({ app, l }: { app:
|
|
54
|
+
const Summary = observer(({ app, l }: { app: RepoSt; l: LastRun }) => (
|
|
54
55
|
<div className="tsum">
|
|
55
56
|
<div className="tsline">
|
|
56
57
|
<span><b>{l.files}</b> files</span>
|
|
@@ -121,7 +122,7 @@ const PhaseFacts = ({ p, wallMs }: { p: Phases; wallMs: number | null }) => {
|
|
|
121
122
|
)
|
|
122
123
|
}
|
|
123
124
|
|
|
124
|
-
const FileRows = observer(({ app, files }: { app:
|
|
125
|
+
const FileRows = observer(({ app, files }: { app: RepoSt; files: FileTime[] }) => (
|
|
125
126
|
<div className="rlist">
|
|
126
127
|
<div className="rhead rtime">
|
|
127
128
|
<span className="tleft">file</span>
|
|
@@ -152,13 +153,13 @@ const FileRows = observer(({ app, files }: { app: HubSt; files: FileTime[] }) =>
|
|
|
152
153
|
|
|
153
154
|
// ── the pages ────────────────────────────────────────────────────────────
|
|
154
155
|
|
|
155
|
-
const Loading = observer(({ app, check }: { app:
|
|
156
|
+
const Loading = observer(({ app, check }: { app: RepoSt; check: string | null }) => {
|
|
156
157
|
const err = app.tests.error(check)
|
|
157
158
|
if (err !== '') return <div className="warnline">✗ {err}</div>
|
|
158
159
|
return <div className="dim">{app.tests.isLoading(check) ? 'reading the record…' : 'nothing loaded yet'}</div>
|
|
159
160
|
})
|
|
160
161
|
|
|
161
|
-
export const TestCheckPage = observer(({ app, check }: { app:
|
|
162
|
+
export const TestCheckPage = observer(({ app, check }: { app: RepoSt; check: string }) => {
|
|
162
163
|
const i = app.cols.find('checks', `check:${check}`)
|
|
163
164
|
const u = i?.check ?? null
|
|
164
165
|
const v: TestCheckView | null = app.tests.check(check)
|
|
@@ -194,7 +195,7 @@ export const TestCheckPage = observer(({ app, check }: { app: HubSt; check: stri
|
|
|
194
195
|
)
|
|
195
196
|
})
|
|
196
197
|
|
|
197
|
-
const CheckRows = observer(({ app, rows }: { app:
|
|
198
|
+
const CheckRows = observer(({ app, rows }: { app: RepoSt; rows: TestCheckRow[] }) => (
|
|
198
199
|
<div className="rlist">
|
|
199
200
|
<div className="rhead rcheckt">
|
|
200
201
|
<span className="tleft">check</span>
|
|
@@ -217,16 +218,42 @@ const CheckRows = observer(({ app, rows }: { app: HubSt; rows: TestCheckRow[] })
|
|
|
217
218
|
</div>
|
|
218
219
|
))
|
|
219
220
|
|
|
220
|
-
|
|
221
|
+
const STATUS_WORD = { pass: 'pass', fail: 'fail' } as const
|
|
222
|
+
|
|
223
|
+
/** every file of every test check on one list, slowest first: the answer to
|
|
224
|
+
* "which tests are slow", whatever check runs them */
|
|
225
|
+
const AllFiles = observer(({ app, rows }: { app: RepoSt; rows: TestFileRow[] }) => (
|
|
226
|
+
<div className="rlist">
|
|
227
|
+
<div className="rhead rall">
|
|
228
|
+
<span className="tleft">file</span>
|
|
229
|
+
<span className="tleft">check</span>
|
|
230
|
+
<span>duration</span>
|
|
231
|
+
<span>status</span>
|
|
232
|
+
<span>when</span>
|
|
233
|
+
</div>
|
|
234
|
+
{rows.map((f) => (
|
|
235
|
+
<div key={f.file} className={`rline rall${f.status === 'fail' ? ' red' : ''}`} title={f.file} onClick={() => app.tests.openFile(f.file)}>
|
|
236
|
+
<span className="rwhat">{f.inUnit}</span>
|
|
237
|
+
<span className="rtag" title="the check that runs it">{f.check}</span>
|
|
238
|
+
<span className={`rnum ${fileTone(f)}`}>{f.ms == null ? <span className="dim">never ran</span> : fmtMs(f.ms)}</span>
|
|
239
|
+
<span className={`rnum ${f.status === 'fail' ? 'bad' : 'dim'}`}>{f.status == null ? '' : STATUS_WORD[f.status]}</span>
|
|
240
|
+
{f.at == null ? <span /> : <TimeAgo clock={app.clock} at={f.at} />}
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
))
|
|
245
|
+
|
|
246
|
+
export const TestsRootPage = observer(({ app }: { app: RepoSt }) => {
|
|
221
247
|
const v: TestsRootView | null = app.tests.root()
|
|
222
248
|
return (
|
|
223
249
|
<div className="colcard checkcard">
|
|
224
250
|
<div className="chead">
|
|
225
251
|
<span className="tid">tests</span>
|
|
226
|
-
<h3 className="ctitle">every test
|
|
252
|
+
<h3 className="ctitle">every test</h3>
|
|
227
253
|
</div>
|
|
228
|
-
<div className="usays">
|
|
254
|
+
<div className="usays">every test file by its last recorded duration, then each test check's last run. Click a file for its page, a check for where its time goes</div>
|
|
229
255
|
{v == null ? <Loading app={app} check={null} /> : <>
|
|
256
|
+
<Section title={`every test file (${v.files.length})`} help="every file of every test check, by its last recorded duration in the shared run, slowest first; the ones that never ran here last. Its tone is against its budget: over it reads red"><AllFiles app={app} rows={v.files} /></Section>
|
|
230
257
|
<Section title="test checks" help="one line per test check, slowest wall first"><CheckRows app={app} rows={v.checks} /></Section>
|
|
231
258
|
<Failing app={app} rows={v.failing} showCheck />
|
|
232
259
|
</>}
|
|
@@ -234,5 +261,5 @@ export const TestsRootPage = observer(({ app }: { app: HubSt }) => {
|
|
|
234
261
|
)
|
|
235
262
|
})
|
|
236
263
|
|
|
237
|
-
export const TestsPage = observer(({ app, check }: { app:
|
|
264
|
+
export const TestsPage = observer(({ app, check }: { app: RepoSt; check: string | null }) =>
|
|
238
265
|
check == null ? <TestsRootPage app={app} /> : <TestCheckPage app={app} check={check} />)
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
// THE ONE RUN CHART: every check run a bar at its true start and length,
|
|
2
2
|
// parallel ones stacked in lanes, each invocation a band behind its bars, now
|
|
3
|
-
// on the right.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// only draws.
|
|
3
|
+
// on the right. Two views draw it and nothing else draws a run: the dock at
|
|
4
|
+
// the bottom of the page, what runs now in every repo, one group of lanes
|
|
5
|
+
// each; and a repo's runs page, that repo's whole record. Geometry is
|
|
6
|
+
// timeline-geometry.ts, view state chart-view.ts; this file only draws.
|
|
7
7
|
import { observer } from 'mobx-react-lite'
|
|
8
|
-
import {
|
|
9
|
-
import type
|
|
8
|
+
import { autoUpdate, FloatingPortal, useFloating } from '@floating-ui/react'
|
|
9
|
+
import { useEffect, useLayoutEffect, useRef, type ReactNode } from 'react'
|
|
10
|
+
import { middleware } from './floating.tsx'
|
|
10
11
|
import type { TimelineBar } from '../../ledger/run-timeline.ts'
|
|
11
12
|
import type { ChartTip, ChartView } from './chart-view.ts'
|
|
12
13
|
import { durationTone, fmtMs } from './dur.ts'
|
|
13
|
-
import { bandLabels, gapSlices, ROWS, ticks,
|
|
14
|
+
import { bandLabels, gapSlices, ROWS, ticks, type GapSlice, type PlacedBar, type PlacedGroup, type RowSize } from './timeline-geometry.ts'
|
|
14
15
|
import { CACHED } from '../../core/outcome-words.ts'
|
|
15
16
|
|
|
16
17
|
/** who started a run, in words. A starter this build does not name reads as
|
|
17
18
|
* the word the record holds */
|
|
18
|
-
export const BY_WORD: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: '
|
|
19
|
+
export const BY_WORD: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: 'the plan', save: 'a save', unknown: 'before the record' }
|
|
19
20
|
export const byWords = (by: string): string => BY_WORD[by] ?? by
|
|
20
21
|
|
|
21
22
|
/** the one tone a bar wears: running in the accent, then the verdict, a
|
|
@@ -35,48 +36,59 @@ export const clockWords = (t: number): string => new Date(t).toLocaleTimeString(
|
|
|
35
36
|
* when it ended or that it still grows */
|
|
36
37
|
export const gapTipWords = (g: GapSlice): string => (g.live ? `since ${clockWords(g.t0)}, nothing ran` : `${clockWords(g.t0)} to ${clockWords(g.t1)}, nothing ran`)
|
|
37
38
|
|
|
38
|
-
/** a point on the viewport
|
|
39
|
-
*
|
|
39
|
+
/** a point on the viewport the tooltip floats from, out of every box of
|
|
40
|
+
* the strip that clips */
|
|
40
41
|
export type TipAt = { x: number; y: number }
|
|
41
42
|
|
|
42
43
|
/** what hovering a bar says: its check, its verdict with its duration or its
|
|
43
44
|
* elapsed time, when it started, the run and who started it */
|
|
44
45
|
export const barTip = (b: PlacedBar, now: number, at: TipAt): ChartTip => ({
|
|
45
46
|
...at,
|
|
46
|
-
head: b.id
|
|
47
|
+
head: b.group == null ? b.id : `${b.group} · ${b.id}`,
|
|
47
48
|
tone: b.status === 'fail' ? 'red' : b.status === 'warn' ? 'warn' : 'idle',
|
|
48
49
|
lines: [verdictWords(b, now), `${clockWords(b.start)} · ${b.label} · ${byWords(b.by)}`],
|
|
49
50
|
})
|
|
50
51
|
|
|
51
|
-
/** right of the pointer, or left of it past the middle of the window, so a
|
|
52
|
-
* tooltip over the running bars at the right edge stays on screen */
|
|
53
|
-
export const tipStyle = (t: TipAt, viewportW: number): { left: number; top: number } | { right: number; top: number } =>
|
|
54
|
-
t.x > viewportW / 2 ? { right: viewportW - t.x, top: t.y } : { left: t.x, top: t.y }
|
|
55
|
-
|
|
56
52
|
/** under the element the pointer entered, at the pointer's x */
|
|
57
53
|
const tipAtEvent = (e: React.MouseEvent<Element>): TipAt => ({ x: e.clientX, y: e.currentTarget.getBoundingClientRect().bottom + 4 })
|
|
58
54
|
|
|
55
|
+
/** what a tooltip says: its head in its tone, then its lines */
|
|
56
|
+
export const TipBody = ({ t }: { t: ChartTip }) => (
|
|
57
|
+
<>
|
|
58
|
+
<b className={`tl ${t.tone}`}>{t.head}</b>
|
|
59
|
+
{t.lines.map((l) => <span key={l}>{l}</span>)}
|
|
60
|
+
</>
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
/** the hovered mark's words, floated from the point the pointer entered at
|
|
64
|
+
* (floating.tsx middleware: it flips and shifts to stay in the window) */
|
|
59
65
|
const Tip = observer(({ view }: { view: ChartView }) => {
|
|
60
66
|
const t = view.hover
|
|
67
|
+
const f = useFloating({ open: t != null, placement: 'bottom-start', strategy: 'fixed', middleware: middleware(0), whileElementsMounted: autoUpdate })
|
|
68
|
+
const x = t?.x ?? 0
|
|
69
|
+
const y = t?.y ?? 0
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
f.refs.setPositionReference({ getBoundingClientRect: () => ({ x, y, left: x, top: y, right: x, bottom: y, width: 0, height: 0 }) })
|
|
72
|
+
}, [f.refs, x, y])
|
|
61
73
|
if (t == null) return null
|
|
62
74
|
return (
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</div>
|
|
75
|
+
<FloatingPortal>
|
|
76
|
+
<div ref={f.refs.setFloating} className="tlaxtip" style={f.floatingStyles}><TipBody t={t} /></div>
|
|
77
|
+
</FloatingPortal>
|
|
67
78
|
)
|
|
68
79
|
})
|
|
69
80
|
|
|
70
81
|
export type ChartProps = {
|
|
71
82
|
view: ChartView
|
|
72
83
|
compact?: boolean
|
|
73
|
-
/**
|
|
74
|
-
|
|
84
|
+
/** the lane sizes; compact draws `compact` and anything else `full` */
|
|
85
|
+
rows?: RowSize
|
|
75
86
|
/** keep now in sight: while the reader has not scrolled away, the view
|
|
76
87
|
* follows the right edge as the window slides */
|
|
77
88
|
follow?: boolean
|
|
78
89
|
onBar: (b: PlacedBar) => void
|
|
79
|
-
|
|
90
|
+
/** what stands left of a group's lanes, when the chart has groups */
|
|
91
|
+
groupLabel?: (g: PlacedGroup) => ReactNode
|
|
80
92
|
/** the zoom control, floating over the top right corner: shown while the
|
|
81
93
|
* pointer is on the chart and while it is zoomed away from home */
|
|
82
94
|
zoomable?: boolean
|
|
@@ -91,7 +103,7 @@ export const DRAG_PX = 4
|
|
|
91
103
|
/** a press that moved this far is a pan, and never also a click */
|
|
92
104
|
export const isPan = (x0: number, x: number): boolean => Math.abs(x - x0) >= DRAG_PX
|
|
93
105
|
|
|
94
|
-
export const TimelineChart = observer(({ view, compact = false,
|
|
106
|
+
export const TimelineChart = observer(({ view, compact = false, rows, follow = false, onBar, groupLabel, zoomable = false }: ChartProps) => {
|
|
95
107
|
const cap = view.cap
|
|
96
108
|
const ref = useRef<HTMLDivElement>(null)
|
|
97
109
|
/** where to scroll once the next zoom has rendered */
|
|
@@ -176,8 +188,8 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
|
|
|
176
188
|
placed.current = true
|
|
177
189
|
}
|
|
178
190
|
})
|
|
179
|
-
const r = compact ?
|
|
180
|
-
const lanes =
|
|
191
|
+
const r = ROWS[rows ?? (compact ? 'compact' : 'full')]
|
|
192
|
+
const lanes = l?.lanes ?? 0
|
|
181
193
|
const lanesH = lanes * (r.lane + r.gap)
|
|
182
194
|
const top = r.band + r.conc
|
|
183
195
|
const h = top + Math.max(r.lane, lanesH) + r.axis
|
|
@@ -196,7 +208,8 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
|
|
|
196
208
|
<div className="tlaxin" style={{ width: l.axis.width }}>
|
|
197
209
|
<svg className="tlsvg" width={l.axis.width} height={h}>
|
|
198
210
|
{l.bands.map((b) => (
|
|
199
|
-
<rect key={b.runId} className={`tlband${b.reds > 0 ? ' red' : ''}${b.running ? ' run' : ''}`} x={b.x}
|
|
211
|
+
<rect key={b.runId} className={`tlband${b.reds > 0 ? ' red' : ''}${b.running ? ' run' : ''}`} x={b.x} width={Math.max(1, b.w)}
|
|
212
|
+
y={b.lane0 == null ? 0 : laneY(b.lane0)} height={b.lane0 == null ? top + lanesH : (b.lanes ?? 1) * (r.lane + r.gap) - r.gap} />
|
|
200
213
|
))}
|
|
201
214
|
{compact ? null : bandLabels(l.bands).map((b) => (
|
|
202
215
|
<text key={b.runId} className="tlbandlbl" x={b.x} y={11}
|
|
@@ -238,16 +251,9 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
|
|
|
238
251
|
onMouseLeave={() => hover(null)}
|
|
239
252
|
onClick={() => onBar(b)} />
|
|
240
253
|
))}
|
|
241
|
-
{
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return (
|
|
245
|
-
<rect key={rl.repo} className="tlb repo" x={x} y={laneY(lane)} width={Math.max(2, l.axis.nowX - x)} height={r.lane}
|
|
246
|
-
onMouseEnter={(e) => hover({ ...tipAtEvent(e), head: rl.repo, tone: 'idle', lines: [`${rl.running} running: ${rl.checks.slice(0, 4).join(', ')}${rl.checks.length > 4 ? ', …' : ''}`, `${rl.label} · ${byWords(rl.by)}`, onRepo == null ? '' : 'click opens its hub when it serves'].filter((x) => x !== '') })}
|
|
247
|
-
onMouseLeave={() => hover(null)}
|
|
248
|
-
onClick={() => onRepo?.(rl)} />
|
|
249
|
-
)
|
|
250
|
-
})}
|
|
254
|
+
{(l.groups ?? []).slice(1).map((g) => (
|
|
255
|
+
<line key={g.key} className="tlgrp" x1={0} x2={l.axis.width} y1={laneY(g.lane0) - r.gap / 2} y2={laneY(g.lane0) - r.gap / 2} />
|
|
256
|
+
))}
|
|
251
257
|
<line className="tlnow" x1={l.axis.nowX} x2={l.axis.nowX} y1={0} y2={top + lanesH} />
|
|
252
258
|
</svg>
|
|
253
259
|
<Tip view={view} />
|
|
@@ -255,10 +261,20 @@ export const TimelineChart = observer(({ view, compact = false, repos = [], foll
|
|
|
255
261
|
)}
|
|
256
262
|
</div>
|
|
257
263
|
)
|
|
258
|
-
|
|
264
|
+
// a group's label stands in a gutter of its own, level with its lanes,
|
|
265
|
+
// so it stays in sight however far the chart is panned
|
|
266
|
+
const gutter = groupLabel == null || l?.groups == null ? null : (
|
|
267
|
+
<div className="tlgutter" style={{ height: h }}>
|
|
268
|
+
{l.groups.map((g) => (
|
|
269
|
+
<div key={g.key} className="tlglbl" style={{ top: laneY(g.lane0), height: g.lanes * (r.lane + r.gap) - r.gap }}>{groupLabel(g)}</div>
|
|
270
|
+
))}
|
|
271
|
+
</div>
|
|
272
|
+
)
|
|
273
|
+
const body = gutter == null ? chart : <div className="tlgroups">{gutter}{chart}</div>
|
|
274
|
+
if (!zoomable && !follow) return body
|
|
259
275
|
return (
|
|
260
276
|
<div className={`tlchart${view.zoomed ? ' zoomed' : ''}`}>
|
|
261
|
-
{
|
|
277
|
+
{body}
|
|
262
278
|
<span className="tlfloat">
|
|
263
279
|
{follow && !view.follow
|
|
264
280
|
? <button className="tllive" onClick={() => view.attach()} title={`the view stays where it is; back to now and follow it (${FOLLOW_KEY})`}>● live</button>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// The horizontal run timeline, as geometry: one time axis with now on the
|
|
2
2
|
// right, every bar at its true start and length, parallel bars stacked in
|
|
3
3
|
// lanes, and the zoom math. Pure, so a test pins every position and the view
|
|
4
|
-
// only draws what this returns.
|
|
5
|
-
//
|
|
4
|
+
// only draws what this returns. Two users: the dock at the bottom of the
|
|
5
|
+
// page, a rolling window that ends now with one group of lanes per repo, and
|
|
6
|
+
// a repo's runs page, its whole record.
|
|
6
7
|
//
|
|
7
8
|
// ONE rule bends a plain linear axis over the record: an idle stretch longer
|
|
8
9
|
// than GAP_MS is folded to a fixed GAP_PX slice that says how long it was,
|
|
@@ -30,11 +31,14 @@ export const TICK_MIN_PX = 72
|
|
|
30
31
|
export type Span = { start: number; end: number }
|
|
31
32
|
|
|
32
33
|
/** the rows of a chart, in px: band labels, the concurrency band, the lanes,
|
|
33
|
-
* the time axis. Compact is
|
|
34
|
+
* the time axis. Compact is thin lanes and nothing else */
|
|
34
35
|
export const ROWS = {
|
|
35
36
|
full: { band: 16, conc: 20, lane: 8, gap: 2, axis: 18 },
|
|
36
37
|
compact: { band: 0, conc: 0, lane: 3, gap: 1, axis: 0 },
|
|
38
|
+
/** the slim dock: compact, with lanes a repo's label fits beside */
|
|
39
|
+
slim: { band: 0, conc: 0, lane: 4, gap: 1, axis: 0 },
|
|
37
40
|
} as const
|
|
41
|
+
export type RowSize = keyof typeof ROWS
|
|
38
42
|
|
|
39
43
|
/** how tall a chart with `lanes` lanes draws */
|
|
40
44
|
export const chartHeight = (lanes: number, compact = false): number => {
|
|
@@ -209,8 +213,15 @@ export function gapSlices(a: Axis, charPx = 6, padPx = 3): GapSlice[] {
|
|
|
209
213
|
})
|
|
210
214
|
}
|
|
211
215
|
|
|
212
|
-
export type PlacedBar = TimelineBar & { x: number; w: number; lane: number; runId: string; label: string; by: string }
|
|
213
|
-
|
|
216
|
+
export type PlacedBar = TimelineBar & { x: number; w: number; lane: number; runId: string; label: string; by: string; group?: string }
|
|
217
|
+
/** `lane0` and `lanes`: the lanes of its group, when the chart has groups */
|
|
218
|
+
export type PlacedBand = { runId: string; label: string; x: number; w: number; reds: number; running: boolean; lane0?: number; lanes?: number }
|
|
219
|
+
|
|
220
|
+
/** one repo's runs, a group of lanes of its own */
|
|
221
|
+
export type ChartGroup = { key: string; invocations: TimelineInvocation[] }
|
|
222
|
+
/** a group as drawn: its first lane, how many it takes, how many of its bars
|
|
223
|
+
* did not fit */
|
|
224
|
+
export type PlacedGroup = { key: string; lane0: number; lanes: number; hidden: number }
|
|
214
225
|
|
|
215
226
|
/** one step of the concurrency band: `n` checks in flight from x for w px */
|
|
216
227
|
export type ConcStep = { x: number; w: number; n: number; over: boolean }
|
|
@@ -225,6 +236,8 @@ export type Layout = {
|
|
|
225
236
|
conc: ConcStep[]
|
|
226
237
|
/** the most in flight at once in view */
|
|
227
238
|
concMax: number
|
|
239
|
+
/** the groups, top to bottom, when the chart has groups */
|
|
240
|
+
groups?: PlacedGroup[]
|
|
228
241
|
}
|
|
229
242
|
|
|
230
243
|
/** how many bars are in flight at each moment, as a step function: the
|
|
@@ -312,3 +325,25 @@ export function layoutTimeline(invs: TimelineInvocation[], now: number, pxPerMs:
|
|
|
312
325
|
})
|
|
313
326
|
return { axis, bars, bands, lanes: count, hidden, conc, concMax: Math.max(0, ...series.map((p) => p.n)) }
|
|
314
327
|
}
|
|
328
|
+
|
|
329
|
+
/** every group on one rolling window, stacked top to bottom: each packs its
|
|
330
|
+
* own lanes, at least `minLanes` so its label fits beside them and at most
|
|
331
|
+
* `laneCap`, and the concurrency band counts every group's bars together. A
|
|
332
|
+
* window only: over the whole record each group would fold its own idle
|
|
333
|
+
* stretches, and the groups would not share one axis */
|
|
334
|
+
export function layoutGroups(groups: ChartGroup[], now: number, pxPerMs: number, o: { from: number; laneCap?: number; minLanes?: number; cap?: number }): Layout {
|
|
335
|
+
const all = layoutTimeline(groups.flatMap((g) => g.invocations), now, pxPerMs, { from: o.from, cap: o.cap, laneCap: 0 })
|
|
336
|
+
const bars: PlacedBar[] = []
|
|
337
|
+
const bands: PlacedBand[] = []
|
|
338
|
+
const placed: PlacedGroup[] = []
|
|
339
|
+
let lane0 = 0
|
|
340
|
+
for (const g of groups) {
|
|
341
|
+
const l = layoutTimeline(g.invocations, now, pxPerMs, { from: o.from, laneCap: o.laneCap })
|
|
342
|
+
const lanes = Math.max(o.minLanes ?? 1, l.lanes)
|
|
343
|
+
for (const b of l.bars) bars.push({ ...b, lane: b.lane + lane0, group: g.key })
|
|
344
|
+
for (const b of l.bands) bands.push({ ...b, lane0, lanes })
|
|
345
|
+
placed.push({ key: g.key, lane0, lanes, hidden: l.hidden })
|
|
346
|
+
lane0 += lanes
|
|
347
|
+
}
|
|
348
|
+
return { axis: all.axis, bars, bands, lanes: lane0, hidden: placed.reduce((a, g) => a + g.hidden, 0), conc: all.conc, concMax: all.concMax, groups: placed }
|
|
349
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// The runs page, as it holds the record: what the server shaped from the run
|
|
2
2
|
// record (ledger/run-timeline.ts), and which invocations the reader opened.
|
|
3
3
|
// Loaded when the page opens and again after a run ends, so it is never a
|
|
4
|
-
// poll the rest of the page pays for. The
|
|
5
|
-
//
|
|
4
|
+
// poll the rest of the page pays for. The chart over the runs page reads the
|
|
5
|
+
// same record from here for the runs older than the live window.
|
|
6
6
|
import { testId } from '../../collections/collection-types.ts'
|
|
7
7
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
8
8
|
import type { Timeline } from '../../ledger/run-timeline.ts'
|
|
9
|
-
import type {
|
|
9
|
+
import type { RepoSt } from './repo-st.ts'
|
|
10
10
|
|
|
11
11
|
export type { Timeline }
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ export class TimelineSvc {
|
|
|
18
18
|
* the reader closes it, every other one is shut until opened */
|
|
19
19
|
opened = new Set<string>()
|
|
20
20
|
closed = new Set<string>()
|
|
21
|
-
constructor(private st:
|
|
21
|
+
constructor(private st: RepoSt) {
|
|
22
22
|
makeAutoObservable<this, 'st'>(this, { st: false, data: observableRef })
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -46,10 +46,11 @@ export class TimelineSvc {
|
|
|
46
46
|
this.st.cols.select('tests', testId(file))
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/** `fresh` after a run: the record it just wrote, not the warm answer */
|
|
50
|
+
async load(fresh = false): Promise<void> {
|
|
50
51
|
runInAction(() => { this.loading = true })
|
|
51
52
|
try {
|
|
52
|
-
const r = await fetch('/api/timeline.json')
|
|
53
|
+
const r = await fetch(this.st.url(fresh ? '/api/timeline.json?fresh=1' : '/api/timeline.json'))
|
|
53
54
|
if (!r.ok) throw new Error(await r.text())
|
|
54
55
|
const t = (await r.json()) as Timeline
|
|
55
56
|
runInAction(() => { this.data = t; this.error = '' })
|
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
// Every line sits on a grid of fixed columns (the .rline grids in hub.css):
|
|
6
6
|
// numbers right aligned in tabular figures, ran and cached each a column of
|
|
7
7
|
// their own told apart by colour, and no separator glyph on any line. The
|
|
8
|
-
// shaping is the server's (ledger/run-timeline.ts).
|
|
9
|
-
//
|
|
8
|
+
// shaping is the server's (ledger/run-timeline.ts). Over the tabs, the one run
|
|
9
|
+
// chart (timeline-chart.tsx) draws this repo's whole record in time; what
|
|
10
|
+
// runs now in every repo is the dock at the bottom of the page.
|
|
10
11
|
import { observer } from 'mobx-react-lite'
|
|
11
12
|
import { useEffect } from 'react'
|
|
12
13
|
import { CACHED, RAN } from '../../core/outcome-words.ts'
|
|
13
14
|
import type { InvocationSummary, SlowCheck, SuiteToFix, Timeline, TimelineBar, TimelineInvocation } from '../../ledger/run-timeline.ts'
|
|
14
|
-
import { byWords, verdictWords } from './timeline-chart.tsx'
|
|
15
|
+
import { byWords, TimelineChart, verdictWords } from './timeline-chart.tsx'
|
|
15
16
|
import { durationTone, fmtMs } from './dur.ts'
|
|
16
17
|
import { Help } from './help.tsx'
|
|
17
|
-
import type {
|
|
18
|
+
import type { RepoSt } from './repo-st.ts'
|
|
18
19
|
import { TimeAgo, stampWords } from './time-ago.tsx'
|
|
19
20
|
import { checkName, editorHref } from './check-words.ts'
|
|
20
21
|
import { RUNS_TABS, runsTab, runsTabId, type RunsTab } from './runs-tabs.ts'
|
|
@@ -40,7 +41,7 @@ export function tabBadge(tab: RunsTab, d: Timeline): TabBadge | null {
|
|
|
40
41
|
|
|
41
42
|
/** who started a run, as a tag short enough for one fixed column. A starter
|
|
42
43
|
* this build does not name reads as the word the record holds */
|
|
43
|
-
export const BY_TAG: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: '
|
|
44
|
+
export const BY_TAG: Record<string, string> = { cli: 'terminal', hub: 'hub', schedule: 'plan', save: 'save', unknown: 'unknown' }
|
|
44
45
|
export const byTag = (by: string): string => BY_TAG[by] ?? by
|
|
45
46
|
|
|
46
47
|
/** a check's verdict, as the label's colour class: the tree's `tl` tones */
|
|
@@ -90,7 +91,7 @@ export function childOrder(bars: readonly TimelineBar[]): TimelineBar[] {
|
|
|
90
91
|
|
|
91
92
|
/** one check of a run, a child line on the run's grid: its word sits in the
|
|
92
93
|
* column its run counts it in */
|
|
93
|
-
const CheckLine = ({ app, b, name }: { app:
|
|
94
|
+
const CheckLine = ({ app, b, name }: { app: RepoSt; b: TimelineBar; name: ChildName }) => (
|
|
94
95
|
<div className={`rline rchild${b.status === 'fail' ? ' red' : ''}`} onClick={() => app.timeline.openCheck(b.id)}
|
|
95
96
|
title={[name.description ?? '', `${b.id}: ${verdictWords(b)}`, stampWords(b.start)].filter((x) => x !== '').join('\n')}>
|
|
96
97
|
<span />
|
|
@@ -102,7 +103,7 @@ const CheckLine = ({ app, b, name }: { app: HubSt; b: TimelineBar; name: ChildNa
|
|
|
102
103
|
</div>
|
|
103
104
|
)
|
|
104
105
|
|
|
105
|
-
const RunLine = observer(({ app, inv, n }: { app:
|
|
106
|
+
const RunLine = observer(({ app, inv, n }: { app: RepoSt; inv: TimelineInvocation; n: number }) => {
|
|
106
107
|
const open = app.timeline.isOpen(inv.runId, n)
|
|
107
108
|
return (
|
|
108
109
|
<>
|
|
@@ -122,12 +123,12 @@ const RunLine = observer(({ app, inv, n }: { app: HubSt; inv: TimelineInvocation
|
|
|
122
123
|
|
|
123
124
|
/** the checks of an unfolded run, in a box of their own that scrolls: a full
|
|
124
125
|
* gate is fifty checks, which would push every other run off the page */
|
|
125
|
-
const RunChecks = observer(({ app, bars }: { app:
|
|
126
|
+
const RunChecks = observer(({ app, bars }: { app: RepoSt; bars: TimelineBar[] }) => {
|
|
126
127
|
const names = checkNames(app.cols.resources)
|
|
127
128
|
return <div className="rkids">{childOrder(bars).map((b) => <CheckLine key={`${b.id}:${b.start}`} app={app} b={b} name={nameOf(names, b.id)} />)}</div>
|
|
128
129
|
})
|
|
129
130
|
|
|
130
|
-
const Runs = ({ app, rows }: { app:
|
|
131
|
+
const Runs = ({ app, rows }: { app: RepoSt; rows: TimelineInvocation[] }) => (
|
|
131
132
|
rows.length === 0 ? <div className="rempty">no run on record yet</div> : <>{rows.map((inv, n) => <RunLine key={inv.runId} app={app} inv={inv} n={n} />)}</>
|
|
132
133
|
)
|
|
133
134
|
|
|
@@ -142,7 +143,7 @@ const inUnit = (r: SuiteToFix): string => {
|
|
|
142
143
|
return at < 0 ? r.file : r.file.slice(at + r.unit.length + 2)
|
|
143
144
|
}
|
|
144
145
|
|
|
145
|
-
const TestFiles = ({ app, rows, recorded }: { app:
|
|
146
|
+
const TestFiles = ({ app, rows, recorded }: { app: RepoSt; rows: SuiteToFix[]; recorded: boolean }) => {
|
|
146
147
|
if (!recorded) return <div className="rempty">no test file run recorded yet: the next run of a test check records one</div>
|
|
147
148
|
if (rows.length === 0) return <div className="rempty">every test file passed and fits the budget</div>
|
|
148
149
|
return (
|
|
@@ -166,7 +167,7 @@ const TestFiles = ({ app, rows, recorded }: { app: HubSt; rows: SuiteToFix[]; re
|
|
|
166
167
|
const FLAG_WORD = { over: 'at its ceiling', near: 'past half its ceiling', budget: 'over its budget' } as const
|
|
167
168
|
|
|
168
169
|
/** two numbers side by side need their names once, above the column */
|
|
169
|
-
const Slowest = ({ app, rows }: { app:
|
|
170
|
+
const Slowest = ({ app, rows }: { app: RepoSt; rows: SlowCheck[] }) => {
|
|
170
171
|
if (rows.length === 0) return <div className="rempty">nothing ran yet</div>
|
|
171
172
|
return (
|
|
172
173
|
<>
|
|
@@ -183,7 +184,7 @@ const Slowest = ({ app, rows }: { app: HubSt; rows: SlowCheck[] }) => {
|
|
|
183
184
|
)
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
const Longest = ({ app, rows }: { app:
|
|
187
|
+
const Longest = ({ app, rows }: { app: RepoSt; rows: InvocationSummary[] }) => {
|
|
187
188
|
if (rows.length === 0) return <div className="rempty">no run on record yet</div>
|
|
188
189
|
return (
|
|
189
190
|
<>
|
|
@@ -202,7 +203,7 @@ const Longest = ({ app, rows }: { app: HubSt; rows: InvocationSummary[] }) => {
|
|
|
202
203
|
)
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
const Tabs = observer(({ app, on }: { app:
|
|
206
|
+
const Tabs = observer(({ app, on }: { app: RepoSt; on: RunsTab }) => {
|
|
206
207
|
const t = app.timeline
|
|
207
208
|
const d = t.data
|
|
208
209
|
return (
|
|
@@ -218,21 +219,37 @@ const Tabs = observer(({ app, on }: { app: HubSt; on: RunsTab }) => {
|
|
|
218
219
|
})}
|
|
219
220
|
<Help text={RUNS_TABS.find((x) => x.key === on)!.help} />
|
|
220
221
|
<span className="sp" />
|
|
221
|
-
<button disabled={t.loading} onClick={() => void t.load()}>{t.loading ? 'reading…' : 'reload'}</button>
|
|
222
|
+
<button disabled={t.loading} onClick={() => void t.load(true)}>{t.loading ? 'reading…' : 'reload'}</button>
|
|
222
223
|
</div>
|
|
223
224
|
)
|
|
224
225
|
})
|
|
225
226
|
|
|
226
|
-
|
|
227
|
+
/** what the chart over the tabs is, behind its (?) */
|
|
228
|
+
export const RECORD_HELP = 'every check this repo ran, on record: the run bands, the time axis, and over the lanes how many checks ran at once, with the cap dashed and the stretches past it in orange. It opens on the last ten minutes and follows now: the right edge is the present. The wheel zooms and a drag pans; leaving the right edge stops the view where it is so a bar can be read, and ● live, the k key or panning back to the right edge follows now again. While a tooltip is open the view stands still. fit shows every run on record, and a click on a bar opens its check. What runs in every repo right now is the dock at the bottom of the page'
|
|
229
|
+
|
|
230
|
+
/** this repo's whole record in time, its height held so it never jumps
|
|
231
|
+
* under the eye as a run adds lanes and ends */
|
|
232
|
+
const RecordChart = observer(({ app }: { app: RepoSt }) => {
|
|
233
|
+
const l = app.live
|
|
234
|
+
return (
|
|
235
|
+
<div className="rchart" style={{ height: l.held.h }}>
|
|
236
|
+
<span className="rchelp"><Help text={RECORD_HELP} /></span>
|
|
237
|
+
<TimelineChart view={l.chart} follow zoomable onBar={(b) => app.cols.select('checks', `check:${b.id}`)} />
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
export const TimelineView = observer(({ app }: { app: RepoSt }) => {
|
|
227
243
|
const t = app.timeline
|
|
228
244
|
useEffect(() => { void t.load() }, [t])
|
|
229
245
|
const d = t.data
|
|
230
246
|
const on = runsTab(app.sel?.t === 'item' ? app.sel.id : null)
|
|
231
247
|
return (
|
|
232
248
|
<div className="colcard tlcard">
|
|
249
|
+
<RecordChart app={app} />
|
|
233
250
|
<Tabs app={app} on={on} />
|
|
234
251
|
{t.error !== '' ? <div className="warnline">{t.error}</div> : null}
|
|
235
|
-
{d == null ? <div className="dim"
|
|
252
|
+
{d == null ? <div className="dim twait"><span className="spin" />reading the run record…</div> : (
|
|
236
253
|
<div className={`rlist ${on}`}>
|
|
237
254
|
{on === 'runs' ? <Runs app={app} rows={d.invocations} />
|
|
238
255
|
: on === 'files' ? <TestFiles app={app} rows={d.testFiles} recorded={d.suitesRecorded} />
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// around. No row carries prose: the words are the tooltip and the page it opens.
|
|
5
5
|
import { observer } from 'mobx-react-lite'
|
|
6
6
|
import type { CSSProperties } from 'react'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
import { GLYPH, markOf, rowName, rowSlots, type TreeRow } from './tree-build.ts'
|
|
9
9
|
|
|
10
10
|
/** the fold marker: a chevron, right when the branch is shut and down when
|
|
@@ -15,7 +15,7 @@ export const Caret = ({ open }: { open: boolean }): React.ReactElement => (
|
|
|
15
15
|
</svg>
|
|
16
16
|
)
|
|
17
17
|
|
|
18
|
-
export const TreeRowView = observer(({ app, row }: { app:
|
|
18
|
+
export const TreeRowView = observer(({ app, row }: { app: RepoSt; row: TreeRow }) => {
|
|
19
19
|
const t = app.tree
|
|
20
20
|
const u = app.checks
|
|
21
21
|
const running = row.checks.some((id) => u.isRunning(id))
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// newline separated paths, text encodable and hand editable, and comes back
|
|
5
5
|
// on reopen.
|
|
6
6
|
import { makeAutoObservable } from 'mobx'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
import type { Resource } from './hub-types.ts'
|
|
9
9
|
import { buildPathTree, buildTypeTree, dirKeys, flattenTree, whenCounts, type TreeNode, type TreeRow } from './tree-build.ts'
|
|
10
10
|
|
|
@@ -37,7 +37,7 @@ export class TreeSvc {
|
|
|
37
37
|
/** the row key under the keyboard cursor */
|
|
38
38
|
cursor: string | null = null
|
|
39
39
|
|
|
40
|
-
constructor(private st:
|
|
40
|
+
constructor(private st: RepoSt) {
|
|
41
41
|
this.tab = readStore(TAB_KEY) === 'type' ? 'type' : 'path'
|
|
42
42
|
for (const p of readStore(EXPANDED_KEY).split('\n')) if (p !== '') this.expanded.add(p)
|
|
43
43
|
makeAutoObservable<this, 'st'>(this, { st: false })
|