diff --git a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaEdgesStyling.tsx b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaEdgesStyling.tsx new file mode 100644 index 000000000..58683761b --- /dev/null +++ b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaEdgesStyling.tsx @@ -0,0 +1,43 @@ +import { Virtuoso } from "react-virtuoso"; +import { Fragment } from "react/jsx-runtime"; + +import { + Divider, + Panel, + PanelContent, + PanelHeader, + PanelTitle, +} from "@/components"; +import { useDisplayEdgeTypeConfigs } from "@/core"; +import { useTranslations } from "@/hooks"; +import SingleEdgeStyling from "@/modules/EdgesStyling/SingleEdgeStyling"; + +/** Styling panel for edge types in the schema explorer sidebar. */ +export function SchemaEdgesStyling() { + const etConfigMap = useDisplayEdgeTypeConfigs(); + const etConfigs = etConfigMap.values().toArray(); + const t = useTranslations(); + + return ( + + + {t("edges-styling.title")} + + + ( + + {index !== 0 ? : null} + + + )} + /> + + + ); +} diff --git a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx new file mode 100644 index 000000000..496410221 --- /dev/null +++ b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.test.tsx @@ -0,0 +1,94 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { Provider } from "jotai"; +import { describe, expect, test, vi } from "vitest"; + +import { TooltipProvider } from "@/components"; +import { getAppStore } from "@/core"; +import { + createTestableEdge, + createTestableVertex, + DbState, +} from "@/utils/testing"; + +import { SchemaExplorerSidebar } from "./SchemaExplorerSidebar"; + +vi.mock("react-virtuoso", () => ({ + Virtuoso: ({ + data, + itemContent, + }: { + data: unknown[]; + itemContent: (index: number, item: unknown) => React.ReactNode; + }) => data?.map((item, index) => itemContent(index, item)), +})); + +function renderSidebar(state: DbState) { + const store = getAppStore(); + state.applyTo(store); + + const queryClient = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + + return render( + + + + + + + , + ); +} + +describe("SchemaExplorerSidebar", () => { + test("renders details tab by default with empty selection message", () => { + const state = new DbState(); + renderSidebar(state); + + expect(screen.getByText("Empty Selection")).toBeInTheDocument(); + }); + + test("shows three sidebar tabs", () => { + const state = new DbState(); + renderSidebar(state); + + const tabs = screen.getAllByRole("tab"); + expect(tabs).toHaveLength(3); + }); + + test("renders node styling content when clicking second tab", async () => { + const user = userEvent.setup(); + const state = new DbState(); + const vertex = createTestableVertex().with({ types: ["Airport"] }); + state.addTestableVertexToGraph(vertex); + + renderSidebar(state); + + const tabs = screen.getAllByRole("tab"); + await user.click(tabs[1]); + + expect(screen.getByText("Airport")).toBeInTheDocument(); + }); + + test("renders edge styling content when clicking third tab", async () => { + const user = userEvent.setup(); + const state = new DbState(); + const source = createTestableVertex().with({ types: ["Airport"] }); + const target = createTestableVertex().with({ types: ["City"] }); + const edge = createTestableEdge() + .with({ type: "locatedIn" }) + .withSource(source) + .withTarget(target); + state.addTestableEdgeToGraph(edge); + + renderSidebar(state); + + const tabs = screen.getAllByRole("tab"); + await user.click(tabs[2]); + + expect(screen.getByText("locatedIn")).toBeInTheDocument(); + }); +}); diff --git a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx index d8892e319..f01d758ea 100644 --- a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx +++ b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaExplorerSidebar.tsx @@ -2,30 +2,37 @@ import { Tabs as TabsPrimitive } from "radix-ui"; import { Resizable } from "re-resizable"; import { type PropsWithChildren, useState } from "react"; -import { DetailsIcon } from "@/components"; +import { DetailsIcon, EdgeIcon, GraphIcon } from "@/components"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/Tooltip"; +import { useTranslations } from "@/hooks"; import { cn, LABELS } from "@/utils"; import type { SchemaGraphSelection } from "../SchemaGraph"; import { SchemaDetailsContent } from "./SchemaDetailsContent"; +import { SchemaEdgesStyling } from "./SchemaEdgesStyling"; import { DEFAULT_SCHEMA_SIDEBAR_WIDTH, useSchemaExplorerSidebarSize, } from "./schemaExplorerLayout"; +import { SchemaNodesStyling } from "./SchemaNodesStyling"; export type SchemaExplorerSidebarProps = { selection: SchemaGraphSelection; }; -/** Resizable sidebar for schema graph with details tab */ +/** Resizable sidebar for schema graph with details, node styling, and edge styling tabs */ export function SchemaExplorerSidebar({ selection, }: SchemaExplorerSidebarProps) { + const t = useTranslations(); + const [activeTab, setActiveTab] = useState("details"); + return ( @@ -36,10 +43,28 @@ export function SchemaExplorerSidebar({ > + + + + + + + + + + + + ); diff --git a/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaNodesStyling.tsx b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaNodesStyling.tsx new file mode 100644 index 000000000..3eeaff714 --- /dev/null +++ b/packages/graph-explorer/src/modules/SchemaGraph/Sidebar/SchemaNodesStyling.tsx @@ -0,0 +1,42 @@ +import { Virtuoso } from "react-virtuoso"; +import { Fragment } from "react/jsx-runtime"; + +import { + Divider, + Panel, + PanelContent, + PanelHeader, + PanelTitle, +} from "@/components"; +import { useDisplayVertexTypeConfigs } from "@/core"; +import { useTranslations } from "@/hooks"; +import SingleNodeStyling from "@/modules/NodesStyling/SingleNodeStyling"; + +/** Styling panel for node types in the schema explorer sidebar. */ +export function SchemaNodesStyling() { + const vtConfigs = useDisplayVertexTypeConfigs().values().toArray(); + const t = useTranslations(); + + return ( + + + {t("nodes-styling.title")} + + + ( + + {index !== 0 ? : null} + + + )} + /> + + + ); +} diff --git a/packages/graph-explorer/src/routes/SchemaExplorer/SchemaExplorer.tsx b/packages/graph-explorer/src/routes/SchemaExplorer/SchemaExplorer.tsx index 09a0757f0..dc900d773 100644 --- a/packages/graph-explorer/src/routes/SchemaExplorer/SchemaExplorer.tsx +++ b/packages/graph-explorer/src/routes/SchemaExplorer/SchemaExplorer.tsx @@ -9,6 +9,8 @@ import { } from "@/components"; import { GraphProvider } from "@/components/Graph"; import { useConfiguration } from "@/core"; +import { EdgeStyleDialog } from "@/modules/EdgesStyling"; +import { NodeStyleDialog } from "@/modules/NodesStyling"; import { SchemaGraph } from "@/modules/SchemaGraph"; export default function SchemaExplorer() { @@ -31,6 +33,8 @@ export default function SchemaExplorer() { + +