@parallelworks/ui 0.57.0 → 0.59.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.
Files changed (60) hide show
  1. package/README.md +60 -4
  2. package/dist/access/PrincipalPicker.js +2 -1
  3. package/dist/ai/agent/diff.d.ts +1 -0
  4. package/dist/ai/agent/diff.js +50 -3
  5. package/dist/ai/ai.css +38 -0
  6. package/dist/ai/components/AttachmentUpload.d.ts +3 -0
  7. package/dist/ai/components/AttachmentUpload.js +51 -43
  8. package/dist/ai/components/agent/AgentMessageParts.js +33 -6
  9. package/dist/ai/strings.d.ts +2 -0
  10. package/dist/ai/strings.js +2 -1
  11. package/dist/components/AppLink.js +8 -3
  12. package/dist/components/Avatar.d.ts +5 -1
  13. package/dist/components/Avatar.js +13 -5
  14. package/dist/components/ExternalLink.d.ts +3 -2
  15. package/dist/components/ExternalLink.js +5 -2
  16. package/dist/components/Field.d.ts +7 -1
  17. package/dist/components/Field.js +83 -56
  18. package/dist/components/HoverCard.js +12 -0
  19. package/dist/components/Markdown.d.ts +3 -0
  20. package/dist/components/Markdown.js +2 -1
  21. package/dist/components/MarkdownImpl.d.ts +1 -1
  22. package/dist/components/MarkdownImpl.js +5 -3
  23. package/dist/components/Provider.d.ts +16 -1
  24. package/dist/components/Provider.js +17 -3
  25. package/dist/components/StickyBar.d.ts +14 -0
  26. package/dist/components/StickyBar.js +22 -0
  27. package/dist/components/Table.d.ts +7 -5
  28. package/dist/components/Table.js +6 -2
  29. package/dist/components/Tabs.d.ts +2 -2
  30. package/dist/components/Tabs.js +5 -2
  31. package/dist/components/category.d.ts +9 -0
  32. package/dist/components/category.js +30 -0
  33. package/dist/components/httpsImages.d.ts +12 -0
  34. package/dist/components/httpsImages.js +27 -0
  35. package/dist/components/stickyTop.d.ts +14 -0
  36. package/dist/components/stickyTop.js +93 -0
  37. package/dist/file-explorer/FileExplorer.js +21 -15
  38. package/dist/graph/DependencyGraph.d.ts +14 -1
  39. package/dist/graph/DependencyGraph.js +169 -77
  40. package/dist/graph/JobSummary.js +13 -3
  41. package/dist/graph/StepSummary.d.ts +4 -0
  42. package/dist/graph/StepSummary.js +31 -0
  43. package/dist/graph/index.d.ts +1 -0
  44. package/dist/graph/index.js +2 -1
  45. package/dist/graph/types.d.ts +3 -0
  46. package/dist/graph/util.d.ts +1 -1
  47. package/dist/icons.d.ts +1 -0
  48. package/dist/icons.js +3 -2
  49. package/dist/index.d.ts +2 -0
  50. package/dist/index.js +3 -1
  51. package/dist/list/ActionToolbar.d.ts +2 -3
  52. package/dist/list/ActionToolbar.js +3 -3
  53. package/dist/list/index.d.ts +1 -1
  54. package/dist/list/index.js +2 -2
  55. package/dist/logviewer/LogViewer.js +3 -3
  56. package/dist/styles/theme.css +27 -0
  57. package/dist/styles.css +1 -1
  58. package/dist/theme/derive.d.ts +2 -1
  59. package/dist/theme/derive.js +45 -1
  60. package/package.json +1 -1
