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
3 changes: 3 additions & 0 deletions docs/features/connections.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ For guides on connecting to specific databases, see [Connecting to databases](..
- **AWS IAM Auth Enabled:** Check this box if connecting to Amazon Neptune using IAM Auth and SigV4 signed requests
- **Service Type:** Choose the service type
- **AWS Region:** Specify the AWS region where the Neptune cluster is hosted (e.g., us-east-1)

The next two settings are grouped under an **Advanced options** section that you expand to reach. It starts expanded when the connection you are editing already overrides one of them, so an existing override is never hidden from you.

- **Fetch Timeout:** Specify the timeout for the fetch request
- **Neighbor Expansion Limit:** Specify the default limit for neighbor expansion. This will override the app setting for neighbor expansion.

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ There are multiple sources of timeouts.

Graph Explorer distinguishes two kinds of timeout and shows a different message for each.

**Fetch timeout exceeded** means the request didn't finish within the connection's own Fetch Timeout setting. This is a client-side limit you configure yourself in the connection's settings, under "Enable Fetch Timeout". Increase the Fetch Timeout value, or retry the request.
**Fetch timeout exceeded** means the request didn't finish within the connection's own Fetch Timeout setting. This is a client-side limit you configure yourself in this connection's advanced options, under "Enable Fetch Timeout". Increase the Fetch Timeout value, or retry the request.

**Database query timed out** means the database itself stopped the query because it ran longer than the database's configured query timeout. For Neptune, this is controlled by the DB cluster parameter group. Increase the query timeout there, or retry the request.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
// @vitest-environment happy-dom

import type { ConnectionConfig } from "@shared/types";

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, test, vi } from "vitest";

import { TooltipProvider } from "@/components";
import { configurationAtom, getAppStore } from "@/core";
import {
type ConfigurationContextProps,
configurationAtom,
createNewConfigurationId,
getAppStore,
} from "@/core";
import { createQueryClient } from "@/core/queryClient";
import { mergeConfiguration } from "@/core/StateProvider/configuration";
import { createRandomRawConfiguration, TestProvider } from "@/utils/testing";
Expand All @@ -26,6 +34,14 @@ function renderCreateConnection(ui: React.ReactElement) {
return store;
}

/** The advanced settings are behind a disclosure, so their content is unmounted until it opens. */
async function openAdvancedOptions(user: ReturnType<typeof userEvent.setup>) {
const trigger = screen.getByRole("button", { name: "Advanced options" });
expect(trigger).toHaveAttribute("aria-expanded", "false");
await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "true");
}

