adispatchd 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +195 -2
- package/dist/api/errors.d.ts +27 -0
- package/dist/api/errors.js +80 -0
- package/dist/api/errors.js.map +1 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +11 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/logs.d.ts +30 -0
- package/dist/api/logs.js +88 -0
- package/dist/api/logs.js.map +1 -0
- package/dist/api/serialize.d.ts +65 -0
- package/dist/api/serialize.js +135 -0
- package/dist/api/serialize.js.map +1 -0
- package/dist/api/server.d.ts +89 -0
- package/dist/api/server.js +751 -0
- package/dist/api/server.js.map +1 -0
- package/dist/api/sse.d.ts +90 -0
- package/dist/api/sse.js +225 -0
- package/dist/api/sse.js.map +1 -0
- package/dist/app.d.ts +173 -0
- package/dist/app.js +488 -0
- package/dist/app.js.map +1 -0
- package/dist/cli/config-validate.d.ts +6 -0
- package/dist/cli/config-validate.js +39 -0
- package/dist/cli/config-validate.js.map +1 -0
- package/dist/cli/control.d.ts +31 -0
- package/dist/cli/control.js +186 -0
- package/dist/cli/control.js.map +1 -0
- package/dist/cli/http-client.d.ts +57 -0
- package/dist/cli/http-client.js +174 -0
- package/dist/cli/http-client.js.map +1 -0
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.js +14 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-templates.d.ts +20 -0
- package/dist/cli/init-templates.js +275 -0
- package/dist/cli/init-templates.js.map +1 -0
- package/dist/cli/init.d.ts +16 -0
- package/dist/cli/init.js +107 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +25 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/program.d.ts +102 -0
- package/dist/cli/program.js +133 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/service.d.ts +27 -0
- package/dist/cli/service.js +252 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/start.d.ts +22 -0
- package/dist/cli/start.js +133 -0
- package/dist/cli/start.js.map +1 -0
- package/dist/cli/workflow-dry-run.d.ts +13 -0
- package/dist/cli/workflow-dry-run.js +144 -0
- package/dist/cli/workflow-dry-run.js.map +1 -0
- package/dist/config/field-catalog.d.ts +33 -0
- package/dist/config/field-catalog.js +172 -0
- package/dist/config/field-catalog.js.map +1 -0
- package/dist/config/filter-schema.d.ts +15 -0
- package/dist/config/filter-schema.js +110 -0
- package/dist/config/filter-schema.js.map +1 -0
- package/dist/config/index.d.ts +15 -0
- package/dist/config/index.js +16 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/issues.d.ts +27 -0
- package/dist/config/issues.js +37 -0
- package/dist/config/issues.js.map +1 -0
- package/dist/config/loader.d.ts +32 -0
- package/dist/config/loader.js +250 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/manager.d.ts +153 -0
- package/dist/config/manager.js +276 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/model.d.ts +334 -0
- package/dist/config/model.js +2 -0
- package/dist/config/model.js.map +1 -0
- package/dist/config/normalize.d.ts +20 -0
- package/dist/config/normalize.js +200 -0
- package/dist/config/normalize.js.map +1 -0
- package/dist/config/parameters.d.ts +37 -0
- package/dist/config/parameters.js +71 -0
- package/dist/config/parameters.js.map +1 -0
- package/dist/config/public.d.ts +6 -0
- package/dist/config/public.js +18 -0
- package/dist/config/public.js.map +1 -0
- package/dist/config/schemas.d.ts +538 -0
- package/dist/config/schemas.js +321 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/snapshot.d.ts +38 -0
- package/dist/config/snapshot.js +57 -0
- package/dist/config/snapshot.js.map +1 -0
- package/dist/config/validate.d.ts +33 -0
- package/dist/config/validate.js +346 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/core/clock.d.ts +59 -0
- package/dist/core/clock.js +133 -0
- package/dist/core/clock.js.map +1 -0
- package/dist/core/duration.d.ts +19 -0
- package/dist/core/duration.js +93 -0
- package/dist/core/duration.js.map +1 -0
- package/dist/core/errors.d.ts +62 -0
- package/dist/core/errors.js +72 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/ids.d.ts +57 -0
- package/dist/core/ids.js +88 -0
- package/dist/core/ids.js.map +1 -0
- package/dist/core/index.d.ts +13 -0
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/logger.d.ts +100 -0
- package/dist/core/logger.js +207 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/paths.d.ts +84 -0
- package/dist/core/paths.js +138 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/template.d.ts +136 -0
- package/dist/core/template.js +261 -0
- package/dist/core/template.js.map +1 -0
- package/dist/core/types.d.ts +544 -0
- package/dist/core/types.js +172 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/worker-protocol.d.ts +102 -0
- package/dist/core/worker-protocol.js +75 -0
- package/dist/core/worker-protocol.js.map +1 -0
- package/dist/executors/classify.d.ts +25 -0
- package/dist/executors/classify.js +54 -0
- package/dist/executors/classify.js.map +1 -0
- package/dist/executors/dispatcher.d.ts +35 -0
- package/dist/executors/dispatcher.js +68 -0
- package/dist/executors/dispatcher.js.map +1 -0
- package/dist/executors/env.d.ts +43 -0
- package/dist/executors/env.js +66 -0
- package/dist/executors/env.js.map +1 -0
- package/dist/executors/event-parser.d.ts +41 -0
- package/dist/executors/event-parser.js +106 -0
- package/dist/executors/event-parser.js.map +1 -0
- package/dist/executors/index.d.ts +12 -0
- package/dist/executors/index.js +13 -0
- package/dist/executors/index.js.map +1 -0
- package/dist/executors/log-capture.d.ts +57 -0
- package/dist/executors/log-capture.js +155 -0
- package/dist/executors/log-capture.js.map +1 -0
- package/dist/executors/process-identity.d.ts +124 -0
- package/dist/executors/process-identity.js +283 -0
- package/dist/executors/process-identity.js.map +1 -0
- package/dist/executors/shell-executor.d.ts +35 -0
- package/dist/executors/shell-executor.js +478 -0
- package/dist/executors/shell-executor.js.map +1 -0
- package/dist/executors/stream-redactor.d.ts +40 -0
- package/dist/executors/stream-redactor.js +89 -0
- package/dist/executors/stream-redactor.js.map +1 -0
- package/dist/executors/workspace.d.ts +10 -0
- package/dist/executors/workspace.js +47 -0
- package/dist/executors/workspace.js.map +1 -0
- package/dist/filters/evaluate.d.ts +77 -0
- package/dist/filters/evaluate.js +187 -0
- package/dist/filters/evaluate.js.map +1 -0
- package/dist/filters/fields.d.ts +35 -0
- package/dist/filters/fields.js +75 -0
- package/dist/filters/fields.js.map +1 -0
- package/dist/filters/index.d.ts +11 -0
- package/dist/filters/index.js +12 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/filters/iso-date.d.ts +10 -0
- package/dist/filters/iso-date.js +42 -0
- package/dist/filters/iso-date.js.map +1 -0
- package/dist/filters/predicate.d.ts +24 -0
- package/dist/filters/predicate.js +178 -0
- package/dist/filters/predicate.js.map +1 -0
- package/dist/filters/priority.d.ts +51 -0
- package/dist/filters/priority.js +91 -0
- package/dist/filters/priority.js.map +1 -0
- package/dist/filters/pushdown.d.ts +12 -0
- package/dist/filters/pushdown.js +52 -0
- package/dist/filters/pushdown.js.map +1 -0
- package/dist/filters/shell-check.d.ts +110 -0
- package/dist/filters/shell-check.js +304 -0
- package/dist/filters/shell-check.js.map +1 -0
- package/dist/identities/github-app.d.ts +55 -0
- package/dist/identities/github-app.js +160 -0
- package/dist/identities/github-app.js.map +1 -0
- package/dist/identities/index.d.ts +6 -0
- package/dist/identities/index.js +7 -0
- package/dist/identities/index.js.map +1 -0
- package/dist/identities/jwt.d.ts +24 -0
- package/dist/identities/jwt.js +56 -0
- package/dist/identities/jwt.js.map +1 -0
- package/dist/lifecycle/context.d.ts +106 -0
- package/dist/lifecycle/context.js +170 -0
- package/dist/lifecycle/context.js.map +1 -0
- package/dist/lifecycle/engine.d.ts +185 -0
- package/dist/lifecycle/engine.js +551 -0
- package/dist/lifecycle/engine.js.map +1 -0
- package/dist/lifecycle/errors.d.ts +48 -0
- package/dist/lifecycle/errors.js +71 -0
- package/dist/lifecycle/errors.js.map +1 -0
- package/dist/lifecycle/index.d.ts +9 -0
- package/dist/lifecycle/index.js +9 -0
- package/dist/lifecycle/index.js.map +1 -0
- package/dist/lifecycle/performers.d.ts +66 -0
- package/dist/lifecycle/performers.js +179 -0
- package/dist/lifecycle/performers.js.map +1 -0
- package/dist/notifications/index.d.ts +6 -0
- package/dist/notifications/index.js +7 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +115 -0
- package/dist/notifications/service.js +364 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/persistence/actions.d.ts +101 -0
- package/dist/persistence/actions.js +196 -0
- package/dist/persistence/actions.js.map +1 -0
- package/dist/persistence/blocks.d.ts +85 -0
- package/dist/persistence/blocks.js +140 -0
- package/dist/persistence/blocks.js.map +1 -0
- package/dist/persistence/cron-firings.d.ts +55 -0
- package/dist/persistence/cron-firings.js +70 -0
- package/dist/persistence/cron-firings.js.map +1 -0
- package/dist/persistence/database.d.ts +60 -0
- package/dist/persistence/database.js +124 -0
- package/dist/persistence/database.js.map +1 -0
- package/dist/persistence/errors.d.ts +39 -0
- package/dist/persistence/errors.js +67 -0
- package/dist/persistence/errors.js.map +1 -0
- package/dist/persistence/events.d.ts +72 -0
- package/dist/persistence/events.js +83 -0
- package/dist/persistence/events.js.map +1 -0
- package/dist/persistence/index.d.ts +18 -0
- package/dist/persistence/index.js +19 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/instance-lock.d.ts +26 -0
- package/dist/persistence/instance-lock.js +117 -0
- package/dist/persistence/instance-lock.js.map +1 -0
- package/dist/persistence/lock-keys.d.ts +16 -0
- package/dist/persistence/lock-keys.js +41 -0
- package/dist/persistence/lock-keys.js.map +1 -0
- package/dist/persistence/notifications.d.ts +113 -0
- package/dist/persistence/notifications.js +213 -0
- package/dist/persistence/notifications.js.map +1 -0
- package/dist/persistence/persistence.d.ts +46 -0
- package/dist/persistence/persistence.js +42 -0
- package/dist/persistence/persistence.js.map +1 -0
- package/dist/persistence/retention.d.ts +46 -0
- package/dist/persistence/retention.js +68 -0
- package/dist/persistence/retention.js.map +1 -0
- package/dist/persistence/runs.d.ts +215 -0
- package/dist/persistence/runs.js +417 -0
- package/dist/persistence/runs.js.map +1 -0
- package/dist/persistence/schema.d.ts +16 -0
- package/dist/persistence/schema.js +183 -0
- package/dist/persistence/schema.js.map +1 -0
- package/dist/persistence/sqlite.d.ts +49 -0
- package/dist/persistence/sqlite.js +131 -0
- package/dist/persistence/sqlite.js.map +1 -0
- package/dist/runs/controller.d.ts +307 -0
- package/dist/runs/controller.js +1078 -0
- package/dist/runs/controller.js.map +1 -0
- package/dist/runs/errors.d.ts +20 -0
- package/dist/runs/errors.js +26 -0
- package/dist/runs/errors.js.map +1 -0
- package/dist/runs/event-bus.d.ts +106 -0
- package/dist/runs/event-bus.js +105 -0
- package/dist/runs/event-bus.js.map +1 -0
- package/dist/runs/execution.d.ts +39 -0
- package/dist/runs/execution.js +146 -0
- package/dist/runs/execution.js.map +1 -0
- package/dist/runs/index.d.ts +12 -0
- package/dist/runs/index.js +13 -0
- package/dist/runs/index.js.map +1 -0
- package/dist/runs/retention.d.ts +64 -0
- package/dist/runs/retention.js +232 -0
- package/dist/runs/retention.js.map +1 -0
- package/dist/runs/workspace.d.ts +20 -0
- package/dist/runs/workspace.js +51 -0
- package/dist/runs/workspace.js.map +1 -0
- package/dist/scheduler/checks.d.ts +41 -0
- package/dist/scheduler/checks.js +111 -0
- package/dist/scheduler/checks.js.map +1 -0
- package/dist/scheduler/cron.d.ts +32 -0
- package/dist/scheduler/cron.js +83 -0
- package/dist/scheduler/cron.js.map +1 -0
- package/dist/scheduler/dry-run.d.ts +42 -0
- package/dist/scheduler/dry-run.js +167 -0
- package/dist/scheduler/dry-run.js.map +1 -0
- package/dist/scheduler/errors.d.ts +22 -0
- package/dist/scheduler/errors.js +22 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/format.d.ts +3 -0
- package/dist/scheduler/format.js +73 -0
- package/dist/scheduler/format.js.map +1 -0
- package/dist/scheduler/index.d.ts +13 -0
- package/dist/scheduler/index.js +14 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/planner.d.ts +88 -0
- package/dist/scheduler/planner.js +255 -0
- package/dist/scheduler/planner.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +160 -0
- package/dist/scheduler/scheduler.js +680 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/state.d.ts +26 -0
- package/dist/scheduler/state.js +25 -0
- package/dist/scheduler/state.js.map +1 -0
- package/dist/scheduler/types.d.ts +197 -0
- package/dist/scheduler/types.js +3 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/service/index.d.ts +6 -0
- package/dist/service/index.js +7 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/install.d.ts +93 -0
- package/dist/service/install.js +250 -0
- package/dist/service/install.js.map +1 -0
- package/dist/service/unit.d.ts +50 -0
- package/dist/service/unit.js +171 -0
- package/dist/service/unit.js.map +1 -0
- package/dist/sources/github/adapter.d.ts +99 -0
- package/dist/sources/github/adapter.js +336 -0
- package/dist/sources/github/adapter.js.map +1 -0
- package/dist/sources/github/fields.d.ts +8 -0
- package/dist/sources/github/fields.js +72 -0
- package/dist/sources/github/fields.js.map +1 -0
- package/dist/sources/github/http.d.ts +100 -0
- package/dist/sources/github/http.js +288 -0
- package/dist/sources/github/http.js.map +1 -0
- package/dist/sources/github/index.d.ts +6 -0
- package/dist/sources/github/index.js +7 -0
- package/dist/sources/github/index.js.map +1 -0
- package/dist/sources/github/pushdown.d.ts +14 -0
- package/dist/sources/github/pushdown.js +47 -0
- package/dist/sources/github/pushdown.js.map +1 -0
- package/dist/sources/github/refs.d.ts +17 -0
- package/dist/sources/github/refs.js +61 -0
- package/dist/sources/github/refs.js.map +1 -0
- package/dist/sources/index.d.ts +6 -0
- package/dist/sources/index.js +7 -0
- package/dist/sources/index.js.map +1 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +28 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/public/app.css +239 -0
- package/dist/ui/public/favicon.svg +1 -0
- package/dist/ui/public/index.html +27 -0
- package/dist/ui/public/js/client/api.js +68 -0
- package/dist/ui/public/js/client/app.js +146 -0
- package/dist/ui/public/js/client/dom.js +34 -0
- package/dist/ui/public/js/client/page.js +9 -0
- package/dist/ui/public/js/client/pages/actions.js +35 -0
- package/dist/ui/public/js/client/pages/blocked.js +58 -0
- package/dist/ui/public/js/client/pages/config.js +126 -0
- package/dist/ui/public/js/client/pages/dashboard.js +175 -0
- package/dist/ui/public/js/client/pages/run.js +367 -0
- package/dist/ui/public/js/client/pages/runs.js +72 -0
- package/dist/ui/public/js/client/pages/views.js +80 -0
- package/dist/ui/public/js/client/pages/workflows.js +291 -0
- package/dist/ui/public/js/client/types.js +1 -0
- package/dist/ui/public/js/client/ui.js +152 -0
- package/dist/ui/public/js/shared/dom.js +77 -0
- package/dist/ui/public/js/shared/events.js +88 -0
- package/dist/ui/public/js/shared/format.js +213 -0
- package/dist/ui/public/js/shared/logs.js +136 -0
- package/dist/ui/public/js/shared/params.js +90 -0
- package/dist/ui/public/js/shared/route.js +36 -0
- package/dist/ui/public/js/shared/sort.js +70 -0
- package/dist/ui/public/js/shared/sse.js +193 -0
- package/dist/ui/public/js/shared/throttle.js +65 -0
- package/dist/views/index.d.ts +6 -0
- package/dist/views/index.js +7 -0
- package/dist/views/index.js.map +1 -0
- package/dist/views/service.d.ts +131 -0
- package/dist/views/service.js +264 -0
- package/dist/views/service.js.map +1 -0
- package/docs/api.md +239 -0
- package/docs/configuration-reference.md +1785 -0
- package/docs/cron-semantics.md +96 -0
- package/docs/manual-tests/github-fixture/config.yaml +30 -0
- package/docs/manual-tests/github-fixture/helpers.sh +93 -0
- package/docs/manual-tests/github-fixture/identities.yaml +18 -0
- package/docs/manual-tests/github-fixture/scripts/test-worker.sh +100 -0
- package/docs/manual-tests/github-fixture/sources.yaml +13 -0
- package/docs/manual-tests/github-fixture/views/dependencies.yaml +17 -0
- package/docs/manual-tests/github-fixture/views/needs-maintainer.yaml +17 -0
- package/docs/manual-tests/github-fixture/workflows/coder-test.yaml +76 -0
- package/docs/manual-tests/github-fixture/workflows/commenter-test.yaml +63 -0
- package/docs/manual-tests/github-fixture/workflows/reviewer-test.yaml +60 -0
- package/docs/manual-tests/github.md +1308 -0
- package/docs/manual-tests/process-supervision.md +64 -0
- package/docs/manual-tests/systemd.md +68 -0
- package/docs/operations.md +271 -0
- package/docs/worker-protocol.md +145 -0
- package/examples/config/config.yaml +35 -0
- package/examples/config/identities.yaml +18 -0
- package/examples/config/sources.yaml +13 -0
- package/examples/config/views/needs-maintainer.yaml +15 -0
- package/examples/config/workflows/coder-project-a.yaml +86 -0
- package/examples/config/workflows/deploy.yaml +15 -0
- package/examples/config/workflows/nightly-maintenance.yaml +13 -0
- package/package.json +75 -4
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure formatting helpers of the web UI (no DOM). Compiled for the browser
|
|
3
|
+
* (tsconfig.ui.json) and unit-tested under Node.
|
|
4
|
+
*/
|
|
5
|
+
/** Duration as `850 ms`, `12s`, `3m 05s`, `2h 04m`, `3d 02h`; `—` for null/invalid. */
|
|
6
|
+
export function formatDuration(ms) {
|
|
7
|
+
if (ms === null || ms === undefined || !Number.isFinite(ms) || ms < 0)
|
|
8
|
+
return '—';
|
|
9
|
+
if (ms < 1000)
|
|
10
|
+
return `${Math.round(ms)} ms`;
|
|
11
|
+
const s = Math.floor(ms / 1000);
|
|
12
|
+
if (s < 60)
|
|
13
|
+
return `${s}s`;
|
|
14
|
+
const m = Math.floor(s / 60);
|
|
15
|
+
if (m < 60)
|
|
16
|
+
return `${m}m ${String(s % 60).padStart(2, '0')}s`;
|
|
17
|
+
const h = Math.floor(m / 60);
|
|
18
|
+
if (h < 24)
|
|
19
|
+
return `${h}h ${String(m % 60).padStart(2, '0')}m`;
|
|
20
|
+
const d = Math.floor(h / 24);
|
|
21
|
+
return `${d}d ${String(h % 24).padStart(2, '0')}h`;
|
|
22
|
+
}
|
|
23
|
+
/** Byte size as `0 B`, `512 B`, `1.5 KiB`, `3.2 MiB`, `1.1 GiB`; `—` for null. */
|
|
24
|
+
export function formatBytes(n) {
|
|
25
|
+
if (n === null || n === undefined || !Number.isFinite(n) || n < 0)
|
|
26
|
+
return '—';
|
|
27
|
+
if (n < 1024)
|
|
28
|
+
return `${n} B`;
|
|
29
|
+
const units = ['KiB', 'MiB', 'GiB', 'TiB'];
|
|
30
|
+
let v = n / 1024;
|
|
31
|
+
let i = 0;
|
|
32
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
33
|
+
v /= 1024;
|
|
34
|
+
i++;
|
|
35
|
+
}
|
|
36
|
+
return `${v >= 100 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
|
|
37
|
+
}
|
|
38
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
39
|
+
/**
|
|
40
|
+
* ISO timestamp as `YYYY-MM-DD HH:MM:SS` in local time (or UTC with
|
|
41
|
+
* `utc: true`); `—` for null, the input itself if unparseable.
|
|
42
|
+
*/
|
|
43
|
+
export function formatDateTime(iso, opts = {}) {
|
|
44
|
+
if (!iso)
|
|
45
|
+
return '—';
|
|
46
|
+
const t = Date.parse(iso);
|
|
47
|
+
if (Number.isNaN(t))
|
|
48
|
+
return iso;
|
|
49
|
+
const d = new Date(t);
|
|
50
|
+
const [y, mo, da, h, mi, s] = opts.utc
|
|
51
|
+
? [d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()]
|
|
52
|
+
: [d.getFullYear(), d.getMonth() + 1, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds()];
|
|
53
|
+
return `${y}-${pad(mo)}-${pad(da)} ${pad(h)}:${pad(mi)}:${pad(s)}`;
|
|
54
|
+
}
|
|
55
|
+
/** Relative time (`just now`, `5s ago`, `3m ago`, `in 2h`, …) of `iso` against `nowMs`. */
|
|
56
|
+
export function formatRelative(iso, nowMs) {
|
|
57
|
+
if (!iso)
|
|
58
|
+
return '—';
|
|
59
|
+
const t = Date.parse(iso);
|
|
60
|
+
if (Number.isNaN(t))
|
|
61
|
+
return iso;
|
|
62
|
+
const diff = nowMs - t;
|
|
63
|
+
const abs = Math.abs(diff);
|
|
64
|
+
if (abs < 2000)
|
|
65
|
+
return 'just now';
|
|
66
|
+
const s = Math.round(abs / 1000);
|
|
67
|
+
const text = s < 60 ? `${s}s` : s < 3600 ? `${Math.round(s / 60)}m` : s < 86400 ? `${Math.round(s / 3600)}h` : `${Math.round(s / 86400)}d`;
|
|
68
|
+
return diff >= 0 ? `${text} ago` : `in ${text}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Only absolute `http:`/`https:` URLs are allowed as external link targets
|
|
72
|
+
* (Spec §20.5). Returns the normalized URL or `null`.
|
|
73
|
+
*/
|
|
74
|
+
export function safeHttpUrl(value) {
|
|
75
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 4096)
|
|
76
|
+
return null;
|
|
77
|
+
// Reject control characters and whitespace that browsers would strip before parsing the scheme.
|
|
78
|
+
if (/[\u0000- \u007f]/.test(value))
|
|
79
|
+
return null;
|
|
80
|
+
let u;
|
|
81
|
+
try {
|
|
82
|
+
u = new URL(value);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return u.protocol === 'http:' || u.protocol === 'https:' ? u.href : null;
|
|
88
|
+
}
|
|
89
|
+
/** Internal link target: a hash route like `#/runs/<id>`. */
|
|
90
|
+
export function isInternalHref(value) {
|
|
91
|
+
return /^#\/[^\s]*$/.test(value) && !/[\u0000-\u001f\u007f]/.test(value);
|
|
92
|
+
}
|
|
93
|
+
/** Shorten `s` to `max` characters with an ellipsis. */
|
|
94
|
+
export function truncate(s, max) {
|
|
95
|
+
if (s.length <= max)
|
|
96
|
+
return s;
|
|
97
|
+
return `${s.slice(0, Math.max(0, max - 1))}…`;
|
|
98
|
+
}
|
|
99
|
+
/** Pretty JSON for display, cut after `maxChars` (with a marker). */
|
|
100
|
+
export function prettyJson(value, maxChars = 200_000) {
|
|
101
|
+
let text;
|
|
102
|
+
try {
|
|
103
|
+
text = JSON.stringify(value, null, 2) ?? String(value);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
text = String(value);
|
|
107
|
+
}
|
|
108
|
+
if (text.length > maxChars)
|
|
109
|
+
return `${text.slice(0, maxChars)}\n… (${text.length - maxChars} more characters not shown)`;
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Unambiguous outcome badges of a run (Spec §21): business `failure` (result
|
|
114
|
+
* file) vs. technical error (`crashed`, `start_error`, `timed_out`) vs.
|
|
115
|
+
* `cancelled`; "lifecycle failed" is a separate badge.
|
|
116
|
+
*/
|
|
117
|
+
export function runBadges(run) {
|
|
118
|
+
const badges = [];
|
|
119
|
+
switch (run.outcome) {
|
|
120
|
+
case 'active':
|
|
121
|
+
badges.push(run.status === 'recovery_pending'
|
|
122
|
+
? { label: 'recovery pending', tone: 'warn', title: 'Recovery could not decide whether the process is gone; operator action required' }
|
|
123
|
+
: { label: run.status, tone: 'info', title: `Active run (technical state ${run.status})` });
|
|
124
|
+
break;
|
|
125
|
+
case 'success':
|
|
126
|
+
badges.push({ label: 'success', tone: 'ok', title: 'Completed; worker reported success' });
|
|
127
|
+
break;
|
|
128
|
+
case 'failure':
|
|
129
|
+
badges.push({
|
|
130
|
+
label: 'failure (business)',
|
|
131
|
+
tone: 'warn',
|
|
132
|
+
title: 'Business result "failure": the worker finished in a controlled way and reported that the task cannot be solved (does not count as technical failure)',
|
|
133
|
+
});
|
|
134
|
+
break;
|
|
135
|
+
case 'technical_error': {
|
|
136
|
+
const st = run.finalStatus ?? run.status;
|
|
137
|
+
badges.push({ label: `technical error: ${st}`, tone: 'error', title: `Technical error (${st}); counts towards retry blocks` });
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'cancelled':
|
|
141
|
+
badges.push({ label: 'cancelled', tone: 'muted', title: 'Cancelled (does not count as technical failure)' });
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
badges.push({ label: String(run.outcome), tone: 'muted', title: '' });
|
|
145
|
+
}
|
|
146
|
+
if (run.lifecycleFailed || run.lifecycleStatus === 'failed') {
|
|
147
|
+
badges.push({ label: 'lifecycle failed', tone: 'error', title: 'A lifecycle action failed; see Failed Actions' });
|
|
148
|
+
}
|
|
149
|
+
else if (run.lifecycleStatus === 'running' && run.outcome !== 'active') {
|
|
150
|
+
badges.push({ label: 'lifecycle running', tone: 'info', title: 'Lifecycle hook chain is running' });
|
|
151
|
+
}
|
|
152
|
+
return badges;
|
|
153
|
+
}
|
|
154
|
+
/** Display value of a check / filter trace value (dry-run never claims `pass` for unevaluated checks). */
|
|
155
|
+
export function checkValueLabel(value) {
|
|
156
|
+
if (value === true || value === 'pass')
|
|
157
|
+
return { label: 'pass', tone: 'ok' };
|
|
158
|
+
if (value === false || value === 'fail')
|
|
159
|
+
return { label: 'fail', tone: 'error' };
|
|
160
|
+
if (value === 'not_evaluated')
|
|
161
|
+
return { label: 'unknown / not evaluated', tone: 'muted' };
|
|
162
|
+
if (value === 'unknown')
|
|
163
|
+
return { label: 'unknown', tone: 'warn' };
|
|
164
|
+
return { label: String(value), tone: 'muted' };
|
|
165
|
+
}
|
|
166
|
+
/** Tone of a planning / dry-run decision. */
|
|
167
|
+
export function decisionTone(decision) {
|
|
168
|
+
if (decision === 'started' || decision === 'would_start')
|
|
169
|
+
return 'ok';
|
|
170
|
+
if (decision === 'pending_checks')
|
|
171
|
+
return 'info';
|
|
172
|
+
if (decision.endsWith('unknown') || decision === 'start_failed' || decision === 'blocked')
|
|
173
|
+
return 'warn';
|
|
174
|
+
return 'muted';
|
|
175
|
+
}
|
|
176
|
+
/** Tone of an action / lifecycle / notification state. */
|
|
177
|
+
export function stateTone(state) {
|
|
178
|
+
switch (state) {
|
|
179
|
+
case 'succeeded':
|
|
180
|
+
case 'delivered':
|
|
181
|
+
case 'ok':
|
|
182
|
+
return 'ok';
|
|
183
|
+
case 'failed':
|
|
184
|
+
return 'error';
|
|
185
|
+
case 'running':
|
|
186
|
+
case 'pending':
|
|
187
|
+
return 'info';
|
|
188
|
+
default:
|
|
189
|
+
return 'muted';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** One-line summary of a run event (all parts are plain text, rendered via textContent). */
|
|
193
|
+
export function eventSummary(e) {
|
|
194
|
+
const p = e.payload ?? {};
|
|
195
|
+
const str = (v) => (typeof v === 'string' ? v : null);
|
|
196
|
+
if (e.type === 'run.status') {
|
|
197
|
+
const from = str(p.from);
|
|
198
|
+
const to = str(p.to) ?? '?';
|
|
199
|
+
return `${from ? `${from} ` : ''}→ ${to}${str(p.target) ? ` (target ${str(p.target)})` : ''}`;
|
|
200
|
+
}
|
|
201
|
+
if (e.type === 'progress' && typeof p.value === 'number') {
|
|
202
|
+
return `${p.value}%${str(p.message) ? ` ${str(p.message)}` : ''}`;
|
|
203
|
+
}
|
|
204
|
+
if (e.type === 'lifecycle.action' || e.type === 'lifecycle.chain') {
|
|
205
|
+
const err = str(p.error);
|
|
206
|
+
return [str(p.hook), str(p.name) ?? str(p.type), str(p.status)].filter(Boolean).join(' ') + (err ? `: ${err}` : '');
|
|
207
|
+
}
|
|
208
|
+
const message = str(p.message) ?? str(p.reason) ?? str(p.error);
|
|
209
|
+
if (message !== null)
|
|
210
|
+
return message;
|
|
211
|
+
const keys = Object.keys(p).filter((k) => k !== 'type');
|
|
212
|
+
return keys.length === 0 ? '' : truncate(JSON.stringify(p), 200);
|
|
213
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paged log window of the run detail (Spec §16: only load the required
|
|
3
|
+
* ranges, never a whole 100 MB log). Pure state: which byte range
|
|
4
|
+
* `[start, end)` of the log file is loaded, and the loaded text. The text is
|
|
5
|
+
* capped at `maxChars`; when it grows beyond, the opposite side is trimmed at
|
|
6
|
+
* a line boundary and the range adjusted, so the DOM never holds more than
|
|
7
|
+
* about `maxChars` characters.
|
|
8
|
+
*/
|
|
9
|
+
/** Default page size (bytes) requested by the UI. */
|
|
10
|
+
export const LOG_PAGE_BYTES = 64 * 1024;
|
|
11
|
+
/** Default cap of characters kept in the log view. */
|
|
12
|
+
export const LOG_MAX_CHARS = 1024 * 1024;
|
|
13
|
+
const encoder = new TextEncoder();
|
|
14
|
+
/** UTF-8 byte length of `s`. */
|
|
15
|
+
export function utf8Length(s) {
|
|
16
|
+
return encoder.encode(s).length;
|
|
17
|
+
}
|
|
18
|
+
/** See module comment. */
|
|
19
|
+
export class LogWindow {
|
|
20
|
+
pageBytes;
|
|
21
|
+
maxChars;
|
|
22
|
+
/** First loaded byte offset. */
|
|
23
|
+
start = 0;
|
|
24
|
+
/** Byte offset after the last loaded byte. */
|
|
25
|
+
end = 0;
|
|
26
|
+
/** Last known file size. */
|
|
27
|
+
size = 0;
|
|
28
|
+
exists = false;
|
|
29
|
+
/** Number of times text was trimmed (for the UI hint). */
|
|
30
|
+
trimmed = false;
|
|
31
|
+
chunks = [];
|
|
32
|
+
chars = 0;
|
|
33
|
+
constructor(pageBytes = LOG_PAGE_BYTES, maxChars = LOG_MAX_CHARS) {
|
|
34
|
+
this.pageBytes = pageBytes;
|
|
35
|
+
this.maxChars = maxChars;
|
|
36
|
+
}
|
|
37
|
+
/** Loaded text. */
|
|
38
|
+
get text() {
|
|
39
|
+
if (this.chunks.length > 1)
|
|
40
|
+
this.chunks = [this.chunks.join('')];
|
|
41
|
+
return this.chunks[0] ?? '';
|
|
42
|
+
}
|
|
43
|
+
/** True if bytes before `start` exist. */
|
|
44
|
+
get hasEarlier() {
|
|
45
|
+
return this.start > 0;
|
|
46
|
+
}
|
|
47
|
+
/** True if the file has bytes after `end` (as of the last known size). */
|
|
48
|
+
get hasLater() {
|
|
49
|
+
return this.end < this.size;
|
|
50
|
+
}
|
|
51
|
+
/** Query for the initial view: the last page (`tail`). */
|
|
52
|
+
tailQuery() {
|
|
53
|
+
return { tail: this.pageBytes };
|
|
54
|
+
}
|
|
55
|
+
/** Query for the page before `start`, `null` if at the beginning. */
|
|
56
|
+
earlierQuery() {
|
|
57
|
+
if (this.start <= 0)
|
|
58
|
+
return null;
|
|
59
|
+
const offset = Math.max(0, this.start - this.pageBytes);
|
|
60
|
+
return { offset, limit: this.start - offset };
|
|
61
|
+
}
|
|
62
|
+
/** Query for the bytes after `end` (follow / load newer). */
|
|
63
|
+
laterQuery() {
|
|
64
|
+
return { offset: this.end, limit: this.pageBytes };
|
|
65
|
+
}
|
|
66
|
+
/** Replace the window with `page` (initial load / resync). */
|
|
67
|
+
reset(page) {
|
|
68
|
+
this.chunks = page.data ? [page.data] : [];
|
|
69
|
+
this.chars = page.data.length;
|
|
70
|
+
this.start = page.offset;
|
|
71
|
+
this.end = page.offset + page.length;
|
|
72
|
+
this.size = page.size;
|
|
73
|
+
this.exists = page.exists;
|
|
74
|
+
this.trimmed = false;
|
|
75
|
+
this.trimStart();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Prepend a page that ends at `start`. Returns false if the page does not
|
|
79
|
+
* fit (range moved meanwhile) – the caller should reload.
|
|
80
|
+
*/
|
|
81
|
+
prepend(page) {
|
|
82
|
+
if (page.offset + page.length !== this.start)
|
|
83
|
+
return false;
|
|
84
|
+
this.chunks = [page.data + this.text];
|
|
85
|
+
this.chars += page.data.length;
|
|
86
|
+
this.start = page.offset;
|
|
87
|
+
this.size = Math.max(this.size, page.size);
|
|
88
|
+
this.trimEnd();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Append a page that starts at `end`. Returns false if it does not fit.
|
|
93
|
+
* The beginning is trimmed if the window grows beyond `maxChars`.
|
|
94
|
+
*/
|
|
95
|
+
append(page) {
|
|
96
|
+
this.size = Math.max(this.size, page.size);
|
|
97
|
+
this.exists = this.exists || page.exists;
|
|
98
|
+
if (page.offset !== this.end)
|
|
99
|
+
return false;
|
|
100
|
+
if (page.length > 0) {
|
|
101
|
+
this.chunks.push(page.data);
|
|
102
|
+
this.chars += page.data.length;
|
|
103
|
+
this.end = page.offset + page.length;
|
|
104
|
+
}
|
|
105
|
+
this.trimStart();
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
trimStart() {
|
|
109
|
+
if (this.chars <= this.maxChars)
|
|
110
|
+
return;
|
|
111
|
+
const text = this.text;
|
|
112
|
+
let cut = text.length - this.maxChars;
|
|
113
|
+
const nl = text.indexOf('\n', cut);
|
|
114
|
+
if (nl >= 0 && nl - cut < 4096)
|
|
115
|
+
cut = nl + 1;
|
|
116
|
+
const dropped = text.slice(0, cut);
|
|
117
|
+
this.chunks = [text.slice(cut)];
|
|
118
|
+
this.chars = text.length - cut;
|
|
119
|
+
this.start += utf8Length(dropped);
|
|
120
|
+
this.trimmed = true;
|
|
121
|
+
}
|
|
122
|
+
trimEnd() {
|
|
123
|
+
if (this.chars <= this.maxChars)
|
|
124
|
+
return;
|
|
125
|
+
const text = this.text;
|
|
126
|
+
let keep = this.maxChars;
|
|
127
|
+
const nl = text.lastIndexOf('\n', keep);
|
|
128
|
+
if (nl > 0 && keep - nl < 4096)
|
|
129
|
+
keep = nl + 1;
|
|
130
|
+
const dropped = text.slice(keep);
|
|
131
|
+
this.chunks = [text.slice(0, keep)];
|
|
132
|
+
this.chars = keep;
|
|
133
|
+
this.end -= utf8Length(dropped);
|
|
134
|
+
this.trimmed = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual start form (Spec §5.5, §16): builds the request body of
|
|
3
|
+
* `POST /api/v1/workflows/:id/start` from form values and the workflow's
|
|
4
|
+
* parameter schema. The server validates again; this only gives early,
|
|
5
|
+
* readable feedback.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @param triggerType workflow trigger (`source` requires an item reference)
|
|
9
|
+
* @param item raw item input (URL, `#7`, `7`)
|
|
10
|
+
* @param defs parameter schema
|
|
11
|
+
* @param values raw form values (strings; booleans as `'true'|'false'|''`)
|
|
12
|
+
*/
|
|
13
|
+
export function buildStartBody(triggerType, item, defs, values) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const body = {};
|
|
16
|
+
const ref = item.trim();
|
|
17
|
+
if (triggerType === 'source') {
|
|
18
|
+
if (!ref)
|
|
19
|
+
errors.push({ field: 'item', message: 'Source item (URL or number) is required for source workflows' });
|
|
20
|
+
else
|
|
21
|
+
body.item = ref;
|
|
22
|
+
}
|
|
23
|
+
else if (ref) {
|
|
24
|
+
errors.push({ field: 'item', message: 'This workflow has no source; leave the item empty' });
|
|
25
|
+
}
|
|
26
|
+
const params = {};
|
|
27
|
+
for (const [name, def] of Object.entries(defs)) {
|
|
28
|
+
const raw = (values[name] ?? '').trim();
|
|
29
|
+
if (raw === '') {
|
|
30
|
+
if (def.required && (def.default === null || def.default === undefined))
|
|
31
|
+
errors.push({ field: name, message: 'Required parameter is missing' });
|
|
32
|
+
continue; // server applies the default
|
|
33
|
+
}
|
|
34
|
+
switch (def.type) {
|
|
35
|
+
case 'number': {
|
|
36
|
+
const n = Number(raw);
|
|
37
|
+
if (!Number.isFinite(n))
|
|
38
|
+
errors.push({ field: name, message: 'Expected a number' });
|
|
39
|
+
else
|
|
40
|
+
params[name] = n;
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case 'boolean':
|
|
44
|
+
if (raw !== 'true' && raw !== 'false')
|
|
45
|
+
errors.push({ field: name, message: 'Expected true or false' });
|
|
46
|
+
else
|
|
47
|
+
params[name] = raw === 'true';
|
|
48
|
+
break;
|
|
49
|
+
case 'enum':
|
|
50
|
+
if (!(def.values ?? []).includes(raw))
|
|
51
|
+
errors.push({ field: name, message: `Expected one of: ${(def.values ?? []).join(', ')}` });
|
|
52
|
+
else
|
|
53
|
+
params[name] = raw;
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
params[name] = values[name] ?? '';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (errors.length > 0)
|
|
60
|
+
return { ok: false, errors };
|
|
61
|
+
if (Object.keys(params).length > 0)
|
|
62
|
+
body.parameters = params;
|
|
63
|
+
return { ok: true, body };
|
|
64
|
+
}
|
|
65
|
+
/** Runtime override form → `PATCH /workflows/:id/runtime` body (`''` = keep YAML value → `null`). */
|
|
66
|
+
export function buildRuntimeBody(values) {
|
|
67
|
+
const errors = [];
|
|
68
|
+
let enabled = null;
|
|
69
|
+
if (values.enabled === 'true' || values.enabled === 'false')
|
|
70
|
+
enabled = values.enabled === 'true';
|
|
71
|
+
else if (values.enabled !== '')
|
|
72
|
+
errors.push({ field: 'enabled', message: 'Expected true, false or empty' });
|
|
73
|
+
let priority = null;
|
|
74
|
+
if (values.priority.trim() !== '') {
|
|
75
|
+
const n = Number(values.priority);
|
|
76
|
+
if (!Number.isFinite(n))
|
|
77
|
+
errors.push({ field: 'priority', message: 'Expected a number' });
|
|
78
|
+
else
|
|
79
|
+
priority = n;
|
|
80
|
+
}
|
|
81
|
+
let maxConcurrent = null;
|
|
82
|
+
if (values.maxConcurrent.trim() !== '') {
|
|
83
|
+
const n = Number(values.maxConcurrent);
|
|
84
|
+
if (!Number.isInteger(n) || n < 1)
|
|
85
|
+
errors.push({ field: 'maxConcurrent', message: 'Expected an integer ≥ 1' });
|
|
86
|
+
else
|
|
87
|
+
maxConcurrent = n;
|
|
88
|
+
}
|
|
89
|
+
return errors.length > 0 ? { ok: false, errors } : { ok: true, body: { enabled, priority, maxConcurrent } };
|
|
90
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash routing (`#/runs/<id>?tab=logs`): no server-side fallback route is
|
|
3
|
+
* needed, the static server only serves `/index.html`.
|
|
4
|
+
*/
|
|
5
|
+
/** Parse `location.hash`. Invalid percent-encoding yields the raw segment. */
|
|
6
|
+
export function parseHash(hash) {
|
|
7
|
+
let h = hash.startsWith('#') ? hash.slice(1) : hash;
|
|
8
|
+
if (h.startsWith('/'))
|
|
9
|
+
h = h.slice(1);
|
|
10
|
+
const q = h.indexOf('?');
|
|
11
|
+
const pathPart = q >= 0 ? h.slice(0, q) : h;
|
|
12
|
+
const queryPart = q >= 0 ? h.slice(q + 1) : '';
|
|
13
|
+
const decode = (s) => {
|
|
14
|
+
try {
|
|
15
|
+
return decodeURIComponent(s);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return s;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const segments = pathPart.split('/').filter((s) => s.length > 0).map(decode);
|
|
22
|
+
const query = {};
|
|
23
|
+
for (const [k, v] of new URLSearchParams(queryPart))
|
|
24
|
+
query[k] = v;
|
|
25
|
+
return { segments, query };
|
|
26
|
+
}
|
|
27
|
+
/** Build a hash (`#/a/b?x=1`); empty/undefined query values are omitted. */
|
|
28
|
+
export function buildHash(segments, query = {}) {
|
|
29
|
+
const path = segments.map((s) => encodeURIComponent(s)).join('/');
|
|
30
|
+
const params = new URLSearchParams();
|
|
31
|
+
for (const [k, v] of Object.entries(query))
|
|
32
|
+
if (v !== null && v !== undefined && v !== '')
|
|
33
|
+
params.set(k, String(v));
|
|
34
|
+
const qs = params.toString();
|
|
35
|
+
return `#/${path}${qs ? `?${qs}` : ''}`;
|
|
36
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side column sorting of views (Spec §17: sorting in the browser is
|
|
3
|
+
* allowed, no further filters). Unknown/empty values always sort last.
|
|
4
|
+
*/
|
|
5
|
+
function isEmpty(c) {
|
|
6
|
+
if (!c || !c.known)
|
|
7
|
+
return true;
|
|
8
|
+
const v = c.value;
|
|
9
|
+
return v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0);
|
|
10
|
+
}
|
|
11
|
+
const collator = typeof Intl !== 'undefined' ? new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) : null;
|
|
12
|
+
/** Comparable primitive of a value of column type `type`. */
|
|
13
|
+
function key(value, type) {
|
|
14
|
+
if (type === 'number' || typeof value === 'number') {
|
|
15
|
+
const n = typeof value === 'number' ? value : Number(value);
|
|
16
|
+
if (Number.isFinite(n))
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
if (type === 'date' || type === 'datetime') {
|
|
20
|
+
const t = Date.parse(String(value));
|
|
21
|
+
if (!Number.isNaN(t))
|
|
22
|
+
return t;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'boolean')
|
|
25
|
+
return value ? 1 : 0;
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.map((v) => String(v)).join(', ');
|
|
28
|
+
if (value !== null && typeof value === 'object')
|
|
29
|
+
return JSON.stringify(value);
|
|
30
|
+
return String(value);
|
|
31
|
+
}
|
|
32
|
+
/** Compare two cells (empty last regardless of direction). */
|
|
33
|
+
export function compareCells(a, b, type, direction) {
|
|
34
|
+
const ea = isEmpty(a);
|
|
35
|
+
const eb = isEmpty(b);
|
|
36
|
+
if (ea || eb)
|
|
37
|
+
return ea === eb ? 0 : ea ? 1 : -1;
|
|
38
|
+
const ka = key(a.value, type);
|
|
39
|
+
const kb = key(b.value, type);
|
|
40
|
+
let r;
|
|
41
|
+
if (typeof ka === 'number' && typeof kb === 'number')
|
|
42
|
+
r = ka - kb;
|
|
43
|
+
else
|
|
44
|
+
r = collator ? collator.compare(String(ka), String(kb)) : String(ka) < String(kb) ? -1 : String(ka) > String(kb) ? 1 : 0;
|
|
45
|
+
return direction === 'asc' ? r : -r;
|
|
46
|
+
}
|
|
47
|
+
/** Stable sort of rows by the cell at `column`. Returns a new array. */
|
|
48
|
+
export function sortRows(rows, column, type, direction) {
|
|
49
|
+
return rows
|
|
50
|
+
.map((row, index) => ({ row, index }))
|
|
51
|
+
.sort((x, y) => compareCells(x.row.cells[column], y.row.cells[column], type, direction) || x.index - y.index)
|
|
52
|
+
.map((x) => x.row);
|
|
53
|
+
}
|
|
54
|
+
/** Next sort state when a column header is clicked. */
|
|
55
|
+
export function nextSort(current, column) {
|
|
56
|
+
if (!current || current.column !== column)
|
|
57
|
+
return { column, direction: 'asc' };
|
|
58
|
+
return { column, direction: current.direction === 'asc' ? 'desc' : 'asc' };
|
|
59
|
+
}
|
|
60
|
+
/** Display text of a cell value (arrays joined, objects as JSON); `''` for empty. */
|
|
61
|
+
export function cellText(c) {
|
|
62
|
+
if (isEmpty(c))
|
|
63
|
+
return '';
|
|
64
|
+
const v = c.value;
|
|
65
|
+
if (Array.isArray(v))
|
|
66
|
+
return v.map((x) => (typeof x === 'object' && x !== null ? JSON.stringify(x) : String(x))).join(', ');
|
|
67
|
+
if (typeof v === 'object' && v !== null)
|
|
68
|
+
return JSON.stringify(v);
|
|
69
|
+
return String(v);
|
|
70
|
+
}
|