@@ -0,0 +1,93 @@
1
+ import { useLayoutEffect } from "react";
2
+ //#region src/components/stickyTop.ts
3
+ /** Pins a bar to the top of its scroller, over what scrolls under it.
4
+ * `useStickyTop` moves it below whatever else is pinned there. */
5
+ var stickyClasses = "sticky top-0 z-(--layer-sticky) bg-(--theme-app-bg)";
6
+ var scrolls = (style) => /auto|scroll|hidden|overlay/.test(`${style.overflow} ${style.overflowX} ${style.overflowY}`);
7
+ /** The box a sticky element sticks to: its nearest ancestor that scrolls, or
8
+ * the document's when none does. */
9
+ function scrollContainer(el) {
10
+ const root = el.ownerDocument.documentElement;
11
+ for (let node = el.parentElement; node && node !== root; node = node.parentElement) if (scrolls(getComputedStyle(node))) return node;
12
+ return root;
13
+ }
14
+ /** Elements laid out as children of `parent`'s box: its children, with those
15
+ * of a `display: contents` child (such as a tab row's root) in its place. */
16
+ function boxChildren(nodes, into) {
17
+ for (const node of nodes) {
18
+ into.push(node);
19
+ if (getComputedStyle(node).display === "contents") boxChildren(node.children, into);
20
+ }
21
+ }
22
+ function chromeAbove(el, scroller) {
23
+ const above = {
24
+ chrome: [],
25
+ parents: [],
26
+ siblings: []
27
+ };
28
+ for (let node = el; node !== scroller && node.parentElement;) {
29
+ const parent = node.parentElement;
30
+ const earlier = [];
31
+ for (let sib = node.previousElementSibling; sib; sib = sib.previousElementSibling) earlier.push(sib);
32
+ const laidOut = [];
33
+ boxChildren(earlier, laidOut);
34
+ for (const sib of laidOut) {
35
+ above.siblings.push(sib);
36
+ const style = getComputedStyle(sib);
37
+ if (style.display === "contents") above.parents.push(sib);
38
+ if (style.position === "sticky" && style.top !== "auto" && sib instanceof HTMLElement) above.chrome.push(sib);
39
+ }
40
+ above.parents.push(parent);
41
+ node = parent;
42
+ }
43
+ return above;
44
+ }
45
+ /** Where `chrome`'s bottom edge sits, below the scroller's top, while it's pinned. */
46
+ function pinnedBottom(chrome) {
47
+ const height = chrome.getBoundingClientRect().height;
48
+ return height > 0 ? (Number.parseFloat(getComputedStyle(chrome).top) || 0) + height : 0;
49
+ }
50
+ /**
51
+ * Pins a `sticky` element just below whatever its scroller already pins at
52
+ * the top, such as a page header, a toolbar and a tab row, rather than at
53
+ * `top: 0` behind them. It finds them in the page, so neither they nor the
54
+ * host declare anything, and it follows them as they change height (a toolbar
55
+ * wrapping on a narrow screen), move, or come and go, before the browser
56
+ * paints. Chrome that uses it stacks: each piece pins under the ones before
57
+ * it, and its own `top` moving is a change the pieces after it follow.
58
+ * In a box that scrolls on its own, nothing sits above, so it pins at 0.
59
+ */
60
+ function useStickyTop(el, enabled) {
61
+ useLayoutEffect(() => {
62
+ if (!enabled || !el) return;
63
+ let chrome = [];
64
+ const place = () => {
65
+ const top = `${Math.max(0, ...chrome.map(pinnedBottom))}px`;
66
+ if (el.style.top !== top) el.style.top = top;
67
+ };
68
+ const resizes = new ResizeObserver(place);
69
+ const changes = new MutationObserver(gather);
70
+ function gather() {
71
+ if (!el) return;
72
+ const above = chromeAbove(el, scrollContainer(el));
73
+ chrome = above.chrome;
74
+ resizes.disconnect();
75
+ changes.disconnect();
76
+ for (const node of chrome) resizes.observe(node);
77
+ for (const parent of above.parents) changes.observe(parent, { childList: true });
78
+ for (const sib of above.siblings) changes.observe(sib, {
79
+ attributes: true,
80
+ attributeFilter: ["class", "style"]
81
+ });
82
+ place();
83
+ }
84
+ gather();
85
+ return () => {
86
+ resizes.disconnect();
87
+ changes.disconnect();
88
+ el.style.top = "";
89
+ };
90
+ }, [el, enabled]);
91
+ }
92
+ //#endregion
93
+ export { stickyClasses, useStickyTop };
@@ -1,6 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { buttonClasses } from "../components/buttonClasses.js";
3
- import { AccessIcon, ChevronDownIcon, ChevronRightIcon, CollapseIcon, DownloadIcon, FileIcon, FileTextIcon, FolderIcon, LinkIcon, PreviewIcon, ShareIcon, TrashIcon, UploadIcon } from "../icons.js";
3
+ import { AccessIcon, ChevronDownIcon, ChevronRightIcon, CollapseIcon, DownloadIcon, FileIcon, FileTextIcon, FolderIcon, FolderTreeIcon, LinkIcon, PreviewIcon, ShareIcon, TrashIcon, UploadIcon } from "../icons.js";
4
4
  import { MenuItem, MenuPopup, MenuSeparator } from "../components/Menu.js";
5
5
  import { safeUrl } from "../safeUrl.js";
6
6
  import { Loader } from "../components/Loader.js";
@@ -676,24 +676,30 @@ function FileExplorer({ storages, onRefresh, refreshKey, onOpenFile, showUserHie
676
676
  ...open,
677
677
  onClick: () => handleSelectTreeItem(node.path, node)
678
678
  }];
