lhcb-ntuple-wizard 2.0.4 → 2.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.
Files changed (34) hide show
  1. package/dist/App.js +2 -1
  2. package/dist/components/DecayCard.d.ts +1 -0
  3. package/dist/components/{DttNameInput.js → DecayCard.js} +4 -4
  4. package/dist/components/DecayLatex.js +1 -1
  5. package/dist/components/DecayTreeCard.js +7 -10
  6. package/dist/components/DecayTreeGraph.d.ts +1 -2
  7. package/dist/components/DecayTreeGraph.js +132 -46
  8. package/dist/components/DeleteButton.d.ts +2 -1
  9. package/dist/components/DeleteButton.js +2 -2
  10. package/dist/components/NtupleWizard.js +6 -1
  11. package/dist/components/RequestButtonGroup.js +2 -2
  12. package/dist/components/RequestRow.js +5 -4
  13. package/dist/components/StrippingLineBadge.js +1 -1
  14. package/dist/components/{TupleToolDropdown.d.ts → TupleToolClassDropdown.d.ts} +2 -5
  15. package/dist/components/{TupleToolDropdown.js → TupleToolClassDropdown.js} +10 -4
  16. package/dist/components/TupleToolGroup.d.ts +1 -2
  17. package/dist/components/TupleToolGroup.js +5 -5
  18. package/dist/components/TupleToolList.d.ts +1 -2
  19. package/dist/components/TupleToolList.js +11 -7
  20. package/dist/components/VerticalLine.d.ts +7 -0
  21. package/dist/components/VerticalLine.js +4 -0
  22. package/dist/components/modals/AddTupleToolModal.d.ts +3 -4
  23. package/dist/components/modals/AddTupleToolModal.js +12 -12
  24. package/dist/components/modals/ConfigureTupleToolModal.d.ts +2 -2
  25. package/dist/components/modals/ConfigureTupleToolModal.js +28 -11
  26. package/dist/config.d.ts +18 -47
  27. package/dist/config.js +26 -39
  28. package/dist/pages/DecayTreeConfigPage.js +13 -13
  29. package/dist/pages/RequestPage.js +10 -13
  30. package/dist/providers/DttProvider.d.ts +2 -0
  31. package/dist/providers/DttProvider.js +4 -1
  32. package/dist/utils/utils.js +36 -3
  33. package/package.json +10 -5
  34. package/dist/components/DttNameInput.d.ts +0 -1
package/dist/App.js CHANGED
@@ -15,6 +15,7 @@ import { Archive, CodeSlash, House } from "react-bootstrap-icons";
15
15
  import { config } from "./config";
16
16
  import { NtupleWizard } from "./components/NtupleWizard";
17
17
  import { BrowserRouter } from "react-router-dom";
18
+ import { Slide, ToastContainer } from "react-toastify";
18
19
  export function App() {
19
- return (_jsxs(_Fragment, { children: [_jsx(Navbar, { collapseOnSelect: true, expand: "lg", bg: "dark", variant: "dark", children: _jsxs(Container, { children: [_jsxs(Navbar.Brand, { href: "/", children: [_jsx("img", { src: "/logo.svg", height: "30", className: "d-inline-block align-top", alt: "LHCb logo" }), " LHCb NTuple Wizard"] }), _jsx(Navbar.Toggle, { "aria-controls": "responsive-navbar-nav" }), _jsx(Navbar.Collapse, { className: "justify-content-end", children: _jsxs(Nav, { children: [_jsxs(NavDropdown, { title: "About", id: "about-dropdown", menuVariant: "dark", children: [_jsxs(NavDropdown.Item, { href: "https://lhcb-dpa.web.cern.ch/lhcb-dpa/wp6/ntupling-wizard.html", target: "_blank", children: [_jsx(House, {}), " Project home"] }), _jsxs(NavDropdown.Item, { href: "https://gitlab.cern.ch/lhcb-dpa/wp6-analysis-preservation-and-open-data/lhcb-ntuple-wizard-frontend", target: "_blank", children: [_jsx(CodeSlash, {}), " Source code"] }), _jsxs(NavDropdown.Item, { href: config.metadata_baseurl, target: "_blank", children: [_jsx(Archive, {}), " Metadata files"] })] }), _jsx(Nav.Link, { href: "https://opendata.cern.ch/", target: "_blank", children: _jsx("img", { src: "/open_data_portal.png", height: "30", alt: "CERN Open Data Portal" }) })] }) })] }) }), _jsx(Container, { className: "mt-4", children: _jsx(BrowserRouter, { children: _jsx(NtupleWizard, {}) }) })] }));
20
+ return (_jsxs(_Fragment, { children: [_jsx(Navbar, { collapseOnSelect: true, expand: "lg", bg: "dark", variant: "dark", children: _jsxs(Container, { children: [_jsxs(Navbar.Brand, { href: "/", children: [_jsx("img", { src: "/logo.svg", height: "30", className: "d-inline-block align-top", alt: "LHCb logo" }), " LHCb NTuple Wizard"] }), _jsx(Navbar.Toggle, { "aria-controls": "responsive-navbar-nav" }), _jsx(Navbar.Collapse, { className: "justify-content-end", children: _jsxs(Nav, { children: [_jsxs(NavDropdown, { title: "About", id: "about-dropdown", menuVariant: "dark", children: [_jsxs(NavDropdown.Item, { href: "https://lhcb-dpa.web.cern.ch/lhcb-dpa/wp6/ntupling-wizard.html", target: "_blank", children: [_jsx(House, {}), " Project home"] }), _jsxs(NavDropdown.Item, { href: "https://gitlab.cern.ch/lhcb-dpa/wp6-analysis-preservation-and-open-data/lhcb-ntuple-wizard-frontend", target: "_blank", children: [_jsx(CodeSlash, {}), " Source code"] }), _jsxs(NavDropdown.Item, { href: config.metadata_baseurl, target: "_blank", children: [_jsx(Archive, {}), " Metadata files"] })] }), _jsx(Nav.Link, { href: "https://opendata.cern.ch/", target: "_blank", children: _jsx("img", { src: "/open_data_portal.png", height: "30", alt: "CERN Open Data Portal" }) })] }) })] }) }), _jsx(Container, { className: "mt-4", children: _jsxs(BrowserRouter, { children: [_jsx(ToastContainer, { transition: Slide, hideProgressBar: true }), _jsx(NtupleWizard, {})] }) })] }));
20
21
  }
@@ -0,0 +1 @@
1
+ export declare function DecayCard(): import("react/jsx-runtime").JSX.Element | null;
@@ -14,9 +14,9 @@ import { UploadDttConfigModal } from "./modals/UploadDttConfigModal";
14
14
  import { VARIABLES_PATH } from "../constants";
15
15
  import { useRow } from "../providers/RowProvider";
16
16
  import { useWizardConfig } from "../providers/WizardConfigProvider";
17
- export function DttNameInput() {
17
+ export function DecayCard() {
18
18
  const { row } = useRow();
19
- const { rows, updateRow } = useRows();
19
+ const { rows, setRows, updateRow } = useRows();
20
20
  const navigate = useNavigate();
21
21
  const metadata = useMetadata();
22
22
  const { basePath, variant } = useWizardConfig();
@@ -50,7 +50,7 @@ export function DttNameInput() {
50
50
  setAutoFocus(true);
51
51
  };
52
52
  const handleConfigureDtt = () => {
53
- updateRow(row.id, (r) => ({ ...r, editTree: true }));
53
+ setRows((rows) => rows.map((r) => (r.id === row.id ? { ...r, editTree: true } : { ...r, editTree: false })));
54
54
  void navigate(`${basePath}${VARIABLES_PATH}`);
55
55
  };
56
56
  const handleDeleteDtt = () => {
@@ -71,5 +71,5 @@ export function DttNameInput() {
71
71
  ? "Name contains invalid characters"
72
72
  : "Name is already taken" })] })), _jsx(ButtonGroup, { children: row.dtt ? (_jsxs(_Fragment, { children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Configure this DecayTreeTuple" }), children: _jsx(Button, { type: "submit", variant: "secondary", onClick: handleConfigureDtt, disabled: !dttNameValid, className: "align-items-center d-flex", children: _jsx(GearWideConnected, {}) }) }), variant === "standalone" && (_jsx(_Fragment, { children: _jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Download DecayTreeTuple YAML configuration file" }), children: _jsx(Button, { className: "align-items-center d-flex ms-auto", type: "button", onClick: () => {
73
73
  downloadText(YamlFile.fromDtt(row.dtt));
74
- }, disabled: !dttNameValid, children: _jsx(Download, {}) }) }) })), _jsx(DeleteButton, { action: handleDeleteDtt })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "d-flex align-items-center gap-2", children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Please add a DecayTreeTuple in order to complete the production configuration" }), children: _jsx(ExclamationCircle, { width: 20, height: 20, className: "text-danger me-2" }) }), _jsxs(ButtonGroup, { children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Add a DecayTreeTuple" }), children: _jsx(Button, { type: "submit", variant: "success", onClick: handleCreateDTT, disabled: !metadata, children: _jsx(PlusLg, {}) }) }), variant === "standalone" && (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Upload DecayTreeTuple configuration file" }), children: _jsx(Button, { className: "ms-auto", type: "button", onClick: () => setShowUploadModal(true), children: _jsx(Upload, {}) }) }))] })] }), showUploadModal && (_jsx(UploadDttConfigModal, { currentRow: row, onClose: () => setShowUploadModal(false) }))] })) })] }), _jsx(Card.Body, { children: _jsx(DecayLatex, { decay: row.decay }) })] }));
74
+ }, disabled: !dttNameValid, children: _jsx(Download, {}) }) }) })), _jsx(DeleteButton, { action: handleDeleteDtt, popupMessage: "Delete DecayTreeTuple" })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "d-flex align-items-center gap-2", children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Please add a DecayTreeTuple in order to complete the production configuration" }), children: _jsx(ExclamationCircle, { width: 20, height: 20, className: "text-danger me-2" }) }), _jsxs(ButtonGroup, { children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Add a DecayTreeTuple" }), children: _jsx(Button, { type: "submit", variant: "success", onClick: handleCreateDTT, disabled: !metadata, children: _jsx(PlusLg, {}) }) }), variant === "standalone" && (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Upload DecayTreeTuple configuration file" }), children: _jsx(Button, { className: "ms-auto", type: "button", onClick: () => setShowUploadModal(true), children: _jsx(Upload, {}) }) }))] })] }), showUploadModal && (_jsx(UploadDttConfigModal, { currentRow: row, onClose: () => setShowUploadModal(false) }))] })) })] }), _jsx(Card.Body, { style: { overflowX: "auto" }, children: _jsx(DecayLatex, { decay: row.decay }) })] }));
75
75
  }
