Skip to content

Commit be9bd9c

Browse files
ericpgreen2grahamplata
authored andcommitted
OLAP Connectors: Add DSN configuration option (#6870)
* Fix bug in connector explorer * Add alternate form for DSN * Add tests * Cleanup * Give the DSN form its own error state * Remove debug line * Add `waitForURL` to tests * Update locator * Use native Clickhouse protocol for placeholder
1 parent 356b291 commit be9bd9c

File tree

9 files changed

+323
-107
lines changed

9 files changed

+323
-107
lines changed

runtime/drivers/clickhouse/clickhouse.go

+2-8
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ var spec = drivers.Spec{
3838
Type: drivers.StringPropertyType,
3939
Required: false,
4040
DisplayName: "Connection string",
41-
Placeholder: "clickhouse://localhost:9000?username=default&password=",
41+
Placeholder: "clickhouse://localhost:9000?username=default&password=password",
42+
Secret: true,
4243
NoPrompt: true,
4344
},
4445
{
@@ -81,13 +82,6 @@ var spec = drivers.Spec{
8182
DisplayName: "SSL",
8283
Description: "Use SSL to connect to the ClickHouse server",
8384
},
84-
{
85-
Key: "database",
86-
Type: drivers.StringPropertyType,
87-
Required: false,
88-
DisplayName: "Database",
89-
Description: "Specify the database within the ClickHouse server",
90-
},
9185
},
9286
ImplementsOLAP: true,
9387
}

web-common/src/features/connectors/ConnectorEntry.svelte

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
<!-- For now, only show OLAP connectors -->
2929
{#if implementsOlap}
3030
{#if connector.name}
31-
<li aria-label={connector.name} class="connector-entry">
31+
<li class="connector-entry">
3232
<button
3333
class="connector-entry-header"
34+
aria-label={connector.name}
3435
on:click={() => {
3536
store.toggleItem(connectorName);
3637
}}

web-common/src/features/entity-management/AddAssetButton.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
</DropdownMenu.Trigger>
173173
<DropdownMenu.Content align="start" class="w-[240px]">
174174
<DropdownMenu.Item
175-
aria-label="Add Source"
175+
aria-label="Add Data"
176176
class="flex gap-x-2"
177177
on:click={handleAddData}
178178
>

web-common/src/features/sources/modal/AddDataForm.svelte

+171-66
Original file line numberDiff line numberDiff line change
@@ -9,59 +9,77 @@
99
type RpcStatus,
1010
type V1ConnectorDriver,
1111
} from "@rilldata/web-common/runtime-client";
12-
import { defaults, superForm } from "sveltekit-superforms";
12+
import type { ActionResult } from "@sveltejs/kit";
13+
import { slide } from "svelte/transition";
14+
import {
15+
defaults,
16+
superForm,
17+
type SuperValidated,
18+
} from "sveltekit-superforms";
1319
import { yup } from "sveltekit-superforms/adapters";
20+
import { ButtonGroup, SubButton } from "../../../components/button-group";
1421
import { inferSourceName } from "../sourceUtils";
1522
import { humanReadableErrorMessage } from "./errors";
1623
import { submitAddDataForm } from "./submitAddDataForm";
1724
import type { AddDataFormType } from "./types";
18-
import { getYupSchema, toYupFriendlyKey } from "./yupSchemas";
25+
import { dsnSchema, getYupSchema } from "./yupSchemas";
26+
27+
const FORM_TRANSITION_DURATION = 150;
1928
2029
export let connector: V1ConnectorDriver;
2130
export let formType: AddDataFormType;
2231
export let onBack: () => void;
2332
export let onClose: () => void;
2433
25-
$: formId = `add-data-${connector.name}-form`;
26-
27-
$: isSourceForm = formType === "source";
28-
$: isConnectorForm = formType === "connector";
29-
$: properties = isConnectorForm
30-
? (connector.configProperties ?? [])
31-
: (connector.sourceProperties ?? []);
32-
33-
let rpcError: RpcStatus | null = null;
34+
const isSourceForm = formType === "source";
35+
const isConnectorForm = formType === "connector";
3436
37+
// Form 1: Individual parameters
38+
const formId = `add-data-${connector.name}-form`;
39+
const properties =
40+
(isSourceForm
41+
? connector.sourceProperties
42+
: connector.configProperties?.filter(
43+
(property) => property.key !== "dsn",
44+
)) ?? [];
3545
const schema = yup(getYupSchema[connector.name as keyof typeof getYupSchema]);
36-
3746
const { form, errors, enhance, tainted, submit, submitting } = superForm(
3847
defaults(schema),
3948
{
4049
SPA: true,
4150
validators: schema,
42-
async onUpdate({ form }) {
43-
if (!form.valid) return;
44-
const values = form.data;
45-
if (isSourceForm) {
46-
try {
47-
await submitAddDataForm(queryClient, formType, connector, values);
48-
onClose();
49-
} catch (e) {
50-
rpcError = e?.response?.data;
51-
}
52-
return;
53-
}
54-
55-
// Connectors
56-
try {
57-
await submitAddDataForm(queryClient, formType, connector, values);
58-
onClose();
59-
} catch (e) {
60-
rpcError = e?.response?.data;
61-
}
62-
},
51+
onUpdate: handleOnUpdate,
6352
},
6453
);
54+
let rpcError: RpcStatus | null = null;
55+
56+
// Form 2: DSN
57+
// SuperForms are not meant to have dynamic schemas, so we use a different form instance for the DSN form
58+
let useDsn = false;
59+
const hasDsnFormOption =
60+
isConnectorForm &&
61+
connector.configProperties?.some((property) => property.key === "dsn");
62+
const dsnFormId = `add-data-${connector.name}-dsn-form`;
63+
const dsnProperties =
64+
connector.configProperties?.filter((property) => property.key === "dsn") ??
65+
[];
66+
const dsnYupSchema = yup(dsnSchema);
67+
const {
68+
form: dsnForm,
69+
errors: dsnErrors,
70+
enhance: dsnEnhance,
71+
submit: dsnSubmit,
72+
submitting: dsnSubmitting,
73+
} = superForm(defaults(dsnYupSchema), {
74+
SPA: true,
75+
validators: dsnYupSchema,
76+
onUpdate: handleOnUpdate,
77+
});
78+
let dsnRpcError: RpcStatus | null = null;
79+
80+
function handleConnectionTypeChange(e: CustomEvent<any>): void {
81+
useDsn = e.detail === "dsn";
82+
}
6583
6684
function onStringInputChange(event: Event) {
6785
const target = event.target as HTMLInputElement;
@@ -80,57 +98,103 @@
8098
);
8199
}
82100
}
101+
102+
async function handleOnUpdate<
103+
T extends Record<string, unknown>,
104+
M = any,
105+
In extends Record<string, unknown> = T,
106+
>(event: {
107+
form: SuperValidated<T, M, In>;
108+
formEl: HTMLFormElement;
109+
cancel: () => void;
110+
result: Extract<ActionResult, { type: "success" | "failure" }>;
111+
}) {
112+
if (!event.form.valid) return;
113+
const values = event.form.data;
114+
115+
try {
116+
await submitAddDataForm(queryClient, formType, connector, values);
117+
onClose();
118+
} catch (e) {
119+
if (useDsn) {
120+
dsnRpcError = e?.response?.data;
121+
} else {
122+
rpcError = e?.response?.data;
123+
}
124+
}
125+
}
83126
</script>
84127

