@parallelworks/ui 0.74.0 → 0.75.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/README.md +9 -1
- package/dist/ai/adapter/types.d.ts +5 -1
- package/dist/ai/components/agent/AgentMessageParts.js +41 -2
- package/dist/ai/core/parts.js +4 -0
- package/dist/ai/strings.d.ts +10 -0
- package/dist/ai/strings.js +9 -0
- package/dist/ai/types.d.ts +12 -0
- package/dist/components/Provider.d.ts +3 -0
- package/dist/components/Provider.js +2 -1
- package/dist/graph/DependencyGraph.d.ts +4 -0
- package/dist/graph/DependencyGraph.js +125 -3
- package/dist/graph/InputsEditor.js +1 -1
- package/dist/graph/JobSummary.d.ts +2 -0
- package/dist/graph/JobSummary.js +19 -5
- package/dist/graph/MatrixGroup.js +28 -12
- package/dist/graph/RunWarningsList.d.ts +19 -0
- package/dist/graph/RunWarningsList.js +94 -0
- package/dist/graph/TreeView.js +16 -5
- package/dist/graph/index.d.ts +1 -0
- package/dist/graph/runWarnings.d.ts +58 -0
- package/dist/graph/runWarnings.js +84 -0
- package/dist/graph/viewport.d.ts +19 -0
- package/dist/graph/viewport.js +62 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { cx } from "../cx.js";
|
|
2
|
+
import { WarningIcon } from "../icons.js";
|
|
3
|
+
import { toneIcon } from "../components/tone.js";
|
|
4
|
+
import { keyedByContent, withPositionKeys } from "../components/keys.js";
|
|
5
|
+
import { Tooltip } from "../components/Tooltip.js";
|
|
6
|
+
import { useStrings, useWorkflowEngine } from "../components/Provider.js";
|
|
7
|
+
import { Callout } from "../components/Callout.js";
|
|
8
|
+
import { jobLabel } from "./util.js";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
//#region src/graph/RunWarningsList.tsx
|
|
11
|
+
/** A warning's message, with the text it puts in backticks, such as a keyword, set as code. */
|
|
12
|
+
function WarningText({ message }) {
|
|
13
|
+
const parts = message.split("`");
|
|
14
|
+
if (parts.length < 3 || parts.length % 2 === 0) return message;
|
|
15
|
+
return withPositionKeys(parts).map(({ key, item }, i) => i % 2 === 1 ? /* @__PURE__ */ jsx("code", {
|
|
16
|
+
className: "rounded bg-current/10 px-1 font-mono text-[0.9em]",
|
|
17
|
+
children: item
|
|
18
|
+
}, key) : item);
|
|
19
|
+
}
|
|
20
|
+
/** Flags a job or step that warnings are about, or that holds something they're about; hovering
|
|
21
|
+
* it lists them. */
|
|
22
|
+
function WarningMark({ warnings, className }) {
|
|
23
|
+
const { dag } = useStrings();
|
|
24
|
+
if (warnings.length === 0) return null;
|
|
25
|
+
return /* @__PURE__ */ jsx(Tooltip, {
|
|
26
|
+
content: /* @__PURE__ */ jsx("ul", {
|
|
27
|
+
className: "m-0 flex list-none flex-col gap-1 p-0",
|
|
28
|
+
children: keyedByContent(warnings, (w) => w.message).map(({ key, item }) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(WarningText, { message: item.message }) }, key))
|
|
29
|
+
}),
|
|
30
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
31
|
+
role: "img",
|
|
32
|
+
"aria-label": dag.warningCount(warnings.length),
|
|
33
|
+
"data-warning-mark": "",
|
|
34
|
+
className: cx("inline-flex shrink-0", toneIcon.warning, className),
|
|
35
|
+
children: /* @__PURE__ */ jsx(WarningIcon, { className: "h-[1.1em] w-[1.1em]" })
|
|
36
|
+
})
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Where a warning is, as people read it, outermost first: each subworkflow step it's under, its job and
|
|
40
|
+
* its step. Jobs go by the workflow's names, which a matrix's runs share. */
|
|
41
|
+
function placeOf({ warning, targets: [target] }, rootJobs, stepLabel, stepNumber) {
|
|
42
|
+
const parts = [];
|
|
43
|
+
let jobs = rootJobs;
|
|
44
|
+
const path = warning.path ?? [];
|
|
45
|
+
for (let i = 0; i + 1 < path.length; i += 2) {
|
|
46
|
+
const via = target?.via[i / 2];
|
|
47
|
+
const step = via ? jobs?.[via.job]?.steps?.[via.step] : void 0;
|
|
48
|
+
const segment = path[i + 1] ?? "";
|
|
49
|
+
const index = /^step_(\d+)$/.exec(segment)?.[1];
|
|
50
|
+
parts.push(jobLabel(path[i] ?? ""), step && via ? stepLabel(step, via.step) : index === void 0 ? segment : stepNumber(Number(index)));
|
|
51
|
+
jobs = step?.subworkflow?.jobs;
|
|
52
|
+
}
|
|
53
|
+
parts.push(jobLabel(warning.job));
|
|
54
|
+
if (warning.step !== void 0 && warning.step !== null) {
|
|
55
|
+
const step = target ? jobs?.[target.job]?.steps?.[warning.step] : void 0;
|
|
56
|
+
parts.push(step ? stepLabel(step, warning.step) : stepNumber(warning.step));
|
|
57
|
+
}
|
|
58
|
+
return parts;
|
|
59
|
+
}
|
|
60
|
+
/** The run's warnings, each with a link to the job or step it's about. */
|
|
61
|
+
function RunWarningsList({ warnings, rootJobs, onShow }) {
|
|
62
|
+
const { dag } = useStrings();
|
|
63
|
+
const engine = useWorkflowEngine();
|
|
64
|
+
if (warnings.length === 0) return null;
|
|
65
|
+
return /* @__PURE__ */ jsx(Callout, {
|
|
66
|
+
tone: "warning",
|
|
67
|
+
title: dag.warningCount(warnings.length),
|
|
68
|
+
testId: "run-warnings",
|
|
69
|
+
children: /* @__PURE__ */ jsx("ul", {
|
|
70
|
+
className: "m-0 flex max-h-72 list-none flex-col gap-2.5 overflow-y-auto p-0",
|
|
71
|
+
children: keyedByContent(warnings, ({ warning }) => `${(warning.path ?? []).join("/")}/${warning.job}:${warning.step ?? ""}:${warning.message}`).map(({ key, item }) => {
|
|
72
|
+
const where = placeOf(item, rootJobs, (step, index) => engine.stepLabel(step, index), (index) => dag.stepNumber(index)).join(" › ");
|
|
73
|
+
const [target] = item.targets;
|
|
74
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
75
|
+
className: "flex flex-col items-start gap-0.5",
|
|
76
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
77
|
+
className: "min-w-0 break-words",
|
|
78
|
+
children: /* @__PURE__ */ jsx(WarningText, { message: item.warning.message })
|
|
79
|
+
}), target && onShow ? /* @__PURE__ */ jsx("button", {
|
|
80
|
+
type: "button",
|
|
81
|
+
onClick: () => onShow(target),
|
|
82
|
+
className: "cursor-pointer rounded-sm text-xs font-medium underline underline-offset-2 hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--theme-focus)",
|
|
83
|
+
children: where
|
|
84
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
85
|
+
className: "text-xs",
|
|
86
|
+
children: where
|
|
87
|
+
})]
|
|
88
|
+
}, key);
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { RunWarningsList, WarningMark, WarningText };
|
package/dist/graph/TreeView.js
CHANGED
|
@@ -9,6 +9,8 @@ import { Elapsed } from "../components/relativeTime.js";
|
|
|
9
9
|
import { RunStatusIcon } from "../components/RunStatusIcon.js";
|
|
10
10
|
import { LogViewer } from "../logviewer/LogViewer.js";
|
|
11
11
|
import { jobLabel, toggled } from "./util.js";
|
|
12
|
+
import { WarningMark } from "./RunWarningsList.js";
|
|
13
|
+
import { rowKey, useWarningMarks } from "./runWarnings.js";
|
|
12
14
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
15
|
import { useCallback, useId, useMemo, useState } from "react";
|
|
14
16
|
//#region src/graph/TreeView.tsx
|
|
@@ -52,6 +54,7 @@ function JobRow({ jobName, job, isExpanded, onToggle, prefix, state }) {
|
|
|
52
54
|
const dependencies = Array.isArray(job.needs) ? job.needs : [];
|
|
53
55
|
const panelId = useId();
|
|
54
56
|
const { dag: t } = useStrings();
|
|
57
|
+
const marks = useWarningMarks();
|
|
55
58
|
const skipped = job.status === "skipped" || job.status === "skipped-failed";
|
|
56
59
|
return /* @__PURE__ */ jsxs("div", {
|
|
57
60
|
className: "border-b border-[var(--theme-border)] last:border-b-0",
|
|
@@ -70,9 +73,12 @@ function JobRow({ jobName, job, isExpanded, onToggle, prefix, state }) {
|
|
|
70
73
|
/* @__PURE__ */ jsx(RunStatusIcon, { status: job.status }),
|
|
71
74
|
/* @__PURE__ */ jsxs("div", {
|
|
72
75
|
className: "flex flex-col gap-1 min-w-0 flex-1",
|
|
73
|
-
children: [/* @__PURE__ */
|
|
74
|
-
className: "
|
|
75
|
-
children:
|
|
76
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
77
|
+
className: "flex min-w-0 items-center gap-1.5",
|
|
78
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
79
|
+
className: "font-mono text-sm text-[var(--theme-app)] truncate",
|
|
80
|
+
children: jobLabel(jobName, job)
|
|
81
|
+
}), /* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName)) })]
|
|
76
82
|
}), dependencies.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
77
83
|
className: "flex items-center gap-1.5",
|
|
78
84
|
children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -120,6 +126,7 @@ function StepRow({ step, index, isExpanded, onToggle, prefix, jobName, state })
|
|
|
120
126
|
const panelId = useId();
|
|
121
127
|
const subworkflowPanelId = useId();
|
|
122
128
|
const { dag: t } = useStrings();
|
|
129
|
+
const marks = useWarningMarks();
|
|
123
130
|
const subworkflowJobs = subworkflow?.jobs ?? {};
|
|
124
131
|
const subworkflowJobNames = Object.keys(subworkflowJobs).filter((name) => subworkflowJobs[name]?.if !== false);
|
|
125
132
|
const subworkflowPrefix = `${prefix}subworkflows/${jobName}/step_${index}/`;
|
|
@@ -139,12 +146,14 @@ function StepRow({ step, index, isExpanded, onToggle, prefix, jobName, state })
|
|
|
139
146
|
className: "h-4 w-4 flex-shrink-0"
|
|
140
147
|
}),
|
|
141
148
|
/* @__PURE__ */ jsxs("span", {
|
|
142
|
-
className: "font-mono text-sm text-[var(--theme-app)] truncate
|
|
149
|
+
className: "font-mono text-sm text-[var(--theme-app)] truncate min-w-0",
|
|
143
150
|
children: [label, subworkflow && /* @__PURE__ */ jsx("span", {
|
|
144
151
|
className: "ml-2 text-xs text-[var(--theme-muted-text-color)]",
|
|
145
152
|
children: t.subworkflow
|
|
146
153
|
})]
|
|
147
154
|
}),
|
|
155
|
+
/* @__PURE__ */ jsx(WarningMark, { warnings: marks.under(rowKey(prefix, jobName, index)) }),
|
|
156
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
148
157
|
/* @__PURE__ */ jsx(Elapsed, {
|
|
149
158
|
start: step.startedAt,
|
|
150
159
|
end: step.completedAt,
|
|
@@ -189,6 +198,7 @@ function TreeView({ jobs, slug, expandedJobs, setExpandedJobs }) {
|
|
|
189
198
|
const engine = useWorkflowEngine();
|
|
190
199
|
const matrixPanelId = useId();
|
|
191
200
|
const { common, dag } = useStrings();
|
|
201
|
+
const marks = useWarningMarks();
|
|
192
202
|
const visibleJobNames = useMemo(() => {
|
|
193
203
|
return Object.keys(jobs).filter((jobName) => jobs[jobName]?.if !== false);
|
|
194
204
|
}, [jobs]);
|
|
@@ -293,7 +303,8 @@ function TreeView({ jobs, slug, expandedJobs, setExpandedJobs }) {
|
|
|
293
303
|
dag.jobCount(group.members.length),
|
|
294
304
|
")"
|
|
295
305
|
]
|
|
296
|
-
})
|
|
306
|
+
}),
|
|
307
|
+
!isGroupExpanded && /* @__PURE__ */ jsx(WarningMark, { warnings: marks.underAny(group.members.map((m) => rowKey("", m))) })
|
|
297
308
|
]
|
|
298
309
|
}), /* @__PURE__ */ jsx(DisclosurePanel, {
|
|
299
310
|
open: isGroupExpanded,
|
package/dist/graph/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { JOB_FIELDS, RETRY_FIELDS, RUNS_ON_FIELDS, SSH_KEYS, STEP_FIELDS, STRATE
|
|
|
6
6
|
export { inputTypes, offeredInputKeys } from './InputDialog.js';
|
|
7
7
|
export { InputsFormEditor } from './InputsEditor.js';
|
|
8
8
|
export { type ListedProblem, ProblemsButton } from './ProblemsButton.js';
|
|
9
|
+
export type { RunWarning } from './runWarnings.js';
|
|
9
10
|
export type { SettingsView } from './settingsViews.js';
|
|
10
11
|
export { type WorkflowActions, withFields } from './stepWith.js';
|
|
11
12
|
export type { RunLink } from './types.js';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { WorkflowJob } from './types.js';
|
|
2
|
+
/** Something probably wrong with a run's workflow, found when the run was created, about one of its jobs or steps. */
|
|
3
|
+
export interface RunWarning {
|
|
4
|
+
/** The steps that lead to the subworkflow the job is in, as `[job, 'step_<n>', …]`, outermost
|
|
5
|
+
* first; absent for the run's own workflow. */
|
|
6
|
+
path?: readonly string[] | null | undefined;
|
|
7
|
+
/** The job, by the name the workflow gives it, which every run of its matrix shares. */
|
|
8
|
+
job: string;
|
|
9
|
+
/** The step's index among the job's steps as the run lists them; absent for the job's own. */
|
|
10
|
+
step?: number | null | undefined;
|
|
11
|
+
/** What is wrong and what to write instead. Text in backticks shows as code. */
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
/** A subworkflow step on the way to a warning: its level's prefix, its job and its index. */
|
|
15
|
+
interface Via {
|
|
16
|
+
prefix: string;
|
|
17
|
+
job: string;
|
|
18
|
+
step: number;
|
|
19
|
+
}
|
|
20
|
+
/** One place in the graph a warning is about, by the keys the graph opens and finds rows by. */
|
|
21
|
+
export interface WarningTarget {
|
|
22
|
+
/** The subworkflow steps it sits under, outermost first, each of which opens to show it. */
|
|
23
|
+
via: Via[];
|
|
24
|
+
/** `subworkflows/<job>/step_<n>/` for each of them, as the graph prefixes the level it is on. */
|
|
25
|
+
prefix: string;
|
|
26
|
+
/** The job as the run lists it, such as one run of a matrix. */
|
|
27
|
+
job: string;
|
|
28
|
+
step?: number | undefined;
|
|
29
|
+
/** `prefix + job`, and `:step` for a step's. */
|
|
30
|
+
key: string;
|
|
31
|
+
}
|
|
32
|
+
/** A warning and where it is: one place, or one for each run of its job's matrix; none while the
|
|
33
|
+
* run lists no such job, such as before a subworkflow's jobs are known. */
|
|
34
|
+
export interface LocatedWarning {
|
|
35
|
+
warning: RunWarning;
|
|
36
|
+
targets: WarningTarget[];
|
|
37
|
+
}
|
|
38
|
+
/** The key a job's row, or with `step` one of its step's rows, is found and marked by. */
|
|
39
|
+
export declare const rowKey: (prefix: string, job: string, step?: number) => string;
|
|
40
|
+
/** Finds each warning's job or step among the jobs the run lists, through the subworkflows its path names. */
|
|
41
|
+
export declare function locateWarnings(warnings: readonly RunWarning[], rootJobs: Record<string, WorkflowJob>): LocatedWarning[];
|
|
42
|
+
/** Which warnings each job and step row shows a mark for: its own, and those of anything inside it. */
|
|
43
|
+
export declare class WarningMarks {
|
|
44
|
+
static readonly EMPTY: WarningMarks;
|
|
45
|
+
private readonly byKey;
|
|
46
|
+
/** Equal for two sets of marks that mark the same rows with the same warnings. */
|
|
47
|
+
readonly signature: string;
|
|
48
|
+
constructor(located: readonly LocatedWarning[]);
|
|
49
|
+
/** The warnings about the job or step at `key`, or about anything in it, such as its steps or a
|
|
50
|
+
* subworkflow it runs. */
|
|
51
|
+
under(key: string): RunWarning[];
|
|
52
|
+
/** The warnings under any of `keys`, each once, such as a matrix's runs. */
|
|
53
|
+
underAny(keys: readonly string[]): RunWarning[];
|
|
54
|
+
}
|
|
55
|
+
export declare const WarningMarksContext: import("react").Context<WarningMarks>;
|
|
56
|
+
/** The run's warning marks, which every job list and tree row in a run's graph reads. */
|
|
57
|
+
export declare const useWarningMarks: () => WarningMarks;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createContext, use } from "react";
|
|
2
|
+
//#region src/graph/runWarnings.ts
|
|
3
|
+
/** The key a job's row, or with `step` one of its step's rows, is found and marked by. */
|
|
4
|
+
var rowKey = (prefix, job, step) => step === void 0 ? `${prefix}${job}` : `${prefix}${job}:${step}`;
|
|
5
|
+
function jobsNamed(jobs, name) {
|
|
6
|
+
if (Object.hasOwn(jobs, name)) return [name];
|
|
7
|
+
return Object.keys(jobs).filter((key) => jobs[key]?._matrix?.originaljob === name);
|
|
8
|
+
}
|
|
9
|
+
var STEP_SEGMENT = /^step_(\d+)$/;
|
|
10
|
+
/** Finds each warning's job or step among the jobs the run lists, through the subworkflows its path names. */
|
|
11
|
+
function locateWarnings(warnings, rootJobs) {
|
|
12
|
+
return warnings.map((warning) => {
|
|
13
|
+
let levels = [{
|
|
14
|
+
prefix: "",
|
|
15
|
+
jobs: rootJobs,
|
|
16
|
+
via: []
|
|
17
|
+
}];
|
|
18
|
+
const path = warning.path ?? [];
|
|
19
|
+
for (let i = 0; i + 1 < path.length; i += 2) {
|
|
20
|
+
const index = Number(STEP_SEGMENT.exec(path[i + 1] ?? "")?.[1] ?? NaN);
|
|
21
|
+
levels = levels.flatMap((level) => Number.isNaN(index) ? [] : jobsNamed(level.jobs, path[i] ?? "").flatMap((job) => {
|
|
22
|
+
const jobs = level.jobs[job]?.steps?.[index]?.subworkflow?.jobs;
|
|
23
|
+
return jobs ? [{
|
|
24
|
+
prefix: `${level.prefix}subworkflows/${job}/step_${index}/`,
|
|
25
|
+
jobs,
|
|
26
|
+
via: [...level.via, {
|
|
27
|
+
prefix: level.prefix,
|
|
28
|
+
job,
|
|
29
|
+
step: index
|
|
30
|
+
}]
|
|
31
|
+
}] : [];
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
const step = warning.step ?? void 0;
|
|
35
|
+
return {
|
|
36
|
+
warning,
|
|
37
|
+
targets: levels.flatMap(({ prefix, jobs, via }) => jobsNamed(jobs, warning.job).map((job) => ({
|
|
38
|
+
via,
|
|
39
|
+
prefix,
|
|
40
|
+
job,
|
|
41
|
+
step,
|
|
42
|
+
key: rowKey(prefix, job, step)
|
|
43
|
+
})))
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Which warnings each job and step row shows a mark for: its own, and those of anything inside it. */
|
|
48
|
+
var WarningMarks = class WarningMarks {
|
|
49
|
+
static EMPTY = new WarningMarks([]);
|
|
50
|
+
byKey = /* @__PURE__ */ new Map();
|
|
51
|
+
/** Equal for two sets of marks that mark the same rows with the same warnings. */
|
|
52
|
+
signature;
|
|
53
|
+
constructor(located) {
|
|
54
|
+
for (const { warning, targets } of located) for (const target of targets) {
|
|
55
|
+
const keys = target.via.flatMap((v) => [rowKey(v.prefix, v.job), rowKey(v.prefix, v.job, v.step)]);
|
|
56
|
+
keys.push(rowKey(target.prefix, target.job));
|
|
57
|
+
if (target.step !== void 0) keys.push(target.key);
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
const marked = this.byKey.get(key);
|
|
60
|
+
if (!marked) this.byKey.set(key, [warning]);
|
|
61
|
+
else if (!marked.includes(warning)) marked.push(warning);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
this.signature = JSON.stringify([...this.byKey].map(([key, marked]) => [key, marked.map((w) => [
|
|
65
|
+
w.message,
|
|
66
|
+
w.job,
|
|
67
|
+
w.step
|
|
68
|
+
])]));
|
|
69
|
+
}
|
|
70
|
+
/** The warnings about the job or step at `key`, or about anything in it, such as its steps or a
|
|
71
|
+
* subworkflow it runs. */
|
|
72
|
+
under(key) {
|
|
73
|
+
return this.byKey.get(key) ?? [];
|
|
74
|
+
}
|
|
75
|
+
/** The warnings under any of `keys`, each once, such as a matrix's runs. */
|
|
76
|
+
underAny(keys) {
|
|
77
|
+
return [...new Set(keys.flatMap((key) => this.under(key)))];
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var WarningMarksContext = createContext(WarningMarks.EMPTY);
|
|
81
|
+
/** The run's warning marks, which every job list and tree row in a run's graph reads. */
|
|
82
|
+
var useWarningMarks = () => use(WarningMarksContext);
|
|
83
|
+
//#endregion
|
|
84
|
+
export { WarningMarks, WarningMarksContext, locateWarnings, rowKey, useWarningMarks };
|
package/dist/graph/viewport.d.ts
CHANGED
|
@@ -38,3 +38,22 @@ export declare function fitView(bounds: Bounds, panel: {
|
|
|
38
38
|
}): View | null;
|
|
39
39
|
/** The least shift that brings [start, end] inside [low, high]; none when it can't all fit, so the view stays. */
|
|
40
40
|
export declare function intoView(start: number, end: number, low: number, high: number): number;
|
|
41
|
+
/** A box on screen, as `getBoundingClientRect` gives it. */
|
|
42
|
+
export interface Rect {
|
|
43
|
+
left: number;
|
|
44
|
+
top: number;
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
}
|
|
48
|
+
/** The smallest box around all of `rects`, which must not be empty. */
|
|
49
|
+
export declare function enclosing(rects: readonly Rect[]): Rect;
|
|
50
|
+
/**
|
|
51
|
+
* The view that shows `region`, a job or step and what it is in, in `visible`, zoomed in to `readable`
|
|
52
|
+
* if it is further out: all of it in the middle where it fits, or else `target`, a part of it, with as
|
|
53
|
+
* much of the rest before it as fits. None while all of it already shows, readable. Boxes are on screen,
|
|
54
|
+
* and a view's x and y count from `origin`, where the pan and zoom place the graph.
|
|
55
|
+
*/
|
|
56
|
+
export declare function viewShowing(region: Rect, target: Rect, visible: Rect, origin: {
|
|
57
|
+
left: number;
|
|
58
|
+
top: number;
|
|
59
|
+
}, view: View, readable: number): View | null;
|
package/dist/graph/viewport.js
CHANGED
|
@@ -52,5 +52,66 @@ function intoView(start, end, low, high) {
|
|
|
52
52
|
if (start < low) return low - start;
|
|
53
53
|
return end > high ? high - end : 0;
|
|
54
54
|
}
|
|
55
|
+
/** The smallest box around all of `rects`, which must not be empty. */
|
|
56
|
+
function enclosing(rects) {
|
|
57
|
+
const left = Math.min(...rects.map((r) => r.left));
|
|
58
|
+
const top = Math.min(...rects.map((r) => r.top));
|
|
59
|
+
const right = Math.max(...rects.map((r) => r.left + r.width));
|
|
60
|
+
const bottom = Math.max(...rects.map((r) => r.top + r.height));
|
|
61
|
+
return {
|
|
62
|
+
left,
|
|
63
|
+
top,
|
|
64
|
+
width: right - left,
|
|
65
|
+
height: bottom - top
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
var within = (inner, outer, margin) => inner.left >= outer.left + margin && inner.top >= outer.top + margin && inner.left + inner.width <= outer.left + outer.width - margin && inner.top + inner.height <= outer.top + outer.height - margin;
|
|
69
|
+
function placeOnAxis(region, target, visible, margin) {
|
|
70
|
+
const low = visible.start + margin;
|
|
71
|
+
const high = visible.start + visible.size - margin;
|
|
72
|
+
if (region.size <= high - low) return visible.start + (visible.size - region.size) / 2;
|
|
73
|
+
const before = target.start - region.start;
|
|
74
|
+
if (target.size > high - low) return visible.start + (visible.size - target.size) / 2 - before;
|
|
75
|
+
return Math.min(low, high - target.size - before);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The view that shows `region`, a job or step and what it is in, in `visible`, zoomed in to `readable`
|
|
79
|
+
* if it is further out: all of it in the middle where it fits, or else `target`, a part of it, with as
|
|
80
|
+
* much of the rest before it as fits. None while all of it already shows, readable. Boxes are on screen,
|
|
81
|
+
* and a view's x and y count from `origin`, where the pan and zoom place the graph.
|
|
82
|
+
*/
|
|
83
|
+
function viewShowing(region, target, visible, origin, view, readable) {
|
|
84
|
+
const margin = fitMargin(visible);
|
|
85
|
+
if (view.scale >= readable && within(region, visible, margin)) return null;
|
|
86
|
+
const scale = Math.max(view.scale, readable);
|
|
87
|
+
const grow = scale / view.scale;
|
|
88
|
+
const x = (left) => (left - origin.left - view.x) / view.scale * scale;
|
|
89
|
+
const y = (top) => (top - origin.top - view.y) / view.scale * scale;
|
|
90
|
+
const left = placeOnAxis({
|
|
91
|
+
start: x(region.left),
|
|
92
|
+
size: region.width * grow
|
|
93
|
+
}, {
|
|
94
|
+
start: x(target.left),
|
|
95
|
+
size: target.width * grow
|
|
96
|
+
}, {
|
|
97
|
+
start: visible.left,
|
|
98
|
+
size: visible.width
|
|
99
|
+
}, margin);
|
|
100
|
+
const top = placeOnAxis({
|
|
101
|
+
start: y(region.top),
|
|
102
|
+
size: region.height * grow
|
|
103
|
+
}, {
|
|
104
|
+
start: y(target.top),
|
|
105
|
+
size: target.height * grow
|
|
106
|
+
}, {
|
|
107
|
+
start: visible.top,
|
|
108
|
+
size: visible.height
|
|
109
|
+
}, margin);
|
|
110
|
+
return {
|
|
111
|
+
x: left - origin.left - x(region.left),
|
|
112
|
+
y: top - origin.top - y(region.top),
|
|
113
|
+
scale
|
|
114
|
+
};
|
|
115
|
+
}
|
|
55
116
|
//#endregion
|
|
56
|
-
export { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, fitMargin, fitView, intoView, wheelFactor, zoomAround, zoomLimit };
|
|
117
|
+
export { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, enclosing, fitMargin, fitView, intoView, viewShowing, wheelFactor, zoomAround, zoomLimit };
|