describe("CreateConnection", () => {
test("removes newlines and surrounding whitespace from URL fields", async () => {
const user = userEvent.setup();
Expand Down Expand Up @@ -62,6 +78,7 @@ describe("CreateConnection", () => {
test("labels the override field Neighbor Expansion Limit", async () => {
const user = userEvent.setup();
renderCreateConnection(<CreateConnection onClose={vi.fn()} />);
await openAdvancedOptions(user);

await user.click(
screen.getByRole("checkbox", {
Expand All @@ -74,6 +91,61 @@ describe("CreateConnection", () => {
).toBeInTheDocument();
});

test("keeps the advanced options collapsed until the user expands them", async () => {
const user = userEvent.setup();
renderCreateConnection(<CreateConnection onClose={vi.fn()} />);

expect(
screen.queryByRole("checkbox", { name: /Enable Fetch Timeout/ }),
).not.toBeInTheDocument();

await openAdvancedOptions(user);

expect(
screen.getByRole("checkbox", { name: /Enable Fetch Timeout/ }),
).toBeInTheDocument();
expect(
screen.getByRole("checkbox", {
name: /Override Default Neighbor Expansion Limit/,
}),
).toBeInTheDocument();
});

test("opens the advanced options when the connection already overrides one", () => {
const configId = createNewConfigurationId();
const store = getAppStore();
const connection: ConnectionConfig = {
url: "https://proxy.example.com",
graphDbUrl: "",
queryEngine: "gremlin",
fetchTimeoutMs: 30000,
};
store.set(
configurationAtom,
new Map([[configId, { id: configId, connection }]]),
);

render(
<TestProvider client={createQueryClient()} store={store}>
<TooltipProvider>
<CreateConnection
existingConfig={
{ id: configId, connection } as ConfigurationContextProps
}
onClose={vi.fn()}
/>
</TooltipProvider>
</TestProvider>,
);

expect(
screen.getByRole("button", { name: "Advanced options" }),
).toHaveAttribute("aria-expanded", "true");
expect(
screen.getByRole("checkbox", { name: /Enable Fetch Timeout/ }),
).toBeChecked();
});

test("rejects a URL that is empty after normalization", async () => {
const user = userEvent.setup();
const store = renderCreateConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import type {

import { useQueryClient } from "@tanstack/react-query";
import { useAtomCallback } from "jotai/utils";
import { ChevronRightIcon } from "lucide-react";
import { useCallback, useState } from "react";

import {
Button,
Checkbox,
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
FormItem,
InfoTooltip,
InputField,
Expand Down Expand Up @@ -90,6 +94,15 @@ function mapToConnection(data: Required<ConnectionForm>): ConnectionConfig {
};
}

/**
* Whether the advanced disclosure should start open. A connection that already
* overrides one of these settings would otherwise hide that fact behind a
* collapsed section, so editing it looks like the defaults are in force.
*/
function hasAdvancedOverrides(form: ConnectionForm): boolean {
return form.fetchTimeoutEnabled || form.nodeExpansionLimitEnabled;
}

/**
* Maps a connection into form values under the given name. The caller picks the
* name because only it knows the fallback: a stored connection falls back to
Expand Down Expand Up @@ -395,66 +408,79 @@ const CreateConnection = ({
</FormItem>
</>
)}
<FormItem>
<Label className="cursor-pointer">
<Checkbox
value="fetchTimeoutEnabled"
checked={form.fetchTimeoutEnabled}
onCheckedChange={checked => {
onFormChange("fetchTimeoutEnabled")(checked);
}}
/>
<span className="flex items-center gap-2">
Enable Fetch Timeout
<InfoTooltip>
Large datasets may require a large amount of time to fetch. If
the timeout is exceeded, the request will be cancelled.
</InfoTooltip>
</span>
</Label>
</FormItem>
{form.fetchTimeoutEnabled && (
<FormItem>
<Label>Fetch Timeout (ms)</Label>
<InputField
aria-label="Fetch Timeout (ms)"
type="number"
value={form.fetchTimeoutMs}
onChange={onFormChange("fetchTimeoutMs")}
min={0}
/>
</FormItem>
)}
<FormItem>
<Label className="cursor-pointer">
<Checkbox
value="nodeExpansionLimitEnabled"
checked={form.nodeExpansionLimitEnabled}
onCheckedChange={checked => {
onFormChange("nodeExpansionLimitEnabled")(checked);
}}
/>
<span className="flex items-center gap-2">
Override Default Neighbor Expansion Limit
<InfoTooltip>
Large datasets may require a default limit to the amount of
neighbors that are returned during any single expansion.
</InfoTooltip>
</span>
</Label>
</FormItem>
{form.nodeExpansionLimitEnabled && (
<FormItem>
<Label>Neighbor Expansion Limit</Label>
<InputField
aria-label="Neighbor Expansion Limit"
type="number"
value={form.nodeExpansionLimit}
onChange={onFormChange("nodeExpansionLimit")}
min={0}
/>
</FormItem>
)}
<Collapsible
defaultOpen={hasAdvancedOverrides(form)}
className="group flex flex-col gap-6"
>
{/* Renders its own button rather than `asChild` onto a div, so the
disclosure stays keyboard operable and announces its expanded state. */}
<CollapsibleTrigger className="group/advanced-trigger focus-visible:ring-primary/50 text-foreground flex w-fit cursor-pointer flex-row items-center gap-2 rounded-md text-sm leading-tight font-medium focus-visible:ring-[3px] focus-visible:outline-hidden">
<ChevronRightIcon className="text-muted-foreground size-5 shrink-0 transition-transform duration-200 ease-in-out group-data-open/advanced-trigger:rotate-90" />
Advanced options
</CollapsibleTrigger>
<CollapsibleContent className="flex flex-col gap-6">
<FormItem>
<Label className="cursor-pointer">
<Checkbox
value="fetchTimeoutEnabled"
checked={form.fetchTimeoutEnabled}
onCheckedChange={checked => {
onFormChange("fetchTimeoutEnabled")(checked);
}}
/>
<span className="flex items-center gap-2">
Enable Fetch Timeout
<InfoTooltip>
Large datasets may require a large amount of time to fetch.
If the timeout is exceeded, the request will be cancelled.
</InfoTooltip>
</span>
</Label>
</FormItem>
{form.fetchTimeoutEnabled && (
<FormItem>
<Label>Fetch Timeout (ms)</Label>
<InputField
aria-label="Fetch Timeout (ms)"
type="number"
value={form.fetchTimeoutMs}
onChange={onFormChange("fetchTimeoutMs")}
min={0}
/>
</FormItem>
)}
<FormItem>
<Label className="cursor-pointer">
<Checkbox
value="nodeExpansionLimitEnabled"
checked={form.nodeExpansionLimitEnabled}
onCheckedChange={checked => {
onFormChange("nodeExpansionLimitEnabled")(checked);
}}
/>
<span className="flex items-center gap-2">
Override Default Neighbor Expansion Limit
<InfoTooltip>
Large datasets may require a default limit to the amount of
neighbors that are returned during any single expansion.
</InfoTooltip>
</span>
</Label>
</FormItem>
{form.nodeExpansionLimitEnabled && (
<FormItem>
<Label>Neighbor Expansion Limit</Label>
<InputField
aria-label="Neighbor Expansion Limit"
type="number"
value={form.nodeExpansionLimit}
onChange={onFormChange("nodeExpansionLimit")}
min={0}
/>
</FormItem>
)}
</CollapsibleContent>
</Collapsible>
</DialogBody>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ describe("createDisplayError", () => {
expect(result).toStrictEqual({
title: "Fetch timeout exceeded",
message:
"The request did not finish within this connection's fetch timeout of 240,000 ms. Increase the Fetch Timeout in the connection's settings, or retry the request.",
"The request did not finish within this connection's fetch timeout of 240,000 ms. Increase the Fetch Timeout in this connection's advanced options, or retry the request.",
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/graph-explorer/src/utils/createDisplayError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export function createDisplayError(error: any): DisplayError {
if (error instanceof FetchTimeoutError) {
return {
title: "Fetch timeout exceeded",
message: `The request did not finish within this connection's fetch timeout of ${error.timeoutMs.toLocaleString()} ms. Increase the Fetch Timeout in the connection's settings, or retry the request.`,
message: `The request did not finish within this connection's fetch timeout of ${error.timeoutMs.toLocaleString()} ms. Increase the Fetch Timeout in this connection's advanced options, or retry the request.`,
};
}

Expand Down
Loading