@@ -95,7 +95,7 @@ export function DecayLatex({ decay, selection }) {
95
95
  let latex = metadata.particleProperties[cleanName].latex;
96
96
  // Underline marked particles
97
97
  if (isMarked) {
98
- latex = LATEX_COMMANDS.COLOR("RoyalBlue", latex);
98
+ latex = LATEX_COMMANDS.COLOR("#4169E1", latex);
99
99
  }
100
100
  return `${openBrace}${latex}${closeBrace}`;
101
101
  };
@@ -13,30 +13,27 @@ import { DecayTagBadge } from "./DecayTagBadge.js";
13
13
  import { DecayLatex } from "./DecayLatex";
14
14
  import { TupleToolList } from "./TupleToolList";
15
15
  import { useMetadata } from "../providers/MetadataProvider.js";
16
- import { Button, Card, OverlayTrigger, Stack, Tooltip } from "react-bootstrap";
17
- import { Lock, QuestionCircle, Search, Unlock } from "react-bootstrap-icons";
18
- import { config } from "../config.js";
19
- import { useEffect, useState } from "react";
16
+ import { Card, OverlayTrigger, Stack, Tooltip } from "react-bootstrap";
17
+ import { QuestionCircle } from "react-bootstrap-icons";
18
+ import { useEffect } from "react";
20
19
  import { DecayTreeGraph } from "./DecayTreeGraph";
21
20
  import { DecayTreeCardHeader } from "./DecayTreeCardHeader";
22
21
  import { ParticleTagFilters } from "./ParticleTagFilters";
23
22
  import { useDtt } from "../providers/DttProvider";