679
- const uriExtras = uris.map((value) => ({
680
- label: `${getUriScheme(value).toUpperCase()} URI`,
681
- value,
682
- icon: /* @__PURE__ */ jsx(LinkIcon, {})
683
- }));
684
- const pageUrl = absoluteUrl(href);
685
- if (pageUrl) uriExtras.unshift({
686
- label: t.preview.link,
687
- value: pageUrl,
688
- icon: /* @__PURE__ */ jsx(LinkIcon, {})
679
+ const copy = copySubmenu({
680
+ name: isFile ? node.name : void 0,
681
+ extra: [
682
+ {
683
+ label: t.preview.path,
684
+ value: node.root ? void 0 : getObjectKeyFromNode(node).replace(/\/$/, ""),
685
+ icon: /* @__PURE__ */ jsx(FolderTreeIcon, {})
686
+ },
687
+ {
688
+ label: t.preview.link,
689
+ value: absoluteUrl(href),
690
+ icon: /* @__PURE__ */ jsx(LinkIcon, {})
691
+ },
692
+ ...uris.map((value) => ({
693
+ label: `${getUriScheme(value).toUpperCase()} URI`,
694
+ value,
695
+ icon: /* @__PURE__ */ jsx(LinkIcon, {})
696
+ }))
697
+ ]
689
698
  });
690
699
  return assembleRowMenu({
691
700
  lead,
692
701
  actions,
693
- copy: uriExtras.length > 0 ? copySubmenu({
694
- name: isFile ? node.name : void 0,
695
- extra: uriExtras
696
- }) : void 0
702
+ copy: copy.items.length > 0 ? copy : void 0
697
703
  });
698
704
  };
699
705
  const openNodeContextMenu = (e, node) => {
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from 'react';
1
2
  import type { GraphLayout } from '../editing.js';
2
3
  import type { DependencyGraphEditor } from './editorApi.js';
3
4
  import { type RunLink, type WorkflowJob } from './types.js';
@@ -17,7 +18,7 @@ interface GraphRun {
17
18
  executedJobs?: Record<string, unknown> | undefined;
18
19
  links?: Record<string, RunLink> | undefined;
19
20
  }
20
- export default function DependencyGraph({ run, preview, removeBorder, viewMode, setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, }: {
21
+ export default function DependencyGraph({ run, preview, removeBorder, viewMode, setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, sidebarStickyTop, children, }: {
21
22
  run: GraphRun;
22
23
  preview?: boolean;
23
24
  removeBorder?: boolean | undefined;
@@ -30,6 +31,18 @@ export default function DependencyGraph({ run, preview, removeBorder, viewMode,
30
31
  /** The workflow as written, for what the editor's expressions can read. */
31
32
  workflow?: Record<string, unknown> | undefined;
32
33
  fixedHeight?: string | undefined;
34
+ /** A step to open; `prefix` is its `subworkflows/<job>/step_<n>/` chain when it runs inside a subworkflow. */
35
+ openStep?: {
36
+ job: string;
37
+ step: number;
38
+ prefix?: string | undefined;
39
+ } | undefined;
40
+ /** Called when the log dialog closes, so a host can forget the step it asked to open. */
41
+ onCloseLog?: (() => void) | undefined;
42
+ /** Pins the job list this many pixels below the top of the page's scroll area, under the host's own pinned bars. */
43
+ sidebarStickyTop?: number | undefined;
44
+ /** Shown under the annotations, above the graph. */
45
+ children?: ReactNode;
33
46
  }): import("react").JSX.Element | undefined;
34
47
  export declare function DependencyGraphPreview(inputs: {
35
48
  yml?: Record<string, unknown>;
@@ -1,5 +1,5 @@
1
1
  import { cx } from "../cx.js";
2
- import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon, FileTextIcon, ZoomInIcon, ZoomOutIcon } from "../icons.js";
2
+ import { ArrowLeftIcon, ArrowRightIcon, ChevronLeftIcon, ChevronRightIcon, CloseIcon, FileTextIcon, ZoomInIcon, ZoomOutIcon } from "../icons.js";
3
3
  import { Loader } from "../components/Loader.js";
4
4
  import { useRunFile, useSlots, useStrings, useWorkflowEngine } from "../components/Provider.js";
5
5
  import { Button } from "../components/Button.js";
@@ -10,6 +10,7 @@ import { DisclosurePanel } from "../components/Disclosure.js";
10
10
  import { formatDateTime } from "../format.js";
11
11
  import { Elapsed } from "../components/relativeTime.js";
12
12
  import { RunStatusIcon } from "../components/RunStatusIcon.js";
13
+ import { Tabs } from "../components/Tabs.js";
13
14
  import { LogViewer } from "../logviewer/LogViewer.js";
14
15
  import { initializeValues } from "../form/lib.js";
15
16
  import { AnnotationBanner } from "./AnnotationBanner.js";
@@ -27,6 +28,7 @@ import { routeConnectors, sameBoxes } from "./connectorGeometry.js";
27
28
  import { isJobRecord } from "./types.js";
28
29
  import { computeGraphLayout, displayJobsOf, hopDistances, positionsOf } from "./graphLayout.js";
29
30
  import { MatrixGroupNode, MatrixGroupSummaryItem } from "./MatrixGroup.js";
31
+ import { StepSummary } from "./StepSummary.js";
30
32
  import TreeView from "./TreeView.js";
31
33
  import { FIT_MAX_SCALE, MIN_SCALE, ZOOM_STEP, fitMargin, fitView, intoView, wheelFactor, zoomAround, zoomLimit } from "./viewport.js";
32
34
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -35,6 +37,12 @@ import { flushSync } from "react-dom";
35
37
  import { DateTime } from "luxon";
36
38
  import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
37
39
  //#region src/graph/DependencyGraph.tsx
40
+ var stepTabFiles = {
41
+ summary: "summary.md",
42
+ logs: "logs.out",
43
+ script: "script-unstable-redacted.sh"
44
+ };
45
+ var fileBeside = (path, file) => path.slice(0, path.lastIndexOf("/") + 1) + file;
38
46
  function ViewToggle({ viewMode, setViewMode }) {
39
47
  const { dag: t } = useStrings();
40
48
  return /* @__PURE__ */ jsxs("div", {
@@ -193,6 +201,11 @@ function offsetWithin(el, ancestor) {
193
201
  y
194
202
  };
195
203
  }
204
+ function unscrolledTop(el) {
205
+ let top = el.getBoundingClientRect().top;
206
+ for (let cur = el.parentElement; cur; cur = cur.parentElement) top += cur.scrollTop;
207
+ return top;
208
+ }
196
209
  function parsePrefixToPath(prefix) {
197
210
  const parts = prefix.split("/").filter(Boolean);
198
211
  const path = [];
@@ -683,7 +696,7 @@ function graphBounds(content) {
683
696
  }
684
697
  return bounds;
685
698
  }
686
- function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight }) {
699
+ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag", setViewMode, layout, editor, yamlJobs, inputs, workflow, fixedHeight, openStep, onCloseLog, sidebarStickyTop, children }) {
687
700
  const engine = useWorkflowEngine();
688
701
  const animT = .125;
689
702
  const rootJobs = isJobRecord(run.executedJobs) ? run.executedJobs : {};
@@ -724,7 +737,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
724
737
  const cur = containerRef.current;
725
738
  if (!cur) return;
726
739
  setTopOffset((prev) => {
727
- const next = cur.getBoundingClientRect().top;
740
+ const next = unscrolledTop(cur);
728
741
  return prev === next ? prev : next;
729
742
  });
730
743
  };
@@ -768,6 +781,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
768
781
  };
769
782
  }, [preview, fixedHeight]);
770
783
  const { common, dag: dagStrings } = useStrings();
784
+ const stepHeaderRef = useRef(null);
771
785
  const slots = useSlots();
772
786
  const [executedJobsPath, setExecutedJobsPath] = useState([[]]);
773
787
  const [executedJobsPathIdx, setExecutedJobsPathIdx] = useState(0);
@@ -827,7 +841,44 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
827
841
  setMatrixOpen((prev) => toggled(prev, key));
828
842
  requestAnimationFrame(invalidateConnectors);
829
843
  }, [invalidateConnectors]);
830
- const { data: sublogs, isLoading: sublogsLoading, error: sublogsError } = useRunFile(runSlug, sublogOpen || null, preview ? void 0 : { refreshInterval: 2e3 });
844
+ const nestingDepth = currentPath.length / 5;
845
+ const levelPrefixes = [];
846
+ for (let i = 0; i < nestingDepth; i++) levelPrefixes.push(`subworkflows/${currentPath[5 * i]}/step_${currentPath[5 * i + 2]}/`);
847
+ const pathAppend = levelPrefixes.join("");
848
+ let generalLogPath = "logs.out";
849
+ if (nestingDepth > 0) {
850
+ const lastJob = String(currentPath[5 * (nestingDepth - 1)] ?? "");
851
+ const lastStep = currentPath[5 * (nestingDepth - 1) + 2] ?? "";
852
+ generalLogPath = engine.stepLogPath(levelPrefixes.slice(0, -1).join(""), lastJob, lastStep);
853
+ }
854
+ const openJob = openStep?.job;
855
+ const openStepIndex = openStep?.step;
856
+ const openPrefix = openStep?.prefix ?? "";
857
+ useEffect(() => {
858
+ if (openJob !== void 0 && openStepIndex !== void 0) setSublogOpen(fileBeside(engine.stepLogPath(openPrefix, openJob, openStepIndex), "summary.md"));
859
+ }, [
860
+ engine,
861
+ openJob,
862
+ openStepIndex,
863
+ openPrefix
864
+ ]);
865
+ const [sublogJob = "", sublogStepDir] = sublogOpen.split("/").slice(-3, -1);
866
+ const sublogStep = sublogStepDir === void 0 ? void 0 : sublogStepDir.split("_")[1] ?? "";
867
+ const sublogIsStep = sublogStep !== void 0;
868
+ const sublogJobs = sublogOpen === generalLogPath ? jobs : walkJobPath(rootJobs, parsePrefixToPath(sublogOpen.split("/").slice(0, -4).join("/")));
869
+ const sublogStepData = sublogIsStep ? sublogJobs[sublogJob]?.steps[Number(sublogStep)] : void 0;
870
+ const stepTitle = sublogIsStep ? `${jobLabel(sublogJob, sublogJobs[sublogJob])} › ${engine.stepLabel(sublogStepData ?? {}, Number(sublogStep))}` : "";
871
+ const sublogIsSubworkflow = !!sublogStepData?.subworkflow;
872
+ const sublogDir = sublogOpen.slice(0, sublogOpen.lastIndexOf("/") + 1);
873
+ const sublogFile = sublogOpen.slice(sublogDir.length);
874
+ const sublogHasSummary = !preview && !!sublogStepData?.summary;
875
+ const sublogTab = sublogFile === "summary.md" && sublogHasSummary ? "summary" : sublogFile === "logs.out" || sublogFile === "summary.md" ? "logs" : "script";
876
+ const stepTabs = !sublogIsStep || preview || sublogIsSubworkflow ? [] : sublogHasSummary ? [
877
+ "summary",
878
+ "logs",
879
+ "script"
880
+ ] : ["logs", "script"];
881
+ const { data: sublogs, isLoading: sublogsLoading, error: sublogsError } = useRunFile(runSlug, sublogOpen === "" || sublogTab === "summary" ? null : sublogFile === "summary.md" ? `${sublogDir}logs.out` : sublogOpen, preview ? void 0 : { refreshInterval: 2e3 });
831
882
  useEffect(() => {
832
883
  if (sublogOpen.endsWith("/script-unstable-redacted.sh") && !sublogsLoading && (sublogsError || !sublogs)) setSublogOpen(sublogOpen.replace(/script-unstable-redacted\.sh$/, "script-unstable.sh"));
833
884
  }, [
@@ -836,14 +887,6 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
836
887
  sublogsError,
837
888
  sublogsLoading
838
889
  ]);
839
- const [sublogJob = "", sublogStepDir] = sublogOpen.split("/").slice(-3, -1);
840
- const sublogStep = sublogStepDir === void 0 ? void 0 : sublogStepDir.split("_")[1] ?? "";
841
- const sublogIsStep = sublogStep !== void 0;
842
- const sublogStepData = sublogIsStep ? jobs[sublogJob]?.steps[Number(sublogStep)] : void 0;
843
- const logName = sublogIsStep ? `Job: ${jobLabel(sublogJob)} \nStep: ${sublogStepData?.name || `Step ${sublogStep}`}` : "";
844
- const sublogIsSubworkflow = !!sublogStepData?.subworkflow;
845
- const sublogFileStart = sublogOpen.lastIndexOf("/") + 1;
846
- const sublogShowsLog = sublogOpen.slice(sublogFileStart) === "logs.out";
847
890
  const rootGraph = useMemo(() => computeGraphLayout(engine, jobs, engine.matrixGroups(jobs), placed), [
848
891
  engine,
849
892
  jobs,
@@ -1022,16 +1065,6 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1022
1065
  windowSize,
1023
1066
  executedJobsPathIdx
1024
1067
  ]);
1025
- const nestingDepth = currentPath.length / 5;
1026
- const levelPrefixes = [];
1027
- for (let i = 0; i < nestingDepth; i++) levelPrefixes.push(`subworkflows/${currentPath[5 * i]}/step_${currentPath[5 * i + 2]}/`);
1028
- const pathAppend = levelPrefixes.join("");
1029
- let generalLogPath = "logs.out";
1030
- if (nestingDepth > 0) {
1031
- const lastJob = String(currentPath[5 * (nestingDepth - 1)] ?? "");
1032
- const lastStep = currentPath[5 * (nestingDepth - 1) + 2] ?? "";
1033
- generalLogPath = engine.stepLogPath(levelPrefixes.slice(0, -1).join(""), lastJob, lastStep);
1034
- }
1035
1068
  const missingLogText = sublogIsSubworkflow ? "Subworkflows generally have no logs/scripts at this level.\nClick on the \"Open in new graph\" icon on the subworkflow step to open the subworkflow graph." : sublogsError ? dagStrings.logLoadFailed : (sublogOpen === generalLogPath ? dagStrings.statusReason(run.statusReason) : void 0) ?? dagStrings.noLogFound;
1036
1069
  const currentSublog = sublogOpen === "" ? "" : preview ? engine.toYaml(sublogStepData) : sublogsError || !sublogs ? missingLogText : sublogs;
1037
1070
  const expansionKeys = useMemo(() => engine.expansionKeys(jobs, pathAppend), [
@@ -1087,6 +1120,8 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1087
1120
  setSublogOpen(`${sublogSplit.slice(0, -3).join("/")}/${job}/step_${step}/${sublogSplit.slice(-1)[0]}`);
1088
1121
  }, [jobs, sublogOpen]);
1089
1122
  const handleKeyDown = useCallback((e) => {
1123
+ const target = e.target instanceof Element ? e.target : null;
1124
+ if (e.defaultPrevented || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || target?.closest("input, textarea, select, [contenteditable=\"\"], [contenteditable=\"true\"], [role=\"tablist\"], [role=\"listbox\"], [role=\"menu\"], [role=\"slider\"], [role=\"radiogroup\"]")) return;
1090
1125
  if (e.key === "ArrowRight") nextStep(false);
1091
1126
  else if (e.key === "ArrowLeft") nextStep(true);
1092
1127
  }, [nextStep]);
@@ -1129,40 +1164,99 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1129
1164
  })
