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
107 changes: 107 additions & 0 deletions packages/graph-explorer/src/connections/activeConnection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { QueryEngine } from "@shared/types";

import { createStore } from "jotai";
import { describe, expect, test } from "vitest";

import type { RawConfiguration } from "@/connections";

import {
activeConfigurationAtom,
configurationAtom,
} from "@/core/StateProvider/storageAtoms";
import { createRandomRawConfiguration } from "@/utils/testing";

import {
activeConfigSelector,
activeConnectionAtom,
queryEngineSelector,
} from "./activeConnection";

function connectionWithEngine(queryEngine: QueryEngine): RawConfiguration {
const config = createRandomRawConfiguration();
return {
...config,
connection: { ...config.connection!, queryEngine },
};
}

function storeWithActiveConnection(config: RawConfiguration) {
const store = createStore();
store.set(configurationAtom, new Map([[config.id, config]]));
store.set(activeConfigurationAtom, config.id);
return store;
}

describe("activeConfigSelector", () => {
test("resolves the active connection's config", () => {
const config = createRandomRawConfiguration();
const store = storeWithActiveConnection(config);

expect(store.get(activeConfigSelector)).toBe(config);
});

// A tab's active connection lives in per-tab sessionStorage, but the
// connections map is shared and only refreshed on reload. A connection
// deleted in another tab leaves this tab pointing at a missing id. The
// selector must degrade to null (the connection screen) rather than expose a
// dangling pointer.
test("resolves to null when the active connection was deleted in another tab", () => {
const deletedConfig = createRandomRawConfiguration();
const store = createStore();
store.set(configurationAtom, new Map());
store.set(activeConfigurationAtom, deletedConfig.id);

expect(store.get(activeConfigSelector)).toBeNull();
});
});

describe("activeConnectionAtom", () => {
test("resolves to null when there is no active connection", () => {
const store = createStore();

expect(store.get(activeConnectionAtom)).toBeNull();
});

test("resolves to null when the active connection has no connection details", () => {
const config: RawConfiguration = {
...createRandomRawConfiguration(),
connection: undefined,
};
const store = storeWithActiveConnection(config);

expect(store.get(activeConnectionAtom)).toBeNull();
});

test("normalizes the active connection's URL and applies defaults", () => {
const config: RawConfiguration = {
...createRandomRawConfiguration(),
connection: { graphDbUrl: "https://neptune.example.com:8182/\n" },
};
const store = storeWithActiveConnection(config);

expect(store.get(activeConnectionAtom)).toStrictEqual({
graphDbUrl: "https://neptune.example.com:8182",
queryEngine: "gremlin",
awsAuthEnabled: false,
});
});
});

describe("queryEngineSelector", () => {
test("falls back to gremlin when there is no active connection", () => {
const store = createStore();

expect(store.get(queryEngineSelector)).toBe("gremlin");
});

test.each(["gremlin", "openCypher", "sparql"] as const)(
"reports the active connection's %s engine",
queryEngine => {
const store = storeWithActiveConnection(
connectionWithEngine(queryEngine),
);

expect(store.get(queryEngineSelector)).toBe(queryEngine);
},
);
});
41 changes: 41 additions & 0 deletions packages/graph-explorer/src/connections/activeConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { atom, useAtomValue } from "jotai";
import { selectAtom } from "jotai/utils";
import { isEqual } from "lodash";

import {
activeConfigurationAtom,
configurationAtom,
} from "@/core/StateProvider/storageAtoms";

import { normalizeConnection } from "./normalizeConnection";

/** Gets the currently active config. */
export const activeConfigSelector = atom(get => {
const configMap = get(configurationAtom);
const id = get(activeConfigurationAtom);
// The id may point at a connection deleted in another tab, so a map miss
// resolves to null (no active connection) rather than a dangling pointer.
return (id && configMap.get(id)) ?? null;
});

export const activeConnectionAtom = atom(get => {
const connection = get(
selectAtom(activeConfigSelector, c => c?.connection, isEqual),
);
if (!connection) {
return null;
}
return normalizeConnection(connection);
});

export const queryEngineSelector = atom(get =>
get(
selectAtom(activeConnectionAtom, c =>
c && c.queryEngine ? c.queryEngine : "gremlin",
),
),
);

export function useQueryEngine() {
return useAtomValue(queryEngineSelector);
}
1 change: 1 addition & 0 deletions packages/graph-explorer/src/connections/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./activeConnection";
export * from "./legacyConnection";
export * from "./normalizeConnection";
export * from "./types";
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { createRandomName } from "@shared/utils/testing";
import { createStore } from "jotai";
import { describe, expect, it, test } from "vitest";

import type { NormalizedConnection, RawConfiguration } from "@/connections";

import { activeConfigurationAtom, configurationAtom } from "@/core";
import {
createEdgeType,
createVertexType,
Expand All @@ -29,8 +27,6 @@ import type { EdgeStyleStorage, VertexStyleStorage } from "./graphStyles";
import type { SchemaStorageModel } from "./schema";

import {
activeConfigSelector,
activeConnectionAtom,
defaultEdgeTypeConfig,
defaultVertexTypeConfig,
getDefaultEdgeTypeConfig,
Expand Down Expand Up @@ -390,64 +386,3 @@ describe("getDefaultEdgeTypeConfig", () => {
});
});
});

