Conversation
fb3eafb to
72d2c25
Compare
2edc240 to
7202e99
Compare
7202e99 to
66514f0
Compare
|
|
||
| return { | ||
| ...parsed.data, | ||
| name: parsed.data.name ?? deriveNameFromUrl(graphDbUrl), |
There was a problem hiding this comment.
Implementation question
When the link does not include a name, this code generates one from the hostname. That generated name is later also used to choose between multiple matching connections.
Could we keep the URL-provided name optional for matching, and only derive a name when the user needs to create a new connection? This would ensure a generated display name does not accidentally influence which existing connection is selected.
A test with two connections pointing to the same database, no name in the link, and one connection named after the hostname would help confirm the expected behavior.
There was a problem hiding this comment.
Deliberate, though the rationale wasn't written down anywhere — fixed now.
The derived name is the link's identity, not just a display fallback: a nameless link identifies the connection it would have created, and what it would have created is named after the hostname. So if you open a nameless link (creating a connection named my-cluster.example.com), then later add a second connection to the same endpoint under a name of your own, reopening that original link returns to the original connection rather than picking arbitrarily.
Dropping the name from matching would fall through to matches[0], i.e. Map insertion order — which happens to give the right answer in that scenario only by accident.
I added the test you suggested (a nameless link prefers the connection its own derived name created), with the hand-named connection placed first in the map so a first-match fallthrough fails it, and recorded the reasoning on deriveNameFromUrl.
| return ( | ||
| <Routes> | ||
| <Route element={<DefaultLayout />}> | ||
| <Route path="/connect" element={<Connect />} /> |
There was a problem hiding this comment.
Spec/design decision, not an implementation defect
This dedicated #/connect?... route differs from #1788, which currently documents top-level search parameters while preserving the hash as the requested landing page. The route-based contract is coherent, but it changes the external URL API.
Can we confirm this intentionally supersedes the issue contract and update #1788 so integrators have one authoritative link format?
There was a problem hiding this comment.
Agreed it diverged from the spec. I kept the dedicated route and updated Activate or create a connection via URL parameters to match. Putting the params after the # means integrators build the link like any other in-app route, and nothing in the app shell has to read window.location.search or strip params.
| } from "@/modules/CreateConnection"; | ||
| import { logger } from "@/utils"; | ||
|
|
||
| const GRAPH_CANVAS_ROUTE = "/graph-explorer"; |
There was a problem hiding this comment.
Spec/design decision, not an implementation defect
Every outcome is hard-coded to /graph-explorer. In #1788, the caller can choose the landing page, and cancelling should strip the parameters while leaving the user on the requested/current page. With the dedicated route, that destination is no longer represented.
Is always landing on the graph canvas the intended replacement behavior? If so, the affected user stories should be updated explicitly. Otherwise, the connect route needs a validated return destination.
There was a problem hiding this comment.
Choosing the landing page is split out into Let a connection link choose which page the user lands on. For now, saving and invalid links land on the graph view and cancelling lands on the connections list, so a declined link leaves you where you can pick a connection yourself.
| "Activating matching connection from URL params", | ||
| connectionIdToActivate, | ||
| ); | ||
| activateConnection(connectionIdToActivate); |
There was a problem hiding this comment.
Spec/design decision, not an implementation defect
This silently switches to an existing matching connection. That aligns with the PR rationale and normal connection-list behavior, but #1788 also says switching to a different connection should be confirmed before its Session is reset.
Can we confirm silent activation is the final product decision and update the issue accordingly? The current implementation looks internally consistent once that contract is settled.
There was a problem hiding this comment.
Confirmed as the final product decision. Two reasons, both now recorded in the ADR:
First, there is no session to lose. Sessions are stored per connection (allGraphSessionsAtom, keyed by connection id) and useResetState only clears the in-memory view atoms — it never touches allGraphSessionsAtom. So switching swaps which session is displayed rather than destroying the previous one: return to that connection and it restores.
Second, a link cannot change an already-open window's state. Following one opens a new tab, or the user pastes it deliberately. Either way the intent to start somewhere new is explicit.
Together that makes this identical to clicking a connection in the connections list, which has never prompted. The create path keeps its friction, since it is the only one that can introduce a new database.
Worth noting the issue contradicts itself here, which is probably what prompted the question: #1788's Modules section item 3 says "prompts to switch (resetting the Session on confirm)", while that same issue's Implementation Decisions say activation is "identical to a manual connection switch". The implementation follows the latter, and I'm correcting the issue text.
| // crafted link from seeding the form with something like `javascript:` — | ||
| // defense in depth on top of the editable create form and proxy allowlist. | ||
| graphDbUrl: z.url({ protocol: /^https?$/ }), | ||
| queryEngine: z.enum(queryEngineOptions).catch("gremlin"), |
There was a problem hiding this comment.
Could we default queryEngine and serviceType only when they are absent, and reject explicitly invalid values? With .catch(), queryEngine=sql silently becomes Gremlin, while an invalid serviceType can later become neptune-db when a region is present. The resulting connection may differ from what the caller requested instead of following the existing invalid-link path.
Using optional schemas with defaults for missing values would preserve the convenience while allowing malformed explicit values to produce the invalid intent. The current coercion tests should then become rejection tests.
There was a problem hiding this comment.
Done. .catch() is gone. An absent queryEngine still defaults, but an explicit unsupported value for either param now rejects the link, and the toast names the param.
| continue. | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
| <DialogBody> |
There was a problem hiding this comment.
Dialog composition
CreateConnection already renders its own DialogBody and DialogFooter, so this outer DialogBody nests two body containers and places the footer inside the outer scrolling region. That can duplicate spacing and make the actions scroll with the form instead of remaining in the dialog footer position.
Could we render CreateConnection directly after the header, or extract form content so one component clearly owns the body and footer?
There was a problem hiding this comment.
Fixed. CreateConnection owns its body and footer. The connect route also renders the form in the page with DialogSurface now, instead of a modal over a blank page.
| // Only http(s) endpoints are meaningful, and constraining the scheme keeps a | ||
| // crafted link from seeding the form with something like `javascript:` — | ||
| // defense in depth on top of the editable create form and proxy allowlist. | ||
| graphDbUrl: z.url({ protocol: /^https?$/ }), |
There was a problem hiding this comment.
Security hardening
This accepts HTTP(S) URLs containing embedded credentials, such as https://user:password@host. Because the value comes from an external link and can be persisted as the database endpoint after confirmation, credentials could end up stored and forwarded as part of the URL.
Could the schema reject URLs whose parsed username or password is non-empty? That also avoids userinfo being used to make an unintended host look trustworthy at a glance.
There was a problem hiding this comment.
Fixed. A graphDbUrl with a username or password is rejected. I also reject backslashes, since https://evil.tld\@prod.neptune.amazonaws.com parses to evil.tld while the form shows a Neptune host.
|
|
||
| > [!NOTE] | ||
| > | ||
| > [Connection links](../features/connections.md#connection-links) never connect to a new database without your confirmation: a link whose details do not match an existing connection only pre-fills the create form for you to review. A link may switch to a connection you already created, but it cannot create one on your behalf. And in every case the proxy still rejects forwarding to any origin outside `PROXY_SERVER_ALLOWED_DB_ORIGINS`, so a crafted link cannot reach an arbitrary database. |
There was a problem hiding this comment.
Security documentation
This guarantee is broader than the current behavior. The allowlist permits any origin when PROXY_SERVER_ALLOWED_DB_ORIGINS is unset, and matching does not require an existing connection to be proxy-backed. A matching direct connection can therefore activate without proxy allowlist enforcement.
Could we either enforce proxy-backed matching for connection links, or qualify this note to say the allowlist applies only when configured and only to requests routed through the proxy?
There was a problem hiding this comment.
Agreed, that overstated it. I dropped the note from security.md, and the ADR now says the allowlist only bounds a link when PROXY_SERVER_ALLOWED_DB_ORIGINS is set.
| - **The link matches your active connection** — nothing changes. | ||
| - **The link matches a different existing connection** — Graph Explorer switches to it, the same as selecting it in the connections list. No prompt: the connection was already created and validated by you, so there is nothing new to confirm. | ||
| - **The link matches no existing connection** — the create-connection form opens, pre-filled with the link's details so you can review or edit any setting before creating it. | ||
| - **The link's details are invalid** (for example, a `graphDbUrl` that is not a valid `http`/`https` URL) — the link is ignored and a notification explains what went wrong. |
There was a problem hiding this comment.
Documentation accuracy
The notification currently reports a generic invalid-link message rather than explaining which value or rule failed. The creation flow also has two outcomes worth stating explicitly: saving creates the connection, while closing or cancelling creates nothing; both then navigate to the graph view.
Could we describe the error as generic and document those two dialog outcomes, or enhance the UI if specific validation feedback is intended?
There was a problem hiding this comment.
Fixed both. The toast now names each bad param and what it requires, for example "graphDbUrl must be a valid http or https URL", and the docs cover the save and cancel outcomes separately.
| shell. | ||
|
|
||
| Connection links are now a first-class route, `#/connect?graphDbUrl=…`. The | ||
| route resolves the params once on entry and redirects (router `navigate`, with |
There was a problem hiding this comment.
Architecture documentation
The implementation does not snapshot the intent once on entry. useUrlConnectionIntent() reads live Jotai state and resolves again on each render, including while the creation dialog is open.
Could we describe this as reactive resolution, or snapshot the initial intent if one-shot processing is an architectural invariant? CONTEXT.md uses the same “resolved once” wording and should stay aligned.
There was a problem hiding this comment.
Fixed in the code, not just the doc. The route now resolves the link once in a useState initializer and acts on that. The AppStatusLoader gate makes that safe, and a test pins the ordering.
|
Manual testing found that the invalid-link warning is not visible. Reproduction:
The PR contract specifies |
## Description - `TestProvider` now always renders a `MemoryRouter` and takes `initialEntries`, and `renderHookWithState` passes it through. Anything that reads the location works without each test wiring its own router. - Moved `DataExplorer.test.tsx` onto it. It was hand-rolling the query client, jotai, and router providers that `TestProvider` already covers. - Dropped the inner `MemoryRouter` in `SchemaDiscoveryBoundary.integration.test.tsx`. Nesting a second router inside `TestProvider` now throws. I split this out of [Add connection links via a dedicated #/connect route](#1828), whose route tests use it, so that PR only carries the feature. ## Validation Test-only change. The full suite passes. ## Related Issues Prep for [Add connection links via a dedicated #/connect route](#1828). ### Check List - [x] I confirm that my contribution is made under the terms of the Apache 2.0 license. - [x] I have verified `pnpm checks` passes with no errors. - [x] I have verified `pnpm test` passes with no failures. - [x] I have covered new added functionality with unit tests if necessary. - [ ] I have updated documentation if necessary.
## Description - Pulled the "switch to this connection" logic out of `ConnectionRow` into a `useActivateConnection` hook, with its own tests. No behavior change. The connect route in [Add connection links via a dedicated #/connect route](#1828) activates a matching connection the same way the connections list does. Sharing one hook keeps the two from drifting, and splitting it out keeps the refactor from hiding inside a feature diff. ## Validation Clicking a connection in the connections list should still activate it and reset the graph view, exactly as before. ## Related Issues Prep for [Add connection links via a dedicated #/connect route](#1828). ### Check List - [x] I confirm that my contribution is made under the terms of the Apache 2.0 license. - [x] I have verified `pnpm checks` passes with no errors. - [x] I have verified `pnpm test` passes with no failures. - [x] I have covered new added functionality with unit tests if necessary. - [ ] I have updated documentation if necessary.
66514f0 to
9301e02
Compare
## Description - `TestProvider` now always renders a `MemoryRouter` and takes `initialEntries`, and `renderHookWithState` passes it through. Anything that reads the location works without each test wiring its own router. - Moved `DataExplorer.test.tsx` onto it. It was hand-rolling the query client, jotai, and router providers that `TestProvider` already covers. - Dropped the inner `MemoryRouter` in `SchemaDiscoveryBoundary.integration.test.tsx`. Nesting a second router inside `TestProvider` now throws. I split this out of [Add connection links via a dedicated #/connect route](aws#1828), whose route tests use it, so that PR only carries the feature. ## Validation Test-only change. The full suite passes. ## Related Issues Prep for [Add connection links via a dedicated #/connect route](aws#1828). ### Check List - [x] I confirm that my contribution is made under the terms of the Apache 2.0 license. - [x] I have verified `pnpm checks` passes with no errors. - [x] I have verified `pnpm test` passes with no failures. - [x] I have covered new added functionality with unit tests if necessary. - [ ] I have updated documentation if necessary.
## Description - `TestProvider` now always renders a `MemoryRouter` and takes `initialEntries`, and `renderHookWithState` passes it through. Anything that reads the location works without each test wiring its own router. - Moved `DataExplorer.test.tsx` onto it. It was hand-rolling the query client, jotai, and router providers that `TestProvider` already covers. - Dropped the inner `MemoryRouter` in `SchemaDiscoveryBoundary.integration.test.tsx`. Nesting a second router inside `TestProvider` now throws. I split this out of [Add connection links via a dedicated #/connect route](aws#1828), whose route tests use it, so that PR only carries the feature. ## Validation Test-only change. The full suite passes. ## Related Issues Prep for [Add connection links via a dedicated #/connect route](aws#1828). ### Check List - [x] I confirm that my contribution is made under the terms of the Apache 2.0 license. - [x] I have verified `pnpm checks` passes with no errors. - [x] I have verified `pnpm test` passes with no failures. - [x] I have covered new added functionality with unit tests if necessary. - [ ] I have updated documentation if necessary.
9301e02 to
f6f255c
Compare
## Description - `CreateConnection` takes an `initialValues` prop. It prefills a new connection form and stays in add mode, so none of the edit-mode "meaningful change" reset logic runs. - `mapToConnectionForm` now takes a name and a connection body instead of a whole stored configuration, so it can map a connection that hasn't been saved and has no id. The edit form still falls back to the id when a connection has no label, which matches how the rest of the app names it. Nothing on main passes `initialValues` yet. The connect route in [Add connection links via a dedicated #/connect route](#1828) will use it to prefill the form from a link. I split this out so the form change gets reviewed separately from the feature. ## Validation The add and edit connection dialogs behave as before. New tests cover the prefill, the mapping, and editing a connection that has no label. ## Related Issues Prep for [Add connection links via a dedicated #/connect route](#1828). ### Check List - [x] I confirm that my contribution is made under the terms of the Apache 2.0 license. - [x] I have verified `pnpm checks` passes with no errors. - [x] I have verified `pnpm test` passes with no failures. - [x] I have covered new added functionality with unit tests if necessary. - [ ] I have updated documentation if necessary.
- `TestProvider` now always renders a `MemoryRouter` and takes `initialEntries`, and `renderHookWithState` passes it through. Anything that reads the location works without each test wiring its own router. - Moved `DataExplorer.test.tsx` onto it. It was hand-rolling the query client, jotai, and router providers that `TestProvider` already covers. - Dropped the inner `MemoryRouter` in `SchemaDiscoveryBoundary.integration.test.tsx`. Nesting a second router inside `TestProvider` now throws. I split this out of [Add connection links via a dedicated #/connect route](aws#1828), whose route tests use it, so that PR only carries the feature. Test-only change. The full suite passes. Prep for [Add connection links via a dedicated #/connect route](aws#1828). - [x] I confirm that my contribution is made under the terms of the Apache 2.0 license. - [x] I have verified `pnpm checks` passes with no errors. - [x] I have verified `pnpm test` passes with no failures. - [x] I have covered new added functionality with unit tests if necessary. - [ ] I have updated documentation if necessary.
4e137a0 to
4163264
Compare
External applications can deep-link into Graph Explorer with a connection preconfigured: #/connect?graphDbUrl=...&queryEngine=...&awsRegion=...&serviceType=...&name=... The route resolves the params against existing connections into one of four intents and acts on it, then redirects to the graph canvas so the connect URL never lingers in history: - none — the params target the active connection; nothing to do. - activate — the params match an existing connection; switch to it silently, the same no-prompt operation as clicking it in the connections list. - create — no match; open the create form prefilled but fully editable. This is the trust gate for the untrusted endpoint details a link can carry, and the only path that can introduce a new database. - invalid — the link's graphDbUrl failed validation; warn with a toast and ignore it. Matching identity folds in auth posture (IAM on/off, and when on, region and service type) alongside graphDbUrl and queryEngine, so a link requesting IAM never silently reuses a plaintext connection to the same endpoint, or vice versa — a mismatch falls through to the create form. graphDbUrl is validated as an http(s) URL with zod; a malformed or non-http link is ignored. The proxy base URL is derived from document.baseURI rather than window.location.origin so a link produces a working proxy connection on path-hosted deployments (e.g. a Neptune notebook at /proxy/9250/explorer/ resolves the proxy to /proxy/9250). Contract core lives in core/urlConnectionParams.ts (parse/match/build/resolve) and core/useUrlConnectionIntent.ts. Documented in the connections feature page, the security reference, CONTEXT.md, and an ADR.
Cancelling a link now lands on the connections list instead of the graph view.
4163264 to
dcae968
Compare
|
Thanks for catching this. sonner dropped toasts raised before the |
Description
Other apps can now send a user into Graph Explorer already pointed at their database with a link like this:
#/connectroute handles the link. The params sit after the#like every other route, so integrators build it the same way as any in-app link. The route always redirects withreplace, so#/connectnever lingers in history.none: it matches the active connection, so nothing changes.activate: it matches another connection, so Graph Explorer switches to it silently, the same as clicking it in the connections list.create: nothing matches, so a prefilled form opens and the user has to save it.invalid: a param failed validation, so a toast names each bad param and the link is ignored.graphDbUrl(normalized, case-insensitive),queryEngine, and IAM on/off plus region and service type all agree. A link asking for IAM never reuses a plaintext connection to the same endpoint. When several match, the active one wins, then the one named byname, then the first. A nameless link uses the hostname as its name.graphDbUrlis missing, isn't http(s), carries a username or password, or contains a backslash (which parses as a slash, so a link could show one host and connect to another)queryEngineorserviceTypeis unsupportedawsRegionisn't shaped like a regionqueryEngineis anything butopenCypherfor Neptune Analytics, which defaults to openCypher since that's all it runsAppStatusLoadergates the route until they load, so a link matching a default connection activates it instead of prompting for a duplicate. A test pins that ordering.document.baseURI, so path-hosted deployments such as Neptune notebooks work.Shared pieces this needed:
DialogSurfaceis pulled out ofDialogContentso the connect route can render the same panel without the portal.CreateConnection.onClosenow receives"saved"or"cancelled", so the route can pick the landing page.ConnectionLinkErrorgivescreateDisplayErrorone problem per offending param.The ADR is
docs/adr/20260612-connection-links.md, and the user docs are under Connection Links indocs/features/connections.md.Follow-ups tracked separately:
Validation
I tried every flow in the browser against a local Gremlin Server with air routes:
namefalling back to the hostnamehistory.back()skips#/connect.To try it, open
#/connect?graphDbUrl=http%3A%2F%2Flocalhost%3A8182&queryEngine=gremlin&name=Air%20Routesagainst a local database, then open the same link again after switching connections.Related Issues
Closes Activate or create a connection via URL parameters.
This should merge after Unify Docker image by removing SageMaker variant. That PR removes the connection's proxy
urlandproxyConnection. After it lands I'll rebase and deletederiveProxyBaseUrl, and matching ongraphDbUrlthen always compares the endpoint that is actually queried.Check List
pnpm checkspasses with no errors.pnpm testpasses with no failures.