1130
1165
  }) : void 0;
1131
1166
  const startDt = run.createdAt ? DateTime.fromISO(run.createdAt) : shownAt;
1132
- const additionalBottomLeftBarComponents = sublogIsStep ? /* @__PURE__ */ jsxs(Fragment, { children: [
1133
- /* @__PURE__ */ jsx(IconButton, {
1134
- variant: "outline",
1167
+ const jobNames = Object.keys(jobs);
1168
+ const stepCount = jobNames.reduce((n, name) => n + (jobs[name]?.steps.length ?? 0), 0);
1169
+ const stepPosition = jobNames.slice(0, Math.max(0, jobNames.indexOf(sublogJob))).reduce((n, name) => n + (jobs[name]?.steps.length ?? 0), 0) + Number(sublogStep ?? 0) + 1;
1170
+ const stepPager = sublogIsStep ? /* @__PURE__ */ jsxs("div", {
1171
+ className: "flex shrink-0 items-center",
1172
+ children: [/* @__PURE__ */ jsx(IconButton, {
1135
1173
  icon: /* @__PURE__ */ jsx(ArrowLeftIcon, {}),
1136
1174
  label: dagStrings.previousStep,
1137
1175
  onClick: () => nextStep(true)
1138
- }),
1139
- /* @__PURE__ */ jsx(IconButton, {
1140
- variant: "outline",
1176
+ }), /* @__PURE__ */ jsx(IconButton, {
1141
1177
  icon: /* @__PURE__ */ jsx(ArrowRightIcon, {}),
1142
1178
  label: dagStrings.nextStep,
1143
1179
  onClick: () => nextStep(false)
1144
- }),
1145
- /* @__PURE__ */ jsx("span", {
1146
- className: "pt-0.5 whitespace-pre-wrap text-xs",
1147
- children: logName
1148
- })
1149
- ] }) : void 0;
1150
- const additionalBottomRightBarComponents = (!preview || sublogIsSubworkflow) && sublogIsStep ? /* @__PURE__ */ jsx(Button, {
1151
- variant: "secondary",
1152
- size: "sm",
1153
- onClick: () => {
1154
- if (sublogIsSubworkflow) {
1155
- openInNewGraph(pathAppend, sublogJob, parseInt(sublogStep ?? "", 10));
1156
- setSublogOpen("");
1157
- } else setSublogOpen(sublogOpen.slice(0, sublogFileStart) + (sublogShowsLog ? "script-unstable-redacted.sh" : "logs.out"));
1158
- },
1159
- children: sublogIsSubworkflow ? "Open Subworkflow" : sublogShowsLog ? "Show Script" : "Show Logs"
1180
+ })]
1181
+ }) : void 0;
1182
+ const stepCounter = sublogIsStep && sublogJobs === jobs ? dagStrings.stepOf(stepPosition, stepCount) : void 0;
1183
+ const stepTabLabels = {
1184
+ summary: dagStrings.summaryTab,
1185
+ logs: dagStrings.logsTab,
1186
+ script: dagStrings.scriptTab
1187
+ };
1188
+ const closeLog = () => {
1189
+ setSublogOpen("");
1190
+ onCloseLog?.();
1191
+ };
1192
+ const narrowDialog = windowSize.w < 640;
1193
+ const stepHeader = sublogIsStep ? /* @__PURE__ */ jsxs("div", {
1194
+ ref: stepHeaderRef,
1195
+ className: "grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-x-2 gap-y-2 border-b border-(--theme-border) px-2 py-2 sm:flex sm:gap-x-3",
1196
+ children: [
1197
+ stepPager,
1198
+ /* @__PURE__ */ jsxs("div", {
1199
+ className: "flex min-w-0 items-baseline gap-x-2.5 sm:flex-1",
1200
+ children: [/* @__PURE__ */ jsx("h2", {
1201
+ className: "min-w-0 truncate font-semibold",
1202
+ title: stepTitle,
1203
+ children: stepTitle
1204
+ }), stepCounter && /* @__PURE__ */ jsx("span", {
1205
+ className: "hidden shrink-0 text-xs tabular-nums theme-muted-text sm:inline",
1206
+ children: stepCounter
1207
+ })]
1208
+ }),
1209
+ sublogIsSubworkflow && /* @__PURE__ */ jsx(Button, {
1210
+ className: narrowDialog ? "col-span-3 row-start-2 justify-self-start" : void 0,
1211
+ variant: "secondary",
1212
+ size: "sm",
1213
+ onClick: () => {
1214
+ openInNewGraph(pathAppend, sublogJob, parseInt(sublogStep ?? "", 10));
1215
+ setSublogOpen("");
1216
+ },
1217
+ children: dagStrings.openSubworkflow
1218
+ }),
1219
+ stepTabs.length > 0 && /* @__PURE__ */ jsx(Tabs, {
1220
+ className: narrowDialog ? "col-span-3 row-start-2 [&>[role=tablist]]:flex-1" : void 0,
1221
+ label: stepTitle,
1222
+ variant: "segmented",
1223
+ fill: narrowDialog,
1224
+ items: stepTabs.map((tab) => ({
1225
+ value: tab,
1226
+ label: stepTabLabels[tab]
1227
+ })),
1228
+ value: sublogTab,
1229
+ onValueChange: (tab) => setSublogOpen(sublogDir + stepTabFiles[tab])
1230
+ }),
1231
+ /* @__PURE__ */ jsx(IconButton, {
1232
+ className: "col-start-3 row-start-1",
1233
+ icon: /* @__PURE__ */ jsx(CloseIcon, {}),
1234
+ label: common.close,
1235
+ onClick: closeLog
1236
+ })
1237
+ ]
1160
1238
  }) : void 0;
1239
+ const sublogLoading = sublogsLoading && (!sublogs || !runNumber);
1240
+ const sublogLoader = /* @__PURE__ */ jsx(Loader, {
1241
+ label: dagStrings.loadingLogs,
1242
+ size: 70
1243
+ });
1244
+ const stepLogViewer = /* @__PURE__ */ jsx(LogViewer, {
1245
+ log: currentSublog.slice(-1e5),
1246
+ height: sublogIsStep ? "100%" : windowSize.h / 1.2,
1247
+ width: sublogIsStep ? "100%" : windowSize.w / 1.2,
1248
+ hideExpand: true,
1249
+ enableWorkflowCommands: true
1250
+ });
1161
1251
  const graph = /* @__PURE__ */ jsxs("div", {
1162
1252
  className: "flex flex-col lg:flex-row w-full",
1163
1253
  children: [
1164
1254
  !preview && /* @__PURE__ */ jsxs("div", {
1165
- className: "hidden lg:block lg:w-[280px] flex-shrink-0 lg:mr-1",
1255
+ className: cx("hidden lg:block lg:w-[280px] flex-shrink-0 lg:mr-1", sidebarStickyTop !== void 0 && "lg:sticky lg:self-start lg:overflow-y-auto"),
1256
+ style: sidebarStickyTop !== void 0 ? {
1257
+ top: sidebarStickyTop,
1258
+ maxHeight: `calc(100dvh - ${sidebarStickyTop + 24}px)`
1259
+ } : void 0,
1166
1260
  children: [
1167
1261
  slots.runSessions && /* @__PURE__ */ jsx("div", {
1168
1262
  className: "mb-4 overflow-y-auto",
@@ -1267,6 +1361,7 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1267
1361
  ]
1268
1362
  }),
1269
1363
  !preview && /* @__PURE__ */ jsx(AnnotationBanner, { executedJobs: jobs }),
1364
+ children,
1270
1365
  viewMode === "tree" && !preview ? /* @__PURE__ */ jsx("div", {
1271
1366
  className: "panel w-full border",
1272
1367
  children: /* @__PURE__ */ jsx(TreeView, {
@@ -1354,45 +1449,42 @@ function DependencyGraph({ run, preview = false, removeBorder, viewMode = "dag",
1354
1449
  }),
1355
1450
  /* @__PURE__ */ jsx(Modal, {
1356
1451
  open: sublogOpen !== "",
1357
- onClose: () => setSublogOpen(""),
1358
- "aria-label": logName.replace(/\s+/g, " ") || sublogOpen.slice(sublogFileStart),
1452
+ onClose: closeLog,
1453
+ "aria-label": stepTitle || sublogFile,
1454
+ initialFocus: () => stepHeaderRef.current?.querySelector("[role=\"tab\"][aria-selected=\"true\"]") ?? true,
1359
1455
  align: "center",
1360
- className: "max-h-full overflow-y-auto p-4 sm:p-6",
1361
- children: /* @__PURE__ */ jsx("div", {
1362
- className: "flex w-full",
1363
- children: sublogsLoading && (!sublogs || !runNumber) ? /* @__PURE__ */ jsx(Loader, {
1364
- label: dagStrings.loadingLogs,
1365
- size: 70
1366
- }) : preview ? /* @__PURE__ */ jsxs("div", { children: [slots.yamlViewer?.({
1456
+ className: sublogIsStep ? "max-h-full overflow-hidden bg-(--theme-panel-bg) p-0" : "max-h-full overflow-y-auto p-4 sm:p-6",
1457
+ children: sublogIsStep ? /* @__PURE__ */ jsxs("div", {
1458
+ className: "flex flex-col",
1459
+ style: preview ? { width: window.innerWidth / 1.4 } : {
1460
+ width: windowSize.w / 1.2,
1461
+ height: windowSize.h / 1.2
1462
+ },
1463
+ children: [stepHeader, preview ? sublogLoading ? /* @__PURE__ */ jsx("div", {
1464
+ className: "flex justify-center py-16",
1465
+ children: sublogLoader
1466
+ }) : slots.yamlViewer?.({
1367
1467
  value: currentSublog,
1368
1468
  height: window.innerHeight / 1.4,
1369
1469
  width: window.innerWidth / 1.4,
1370
1470
  contentKey: `editor_${sublogOpen}`
1371
1471
  }) ?? /* @__PURE__ */ jsx("pre", {
1372
- className: "h-full overflow-auto text-xs p-4",
1373
- style: {
1374
- height: window.innerHeight / 1.4,
1375
- width: window.innerWidth / 1.4
1376
- },
1472
+ className: "overflow-auto p-4 text-xs",
1473
+ style: { height: window.innerHeight / 1.4 },
1377
1474
  children: currentSublog
1378
- }), /* @__PURE__ */ jsxs("div", {
1379
- className: "flex theme-panel select-none items-center gap-x-2 p-2",
1380
- children: [/* @__PURE__ */ jsx("div", {
1381
- className: "w-full flex flex-row gap-x-2 items-center",
1382
- children: /* @__PURE__ */ jsx("div", {
1383
- className: "flex gap-x-2 w-full",
1384
- children: additionalBottomLeftBarComponents
1385
- })
1386
- }), additionalBottomRightBarComponents]
1387
- })] }) : /* @__PURE__ */ jsx(LogViewer, {
1388
- log: currentSublog.slice(-1e5),
1389
- height: windowSize.h / 1.2,
1390
- width: windowSize.w / 1.2,
1391
- hideExpand: true,
1392
- additionalBottomLeftBarComponents,
1393
- additionalBottomRightBarComponents,
1394
- enableWorkflowCommands: true
1395
- })
1475
+ }) : /* @__PURE__ */ jsx("div", {
1476
+ className: "flex min-h-0 flex-1 flex-col",
1477
+ children: sublogTab === "summary" ? /* @__PURE__ */ jsx(StepSummary, {
1478
+ runSlug,
1479
+ path: `${sublogDir}summary.md`
1480
+ }) : sublogLoading ? /* @__PURE__ */ jsx("div", {
1481
+ className: "flex flex-1 items-center justify-center",
1482
+ children: sublogLoader
1483
+ }) : stepLogViewer
1484
+ })]
1485
+ }) : /* @__PURE__ */ jsx("div", {
1486
+ className: "flex w-full",
1487
+ children: sublogLoading ? sublogLoader : stepLogViewer
1396
1488
  })
1397
1489
  })
1398
1490
  ]
@@ -1,5 +1,5 @@
1
1
  import { cx } from "../cx.js";
2
- import { FileTextIcon, OpenInNewGraphIcon } from "../icons.js";
2
+ import { FileTextIcon, OpenInNewGraphIcon, SummaryIcon } from "../icons.js";
3
3
  import { Tooltip } from "../components/Tooltip.js";
4
4
  import { useNavigation, useSlots, useStrings, useWorkflowEngine } from "../components/Provider.js";
5
5
  import { RunStatusIcon } from "../components/RunStatusIcon.js";
@@ -30,7 +30,7 @@ function UsesIcon({ uses, subworkflow }) {
30
30
  });
31
31
  }
32
32
  function Joblist(inputs) {
33
- const { jobActions } = useStrings();
33
+ const { dag, jobActions } = useStrings();
34
34
  const engine = useWorkflowEngine();
35
35
  return inputs.jobNames.map((jobName) => {
36
36
  if (inputs.jobs[jobName]?._matrixGroup) return null;
@@ -68,7 +68,17 @@ function Joblist(inputs) {
68
68
  onClick: () => inputs.onStepClick(jobName, i),
69
69
  className: cx(inputs.preview ? "ml-1.5" : "ml-4", "gap-x-1.5 flex items-center text-left"),
70
70
  "data-testid": stepLabel,
71
- children: [inputs.preview ? "↳" : /* @__PURE__ */ jsx(RunStatusIcon, { status: step.status }), /* @__PURE__ */ jsx("div", { children: stepLabel })]
71
+ children: [
72
+ inputs.preview ? "↳" : /* @__PURE__ */ jsx(RunStatusIcon, { status: step.status }),
73
+ /* @__PURE__ */ jsx("div", { children: stepLabel }),
74
+ step.summary && !inputs.preview && /* @__PURE__ */ jsx(Tooltip, {
75
+ content: dag.hasSummary,
76
+ children: /* @__PURE__ */ jsx(SummaryIcon, {
77
+ "aria-label": dag.hasSummary,
78
+ className: "w-[1.1em] h-[1.1em] shrink-0 text-(--theme-muted-text-color)"
79
+ })
80
+ })
81
+ ]
72
82
  });
73
83
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
74
84
  className: cx("flex items-center", step.status === "skipped" || step.status === "skipped-failed" ? "cursor-default" : "cursor-pointer"),
@@ -0,0 +1,4 @@
1
+ export declare function StepSummary({ runSlug, path }: {
2
+ runSlug: string;
3
+ path: string;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,31 @@
1
+ import { Loader } from "../components/Loader.js";
2
+ import { useRunFile, useStrings } from "../components/Provider.js";
3
+ import Markdown from "../components/Markdown.js";
4
+ import { jsx } from "react/jsx-runtime";
5
+ //#region src/graph/StepSummary.tsx
6
+ function StepSummary({ runSlug, path }) {
7
+ const { dag } = useStrings();
8
+ const { data, isLoading, error } = useRunFile(runSlug, path);
9
+ return /* @__PURE__ */ jsx("div", {
10
+ className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-5 py-5 sm:px-8 sm:py-7",
11
+ children: isLoading && data === void 0 ? /* @__PURE__ */ jsx("div", {
12
+ className: "flex h-full items-center justify-center",
13
+ children: /* @__PURE__ */ jsx(Loader, {
14
+ label: dag.loadingSummary,
15
+ size: 70
16
+ })
17
+ }) : error || data === void 0 ? /* @__PURE__ */ jsx("p", {
18
+ className: "text-xs theme-muted-text",
19
+ children: dag.summaryLoadFailed
20
+ }) : /* @__PURE__ */ jsx("div", {
21
+ className: "max-w-[72ch]",
22
+ children: /* @__PURE__ */ jsx(Markdown, {
23
+ httpsImagesOnly: true,
24
+ compact: true,
25
+ children: data
26
+ })
27
+ })
28
+ });
29
+ }
30
+ //#endregion
31
+ export { StepSummary };
@@ -9,4 +9,5 @@ export { type ListedProblem, ProblemsButton } from './ProblemsButton.js';
9
9
  export type { SettingsView } from './settingsViews.js';
10
10
  export { type WorkflowActions, withFields } from './stepWith.js';
11
11
  export type { RunLink } from './types.js';
12
+ export { jobLabel } from './util.js';
12
13
  export { INPUT_FORM_FIELDS, LINK_FIELDS, NEEDS_FIELDS, SESSION_FIELDS, WORKFLOW_FIELDS, WorkflowSettingsDialog, } from './WorkflowSettingsDialog.js';
@@ -1,4 +1,5 @@
1
1
  import { trackDrag } from "./editorPrimitives.js";
2
+ import { jobLabel } from "./util.js";
2
3
  import DependencyGraph, { DependencyGraphPreview } from "./DependencyGraph.js";
3
4
  import { GRAPH_EDITOR_STRINGS, INPUTS_EDITOR_STRINGS } from "./editorStrings.js";
4
5
  import { inputTypes, offeredInputKeys } from "./InputDialog.js";
@@ -7,4 +8,4 @@ import { JOB_FIELDS, RETRY_FIELDS, RUNS_ON_FIELDS, SSH_KEYS, STEP_FIELDS, STRATE
7
8
  import { ProblemsButton } from "./ProblemsButton.js";
8
9
  import { InputsFormEditor } from "./InputsEditor.js";
9
10
  import { INPUT_FORM_FIELDS, LINK_FIELDS, NEEDS_FIELDS, SESSION_FIELDS, WORKFLOW_FIELDS, WorkflowSettingsDialog } from "./WorkflowSettingsDialog.js";
10
- export { DependencyGraph, DependencyGraphPreview, GRAPH_EDITOR_STRINGS, INPUTS_EDITOR_STRINGS, INPUT_FORM_FIELDS, InputsFormEditor, JOB_FIELDS, LINK_FIELDS, NEEDS_FIELDS, ProblemsButton, RETRY_FIELDS, RUNS_ON_FIELDS, SESSION_FIELDS, SSH_KEYS, STEP_FIELDS, STRATEGY_FIELDS, WORKFLOW_FIELDS, WorkflowSettingsDialog, inputTypes, offeredInputKeys, trackDrag, withFields };
11
+ export { DependencyGraph, DependencyGraphPreview, GRAPH_EDITOR_STRINGS, INPUTS_EDITOR_STRINGS, INPUT_FORM_FIELDS, InputsFormEditor, JOB_FIELDS, LINK_FIELDS, NEEDS_FIELDS, ProblemsButton, RETRY_FIELDS, RUNS_ON_FIELDS, SESSION_FIELDS, SSH_KEYS, STEP_FIELDS, STRATEGY_FIELDS, WORKFLOW_FIELDS, WorkflowSettingsDialog, inputTypes, jobLabel, offeredInputKeys, trackDrag, withFields };
@@ -30,6 +30,9 @@ export interface WorkflowStep {
30
30
  startedAt?: string;
31
31
  completedAt?: string;
32
32
  annotations?: WorkflowAnnotation[];
33
+ summary?: {
34
+ bytes: number;
35
+ };
33
36
  subworkflow?: WorkflowSubworkflow | undefined;
34
37
  }
35
38
  interface WorkflowMatrixMeta {