lhcb-ntuple-wizard-test 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ import { OptionProps } from "react-select";
2
+ export interface SelectOption {
3
+ value: string;
4
+ label: string;
5
+ description?: string;
6
+ }
7
+ export declare function DropdownOptionWithDesc({ data, innerRef, innerProps, isFocused, isSelected }: OptionProps<SelectOption>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function DropdownOptionWithDesc({ data, innerRef, innerProps, isFocused, isSelected }) {
3
+ return (_jsxs("div", { ref: innerRef, ...innerProps, style: {
4
+ backgroundColor: isSelected ? "#0d6efd" : isFocused ? "#e9ecef" : "white",
5
+ color: isSelected ? "white" : "inherit",
6
+ padding: "6px 12px",
7
+ cursor: "pointer",
8
+ }, children: [data.label, isFocused && data.description && (_jsx("div", { style: {
9
+ fontSize: "0.75em",
10
+ color: isSelected ? "rgba(255,255,255,0.75)" : "#6c757d",
11
+ marginTop: 2,
12
+ whiteSpace: "normal",
13
+ lineHeight: 1.3,
14
+ }, children: data.description }))] }));
15
+ }
@@ -1,18 +1,3 @@
1
- /*****************************************************************************\
2
- * (c) Copyright 2021 CERN for the benefit of the LHCb Collaboration *
3
- * *
4
- * This software is distributed under the terms of the GNU General Public *
5
- * Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING". *
6
- * *
7
- * In applying this licence, CERN does not waive the privileges and immunities *
8
- * granted to it by virtue of its status as an Intergovernmental Organization *
9
- * or submit itself to any jurisdiction. *
10
- \*****************************************************************************/
11
1
  import { GroupBase, Props as SelectProps } from "react-select";
12
- import { ReactNode } from "react";
13
- type ToolDropdownOption = {
14
- value: string;
15
- label: ReactNode;
16
- };
17
- export declare function TupleToolClassDropdown(props: SelectProps<ToolDropdownOption, false, GroupBase<ToolDropdownOption>>): import("react/jsx-runtime").JSX.Element;
18
- export {};
2
+ import { SelectOption } from "./DropdownOptionWithDesc";
3
+ export declare function TupleToolClassDropdown(props: SelectProps<SelectOption, false, GroupBase<SelectOption>>): import("react/jsx-runtime").JSX.Element;
@@ -9,24 +9,75 @@ import { jsx as _jsx } from "react/jsx-runtime";
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 { useDeferredValue, useMemo, useState } from "react";
12
13
  import Select from "react-select";
14
+ import Fuse from "fuse.js";
13
15
  import { useMetadata } from "../providers/MetadataProvider.js";
14
16
  import { TupleTool } from "../models/tupleTool";
15
17
  import { useDtt } from "../providers/DttProvider";
16
18
  import { config } from "../config";
19
+ import { DropdownOptionWithDesc } from "./DropdownOptionWithDesc";
20
+ function flattenVariableEntries(vars) {
21
+ const entries = [];
22
+ for (const [key, value] of Object.entries(vars)) {
23
+ if (typeof value === "string") {
24
+ entries.push(value ? `${key}: ${value}` : key);
25
+ }
26
+ else if (value !== null && typeof value === "object") {
27
+ entries.push(...flattenVariableEntries(value));
28
+ }
29
+ }
30
+ return entries;
31
+ }
17
32
  export function TupleToolClassDropdown(props) {
18
33
  const metadata = useMetadata();
19
34
  const { selectedBranch } = useDtt();
20
- const allowedTags = ["IParticleTupleTool", ...(selectedBranch.length === 0 ? ["IEventTupleTool"] : [])];
21
- // Group tools by tag
22
- const options = allowedTags.map((allowedTag) => ({
23
- label: allowedTag,
24
- options: Object.keys(metadata.tupleTools.tupleTools)
25
- .sort()
26
- .map(TupleTool.fromString)
27
- .map((tool) => ({ value: tool.toString(), label: tool.class }))
28
- .filter((tool) => metadata.tupleTools.tupleTools[tool.value].tags.includes(allowedTag) &&
29
- !config.disabledTupleTools.includes(tool.value)),
30
- }));
31
- return (_jsx("div", { className: "react-select form-control p-0", children: _jsx(Select, { placeholder: "TupleTool class", options: options, ...props }) }));
35
+ const [inputValue, setInputValue] = useState("");
36
+ const deferredInput = useDeferredValue(inputValue);
37
+ const allowedTags = useMemo(() => ["IParticleTupleTool", ...(selectedBranch.length === 0 ? ["IEventTupleTool"] : [])], [selectedBranch.length]);
38
+ const allTools = useMemo(() => Object.keys(metadata.tupleTools.tupleTools)
39
+ .sort()
40
+ .map(TupleTool.fromString)
41
+ .map((tool) => ({
42
+ value: tool.toString(),
43
+ label: tool.class,
44
+ description: metadata.tupleTools.tupleTools[tool.toString()].description,
45
+ variables: metadata.kgdoc[tool.class]
46
+ ? flattenVariableEntries(metadata.kgdoc[tool.class].variables)
47
+ : [],
48
+ }))
49
+ .filter((tool) => metadata.tupleTools.tupleTools[tool.value].tags.some((tag) => allowedTags.includes(tag)) &&
50
+ !config.disabledTupleTools.includes(tool.value)), [metadata, allowedTags]);
51
+ const groupedOptions = useMemo(() => allowedTags.map((tag) => ({
52
+ label: tag,
53
+ options: allTools.filter((tool) => metadata.tupleTools.tupleTools[tool.value].tags.includes(tag)),
54
+ })), [allTools, allowedTags, metadata]);
55
+ const fuse = useMemo(() => new Fuse(allTools, {
56
+ keys: [
57
+ { name: "label", weight: 2 },
58
+ { name: "description", weight: 1 },
59
+ { name: "variables", weight: 0.5 },
60
+ ],
61
+ includeMatches: true,
62
+ threshold: 0.4,
63
+ minMatchCharLength: 2,
64
+ shouldSort: true,
65
+ }), [allTools]);
66
+ const displayOptions = useMemo(() => {
67
+ if (!deferredInput)
68
+ return groupedOptions;
69
+ return fuse.search(deferredInput, { limit: 50 }).map((r) => {
70
+ const matchedVars = (r.matches ?? [])
71
+ .filter((m) => m.key === "variables")
72
+ .map((m) => m.value)
73
+ .filter(Boolean)
74
+ .join(", ");
75
+ return matchedVars ? { ...r.item, description: matchedVars } : r.item;
76
+ });
77
+ }, [deferredInput, fuse, groupedOptions]);
78
+ return (_jsx("div", { className: "react-select form-control p-0", children: _jsx(Select, { placeholder: "Search by name, description, or variables...", styles: { control: (base) => ({ ...base, border: 0, boxShadow: "none" }) }, ...props, options: displayOptions, inputValue: inputValue, onInputChange: (val, { action }) => {
79
+ if (action !== "input-blur" && action !== "menu-close") {
80
+ setInputValue(val);
81
+ }
82
+ }, filterOption: () => true, components: { Option: DropdownOptionWithDesc } }) }));
32
83
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, ButtonGroup, FormControl, Modal } from "react-bootstrap";
2
+ import { Button, ButtonGroup, Form, FormControl, Modal } from "react-bootstrap";
3
3
  import { TupleToolClassDropdown } from "../TupleToolClassDropdown";
4
4
  import { TupleToolDocsAccordion } from "../TupleToolDocsAccordion";
5
5
  import { useDtt } from "../../providers/DttProvider";
@@ -13,20 +13,25 @@ export function AddTupleToolModal({ onClose }) {
13
13
  useEffect(() => {
14
14
  nameInputRef.current?.focus();
15
15
  }, [selectedTool]);
16
- return (_jsxs(Modal, { show: true, onHide: () => onClose(null), size: "lg", children: [_jsx(Modal.Header, { closeButton: true, children: _jsx(Modal.Title, { children: "Add TupleTool" }) }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [selectedBranch.length > 0 && (_jsxs("div", { className: "d-flex flex-row gap-2", children: [_jsx("h6", { className: "my-auto", children: "Target:" }), selectedBranch.map((pid, i) => (_jsx(ParticleLatex, { particleId: pid }, i)))] })), _jsxs("div", { className: "d-flex flex-row gap-2", children: [_jsx(TupleToolClassDropdown, { onChange: (option) => {
17
- if (option) {
18
- setSelectedTool((prev) => new TupleTool(option.value, prev?.name || ""));
19
- }
20
- }, value: selectedTool ? { value: selectedTool.class, label: selectedTool.class } : null }), _jsx(FormControl, { ref: nameInputRef, placeholder: "TupleTool name", onChange: (event) => {
21
- // Remove all non-word characters
22
- const name = event.target.value.replaceAll(/[^\w]/g, "");
23
- setSelectedTool((prev) => new TupleTool(prev.class, name));
24
- }, value: selectedTool?.name || "", disabled: !selectedTool, isInvalid: !!selectedTool && dtt.toolExists(selectedTool, selectedBranch), onKeyDown: (event) => {
25
- if (event.key === "Enter") {
26
- addTool(selectedBranch, selectedTool);
27
- onClose(selectedTool);
28
- }
29
- } }), 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: () => {
16
+ return (_jsxs(Modal, { show: true, onHide: () => onClose(null), size: "lg", children: [_jsx(Modal.Header, { closeButton: true, children: _jsx(Modal.Title, { children: "Add TupleTool" }) }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [selectedBranch.length > 0 && (_jsxs("div", { className: "d-flex flex-row gap-2", children: [_jsx("h6", { className: "my-auto", children: "Target:" }), selectedBranch.map((pid, i) => (_jsx(ParticleLatex, { particleId: pid }, i)))] })), _jsxs("div", { className: "d-flex flex-column gap-1", children: [_jsxs("div", { className: "d-flex flex-row gap-2", children: [_jsxs(Form.Group, { className: "w-100", children: [_jsx(Form.Label, { className: "fw-bold", children: "Class" }), _jsx(TupleToolClassDropdown, { isClearable: true, onChange: (option) => {
17
+ if (option) {
18
+ setSelectedTool((prev) => new TupleTool(option.value, prev?.name || ""));
19
+ }
20
+ else {
21
+ setSelectedTool(null);
22
+ }
23
+ }, value: selectedTool ? { value: selectedTool.class, label: selectedTool.class } : null })] }), _jsxs(Form.Group, { className: "w-100", children: [_jsx(Form.Label, { className: "fw-bold", children: "Name (optional)" }), _jsx(FormControl, { ref: nameInputRef, placeholder: "e.g. MyTool", onChange: (event) => {
24
+ // Remove all non-word characters
25
+ const name = event.target.value.replaceAll(/[^\w]/g, "");
26
+ setSelectedTool((prev) => new TupleTool(prev.class, name));
27
+ }, value: selectedTool?.name || "", disabled: !selectedTool, isInvalid: !!selectedTool &&
28
+ !!selectedTool.name &&
29
+ dtt.toolExists(selectedTool, selectedBranch), onKeyDown: (event) => {
30
+ if (event.key === "Enter") {
31
+ addTool(selectedBranch, selectedTool);
32
+ onClose(selectedTool);
33
+ }
34
+ } })] })] }), selectedTool && selectedTool.name && dtt.toolExists(selectedTool, selectedBranch) && (_jsxs(FormControl.Feedback, { type: "invalid", className: "d-block", 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: () => {
30
35
  addTool(selectedBranch, selectedTool);
31
36
  onClose(selectedTool);
32
37
  }, children: "Add tool" })] })] })] }));
@@ -5,13 +5,17 @@ import { BoolParamInput } from "../tupleToolParams/BoolParamInput";
5
5
  import { NumParamInput } from "../tupleToolParams/NumParamInput";
6
6
  import { ListParamInput } from "../tupleToolParams/ListParamInput";
7
7
  import { DictParamInput } from "../tupleToolParams/DictParamInput";
8
+ import { LokiVariableDictInput } from "../tupleToolParams/LokiVariableDictInput";
8
9
  import { QuestionCircle } from "react-bootstrap-icons";
9
10
  import { TupleToolDocsAccordion } from "../TupleToolDocsAccordion";
10
11
  import { useDtt } from "../../providers/DttProvider";
12
+ import { useMetadata } from "../../providers/MetadataProvider";
11
13
  import { config } from "../../config";
12
14
  import { toast } from "react-toastify";
15
+ const LOKI_VARIABLE_PARAMS = new Set(["Variables", "BoolVariables", "FloatVariables", "IntVariables"]);
13
16
  export function ConfigureTupleToolModal({ tool, deleteIfCancelled, onClose }) {
14
17
  const { dtt, updateToolParam, removeTool, selectedBranch } = useDtt();
18
+ const { lokiVariables } = useMetadata();
15
19
  const toolConfig = dtt.getToolConfig(selectedBranch, tool);
16
20
  return (_jsxs(Modal, { show: true, size: "xl", fullscreen: "lg-down", backdrop: "static", keyboard: false, children: [_jsx(Modal.Header, { children: _jsxs(Modal.Title, { style: { textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap" }, children: ["Configure ", tool.toString()] }) }), _jsxs(Modal.Body, { className: "gap-3 d-flex flex-column", children: [_jsx(Form, { children: Object.entries(toolConfig).map(([paramName, param]) => {
17
21
  const { description, type, default: defaultValue, value } = param;
@@ -37,6 +41,13 @@ export function ConfigureTupleToolModal({ tool, deleteIfCancelled, onClose }) {
37
41
  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 })) }));
38
42
  break;
39
43
  case "{str:str}":
44
+ if (tool.class === "LoKi__Hybrid__TupleTool" && LOKI_VARIABLE_PARAMS.has(paramName)) {
45
+ inputComponent = (_jsx(LokiVariableDictInput, { value: value, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue), lokiVariables: lokiVariables.lokiVariables }));
46
+ }
47
+ else {
48
+ inputComponent = (_jsx(DictParamInput, { value: value, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue) }));
49
+ }
50
+ break;
40
51
  case "{str:[str]}":
41
52
  inputComponent = (_jsx(DictParamInput, { value: value, onChange: (newValue) => updateToolParam(selectedBranch, tool, paramName, newValue) }));
42
53
  break;
@@ -0,0 +1,14 @@
1
+ import { LokiVariable } from "../../providers/MetadataProvider";
2
+ interface Props {
3
+ value: {
4
+ [key: string]: string;
5
+ };
6
+ onChange: (value: {
7
+ [key: string]: string;
8
+ }) => void;
9
+ lokiVariables: {
10
+ [variableName: string]: LokiVariable;
11
+ };
12
+ }
13
+ export declare function LokiVariableDictInput({ value, onChange, lokiVariables }: Props): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useDeferredValue, useMemo, useState } from "react";
3
+ import { Button, CloseButton, Form, ListGroup } from "react-bootstrap";
4
+ import { PlusLg } from "react-bootstrap-icons";
5
+ import Select from "react-select";
6
+ import Fuse from "fuse.js";
7
+ import { DropdownOptionWithDesc } from "../DropdownOptionWithDesc";
8
+ function LokiValueSelect({ value, onChange, allOptions, fuse }) {
9
+ const [inputValue, setInputValue] = useState("");
10
+ const deferredInput = useDeferredValue(inputValue);
11
+ const filteredOptions = useMemo(() => (deferredInput ? fuse.search(deferredInput, { limit: 50 }).map((r) => r.item) : allOptions), [deferredInput, fuse, allOptions]);
12
+ return (_jsx(Select, { options: filteredOptions, value: allOptions.find((o) => o.value === value) ?? null, inputValue: inputValue, onInputChange: (val, { action }) => {
13
+ if (action !== "input-blur" && action !== "menu-close") {
14
+ setInputValue(val);
15
+ }
16
+ }, filterOption: () => true, onChange: (opt) => onChange(opt?.value ?? ""), placeholder: "LoKi variable...", isClearable: true, components: { Option: DropdownOptionWithDesc } }));
17
+ }
18
+ const entriesToDict = (entries) => Object.fromEntries(entries.map(({ key, value }) => [key, value]));
19
+ export function LokiVariableDictInput({ value, onChange, lokiVariables }) {
20
+ const [entries, setEntries] = useState(Object.entries(value).map(([k, v]) => ({ key: k, value: v })));
21
+ const allOptions = useMemo(() => Object.keys(lokiVariables)
22
+ .sort()
23
+ .map((name) => ({ value: name, label: name, description: lokiVariables[name].description })), [lokiVariables]);
24
+ const fuse = useMemo(() => new Fuse(allOptions, {
25
+ keys: [
26
+ { name: "label", weight: 2 },
27
+ { name: "description", weight: 1 },
28
+ ],
29
+ threshold: 0.4,
30
+ minMatchCharLength: 2,
31
+ shouldSort: true,
32
+ }), [allOptions]);
33
+ const update = (newEntries) => {
34
+ setEntries(newEntries);
35
+ onChange(entriesToDict(newEntries));
36
+ };
37
+ const handleAdd = () => update([...entries, { key: "", value: "" }]);
38
+ const handleRemove = (index) => update(entries.filter((_, i) => i !== index));
39
+ const handleKeyChange = (index, key) => update(entries.map((entry, i) => (i === index ? { ...entry, key } : entry)));
40
+ const handleValueChange = (index, val) => update(entries.map((entry, i) => (i === index ? { ...entry, value: val } : entry)));
41
+ return (_jsxs(_Fragment, { children: [_jsx(ListGroup, { variant: "flush", children: entries.map(({ key, value: entryValue }, i) => (_jsx(ListGroup.Item, { className: "px-0", children: _jsxs("div", { className: "d-flex gap-2 align-items-center", children: [_jsx(Form.Control, { type: "text", value: key, placeholder: "Branch name", style: { flex: "0 0 35%" }, onChange: (e) => handleKeyChange(i, e.target.value) }), _jsx("div", { style: { flex: 1 }, children: _jsx(LokiValueSelect, { value: entryValue, onChange: (val) => handleValueChange(i, val), allOptions: allOptions, fuse: fuse }) }), _jsx(CloseButton, { onClick: () => handleRemove(i) })] }) }, i))) }), _jsx(Button, { variant: "success", size: "sm", onClick: handleAdd, className: "mt-1", children: _jsx(PlusLg, {}) })] }));
42
+ }
package/dist/config.js CHANGED
@@ -46,11 +46,13 @@ export const config = {
46
46
  tupleToolValidation: {
47
47
  ["LoKi__Hybrid__TupleTool"]: {
48
48
  validate: (config) => {
49
- const hasVariables = Object.entries(config["Variables"].value).length > 0;
50
- const hasBoolVariables = Object.entries(config["BoolVariables"].value).length > 0;
51
- const hasFloatVariables = Object.entries(config["FloatVariables"].value).length > 0;
52
- const hasIntVariables = Object.entries(config["IntVariables"].value).length > 0;
53
- if (!hasVariables && !hasBoolVariables && !hasFloatVariables && !hasIntVariables) {
49
+ const paramNames = ["Variables", "BoolVariables", "FloatVariables", "IntVariables"];
50
+ const allEntries = paramNames.flatMap((name) => Object.entries(config[name].value));
51
+ const hasIncomplete = allEntries.some(([key, value]) => !key || !value);
52
+ if (hasIncomplete) {
53
+ return "All variable entries must have both a branch name and a LoKi variable selected";
54
+ }
55
+ if (allEntries.length === 0) {
54
56
  return "Please add at least one variable";
55
57
  }
56
58
  return null;
@@ -39,6 +39,7 @@ export interface ToolMetadata {
39
39
  description: string;
40
40
  documentation: string;
41
41
  interface: ToolParamMetadata<keyof ToolParamTypeMap>[];
42
+ summary: string;
42
43
  tags: string[];
43
44
  };
44
45
  }
@@ -17,14 +17,15 @@ export default class Dtt {
17
17
  static formatToolsForLoading(tools, toolMetadata) {
18
18
  return Object.fromEntries(tools
19
19
  .map((toolConfig) => {
20
- const toolClass = Object.keys(toolConfig)[0];
21
- const params = toolConfig[toolClass];
20
+ const toolString = Object.keys(toolConfig)[0];
21
+ const params = toolConfig[toolString];
22
+ const toolClass = toolString.split("/")[0];
22
23
  const toolInterface = toolMetadata[toolClass]?.interface;
23
24
  if (!toolInterface) {
24
25
  return null;
25
26
  }
26
27
  return [
27
- toolClass,
28
+ toolString,
28
29
  Object.fromEntries(Object.entries(params)
29
30
  .map(([paramName, value]) => {
30
31
  const paramMetadata = toolInterface.find((param) => param.name === paramName);
@@ -40,7 +41,8 @@ export default class Dtt {
40
41
  }
41
42
  static fromSavedConfig(savedConfig, toolMetadata) {
42
43
  const config = {
43
- ...savedConfig,
44
+ input: savedConfig.inputs?.length ? savedConfig.inputs[0] : undefined,
45
+ descriptorTemplate: savedConfig.descriptorTemplate,
44
46
  tools: Dtt.formatToolsForLoading(savedConfig.tools, toolMetadata),
45
47
  branches: Object.fromEntries(Object.entries(savedConfig.branches).map(([branchId, branchConfig]) => [
46
48
  branchId,
@@ -56,12 +58,14 @@ export default class Dtt {
56
58
  tools: Dtt.formatToolsForLoading(groupConfig.tools, toolMetadata),
57
59
  },
58
60
  ])),
61
+ name: savedConfig.name,
59
62
  };
60
63
  return new Dtt(config, {});
61
64
  }
62
65
  toSavedConfig() {
63
66
  return {
64
- ...this.config,
67
+ inputs: this.config.input ? [this.config.input] : undefined,
68
+ descriptorTemplate: this.config.descriptorTemplate,
65
69
  tools: Dtt.formatToolsForSaving(this.config.tools),
66
70
  branches: Object.fromEntries(Object.entries(this.config.branches).map(([branchId, branchConfig]) => [
67
71
  branchId,
@@ -77,6 +81,7 @@ export default class Dtt {
77
81
  tools: Dtt.formatToolsForSaving(groupConfig.tools),
78
82
  },
79
83
  ])),
84
+ name: this.config.name,
80
85
  };
81
86
  }
82
87
  static getBranchConfigs(branchItemsList) {
@@ -1,6 +1,6 @@
1
1
  import Dtt from "./dtt";
2
2
  import { RowData } from "./rowData";
3
- import { MetadataContext } from "../providers/MetadataProvider";
3
+ import { Metadata } from "../providers/MetadataProvider";
4
4
  import { JobConfig } from "./jobConfig";
5
5
  interface InfoYamlDefaults {
6
6
  application: string;
@@ -19,6 +19,6 @@ export declare class YamlFile {
19
19
  content: string;
20
20
  constructor(name: string, content: string);
21
21
  static fromDtt(dtt: Dtt): YamlFile;
22
- static createInfoYaml(rows: RowData[], metadata: MetadataContext): YamlFile;
22
+ static createInfoYaml(rows: RowData[], metadata: Metadata): YamlFile;
23
23
  }
24
24
  export {};
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from "react";
2
2
  import Dtt, { DttConfig, ToolParamTypeMap } from "../models/dtt";
3
- import { MetadataContext } from "./MetadataProvider";
3
+ import { Metadata } from "./MetadataProvider";
4
4
  import { DecayData } from "../models/decayData";
5
5
  import { TupleTool } from "../models/tupleTool";
6
6
  interface DttContextType {
@@ -18,7 +18,7 @@ interface DttContextType {
18
18
  interface DttProviderProps {
19
19
  initialConfig: DttConfig;
20
20
  decay: DecayData;
21
- metadata: MetadataContext;
21
+ metadata: Metadata;
22
22
  children: ReactNode;
23
23
  }
24
24
  export declare function DttProvider({ initialConfig, decay, metadata, children }: DttProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -37,7 +37,58 @@ interface StrippingInfo {
37
37
  [key: string]: string;
38
38
  };
39
39
  }
40
- export interface MetadataContext {
40
+ export interface LokiVariable {
41
+ description: string;
42
+ documentation: string;
43
+ source: string;
44
+ }
45
+ interface KgDocItem {
46
+ variables: {
47
+ [key: string]: string | object;
48
+ };
49
+ }
50
+ export interface Metadata {
51
+ dataset: string[];
52
+ decays: {
53
+ [key: string]: DecayData;
54
+ };
55
+ kgdoc: {
56
+ [key: string]: KgDocItem;
57
+ };
58
+ lokiVariables: {
59
+ applicationInfo: ApplicationInfo;
60
+ lokiVariables: {
61
+ [key: string]: LokiVariable;
62
+ };
63
+ };
64
+ particleProperties: {
65
+ [key: string]: Particle;
66
+ };
67
+ stripping: {
68
+ [key: string]: {
69
+ [key: string]: StrippingInfo;
70
+ };
71
+ };
72
+ strippingHints: {
73
+ [key: string]: {
74
+ davinci: string;
75
+ description: string;
76
+ };
77
+ };
78
+ tupleTools: {
79
+ applicationInfo: ApplicationInfo;
80
+ tupleTools: ToolMetadata;
81
+ };
82
+ userHints: {
83
+ decayTags: {
84
+ [key: string]: DecayTagHint;
85
+ };
86
+ particleTags: {
87
+ [key: string]: ParticleTagHint;
88
+ };
89
+ };
90
+ }
91
+ export interface MetadataSchema {
41
92
  dataset: string[];
42
93
  decays: {
43
94
  [key: string]: DecayData;
@@ -46,11 +97,13 @@ export interface MetadataContext {
46
97
  [key: string]: string;
47
98
  };
48
99
  kgdoc: {
49
- [key: string]: object;
100
+ [key: string]: KgDocItem;
50
101
  };
51
102
  lokiVariables: {
52
103
  applicationInfo: ApplicationInfo;
53
- lokiVariables: object;
104
+ lokiVariables: {
105
+ [key: string]: LokiVariable;
106
+ };
54
107
  };
55
108
  particleProperties: {
56
109
  [key: string]: Particle;
@@ -83,5 +136,5 @@ interface Props {
83
136
  children: ReactNode;
84
137
  }
85
138
  export declare function MetadataProvider({ children }: Props): import("react/jsx-runtime").JSX.Element;
86
- export declare const useMetadata: () => MetadataContext;
139
+ export declare const useMetadata: () => Metadata;
87
140
  export {};
@@ -32,10 +32,27 @@ export function MetadataProvider({ children }) {
32
32
  const metadataContextKeys = Object.keys(config.metadata_files);
33
33
  // Load all metadata files
34
34
  const promises = metadataContextKeys.map(async (key) => [key, await loadDict(config.metadata_files[key])]);
35
- // Build the context object
35
+ // Build the Metadata object
36
36
  Promise.all(promises)
37
37
  .then((entries) => {
38
- setMetadata(Object.fromEntries(entries));
38
+ const m = Object.fromEntries(entries);
39
+ setMetadata({
40
+ dataset: m.dataset,
41
+ decays: m.decays,
42
+ kgdoc: m.kgdoc,
43
+ lokiVariables: m.lokiVariables,
44
+ particleProperties: m.particleProperties,
45
+ stripping: m.stripping,
46
+ strippingHints: m.strippingHints,
47
+ tupleTools: {
48
+ applicationInfo: m.tupleTools?.applicationInfo ?? {},
49
+ tupleTools: Object.fromEntries(Object.entries(m.tupleTools?.tupleTools ?? {}).map(([key, tool]) => [
50
+ key,
51
+ { ...tool, summary: m.embedding?.[key] ?? "" },
52
+ ])),
53
+ },
54
+ userHints: m.userHints,
55
+ });
39
56
  })
40
57
  .catch(setError);
41
58
  }, []);
@@ -63,7 +63,8 @@ const diverseToolMetadata = {
63
63
  ...mockToolMetadata,
64
64
  DiverseTool: {
65
65
  description: "Tool with diverse param types",
66
- documentation: "",
66
+ documentation: "No documentation available.",
67
+ summary: "",
67
68
  tags: [],
68
69
  interface: [
69
70
  { name: "StrParam", type: "str", cpp_type: "string", default: "hello", description: "A string param" },
@@ -185,7 +186,7 @@ describe("AddTupleToolModal", () => {
185
186
  it("sanitizes the tool name on input and reflects the value", () => {
186
187
  render(_jsx(AddTupleToolModal, { onClose: vi.fn() }));
187
188
  fireEvent.click(screen.getByTestId("tool-class-dropdown"));
188
- const nameInput = screen.getByPlaceholderText("TupleTool name");
189
+ const nameInput = screen.getByPlaceholderText("e.g. MyTool");
189
190
  fireEvent.change(nameInput, { target: { value: "my tool!" } });
190
191
  expect(nameInput).toHaveValue("mytool");
191
192
  });
@@ -194,7 +195,7 @@ describe("AddTupleToolModal", () => {
194
195
  const onClose = vi.fn();
195
196
  render(_jsx(AddTupleToolModal, { onClose: onClose }));
196
197
  fireEvent.click(screen.getByTestId("tool-class-dropdown"));
197
- const nameInput = screen.getByPlaceholderText("TupleTool name");
198
+ const nameInput = screen.getByPlaceholderText("e.g. MyTool");
198
199
  fireEvent.keyDown(nameInput, { key: "Enter" });
199
200
  expect(mockAddTool).toHaveBeenCalled();
200
201
  expect(onClose).toHaveBeenCalledWith(expect.any(TupleTool));
@@ -1,5 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
+ import yaml from "js-yaml";
2
3
  import Dtt from "../../models/dtt";
4
+ // @ts-expect-error this fixture import shows an error for some reason
5
+ // but doesn't cause any problems in practice
6
+ import fixtureYaml from "../fixtures/example_dtt.yaml?raw";
3
7
  import { TupleTool } from "../../models/tupleTool";
4
8
  import { mockBranchItems, mockToolMetadata } from "../testUtils";
5
9
  const DEFAULT_TOOLS = [
@@ -374,3 +378,145 @@ describe("Dtt", () => {
374
378
  });
375
379
  });
376
380
  });
381
+ // ---------------------------------------------------------------------------
382
+ // Fixture-based tests
383
+ // ---------------------------------------------------------------------------
384
+ const fixtureParsed = yaml.load(fixtureYaml);
385
+ const fixtureToolMetadata = {
386
+ TupleToolKinematic: {
387
+ description: "",
388
+ documentation: "No documentation available.",
389
+ summary: "",
390
+ tags: [],
391
+ interface: [
392
+ { name: "ExtraName", type: "str", cpp_type: "std::string", default: "", description: "" },
393
+ { name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "" },
394
+ { name: "MaxPV", type: "uint", cpp_type: "unsigned int", default: 100, description: "" },
395
+ {
396
+ name: "Transporter",
397
+ type: "str",
398
+ cpp_type: "std::string",
399
+ default: "ParticleTransporter:PUBLIC",
400
+ description: "",
401
+ },
402
+ ],
403
+ },
404
+ TupleToolPid: {
405
+ description: "",
406
+ documentation: "No documentation available.",
407
+ summary: "",
408
+ tags: [],
409
+ interface: [
410
+ { name: "ExtraName", type: "str", cpp_type: "std::string", default: "", description: "" },
411
+ { name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "" },
412
+ { name: "MaxPV", type: "uint", cpp_type: "unsigned int", default: 100, description: "" },
413
+ ],
414
+ },
415
+ TupleToolANNPID: {
416
+ description: "",
417
+ documentation: "No documentation available.",
418
+ summary: "",
419
+ tags: [],
420
+ interface: [
421
+ { name: "ExtraName", type: "str", cpp_type: "std::string", default: "", description: "" },
422
+ { name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "" },
423
+ { name: "MaxPV", type: "uint", cpp_type: "unsigned int", default: 100, description: "" },
424
+ {
425
+ name: "ANNPIDTunes",
426
+ type: "text",
427
+ cpp_type: "std::vector<std::string>",
428
+ default: ["MC12TuneV2", "MC12TuneV3", "MC12TuneV4", "MC15TuneV1"],
429
+ description: "",
430
+ },
431
+ {
432
+ name: "PIDTypes",
433
+ type: "text",
434
+ cpp_type: "std::vector<std::string>",
435
+ default: ["Electron", "Muon", "Pion", "Kaon", "Proton", "Ghost"],
436
+ description: "",
437
+ },
438
+ ],
439
+ },
440
+ TupleToolAngles: {
441
+ description: "",
442
+ documentation: "No documentation available.",
443
+ summary: "",
444
+ tags: [],
445
+ interface: [
446
+ { name: "ExtraName", type: "str", cpp_type: "std::string", default: "", description: "" },
447
+ { name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "" },
448
+ { name: "MaxPV", type: "uint", cpp_type: "unsigned int", default: 100, description: "" },
449
+ { name: "WRTMother", type: "bool", cpp_type: "bool", default: false, description: "" },
450
+ ],
451
+ },
452
+ TupleToolCorrectedMass: {
453
+ description: "",
454
+ documentation: "No documentation available.",
455
+ summary: "",
456
+ tags: [],
457
+ interface: [
458
+ { name: "ExtraName", type: "str", cpp_type: "std::string", default: "", description: "" },
459
+ { name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "" },
460
+ { name: "MaxPV", type: "uint", cpp_type: "unsigned int", default: 100, description: "" },
461
+ { name: "MassInvisible", type: "float", cpp_type: "double", default: 0, description: "" },
462
+ ],
463
+ },
464
+ };
465
+ describe("fromSavedConfig() with fixture", () => {
466
+ let dtt;
467
+ dtt = Dtt.fromSavedConfig(fixtureParsed, fixtureToolMetadata);
468
+ it("loads the correct branch IDs and particle assignments", () => {
469
+ expect(Object.keys(dtt.config.branches).sort()).toEqual(["eminus", "eplus", "gamma"]);
470
+ expect(dtt.config.branches["gamma"].particle).toBe("gamma");
471
+ expect(dtt.config.branches["eplus"].particle).toBe("e+");
472
+ expect(dtt.config.branches["eminus"].particle).toBe("e-");
473
+ });
474
+ it("loads the descriptor template", () => {
475
+ expect(dtt.config.descriptorTemplate).toBe("${gamma}gamma -> ${eplus}e+ ${eminus}e-");
476
+ });
477
+ it("loads the name", () => {
478
+ expect(dtt.config.name).toBe("DecayTreeTuple/MyDecayTree0");
479
+ });
480
+ it("loads the input location", () => {
481
+ expect(dtt.config.input).toBe("/Event/Leptonic/Phys/BuToKJpsiee2Line/Particles");
482
+ });
483
+ it("loads global tools by class", () => {
484
+ expect(dtt.toolExists(TupleTool.fromString("TupleToolKinematic"))).toBe(true);
485
+ expect(dtt.toolExists(TupleTool.fromString("TupleToolPid"))).toBe(true);
486
+ expect(dtt.toolExists(TupleTool.fromString("TupleToolANNPID"))).toBe(true);
487
+ });
488
+ it("loads a named tool instance", () => {
489
+ expect(dtt.toolExists(TupleTool.fromString("TupleToolAngles/myNamedTool"))).toBe(true);
490
+ });
491
+ it("loads a modified string param (TupleToolPid ExtraName)", () => {
492
+ expect(dtt.config.tools["TupleToolPid"]["ExtraName"].value).toBe("myExtraName");
493
+ });
494
+ it("loads a string param (TupleToolKinematic Transporter)", () => {
495
+ expect(dtt.config.tools["TupleToolKinematic"]["Transporter"].value).toBe("ParticleTransporter:PUBLIC");
496
+ });
497
+ it("loads an array param (TupleToolANNPID ANNPIDTunes)", () => {
498
+ expect(dtt.config.tools["TupleToolANNPID"]["ANNPIDTunes"].value).toEqual([
499
+ "MC12TuneV2",
500
+ "MC12TuneV3",
501
+ "MC12TuneV4",
502
+ "MC15TuneV1",
503
+ ]);
504
+ });
505
+ it("loads a bool param on a named tool (TupleToolAngles/myNamedTool WRTMother)", () => {
506
+ expect(dtt.config.tools["TupleToolAngles/myNamedTool"]["WRTMother"].value).toBe(true);
507
+ });
508
+ it("loads the group key and its particle list", () => {
509
+ expect(dtt.config.groups["eminus,gamma"].particles).toEqual(["e-", "gamma"]);
510
+ });
511
+ it("loads the named group tool and its params", () => {
512
+ const groupTool = dtt.config.groups["eminus,gamma"].tools["TupleToolCorrectedMass/myGroupTool"];
513
+ expect(groupTool["MassInvisible"].value).toBe(0);
514
+ expect(groupTool["MaxPV"].value).toBe(100);
515
+ });
516
+ });
517
+ describe("toSavedConfig() roundtrip with fixture", () => {
518
+ it("fromSavedConfig -> toSavedConfig reproduces the fixture exactly", () => {
519
+ const dtt = Dtt.fromSavedConfig(fixtureParsed, fixtureToolMetadata);
520
+ expect(dtt.toSavedConfig()).toEqual(fixtureParsed);
521
+ });
522
+ });
@@ -4,7 +4,7 @@ import Dtt from "../models/dtt";
4
4
  import { DecayData } from "../models/decayData";
5
5
  import { RowData } from "../models/rowData";
6
6
  import { StrippingLine } from "../models/strippingLine";
7
- import { MetadataContext } from "../providers/MetadataProvider";
7
+ import { Metadata } from "../providers/MetadataProvider";
8
8
  import { Particle } from "../models/particle";
9
9
  export declare const mockToolMetadata: ToolMetadata;
10
10
  export declare const mockBranchItems: BranchMapItem[];
@@ -16,7 +16,7 @@ export declare function createMockRow(overrides?: Partial<RowData>): RowData;
16
16
  export declare const mockParticleProperties: {
17
17
  [key: string]: Particle;
18
18
  };
19
- export declare const mockMetadata: MetadataContext;
19
+ export declare const mockMetadata: Metadata;
20
20
  export declare const mockInlineMath: ({ math }: {
21
21
  math: string;
22
22
  }) => import("react/jsx-runtime").JSX.Element;
@@ -8,37 +8,43 @@ import Dtt from "../models/dtt";
8
8
  export const mockToolMetadata = {
9
9
  TupleToolKinematic: {
10
10
  description: "Kinematic information",
11
- documentation: "",
11
+ documentation: "No documentation available.",
12
+ summary: "TupleToolKinematic summary",
12
13
  tags: [],
13
14
  interface: [{ name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "Verbose output" }],
14
15
  },
15
16
  TupleToolPid: {
16
17
  description: "PID information",
17
- documentation: "",
18
+ documentation: "No documentation available.",
19
+ summary: "TupleToolPid summary",
18
20
  tags: [],
19
21
  interface: [{ name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "Verbose output" }],
20
22
  },
21
23
  TupleToolANNPID: {
22
24
  description: "ANN PID",
23
- documentation: "",
25
+ documentation: "No documentation available.",
26
+ summary: "TupleToolANNPID summary",
24
27
  tags: [],
25
28
  interface: [],
26
29
  },
27
30
  TupleToolGeometry: {
28
31
  description: "Geometry",
29
- documentation: "",
32
+ documentation: "No documentation available.",
33
+ summary: "TupleToolGeometry summary",
30
34
  tags: [],
31
35
  interface: [],
32
36
  },
33
37
  TupleToolEventInfo: {
34
38
  description: "Event info",
35
- documentation: "",
39
+ documentation: "No documentation available.",
40
+ summary: "TupleToolEventInfo summary",
36
41
  tags: [],
37
42
  interface: [],
38
43
  },
39
44
  TupleToolL0Data: {
40
45
  description: "L0 trigger data",
41
- documentation: "",
46
+ documentation: "No documentation available.",
47
+ summary: "TupleToolL0Data summary",
42
48
  tags: [],
43
49
  interface: [{ name: "Verbose", type: "bool", cpp_type: "bool", default: false, description: "Verbose output" }],
44
50
  },
@@ -136,7 +142,6 @@ export const mockMetadata = {
136
142
  decays: {
137
143
  "[B+ -> K+ mu+ mu-]cc": mockDecay,
138
144
  },
139
- embedding: {},
140
145
  kgdoc: {},
141
146
  lokiVariables: {
142
147
  applicationInfo: { Analysis: "v25r1", DaVinci: "v45r7", Phys: "v26r2" },
@@ -1,13 +1,13 @@
1
1
  import { DecayData } from "../models/decayData";
2
- import { MetadataContext } from "../providers/MetadataProvider";
2
+ import { Metadata } from "../providers/MetadataProvider";
3
3
  export declare const LATEX_SELECTED_COLOR = "#0d6efd";
4
4
  /**
5
5
  * Converts a LoKi decay selector string to LaTeX representation
6
6
  */
7
- export declare const selectionDescriptorToLatex: (metadata: MetadataContext, decay: DecayData, selection: string[]) => string;
7
+ export declare const selectionDescriptorToLatex: (metadata: Metadata, decay: DecayData, selection: string[]) => string;
8
8
  /**
9
9
  * Converts a single particle token to LaTeX representation.
10
10
  * Particle tokens may include a LoKi selection prefix ('^') and/or sub-decay parentheses.
11
11
  * The `highlighted` param can be used instead of a '^' prefix when calling directly.
12
12
  */
13
- export declare const convertParticleToLatex: (metadata: MetadataContext, particle: string, highlighted?: boolean) => string;
13
+ export declare const convertParticleToLatex: (metadata: Metadata, particle: string, highlighted?: boolean) => string;
@@ -1,12 +1,12 @@
1
1
  import { InfoYaml, YamlFile } from "../models/yamlFile";
2
2
  import { SavedDttConfig } from "../models/dtt";
3
3
  import { RowData } from "../models/rowData";
4
- import { MetadataContext } from "../providers/MetadataProvider";
5
- export declare function parseProductionFiles(files: File[], metadata: MetadataContext): Promise<string | {
4
+ import { Metadata } from "../providers/MetadataProvider";
5
+ export declare function parseProductionFiles(files: File[], metadata: Metadata): Promise<string | {
6
6
  rows: RowData[];
7
7
  emails: string[];
8
8
  }>;
9
- export declare function processProductionFiles(metadata: MetadataContext, configs: SavedDttConfig[], infoYaml: InfoYaml | null): string | {
9
+ export declare function processProductionFiles(metadata: Metadata, configs: SavedDttConfig[], infoYaml: InfoYaml | null): string | {
10
10
  rows: RowData[];
11
11
  emails: string[];
12
12
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lhcb-ntuple-wizard-test",
3
- "version": "2.2.2",
3
+ "version": "2.3.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,
@@ -21,6 +21,7 @@
21
21
  "cytoscape": "3.33.1",
22
22
  "cytoscape-dagre": "^2.5.0",
23
23
  "email-validator": "2.0.4",
24
+ "fuse.js": "^7.3.0",
24
25
  "js-yaml": "4.1.1",
25
26
  "jszip": "3.10.1",
26
27
  "katex": "^0.16.33",
@@ -43,7 +44,7 @@
43
44
  },
44
45
  "overrides": {
45
46
  "autoprefixer": "10.4.5",
46
- "@xmldom/xmldom": "0.9.9"
47
+ "@xmldom/xmldom": "0.9.10"
47
48
  },
48
49
  "scripts": {
49
50
  "start": "vite --open",