Skip to content
Closed
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
72 changes: 70 additions & 2 deletions packages/graph-explorer/src/core/AppStatusLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type PropsWithChildren,
startTransition,
Suspense,
useState,
useEffect,
} from "react";

Expand All @@ -12,6 +13,15 @@ import { logger } from "@/utils";

import { fetchDefaultConnection } from "./defaultConnection";
import { activeConfigurationAtom, configurationAtom } from "./StateProvider";
import { UrlConnectionDialog } from "./UrlConnectionDialog";
import {
parseUrlConnectionParams,
findMatchingConnection,
buildConnectionFromParams,
} from "./urlConnectionParams";

// Read URL params once at module level
const initialUrlParams = parseUrlConnectionParams(window.location.search);

function AppStatusLoader({ children }: PropsWithChildren) {
return (
Expand All @@ -24,6 +34,7 @@ function AppStatusLoader({ children }: PropsWithChildren) {
function LoadDefaultConfig({ children }: PropsWithChildren) {
const [activeConfig, setActiveConfig] = useAtom(activeConfigurationAtom);
const [configuration, setConfiguration] = useAtom(configurationAtom);
const [urlHandled, setUrlHandled] = useState(!initialUrlParams);

const defaultConfigQuery = useQuery({
queryKey: ["default-connection"],
Expand Down Expand Up @@ -65,6 +76,53 @@ function LoadDefaultConfig({ children }: PropsWithChildren) {
defaultConnectionConfigs,
]);

// Compute dialog state during render
const existingMatch =
!urlHandled && initialUrlParams
? findMatchingConnection(configuration, initialUrlParams)
: null;
const pendingConnection =
!urlHandled && initialUrlParams && !existingMatch
? buildConnectionFromParams(initialUrlParams, window.location.origin)
: null;

function handleConfirm() {
if (existingMatch) {
logger.debug(
"Activating matching connection from URL params",
existingMatch.id,
);
startTransition(() => {
setActiveConfig(existingMatch.id);
});
} else if (pendingConnection) {
logger.debug("Adding connection from URL params", pendingConnection);
startTransition(() => {
setConfiguration(prev => {
const updated = new Map(prev);
updated.set(pendingConnection.id, pendingConnection);
return updated;
});
setActiveConfig(pendingConnection.id);
});
}
setUrlHandled(true);
window.history.replaceState(
{},
"",
window.location.pathname + window.location.hash,
);
}

function handleCancel() {
setUrlHandled(true);
window.history.replaceState(
{},
"",
window.location.pathname + window.location.hash,
);
}

if (configuration.size === 0 && defaultConfigQuery.isLoading) {
return (
<PanelEmptyState
Expand All @@ -75,7 +133,6 @@ function LoadDefaultConfig({ children }: PropsWithChildren) {
);
}

// Loading from config file if exists
if (
configuration.size === 0 &&
defaultConnectionConfigs &&
Expand All @@ -90,7 +147,18 @@ function LoadDefaultConfig({ children }: PropsWithChildren) {
);
}

return <>{children}</>;
return (
<>
{children}
<UrlConnectionDialog
open={!urlHandled && !!initialUrlParams}
connection={existingMatch ?? pendingConnection}
isExisting={!!existingMatch}
onConfirm={handleConfirm}
onCancel={handleCancel}
/>
</>
);
}

function PreparingEnvironment() {
Expand Down
71 changes: 71 additions & 0 deletions packages/graph-explorer/src/core/UrlConnectionDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Button } from "@/components/Button/Button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogBody,
DialogFooter,
} from "@/components/Dialog";

import type { RawConfiguration } from "./ConfigurationProvider";

interface UrlConnectionDialogProps {
open: boolean;
connection: RawConfiguration | null;
isExisting: boolean;
onConfirm: () => void;
onCancel: () => void;
}

export function UrlConnectionDialog({
open,
connection,
isExisting,
onConfirm,
onCancel,
}: UrlConnectionDialogProps) {
return (
<Dialog open={open} onOpenChange={o => !o && onCancel()}>
<DialogContent>
<DialogHeader>
<DialogTitle>
{isExisting ? "Activate Connection" : "Create Connection"}
</DialogTitle>
</DialogHeader>
<DialogBody>
{connection && (
<div className="space-y-2 text-sm">
<p>
<span className="font-medium">Name:</span>{" "}
{connection.displayLabel}
</p>
<p>
<span className="font-medium">Endpoint:</span>{" "}
{connection.connection?.graphDbUrl}
</p>
<p>
<span className="font-medium">Query Engine:</span>{" "}
{connection.connection?.queryEngine}
</p>
{connection.connection?.awsRegion && (
<p>
<span className="font-medium">Region:</span>{" "}
{connection.connection.awsRegion}
</p>
)}
</div>
)}
</DialogBody>
<DialogFooter>
<Button variant="ghost" onClick={onCancel}>
Cancel
</Button>
<Button variant="primary" onClick={onConfirm}>
{isExisting ? "Connect" : "Create"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
185 changes: 185 additions & 0 deletions packages/graph-explorer/src/core/urlConnectionParams.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import type {
ConfigurationId,
RawConfiguration,
} from "./ConfigurationProvider";

import {
parseUrlConnectionParams,
findMatchingConnection,
buildConnectionFromParams,
} from "./urlConnectionParams";

describe("parseUrlConnectionParams", () => {
test("returns null when graphDbUrl is missing", () => {
expect(parseUrlConnectionParams("")).toBeNull();
expect(parseUrlConnectionParams("?queryEngine=openCypher")).toBeNull();
});

test("parses graphDbUrl with defaults", () => {
const result = parseUrlConnectionParams(
"?graphDbUrl=https%3A%2F%2Fg-xxx.us-west-2.neptune-graph.amazonaws.com",
);
expect(result).toEqual({
graphDbUrl: "https://g-xxx.us-west-2.neptune-graph.amazonaws.com",
queryEngine: "gremlin",
awsRegion: "",
serviceType: "",
name: "https://g-xxx.us-west-2.neptune-graph.amazonaws.com",
});
});

test("parses all parameters", () => {
const result = parseUrlConnectionParams(
"?graphDbUrl=https%3A%2F%2Fg-xxx.neptune-graph.amazonaws.com&queryEngine=openCypher&awsRegion=us-west-2&serviceType=neptune-graph&name=My+Graph",
);
expect(result).toEqual({
graphDbUrl: "https://g-xxx.neptune-graph.amazonaws.com",
queryEngine: "openCypher",
awsRegion: "us-west-2",
serviceType: "neptune-graph",
name: "My Graph",
});
});
});

describe("findMatchingConnection", () => {
const configs = new Map<ConfigurationId, RawConfiguration>([
[
"conn-1" as ConfigurationId,
{
id: "conn-1" as ConfigurationId,
displayLabel: "Test",
connection: {
url: "https://localhost",
queryEngine: "openCypher",
graphDbUrl: "https://g-abc.us-west-2.neptune-graph.amazonaws.com",
},
},
],
[
"conn-2" as ConfigurationId,
{
id: "conn-2" as ConfigurationId,
displayLabel: "Gremlin DB",
connection: {
url: "https://localhost",
queryEngine: "gremlin",
graphDbUrl: "https://my-cluster.neptune.amazonaws.com",
},
},
],
]);

test("finds match by graphDbUrl and queryEngine", () => {
const match = findMatchingConnection(configs, {
graphDbUrl: "https://g-abc.us-west-2.neptune-graph.amazonaws.com",
queryEngine: "openCypher",
awsRegion: "",
serviceType: "",
name: "",
});
expect(match?.id).toBe("conn-1");
});

test("matches case-insensitively on graphDbUrl", () => {
const match = findMatchingConnection(configs, {
graphDbUrl: "https://G-ABC.US-WEST-2.NEPTUNE-GRAPH.AMAZONAWS.COM",
queryEngine: "openCypher",
awsRegion: "",
serviceType: "",
name: "",
});
expect(match?.id).toBe("conn-1");
});

test("returns null when queryEngine differs", () => {
const match = findMatchingConnection(configs, {
graphDbUrl: "https://g-abc.us-west-2.neptune-graph.amazonaws.com",
queryEngine: "gremlin",
awsRegion: "",
serviceType: "",
name: "",
});
expect(match).toBeNull();
});

test("returns null when no match", () => {
const match = findMatchingConnection(configs, {
graphDbUrl: "https://unknown.neptune.amazonaws.com",
queryEngine: "gremlin",
awsRegion: "",
serviceType: "",
name: "",
});
expect(match).toBeNull();
});
});

describe("buildConnectionFromParams", () => {
test("builds connection with IAM enabled", () => {
const connection = buildConnectionFromParams(
{
graphDbUrl: "https://g-xxx.neptune-graph.amazonaws.com",
queryEngine: "openCypher",
awsRegion: "us-west-2",
serviceType: "neptune-graph",
name: "My Graph",
},
"https://localhost",
);

expect(connection.id).toBe(
"url-https://g-xxx.neptune-graph.amazonaws.com-openCypher",
);
expect(connection.displayLabel).toBe("My Graph");
expect(connection.connection).toEqual({
url: "https://localhost",
queryEngine: "openCypher",
proxyConnection: true,
graphDbUrl: "https://g-xxx.neptune-graph.amazonaws.com",
awsAuthEnabled: true,
awsRegion: "us-west-2",
serviceType: "neptune-graph",
});
});

test("builds connection with IAM disabled when region/serviceType missing", () => {
const connection = buildConnectionFromParams(
{
graphDbUrl: "https://g-xxx.neptune-graph.amazonaws.com",
queryEngine: "gremlin",
awsRegion: "",
serviceType: "",
name: "No IAM",
},
"https://localhost",
);

expect(connection.connection?.awsAuthEnabled).toBe(false);
expect(connection.connection?.serviceType).toBeUndefined();
});

test("generates deterministic ID", () => {
const a = buildConnectionFromParams(
{
graphDbUrl: "https://g-xxx.neptune-graph.amazonaws.com",
queryEngine: "openCypher",
awsRegion: "",
serviceType: "",
name: "A",
},
"https://localhost",
);
const b = buildConnectionFromParams(
{
graphDbUrl: "https://g-xxx.neptune-graph.amazonaws.com",
queryEngine: "openCypher",
awsRegion: "",
serviceType: "",
name: "B",
},
"https://localhost",
);
expect(a.id).toBe(b.id);
});
});
Loading