diff --git a/query-graphs/src/hyper.ts b/query-graphs/src/hyper.ts index b5e917a..499b731 100644 --- a/query-graphs/src/hyper.ts +++ b/query-graphs/src/hyper.ts @@ -36,6 +36,7 @@ interface ConversionState { crosslinks: UnresolvedCrosslink[]; edgeWidths: {node: TreeNode; width: number}[]; runtimes: {node: TreeNode; time: number}[]; + metadata: Map; } // Customization points for rendering the various different @@ -222,6 +223,12 @@ function convertHyperNode(rawNode: Json, parentKey, conversionState: ConversionS expandedByDefault: nodeType != "operator" && expandedChildren.length == 0, } as TreeNode; + // Highlight the node which errored out, in case the query failed + const errored = conversionState.metadata.has("Error") && tryGetPropertyPath(rawNode, ["analyze", "running"]) === true; + if (errored) { + convertedNode.iconColor = "red"; + } + // Information on the execution time const execTime = tryGetPropertyPath(rawNode, ["analyze", "execution-time"]); if (typeof execTime === "number") { @@ -320,18 +327,20 @@ function setEdgeWidths(state: ConversionState) { } } -interface LinkedNodes { - root: TreeNode; - crosslinks: Crosslink[]; -} - -function convertHyperPlan(node: Json): LinkedNodes { +function convertHyperPlan(node: Json): TreeDescription { const conversionState = { operatorsById: new Map(), crosslinks: [], edgeWidths: [], runtimes: [], + metadata: new Map(), } as ConversionState; + // Check if the query failed + const errorMsg = tryGetPropertyPath(node, ["analyze", "error", "message", "original"]); + if (errorMsg) { + conversionState.metadata.set("Error", forceToString(errorMsg)); + } + const root = convertHyperNode(node, "result", conversionState); if (Array.isArray(root)) { throw new Error("Invalid Hyper query plan"); @@ -339,10 +348,10 @@ function convertHyperPlan(node: Json): LinkedNodes { colorRelativeExecutionTime(conversionState); setEdgeWidths(conversionState); const crosslinks = resolveCrosslinks(conversionState); - return {root, crosslinks}; + return {root, crosslinks, metadata: conversionState.metadata}; } -function convertOptimizerSteps(node: Json): LinkedNodes | undefined { +function convertOptimizerSteps(node: Json): TreeDescription | undefined { // Check if we have a top-level object with a single key "optimizersteps" containing an array if (typeof node !== "object" || Array.isArray(node) || node === null) return undefined; if (Object.getOwnPropertyNames(node).length != 1) return undefined; @@ -353,6 +362,7 @@ function convertOptimizerSteps(node: Json): LinkedNodes | undefined { // Transform the optimizer steps const crosslinks: Crosslink[] = []; const children: TreeNode[] = []; + const properties = new Map(); for (const step of steps) { // Check that our step has two subproperties: "name" and "plan" if (typeof step !== "object" || Array.isArray(step) || step === null) return undefined; @@ -364,20 +374,20 @@ function convertOptimizerSteps(node: Json): LinkedNodes | undefined { if (typeof name !== "string") return undefined; // Add the child - const {root: childRoot, crosslinks: newCrosslinks} = convertHyperPlan(plan); - crosslinks.push(...newCrosslinks); + const {root: childRoot, crosslinks: newCrosslinks, metadata: newProperties} = convertHyperPlan(plan); + crosslinks.push(...(newCrosslinks ?? [])); children.push({name: name, children: [childRoot]}); + for (const p of newProperties ?? new Map()) { + properties.set(p[0], p[1]); + } } const root = {name: "optimizersteps", children: children}; - return {root, crosslinks}; + return {root, crosslinks, metadata: properties}; } // Loads a Hyper query plan export function loadHyperPlan(json: Json): TreeDescription { - // Load the graph with the nodes collapsed in an automatic way - const {root, crosslinks} = convertOptimizerSteps(json) ?? convertHyperPlan(json); - // Adjust the graph so it is collapsed as requested by the user - return {root, crosslinks}; + return convertOptimizerSteps(json) ?? convertHyperPlan(json); } function tryStripPrefix(str, pre) { diff --git a/query-graphs/src/tree-description.ts b/query-graphs/src/tree-description.ts index 51a1bd2..7b1634a 100644 --- a/query-graphs/src/tree-description.ts +++ b/query-graphs/src/tree-description.ts @@ -47,9 +47,8 @@ export interface Crosslink { export interface TreeDescription { /// The tree root root: TreeNode; - /// Displayed in the top-level tree label - /// XXX remove - properties?: Map; + /// Metadata about the graph; displayed in the top-level tree label + metadata?: Map; /// Additional links between indirectly related nodes crosslinks?: Crosslink[]; } diff --git a/query-graphs/src/ui/QueryNode.css b/query-graphs/src/ui/QueryNode.css index 20e1993..7a7323b 100644 --- a/query-graphs/src/ui/QueryNode.css +++ b/query-graphs/src/ui/QueryNode.css @@ -88,11 +88,13 @@ .qg-prop-name { color: hsl(0, 0%, 50%); + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; } .qg-prop-value { - -webkit-user-select: all; - -moz-user-select: all; - -ms-user-select: all; - user-select: all; + -webkit-user-select: text; + -ms-user-select: text; + user-select: text; } \ No newline at end of file diff --git a/standalone-app/src/QueryGraphsApp.tsx b/standalone-app/src/QueryGraphsApp.tsx index cb82f7f..99c3b56 100644 --- a/standalone-app/src/QueryGraphsApp.tsx +++ b/standalone-app/src/QueryGraphsApp.tsx @@ -122,7 +122,7 @@ export function QueryGraphsApp() { } else { return ( - + ); } diff --git a/standalone-app/src/TreeLabel.css b/standalone-app/src/TreeLabel.css index b9aaca4..be41e98 100644 --- a/standalone-app/src/TreeLabel.css +++ b/standalone-app/src/TreeLabel.css @@ -1,5 +1,6 @@ .graph-title { - font-size: 1.2em; + background: #fff; + font-size: 1.1em; font-weight: bold; field-sizing: content; min-width: 10em; @@ -8,4 +9,10 @@ .graph-title:hover { background: #ffeded; +} + +.graph-metadata { + background: #fff; + max-width: 20em; + word-break: break-all; } \ No newline at end of file diff --git a/standalone-app/src/TreeLabel.tsx b/standalone-app/src/TreeLabel.tsx index 99c91ff..c3ffb01 100644 --- a/standalone-app/src/TreeLabel.tsx +++ b/standalone-app/src/TreeLabel.tsx @@ -1,11 +1,22 @@ +import {ReactElement} from "react"; import "./TreeLabel.css"; export interface TreeLabelProps { title: string; setTitle?: (v: string) => void; + metadata?: Map; } -export function TreeLabel({title, setTitle}: TreeLabelProps) { +export function TreeLabel({title, setTitle, metadata}: TreeLabelProps) { + const metadataChildren = [] as ReactElement[]; + for (const [key, value] of (metadata || []).entries()) { + metadataChildren.push( +
+ {key}: {value} +
, + ); + } + return (
(setTitle ? setTitle(e.target.value) : undefined)} /> +
{metadataChildren}
); }