Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 55 additions & 49 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import "./index.css";
import { EditorSidebar } from "./app/EditorSidebar";
import { GraphMap } from "./app/GraphMap";
import { Panel, TypeFilterControl } from "./app/components";
import { TypeFilterControl } from "./app/components";
import type { EditorTab, GraphContext, GraphEdge, GraphNode, GraphSnapshot, PendingSelection } from "./app/types";
import { emptyGraph } from "./app/types";
import { api, edgeDirectionFormValue, formBooleanValue, formValue, metadataFormValue, parseJsonObject, stableStringify } from "./app/utils";
Expand Down Expand Up @@ -445,6 +445,55 @@ export function App() {
const deleteEdgeType = (id: string) =>
run("Edge type deleted.", () => api(`/api/edge-types/${id}`, { method: "DELETE" }));

const renderFilters = () => (
<div
id="graph-filter-panel"
className="grid w-full max-w-5xl gap-3 lg:w-[min(64rem,calc(100vw-28rem))] lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1fr)]"
>
<div className="graph-search">
<input value={search} onChange={event => setSearch(event.target.value)} placeholder="Search nodes..." />
{search.trim() && (
<div className="graph-search__results">
{nodeSearchMatches.length === 0 ? (
<p className="graph-search__empty">No matching nodes.</p>
) : (
nodeSearchMatches.map(node => (
<button
type="button"
key={node.id}
className={node.id === selectedNode?.id ? "item selected" : "item"}
onClick={() => {
maybeRequestSelection({ nodeId: node.id, edgeId: "" });
setSearch("");
}}
>
<span>
<strong>{node.name}</strong>
<small>{node.typeId}</small>
</span>
</button>
))
)}
</div>
)}
</div>
<TypeFilterControl
label="Node types"
allLabel="All node types"
options={graph.nodeTypes}
selectedIds={nodeTypeFilterIds}
onChange={setNodeTypeFilterIds}
/>
<TypeFilterControl
label="Edge types"
allLabel="All edge types"
options={graph.edgeTypes}
selectedIds={edgeTypeFilterIds}
onChange={setEdgeTypeFilterIds}
/>
</div>
);

return (
<main className="flex h-screen flex-col bg-zinc-950 text-zinc-100">
<header className="flex-none border-b border-zinc-800 bg-zinc-900/80 px-6 py-5">
Expand All @@ -467,55 +516,10 @@ export function App() {
</div>
</header>

<section className="flex min-h-0 flex-1 flex-col gap-6 overflow-y-auto px-6 py-6">
<section className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto px-2 py-2">
{error && <p className="rounded-lg border border-violet-500/20 bg-zinc-900 p-3 text-sm text-zinc-200">{error}</p>}

<Panel className="flex min-h-0 flex-1 flex-col">
{filtersOpen && (
<div className="mb-3 grid gap-3 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)_minmax(0,1fr)]">
<div className="graph-search">
<input value={search} onChange={event => setSearch(event.target.value)} placeholder="Search nodes..." />
{search.trim() && (
<div className="graph-search__results">
{nodeSearchMatches.length === 0 ? (
<p className="graph-search__empty">No matching nodes.</p>
) : (
nodeSearchMatches.map(node => (
<button
type="button"
key={node.id}
className={node.id === selectedNode?.id ? "item selected" : "item"}
onClick={() => {
maybeRequestSelection({ nodeId: node.id, edgeId: "" });
setSearch("");
}}
>
<span>
<strong>{node.name}</strong>
<small>{node.typeId}</small>
</span>
</button>
))
)}
</div>
)}
</div>
<TypeFilterControl
label="Node types"
allLabel="All node types"
options={graph.nodeTypes}
selectedIds={nodeTypeFilterIds}
onChange={setNodeTypeFilterIds}
/>
<TypeFilterControl
label="Edge types"
allLabel="All edge types"
options={graph.edgeTypes}
selectedIds={edgeTypeFilterIds}
onChange={setEdgeTypeFilterIds}
/>
</div>
)}
<div className="flex min-h-0 flex-1 flex-col">
<GraphMap
graph={graph}
selectedNodeId={selectedNode?.id ?? ""}
Expand All @@ -527,19 +531,21 @@ export function App() {
onClearSelection={() => {
maybeRequestSelection({ nodeId: "", edgeId: "" });
}}
filters={filtersOpen ? renderFilters() : null}
controls={
<button
type="button"
className="bg-zinc-950/95 shadow-lg shadow-black/30 backdrop-blur"
onClick={() => setFiltersOpen(current => !current)}
aria-expanded={filtersOpen}
aria-controls="graph-filter-panel"
aria-label={filtersOpen ? "Hide search and filters" : "Show search and filters"}
>
{filtersOpen ? "Hide filters" : "Show filters"}
</button>
}
/>
</Panel>
</div>

<EditorSidebar
activeTab={activeTab}
Expand Down
18 changes: 13 additions & 5 deletions src/app/GraphMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ interface GraphMapProps {
onSelectNode: (id: string) => void;
onClearSelection: () => void;
controls: ReactNode;
filters?: ReactNode;
onFullscreenChange?: (isFullscreen: boolean) => void;
}

export function GraphMap(props: GraphMapProps) {
Expand All @@ -42,6 +44,7 @@ export function GraphMap(props: GraphMapProps) {
const [layoutSpacing, setLayoutSpacing] = useState(1);
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [isFullscreen, setIsFullscreen] = useState(false);
const onFullscreenChange = props.onFullscreenChange;
const dragStateRef = useRef<{ startX: number; startY: number; lastX: number; lastY: number; hasDragged: boolean } | null>(null);
const graphViewportRef = useRef<HTMLDivElement | null>(null);
const svgRef = useRef<SVGSVGElement | null>(null);
Expand Down Expand Up @@ -154,14 +157,16 @@ export function GraphMap(props: GraphMapProps) {

useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(document.fullscreenElement === graphViewportRef.current);
const nextIsFullscreen = document.fullscreenElement === graphViewportRef.current;
setIsFullscreen(nextIsFullscreen);
onFullscreenChange?.(nextIsFullscreen);
};

document.addEventListener("fullscreenchange", handleFullscreenChange);
handleFullscreenChange();

return () => document.removeEventListener("fullscreenchange", handleFullscreenChange);
}, []);
}, [onFullscreenChange]);

const toggleFullscreen = async () => {
const viewport = graphViewportRef.current;
Expand Down Expand Up @@ -270,7 +275,7 @@ export function GraphMap(props: GraphMapProps) {
ref={graphViewportRef}
className={isFullscreen ? "graph-map-fullscreen flex h-full flex-col bg-zinc-950 p-4" : "flex h-full min-h-0 flex-col"}
>
<div className="graph-map-shell relative flex-1 overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950">
<div className="graph-map-shell relative flex-1 overflow-hidden bg-zinc-950">
<svg
ref={svgRef}
viewBox={viewBox}
Expand Down Expand Up @@ -392,7 +397,10 @@ export function GraphMap(props: GraphMapProps) {
)}
</g>
</svg>
<div className="absolute right-3 top-3 z-10 flex max-w-full flex-wrap justify-end gap-2">
<div className="absolute left-3 top-3 z-10 flex max-w-[calc(100%-1.5rem)] flex-wrap items-start justify-start gap-2">
{props.filters}
</div>
<div className="absolute right-3 top-3 z-10 flex max-w-[calc(100%-1.5rem)] flex-wrap items-start justify-end gap-2">
{props.controls}
<button type="button" className="bg-zinc-950/95 shadow-lg shadow-black/30 backdrop-blur" onClick={handleResetViewport}>
Reset view
Expand Down Expand Up @@ -439,4 +447,4 @@ export function GraphMap(props: GraphMapProps) {
</div>
</div>
);
}
}
8 changes: 4 additions & 4 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}

