|
9 | 9 | type RpcStatus,
|
10 | 10 | type V1ConnectorDriver,
|
11 | 11 | } 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"; |
13 | 19 | import { yup } from "sveltekit-superforms/adapters";
|
| 20 | + import { ButtonGroup, SubButton } from "../../../components/button-group"; |
14 | 21 | import { inferSourceName } from "../sourceUtils";
|
15 | 22 | import { humanReadableErrorMessage } from "./errors";
|
16 | 23 | import { submitAddDataForm } from "./submitAddDataForm";
|
17 | 24 | import type { AddDataFormType } from "./types";
|
18 |
| - import { getYupSchema, toYupFriendlyKey } from "./yupSchemas"; |
| 25 | + import { dsnSchema, getYupSchema } from "./yupSchemas"; |
| 26 | +
|
| 27 | + const FORM_TRANSITION_DURATION = 150; |
19 | 28 |
|
20 | 29 | export let connector: V1ConnectorDriver;
|
21 | 30 | export let formType: AddDataFormType;
|
22 | 31 | export let onBack: () => void;
|
23 | 32 | export let onClose: () => void;
|
24 | 33 |
|
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"; |
34 | 36 |
|
| 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 | + )) ?? []; |
35 | 45 | const schema = yup(getYupSchema[connector.name as keyof typeof getYupSchema]);
|
36 |
| -
|
37 | 46 | const { form, errors, enhance, tainted, submit, submitting } = superForm(
|
38 | 47 | defaults(schema),
|
39 | 48 | {
|
40 | 49 | SPA: true,
|
41 | 50 | 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, |
63 | 52 | },
|
64 | 53 | );
|
| 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 | + } |
65 | 83 |
|
66 | 84 | function onStringInputChange(event: Event) {
|
67 | 85 | const target = event.target as HTMLInputElement;
|
|
80 | 98 | );
|
81 | 99 | }
|
82 | 100 | }
|
| 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 | + } |
83 | 126 | </script>
|
84 | 127 |
|
85 | 128 | <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> |
99 | 152 | </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 ?? ""} |
112 | 176 | {@const label =
|
113 | 177 | property.displayName + (property.required ? "" : " (optional)")}
|
114 | 178 | <div class="py-1.5">
|
115 | 179 | {#if property.type === ConnectorDriverPropertyType.TYPE_STRING || property.type === ConnectorDriverPropertyType.TYPE_NUMBER}
|
116 | 180 | <Input
|
117 |
| - id={toYupFriendlyKey(property.key)} |
| 181 | + id={propertyKey} |
118 | 182 | label={property.displayName}
|
119 | 183 | placeholder={property.placeholder}
|
120 | 184 | optional={!property.required}
|
121 | 185 | secret={property.secret}
|
122 | 186 | 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]} |
125 | 189 | onInput={(_, e) => onStringInputChange(e)}
|
126 | 190 | alwaysShowError
|
127 | 191 | />
|
128 | 192 | {:else if property.type === ConnectorDriverPropertyType.TYPE_BOOLEAN}
|
129 | 193 | <label for={property.key} class="flex items-center">
|
130 | 194 | <input
|
131 |
| - id={property.key} |
| 195 | + id={propertyKey} |
132 | 196 | type="checkbox"
|
133 |
| - bind:checked={$form[property.key]} |
| 197 | + bind:checked={$form[propertyKey]} |
134 | 198 | class="h-5 w-5"
|
135 | 199 | />
|
136 | 200 | <span class="ml-2 text-sm">{label}</span>
|
|
143 | 207 | />
|
144 | 208 | {/if}
|
145 | 209 | </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 | + /> |
146 | 229 | {/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 | + |
149 | 249 | <div class="flex items-center space-x-2 ml-auto">
|
150 | 250 | <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 | + > |
152 | 257 | Add data
|
153 | 258 | </Button>
|
154 | 259 | </div>
|
|
0 commit comments