24
23
  export function DecayTreeCard({ onConfigSaved, onDirtyUpdated }) {
25
24
  const metadata = useMetadata();
26
- const { dtt, decay, dirty } = useDtt();
27
- const [selectedBranch, setSelectedBranch] = useState([]);
28
- const [lockGraphZoom, setLockGraphZoom] = useState(!config.dttGraphOptions.interaction.zoomView);
25
+ const { dtt, decay, dirty, selectedBranch, setSelectedBranch } = useDtt();
29
26
  useEffect(() => {
30
27
  onDirtyUpdated(dtt.getName(), dirty);
31
28
  }, [dtt, dirty]);
32
29
  if (!metadata) {
33
30
  return null;
34
31
  }
35
- return (_jsxs(Card, { children: [_jsx(DecayTreeCardHeader, { dttName: dtt.getName(), inputs: dtt.config.inputs }), _jsxs(Card.Body, { children: [_jsxs(Card.Title, { className: "d-flex justify-content-between align-items-start", children: [_jsxs("span", { children: [_jsx(OverlayTrigger, { placement: "bottom", overlay: _jsx(Tooltip, { children: "Click to select particles to configure. Hold ctrl to select multiple. Clear the selection to configure the entire decay." }), children: _jsx(QuestionCircle, {}) }), " ", "Configure ", _jsx(DecayLatex, { decay: decay })] }), _jsx(OverlayTrigger, { placement: "bottom", overlay: _jsxs(Tooltip, { children: ["Click to ", lockGraphZoom ? "unlock" : "lock", " scroll-to-zoom."] }), children: _jsxs(Button, { onClick: () => setLockGraphZoom(!lockGraphZoom), variant: "secondary-outline", children: [lockGraphZoom ? _jsx(Lock, {}) : _jsx(Unlock, {}), _jsx(Search, {})] }) })] }), _jsx(Stack, { direction: "horizontal", style: { flexWrap: "wrap", justifyContent: "center" }, gap: 2, children: decay.tags
32
+ return (_jsxs(Card, { children: [_jsx(DecayTreeCardHeader, { dttName: dtt.getName(), inputs: dtt.config.inputs }), _jsxs(Card.Body, { children: [_jsx(Card.Title, { className: "d-flex justify-content-between align-items-start", children: _jsxs("span", { children: [_jsx(OverlayTrigger, { placement: "bottom", overlay: _jsx(Tooltip, { children: "Click to select particles to configure. Hold ctrl to select multiple. Clear the selection to configure the entire decay." }), children: _jsx(QuestionCircle, {}) }), " ", "Configure ", _jsx(DecayLatex, { decay: decay })] }) }), _jsx(Stack, { direction: "horizontal", style: { flexWrap: "wrap", justifyContent: "center" }, gap: 2, children: decay.tags
36
33
  .filter((tag) => metadata.userHints.decayTags[tag].warn)
37
- .map((tag) => (_jsx(DecayTagBadge, { tag: tag }, tag))) }), _jsx(DecayTreeGraph, { decay: decay, lockGraphZoom: lockGraphZoom, selectedParticlesIds: selectedBranch, onNodeSelectionChanged: (nodeIds) => {
34
+ .map((tag) => (_jsx(DecayTagBadge, { tag: tag }, tag))) }), _jsx(DecayTreeGraph, { decay: decay, selectedParticlesIds: selectedBranch, onNodeSelectionChanged: (nodeIds) => {
38
35
  setSelectedBranch([...nodeIds].sort());
39
- } }), _jsx(ParticleTagFilters, { branch: selectedBranch, onSelectTag: setSelectedBranch }), _jsxs(Card.Title, { className: "mt-4", children: ["Current selection: ", _jsx(DecayLatex, { decay: decay, selection: selectedBranch })] }), _jsx(TupleToolList, { branch: selectedBranch, onGroupSelected: (group) => {
36
+ } }), _jsx(ParticleTagFilters, { branch: selectedBranch, onSelectTag: setSelectedBranch }), _jsxs(Card.Title, { className: "mt-4", children: ["Current selection: ", _jsx(DecayLatex, { decay: decay, selection: selectedBranch })] }), _jsx(TupleToolList, { onGroupSelected: (group) => {
40
37
  const selection = group.split(",");
41
38
  setSelectedBranch(selection);
42
39
  }, onSave: () => onConfigSaved(dtt.config) })] })] }));
@@ -1,9 +1,8 @@
1
1
  import { DecayData } from "../models/decayData";
2
2
  interface Props {
3
3
  decay: DecayData;
4
- lockGraphZoom: boolean;
5
4
  selectedParticlesIds: string[];
6
5
  onNodeSelectionChanged: (nodeIds: string[]) => void;
7
6
  }
8
- export declare function DecayTreeGraph({ decay, lockGraphZoom, selectedParticlesIds, onNodeSelectionChanged }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export declare function DecayTreeGraph({ decay, selectedParticlesIds, onNodeSelectionChanged }: Props): import("react/jsx-runtime").JSX.Element;
9
8
  export {};
@@ -1,52 +1,66 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- // @ts-expect-error The react-graph-vis package does not have types
3
- import Graph from "react-graph-vis";
2
+ import CytoscapeComponent from "react-cytoscapejs";
3
+ import cytoscape from "cytoscape";
4
+ import dagre from "cytoscape-dagre";
4
5
  import { config } from "../config";
5
6
  import { tex2svg } from "../utils/mathjaxUtils";
6
7
  import { useMetadata } from "../providers/MetadataProvider";
7
- import { useEffect, useState } from "react";
8
- export function DecayTreeGraph({ decay, lockGraphZoom, selectedParticlesIds, onNodeSelectionChanged }) {
8
+ import { useEffect, useRef } from "react";
9
+ cytoscape.use(dagre);
10
+ export function DecayTreeGraph({ decay, selectedParticlesIds, onNodeSelectionChanged }) {
9
11
  const metadata = useMetadata();
10
- // eslint-disable-next-line
11
- const [network, setNetwork] = useState(null);
12
+ const cyRef = useRef(null);
13
+ // Sync external selection state to cytoscape selection
12
14
  useEffect(() => {
13
- // eslint-disable-next-line
14
- network?.selectNodes(selectedParticlesIds);
15
+ const cy = cyRef.current;
16
+ if (!cy)
17
+ return;
18
+ cy.nodes().forEach((node) => {
19
+ if (selectedParticlesIds.includes(node.id())) {
20
+ node.select();
21
+ }
22
+ else {
23
+ node.unselect();
24
+ }
25
+ });
15
26
  }, [selectedParticlesIds]);
16
- useEffect(() => {
17
- // eslint-disable-next-line
18
- network?.setOptions({ interaction: { zoomView: lockGraphZoom } });
19
- }, [lockGraphZoom]);
20
- function htmlTitle(text) {
21
- const container = document.createElement("span");
22
- container.innerText = text;
23
- return container;
24
- }
25
27
  function makeSVG(text, colour = "black") {
26
- const pipes = "\\phantom{{}^{|-}_{|-}}"; // looks weird but helps ensure uniform text scaling
28
+ const pipes = "\\phantom{{}^{|-}_{|-}}";
27
29
  const texSVG = tex2svg(`${pipes}${text}${pipes}`).innerHTML.replaceAll("currentColor", colour);
28
30
  return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(texSVG)}`;
29
31
  }
30
- function buildGraph() {
31
- const graph = { nodes: [], edges: [] };
32
- if (!metadata) {
33
- return graph;
34
- }
32
+ function getHtmlTitle(text) {
33
+ const container = document.createElement("span");
34
+ container.innerText = text;
35
+ return container;
36
+ }
37
+ function buildElements() {
38
+ const elements = [];
39
+ if (!metadata)
40
+ return elements;
35
41
  const addNodeAndEdge = (item, parentId) => {
36
42
  const { branch, particle } = item;
37
43
  const particleProperties = metadata.particleProperties[particle];
38
- graph.nodes.push({
39
- id: branch,
40
- label: branch,
41
- title: htmlTitle(particleProperties.html),
42
- shape: "image",
43
- image: {
44
- selected: makeSVG(particleProperties.latex, "#0d6efd"),
45
- unselected: makeSVG(particleProperties.latex, "black"),
44
+ elements.push({
45
+ data: {
46
+ id: branch,
47
+ label: branch,
48
+ // Store both SVG variants so we can swap on select/unselect
49
+ imageSrc: makeSVG(particleProperties.latex, "black"),
50
+ imageSelected: makeSVG(particleProperties.latex, "#0d6efd"),
51
+ // Tooltip text (accessible via tippy or title attr if needed)
52
+ title: getHtmlTitle(particleProperties.html).innerText,
46
53
  },
54
+ selected: selectedParticlesIds.includes(branch),
47
55
  });
48
56
  if (parentId !== null) {
49
- graph.edges.push({ from: parentId, to: branch });
57
+ elements.push({
58
+ data: {
59
+ id: `${parentId}->${branch}`,
60
+ source: parentId,
61
+ target: branch,
62
+ },
63
+ });
50
64
  }
51
65
  return branch;
52
66
  };
@@ -68,22 +82,94 @@ export function DecayTreeGraph({ decay, lockGraphZoom, selectedParticlesIds, onN
68
82
  }
69
83
  };
70
84
  walk(decay.descriptors.mapped_list, null);
71
- return graph;
85
+ return elements;
72
86
  }
73
- return (_jsx(Graph, { graph: buildGraph(), options: config.dttGraphOptions, events: {
74
- // @ts-expect-error The react-graph-vis package does not have types
75
- selectNode: (event) => {
76
- // eslint-disable-next-line
77
- onNodeSelectionChanged(event.nodes);
87
+ const stylesheet = [
88
+ // Disable the grey circle on pan/click
89
+ {
90
+ selector: "core",
91
+ style: {
92
+ "active-bg-size": 0,
93
+ },
94
+ },
95
+ {
96
+ selector: "node",
97
+ style: {
98
+ shape: "ellipse",
99
+ // Use the unselected SVG as the background image
100
+ "background-image": "data(imageSrc)",
101
+ "background-fit": "contain",
102
+ "background-opacity": 0,
103
+ width: config.dttGraphOptions.nodes.width,
104
+ height: config.dttGraphOptions.nodes.height,
105
+ label: "",
106
+ },
107
+ },
108
+ {
109
+ selector: "node:active",
110
+ style: {
111
+ "overlay-opacity": 0,
112
+ },
113
+ },
114
+ {
115
+ selector: "node.hover",
116
+ style: {
117
+ "background-opacity": 0.2,
118
+ "background-color": config.dttGraphOptions.nodes.hoverColor,
78
119
  },
79
- // @ts-expect-error The react-graph-vis package does not have types
80
- deselectNode: (event) => {
81
- // eslint-disable-next-line
82
- onNodeSelectionChanged(event.nodes);
120
+ },
121
+ {
122
+ selector: "node:selected",
123
+ style: {
124
+ // Swap to the selected SVG when the node is selected
125
+ "background-image": "data(imageSelected)",
83
126
  },
84
- },
85
- // @ts-expect-error The react-graph-vis package does not have types
86
- getNetwork: (n) => {
87
- setNetwork(n);
127
+ },
128
+ {
129
+ selector: "edge",
130
+ style: {
131
+ "curve-style": "bezier",
132
+ "target-arrow-shape": "triangle",
133
+ width: config.dttGraphOptions.edges.width,
134
+ "line-color": config.dttGraphOptions.edges.color,
135
+ "target-arrow-color": config.dttGraphOptions.edges.color,
136
+ events: "no",
137
+ },
138
+ },
139
+ ];
140
+ return (_jsx(CytoscapeComponent, { elements: buildElements(), stylesheet: stylesheet, style: { width: "100%", height: config.dttGraphOptions.height }, autoungrabify: !config.dttGraphOptions.nodes.draggable, autounselectify: false, boxSelectionEnabled: true, cy: (cy) => {
141
+ cyRef.current = cy;
142
+ // Set up zoom
143
+ cy.userZoomingEnabled(false);
144
+ cy.minZoom(config.dttGraphOptions.zoomMin);
145
+ cy.maxZoom(config.dttGraphOptions.zoomMax);
146
+ // Handle zoom event
147
+ cy.container()?.addEventListener("wheel", (e) => {
148
+ if (!(e.ctrlKey || e.metaKey))
149
+ return;
150
+ e.preventDefault();
151
+ const zoom = cy.zoom();
152
+ const factor = 1 + (e.deltaY < 0 ? config.dttGraphOptions.zoomStep : -config.dttGraphOptions.zoomStep);
153
+ cy.zoom({
154
+ level: zoom * factor,
155
+ renderedPosition: { x: e.offsetX, y: e.offsetY },
156
+ });
157
+ }, { passive: false });
158
+ // Handle node selection events
159
+ cy.off("select unselect", "node");
160
+ cy.on("select unselect", "node", () => {
161
+ const selected = cy.nodes(":selected").map((n) => n.id());
162
+ onNodeSelectionChanged(selected);
163
+ });
164
+ // Handle hover events
165
+ cy.on("mouseover", "node", (e) => {
166
+ e.target.addClass("hover");
167
+ });
168
+ cy.on("mouseout", "node", (e) => {
169
+ e.target.removeClass("hover");
170
+ });
171
+ }, layout: {
172
+ ...config.dttGraphOptions.layout,
173
+ name: "dagre",
88
174
  } }));
89
175
  }
@@ -3,7 +3,8 @@ interface Props {
3
3
  action: () => void;
4
4
  outline?: boolean;
5
5
  disabled?: boolean;
6
+ popupMessage?: string;
6
7
  children?: ReactNode;
7
8
  }
8
- export declare function DeleteButton({ action, outline, disabled, children }: Props): import("react/jsx-runtime").JSX.Element | null;
9
+ export declare function DeleteButton({ action, outline, disabled, popupMessage, children }: Props): import("react/jsx-runtime").JSX.Element | null;
9
10
  export {};
@@ -11,9 +11,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
11
  \*****************************************************************************/
12
12
  import { Button, OverlayTrigger, Popover } from "react-bootstrap";
13
13
  import { Trash } from "react-bootstrap-icons";
14
- export function DeleteButton({ action, outline, disabled, children }) {
14
+ export function DeleteButton({ action, outline, disabled, popupMessage = "Confirm", children }) {
15
15
  if (disabled) {
16
16
  return null;
17
17
  }
18
- return (_jsx(OverlayTrigger, { trigger: "click", placement: "auto", rootClose: true, overlay: _jsx(Popover, { children: _jsx(Popover.Body, { children: _jsx(Button, { variant: "danger", onClick: action, children: "Confirm" }) }) }), children: _jsxs(Button, { variant: outline ? "outline-danger" : "danger", disabled: disabled, children: [_jsx(Trash, {}), " ", children] }) }));
18
+ return (_jsx(OverlayTrigger, { trigger: "click", placement: "auto", rootClose: true, overlay: _jsx(Popover, { children: _jsx(Popover.Body, { children: _jsx(Button, { variant: "danger", onClick: action, children: popupMessage }) }) }), children: _jsxs(Button, { variant: outline ? "outline-danger" : "danger", disabled: disabled, children: [_jsx(Trash, {}), " ", children] }) }));
19
19
  }
@@ -25,5 +25,10 @@ export function NtupleWizard({ basePath = "", contactEmail, submitLocation, requ
25
25
  if (contactEmail) {
26
26
  localStorage.setItem("email", contactEmail);
27
27
  }
28
- return (_jsx(MetadataProvider, { children: _jsx(WizardConfigProvider, { basePath: basePath, submitLocation: submitLocation, requestReasonMessage: requestReasonMessage, requestSubmittedMessage: requestSubmittedMessage, onRequestSubmitted: onRequestSubmitted, children: _jsx(RowsProvider, { children: _jsx(RequestProvider, { emailIsKnown: !!contactEmail, children: _jsx(MathJaxContext, { children: pathname.endsWith(SELECT_DECAYS_PATH) ? (_jsx(DecaySearchPage, {})) : pathname.endsWith(VARIABLES_PATH) ? (_jsx(DecayTreeConfigPage, {})) : (_jsx(RequestPage, {})) }) }) }) }) }));
28
+ return (_jsx(MetadataProvider, { children: _jsx(WizardConfigProvider, { basePath: basePath, submitLocation: submitLocation, requestReasonMessage: requestReasonMessage, requestSubmittedMessage: requestSubmittedMessage, onRequestSubmitted: onRequestSubmitted, children: _jsx(RowsProvider, { children: _jsx(RequestProvider, { emailIsKnown: !!contactEmail, children: _jsx(MathJaxContext, { config: {
29
+ loader: { load: ["[tex]/color"] },
30
+ tex: {
31
+ packages: { "[+]": ["color"] },
32
+ },
33
+ }, children: pathname.endsWith(SELECT_DECAYS_PATH) ? (_jsx(DecaySearchPage, {})) : pathname.endsWith(VARIABLES_PATH) ? (_jsx(DecayTreeConfigPage, {})) : (_jsx(RequestPage, {})) }) }) }) }) }));
29
34
  }
@@ -18,7 +18,7 @@ export function RequestButtonGroup() {
18
18
  const { rows, configuredRows, setRows } = useRows();
19
19
  const navigate = useNavigate();
20
20
  const [showProdUploadModal, setShowProdUploadModal] = useState(false);
21
- const handleSubmitRows = async () => {
21
+ const configureAllDtts = async () => {
22
22
  setRows((prev) => prev.map((r) => ({ ...r, editTree: true })));
23
23
  await navigate(`${basePath}${VARIABLES_PATH}`);
24
24
  };
@@ -26,6 +26,6 @@ export function RequestButtonGroup() {
26
26
  return null;
27
27
  }
28
28
  return (_jsxs(_Fragment, { children: [showProdUploadModal && _jsx(UploadDttConfigModal, { onClose: () => setShowProdUploadModal(false) }), _jsxs(ButtonGroup, { children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Select decays" }), children: _jsxs(Button, { type: "submit", variant: "success", onClick: () => void navigate(`${basePath}${SELECT_DECAYS_PATH}`), className: "align-items-center d-flex gap-1", children: [_jsx(PlusLg, {}), " Select decays"] }) }), variant === "standalone" && (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Upload Production configuration file" }), children: _jsx(Button, { className: "ms-auto align-items-center d-flex", type: "button", onClick: () => setShowProdUploadModal(true), children: _jsx(Upload, {}) }) })), _jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Configure all DecayTreeTuples" }), children: _jsxs(Button, { type: "submit", variant: "secondary", onClick: () => {
29
- handleSubmitRows().catch(console.error);
29
+ configureAllDtts().catch(console.error);
30
30
  }, disabled: !validation.allRowsHaveDtt, className: "align-items-center d-flex gap-1", children: [_jsx(GearWideConnected, {}), " ", _jsx(Badge, { pill: true, bg: "primary", children: configuredRows.length })] }) }), variant === "standalone" && (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Download Analysis Production configuration file (info.yaml)" }), children: _jsx(Button, { variant: "primary", type: "button", onClick: () => downloadText(YamlFile.createInfoYaml(rows, metadata)), disabled: !validation.allRowsHaveDtt || !validation.allRowsHavePaths || !validation.isEmailValid, className: "align-items-center d-flex", children: _jsx(Download, {}) }) }))] })] }));
31
31
  }
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Col, OverlayTrigger, Popover, Row } from "react-bootstrap";
3
3
  import { InfoCircle } from "react-bootstrap-icons";
4
4
  import { StrippingLineInfo } from "./StrippingLineInfo";
5
- import { DeleteButton } from "./DeleteButton";
6
- import { DttNameInput } from "./DttNameInput";
5
+ import { DecayCard } from "./DecayCard";
7
6
  import { useMetadata } from "../providers/MetadataProvider";
8
- import { useRows } from "../providers/RowsProvider";
9
7
  import { BookkeepingPathDropdown } from "./BookkeepingPathDropdown";
10
8
  import { StrippingLineDropdown } from "./StrippingLineDropdown";
11
9
  import { useRow } from "../providers/RowProvider";
10
+ import { VerticalLine } from "./VerticalLine";
11
+ import { DeleteButton } from "./DeleteButton";
12
+ import { useRows } from "../providers/RowsProvider";
12
13
  export function RequestRow() {
13
14
  const metadata = useMetadata();
14
15
  const { row } = useRow();
@@ -16,5 +17,5 @@ export function RequestRow() {
16
17
  if (!metadata) {
17
18
  return null;
18
19
  }
19
- return (_jsxs(Row, { className: "align-items-center", children: [_jsx(Col, { lg: true, children: _jsx(DttNameInput, {}) }), _jsx(Col, { lg: true, children: _jsx(StrippingLineDropdown, {}) }), _jsx(Col, { xs: "auto", children: _jsx(OverlayTrigger, { trigger: "click", placement: "right", rootClose: true, overlay: _jsx(Popover, { children: _jsx(Popover.Body, { children: row.lines.map((line) => (_jsx(StrippingLineInfo, { line: line.line, stream: line.stream, versions: line.versions, showLink: true }, line.line))) }) }), children: _jsx(Button, { disabled: row.lines.length === 0, children: _jsx(InfoCircle, {}) }) }) }), _jsx(Col, { children: _jsx(BookkeepingPathDropdown, {}) }), _jsx(Col, { xs: "auto", children: _jsx(DeleteButton, { action: () => removeRow(row.id) }) })] }));
20
+ return (_jsxs(Row, { className: "align-items-stretch mt-1", children: [_jsx(Col, { xs: 5, children: _jsx(DecayCard, {}) }), _jsxs(Col, { xs: 6, className: "d-flex flex-column gap-2", children: [_jsx(Row, { children: _jsxs("div", { className: "d-flex flex-row justify-content-between align-items-center gap-3", children: [_jsx("div", { style: { width: "100%" }, children: _jsx(StrippingLineDropdown, {}) }), _jsx(OverlayTrigger, { trigger: "click", rootClose: true, overlay: _jsx(Popover, { children: _jsx(Popover.Body, { children: row.lines.map((line) => (_jsx(StrippingLineInfo, { line: line.line, stream: line.stream, versions: line.versions, showLink: true }, line.line))) }) }), children: _jsx(Button, { disabled: row.lines.length === 0, children: _jsx(InfoCircle, {}) }) })] }) }), _jsx(Row, { children: _jsx(BookkeepingPathDropdown, {}) })] }), _jsxs(Col, { xs: 1, className: "d-flex align-items-center justify-content-center gap-4", children: [_jsx(VerticalLine, {}), _jsx(DeleteButton, { action: () => removeRow(row.id), popupMessage: "Delete row" })] })] }));
20
21
  }
@@ -24,5 +24,5 @@ export function StrippingLineBadge({ version, line, stream, showLink }) {
24
24
  target: "_blank",
25
25
  }
26
26
  : {};
27
- return (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: metadata ? metadata.strippingHints[version].description : "..." }), children: _jsx(Badge, { pill: true, bg: "info", ...badgeProps, children: "S" + version }) }));
27
+ return (_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { style: { zIndex: 9999 }, children: metadata ? metadata.strippingHints[version].description : "..." }), children: _jsx(Badge, { pill: true, bg: "info", ...badgeProps, children: "S" + version }) }));
28
28
  }
@@ -10,12 +10,9 @@
10
10
  \*****************************************************************************/
11
11
  import { GroupBase, Props as SelectProps } from "react-select";
12
12
  import { ReactNode } from "react";
13
- type ToolOption = {
13
+ type ToolDropdownOption = {
14
14
  value: string;
15
15
  label: ReactNode;
16
16
  };
17
- interface Props extends SelectProps<ToolOption, false, GroupBase<ToolOption>> {
18
- filterToolTags: string[];
19
- }
20
- export declare function TupleToolDropdown({ filterToolTags, ...props }: Props): import("react/jsx-runtime").JSX.Element;
17
+ export declare function TupleToolClassDropdown(props: SelectProps<ToolDropdownOption, false, GroupBase<ToolDropdownOption>>): import("react/jsx-runtime").JSX.Element;
21
18
  export {};
@@ -13,17 +13,23 @@ import Select from "react-select";
13
13
  import { TupleToolLabel } from "./TupleToolLabel";
14
14
  import { useMetadata } from "../providers/MetadataProvider.js";
15
15
  import { TupleTool } from "../models/tupleTool";
16
- export function TupleToolDropdown({ filterToolTags, ...props }) {
16
+ import { useDtt } from "../providers/DttProvider";
17
+ export function TupleToolClassDropdown(props) {
17
18
  const metadata = useMetadata();
19
+ const { selectedBranch } = useDtt();
20
+ const allowedTags = ["IParticleTupleTool", ...(selectedBranch.length === 0 ? ["IEventTupleTool"] : [])];
21
+ const disallowedTools = ["LoKi__Hybrid__ArrayTupleTool"];
22
+ // Group tools by tag
18
23
  const options = metadata
19
- ? filterToolTags.map((allowedTag) => ({
24
+ ? allowedTags.map((allowedTag) => ({
20
25
  label: allowedTag,
21
26
  options: Object.keys(metadata.tupleTools.tupleTools)
22
27
  .sort()
23
28
  .map(TupleTool.fromString)
24
29
  .map((tool) => ({ value: tool.toString(), label: _jsx(TupleToolLabel, { tool: tool }) }))
25
- .filter((tool) => metadata.tupleTools.tupleTools[tool.value].tags.includes(allowedTag)),
30
+ .filter((tool) => metadata.tupleTools.tupleTools[tool.value].tags.includes(allowedTag) &&
31
+ !disallowedTools.includes(tool.value)),
26
32
  }))
27
33
  : [];
28
- return (_jsx("div", { className: "react-select form-control p-0", children: _jsx(Select, { options: options, isLoading: !metadata, ...props }) }));
34
+ return (_jsx("div", { className: "react-select form-control p-0", children: _jsx(Select, { placeholder: "TupleTool class", options: options, isLoading: !metadata, ...props }) }));
29
35
  }
@@ -1,6 +1,5 @@
1
1
  interface Props {
2
- branch: string[];
3
2
  onGroupSelected: (group: string) => void;
4
3
  }
5
- export declare function TupleToolGroup({ branch, onGroupSelected }: Props): import("react/jsx-runtime").JSX.Element;
4
+ export declare function TupleToolGroup({ onGroupSelected }: Props): import("react/jsx-runtime").JSX.Element;
6
5
  export {};
@@ -3,14 +3,14 @@ import { Button, Card, ListGroup, OverlayTrigger, Popover } from "react-bootstra
3
3
  import { PencilSquare } from "react-bootstrap-icons";
4
4
  import { DecayLatex } from "./DecayLatex";
5
5
  import { useDtt } from "../providers/DttProvider";
6
- export function TupleToolGroup({ branch, onGroupSelected }) {
7
- const { dtt, decay } = useDtt();
6
+ export function TupleToolGroup({ onGroupSelected }) {
7
+ const { dtt, decay, selectedBranch } = useDtt();
8
8
  let groups = {};
9
- if (branch.length === 0) {
9
+ if (selectedBranch.length === 0) {
10
10
  groups = dtt.config.groups;
11
11
  }
12
- else if (branch.length === 1) {
13
- const filteredGroups = Object.entries(dtt.config.groups).filter(([key]) => key.includes(branch[0]));
12
+ else if (selectedBranch.length === 1) {
13
+ const filteredGroups = Object.entries(dtt.config.groups).filter(([key]) => key.includes(selectedBranch[0]));
14
14
  groups = Object.fromEntries(filteredGroups);
15
15
  }
16
16
  const groupKeys = Object.keys(groups);
@@ -1,7 +1,6 @@
1
1
  interface Props {
2
- branch: string[];
3
2
  onGroupSelected: (group: string) => void;
4
3
  onSave: () => void;
5
4
  }
6
- export declare function TupleToolList({ branch, onGroupSelected, onSave }: Props): import("react/jsx-runtime").JSX.Element | null;
5
+ export declare function TupleToolList({ onGroupSelected, onSave }: Props): import("react/jsx-runtime").JSX.Element | null;
7
6
  export {};
@@ -19,19 +19,23 @@ import { TupleToolGroup } from "./TupleToolGroup";
19
19
  import { AddTupleToolModal } from "./modals/AddTupleToolModal";
20
20
  import { ConfigureTupleToolModal } from "./modals/ConfigureTupleToolModal";
21
21
  import { useDtt } from "../providers/DttProvider";
22
- export function TupleToolList({ branch, onGroupSelected, onSave }) {
22
+ export function TupleToolList({ onGroupSelected, onSave }) {
23
23
  const metadata = useMetadata();
24
- const { dtt, dirty, save, revert, removeTool } = useDtt();
24
+ const { dtt, dirty, save, revert, removeTool, selectedBranch } = useDtt();
25
25
  const [showAddModal, setShowAddModal] = useState(false);
26
- const [showConfigModalWithTool, setShowConfigModalWithTool] = useState(null);
26
+ const [showConfigModal, setShowConfigModal] = useState(null);
27
27
  if (!metadata) {
28
28
  return null;
29
29
  }
30
- const tools = dtt.listTools(branch);
31
- const filterToolTags = branch.length === 0 ? ["IParticleTupleTool", "IEventTupleTool"] : ["IParticleTupleTool"];
30
+ const tools = dtt.listTools(selectedBranch);
32
31
  return (_jsxs(_Fragment, { children: [_jsxs(Card, { children: [_jsxs(Card.Header, { className: "d-flex justify-content-between align-items-start", children: [_jsxs("span", { children: [tools.length, " TupleTool", tools.length === 1 ? "" : "s"] }), _jsx(Button, { variant: "success", size: "sm", onClick: () => setShowAddModal(true), children: _jsx(PlusLg, {}) })] }), _jsx(ListGroup, { variant: "flush", children: tools.map((tool) => {
33
- return (_jsxs(ListGroup.Item, { className: "d-flex justify-content-between align-items-start", variant: showConfigModalWithTool?.equals(tool) ? "secondary" : "", children: [_jsx(TupleToolLabel, { tool: tool }), _jsxs(ButtonGroup, { size: "sm", children: [_jsx(Button, { variant: "outline-secondary", onClick: () => setShowConfigModalWithTool(tool), children: _jsx(PencilSquare, {}) }), _jsx(DeleteButton, { outline: true, action: () => removeTool(branch, tool) })] })] }, tool.toString()));
34
- }) })] }), showAddModal && (_jsx(AddTupleToolModal, { branch: branch, filterToolTags: filterToolTags, onClose: () => setShowAddModal(false) })), showConfigModalWithTool && (_jsx(ConfigureTupleToolModal, { branch: branch, tool: showConfigModalWithTool, onClose: () => setShowConfigModalWithTool(null) })), branch.length === 0 && Object.keys(dtt.config.groups).length > 0 && (_jsx(TupleToolGroup, { branch: branch, onGroupSelected: onGroupSelected })), _jsxs(ButtonGroup, { className: "mt-2", children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Save changes to this DecayTreeTuple" }), children: _jsx(Button, { onClick: () => {
32
+ return (_jsxs(ListGroup.Item, { className: "d-flex justify-content-between align-items-start", variant: showConfigModal?.tool.equals(tool) ? "secondary" : "", children: [_jsx(TupleToolLabel, { tool: tool }), _jsxs(ButtonGroup, { size: "sm", children: [_jsx(Button, { variant: "outline-secondary", onClick: () => setShowConfigModal({ tool, deleteIfCancelled: false }), children: _jsx(PencilSquare, {}) }), _jsx(DeleteButton, { outline: true, action: () => removeTool(selectedBranch, tool) })] })] }, tool.toString()));
33
+ }) })] }), showAddModal && (_jsx(AddTupleToolModal, { onClose: (newTool) => {
34
+ setShowAddModal(false);
35
+ if (newTool && newTool.class === "LoKi__Hybrid__TupleTool") {
36
+ setShowConfigModal({ tool: newTool, deleteIfCancelled: true });
37
+ }
38
+ } })), showConfigModal && (_jsx(ConfigureTupleToolModal, { tool: showConfigModal.tool, deleteIfCancelled: showConfigModal.deleteIfCancelled, onClose: () => setShowConfigModal(null) })), selectedBranch.length === 0 && Object.keys(dtt.config.groups).length > 0 && (_jsx(TupleToolGroup, { onGroupSelected: onGroupSelected })), _jsxs(ButtonGroup, { className: "mt-2", children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Save changes to this DecayTreeTuple" }), children: _jsx(Button, { onClick: () => {
35
39
  save();
36
40
  onSave();
37
41
  }, disabled: !dirty, children: "Save" }) }), _jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: "Discard changes to this DecayTreeTuple" }), children: _jsx(Button, { variant: "outline-danger", onClick: revert, disabled: !dirty, children: "Revert" }) })] })] }));
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ width?: string;
3
+ height?: string;
4
+ color?: string;
5
+ }
6
+ export declare function VerticalLine({ width, height, color }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function VerticalLine({ width = "1px", height = "100%", color = "#bbb" }) {
3
+ return _jsx("div", { style: { borderLeft: `${width} solid ${color}`, height } });
4
+ }
@@ -1,7 +1,6 @@
1
+ import { TupleTool } from "../../models/tupleTool";
1
2
  interface Props {
2
- branch: string[];
3
- filterToolTags: string[];
4
- onClose: () => void;
3
+ onClose: (newTool: TupleTool | null) => void;
5
4
  }
6
- export declare function AddTupleToolModal({ branch, filterToolTags, onClose }: Props): import("react/jsx-runtime").JSX.Element;
5
+ export declare function AddTupleToolModal({ onClose }: Props): import("react/jsx-runtime").JSX.Element;
7
6
  export {};
@@ -1,18 +1,18 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, FormControl, InputGroup, Modal } from "react-bootstrap";
3
- import { TupleToolDropdown } from "../TupleToolDropdown";
2
+ import { Button, ButtonGroup, FormControl, InputGroup, Modal } from "react-bootstrap";
3
+ import { TupleToolClassDropdown } from "../TupleToolClassDropdown";
4
4
  import { TupleToolDocsAccordion } from "../TupleToolDocsAccordion";
5
5
  import { useDtt } from "../../providers/DttProvider";
6
6
  import { TupleTool } from "../../models/tupleTool";
7
7
  import { useEffect, useRef, useState } from "react";
8
- export function AddTupleToolModal({ branch, filterToolTags, onClose }) {
9
- const { dtt, addTool } = useDtt();
8
+ export function AddTupleToolModal({ onClose }) {
9
+ const { dtt, addTool, selectedBranch } = useDtt();
10
10
  const nameInputRef = useRef(null);
11
11
  const [selectedTool, setSelectedTool] = useState(null);
12
12
  useEffect(() => {
13
13
  nameInputRef.current?.focus();
14
14
  }, [selectedTool]);
15
- return (_jsxs(Modal, { show: true, onHide: onClose, children: [_jsx(Modal.Header, { closeButton: true, children: "Add TupleTool" }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [_jsxs(InputGroup, { hasValidation: true, children: [_jsx(TupleToolDropdown, { filterToolTags: filterToolTags, placeholder: "TupleTool class", onChange: (option) => {
15
+ return (_jsxs(Modal, { show: true, onHide: () => onClose(null), children: [_jsx(Modal.Header, { closeButton: true, children: "Add TupleTool" }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [_jsxs(InputGroup, { hasValidation: true, children: [_jsx(TupleToolClassDropdown, { onChange: (option) => {
16
16
  if (option) {
17
17
  setSelectedTool((prev) => new TupleTool(option.value, prev?.name || ""));
18
18
  }
@@ -20,13 +20,13 @@ export function AddTupleToolModal({ branch, filterToolTags, onClose }) {
20
20
  // Remove all non-word characters
21
21
  const name = event.target.value.replaceAll(/[^\w]/g, "");
22
22
  setSelectedTool((prev) => new TupleTool(prev.class, name));
23
- }, value: selectedTool?.name || "", disabled: !selectedTool, isInvalid: !!selectedTool && dtt.toolExists(selectedTool, branch), onKeyDown: (event) => {
23
+ }, value: selectedTool?.name || "", disabled: !selectedTool, isInvalid: !!selectedTool && dtt.toolExists(selectedTool, selectedBranch), onKeyDown: (event) => {
24
24
  if (event.key === "Enter") {
25
- addTool(branch, selectedTool);
26
- onClose();
25
+ addTool(selectedBranch, selectedTool);
26
+ onClose(selectedTool);
27
27
  }
28
- } }), selectedTool && (_jsxs(FormControl.Feedback, { type: "invalid", children: ["A ", selectedTool.class, " with the name \"", selectedTool.name, "\" already exists"] }))] }), selectedTool && _jsx(TupleToolDocsAccordion, { toolClass: selectedTool.class }), _jsx(Button, { className: "align-self-end", variant: "success", disabled: !selectedTool || dtt.toolExists(selectedTool, branch), onClick: () => {
29
- addTool(branch, selectedTool);
30
- onClose();
31
- }, children: "Add tool" })] })] }));
28
+ } }), selectedTool && (_jsxs(FormControl.Feedback, { type: "invalid", children: ["A ", selectedTool.class, " with the name \"", selectedTool.name, "\" already exists"] }))] }), selectedTool && _jsx(TupleToolDocsAccordion, { toolClass: selectedTool.class }), _jsxs(ButtonGroup, { className: "align-self-end", children: [_jsx(Button, { variant: "outline-dark", onClick: () => onClose(null), children: "Cancel" }), _jsx(Button, { disabled: !selectedTool || dtt.toolExists(selectedTool, selectedBranch), onClick: () => {
29
+ addTool(selectedBranch, selectedTool);
30
+ onClose(selectedTool);
31
+ }, children: "Add tool" })] })] })] }));
32
32
  }
@@ -1,8 +1,8 @@
1
1
  import { TupleTool } from "../../models/tupleTool";
2
2
  interface Props {
3
- branch: string[];
4
3
  tool: TupleTool;
4
+ deleteIfCancelled: boolean;
5
5
  onClose: () => void;
6
6
  }
7
- export declare function ConfigureTupleToolModal({ branch, tool, onClose }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export declare function ConfigureTupleToolModal({ tool, deleteIfCancelled, onClose }: Props): import("react/jsx-runtime").JSX.Element;
8
8
  export {};
@@ -1,5 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { Button, Col, Form, Modal, OverlayTrigger, Row, Tooltip } from "react-bootstrap";
2
+ import { Button, ButtonGroup, Col, Form, Modal, OverlayTrigger, Row, Tooltip } from "react-bootstrap";
3
3
  import { StrParamInput } from "../tupleToolParams/StrParamInput";
4
4
  import { BoolParamInput } from "../tupleToolParams/BoolParamInput";
5
5
  import { NumParamInput } from "../tupleToolParams/NumParamInput";
@@ -8,36 +8,53 @@ import { DictParamInput } from "../tupleToolParams/DictParamInput";
8
8
  import { QuestionCircle } from "react-bootstrap-icons";
9
9
  import { TupleToolDocsAccordion } from "../TupleToolDocsAccordion";
10
10
  import { useDtt } from "../../providers/DttProvider";
11
- export function ConfigureTupleToolModal({ branch, tool, onClose }) {
12
- const { dtt, updateToolParam } = useDtt();
13
- return (_jsxs(Modal, { show: true, size: "xl", fullscreen: "lg-down", onHide: onClose, children: [_jsxs(Modal.Header, { closeButton: true, children: ["Configure ", tool.toString()] }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [tool ? (_jsx(Form, { children: Object.entries(dtt.getToolConfig(branch, tool)).map(([paramName, param]) => {
11
+ import { config } from "../../config";
12
+ import { toast } from "react-toastify";
13
+ export function ConfigureTupleToolModal({ tool, deleteIfCancelled, onClose }) {
14
+ const { dtt, updateToolParam, removeTool, selectedBranch } = useDtt();
15
+ const toolConfig = dtt.getToolConfig(selectedBranch, tool);
16
+ return (_jsxs(Modal, { show: true, size: "xl", fullscreen: "lg-down", backdrop: "static", keyboard: false, children: [_jsxs(Modal.Header, { children: ["Configure ", tool.toString()] }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [_jsx(Form, { children: Object.entries(toolConfig).map(([paramName, param]) => {
14
17
  const { description, type, default: defaultValue, value } = param;
15
18
  let inputComponent;
16
19
  switch (type) {
17
20
  case "str":
18
- inputComponent = (_jsx(StrParamInput, { value: value, defaultValue: defaultValue, onChange: (newValue) => updateToolParam(branch, tool, paramName, newValue), param: paramName }));
21
+ inputComponent = (_jsx(StrParamInput, { value: value, defaultValue: defaultValue, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue), param: paramName }));
19
22
  break;
20
23
  case "bool":
21
- inputComponent = (_jsx(BoolParamInput, { value: value, onChange: (newValue) => updateToolParam(branch, tool, paramName, newValue) }));
24
+ inputComponent = (_jsx(BoolParamInput, { value: value, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue) }));
22
25
  break;
23
26
  case "int":
24
27
  case "uint":
25
28
  case "float":
26
- inputComponent = (_jsx(NumParamInput, { type: type, value: value, defaultValue: defaultValue, onChange: (newValue) => updateToolParam(branch, tool, paramName, newValue) }));
29
+ inputComponent = (_jsx(NumParamInput, { type: type, value: value, defaultValue: defaultValue, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue) }));
27
30
  break;
28
31
  case "text":
29
- inputComponent = (_jsx(ListParamInput, { initialValues: value, defaultValues: defaultValue, newItemValue: "", onChange: (newValue) => updateToolParam(branch, tool, paramName, newValue), buildInnerInput: (props) => _jsx(StrParamInput, { ...props, param: paramName }) }));
32
+ inputComponent = (_jsx(ListParamInput, { initialValues: value, defaultValues: defaultValue, newItemValue: "", onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue), buildInnerInput: (props) => _jsx(StrParamInput, { ...props, param: paramName }) }));
30
33
  break;
31
34
  case "[int]":
32
35
  case "[uint]":
33
36
  case "[float]":
34
- inputComponent = (_jsx(ListParamInput, { initialValues: value, defaultValues: defaultValue, newItemValue: 0, onChange: (newValue) => updateToolParam(branch, tool, paramName, newValue), buildInnerInput: (props) => (_jsx(NumParamInput, { type: type.slice(1, -1), ...props })) }));
37
+ inputComponent = (_jsx(ListParamInput, { initialValues: value, defaultValues: defaultValue, newItemValue: 0, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue), buildInnerInput: (props) => (_jsx(NumParamInput, { type: type.slice(1, -1), ...props })) }));
35
38
  break;
36
39
  case "{str:str}":
37
40
  case "{str:[str]}":
38
- inputComponent = (_jsx(DictParamInput, { value: value, onChange: (newValue) => updateToolParam(branch, tool, paramName, newValue) }));
41
+ inputComponent = (_jsx(DictParamInput, { value: value, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue) }));
39
42
  break;
40
43
  }
41
44
  return (_jsxs(Form.Group, { as: Row, className: "mb-2", children: [_jsxs(Form.Label, { column: true, sm: 3, children: [_jsx(OverlayTrigger, { overlay: _jsx(Tooltip, { children: description }), children: _jsx(QuestionCircle, {}) }), _jsxs("code", { children: [" ", paramName, " "] })] }), _jsx(Form.Label, { column: true, sm: 2, children: _jsxs("code", { children: [" ", type, " "] }) }), _jsx(Col, { sm: 7, children: inputComponent })] }, paramName));
42
- }) })) : ("No tool selected"), _jsx(TupleToolDocsAccordion, { toolClass: tool.class }), _jsx(Button, { className: "align-self-end", onClick: onClose, children: "Save & close" })] })] }));
45
+ }) }), _jsx(TupleToolDocsAccordion, { toolClass: tool.class }), _jsxs(ButtonGroup, { className: "align-self-end", children: [_jsx(Button, { variant: deleteIfCancelled ? "outline-danger" : "outline-dark", onClick: () => {
46
+ if (deleteIfCancelled) {
47
+ removeTool(selectedBranch, tool);
48
+ }
49
+ onClose();
50
+ }, children: "Cancel" }), _jsx(Button, { onClick: () => {
51
+ if (Object.keys(config.tupleToolValidation).includes(tool.class)) {
52
+ const errorMsg = config.tupleToolValidation[tool.class].validate(toolConfig);
53
+ if (errorMsg) {
54
+ toast(errorMsg, { type: "error" });
55
+ return;
56
+ }
57
+ }
58
+ onClose();
59
+ }, children: "Save & close" })] })] })] }));
43
60
  }
package/dist/config.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { DagreLayoutOptions } from "cytoscape-dagre";
2
+ import { ToolConfig } from "./models/dtt";
1
3
  export declare const config: {
2
4
  metadata_baseurl: string;
3
5
  metadata_files: {
@@ -21,58 +23,27 @@ export declare const config: {
21
23
  lifetime: string;
22
24
  };
23
25
  dttGraphOptions: {
24
- layout: {
25
- hierarchical: {
26
- sortMethod: string;
27
- };
28
- };
26
+ height: string;
27
+ zoomMin: number;
28
+ zoomMax: number;
29
+ zoomStep: number;
30
+ layout: DagreLayoutOptions;
29
31
  nodes: {
30
- borderWidth: number;
31
- borderWidthSelected: number;
32
- imagePadding: number;
33
- font: {
34
- strokeWidth: number;
35
- background: string;
36
- };
37
- color: {
38
- background: string;
39
- highlight: {
40
- background: string;
41
- };
42
- hover: {
43
- background: string;
44
- };
45
- };
46
- scaling: {
47
- min: number;
48
- max: number;
49
- };
50
- value: number;
51
- shape: string;
52
- shapeProperties: {
53
- useBorderWithImage: boolean;
54
- useImageSize: boolean;
55
- };
32
+ draggable: boolean;
33
+ width: number;
34
+ height: number;
35
+ hoverColor: string;
56
36
  };
57
37
  edges: {
58
38
  color: string;
59
39
  width: number;
60
- selectionWidth: number;
61
- hoverWidth: number;
62
- };
63
- physics: {
64
- enabled: boolean;
65
40
  };
66
- interaction: {
67
- dragNodes: boolean;
68
- multiselect: boolean;
69
- selectConnectedEdges: boolean;
70
- hoverConnectedEdges: boolean;
71
- hover: boolean;
72
- zoomView: boolean;
73
- };
74
- height: string;
75
- autoResize: boolean;
76
- width: string;
77
41
  };
42
+ tupleToolValidation: TupleToolValidation;
78
43
  };
44
+ interface TupleToolValidation {
45
+ [toolClass: string]: {
46
+ validate: (config: ToolConfig) => string | null;
47
+ };
48
+ }
49
+ export {};
package/dist/config.js CHANGED
@@ -21,52 +21,39 @@ export const config = {
21
21
  lifetime: "secondary",
22
22
  },
23
23
  dttGraphOptions: {
24
+ height: "500px",
25
+ zoomMin: 0.2,
26
+ zoomMax: 3.0,
27
+ zoomStep: 0.018,
24
28
  layout: {
25
- hierarchical: { sortMethod: "directed" },
29
+ rankDir: "TB", // top -> bottom
30
+ nodeSep: 50,
31
+ rankSep: 80,
32
+ animate: false,
26
33
  },
27
34
  nodes: {
28
- borderWidth: 0,
29
- borderWidthSelected: 0,
30
- imagePadding: 10,
31
- font: {
32
- strokeWidth: 20,
33
- background: "white",
34
- },
35
- color: {
36
- background: "white",
37
- highlight: { background: "white" },
38
- hover: { background: "#eee" },
39
- },
40
- scaling: {
41
- min: 32,
42
- max: 32,
43
- },
44
- value: 1,
45
- shape: "image",
46
- shapeProperties: {
47
- useBorderWithImage: true,
48
- useImageSize: false,
49
- },
35
+ draggable: false,
36
+ width: 80,
37
+ height: 30,
38
+ hoverColor: "royalblue",
50
39
  },
51
40
  edges: {
52
- color: "black",
41
+ color: "#222",
53
42
  width: 1,
54
- selectionWidth: 0,
55
- hoverWidth: 0,
56
43
  },
57
- physics: {
58
- enabled: false,
59
- },
60
- interaction: {
61
- dragNodes: false,
62
- multiselect: true,
63
- selectConnectedEdges: false,
64
- hoverConnectedEdges: false,
65
- hover: true,
66
- zoomView: false,
44
+ },
45
+ tupleToolValidation: {
46
+ ["LoKi__Hybrid__TupleTool"]: {
47
+ validate: (config) => {
48
+ const hasVariables = Object.entries(config["Variables"].value).length > 0;
49
+ const hasBoolVariables = Object.entries(config["BoolVariables"].value).length > 0;
50
+ const hasFloatVariables = Object.entries(config["FloatVariables"].value).length > 0;
51
+ const hasIntVariables = Object.entries(config["IntVariables"].value).length > 0;
52
+ if (!hasVariables && !hasBoolVariables && !hasFloatVariables && !hasIntVariables) {
53
+ return "Please add at least one variable";
54
+ }
55
+ return null;
56
+ },
67
57
  },
68
- height: "500px",
69
- autoResize: true,
70
- width: "100%",
71
58
  },
72
59
  };
@@ -9,7 +9,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
9
9
  * granted to it by virtue of its status as an Intergovernmental Organization *
10
10
  * or submit itself to any jurisdiction. *
11
11
  \*****************************************************************************/
12
- import { Button, Col, OverlayTrigger, Row, Stack, Tooltip } from "react-bootstrap";
12
+ import { Button, OverlayTrigger, Stack, Tooltip } from "react-bootstrap";
13
13
  import { useNavigate } from "react-router-dom";
14
14
  import { useMetadata } from "../providers/MetadataProvider";
15
15
  import { DecayTreeCard } from "../components/DecayTreeCard";
@@ -41,18 +41,18 @@ export function DecayTreeConfigPage() {
41
41
  return _jsx(LoadingIndicator, { height: "70vh" });
42
42
  }
43
43
  const rowsToEdit = rows.filter((row) => !!row.editTree);
44
- return (_jsxs(_Fragment, { children: [_jsx("h3", { children: "DecayTreeTuple configuration" }), _jsx(Stack, { gap: 5, children: rowsToEdit.map((row) => (_jsx(Row, { className: "justify-content-lg-center", children: _jsx(Col, { lg: "auto", children: _jsx(DttProvider, { metadata: metadata, decay: row.decay, initialConfig: row.dtt.config, children: _jsx(DecayTreeCard, { onConfigSaved: (newConfig) => {
45
- updateRow(row.id, (r) => ({ ...r, dtt: new Dtt(newConfig, {}) }));
46
- }, onDirtyUpdated: (dttName, dirty) => setDirtyDtts((prev) => {
47
- if (dirty) {
48
- return new Set(prev.add(dttName));
49
- }
50
- else {
51
- const newSet = new Set(prev);
52
- newSet.delete(dttName);
53
- return newSet;
54
- }
55
- }) }) }) }, row.id) }, row.id))) }), _jsx(OverlayTrigger, { placement: "right", overlay: _jsx(Tooltip, { children: "Return to Stripping line and dataset selection" }), children: _jsx(Button, { className: "mt-2 mb-4", type: "submit", onClick: () => {
44
+ return (_jsxs(_Fragment, { children: [_jsx("h3", { className: "mb-4", children: "DecayTreeTuple configuration" }), _jsx(Stack, { gap: 5, children: rowsToEdit.map((row) => (_jsx(DttProvider, { metadata: metadata, decay: row.decay, initialConfig: row.dtt.config, children: _jsx(DecayTreeCard, { onConfigSaved: (newConfig) => {
45
+ updateRow(row.id, (r) => ({ ...r, dtt: new Dtt(newConfig, {}) }));
46
+ }, onDirtyUpdated: (dttName, dirty) => setDirtyDtts((prev) => {
47
+ if (dirty) {
48
+ return new Set(prev.add(dttName));
49
+ }
50
+ else {
51
+ const newSet = new Set(prev);
52
+ newSet.delete(dttName);
53
+ return newSet;
54
+ }
55
+ }) }) }, row.id))) }), _jsx(OverlayTrigger, { placement: "right", overlay: _jsx(Tooltip, { children: "Return to Stripping line and dataset selection" }), children: _jsx(Button, { className: "mt-2 mb-4", type: "submit", onClick: () => {
56
56
  if (dirtyDtts.size > 0) {
57
57
  if (!confirm("You have unsaved changes. Are you sure you want to discard them?")) {
58
58
  return;
@@ -12,7 +12,6 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
12
12
  import { useEffect, useState } from "react";
13
13
  import { Alert, Button, Col, Row, Stack } from "react-bootstrap";
14
14
  import { Download, Send } from "react-bootstrap-icons";
15
- import { useLocation } from "react-router-dom";
16
15
  import { downloadZip, processProductionFiles } from "../utils/utils";
17
16
  import { DeleteButton } from "../components/DeleteButton";
18
17
  import { useMetadata } from "../providers/MetadataProvider";
@@ -29,7 +28,6 @@ import { RequestEmailInput } from "../components/RequestEmailInput";
29
28
  import { RequestButtonGroup } from "../components/RequestButtonGroup";
30
29
  import { useWizardConfig } from "../providers/WizardConfigProvider";
31
30
  export function RequestPage() {
32
- const location = useLocation();
33
31
  const metadata = useMetadata();
34
32
  const { rows, setRows, generateAllFiles } = useRows();
35
33
  const { emailIsKnown, productionName, setProductionName, validation, showErrors, clearAll, trySubmit } = useRequest();
@@ -38,17 +36,15 @@ export function RequestPage() {
38
36
  const [prodUploadLoading, setProdUploadLoading] = useState(false);
39
37
  const [requestSubmitted, setRequestSubmitted] = useState(false);
40
38
  const [showReasonForRequestModal, setShowReasonForRequestModal] = useState(false);
39
+ const [checkedIfCloneNeeded, setCheckedIfCloneNeeded] = useState(false);
41
40
  useEffect(() => {
42
41
  if (!metadata) {
43
42
  return;
44
43
  }
45
- const urlParams = new URLSearchParams(location.search);
46
- if (urlParams.get("clone") === "1") {
47
- const infoYamlString = localStorage.getItem("infoYamlToClone");
48
- const dttConfigsString = localStorage.getItem("dttConfigsToClone");
49
- if (!dttConfigsString) {
50
- return;
51
- }
44
+ // If the localStorage has been set, we should clone the configuration
45
+ const infoYamlString = localStorage.getItem("infoYamlToClone");
46
+ const dttConfigsString = localStorage.getItem("dttConfigsToClone");
47
+ if (dttConfigsString) {
52
48
  setProdUploadLoading(true);
53
49
  setRows([]);
54
50
  try {
@@ -69,7 +65,8 @@ export function RequestPage() {
69
65
  localStorage.removeItem("dttConfigsToClone");
70
66
  }
71
67
  }
72
- }, [metadata, location.search]);
68
+ setCheckedIfCloneNeeded(true);
69
+ }, [metadata]);
73
70
  const handlePrimaryAction = async () => {
74
71
  if (trySubmit()) {
75
72
  if (variant === "standalone") {
@@ -81,7 +78,7 @@ export function RequestPage() {
81
78
  }
82
79
  }
83
80
  };
84
- if (!metadata) {
81
+ if (!metadata || !checkedIfCloneNeeded) {
85
82
  return _jsx(LoadingIndicator, { height: "70vh" });
86
83
  }
87
84
  if (requestSubmitted) {
@@ -93,11 +90,11 @@ export function RequestPage() {
93
90
  setRequestSubmitted(true);
94
91
  onRequestSubmitted?.();
95
92
  }
96
- } })), showProdUploadModal && _jsx(UploadDttConfigModal, { onClose: () => setShowProdUploadModal(false) }), _jsxs("div", { className: "d-flex flex-column gap-3", children: [rows.map((row) => (_jsx(RowProvider, { row: row, children: _jsx(RequestRow, {}, row.id) }, row.id))), prodUploadLoading && _jsx(ConfigFilesUploadingAlert, {}), showErrors &&
93
+ } })), showProdUploadModal && _jsx(UploadDttConfigModal, { onClose: () => setShowProdUploadModal(false) }), _jsxs("div", { className: "d-flex flex-column gap-4", children: [rows.map((row) => (_jsx(RowProvider, { row: row, children: _jsx(RequestRow, {}, row.id) }, row.id))), prodUploadLoading && _jsx(ConfigFilesUploadingAlert, {}), showErrors &&
97
94
  (rows.length === 0 ||
98
95
  !validation.allRowsHaveDtt ||
99
96
  !validation.allRowsHaveStrippingLine ||
100
97
  !validation.allRowsHavePaths) && (_jsx(Alert, { variant: "danger", className: "mb-0", children: rows.length === 0
101
98
  ? "Please select at least one decay"
102
- : "Please name all DecayTreeTuples and select at least one stripping line and bookkeeping path for each decay" })), _jsx(Row, { children: _jsx(Col, { xs: "auto", children: _jsx(RequestButtonGroup, {}) }) }), _jsx(Row, { children: _jsxs(Col, { xs: 4, children: [_jsx(ProductionNameInput, {}), !emailIsKnown && _jsx(RequestEmailInput, {}), _jsxs(Stack, { direction: "horizontal", gap: 1, className: "mt-3 mb-3", children: [_jsxs(Button, { className: "align-items-center d-flex gap-1", onClick: () => void handlePrimaryAction(), children: [variant === "standalone" ? _jsx(Download, {}) : _jsx(Send, {}), variant === "standalone" ? "Download" : "Submit"] }), _jsx(DeleteButton, { action: clearAll, disabled: validation.isEmptySession, outline: undefined, children: "Clear all" })] })] }) })] })] }));
99
+ : "Please name all DecayTreeTuples and select at least one stripping line and bookkeeping path for each decay" })), _jsx(Row, { children: _jsx(Col, { xs: "auto", children: _jsx(RequestButtonGroup, {}) }) }), _jsx(Row, { children: _jsxs(Col, { xs: "auto", children: [_jsx(ProductionNameInput, {}), !emailIsKnown && _jsx(RequestEmailInput, {}), _jsxs(Stack, { direction: "horizontal", gap: 1, className: "mt-3 mb-3", children: [_jsxs(Button, { className: "align-items-center d-flex gap-1", onClick: () => void handlePrimaryAction(), children: [variant === "standalone" ? _jsx(Download, {}) : _jsx(Send, {}), variant === "standalone" ? "Download" : "Submit"] }), _jsx(DeleteButton, { action: clearAll, disabled: validation.isEmptySession, outline: undefined, children: "Clear all" })] })] }) })] })] }));
103
100
  }
@@ -12,6 +12,8 @@ interface DttContextType {
12
12
  dirty: boolean;
13
13
  save(this: void): void;
14
14
  revert(this: void): void;
15
+ selectedBranch: string[];
16
+ setSelectedBranch: (branch: string[]) => void;
15
17
  }
16
18
  interface DttProviderProps {
17
19
  initialConfig: DttConfig;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useReducer } from "react";
2
+ import { createContext, useCallback, useContext, useReducer, useState } from "react";
3
3
  import Dtt from "../models/dtt";
4
4
  const DttContext = createContext(null);
5
5
  function reducer(state, action) {
@@ -31,6 +31,7 @@ function reducer(state, action) {
31
31
  }
32
32
  }
33
33
  export function DttProvider({ initialConfig, decay, metadata, children }) {
34
+ const [selectedBranch, setSelectedBranch] = useState([]);
34
35
  const [state, dispatch] = useReducer(reducer, null, () => ({
35
36
  dtt: new Dtt(structuredClone(initialConfig), metadata.tupleTools.tupleTools),
36
37
  decay,
@@ -66,6 +67,8 @@ export function DttProvider({ initialConfig, decay, metadata, children }) {
66
67
  removeTool,
67
68
  save,
68
69
  revert,
70
+ selectedBranch,
71
+ setSelectedBranch,
69
72
  };
70
73
  return _jsx(DttContext.Provider, { value: value, children: children });
71
74
  }
@@ -23,18 +23,41 @@ const parseInput = (decay, input) => {
23
23
  versions: decay.lines[`${stream}/${line}`],
24
24
  };
25
25
  };
26
+ const getDttName = (value) => {
27
+ const basename = value.split("/").pop() ?? value;
28
+ return basename.replace(/\.py$/i, "");
29
+ };
30
+ const getConfigOrderFromInfoYaml = (infoYaml) => {
31
+ const order = new Map();
32
+ if (!infoYaml) {
33
+ return order;
34
+ }
35
+ for (const [key, jobConfig] of Object.entries(infoYaml)) {
36
+ if (key === "defaults") {
37
+ continue;
38
+ }
39
+ const job = jobConfig;
40
+ for (const option of job.options) {
41
+ const dttName = getDttName(option);
42
+ if (!order.has(dttName)) {
43
+ order.set(dttName, order.size);
44
+ }
45
+ }
46
+ }
47
+ return order;
48
+ };
26
49
  const addBkPathsToRows = (infoYaml, rows) => {
27
50
  for (const row of rows) {
28
51
  if (!row.dtt?.config.name) {
29
52
  continue;
30
53
  }
31
- const dttName = row.dtt.config.name.split("/")[1];
54
+ const dttName = getDttName(row.dtt.config.name);
32
55
  for (const [key, jobConfig] of Object.entries(infoYaml)) {
33
56
  if (key === "defaults") {
34
57
  continue;
35
58
  }
36
59
  const job = jobConfig;
37
- if (job.options.some((option) => option.split(".")[0] === dttName)) {
60
+ if (job.options.some((option) => getDttName(option) === dttName)) {
38
61
  row.paths.push(job.input.bk_query);
39
62
  }
40
63
  }
@@ -516,7 +539,17 @@ export async function parseProductionFiles(files, metadata) {
516
539
  export function processProductionFiles(metadata, configs, infoYaml) {
517
540
  const rows = [];
518
541
  const emailsToInform = [];
519
- for (const config of configs) {
542
+ const configOrder = getConfigOrderFromInfoYaml(infoYaml);
543
+ const orderedConfigs = [...configs].sort((a, b) => {
544
+ const aName = getDttName(a.name ?? "");
545
+ const bName = getDttName(b.name ?? "");
546
+ const aOrder = configOrder.get(aName) ?? Number.MAX_SAFE_INTEGER;
547
+ const bOrder = configOrder.get(bName) ?? Number.MAX_SAFE_INTEGER;
548
+ if (aOrder !== bOrder)
549
+ return aOrder - bOrder;
550
+ return aName.localeCompare(bName);
551
+ });
552
+ for (const config of orderedConfigs) {
520
553
  const decay = Object.values(metadata.decays).find((d) => d.descriptors.template === config.descriptorTemplate);
521
554
  if (!decay) {
522
555
  return `[${config.name}] Unknown decay '${config.descriptorTemplate}'`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lhcb-ntuple-wizard",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "An application to access large-scale open data from LHCb",
5
5
  "url": "https://gitlab.cern.ch/lhcb-dpa/wp6-analysis-preservation-and-open-data/lhcb-ntuple-wizard-frontend/issues",
6
6
  "private": false,
@@ -19,6 +19,8 @@
19
19
  "@mathjax/src": "4.1.0",
20
20
  "better-react-mathjax": "2.3.0",
21
21
  "bootstrap": "5.3.8",
22
+ "cytoscape": "3.33.1",
23
+ "cytoscape-dagre": "^2.5.0",
22
24
  "dompurify": "^3.3.1",
23
25
  "email-validator": "2.0.4",
24
26
  "js-yaml": "4.1.1",
@@ -29,9 +31,10 @@
29
31
  "pako": "2.1.0",
30
32
  "react-bootstrap": "2.10.10",
31
33
  "react-bootstrap-icons": "1.11.6",
32
- "react-graph-vis": "1.0.7",
34
+ "react-cytoscapejs": "^2.0.0",
33
35
  "react-infinite-scroll-component": "^6.1.1",
34
36
  "react-select": "5.10.2",
37
+ "react-toastify": "^11.0.5",
35
38
  "typia": "^11.0.3"
36
39
  },
37
40
  "peerDependencies": {
@@ -69,19 +72,21 @@
69
72
  "@babel/preset-react": "7.27.1",
70
73
  "@eslint/js": "^9.17.0",
71
74
  "@kennethwkz/unplugin-typia": "^2.6.7",
75
+ "@types/cytoscape-dagre": "^2.3.4",
72
76
  "@types/jest": "^30.0.0",
73
77
  "@types/js-yaml": "^4.0.9",
74
78
  "@types/lodash": "^4.17.21",
75
79
  "@types/lodash.memoize": "^4.1.9",
76
80
  "@types/pako": "^2.0.4",
77
81
  "@types/react": "^19.2.7",
82
+ "@types/react-cytoscapejs": "^1.2.6",
78
83
  "@types/react-dom": "^19.2.3",
79
84
  "@types/react-router-dom": "^5.3.3",
80
85
  "@types/vis": "^4.21.27",
81
- "@typescript-eslint/eslint-plugin": "^8.51.0",
82
- "@typescript-eslint/parser": "^8.51.0",
86
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
87
+ "@typescript-eslint/parser": "^8.56.0",
83
88
  "@vitejs/plugin-react": "5.1.2",
84
- "eslint": "^9.17.0",
89
+ "eslint": "^9.39.3",
85
90
  "eslint-plugin-react": "^7.37.5",
86
91
  "globals": "^16.0.0",
87
92
  "react": "^19.2.3",
@@ -1 +0,0 @@
1
- export declare function DttNameInput(): import("react/jsx-runtime").JSX.Element | null;