85128
<div class="h-full w-full flex flex-col">
86-
<form
87-
class="pb-5 flex-grow overflow-y-auto"
88-
id={formId}
89-
use:enhance
90-
on:submit|preventDefault={submit}
91-
>
92-
<div class="pb-2 text-slate-500">
93-
Need help? Refer to our
94-
<a
95-
href="https://docs.rilldata.com/build/connect"
96-
rel="noreferrer noopener"
97-
target="_blank">docs</a
98-
> for more information.
129+
<div class="pb-2 text-slate-500">
130+
Need help? Refer to our
131+
<a
132+
href="https://docs.rilldata.com/build/connect"
133+
rel="noreferrer noopener"
134+
target="_blank">docs</a
135+
> for more information.
136+
</div>
137+
138+
{#if hasDsnFormOption}
139+
<div class="py-3">
140+
<div class="text-sm font-medium mb-2">Connection method</div>
141+
<ButtonGroup
142+
selected={[useDsn ? "dsn" : "parameters"]}
143+
on:subbutton-click={handleConnectionTypeChange}
144+
>
145+
<SubButton value="parameters" ariaLabel="Enter parameters">
146+
<span class="px-2">Enter parameters</span>
147+
</SubButton>
148+
<SubButton value="dsn" ariaLabel="Use connection string">
149+
<span class="px-2">Enter connection string</span>
150+
</SubButton>
151+
</ButtonGroup>
99152
</div>
100-
{#if rpcError}
101-
<SubmissionError
102-
message={humanReadableErrorMessage(
103-
connector.name,
104-
rpcError.code,
105-
rpcError.message,
106-
)}
107-
/>
108-
{/if}
109-
110-
{#each properties as property (property.key)}
111-
{#if property.key !== undefined && !property.noPrompt}
153+
{/if}
154+
155+
{#if !useDsn}
156+
<!-- Form 1: Individual parameters -->
157+
<form
158+
id={formId}
159+
class="pb-5 flex-grow overflow-y-auto"
160+
use:enhance
161+
on:submit|preventDefault={submit}
162+
transition:slide={{ duration: FORM_TRANSITION_DURATION }}
163+
>
164+
{#if rpcError}
165+
<SubmissionError
166+
message={humanReadableErrorMessage(
167+
connector.name,
168+
rpcError.code,
169+
rpcError.message,
170+
)}
171+
/>
172+
{/if}
173+
174+
{#each properties as property (property.key)}
175+
{@const propertyKey = property.key ?? ""}
112176
{@const label =
113177
property.displayName + (property.required ? "" : " (optional)")}
114178
<div class="py-1.5">
115179
{#if property.type === ConnectorDriverPropertyType.TYPE_STRING || property.type === ConnectorDriverPropertyType.TYPE_NUMBER}
116180
<Input
117-
id={toYupFriendlyKey(property.key)}
181+
id={propertyKey}
118182
label={property.displayName}
119183
placeholder={property.placeholder}
120184
optional={!property.required}
121185
secret={property.secret}
122186
hint={property.hint}
123-
errors={$errors[toYupFriendlyKey(property.key)]}
124-
bind:value={$form[toYupFriendlyKey(property.key)]}
187+
errors={$errors[propertyKey]}
188+
bind:value={$form[propertyKey]}
125189
onInput={(_, e) => onStringInputChange(e)}
126190
alwaysShowError
127191
/>
128192
{:else if property.type === ConnectorDriverPropertyType.TYPE_BOOLEAN}
129193
<label for={property.key} class="flex items-center">
130194
<input
131-
id={property.key}
195+
id={propertyKey}
132196
type="checkbox"
133-
bind:checked={$form[property.key]}
197+
bind:checked={$form[propertyKey]}
134198
class="h-5 w-5"
135199
/>
136200
<span class="ml-2 text-sm">{label}</span>
@@ -143,12 +207,53 @@
143207
/>
144208
{/if}
145209
</div>
210+
{/each}
211+
</form>
212+
{:else}
213+
<!-- Form 2: DSN -->
214+
<form
215+
id={dsnFormId}
216+
class="pb-5 flex-grow overflow-y-auto"
217+
use:dsnEnhance
218+
on:submit|preventDefault={dsnSubmit}
219+
transition:slide={{ duration: FORM_TRANSITION_DURATION }}
220+
>
221+
{#if dsnRpcError}
222+
<SubmissionError
223+
message={humanReadableErrorMessage(
224+
connector.name,
225+
dsnRpcError.code,
226+
dsnRpcError.message,
227+
)}
228+
/>
146229
{/if}
147-
{/each}
148-
</form>
230+
231+
{#each dsnProperties as property (property.key)}
232+
{@const propertyKey = property.key ?? ""}
233+
<div class="py-1.5">
234+
<Input
235+
id={propertyKey}
236+
label={property.displayName}
237+
placeholder={property.placeholder}
238+
secret={property.secret}
239+
hint={property.hint}
240+
errors={$dsnErrors[propertyKey]}
241+
bind:value={$dsnForm[propertyKey]}
242+
alwaysShowError
243+
/>
244+
</div>
245+
{/each}
246+
</form>
247+
{/if}
248+
149249
<div class="flex items-center space-x-2 ml-auto">
150250
<Button on:click={onBack} type="secondary">Back</Button>
151-
<Button disabled={$submitting} form={formId} submitForm type="primary">
251+
<Button
252+
disabled={useDsn ? $dsnSubmitting : $submitting}
253+
form={useDsn ? dsnFormId : formId}
254+
submitForm
255+
type="primary"
256+
>
152257
Add data
153258
</Button>
154259
</div>

web-common/src/features/sources/modal/submitAddDataForm.ts

+2-21
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import { EMPTY_PROJECT_TITLE } from "../../welcome/constants";
2828
import { isProjectInitialized } from "../../welcome/is-project-initialized";
2929
import { compileSourceYAML, maybeRewriteToDuckDb } from "../sourceUtils";
3030
import type { AddDataFormType } from "./types";
31-
import { fromYupFriendlyKey } from "./yupSchemas";
3231

3332
interface AddDataFormValues {
3433
// name: string; // Commenting out until we add user-provided names for Connectors
@@ -39,7 +38,7 @@ export async function submitAddDataForm(
3938
queryClient: QueryClient,
4039
formType: AddDataFormType,
4140
connector: V1ConnectorDriver,
42-
values: AddDataFormValues,
41+
formValues: AddDataFormValues,
4342
): Promise<void> {
4443
const instanceId = get(runtime).instanceId;
4544

@@ -64,24 +63,6 @@ export async function submitAddDataForm(
6463
await invalidate("init");
6564
}
6665

67-
// Convert the form values to Source YAML
68-
// TODO: Quite a few adhoc code is being added. We should revisit the way we generate the yaml.
69-
const formValues = Object.fromEntries(
70-
Object.entries(values).map(([key, value]) => {
71-
switch (key) {
72-
case "project_id":
73-
case "account":
74-
case "output_location":
75-
case "workgroup":
76-
case "database_url":
77-
case "azure_storage_account":
78-
return [key, value];
79-
default:
80-
return [fromYupFriendlyKey(key), value];
81-
}
82-
}),
83-
);
84-
8566
/**
8667
* Sources
8768
*/
@@ -94,7 +75,7 @@ export async function submitAddDataForm(
9475

9576
// Make a new <source>.yaml file
9677
const newSourceFilePath = getFileAPIPathFromNameAndType(
97-
values.name as string,
78+
formValues.name as string,
9879
EntityType.Table,
9980
);
10081
await runtimeServicePutFile(instanceId, {

0 commit comments

Comments
 (0)