button {
@apply rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 transition hover:border-violet-400/50 hover:bg-zinc-700 disabled:cursor-not-allowed disabled:border-zinc-800 disabled:bg-zinc-800 disabled:text-zinc-500;
@apply h-[38px] rounded-lg border border-zinc-700 bg-zinc-800 px-3 py-2 text-sm font-semibold leading-5 text-zinc-100 transition hover:border-violet-400/50 hover:bg-zinc-700 disabled:cursor-not-allowed disabled:border-zinc-800 disabled:bg-zinc-800 disabled:text-zinc-500;
}

button.danger {
Expand All @@ -36,7 +36,7 @@
input,
select,
textarea {
@apply w-full rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-100 placeholder:text-zinc-500 outline-none focus:border-stone-400;
@apply h-[38px] w-full rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm leading-5 text-zinc-100 placeholder:text-zinc-500 outline-none focus:border-stone-400;
}
Comment on lines 36 to 40

form {
Expand Down Expand Up @@ -78,7 +78,7 @@

.filter-menu__summary {
display: flex;
min-height: 42px;
height: 38px;
cursor: pointer;
list-style: none;
align-items: center;
Expand All @@ -87,7 +87,7 @@
border-radius: 0.5rem;
border: 1px solid rgb(63 63 70);
background: rgb(9 9 11);
padding: 0.625rem 0.75rem;
padding: 0.5rem 0.75rem;
color: rgb(244 244 245);
}

Expand Down