describe("activeConfigSelector", () => {
test("resolves the active connection's config", () => {
const config = createRandomRawConfiguration();
const store = createStore();
store.set(configurationAtom, new Map([[config.id, config]]));
store.set(activeConfigurationAtom, config.id);

expect(store.get(activeConfigSelector)).toBe(config);
});

// A tab's active connection lives in per-tab sessionStorage, but the
// connections map is shared and only refreshed on reload. A connection
// deleted in another tab leaves this tab pointing at a missing id. The
// selector must degrade to null (the connection screen) rather than expose a
// dangling pointer.
test("resolves to null when the active connection was deleted in another tab", () => {
const deletedConfig = createRandomRawConfiguration();
const store = createStore();
store.set(configurationAtom, new Map());
store.set(activeConfigurationAtom, deletedConfig.id);

expect(store.get(activeConfigSelector)).toBeNull();
});
});

describe("activeConnectionAtom", () => {
test("resolves to null when there is no active connection", () => {
const store = createStore();

expect(store.get(activeConnectionAtom)).toBeNull();
});

test("resolves to null when the active connection has no connection details", () => {
const config: RawConfiguration = {
...createRandomRawConfiguration(),
connection: undefined,
};
const store = createStore();
store.set(configurationAtom, new Map([[config.id, config]]));
store.set(activeConfigurationAtom, config.id);

expect(store.get(activeConnectionAtom)).toBeNull();
});

test("normalizes the active connection's URL and applies defaults", () => {
const config: RawConfiguration = {
...createRandomRawConfiguration(),
connection: { graphDbUrl: "https://neptune.example.com:8182/\n" },
};
const store = createStore();
store.set(configurationAtom, new Map([[config.id, config]]));
store.set(activeConfigurationAtom, config.id);

expect(store.get(activeConnectionAtom)).toStrictEqual({
graphDbUrl: "https://neptune.example.com:8182",
queryEngine: "gremlin",
awsAuthEnabled: false,
});
});
});
25 changes: 1 addition & 24 deletions packages/graph-explorer/src/core/StateProvider/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { atom } from "jotai";
import { selectAtom } from "jotai/utils";
import { isEqual } from "lodash";

import type { RawConfiguration } from "@/connections";

import { normalizeConnection } from "@/connections";
import { activeConfigSelector, normalizeConnection } from "@/connections";
import {
activeConfigurationAtom,
type AttributeConfig,
configurationAtom,
userEdgeStylesAtom,
type EdgeType,
type EdgeTypeConfig,
Expand All @@ -27,25 +23,6 @@ import {
} from "./graphStyles";
import { activeSchemaSelector, type SchemaStorageModel } from "./schema";

/** Gets the currently active config. */
export const activeConfigSelector = atom(get => {
const configMap = get(configurationAtom);
const id = get(activeConfigurationAtom);
// The id may point at a connection deleted in another tab, so a map miss
// resolves to null (no active connection) rather than a dangling pointer.
return (id && configMap.get(id)) ?? null;
});

export const activeConnectionAtom = atom(get => {
const connection = get(
selectAtom(activeConfigSelector, c => c?.connection, isEqual),
);
if (!connection) {
return null;
}
return normalizeConnection(connection);
});

export const mergedConfigurationSelector = atom(get => {
const currentConfig = get(activeConfigSelector);
if (!currentConfig) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { atom, useAtomValue } from "jotai";
import { atomFamily } from "jotai-family";

import { queryEngineSelector } from "@/connections";
import {
type DisplayAttribute,
displayEdgeTypeConfigSelector,
Expand All @@ -13,7 +14,6 @@ import {
type EdgeType,
getRawId,
getSortedDisplayAttributes,
queryEngineSelector,
useEdgeInCanvas,
type VertexId,
} from "@/core";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { atom, useAtomValue } from "jotai";
import { atomFamily } from "jotai-family";

import { queryEngineSelector } from "@/connections";
import {
type DisplayAttribute,
getRawId,
getSortedDisplayAttributes,
nodesAtom,
nodeSelector,
nodesSelectedIdsAtom,
queryEngineSelector,
useVertex,
type Vertex,
type VertexId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAtom } from "jotai";

import { useQueryEngine } from "../connector";
import { useQueryEngine } from "@/connections";

import {
DEFAULT_SIDEBAR_WIDTH,
DEFAULT_TABLE_VIEW_HEIGHT,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { ConfigurationId, RawConfiguration } from "@/connections";

// Deep import: this module's top-level await creates the atoms, so its import
// graph must not include atom readers the index may export.
// graph must not include atom readers. The `@/connections` index exports the
// active-connection selectors, which read these atoms, so the transform is
// imported from its own file instead.
import { transformConfiguration } from "@/connections/legacyConnection";

import type { EdgeType, VertexType } from "../entities";
Expand Down
25 changes: 1 addition & 24 deletions packages/graph-explorer/src/core/connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import type { RawConfiguration } from "@/connections";
import { emptyExplorer } from "@/connector/emptyExplorer";
import { createRandomRawConfiguration } from "@/utils/testing";

import {
explorerAtom,
explorerForTestingAtom,
queryEngineSelector,
} from "./connector";
import { explorerAtom, explorerForTestingAtom } from "./connector";
import { activeConfigurationAtom, configurationAtom } from "./StateProvider";

function connectionWithEngine(queryEngine: QueryEngine): RawConfiguration {
Expand All @@ -30,25 +26,6 @@ function storeWithActiveConnection(config: RawConfiguration) {
return store;
}

describe("queryEngineSelector", () => {
test("falls back to gremlin when there is no active connection", () => {
const store = createStore();

expect(store.get(queryEngineSelector)).toBe("gremlin");
});

test.each(["gremlin", "openCypher", "sparql"] as const)(
"reports the active connection's %s engine",
queryEngine => {
const store = storeWithActiveConnection(
connectionWithEngine(queryEngine),
);

expect(store.get(queryEngineSelector)).toBe(queryEngine);
},
);
});

describe("explorerAtom", () => {
test("returns the empty explorer when there is no active connection", () => {
const store = createStore();
Expand Down
Loading
Loading