Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/graph-explorer/src/components/Graph/Graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useState,
} from "react";

import { DEFAULT_GRAPH_LAYOUT } from "@/core/graphLayout";
import { cn } from "@/utils";

import type {
Expand Down Expand Up @@ -163,7 +164,7 @@ export const Graph = ({
onNodeRightClick,
onGraphClick,
onGraphRightClick,
layout = "F_COSE",
layout = DEFAULT_GRAPH_LAYOUT,
badgesEnabled = false,
useAnimation = true,
pan,
Expand Down
4 changes: 2 additions & 2 deletions packages/graph-explorer/src/components/Graph/SelectLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ComponentPropsWithRef } from "react";

import { type PrimitiveAtom, useAtom } from "jotai";
import { useAtom, type WritableAtom } from "jotai";

import type { LayoutName } from "@/components/Graph/helpers/layoutConfig";

Expand All @@ -19,7 +19,7 @@ export function SelectLayout({
layoutAtom,
...props
}: ComponentPropsWithRef<typeof SelectTrigger> & {
layoutAtom: PrimitiveAtom<LayoutName>;
layoutAtom: WritableAtom<LayoutName, [LayoutName], unknown>;
}) {
const [value, setValue] = useAtom(layoutAtom);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type cytoscape from "cytoscape";

import type { LayoutName } from "@/core/graphLayout";

export const concentricLayout = {
name: "concentric",

Expand Down Expand Up @@ -335,6 +337,6 @@ export const availableLayoutsConfig = {
SUBWAY_BT: subwayLayoutBottomToTop,
SUBWAY_LR: subwayLayoutLeftToRight,
SUBWAY_RL: subwayLayoutRightToLeft,
};
} satisfies Record<LayoutName, unknown>;

export type LayoutName = keyof typeof availableLayoutsConfig;
export type { LayoutName } from "@/core/graphLayout";
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
import { logger } from "@/utils";

import {
defaultSchemaViewLayout,
transformSchemaViewLayout,
type SchemaViewLayout,
} from "./schemaViewLayoutDefaults";

it("defaults the layout algorithm to F_COSE", () => {
expect(defaultSchemaViewLayout.layoutAlgorithm).toBe("F_COSE");
});

/**
* BACKWARD COMPATIBILITY — PERSISTED DATA
*
* SchemaViewLayout is persisted to IndexedDB via localforage. Older versions
* stored the styling sidebar as two separate panels, so `activeSidebarItem`
* could be "nodes-styling" or "edges-styling". Those were merged into a single
* "styles" panel, but previously persisted layouts may still hold the old
* values. transformSchemaViewLayout normalizes them on read so the sidebar isn't
* stuck pointing at a panel that no longer exists.
* could be "nodes-styling" or "edges-styling". Those versions also had no
* `layoutAlgorithm`. transformSchemaViewLayout maps the old panels to "styles"
* and supplies F_COSE when the algorithm is absent so legacy preferences remain
* usable.
*
* DO NOT delete or weaken these tests without confirming that all persisted
* data has been transformed or that the old values are no longer in the wild.
*/
describe("transformSchemaViewLayout backward compatibility", () => {
it("defaults a missing layout algorithm to F_COSE", () => {
const legacy = {
activeSidebarItem: "details",
sidebar: { width: 400 },
detailsAutoOpenOnSelection: false,
} as unknown as SchemaViewLayout;

expect(transformSchemaViewLayout(legacy)).toStrictEqual({
activeSidebarItem: "details",
sidebar: { width: 400 },
detailsAutoOpenOnSelection: false,
layoutAlgorithm: "F_COSE",
});
expect(logger.debug).not.toHaveBeenCalled();
});

it("preserves a recognized layout algorithm", () => {
const layout: SchemaViewLayout = {
activeSidebarItem: "details",
sidebar: { width: 400 },
layoutAlgorithm: "D3",
};

expect(transformSchemaViewLayout(layout)).toBe(layout);
});

it("recovers an unrecognized layout algorithm to F_COSE", () => {
const invalid = {
activeSidebarItem: "details",
sidebar: { width: 400 },
layoutAlgorithm: "REMOVED_LAYOUT",
} as unknown as SchemaViewLayout;

expect(transformSchemaViewLayout(invalid)).toStrictEqual({
activeSidebarItem: "details",
sidebar: { width: 400 },
layoutAlgorithm: "F_COSE",
});
expect(logger.debug).toHaveBeenCalledWith(
'[schema-view-layout] Unrecognized layout algorithm; using "F_COSE"',
"REMOVED_LAYOUT",
);
});

it("maps legacy nodes-styling to styles", () => {
const legacy = {
activeSidebarItem: "nodes-styling",
Expand All @@ -39,6 +90,7 @@ describe("transformSchemaViewLayout backward compatibility", () => {
const layout: SchemaViewLayout = {
activeSidebarItem: "details",
sidebar: { width: 400 },
layoutAlgorithm: "F_COSE",
};

expect(transformSchemaViewLayout(layout)).toBe(layout);
Expand All @@ -48,6 +100,7 @@ describe("transformSchemaViewLayout backward compatibility", () => {
const layout: SchemaViewLayout = {
activeSidebarItem: null,
sidebar: { width: 400 },
layoutAlgorithm: "F_COSE",
};

expect(transformSchemaViewLayout(layout)).toBe(layout);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import {
DEFAULT_GRAPH_LAYOUT,
isLayoutName,
type LayoutName,
} from "@/core/graphLayout";
import { logger } from "@/utils";

import {
DEFAULT_SIDEBAR_WIDTH,
transformLegacySidebarItem,
Expand All @@ -12,13 +19,15 @@ export type SchemaViewLayout = {
activeSidebarItem: SchemaViewSidebarItem | null;
sidebar: { width: number };
detailsAutoOpenOnSelection?: boolean;
layoutAlgorithm: LayoutName;
};

/** Initial layout state used when no persisted layout exists. */
export const defaultSchemaViewLayout: SchemaViewLayout = {
activeSidebarItem: "details",
sidebar: { width: DEFAULT_SIDEBAR_WIDTH },
detailsAutoOpenOnSelection: true,
layoutAlgorithm: DEFAULT_GRAPH_LAYOUT,
};

/** Normalizes a persisted schema view layout from an older app version. */
Expand All @@ -28,7 +37,19 @@ export function transformSchemaViewLayout(
const activeSidebarItem = transformLegacySidebarItem(
layout.activeSidebarItem,
);
return activeSidebarItem === layout.activeSidebarItem
const layoutAlgorithm = resolveLayoutAlgorithm(layout.layoutAlgorithm);
return activeSidebarItem === layout.activeSidebarItem &&
layoutAlgorithm === layout.layoutAlgorithm
? layout
: { ...layout, activeSidebarItem };
: { ...layout, activeSidebarItem, layoutAlgorithm };
}

function resolveLayoutAlgorithm(value: unknown): LayoutName {
if (value == null) return DEFAULT_GRAPH_LAYOUT;
if (isLayoutName(value)) return value;
logger.debug(
`[schema-view-layout] Unrecognized layout algorithm; using "${DEFAULT_GRAPH_LAYOUT}"`,
value,
);
return DEFAULT_GRAPH_LAYOUT;
}
15 changes: 15 additions & 0 deletions packages/graph-explorer/src/core/graphLayout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DEFAULT_GRAPH_LAYOUT, isLayoutName, layoutNames } from "./graphLayout";

describe("graph layout vocabulary", () => {
it("defines F_COSE as the default layout", () => {
expect(DEFAULT_GRAPH_LAYOUT).toBe("F_COSE");
});

it("recognizes every supported layout name", () => {
expect(layoutNames.every(isLayoutName)).toBe(true);
});

it("rejects an unsupported layout name", () => {
expect(isLayoutName("UNSUPPORTED_LAYOUT")).toBe(false);
});
});
25 changes: 25 additions & 0 deletions packages/graph-explorer/src/core/graphLayout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const layoutNames = [
"CONCENTRIC",
"DAGRE_TB",
"DAGRE_BT",
"DAGRE_LR",
"DAGRE_RL",
"F_COSE",
"D3",
"KLAY_LR",
"KLAY_TB",
"SUBWAY_TB",
"SUBWAY_BT",
"SUBWAY_LR",
"SUBWAY_RL",
] as const;

export type LayoutName = (typeof layoutNames)[number];

export const DEFAULT_GRAPH_LAYOUT: LayoutName = "F_COSE";

const layoutNameSet = new Set<string>(layoutNames);

export function isLayoutName(value: unknown): value is LayoutName {
return typeof value === "string" && layoutNameSet.has(value);
}
1 change: 1 addition & 0 deletions packages/graph-explorer/src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./ConfigurationProvider";
export * from "./StateProvider";
export * from "./connector";
export * from "./entities";
export * from "./graphLayout";
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
DownloadScreenshotButton,
Graph,
GraphProvider,
type LayoutName,
RerunLayoutButton,
type SelectedElements,
SelectLayout,
Expand All @@ -34,6 +33,7 @@ import {
ZoomToFitButton,
} from "@/components/Graph";
import {
DEFAULT_GRAPH_LAYOUT,
createRenderedEdgeId,
createRenderedVertexId,
getEdgeIdFromRenderedEdgeId,
Expand All @@ -60,7 +60,7 @@ import { useGraphSelection } from "./useGraphSelection";
import useGraphStyles from "./useGraphStyles";
import useNodeBadges from "./useNodeBadges";

const graphLayoutSelectionAtom = atom<LayoutName>("F_COSE");
const graphLayoutSelectionAtom = atom(DEFAULT_GRAPH_LAYOUT);

// Prevent open context menu on Windows
function onContextMenu(e: MouseEvent<HTMLDivElement>) {
Expand Down
14 changes: 4 additions & 10 deletions packages/graph-explorer/src/modules/SchemaGraph/SchemaGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { atom, useAtomValue } from "jotai";
import { useAtomValue } from "jotai";
import { type ComponentPropsWithRef, type MouseEvent, useState } from "react";

import {
Expand All @@ -7,18 +7,15 @@ import {
PanelContent,
PanelGroup,
} from "@/components";
import {
Graph,
type LayoutName,
type SelectedElements,
} from "@/components/Graph";
import { Graph, type SelectedElements } from "@/components/Graph";
import {
createVertexType,
type EdgeConnectionId,
type VertexType,
} from "@/core";
import { cn, logger } from "@/utils";

import { schemaViewLayoutAlgorithmAtom } from "./schemaGraphLayout";
import { SchemaGraphToolbar } from "./SchemaGraphToolbar";
import { SchemaExplorerSidebar } from "./Sidebar/SchemaExplorerSidebar";
import { useSchemaViewSidebar } from "./Sidebar/schemaViewLayout";
Expand All @@ -41,9 +38,6 @@ export type SchemaGraphProps = Omit<
"children" | "onContextMenu"
>;

/** Atom for storing the selected graph layout algorithm */
export const schemaGraphLayoutAtom = atom<LayoutName>("F_COSE");

function preventContextMenu(e: MouseEvent<HTMLDivElement>) {
e.preventDefault();
e.stopPropagation();
Expand All @@ -53,7 +47,7 @@ function preventContextMenu(e: MouseEvent<HTMLDivElement>) {
export default function SchemaGraph({ className, ...props }: SchemaGraphProps) {
const { nodes, edges } = useSchemaGraphData();
const styles = useSchemaGraphStyles();
const layout = useAtomValue(schemaGraphLayoutAtom);
const layout = useAtomValue(schemaViewLayoutAlgorithmAtom);

const [selection, setSelection] = useState<SchemaGraphSelection>(null);
const [graphSelection, setGraphSelection] = useState<SelectedElements | null>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
import { useSchemaSync } from "@/hooks/useSchemaSync";
import { ASCII, logger } from "@/utils";

import { schemaGraphLayoutAtom } from "./SchemaGraph";
import { schemaViewLayoutAlgorithmAtom } from "./schemaGraphLayout";

/** Toolbar for schema graph with layout controls and schema refresh */
export function SchemaGraphToolbar() {
Expand All @@ -29,7 +29,7 @@ export function SchemaGraphToolbar() {
<PanelHeaderActions className="gap-1.5">
<SelectLayout
className="max-w-64 min-w-auto"
layoutAtom={schemaGraphLayoutAtom}
layoutAtom={schemaViewLayoutAlgorithmAtom}
/>
<RerunLayoutButton />
<ZoomToFitButton />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function stateWithDetailsTab() {
return new DbState().withSchemaViewLayout({
activeSidebarItem: "details",
sidebar: { width: 400 },
layoutAlgorithm: "F_COSE",
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useSchemaViewSidebar } from "./schemaViewLayout";
const baseLayout: SchemaViewLayout = {
activeSidebarItem: "details",
sidebar: { width: DEFAULT_SIDEBAR_WIDTH },
layoutAlgorithm: "F_COSE",
};

/** Seeds a schema view layout, overriding only the fields a test pins. */
Expand Down
Loading