Skip to content

Commit

Permalink
Display an editable graph title (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
vogelsgesang authored Oct 20, 2024
1 parent bc4dadb commit 2561b8c
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 40 deletions.
8 changes: 5 additions & 3 deletions query-graphs/src/ui/QueryGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import "reactflow/dist/base.css";

import {layoutTree} from "./tree-layout";
import {TreeDescription, TreeNode, allChildren, visitTreeNodes} from "../tree-description";
import {useMemo, useEffect, useRef} from "react";
import {useMemo, useEffect, useRef, ReactNode} from "react";
import {QueryNode} from "./QueryNode";
import "./QueryGraph.css";
import {useGraphRenderingStore} from "./store";
import "./QueryGraph.css";

interface QueryGraphProps {
treeDescription: TreeDescription;
children: ReactNode | ReactNode[];
}

function minimapNodeColor(n: Node<TreeNode>): string {
Expand All @@ -22,7 +23,7 @@ const nodeTypes = {
querynode: QueryNode,
};

function QueryGraphInternal({treeDescription}: QueryGraphProps) {
function QueryGraphInternal({treeDescription, children}: QueryGraphProps) {
// Assign ids to all nodes
const nodeIdMapping = useMemo(() => {
let nextId = 0;
Expand Down Expand Up @@ -92,6 +93,7 @@ function QueryGraphInternal({treeDescription}: QueryGraphProps) {
nodesFocusable={false}
className={"query-graph"}
>
{...Array.isArray(children) ? children : [children]}
<MiniMap zoomable={true} pannable={true} nodeColor={minimapNodeColor} />
<Controls showInteractive={false} />
</ReactFlow>
Expand Down
30 changes: 10 additions & 20 deletions standalone-app/src/QueryGraphsApp.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {useCallback, useEffect, useMemo, useState} from "react";
import {useCallback, useEffect, useState} from "react";
import {useBrowserUrl, useUrlParam} from "./browserUrlHooks";
import {FileOpener, FileOpenerData, useLoadStateController} from "./FileOpener";
import {QueryGraph} from "@tableau/query-graphs/lib/ui/QueryGraph";
import {TreeDescription} from "@tableau/query-graphs/lib/tree-description";
import {loadPlan} from "./tree-loader";
import {tryCreateLocalStorageUrl, isLocalStorageURL, loadLocalStorageURL} from "./LocalStorageUrl";
import {assert} from "./assert";
import {TreeLabel} from "./TreeLabel";

export function QueryGraphsApp() {
const loadStateController = useLoadStateController();
Expand All @@ -15,7 +16,7 @@ export function QueryGraphsApp() {
// We store the currently opened tree in a URL parameter.
// Thereby, we automatically integrate with the browser's history.
const [treeUrl, setTreeUrl] = useUrlParam(browserUrl, "file");
const [treeTitle, setTreeTitle] = useUrlParam(browserUrl, "title");
const [treeTitle, setTreeTitle] = useUrlParam(browserUrl, "title", true);
const [uploadServer] = useUrlParam(browserUrl, "uploadServer");
// Callback for the file opener
const openPickedData = async (data: FileOpenerData): Promise<void> => {
Expand Down Expand Up @@ -49,7 +50,7 @@ export function QueryGraphsApp() {
if (data.fileName) {
title = data.fileName;
} else {
title = "query plan";
title = new Date().toLocaleString();
}
// Update the tree URL such that link sharing works.
setTreeTitle(title);
Expand Down Expand Up @@ -116,24 +117,13 @@ export function QueryGraphsApp() {
}
}, []);

// We annotate the tree with the `title`
const annotatedTree = useMemo(() => {
if (tree === undefined) return undefined;
const newTree = {
...tree,
};
if (newTree.properties === undefined) {
newTree.properties = new Map<string, string>();
}
if (treeTitle) {
newTree.properties.set("title", treeTitle);
}
return newTree;
}, [tree, treeTitle]);

if (!annotatedTree) {
if (!tree) {
return <FileOpener setData={openPickedData} loadStateController={loadStateController} validate={validate} />;
} else {
return <QueryGraph treeDescription={annotatedTree} />;
return (
<QueryGraph treeDescription={tree}>
<TreeLabel title={treeTitle ?? ""} setTitle={setTreeTitle} />
</QueryGraph>
);
}
}
11 changes: 11 additions & 0 deletions standalone-app/src/TreeLabel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.graph-title {
font-size: 1.2em;
font-weight: bold;
field-sizing: content;
min-width: 10em;
border: none;
}

.graph-title:hover {
background: #ffeded;
}
20 changes: 20 additions & 0 deletions standalone-app/src/TreeLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import "./TreeLabel.css";

export interface TreeLabelProps {
title: string;
setTitle?: (v: string) => void;
}

export function TreeLabel({title, setTitle}: TreeLabelProps) {
return (
<div className="react-flow__panel">
<input
type="text"
className="graph-title"
placeholder="Untitled"
value={title}
onChange={(e) => (setTitle ? setTitle(e.target.value) : undefined)}
/>
</div>
);
}
41 changes: 25 additions & 16 deletions standalone-app/src/browserUrlHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,42 @@
import type React from "react";
import {useCallback, useEffect, useState} from "react";

type URLState = [URL, React.Dispatch<React.SetStateAction<URL>>];
type URLState = [URL, (value: React.SetStateAction<URL>, noHistoryEntry?: boolean) => void];

export function useBrowserUrl(): URLState {
const [url, setUrl] = useState<URL>(() => new URL(window.location.toString()));
const [url, setUrlInternal] = useState<URL>(() => new URL(window.location.toString()));
useEffect(() => {
const listener = (_e: PopStateEvent) => {
setUrl(new URL(window.location.toString()));
setUrlInternal(new URL(window.location.toString()));
};
window.addEventListener("popstate", listener);
return () => window.removeEventListener("popstate", listener);
}, [setUrl]);
useEffect(() => {
if (url.toString() != new URL(window.location.toString()).toString()) {
window.history.pushState(null, "", url);
}
}, [url]);
}, [setUrlInternal]);
const setUrl = useCallback(
(action: React.SetStateAction<URL>, noHistoryEntry?: boolean) => {
let newUrl: URL;
if (action instanceof Function) newUrl = action(url);
else newUrl = action;
setUrlInternal(newUrl);
// Don't change the URL if the parameter didn't change.
// This is important to ensure the history stays intact.
if (newUrl.toString() != new URL(window.location.toString()).toString()) {
if (noHistoryEntry) {
window.history.replaceState(null, "", newUrl);
} else {
window.history.pushState(null, "", newUrl);
}
}
},
[url, setUrlInternal],
);
return [url, setUrl];
}

export function useUrlParam(
urlState: URLState,
key: string,
noHistoryEntry?: boolean,
): [string | undefined, React.Dispatch<React.SetStateAction<string | undefined>>] {
const [url, setUrl] = urlState;
const v = url.searchParams.get(key) ?? undefined;
Expand All @@ -35,21 +49,16 @@ export function useUrlParam(
let newValue: string | undefined;
if (action instanceof Function) newValue = action(currentValue);
else newValue = action;
if (newValue === currentValue) {
// Don't change the URL if the parameter didn't change.
// This is important to ensure the history stays intact.
return url;
}
const newURL = new URL(url);
if (newValue === undefined) {
newURL.searchParams.delete(key);
} else {
newURL.searchParams.set(key, newValue);
}
return newURL;
});
}, noHistoryEntry);
},
[setUrl, key],
[setUrl, key, noHistoryEntry],
);
return [v, set];
}
2 changes: 1 addition & 1 deletion standalone-app/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
"ts-node": {
"compilerOptions": { "module": "commonjs" }
}
}
}

0 comments on commit 2561b8c

Please sign in to comment.