studio/src/components/playground/plan-view.tsx (224 lines of code) (raw):
import { useResolvedTheme } from "@/hooks/use-resolved-theme";
import {
Bars3BottomLeftIcon,
ExclamationTriangleIcon,
} from "@heroicons/react/24/outline";
import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { useContext, useEffect, useMemo, useState } from "react";
import { LuLayoutDashboard, LuNetwork } from "react-icons/lu";
import { Edge, Node, ReactFlowProvider } from "reactflow";
import { EmptyState } from "../empty-state";
import { schemaViewerDarkTheme } from "../schema/monaco-dark-theme";
import { CLI } from "../ui/cli";
import { Tabs, TabsList, TabsTrigger } from "../ui/tabs";
import { FetchFlow, ReactFlowQueryPlanFetchNode } from "./fetch-flow";
import { PlanPrinter } from "./prettyPrint";
import { TraceContext } from "./trace-view";
import { QueryPlan, QueryPlanFetchTypeNode } from "./types";
loader.config({
paths: {
// Load Monaco Editor from "public" directory
vs: "/monaco-editor/min/vs",
// Load Monaco Editor from different CDN
// vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs',
},
});
const PlanTree = ({ queryPlan }: { queryPlan: QueryPlan }) => {
const [initialNodes, setInitialNodes] = useState<Node[]>([]);
const [initialEdges, setInitialEdges] = useState<Edge[]>([]);
useEffect(() => {
const tempNodes: Node[] = [];
const tempEdges: Edge[] = [];
tempNodes.push({
id: "root",
type: "fetch",
data: {
...queryPlan,
},
position: {
x: 0,
y: 0,
},
});
const parseNodes = (node: QueryPlanFetchTypeNode, parentId: string) => {
node.children?.forEach((child) => {
const id = crypto.randomUUID();
tempNodes.push({
id,
type: "fetch",
data: {
...child,
},
position: {
x: 0,
y: 0,
},
connectable: false,
deletable: false,
});
tempEdges.push({
id: `${id}-${parentId}`,
source: parentId,
target: id,
animated: true,
});
parseNodes(child, id);
});
};
parseNodes(queryPlan, "root");
if (queryPlan.trigger) {
tempNodes.unshift({
id: "trigger",
type: "fetch",
data: {
kind: "Trigger",
fetch: queryPlan.trigger,
},
position: {
x: 0,
y: 0,
},
connectable: false,
deletable: false,
});
tempEdges.unshift({
id: `trigger-root`,
source: "trigger",
target: "root",
animated: true,
});
}
setInitialNodes(tempNodes);
setInitialEdges(tempEdges);
}, [queryPlan]);
const nodeTypes = useMemo<any>(
() => ({
fetch: ReactFlowQueryPlanFetchNode,
}),
[],
);
return (
<ReactFlowProvider>
<FetchFlow
initialNodes={initialNodes}
initialEdges={initialEdges}
nodeTypes={nodeTypes}
direction="TB"
nodeWidth={400}
nodeHeight={100}
/>
</ReactFlowProvider>
);
};
export const PlanView = () => {
const { plan, planError } = useContext(TraceContext);
const [formattedPlan, setFormattedPlan] = useState<string | null>(null);
useEffect(() => {
(async () => {
if (plan) {
const printer = new PlanPrinter();
const prettyPrintedQueryPlan = await printer.print(plan);
setFormattedPlan(prettyPrintedQueryPlan);
}
})();
}, [plan]);
const [view, setView] = useState<"tree" | "text">("tree");
const selectedTheme = useResolvedTheme();
const monaco = useMonaco();
useEffect(() => {
if (!monaco) return;
if (selectedTheme === "dark") {
monaco.editor.setTheme("wg-dark");
} else {
monaco.editor.setTheme("light");
}
}, [selectedTheme, monaco]);
if (planError) {
return (
<EmptyState
icon={<ExclamationTriangleIcon className="h-12 w-12" />}
title="Error fetching plan"
description={planError}
/>
);
}
if (!plan) {
return (
<EmptyState
icon={<LuLayoutDashboard />}
title="No query plan found"
description="Include the below header before executing your queries. Router version 0.104.0 or above is required."
actions={<CLI command={`"X-WG-Include-Query-Plan" : "true"`} />}
/>
);
}
return (
<div className="relative flex h-full w-full flex-1 flex-col font-sans">
<Tabs
defaultValue="tree"
className="absolute bottom-3 right-4 z-30 w-max"
onValueChange={(v: any) => setView(v)}
>
<TabsList className="grid w-full grid-cols-2 shadow-lg">
<TabsTrigger value="tree">
<div className="flex items-center gap-x-2">
<LuNetwork />
Tree View
</div>
</TabsTrigger>
<TabsTrigger value="text">
<div className="flex items-center gap-x-2">
<Bars3BottomLeftIcon className="h-4 w-4" />
Text View
</div>
</TabsTrigger>
</TabsList>
</Tabs>
{view === "tree" && <PlanTree queryPlan={plan} />}
{view === "text" && (
<div className="scrollbar-custom h-full w-full overflow-auto rounded-xl">
<Editor
theme={selectedTheme === "dark" ? "wg-dark" : "light"}
className="scrollbar-custom h-full"
language="customLang"
value={formattedPlan || ""}
options={{
fontSize: 14,
scrollbar: {
verticalScrollbarSize: 6,
horizontalScrollbarSize: 6,
},
smoothScrolling: true,
padding: {
top: 21,
},
minimap: {
enabled: false,
},
readOnly: true,
}}
beforeMount={(monaco) => {
monaco.editor.defineTheme("wg-dark", schemaViewerDarkTheme);
if (selectedTheme === "dark") {
monaco.editor.setTheme("wg-dark");
}
monaco.languages.register({
id: "customLang",
});
monaco.languages.setMonarchTokensProvider("customLang", {
// Define some basic tokens
tokenizer: {
root: [
// Match keywords followed by (service
[/\b(\w+)(?=\s*\(service)/, "keyword"],
// Match Flatten followed by (path
[/\b(Flatten)(?=\s*\(path)/, "keyword"],
// Match Sequence, Parallel, Single followed by {
[/\b(QueryPlan|Sequence|Parallel)(?=\s*{)/, "keyword"],
// Match keywords followed by { with previous line ending in }
[/(?<=}\s*\n\s*)(\w+)/, "keyword"],
// Match service declarations: service: "serviceName"
[
/(service|path)(\s*:\s*)("[^"]*")/,
["identifier", "", "string.service"],
],
// Match variables: $variableName
[/\$[a-zA-Z_]\w*/, "variable"],
],
},
});
}}
/>
</div>
)}
</div>
);
};