packages/graph-explorer/src/modules/CreateConnection/CreateConnection.tsx (405 lines of code) (raw):

import { useCallback, useState } from "react"; import { Button, Checkbox, FormItem, InfoTooltip, InputField, Label, SelectField, TextArea, } from "@/components"; import { ConnectionConfig, QueryEngine, NeptuneServiceType, } from "@shared/types"; import { allGraphSessionsAtom, ConfigurationContextProps, createNewConfigurationId, RawConfiguration, useWithTheme, } from "@/core"; import { activeConfigurationAtom, configurationAtom, } from "@/core/StateProvider/configuration"; import { schemaAtom } from "@/core/StateProvider/schema"; import useResetState from "@/core/StateProvider/useResetState"; import { cn, formatDate } from "@/utils"; import defaultStyles from "./CreateConnection.styles"; import { DEFAULT_FETCH_TIMEOUT, DEFAULT_NODE_EXPAND_LIMIT, } from "@/utils/constants"; import { useAtomCallback } from "jotai/utils"; type ConnectionForm = { name?: string; url?: string; queryEngine?: QueryEngine; proxyConnection?: boolean; graphDbUrl?: string; awsAuthEnabled?: boolean; serviceType?: NeptuneServiceType; awsRegion?: string; fetchTimeoutEnabled: boolean; fetchTimeoutMs?: number; nodeExpansionLimitEnabled: boolean; nodeExpansionLimit?: number; }; const CONNECTIONS_OP: { label: string; value: QueryEngine; }[] = [ { label: "Gremlin - PG (Property Graph)", value: "gremlin" }, { label: "OpenCypher - PG (Property Graph)", value: "openCypher" }, { label: "SPARQL - RDF (Resource Description Framework)", value: "sparql" }, ]; export type CreateConnectionProps = { existingConfig?: ConfigurationContextProps; onClose(): void; }; function mapToConnection(data: Required<ConnectionForm>): ConnectionConfig { return { url: data.url, queryEngine: data.queryEngine, proxyConnection: data.proxyConnection, graphDbUrl: data.graphDbUrl, awsAuthEnabled: data.awsAuthEnabled, serviceType: data.serviceType, awsRegion: data.awsRegion, fetchTimeoutMs: data.fetchTimeoutEnabled ? data.fetchTimeoutMs : undefined, nodeExpansionLimit: data.nodeExpansionLimitEnabled ? data.nodeExpansionLimit : undefined, }; } const CreateConnection = ({ existingConfig, onClose, }: CreateConnectionProps) => { const styleWithTheme = useWithTheme(); const configId = existingConfig?.id; const initialData: ConnectionForm | undefined = existingConfig ? { ...(existingConfig.connection || {}), name: existingConfig.displayLabel || existingConfig.id, fetchTimeoutEnabled: Boolean(existingConfig.connection?.fetchTimeoutMs), nodeExpansionLimitEnabled: Boolean( existingConfig.connection?.nodeExpansionLimit ), } : undefined; const onSave = useAtomCallback( useCallback( async (_get, set, data: Required<ConnectionForm>) => { if (!configId) { const newConfigId = createNewConfigurationId(); const newConfig: RawConfiguration = { id: newConfigId, displayLabel: data.name, connection: mapToConnection(data), }; await set(configurationAtom, async prevConfigMap => { const updatedConfig = new Map(await prevConfigMap); updatedConfig.set(newConfigId, newConfig); return updatedConfig; }); set(activeConfigurationAtom, newConfigId); return; } await set(configurationAtom, async prevConfigMap => { const updatedConfig = new Map(await prevConfigMap); const currentConfig = updatedConfig.get(configId); updatedConfig.set(configId, { ...(currentConfig || {}), id: configId, displayLabel: data.name, connection: mapToConnection(data), }); return updatedConfig; }); const urlChange = initialData?.url !== data.url; const dbUrlChange = initialData?.graphDbUrl !== data.graphDbUrl; const typeChange = initialData?.queryEngine !== data.queryEngine; if (urlChange || dbUrlChange || typeChange) { // Force a sync of the schema await set(schemaAtom, async prevSchemaMap => { const updatedSchema = new Map(await prevSchemaMap); const currentSchema = updatedSchema.get(configId); updatedSchema.set(configId, { vertices: currentSchema?.vertices || [], edges: currentSchema?.edges || [], prefixes: currentSchema?.prefixes || [], // If the URL or Engine change, show as not synchronized lastUpdate: undefined, lastSyncFail: undefined, triedToSync: undefined, }); return updatedSchema; }); // Delete previous session data await set(allGraphSessionsAtom, async prev => { const updatedGraphs = new Map(await prev); updatedGraphs.delete(configId); return updatedGraphs; }); } }, [ configId, initialData?.url, initialData?.graphDbUrl, initialData?.queryEngine, ] ) ); const [form, setForm] = useState<ConnectionForm>({ queryEngine: initialData?.queryEngine || "gremlin", name: initialData?.name || `Connection (${formatDate(new Date(), "yyyy-MM-dd HH:mm")})`, url: initialData?.url || "", proxyConnection: initialData?.proxyConnection || false, graphDbUrl: initialData?.graphDbUrl || "", awsAuthEnabled: initialData?.awsAuthEnabled || false, serviceType: initialData?.serviceType || "neptune-db", awsRegion: initialData?.awsRegion || "", fetchTimeoutEnabled: initialData?.fetchTimeoutEnabled || false, fetchTimeoutMs: initialData?.fetchTimeoutMs, nodeExpansionLimitEnabled: initialData?.nodeExpansionLimitEnabled || false, nodeExpansionLimit: initialData?.nodeExpansionLimit, }); const [hasError, setError] = useState(false); const onFormChange = useCallback( (attribute: keyof ConnectionForm) => (value: number | string | string[] | boolean) => { if (attribute === "serviceType" && value === "neptune-graph") { setForm(prev => ({ ...prev, [attribute]: value, ["queryEngine"]: "openCypher", })); } else if ( attribute === "fetchTimeoutEnabled" && typeof value === "boolean" ) { setForm(prev => ({ ...prev, [attribute]: value, ["fetchTimeoutMs"]: value ? DEFAULT_FETCH_TIMEOUT : undefined, })); } else if ( attribute === "nodeExpansionLimitEnabled" && typeof value === "boolean" ) { setForm(prev => ({ ...prev, [attribute]: value, ["nodeExpansionLimit"]: value ? DEFAULT_NODE_EXPAND_LIMIT : undefined, })); } else { setForm(prev => ({ ...prev, [attribute]: value, })); } }, [] ); const reset = useResetState(); const onSubmit = useCallback(() => { if (!form.name || !form.url || !form.queryEngine) { setError(true); return; } if (form.proxyConnection && !form.graphDbUrl) { setError(true); return; } if (form.awsAuthEnabled && (!form.awsRegion || !form.serviceType)) { setError(true); return; } onSave(form as Required<ConnectionForm>); reset(); onClose(); }, [form, onClose, onSave, reset]); return ( <div className={cn(styleWithTheme(defaultStyles), "flex flex-col gap-6")}> <div className="space-y-6"> <FormItem> <Label>Name</Label> <InputField aria-label="Name" value={form.name} onChange={onFormChange("name")} errorMessage="Name is required" validationState={hasError && !form.name ? "invalid" : "valid"} /> </FormItem> <FormItem> <Label>Graph Type</Label> <SelectField options={CONNECTIONS_OP} value={form.queryEngine} onValueChange={onFormChange("queryEngine")} disabled={form.serviceType === "neptune-graph"} /> </FormItem> <FormItem> <Label> Public or Proxy Endpoint <InfoTooltip> Provide the endpoint URL for an open graph database, e.g., Gremlin Server. If connecting to Amazon Neptune, then provide a proxy endpoint URL that is accessible from outside the VPC, e.g., EC2. </InfoTooltip> </Label> <TextArea aria-label="Public or Proxy Endpoint" data-autofocus={true} value={form.url} onChange={onFormChange("url")} errorMessage="URL is required" placeholder="https://example.com" validationState={hasError && !form.url ? "invalid" : "valid"} /> </FormItem> <Label className="cursor-pointer"> <Checkbox value="proxyConnection" checked={form.proxyConnection} onCheckedChange={checked => { onFormChange("proxyConnection")(checked); }} /> Using Proxy-Server </Label> {form.proxyConnection && ( <FormItem> <Label>Graph Connection URL</Label> <TextArea aria-label="Graph Connection URL" data-autofocus={true} value={form.graphDbUrl} onChange={onFormChange("graphDbUrl")} errorMessage="URL is required" placeholder="https://neptune-cluster.amazonaws.com" validationState={ hasError && !form.graphDbUrl ? "invalid" : "valid" } /> </FormItem> )} {form.proxyConnection && ( <Label className="cursor-pointer"> <Checkbox value="awsAuthEnabled" checked={form.awsAuthEnabled} onCheckedChange={checked => { onFormChange("awsAuthEnabled")(checked); }} /> AWS IAM Auth Enabled </Label> )} {form.proxyConnection && form.awsAuthEnabled && ( <> <FormItem> <Label>AWS Region</Label> <InputField aria-label="AWS Region" data-autofocus={true} value={form.awsRegion} onChange={onFormChange("awsRegion")} errorMessage="Region is required" placeholder="us-east-1" validationState={ hasError && !form.awsRegion ? "invalid" : "valid" } /> </FormItem> <FormItem> <Label>Service Type</Label> <SelectField options={[ { label: "Neptune DB", value: "neptune-db" }, { label: "Neptune Analytics", value: "neptune-graph" }, ]} value={form.serviceType} onValueChange={onFormChange("serviceType")} /> </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"> Enable Node 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>Node Expansion Limit</Label> <InputField aria-label="Node Expansion Limit" type="number" value={form.nodeExpansionLimit} onChange={onFormChange("nodeExpansionLimit")} min={0} /> </FormItem> )} </div> <div className="flex justify-between border-t pt-4"> <Button variant="default" onPress={onClose}> Cancel </Button> <Button variant="filled" onPress={onSubmit}> {!configId ? "Add Connection" : "Update Connection"} </Button> </div> </div> ); }; export default CreateConnection;