diff --git a/runtime/drivers/athena/athena.go b/runtime/drivers/athena/athena.go
index a3b24c2b12f..53fc444f021 100644
--- a/runtime/drivers/athena/athena.go
+++ b/runtime/drivers/athena/athena.go
@@ -60,6 +60,20 @@ var spec = drivers.Spec{
Placeholder: "s3://bucket-name/path/",
Required: true,
},
+ {
+ Key: "region",
+ Type: drivers.StringPropertyType,
+ DisplayName: "AWS Region",
+ Description: "AWS region where Athena is configured",
+ Placeholder: "us-east-1",
+ },
+ {
+ Key: "workgroup",
+ Type: drivers.StringPropertyType,
+ DisplayName: "Workgroup",
+ Description: "Athena workgroup name (optional)",
+ Placeholder: "primary",
+ },
},
ImplementsWarehouse: true,
}
diff --git a/runtime/drivers/azure/azure.go b/runtime/drivers/azure/azure.go
index dbfd51ffd90..3208ad8a981 100644
--- a/runtime/drivers/azure/azure.go
+++ b/runtime/drivers/azure/azure.go
@@ -39,9 +39,12 @@ var spec = drivers.Spec{
Secret: true,
},
{
- Key: "azure_storage_connection_string",
- Type: drivers.StringPropertyType,
- Secret: true,
+ Key: "azure_storage_connection_string",
+ Type: drivers.StringPropertyType,
+ DisplayName: "Azure Connection String",
+ Description: "Azure connection string for storage account",
+ Placeholder: "Paste your Azure connection string here",
+ Secret: true,
},
},
// Important: Any edits to the below properties must be accompanied by changes to the client-side form validation schemas.
diff --git a/runtime/drivers/https/https.go b/runtime/drivers/https/https.go
index fa70e25123f..7ae79b7058e 100644
--- a/runtime/drivers/https/https.go
+++ b/runtime/drivers/https/https.go
@@ -21,17 +21,25 @@ func init() {
}
var spec = drivers.Spec{
- DisplayName: "https",
- Description: "Connect to a remote file.",
+ DisplayName: "HTTPS",
+ Description: "Connect to remote files and REST APIs.",
DocsURL: "https://docs.rilldata.com/build/connect/#adding-a-remote-source",
- // Important: Any edits to the below properties must be accompanied by changes to the client-side form validation schemas.
+ ConfigProperties: []*drivers.PropertySpec{
+ {
+ Key: "headers",
+ Type: drivers.StringPropertyType,
+ DisplayName: "HTTP Headers (JSON)",
+ Description: `HTTP headers as JSON object. Example: {"Authorization": "Bearer my-token"}`,
+ Placeholder: `{"Authorization": "Bearer my-token"}`,
+ },
+ },
SourceProperties: []*drivers.PropertySpec{
{
Key: "path",
Type: drivers.StringPropertyType,
- DisplayName: "Path",
- Description: "Path to the remote file.",
- Placeholder: "https://example.com/file.csv",
+ DisplayName: "URL",
+ Description: "URL to the remote file or API endpoint",
+ Placeholder: "https://api.example.com/data",
Required: true,
},
{
@@ -102,8 +110,9 @@ func (d driver) Open(instanceID string, config map[string]any, st *storage.Clien
}
conn := &Connection{
- config: config,
- logger: logger,
+ config: config,
+ configProp: cfg,
+ logger: logger,
}
return conn, nil
}
@@ -121,8 +130,9 @@ func (d driver) TertiarySourceConnectors(ctx context.Context, src map[string]any
}
type Connection struct {
- config map[string]any
- logger *zap.Logger
+ config map[string]any
+ configProp *ConfigProperties
+ logger *zap.Logger
}
var _ drivers.Handle = &Connection{}
@@ -254,6 +264,12 @@ func (c *Connection) FilePaths(ctx context.Context, src map[string]any) ([]strin
return nil, fmt.Errorf("failed to create request for path %s: %w", path, err)
}
+ // Apply connector-level headers first (from config)
+ for k, v := range c.configProp.Headers {
+ req.Header.Set(k, v)
+ }
+
+ // Model-specific headers override connector headers
for k, v := range modelProp.Headers {
req.Header.Set(k, v)
}
diff --git a/runtime/drivers/s3/s3.go b/runtime/drivers/s3/s3.go
index 90e84dfd0ca..005e7ab4868 100644
--- a/runtime/drivers/s3/s3.go
+++ b/runtime/drivers/s3/s3.go
@@ -26,14 +26,22 @@ var spec = drivers.Spec{
DocsURL: "https://docs.rilldata.com/build/connectors/data-source/s3",
ConfigProperties: []*drivers.PropertySpec{
{
- Key: "aws_access_key_id",
- Type: drivers.StringPropertyType,
- Secret: true,
+ Key: "aws_access_key_id",
+ Type: drivers.StringPropertyType,
+ DisplayName: "AWS access key ID",
+ Description: "AWS access key ID for explicit credentials",
+ Placeholder: "Enter your AWS access key ID",
+ Secret: true,
+ Required: true,
},
{
- Key: "aws_secret_access_key",
- Type: drivers.StringPropertyType,
- Secret: true,
+ Key: "aws_secret_access_key",
+ Type: drivers.StringPropertyType,
+ DisplayName: "AWS secret access key",
+ Description: "AWS secret access key for explicit credentials",
+ Placeholder: "Enter your AWS secret access key",
+ Secret: true,
+ Required: true,
},
{
Key: "region",
diff --git a/runtime/drivers/snowflake/snowflake.go b/runtime/drivers/snowflake/snowflake.go
index 8b9dd5ed513..67c7ed257c9 100644
--- a/runtime/drivers/snowflake/snowflake.go
+++ b/runtime/drivers/snowflake/snowflake.go
@@ -95,6 +95,14 @@ var spec = drivers.Spec{
Placeholder: "your_role",
Hint: "The Snowflake role to use (defaults to your default role if not specified)",
},
+ {
+ Key: "privateKey",
+ Type: drivers.StringPropertyType,
+ DisplayName: "Private Key",
+ Description: "RSA private key in PEM format for key pair authentication",
+ Placeholder: "-----BEGIN PRIVATE KEY-----\\n...\\n-----END PRIVATE KEY-----",
+ Secret: true,
+ },
},
ImplementsWarehouse: true,
}
diff --git a/runtime/drivers/sqlite/sqlite.go b/runtime/drivers/sqlite/sqlite.go
index 355299369ab..9e9ba986f80 100644
--- a/runtime/drivers/sqlite/sqlite.go
+++ b/runtime/drivers/sqlite/sqlite.go
@@ -91,9 +91,9 @@ func (d driver) Spec() drivers.Spec {
Key: "db",
Type: drivers.StringPropertyType,
Required: true,
- DisplayName: "DB",
- Description: "Path to SQLite db file",
- Placeholder: "/path/to/sqlite.db",
+ DisplayName: "Database Path",
+ Description: "Path to SQLite database file",
+ Placeholder: "/path/to/database.db",
},
{
Key: "table",
diff --git a/web-common/src/components/icons/connectors/ClickHouseCloud.svelte b/web-common/src/components/icons/connectors/ClickHouseCloud.svelte
new file mode 100644
index 00000000000..9c74e3a6f6d
--- /dev/null
+++ b/web-common/src/components/icons/connectors/ClickHouseCloud.svelte
@@ -0,0 +1,46 @@
+
diff --git a/web-common/src/features/connectors/code-utils.ts b/web-common/src/features/connectors/code-utils.ts
index edb0bcb4e06..f82f1e5f02a 100644
--- a/web-common/src/features/connectors/code-utils.ts
+++ b/web-common/src/features/connectors/code-utils.ts
@@ -62,16 +62,18 @@ driver: ${getDriverNameForConnector(connector.name as string)}`;
properties = properties.filter(options.fieldFilter);
}
- // Get the secret property keys
+ // Get the secret property keys from the properties being used (orderedProperties or configProperties)
+ const propertiesForTypeChecking =
+ options?.orderedProperties ?? connector.configProperties ?? [];
const secretPropertyKeys =
- connector.configProperties
- ?.filter((property) => property.secret)
+ propertiesForTypeChecking
+ .filter((property) => property.secret)
.map((property) => property.key) || [];
- // Get the string property keys
+ // Get the string property keys from the properties being used
const stringPropertyKeys =
- connector.configProperties
- ?.filter(
+ propertiesForTypeChecking
+ .filter(
(property) => property.type === ConnectorDriverPropertyType.TYPE_STRING,
)
.map((property) => property.key) || [];
@@ -81,6 +83,12 @@ driver: ${getDriverNameForConnector(connector.name as string)}`;
.filter((property) => {
if (!property.key) return false;
const value = formValues[property.key];
+
+ // Secret fields should be shown with env variable placeholder if they exist in formValues
+ // Don't include secrets that weren't provided (e.g., password when using DSN)
+ const isSecretProperty = secretPropertyKeys.includes(property.key);
+ if (isSecretProperty && value !== undefined) return true;
+
if (value === undefined) return false;
// Filter out empty strings for optional fields
if (typeof value === "string" && value.trim() === "") return false;
@@ -96,7 +104,7 @@ driver: ${getDriverNameForConnector(connector.name as string)}`;
})
.map((property) => {
const key = property.key as string;
- const value = formValues[key] as string;
+ const value = formValues[key];
const isSecretProperty = secretPropertyKeys.includes(key);
if (isSecretProperty) {
@@ -107,12 +115,14 @@ driver: ${getDriverNameForConnector(connector.name as string)}`;
)} }}"`;
}
+ // At this point, value is guaranteed to be defined due to the filter above
+ const stringValue = value as string;
const isStringProperty = stringPropertyKeys.includes(key);
if (isStringProperty) {
- return `${key}: "${value}"`;
+ return `${key}: "${stringValue}"`;
}
- return `${key}: ${value}`;
+ return `${key}: ${stringValue}`;
})
.join("\n");
diff --git a/web-common/src/features/connectors/connector-icon-mapping.ts b/web-common/src/features/connectors/connector-icon-mapping.ts
index a5e581f0395..e5bd344eef5 100644
--- a/web-common/src/features/connectors/connector-icon-mapping.ts
+++ b/web-common/src/features/connectors/connector-icon-mapping.ts
@@ -15,8 +15,8 @@ import ClickHouseCloudIcon from "../../components/icons/connectors/ClickHouseClo
export const connectorIconMapping = {
athena: AthenaIcon,
bigquery: GoogleBigQueryIcon,
- clickhouse: ClickHouseIcon,
clickhousecloud: ClickHouseCloudIcon,
+ clickhouse: ClickHouseIcon,
motherduck: MotherDuckIcon,
druid: ApacheDruidIcon,
duckdb: DuckDbIcon,
diff --git a/web-common/src/features/connectors/connectors-utils.ts b/web-common/src/features/connectors/connectors-utils.ts
index b82c0c2532d..af7e562b848 100644
--- a/web-common/src/features/connectors/connectors-utils.ts
+++ b/web-common/src/features/connectors/connectors-utils.ts
@@ -139,8 +139,12 @@ export function getConnectorIconKey(connector: V1AnalyzedConnector): string {
/**
* Determines the driver name for a connector.
- * Special case: MotherDuck connectors use "duckdb" as the driver name.
+ * Special cases:
+ * - MotherDuck connectors use "duckdb" as the driver name.
+ * - ClickHouse Cloud connectors use "clickhouse" as the driver name.
*/
export function getDriverNameForConnector(connectorName: string): string {
- return connectorName === "motherduck" ? "duckdb" : connectorName;
+ if (connectorName === "motherduck") return "duckdb";
+ if (connectorName === "clickhousecloud") return "clickhouse";
+ return connectorName;
}
diff --git a/web-common/src/features/entity-management/name-utils.ts b/web-common/src/features/entity-management/name-utils.ts
index 01f9567a937..52a6d72dc99 100644
--- a/web-common/src/features/entity-management/name-utils.ts
+++ b/web-common/src/features/entity-management/name-utils.ts
@@ -21,8 +21,22 @@ export function getName(name: string, others: string[]): string {
const set = new Set(others.map((other) => other.toLowerCase()));
let result = name;
+ const incrementableSuffix = /(.+)_([0-9]+)$/;
while (set.has(result.toLowerCase())) {
+ // Special-case for "s3": don't roll over to "s4", append suffix instead.
+ if (name.toLowerCase() === "s3") {
+ const match = incrementableSuffix.exec(result);
+ if (match) {
+ const base = match[1];
+ const number = Number.parseInt(match[2], 10) + 1;
+ result = `${base}_${number}`;
+ continue;
+ }
+ result = `${name}_1`;
+ continue;
+ }
+
result = INCREMENT.exec(result)?.[1]
? result.replace(INCREMENT, (m) => (+m + 1).toString())
: `${result}_1`;
diff --git a/web-common/src/features/sources/modal/AddClickHouseForm.svelte b/web-common/src/features/sources/modal/AddClickHouseForm.svelte
deleted file mode 100644
index 3248cf7487d..00000000000
--- a/web-common/src/features/sources/modal/AddClickHouseForm.svelte
+++ /dev/null
@@ -1,506 +0,0 @@
-
-
-
-
-
- {#if connectorType === "rill-managed"}
-
-
-
- {/if}
-
-
- {#if connectorType === "self-hosted" || connectorType === "clickhouse-cloud"}
-
-
-
-
-
-
-
-
- {:else}
-
-
- {/if}
-
diff --git a/web-common/src/features/sources/modal/AddDataExplorer.svelte b/web-common/src/features/sources/modal/AddDataExplorer.svelte
new file mode 100644
index 00000000000..2beca06bc91
--- /dev/null
+++ b/web-common/src/features/sources/modal/AddDataExplorer.svelte
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+ Select a table to explore
+
+
+ Choose a table from your {connector.displayName ?? connector.name} connector
+ to generate metrics and create a dashboard.
+
+
+
+
+ {#if analyzedConnector}
+
+
+
+ {:else if $analyzedConnectors.isLoading}
+
Loading connector...
+ {:else}
+
No tables found.
+ {/if}
+
+
+ {#if selectedTable}
+
+
+ Selected: {selectedTable.table}
+
+
+ {/if}
+
+
+
+
+
+
+
+
diff --git a/web-common/src/features/sources/modal/AddDataForm.svelte b/web-common/src/features/sources/modal/AddDataForm.svelte
index ee081dc8f34..5a9f4314595 100644
--- a/web-common/src/features/sources/modal/AddDataForm.svelte
+++ b/web-common/src/features/sources/modal/AddDataForm.svelte
@@ -8,24 +8,21 @@
import type { SuperValidated } from "sveltekit-superforms";
import type { AddDataFormType, ConnectorType } from "./types";
- import AddClickHouseForm from "./AddClickHouseForm.svelte";
+ import MultiStepConnectorFlow from "./MultiStepConnectorFlow.svelte";
import NeedHelpText from "./NeedHelpText.svelte";
import Tabs from "@rilldata/web-common/components/forms/Tabs.svelte";
import { TabsContent } from "@rilldata/web-common/components/tabs";
- import { isEmpty } from "./utils";
- import {
- CONNECTION_TAB_OPTIONS,
- type ClickHouseConnectorType,
- } from "./constants";
- import { getInitialFormValuesFromProperties } from "../sourceUtils";
+ import { hasOnlyDsn, isEmpty } from "./utils";
+ import { CONNECTION_TAB_OPTIONS } from "./constants";
import { connectorStepStore } from "./connectorStepStore";
import FormRenderer from "./FormRenderer.svelte";
import YamlPreview from "./YamlPreview.svelte";
- import GCSMultiStepForm from "./GCSMultiStepForm.svelte";
+ import AddDataExplorer from "./AddDataExplorer.svelte";
+
import { AddDataFormManager } from "./AddDataFormManager";
- import { hasOnlyDsn } from "./utils";
import AddDataFormSection from "./AddDataFormSection.svelte";
+ import { get } from "svelte/store";
export let connector: V1ConnectorDriver;
export let formType: AddDataFormType;
@@ -54,45 +51,31 @@
formType,
onParamsUpdate: (e: any) => handleOnUpdate(e),
onDsnUpdate: (e: any) => handleOnUpdate(e),
+ getSelectedAuthMethod: () =>
+ get(connectorStepStore).selectedAuthMethod ?? undefined,
});
const isMultiStepConnector = formManager.isMultiStepConnector;
const isSourceForm = formManager.isSourceForm;
const isConnectorForm = formManager.isConnectorForm;
const onlyDsn = hasOnlyDsn(connector, isConnectorForm);
- $: stepState = $connectorStepStore;
- $: stepProperties =
- isMultiStepConnector && stepState.step === "source"
- ? (connector.sourceProperties ?? [])
- : properties;
- $: if (
- isMultiStepConnector &&
- stepState.step === "source" &&
- stepState.connectorConfig
- ) {
- // Initialize form with source properties and default values
- const sourceProperties = connector.sourceProperties ?? [];
- const initialValues = getInitialFormValuesFromProperties(sourceProperties);
-
- // Merge with stored connector config
- const combinedValues = { ...stepState.connectorConfig, ...initialValues };
-
- paramsForm.update(() => combinedValues, { taint: false });
- }
+ let activeAuthMethod: string | null = null;
+ let prevAuthMethod: string | null = null;
+ let stepState = $connectorStepStore;
+ let multiStepSubmitDisabled = true;
+ let multiStepButtonLabel = "Test and Connect";
+ let multiStepLoadingCopy = "Testing connection...";
+ let shouldShowSkipLink = false;
+ let primaryButtonLabel = "Test and Connect";
+ let primaryLoadingCopy = "Testing connection...";
- // Update form when (re)entering step 1: restore defaults for connector properties
- $: if (isMultiStepConnector && stepState.step === "connector") {
- paramsForm.update(
- () =>
- getInitialFormValuesFromProperties(connector.configProperties ?? []),
- { taint: false },
- );
- }
+ $: stepState = $connectorStepStore;
// Form 1: Individual parameters
const paramsFormId = formManager.paramsFormId;
const properties = formManager.properties;
const filteredParamsProperties = formManager.filteredParamsProperties;
+ let multiStepFormId = paramsFormId;
const {
form: paramsForm,
errors: paramsErrors,
@@ -121,17 +104,16 @@
let dsnError: string | null = null;
let dsnErrorDetails: string | undefined = undefined;
- let clickhouseError: string | null = null;
- let clickhouseErrorDetails: string | undefined = undefined;
- let clickhouseFormId: string = "";
- let clickhouseSubmitting: boolean;
- let clickhouseIsSubmitDisabled: boolean;
- let clickhouseConnectorType: ClickHouseConnectorType = "self-hosted";
- let clickhouseParamsForm;
- let clickhouseDsnForm;
- let clickhouseShowSaveAnyway: boolean = false;
+ // Hide Save Anyway once we advance to the model step in multi-step flows.
+ $: if (isMultiStepConnector && stepState.step === "source") {
+ showSaveAnyway = false;
+ }
$: isSubmitDisabled = (() => {
+ if (isMultiStepConnector) {
+ return multiStepSubmitDisabled;
+ }
+
if (onlyDsn || connectionTab === "dsn") {
// DSN form: check required DSN properties
for (const property of dsnProperties) {
@@ -149,12 +131,7 @@
return false;
} else {
// Parameters form: check required properties
- // Use stepProperties for multi-step connectors, otherwise use properties
- const propertiesToCheck = isMultiStepConnector
- ? stepProperties
- : properties;
-
- for (const property of propertiesToCheck) {
+ for (const property of properties) {
if (property.required) {
const key = String(property.key);
const value = $paramsForm[key];
@@ -168,7 +145,9 @@
}
})();
- $: formId = formManager.getActiveFormId({ connectionTab, onlyDsn });
+ $: formId = isMultiStepConnector
+ ? multiStepFormId || formManager.getActiveFormId({ connectionTab, onlyDsn })
+ : formManager.getActiveFormId({ connectionTab, onlyDsn });
$: submitting = (() => {
if (onlyDsn || connectionTab === "dsn") {
@@ -178,6 +157,29 @@
}
})();
+ $: primaryButtonLabel = isMultiStepConnector
+ ? multiStepButtonLabel
+ : formManager.getPrimaryButtonLabel({
+ isConnectorForm,
+ step: stepState.step,
+ submitting,
+ selectedAuthMethod: activeAuthMethod ?? undefined,
+ });
+
+ $: primaryLoadingCopy = (() => {
+ if (isMultiStepConnector) return multiStepLoadingCopy;
+ return activeAuthMethod === "public"
+ ? "Continuing..."
+ : "Testing connection...";
+ })();
+
+ // Clear Save Anyway state whenever auth method changes (any direction).
+ $: if (activeAuthMethod !== prevAuthMethod) {
+ prevAuthMethod = activeAuthMethod;
+ showSaveAnyway = false;
+ saveAnyway = false;
+ }
+
$: isSubmitting = submitting;
// Reset errors when form is modified
@@ -210,34 +212,15 @@
// For other connectors, use manager helper
saveAnyway = true;
- const values =
- connector.name === "clickhouse"
- ? connectionTab === "dsn"
- ? $clickhouseDsnForm
- : $clickhouseParamsForm
- : onlyDsn || connectionTab === "dsn"
- ? $dsnForm
- : $paramsForm;
- if (connector.name === "clickhouse") {
- clickhouseSubmitting = true;
- }
+ const values = onlyDsn || connectionTab === "dsn" ? $dsnForm : $paramsForm;
const result = await formManager.saveConnectorAnyway({
queryClient,
values,
- clickhouseConnectorType,
});
if (result.ok) {
onClose();
} else {
- if (connector.name === "clickhouse") {
- if (connectionTab === "dsn") {
- dsnError = result.message;
- dsnErrorDetails = result.details;
- } else {
- paramsError = result.message;
- paramsErrorDetails = result.details;
- }
- } else if (onlyDsn || connectionTab === "dsn") {
+ if (onlyDsn || connectionTab === "dsn") {
dsnError = result.message;
dsnErrorDetails = result.details;
} else {
@@ -246,9 +229,6 @@
}
}
saveAnyway = false;
- if (connector.name === "clickhouse") {
- clickhouseSubmitting = false;
- }
}
$: yamlPreview = formManager.computeYamlPreview({
@@ -261,21 +241,15 @@
isConnectorForm,
paramsFormValues: $paramsForm,
dsnFormValues: $dsnForm,
- clickhouseConnectorType,
- clickhouseParamsValues: $clickhouseParamsForm,
- clickhouseDsnValues: $clickhouseDsnForm,
});
- $: isClickhouse = connector.name === "clickhouse";
- $: shouldShowSaveAnywayButton =
- isConnectorForm && (showSaveAnyway || clickhouseShowSaveAnyway);
- $: saveAnywayLoading = isClickhouse
- ? clickhouseSubmitting && saveAnyway
- : submitting && saveAnyway;
+ $: shouldShowSaveAnywayButton = isConnectorForm && showSaveAnyway;
+ $: saveAnywayLoading = submitting && saveAnyway;
handleOnUpdate = formManager.makeOnUpdate({
onClose,
queryClient,
getConnectionTab: () => connectionTab,
+ getSelectedAuthMethod: () => activeAuthMethod || undefined,
setParamsError: (message: string | null, details?: string) => {
paramsError = message;
paramsErrorDetails = details;
@@ -301,30 +275,35 @@
}
-
+{/if}
diff --git a/web-common/src/features/sources/modal/AddDataFormManager.ts b/web-common/src/features/sources/modal/AddDataFormManager.ts
index e2d0f9c4c10..3ffac6acb59 100644
--- a/web-common/src/features/sources/modal/AddDataFormManager.ts
+++ b/web-common/src/features/sources/modal/AddDataFormManager.ts
@@ -20,31 +20,50 @@ import { normalizeConnectorError } from "./utils";
import {
FORM_HEIGHT_DEFAULT,
FORM_HEIGHT_TALL,
+ FORM_HEIGHT_MEDIUM,
MULTI_STEP_CONNECTORS,
+ MEDIUM_FORM_CONNECTORS,
TALL_FORM_CONNECTORS,
+ OLAP_ENGINES,
} from "./constants";
import {
connectorStepStore,
setConnectorConfig,
setStep,
+ type ConnectorStepState,
} from "./connectorStepStore";
import { get } from "svelte/store";
import { compileConnectorYAML } from "../../connectors/code-utils";
-import { compileSourceYAML, prepareSourceFormData } from "../sourceUtils";
-import type { ConnectorDriverProperty } from "@rilldata/web-common/runtime-client";
-import type { ClickHouseConnectorType } from "./constants";
-import { applyClickHouseCloudRequirements } from "./utils";
+import {
+ compileSourceYAML,
+ prepareSourceFormData,
+ prepareConnectorFormData,
+} from "../sourceUtils";
+import {
+ ConnectorDriverPropertyType,
+ type ConnectorDriverProperty,
+} from "@rilldata/web-common/runtime-client";
import type { ActionResult } from "@sveltejs/kit";
+import { getConnectorSchema } from "./connector-schemas";
+import {
+ findRadioEnumKey,
+ findGroupedEnumKeys,
+ isVisibleForValues,
+} from "../../templates/schema-utils";
// Minimal onUpdate event type carrying Superforms's validated form
type SuperFormUpdateEvent = {
form: SuperValidated, any, Record>;
};
-// Shape of the step store for multi-step connectors
-type ConnectorStepState = {
- step: "connector" | "source";
- connectorConfig: Record | null;
+const BUTTON_LABELS = {
+ public: { idle: "Continue", submitting: "Continuing..." },
+ connector: { idle: "Test and Connect", submitting: "Testing connection..." },
+ connectorReadOnly: {
+ idle: "Test and Add Connector",
+ submitting: "Testing connection...",
+ },
+ source: { idle: "Import Data", submitting: "Importing data..." },
};
export class AddDataFormManager {
@@ -69,20 +88,34 @@ export class AddDataFormManager {
return normalizeConnectorError(this.connector.name ?? "", e);
}
+ private getSelectedAuthMethod?: () => string | undefined;
+
constructor(args: {
connector: V1ConnectorDriver;
formType: AddDataFormType;
onParamsUpdate: (event: SuperFormUpdateEvent) => void;
onDsnUpdate: (event: SuperFormUpdateEvent) => void;
+ getSelectedAuthMethod?: () => string | undefined;
}) {
- const { connector, formType, onParamsUpdate, onDsnUpdate } = args;
+ const {
+ connector,
+ formType,
+ onParamsUpdate,
+ onDsnUpdate,
+ getSelectedAuthMethod,
+ } = args;
this.connector = connector;
this.formType = formType;
+ this.getSelectedAuthMethod = getSelectedAuthMethod;
// Layout height
- this.formHeight = TALL_FORM_CONNECTORS.has(connector.name ?? "")
- ? FORM_HEIGHT_TALL
- : FORM_HEIGHT_DEFAULT;
+ if (TALL_FORM_CONNECTORS.has(connector.name ?? "")) {
+ this.formHeight = FORM_HEIGHT_TALL;
+ } else if (MEDIUM_FORM_CONNECTORS.has(connector.name ?? "")) {
+ this.formHeight = FORM_HEIGHT_MEDIUM;
+ } else {
+ this.formHeight = FORM_HEIGHT_DEFAULT;
+ }
// IDs
this.paramsFormId = `add-data-${connector.name}-form`;
@@ -127,13 +160,34 @@ export class AddDataFormManager {
// Superforms: params
const paramsSchemaDef = getValidationSchemaForConnector(
connector.name as string,
+ formType,
+ {
+ isMultiStepConnector: this.isMultiStepConnector,
+ authMethodGetter: this.getSelectedAuthMethod,
+ },
);
const paramsAdapter = yup(paramsSchemaDef);
type ParamsOut = YupInfer;
type ParamsIn = YupInferIn;
- const initialFormValues = getInitialFormValuesFromProperties(
- this.properties,
- );
+
+ // For schema-based connectors, use schema defaults instead of backend properties
+ // to avoid pulling defaults from the backend driver
+ const schema = connector.name ? getConnectorSchema(connector.name) : null;
+ let initialFormValues: Record;
+
+ if (schema?.properties) {
+ // Extract defaults from schema
+ initialFormValues = {};
+ for (const [key, prop] of Object.entries(schema.properties)) {
+ if (prop.default !== undefined) {
+ initialFormValues[key] = prop.default;
+ }
+ }
+ } else {
+ // Fall back to backend properties for non-schema connectors
+ initialFormValues = getInitialFormValuesFromProperties(this.properties);
+ }
+
const paramsDefaults = defaults(
initialFormValues as Partial,
paramsAdapter,
@@ -143,6 +197,7 @@ export class AddDataFormManager {
validators: paramsAdapter,
onUpdate: onParamsUpdate,
resetForm: false,
+ validationMethod: "onsubmit",
});
// Superforms: dsn
@@ -154,6 +209,7 @@ export class AddDataFormManager {
validators: dsnAdapter,
onUpdate: onDsnUpdate,
resetForm: false,
+ validationMethod: "onsubmit",
});
}
@@ -169,6 +225,37 @@ export class AddDataFormManager {
return MULTI_STEP_CONNECTORS.includes(this.connector.name ?? "");
}
+ /**
+ * Determines whether the "Save Anyway" button should be shown for the current submission.
+ */
+ private shouldShowSaveAnywayButton(args: {
+ isConnectorForm: boolean;
+ event?:
+ | {
+ result?: Extract;
+ }
+ | undefined;
+ stepState: ConnectorStepState | undefined;
+ selectedAuthMethod?: string;
+ }): boolean {
+ const { isConnectorForm, event, stepState, selectedAuthMethod } = args;
+
+ // Only show for connector forms (not sources)
+ if (!isConnectorForm) return false;
+
+ // Need a submission result to show the button
+ if (!event?.result) return false;
+
+ // Multi-step connectors: don't show on source step (final step)
+ if (stepState?.step === "source") return false;
+
+ // Public auth bypasses connection test, so no "Save Anyway" needed
+ if (stepState?.step === "connector" && selectedAuthMethod === "public")
+ return false;
+
+ return true;
+ }
+
getActiveFormId(args: {
connectionTab: "parameters" | "dsn";
onlyDsn: boolean;
@@ -182,7 +269,12 @@ export class AddDataFormManager {
handleSkip(): void {
const stepState = get(connectorStepStore) as ConnectorStepState;
if (!this.isMultiStepConnector || stepState.step !== "connector") return;
- setConnectorConfig(get(this.params.form) as Record);
+
+ const formValues = get(this.params.form);
+ const mode = formValues?.mode as string | undefined;
+
+ // Preserve the form values (especially mode) when navigating to source step
+ setConnectorConfig(formValues);
setStep("source");
}
@@ -199,35 +291,37 @@ export class AddDataFormManager {
isConnectorForm: boolean;
step: "connector" | "source" | string;
submitting: boolean;
- clickhouseConnectorType?: ClickHouseConnectorType;
- clickhouseSubmitting?: boolean;
+ selectedAuthMethod?: string;
+ mode?: string;
}): string {
- const {
- isConnectorForm,
- step,
- submitting,
- clickhouseConnectorType,
- clickhouseSubmitting,
- } = args;
- const isClickhouse = this.connector.name === "clickhouse";
-
- if (isClickhouse) {
- if (clickhouseConnectorType === "rill-managed") {
- return clickhouseSubmitting ? "Connecting..." : "Connect";
- }
- return clickhouseSubmitting
- ? "Testing connection..."
- : "Test and Connect";
- }
+ const { isConnectorForm, step, submitting, selectedAuthMethod, mode } =
+ args;
if (isConnectorForm) {
if (this.isMultiStepConnector && step === "connector") {
- return submitting ? "Testing connection..." : "Test and Connect";
+ if (selectedAuthMethod === "public") {
+ return submitting
+ ? BUTTON_LABELS.public.submitting
+ : BUTTON_LABELS.public.idle;
+ }
+ // For read-only mode, show "Test and Add Connector" instead of "Test and Connect"
+ if (mode === "read") {
+ return submitting
+ ? BUTTON_LABELS.connectorReadOnly.submitting
+ : BUTTON_LABELS.connectorReadOnly.idle;
+ }
+ return submitting
+ ? BUTTON_LABELS.connector.submitting
+ : BUTTON_LABELS.connector.idle;
}
if (this.isMultiStepConnector && step === "source") {
- return submitting ? "Creating model..." : "Test and Add data";
+ return submitting
+ ? BUTTON_LABELS.source.submitting
+ : BUTTON_LABELS.source.idle;
}
- return submitting ? "Testing connection..." : "Test and Connect";
+ return submitting
+ ? BUTTON_LABELS.connector.submitting
+ : BUTTON_LABELS.connector.idle;
}
return "Test and Add data";
@@ -237,6 +331,7 @@ export class AddDataFormManager {
onClose: () => void;
queryClient: any;
getConnectionTab: () => "parameters" | "dsn";
+ getSelectedAuthMethod?: () => string | undefined;
setParamsError: (message: string | null, details?: string) => void;
setDsnError: (message: string | null, details?: string) => void;
setShowSaveAnyway?: (value: boolean) => void;
@@ -245,6 +340,7 @@ export class AddDataFormManager {
onClose,
queryClient,
getConnectionTab,
+ getSelectedAuthMethod,
setParamsError,
setDsnError,
setShowSaveAnyway,
@@ -263,35 +359,97 @@ export class AddDataFormManager {
>;
result?: Extract;
}) => {
- // For non-ClickHouse connectors, expose Save Anyway when a submission starts
+ const values = event.form.data;
+ const schema = getConnectorSchema(this.connector.name ?? "");
+ const authKey = schema ? findRadioEnumKey(schema) : null;
+ const selectedAuthMethod =
+ (authKey && values && values[authKey] != null
+ ? String(values[authKey])
+ : undefined) ||
+ getSelectedAuthMethod?.() ||
+ "";
+ const stepState = get(connectorStepStore) as ConnectorStepState;
+
+ // Fast-path: public auth skips validation/test and goes straight to source step.
if (
- isConnectorForm &&
- connector.name !== "clickhouse" &&
- typeof setShowSaveAnyway === "function" &&
- event?.result
+ isMultiStepConnector &&
+ stepState.step === "connector" &&
+ selectedAuthMethod === "public"
) {
- setShowSaveAnyway(true);
+ setConnectorConfig(values);
+ setStep("source");
+ return;
}
- if (!event.form.valid) return;
+ // When in the source step of a multi-step flow, the superform still uses
+ // the connector schema, so it can appear invalid because connector fields
+ // were intentionally skipped. Allow submission in that case and rely on
+ // the UI-level required checks for source fields.
+ if (
+ !event.form.valid &&
+ !(isMultiStepConnector && stepState.step === "source")
+ )
+ return;
- const values = event.form.data;
+ if (
+ typeof setShowSaveAnyway === "function" &&
+ this.shouldShowSaveAnywayButton({
+ isConnectorForm,
+ event,
+ stepState,
+ selectedAuthMethod,
+ })
+ ) {
+ setShowSaveAnyway(true);
+ }
try {
- const stepState = get(connectorStepStore) as ConnectorStepState;
if (isMultiStepConnector && stepState.step === "source") {
await submitAddSourceForm(queryClient, connector, values);
onClose();
} else if (isMultiStepConnector && stepState.step === "connector") {
- await submitAddConnectorForm(queryClient, connector, values, false);
- setConnectorConfig(values);
+ // For public auth, skip Test & Connect and go straight to the next step.
+ if (selectedAuthMethod === "public") {
+ setConnectorConfig(values);
+ setStep("source");
+ return;
+ }
+ const preparedValues = prepareConnectorFormData(connector, values);
+ await submitAddConnectorForm(
+ queryClient,
+ connector,
+ preparedValues,
+ false,
+ );
+
+ // If mode is "read" (read-only), navigate to explorer step.
+ // For OLAP connectors without a mode field (Pinot, Druid), also go to explorer.
+ // Only advance to source step when mode is "readwrite" or undefined (rill-managed).
+ const mode = values?.mode as string | undefined;
+ const schema = getConnectorSchema(connector.name ?? "");
+ const hasModeField = schema?.properties?.mode !== undefined;
+ const isReadOnlyOlap = !hasModeField && OLAP_ENGINES.includes(connector.name ?? "");
+
+ if (mode === "read" || isReadOnlyOlap) {
+ setConnectorConfig(preparedValues);
+ setStep("explorer");
+ return;
+ }
+
+ setConnectorConfig(preparedValues);
setStep("source");
return;
} else if (this.formType === "source") {
await submitAddSourceForm(queryClient, connector, values);
onClose();
} else {
- await submitAddConnectorForm(queryClient, connector, values, false);
+ const preparedValues = prepareConnectorFormData(connector, values);
+ await submitAddConnectorForm(
+ queryClient,
+ connector,
+ preparedValues,
+ false,
+ );
onClose();
}
} catch (e) {
@@ -372,9 +530,6 @@ export class AddDataFormManager {
isConnectorForm: boolean;
paramsFormValues: Record;
dsnFormValues: Record;
- clickhouseConnectorType?: ClickHouseConnectorType;
- clickhouseParamsValues?: Record;
- clickhouseDsnValues?: Record;
}): string {
const connector = this.connector;
const {
@@ -387,45 +542,90 @@ export class AddDataFormManager {
isConnectorForm,
paramsFormValues,
dsnFormValues,
- clickhouseConnectorType,
- clickhouseParamsValues,
- clickhouseDsnValues,
} = ctx;
+ const connectorPropertiesForPreview =
+ isMultiStepConnector && stepState?.step === "connector"
+ ? (connector.configProperties ?? [])
+ : filteredParamsProperties;
+
const getConnectorYamlPreview = (values: Record) => {
- return compileConnectorYAML(connector, values, {
- fieldFilter: (property) => {
- if (onlyDsn || connectionTab === "dsn") return true;
- return !property.noPrompt;
- },
- orderedProperties:
+ let orderedProperties: ConnectorDriverProperty[] = [];
+
+ // For multi-step connectors with schemas, build properties from schema
+ const schema =
+ isMultiStepConnector &&
+ stepState?.step === "connector" &&
+ connector.name
+ ? getConnectorSchema(connector.name)
+ : null;
+
+ if (schema) {
+ // Build ordered properties from schema fields that are visible and on connector step
+ const schemaProperties: ConnectorDriverProperty[] = [];
+ const properties = schema.properties ?? {};
+
+ // Find all grouped enum keys (radio or tabs with grouped fields) - these are UI control fields we don't want in YAML
+ const groupedEnumKeys = findGroupedEnumKeys(schema);
+ const authMethodKey = findRadioEnumKey(schema);
+
+ // Ensure all grouped enum keys have values for visibility checks (use actual value or fallback to default)
+ let valuesForVisibility = { ...values };
+ for (const enumKey of groupedEnumKeys) {
+ if (!valuesForVisibility[enumKey]) {
+ const enumProp = properties[enumKey];
+ if (enumProp?.default) {
+ valuesForVisibility[enumKey] = enumProp.default;
+ } else if (enumProp?.enum?.length) {
+ valuesForVisibility[enumKey] = enumProp.enum[0];
+ }
+ }
+ }
+
+ for (const [key, prop] of Object.entries(properties)) {
+ // Skip grouped enum keys (auth_method, connection_method, etc.) - they're just UI control fields
+ if (groupedEnumKeys.includes(key)) continue;
+
+ // Only include connector step fields that are currently visible
+ if (
+ prop["x-step"] === "connector" &&
+ isVisibleForValues(schema, key, valuesForVisibility)
+ ) {
+ const value = values[key];
+ const isSecret = prop["x-secret"] || false;
+
+ // Skip if value is undefined/null/empty string
+ if (value === undefined || value === null || value === "") continue;
+
+ // Should we add other properties, like Array for list of strings, Ie path_prefix, etc.
+ schemaProperties.push({
+ key,
+ type:
+ prop.type === "number"
+ ? ConnectorDriverPropertyType.TYPE_NUMBER
+ : prop.type === "boolean"
+ ? ConnectorDriverPropertyType.TYPE_BOOLEAN
+ : ConnectorDriverPropertyType.TYPE_STRING,
+ secret: isSecret,
+ });
+ }
+ }
+
+ orderedProperties = schemaProperties;
+ } else {
+ // Non-schema connectors use the old DSN/parameters tab system
+ orderedProperties =
onlyDsn || connectionTab === "dsn"
? filteredDsnProperties
- : filteredParamsProperties,
- });
- };
+ : connectorPropertiesForPreview;
+ }
- const getClickHouseYamlPreview = (
- values: Record,
- chType: ClickHouseConnectorType | undefined,
- ) => {
- // Convert to managed boolean and apply CH Cloud requirements for preview
- const managed = chType === "rill-managed";
- const previewValues = { ...values, managed } as Record;
- const finalValues = applyClickHouseCloudRequirements(
- connector.name,
- chType as ClickHouseConnectorType,
- previewValues,
- );
- return compileConnectorYAML(connector, finalValues, {
+ return compileConnectorYAML(connector, values, {
fieldFilter: (property) => {
if (onlyDsn || connectionTab === "dsn") return true;
return !property.noPrompt;
},
- orderedProperties:
- connectionTab === "dsn"
- ? filteredDsnProperties
- : filteredParamsProperties,
+ orderedProperties,
});
};
@@ -436,9 +636,17 @@ export class AddDataFormManager {
const connectorPropertyKeys = new Set(
connector.configProperties?.map((p) => p.key).filter(Boolean) || [],
);
+
+ // Also filter out grouped enum keys (auth_method, connection_method, mode, etc.)
+ const schema = connector.name
+ ? getConnectorSchema(connector.name)
+ : null;
+ const groupedEnumKeys = schema ? findGroupedEnumKeys(schema) : [];
+
filteredValues = Object.fromEntries(
Object.entries(values).filter(
- ([key]) => !connectorPropertyKeys.has(key),
+ ([key]) =>
+ !connectorPropertyKeys.has(key) && !groupedEnumKeys.includes(key),
),
);
}
@@ -447,6 +655,12 @@ export class AddDataFormManager {
connector,
filteredValues,
);
+
+ // For multi-step connectors on source step, always show model YAML
+ if (isMultiStepConnector && stepState?.step === "source") {
+ return compileSourceYAML(rewrittenConnector, rewrittenFormValues);
+ }
+
const isRewrittenToDuckDb = rewrittenConnector.name === "duckdb";
if (isRewrittenToDuckDb) {
return compileSourceYAML(rewrittenConnector, rewrittenFormValues);
@@ -454,15 +668,6 @@ export class AddDataFormManager {
return getConnectorYamlPreview(rewrittenFormValues);
};
- // ClickHouse special-case
- if (connector.name === "clickhouse") {
- const values =
- connectionTab === "dsn"
- ? clickhouseDsnValues || {}
- : clickhouseParamsValues || {};
- return getClickHouseYamlPreview(values, clickhouseConnectorType);
- }
-
// Multi-step connectors
if (isMultiStepConnector) {
if (stepState?.step === "connector") {
@@ -483,20 +688,14 @@ export class AddDataFormManager {
}
/**
- * Save connector anyway (non-ClickHouse), returning a result object for the caller to handle.
+ * Save connector anyway, bypassing validation. Returns a result object for the caller to handle.
*/
async saveConnectorAnyway(args: {
queryClient: any;
values: Record;
- clickhouseConnectorType?: ClickHouseConnectorType;
}): Promise<{ ok: true } | { ok: false; message: string; details?: string }> {
- const { queryClient, values, clickhouseConnectorType } = args;
- const processedValues = applyClickHouseCloudRequirements(
- this.connector.name,
- (clickhouseConnectorType as ClickHouseConnectorType) ||
- ("self-hosted" as ClickHouseConnectorType),
- values,
- );
+ const { queryClient, values } = args;
+ const processedValues = prepareConnectorFormData(this.connector, values);
try {
await submitAddConnectorForm(
queryClient,
diff --git a/web-common/src/features/sources/modal/AddDataModal.svelte b/web-common/src/features/sources/modal/AddDataModal.svelte
index 42c8289d1b1..cfff6ef215b 100644
--- a/web-common/src/features/sources/modal/AddDataModal.svelte
+++ b/web-common/src/features/sources/modal/AddDataModal.svelte
@@ -21,7 +21,12 @@
import DuplicateSource from "./DuplicateSource.svelte";
import LocalSourceUpload from "./LocalSourceUpload.svelte";
import RequestConnectorForm from "./RequestConnectorForm.svelte";
- import { OLAP_ENGINES, ALL_CONNECTORS, SOURCES } from "./constants";
+ import {
+ OLAP_ENGINES,
+ ALL_CONNECTORS,
+ SOURCES,
+ MULTI_STEP_CONNECTORS,
+ } from "./constants";
import { ICONS } from "./icons";
import { resetConnectorStep } from "./connectorStepStore";
@@ -34,22 +39,35 @@
query: {
// arrange connectors in the way we would like to display them
select: (data) => {
- data.connectors =
- data.connectors &&
- data.connectors
- .filter(
- // Only show connectors in SOURCES or OLAP_ENGINES
- (a) =>
- a.name &&
- (SOURCES.includes(a.name) || OLAP_ENGINES.includes(a.name)),
- )
- .sort(
- // CAST SAFETY: we have filtered out any connectors that
- // don't have a `name` in the previous filter
- (a, b) =>
- ALL_CONNECTORS.indexOf(a.name as string) -
- ALL_CONNECTORS.indexOf(b.name as string),
- );
+ let connectors = data.connectors || [];
+
+ // Clone clickhouse connector to create clickhousecloud (frontend-only)
+ const clickhouseConnector = connectors.find(
+ (c) => c.name === "clickhouse",
+ );
+ if (clickhouseConnector) {
+ const clickhouseCloudConnector: V1ConnectorDriver = {
+ ...clickhouseConnector,
+ name: "clickhousecloud",
+ displayName: "ClickHouse Cloud",
+ };
+ connectors = [...connectors, clickhouseCloudConnector];
+ }
+
+ data.connectors = connectors
+ .filter(
+ // Only show connectors in SOURCES or OLAP_ENGINES
+ (a) =>
+ a.name &&
+ (SOURCES.includes(a.name) || OLAP_ENGINES.includes(a.name)),
+ )
+ .sort(
+ // CAST SAFETY: we have filtered out any connectors that
+ // don't have a `name` in the previous filter
+ (a, b) =>
+ ALL_CONNECTORS.indexOf(a.name as string) -
+ ALL_CONNECTORS.indexOf(b.name as string),
+ );
return data;
},
},
@@ -121,7 +139,8 @@
// FIXME: excluding salesforce until we implement the table discovery APIs
$: isConnectorType =
- selectedConnector?.name === "gcs" ||
+ MULTI_STEP_CONNECTORS.includes(selectedConnector?.name ?? "") ||
+ selectedConnector?.implementsObjectStore ||
selectedConnector?.implementsOlap ||
selectedConnector?.implementsSqlStore ||
(selectedConnector?.implementsWarehouse &&
diff --git a/web-common/src/features/sources/modal/FormValidation.ts b/web-common/src/features/sources/modal/FormValidation.ts
index 92c0f87c338..b5853948fd2 100644
--- a/web-common/src/features/sources/modal/FormValidation.ts
+++ b/web-common/src/features/sources/modal/FormValidation.ts
@@ -1,7 +1,88 @@
+import * as yup from "yup";
import { dsnSchema, getYupSchema } from "./yupSchemas";
+import { getConnectorSchema } from "./connector-schemas";
+import {
+ getFieldLabel,
+ getRequiredFieldsByEnumValue,
+} from "../../templates/schema-utils";
+import type { AddDataFormType, MultiStepFormSchema } from "./types";
export { dsnSchema };
-export function getValidationSchemaForConnector(name: string) {
+export function getValidationSchemaForConnector(
+ name: string,
+ formType: AddDataFormType,
+ opts?: {
+ isMultiStepConnector?: boolean;
+ authMethodGetter?: () => string | undefined;
+ },
+) {
+ const { isMultiStepConnector, authMethodGetter } = opts || {};
+
+ // For multi-step source flows, prefer the connector-specific schema when present
+ // so step 1 (connector) validation doesn't require source-only fields.
+ if (isMultiStepConnector && formType === "source") {
+ const connectorKey = `${name}_connector`;
+ if (connectorKey in getYupSchema) {
+ return getYupSchema[connectorKey as keyof typeof getYupSchema];
+ }
+ }
+
+ // For multi-step connector step, prefer connector-specific schema when present.
+ if (isMultiStepConnector && formType === "connector") {
+ // Generic dynamic schema based on auth options, driven by config.
+ const dynamicSchema = makeAuthOptionValidationSchema(
+ name,
+ authMethodGetter,
+ );
+ if (dynamicSchema) return dynamicSchema;
+
+ const connectorKey = `${name}_connector`;
+ if (connectorKey in getYupSchema) {
+ return getYupSchema[connectorKey as keyof typeof getYupSchema];
+ }
+ }
+
return getYupSchema[name as keyof typeof getYupSchema];
}
+
+/**
+ * Build a yup schema that enforces required fields for the selected auth option
+ * using the multi-step auth config. This keeps validation in sync with the UI
+ * definitions alongside the schema utilities.
+ */
+function makeAuthOptionValidationSchema(
+ connectorName: string,
+ getAuthMethod?: () => string | undefined,
+) {
+ const schema = getConnectorSchema(connectorName);
+ if (!schema) return null;
+
+ const fieldValidations: Record = {};
+ const requiredByMethod = getRequiredFieldsByEnumValue(schema, {
+ step: "connector",
+ });
+
+ for (const [method, fields] of Object.entries(requiredByMethod || {})) {
+ for (const fieldId of fields) {
+ const label = getFieldLabel(schema as MultiStepFormSchema, fieldId);
+ fieldValidations[fieldId] = (
+ fieldValidations[fieldId] || yup.string()
+ ).test(
+ `required-${fieldId}-${method}`,
+ `${label} is required`,
+ (value) => {
+ if (!getAuthMethod) return true;
+ const current = getAuthMethod();
+ if (current !== method) return true;
+ return !!value;
+ },
+ );
+ }
+ }
+
+ // If nothing to validate, skip dynamic schema.
+ if (!Object.keys(fieldValidations).length) return null;
+
+ return yup.object().shape(fieldValidations);
+}
diff --git a/web-common/src/features/sources/modal/GCSMultiStepForm.svelte b/web-common/src/features/sources/modal/GCSMultiStepForm.svelte
deleted file mode 100644
index e007489218a..00000000000
--- a/web-common/src/features/sources/modal/GCSMultiStepForm.svelte
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
-
-
-
-
- {#each filteredParamsProperties as property (property.key)}
- {@const propertyKey = property.key ?? ""}
- {#if propertyKey !== "path" && propertyKey !== "google_application_credentials" && propertyKey !== "key_id" && propertyKey !== "secret"}
-
- {#if property.type === ConnectorDriverPropertyType.TYPE_STRING || property.type === ConnectorDriverPropertyType.TYPE_NUMBER}
- onStringInputChange(e)}
- alwaysShowError
- />
- {:else if property.type === ConnectorDriverPropertyType.TYPE_BOOLEAN}
-
- {:else if property.type === ConnectorDriverPropertyType.TYPE_INFORMATIONAL}
-
- {/if}
-
- {/if}
- {/each}
-
diff --git a/web-common/src/features/sources/modal/JSONSchemaFieldControl.svelte b/web-common/src/features/sources/modal/JSONSchemaFieldControl.svelte
new file mode 100644
index 00000000000..6466f5132f7
--- /dev/null
+++ b/web-common/src/features/sources/modal/JSONSchemaFieldControl.svelte
@@ -0,0 +1,71 @@
+
+
+{#if prop["x-display"] === "file" || prop.format === "file"}
+
+{:else if prop.type === "boolean"}
+
+{:else if isSelectEnum && options}
+
+{:else if options?.length}
+
+{:else}
+ onStringInputChange(e)}
+ alwaysShowError
+ />
+{/if}
diff --git a/web-common/src/features/sources/modal/MultiStepConnectorFlow.svelte b/web-common/src/features/sources/modal/MultiStepConnectorFlow.svelte
new file mode 100644
index 00000000000..989956ba657
--- /dev/null
+++ b/web-common/src/features/sources/modal/MultiStepConnectorFlow.svelte
@@ -0,0 +1,255 @@
+
+
+{#if stepState.step === "connector"}
+
+ {#if activeSchema}
+
+ {/if}
+
+{:else}
+
+ {#if activeSchema}
+
+ {/if}
+
+{/if}
diff --git a/web-common/src/features/sources/modal/connector-schemas.ts b/web-common/src/features/sources/modal/connector-schemas.ts
new file mode 100644
index 00000000000..90b228320c2
--- /dev/null
+++ b/web-common/src/features/sources/modal/connector-schemas.ts
@@ -0,0 +1,66 @@
+import type { MultiStepFormSchema } from "../../templates/schemas/types";
+import { athenaSchema } from "../../templates/schemas/athena";
+import { azureSchema } from "../../templates/schemas/azure";
+import { bigquerySchema } from "../../templates/schemas/bigquery";
+import { clickhouseSchema } from "../../templates/schemas/clickhouse";
+import { clickhouseCloudSchema } from "../../templates/schemas/clickhouse-cloud";
+import { druidSchema } from "../../templates/schemas/druid";
+import { duckdbSchema } from "../../templates/schemas/duckdb";
+import { gcsSchema } from "../../templates/schemas/gcs";
+import { httpsSchema } from "../../templates/schemas/https";
+import { localFileSchema } from "../../templates/schemas/local_file";
+import { motherduckSchema } from "../../templates/schemas/motherduck";
+import { mysqlSchema } from "../../templates/schemas/mysql";
+import { pinotSchema } from "../../templates/schemas/pinot";
+import { postgresSchema } from "../../templates/schemas/postgres";
+import { redshiftSchema } from "../../templates/schemas/redshift";
+import { s3Schema } from "../../templates/schemas/s3";
+import { salesforceSchema } from "../../templates/schemas/salesforce";
+import { snowflakeSchema } from "../../templates/schemas/snowflake";
+import { sqliteSchema } from "../../templates/schemas/sqlite";
+
+export const multiStepFormSchemas: Record = {
+ s3: s3Schema,
+ gcs: gcsSchema,
+ azure: azureSchema,
+ https: httpsSchema,
+ postgres: postgresSchema,
+ mysql: mysqlSchema,
+ snowflake: snowflakeSchema,
+ bigquery: bigquerySchema,
+ redshift: redshiftSchema,
+ athena: athenaSchema,
+ clickhousecloud: clickhouseCloudSchema,
+ clickhouse: clickhouseSchema,
+ duckdb: duckdbSchema,
+ motherduck: motherduckSchema,
+ druid: druidSchema,
+ pinot: pinotSchema,
+ // Source-only connectors (no multi-step flow)
+ salesforce: salesforceSchema,
+ sqlite: sqliteSchema,
+ local_file: localFileSchema,
+};
+
+export function getConnectorSchema(
+ connectorName: string,
+): MultiStepFormSchema | null {
+ const schema =
+ multiStepFormSchemas[connectorName as keyof typeof multiStepFormSchemas];
+ if (!schema?.properties) return null;
+ return schema;
+}
+
+export function isStepMatch(
+ schema: MultiStepFormSchema | null,
+ key: string,
+ step?: "connector" | "source" | string,
+): boolean {
+ if (!schema?.properties) return false;
+ const prop = schema.properties[key];
+ if (!prop) return false;
+ if (!step) return true;
+ const propStep = prop["x-step"];
+ if (!propStep) return true;
+ return propStep === step;
+}
diff --git a/web-common/src/features/sources/modal/connectorStepStore.ts b/web-common/src/features/sources/modal/connectorStepStore.ts
index 9ce2451f126..9f58cb53645 100644
--- a/web-common/src/features/sources/modal/connectorStepStore.ts
+++ b/web-common/src/features/sources/modal/connectorStepStore.ts
@@ -1,13 +1,17 @@
import { writable } from "svelte/store";
-export type ConnectorStep = "connector" | "source";
+export type ConnectorStep = "connector" | "source" | "explorer";
-export const connectorStepStore = writable<{
+export type ConnectorStepState = {
step: ConnectorStep;
connectorConfig: Record | null;
-}>({
+ selectedAuthMethod: string | null;
+};
+
+export const connectorStepStore = writable({
step: "connector",
connectorConfig: null,
+ selectedAuthMethod: null,
});
export function setStep(step: ConnectorStep) {
@@ -18,9 +22,17 @@ export function setConnectorConfig(config: Record) {
connectorStepStore.update((state) => ({ ...state, connectorConfig: config }));
}
+export function setAuthMethod(method: string | null) {
+ connectorStepStore.update((state) => ({
+ ...state,
+ selectedAuthMethod: method,
+ }));
+}
+
export function resetConnectorStep() {
connectorStepStore.set({
step: "connector",
connectorConfig: null,
+ selectedAuthMethod: null,
});
}
diff --git a/web-common/src/features/sources/modal/constants.ts b/web-common/src/features/sources/modal/constants.ts
index 4bffc52825d..5453813c369 100644
--- a/web-common/src/features/sources/modal/constants.ts
+++ b/web-common/src/features/sources/modal/constants.ts
@@ -1,44 +1,8 @@
-export type ClickHouseConnectorType =
- | "rill-managed"
- | "self-hosted"
- | "clickhouse-cloud";
-
-export const CONNECTOR_TYPE_OPTIONS: {
- value: ClickHouseConnectorType;
- label: string;
-}[] = [
- { value: "rill-managed", label: "Rill-managed ClickHouse" },
- { value: "self-hosted", label: "Self-hosted ClickHouse" },
- { value: "clickhouse-cloud", label: "ClickHouse Cloud" },
-];
-
export const CONNECTION_TAB_OPTIONS: { value: string; label: string }[] = [
{ value: "parameters", label: "Enter parameters" },
{ value: "dsn", label: "Enter connection string" },
];
-export type GCSAuthMethod = "credentials" | "hmac";
-
-export const GCS_AUTH_OPTIONS: {
- value: GCSAuthMethod;
- label: string;
- description: string;
- hint?: string;
-}[] = [
- {
- value: "credentials",
- label: "GCP credentials",
- description:
- "Upload a JSON key file for a service account with GCS access.",
- },
- {
- value: "hmac",
- label: "HMAC keys",
- description:
- "Use HMAC access key and secret for S3-compatible authentication.",
- },
-];
-
// pre-defined order for sources
export const SOURCES = [
"athena",
@@ -57,6 +21,7 @@ export const SOURCES = [
];
export const OLAP_ENGINES = [
+ "clickhousecloud",
"clickhouse",
"motherduck",
"duckdb",
@@ -67,12 +32,34 @@ export const OLAP_ENGINES = [
export const ALL_CONNECTORS = [...SOURCES, ...OLAP_ENGINES];
// Connectors that support multi-step forms (connector -> source)
-export const MULTI_STEP_CONNECTORS = ["gcs"];
+export const MULTI_STEP_CONNECTORS = [
+ "gcs",
+ "s3",
+ "azure",
+ "https",
+ "postgres",
+ "mysql",
+ "snowflake",
+ "bigquery",
+ "redshift",
+ "athena",
+ "clickhousecloud",
+ "clickhouse",
+ "duckdb",
+ "motherduck",
+ "druid",
+ "pinot",
+];
-export const FORM_HEIGHT_TALL = "max-h-[38.5rem] min-h-[38.5rem]";
+export const FORM_HEIGHT_TALL = "max-h-[55.5rem] min-h-[38.5rem]";
+export const FORM_HEIGHT_MEDIUM = "max-h-[47.5rem] min-h-[34.5rem]";
export const FORM_HEIGHT_DEFAULT = "max-h-[34.5rem] min-h-[34.5rem]";
-export const TALL_FORM_CONNECTORS = new Set([
- "clickhouse",
- "snowflake",
+export const MEDIUM_FORM_CONNECTORS = new Set([
+ "clickhousecloud",
"salesforce",
+ "postgres",
+ "s3",
+ "mysql",
+ "pinot",
]);
+export const TALL_FORM_CONNECTORS = new Set(["clickhouse", "snowflake"]);
diff --git a/web-common/src/features/sources/modal/icons.ts b/web-common/src/features/sources/modal/icons.ts
index 9624685b2a5..2fd0e092ebf 100644
--- a/web-common/src/features/sources/modal/icons.ts
+++ b/web-common/src/features/sources/modal/icons.ts
@@ -16,7 +16,7 @@ import Postgres from "../../../components/icons/connectors/Postgres.svelte";
import Salesforce from "../../../components/icons/connectors/Salesforce.svelte";
import Snowflake from "../../../components/icons/connectors/Snowflake.svelte";
import SQLite from "../../../components/icons/connectors/SQLite.svelte";
-import ClickHouseCloud from "../../../components/icons/connectors/ClickHouseCloudIcon.svelte";
+import ClickHouseCloud from "../../../components/icons/connectors/ClickHouseCloud.svelte";
export const ICONS = {
gcs: GoogleCloudStorage,
@@ -34,8 +34,8 @@ export const ICONS = {
salesforce: Salesforce,
local_file: LocalFile,
https: Https,
- clickhouse: ClickHouse,
clickhousecloud: ClickHouseCloud,
+ clickhouse: ClickHouse,
druid: ApacheDruid,
pinot: ApachePinot,
};
diff --git a/web-common/src/features/sources/modal/types.ts b/web-common/src/features/sources/modal/types.ts
index 5cb0785a5a5..8fd405fe6de 100644
--- a/web-common/src/features/sources/modal/types.ts
+++ b/web-common/src/features/sources/modal/types.ts
@@ -1,3 +1,51 @@
+import type { MultiStepFormSchema } from "../../templates/schemas/types";
+
+export type {
+ JSONSchemaCondition,
+ JSONSchemaConditional,
+ JSONSchemaConstraint,
+ JSONSchemaField,
+ JSONSchemaObject,
+ MultiStepFormSchema,
+} from "../../templates/schemas/types";
+
export type AddDataFormType = "source" | "connector";
export type ConnectorType = "parameters" | "dsn";
+
+export type AuthOption = {
+ value: string;
+ label: string;
+ description: string;
+ hint?: string;
+};
+
+export type AuthField =
+ | {
+ type: "credentials";
+ id: string;
+ hint?: string;
+ optional?: boolean;
+ accept?: string;
+ }
+ | {
+ type: "input";
+ id: string;
+ label: string;
+ placeholder?: string;
+ optional?: boolean;
+ secret?: boolean;
+ hint?: string;
+ };
+
+export type MultiStepFormConfig = {
+ schema: MultiStepFormSchema;
+ authMethodKey: string;
+ authOptions: AuthOption[];
+ clearFieldsByMethod: Record;
+ excludedKeys: string[];
+ authFieldGroups: Record;
+ requiredFieldsByMethod: Record;
+ fieldLabels: Record;
+ defaultAuthMethod?: string;
+};
diff --git a/web-common/src/features/sources/modal/utils.ts b/web-common/src/features/sources/modal/utils.ts
index 5a7128802d2..ccaeefaecf3 100644
--- a/web-common/src/features/sources/modal/utils.ts
+++ b/web-common/src/features/sources/modal/utils.ts
@@ -1,12 +1,21 @@
import { humanReadableErrorMessage } from "../errors/errors";
import type { V1ConnectorDriver } from "@rilldata/web-common/runtime-client";
-import type { ClickHouseConnectorType } from "./constants";
+import type { MultiStepFormSchema } from "./types";
+import {
+ findRadioEnumKey,
+ getRadioEnumOptions,
+ getRequiredFieldsByEnumValue,
+} from "../../templates/schema-utils";
+import { isStepMatch } from "./connector-schemas";
/**
* Returns true for undefined, null, empty string, or whitespace-only string.
* Useful for validating optional text inputs.
*/
export function isEmpty(val: any) {
+ // Booleans are never empty (false is a valid value)
+ if (typeof val === "boolean") return false;
+
return (
val === undefined ||
val === null ||
@@ -84,24 +93,91 @@ export function hasOnlyDsn(
}
/**
- * Applies ClickHouse Cloud-specific default requirements for connector values.
- * - For ClickHouse Cloud: enforces `ssl: true`
- * - Otherwise returns values unchanged
+ * Returns true when the active multi-step auth method has missing or invalid
+ * required fields. Falls back to configured default/first auth method.
*/
-export function applyClickHouseCloudRequirements(
- connectorName: string | undefined,
- connectorType: ClickHouseConnectorType,
- values: Record,
-): Record {
- // Only force SSL for ClickHouse Cloud when the user is using individual params.
- // DSN strings encapsulate their own protocol, so we should not inject `ssl` there.
- const isDsnBased = "dsn" in values;
- const shouldEnforceSSL =
- connectorName === "clickhouse" &&
- connectorType === "clickhouse-cloud" &&
- !isDsnBased;
- if (shouldEnforceSSL) {
- return { ...values, ssl: true } as Record;
+export function isMultiStepConnectorDisabled(
+ schema: MultiStepFormSchema | null,
+ paramsFormValue: Record,
+ paramsFormErrors: Record,
+ currentStep: "connector" | "source" = "connector",
+) {
+ if (!schema || !paramsFormValue) return true;
+
+ const authInfo = getRadioEnumOptions(schema);
+
+ // Handle schemas without auth method radio selector (e.g., BigQuery, Athena)
+ if (!authInfo) {
+ const requiredFields = (schema.required ?? []).filter((fieldId) =>
+ isStepMatch(schema, fieldId, currentStep),
+ );
+ if (!requiredFields.length) return false;
+
+ return !requiredFields.every((fieldId) => {
+ const value = paramsFormValue[fieldId];
+ const errorsForField = paramsFormErrors[fieldId] as any;
+ const hasErrors = Boolean(errorsForField?.length);
+ return !isEmpty(value) && !hasErrors;
+ });
}
- return values;
+
+ const options = authInfo?.options ?? [];
+ const authKey = authInfo?.key || findRadioEnumKey(schema);
+ const methodFromForm =
+ authKey && paramsFormValue?.[authKey] != null
+ ? String(paramsFormValue[authKey])
+ : undefined;
+ const hasValidFormSelection = options.some(
+ (opt) => opt.value === methodFromForm,
+ );
+ const method =
+ (hasValidFormSelection && methodFromForm) ||
+ authInfo?.defaultValue ||
+ options[0]?.value;
+
+ if (!method) return true;
+
+ // Selecting "public" should always enable the button for multi-step auth flows.
+ if (method === "public") return false;
+
+ // When on source step and auth method isn't set (user skipped connector step),
+ // validate all source step required fields regardless of auth method
+ if (currentStep === "source" && !methodFromForm) {
+ const allSourceRequired = new Set();
+ const requiredByMethod = getRequiredFieldsByEnumValue(schema, {
+ step: currentStep,
+ });
+
+ // Collect all source step required fields across all auth methods
+ for (const fields of Object.values(requiredByMethod)) {
+ fields.forEach((field) => allSourceRequired.add(field));
+ }
+
+ const sourceRequiredFields = Array.from(allSourceRequired);
+ if (!sourceRequiredFields.length) return false;
+
+ return !sourceRequiredFields.every((fieldId) => {
+ const value = paramsFormValue[fieldId];
+ const errorsForField = paramsFormErrors[fieldId] as any;
+ const hasErrors = Boolean(errorsForField?.length);
+ return !isEmpty(value) && !hasErrors;
+ });
+ }
+
+ const requiredByMethod = getRequiredFieldsByEnumValue(schema, {
+ step: currentStep,
+ });
+ const requiredFields = requiredByMethod[method] ?? [];
+
+ // If no required fields found for this step, button should be enabled
+ if (!requiredFields.length) return false;
+
+ // Check if all required fields are filled and have no errors
+ return !requiredFields.every((fieldId) => {
+ if (!isStepMatch(schema, fieldId, currentStep)) return true;
+ const value = paramsFormValue[fieldId];
+ const errorsForField = paramsFormErrors[fieldId] as any;
+ const hasErrors = Boolean(errorsForField?.length);
+ return !isEmpty(value) && !hasErrors;
+ });
}
diff --git a/web-common/src/features/sources/modal/yupSchemas.ts b/web-common/src/features/sources/modal/yupSchemas.ts
index b12ec332eae..becb927a448 100644
--- a/web-common/src/features/sources/modal/yupSchemas.ts
+++ b/web-common/src/features/sources/modal/yupSchemas.ts
@@ -5,19 +5,29 @@ import {
} from "../../entity-management/name-utils";
export const getYupSchema = {
- s3: yup.object().shape({
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ s3_connector: yup.object().shape({
+ aws_access_key_id: yup.string().optional(),
+ aws_secret_access_key: yup.string().optional(),
+ region: yup.string().optional(),
+ endpoint: yup.string().optional(),
+ }),
+
+ s3_source: yup.object().shape({
path: yup
.string()
.matches(/^s3:\/\//, "Must be an S3 URI (e.g. s3://bucket/path)")
- .required("S3 URI is required"),
- aws_region: yup.string(),
+ .required(),
name: yup
.string()
.matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
- .required("Source name is required"),
+ .required(),
}),
- gcs: yup.object().shape({
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ gcs_connector: yup.object().shape({
google_application_credentials: yup.string().optional(),
key_id: yup.string().optional(),
secret: yup.string().optional(),
@@ -27,6 +37,34 @@ export const getYupSchema = {
.optional(),
}),
+ gcs_source: yup.object().shape({
+ path: yup
+ .string()
+ .matches(/^gs:\/\//, "Must be a GS URI (e.g. gs://bucket/path)")
+ .optional(),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required(),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ https_connector: yup.object().shape({
+ headers: yup.string().optional(),
+ }),
+
+ https_source: yup.object().shape({
+ path: yup
+ .string()
+ .matches(/^https?:\/\//, 'Path must start with "http(s)://"')
+ .required("Path is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Source name is required"),
+ }),
+
https: yup.object().shape({
path: yup
.string()
@@ -41,12 +79,38 @@ export const getYupSchema = {
duckdb: yup.object().shape({
path: yup.string().required("path is required"),
attach: yup.string().optional(),
+ sql: yup.string().optional(),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .optional(),
+ }),
+
+ duckdb_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
}),
motherduck: yup.object().shape({
token: yup.string().required("Token is required"),
path: yup.string().required("Path is required"),
schema_name: yup.string().required("Schema name is required"),
+ sql: yup.string().optional(),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .optional(),
+ }),
+
+ motherduck_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
}),
sqlite: yup.object().shape({
@@ -65,7 +129,17 @@ export const getYupSchema = {
.required("Google application credentials is required"),
}),
- azure: yup.object().shape({
+ // Keep these optional here; per-auth required fields are enforced dynamically
+ // via multi-step auth configs. This schema acts as a safe fallback (e.g. source
+ // step selection of `${name}_connector`).
+ azure_connector: yup.object().shape({
+ azure_storage_account: yup.string().optional(),
+ azure_storage_key: yup.string().optional(),
+ azure_storage_sas_token: yup.string().optional(),
+ azure_storage_connection_string: yup.string().optional(),
+ }),
+
+ azure_source: yup.object().shape({
path: yup
.string()
.matches(
@@ -73,13 +147,184 @@ export const getYupSchema = {
"Must be an Azure URI (e.g. azure://container/path)",
)
.required("Path is required"),
- azure_storage_account: yup.string(),
name: yup
.string()
.matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
.required("Source name is required"),
}),
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ postgres_connector: yup.object().shape({
+ host: yup.string().optional(),
+ port: yup.number().optional(),
+ database: yup.string().optional(),
+ user: yup.string().optional(),
+ password: yup.string().optional(),
+ sslmode: yup.string().optional(),
+ dsn: yup.string().optional(),
+ }),
+
+ postgres_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ mysql_connector: yup.object().shape({
+ host: yup.string().optional(),
+ port: yup.number().optional(),
+ database: yup.string().optional(),
+ user: yup.string().optional(),
+ password: yup.string().optional(),
+ sslmode: yup.string().optional(),
+ dsn: yup.string().optional(),
+ }),
+
+ mysql_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ snowflake_connector: yup.object().shape({
+ account: yup.string().optional(),
+ user: yup.string().optional(),
+ password: yup.string().optional(),
+ private_key: yup.string().optional(),
+ private_key_passphrase: yup.string().optional(),
+ warehouse: yup.string().optional(),
+ database: yup.string().optional(),
+ schema: yup.string().optional(),
+ role: yup.string().optional(),
+ dsn: yup.string().optional(),
+ }),
+
+ snowflake_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ bigquery_connector: yup.object().shape({
+ google_application_credentials: yup.string().optional(),
+ project_id: yup.string().optional(),
+ }),
+
+ bigquery_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ redshift_connector: yup.object().shape({
+ aws_access_key_id: yup.string().optional(),
+ aws_secret_access_key: yup.string().optional(),
+ region: yup.string().optional(),
+ database: yup.string().optional(),
+ workgroup: yup.string().optional(),
+ cluster_identifier: yup.string().optional(),
+ }),
+
+ redshift_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ athena_connector: yup.object().shape({
+ aws_access_key_id: yup.string().optional(),
+ aws_secret_access_key: yup.string().optional(),
+ region: yup.string().optional(),
+ workgroup: yup.string().optional(),
+ output_location: yup.string().optional(),
+ }),
+
+ athena_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ duckdb_connector: yup.object().shape({
+ path: yup.string().optional(),
+ attach: yup.string().optional(),
+ mode: yup.string().optional(),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ motherduck_connector: yup.object().shape({
+ path: yup.string().optional(),
+ token: yup.string().optional(),
+ schema_name: yup.string().optional(),
+ mode: yup.string().optional(),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ druid_connector: yup.object().shape({
+ host: yup.string().optional(),
+ port: yup.number().optional(),
+ username: yup.string().optional(),
+ password: yup.string().optional(),
+ ssl: yup.boolean().optional(),
+ dsn: yup.string().optional(),
+ }),
+
+ druid_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ pinot_connector: yup.object().shape({
+ broker_host: yup.string().optional(),
+ broker_port: yup.number().optional(),
+ controller_host: yup.string().optional(),
+ controller_port: yup.number().optional(),
+ username: yup.string().optional(),
+ password: yup.string().optional(),
+ ssl: yup.boolean().optional(),
+ dsn: yup.string().optional(),
+ }),
+
+ pinot_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
postgres: yup.object().shape({
dsn: yup.string().optional(),
host: yup.string().optional(),
@@ -162,6 +407,39 @@ export const getYupSchema = {
// .required("Connector name is required"),
}),
+ // Keep base auth fields optional; per-method required fields come from
+ // multi-step auth configs. This schema is a safe fallback.
+ clickhousecloud_connector: yup.object().shape({
+ host: yup.string().optional(),
+ port: yup.number().optional(),
+ username: yup.string().optional(),
+ password: yup.string().optional(),
+ database: yup.string().optional(),
+ cluster: yup.string().optional(),
+ mode: yup.string().optional(),
+ }),
+
+ clickhousecloud_source: yup.object().shape({
+ sql: yup.string().required("SQL query is required"),
+ name: yup
+ .string()
+ .matches(VALID_NAME_PATTERN, INVALID_NAME_MESSAGE)
+ .required("Model name is required"),
+ }),
+
+ clickhousecloud: yup.object().shape({
+ host: yup.string(),
+ port: yup
+ .string() // Purposefully using a string input, not a numeric input
+ .matches(/^\d+$/, "Port must be a number"),
+ username: yup.string(),
+ password: yup.string(),
+ database: yup.string(),
+ cluster: yup.string(),
+ mode: yup.string(),
+ name: yup.string(), // Required for typing
+ }),
+
druid: yup.object().shape({
host: yup
.string()
diff --git a/web-common/src/features/sources/sourceUtils.ts b/web-common/src/features/sources/sourceUtils.ts
index 480709142f6..b4c78fd870d 100644
--- a/web-common/src/features/sources/sourceUtils.ts
+++ b/web-common/src/features/sources/sourceUtils.ts
@@ -6,7 +6,10 @@ import {
type V1Source,
} from "@rilldata/web-common/runtime-client";
import { makeDotEnvConnectorKey } from "../connectors/code-utils";
+import { getDriverNameForConnector } from "../connectors/connectors-utils";
import { sanitizeEntityName } from "../entity-management/name-utils";
+import { getConnectorSchema } from "./modal/connector-schemas";
+import { findGroupedEnumKeys } from "../templates/schema-utils";
// Helper text that we put at the top of every Model YAML file
const SOURCE_MODEL_FILE_TOP = `# Model YAML
@@ -51,17 +54,22 @@ export function compileSourceYAML(
if (isSecretProperty) {
// For source files, we include secret properties
return `${key}: "{{ .env.${makeDotEnvConnectorKey(
- connector.name as string,
+ getDriverNameForConnector(connector.name as string),
key,
)} }}"`;
}
if (key === "sql") {
- // For SQL, we want to use a multi-line string
- return `${key}: |\n ${value
+ // For SQL, we want to use a multi-line string and add a dev section
+ const sqlLines = value
.split("\n")
- .map((line) => `${line}`)
- .join("\n")}`;
+ .map((line) => ` ${line}`)
+ .join("\n");
+ const devSqlLines = value
+ .split("\n")
+ .map((line) => ` ${line}`)
+ .join("\n");
+ return `${key}: |\n${sqlLines}\n\ndev:\n ${key}: |\n${devSqlLines}\n limit 10000`;
}
const isStringProperty = stringPropertyKeys.includes(key);
@@ -74,7 +82,7 @@ export function compileSourceYAML(
.join("\n");
return (
- `${SOURCE_MODEL_FILE_TOP}\n\nconnector: ${connector.name}\n\n` +
+ `${SOURCE_MODEL_FILE_TOP}\n\nconnector: ${getDriverNameForConnector(connector.name as string)}\n\n` +
compiledKeyValues
);
}
@@ -201,6 +209,130 @@ export function maybeRewriteToDuckDb(
return [connectorCopy, formValues];
}
+/**
+ * Prepare connector form values before submission.
+ * Handles special transformations like ClickHouse auth_method → managed.
+ */
+export function prepareConnectorFormData(
+ connector: V1ConnectorDriver,
+ formValues: Record,
+): Record {
+ const processedValues = { ...formValues };
+
+ // Get schema to check for grouped fields
+ const schema = connector.name ? getConnectorSchema(connector.name) : null;
+
+ if (schema) {
+ // Find all grouped enum keys (auth_method, connection_method, etc.)
+ const groupedEnumKeys = findGroupedEnumKeys(schema);
+
+ if (groupedEnumKeys.length > 0) {
+ // Collect all fields that should be included based on active selections
+ const allowedFields = new Set();
+
+ // For each grouped enum, find which fields are in the active option's group
+ for (const enumKey of groupedEnumKeys) {
+ const enumValue = processedValues[enumKey] as string | undefined;
+ const prop = schema.properties?.[enumKey];
+ const groupedFields = prop?.["x-grouped-fields"];
+
+ if (enumValue && groupedFields && groupedFields[enumValue]) {
+ // Add all fields from the active group
+ for (const fieldKey of groupedFields[enumValue]) {
+ allowedFields.add(fieldKey);
+ }
+ }
+ }
+
+ // Also include fields that aren't controlled by any grouped enum (standalone fields)
+ // Collect all fields that ARE controlled by some group
+ const allGroupedFieldKeys = new Set();
+ for (const enumKey of groupedEnumKeys) {
+ const prop = schema.properties?.[enumKey];
+ const groupedFields = prop?.["x-grouped-fields"];
+ if (groupedFields) {
+ for (const fieldArray of Object.values(groupedFields)) {
+ for (const fieldKey of fieldArray) {
+ allGroupedFieldKeys.add(fieldKey);
+ }
+ }
+ }
+ }
+
+ // Filter processedValues to only include allowed fields
+ const filteredValues: Record = {};
+ for (const [key, value] of Object.entries(processedValues)) {
+ // Include if:
+ // - It's in the allowed fields for active groups, OR
+ // - It's not controlled by any group (standalone field), OR
+ // - It's a grouped enum key itself (we'll remove it later if needed)
+ if (allowedFields.has(key) || !allGroupedFieldKeys.has(key)) {
+ filteredValues[key] = value;
+ }
+ }
+
+ // ClickHouse: translate auth_method to managed boolean BEFORE removing grouped enum keys
+ if (connector.name === "clickhouse" && processedValues.auth_method) {
+ const authMethod = processedValues.auth_method as string;
+
+ if (authMethod === "rill-managed") {
+ // Rill-managed: set managed=true, mode=readwrite
+ filteredValues.managed = true;
+ filteredValues.mode = "readwrite";
+ } else if (authMethod === "self-managed") {
+ // Self-managed: set managed=false
+ filteredValues.managed = false;
+ }
+ }
+
+ // ClickHouse Cloud: set managed=false, ssl will be in filteredValues if using parameters tab
+ if (connector.name === "clickhousecloud") {
+ filteredValues.managed = false;
+ // Only set ssl=true if it's in the filtered values (i.e., using parameters tab)
+ if ("ssl" in filteredValues) {
+ filteredValues.ssl = true;
+ }
+ }
+
+ // Replace with filtered values
+ Object.keys(processedValues).forEach(
+ (key) => delete processedValues[key],
+ );
+ Object.assign(processedValues, filteredValues);
+
+ // Remove the grouped enum keys themselves - they're UI-only fields
+ for (const enumKey of groupedEnumKeys) {
+ delete processedValues[enumKey];
+ }
+ }
+ } else {
+ // No schema, handle ClickHouse auth_method the old way
+ if (connector.name === "clickhouse" && processedValues.auth_method) {
+ const authMethod = processedValues.auth_method as string;
+
+ if (authMethod === "rill-managed") {
+ // Rill-managed: set managed=true, mode=readwrite
+ processedValues.managed = true;
+ processedValues.mode = "readwrite";
+ } else if (authMethod === "self-managed") {
+ // Self-managed: set managed=false
+ processedValues.managed = false;
+ }
+
+ // Remove the UI-only auth_method field
+ delete processedValues.auth_method;
+ }
+
+ // ClickHouse Cloud: set managed=false and ssl=true (only in non-schema path)
+ if (connector.name === "clickhousecloud") {
+ processedValues.managed = false;
+ processedValues.ssl = true;
+ }
+ }
+
+ return processedValues;
+}
+
/**
* Process form data for sources, including DuckDB rewrite logic and placeholder handling.
* This serves as a single source of truth for both preview and submission.
@@ -212,36 +344,47 @@ export function prepareSourceFormData(
// Create a copy of form values to avoid mutating the original
const processedValues = { ...formValues };
+ // Apply DuckDB rewrite logic FIRST (before stripping connector properties)
+ // This is important for connectors like SQLite that need connector properties
+ // to build the SQL query before they're removed.
+ const [rewrittenConnector, rewrittenFormValues] = maybeRewriteToDuckDb(
+ connector,
+ processedValues,
+ );
+
// Strip connector configuration keys from the source form values to prevent
// leaking connector-level fields (e.g., credentials) into the model file.
if (connector.configProperties) {
const connectorPropertyKeys = new Set(
connector.configProperties.map((p) => p.key).filter(Boolean),
);
- for (const key of Object.keys(processedValues)) {
+ for (const key of Object.keys(rewrittenFormValues)) {
if (connectorPropertyKeys.has(key)) {
- delete processedValues[key];
+ delete rewrittenFormValues[key];
}
}
}
+ // Also strip UI-only grouped enum keys (auth_method, connection_method, mode, etc.)
+ const schema = connector.name ? getConnectorSchema(connector.name) : null;
+ if (schema) {
+ const groupedEnumKeys = findGroupedEnumKeys(schema);
+ for (const key of groupedEnumKeys) {
+ delete rewrittenFormValues[key];
+ }
+ }
+
// Handle placeholder values for required source properties
- if (connector.sourceProperties) {
- for (const prop of connector.sourceProperties) {
- if (prop.key && prop.required && !(prop.key in processedValues)) {
+ if (rewrittenConnector.sourceProperties) {
+ for (const prop of rewrittenConnector.sourceProperties) {
+ if (prop.key && prop.required && !(prop.key in rewrittenFormValues)) {
if (prop.placeholder) {
- processedValues[prop.key] = prop.placeholder;
+ rewrittenFormValues[prop.key] = prop.placeholder;
}
}
}
}
- // Apply DuckDB rewrite logic
- const [rewrittenConnector, rewrittenFormValues] = maybeRewriteToDuckDb(
- connector,
- processedValues,
- );
-
return [rewrittenConnector, rewrittenFormValues];
}
diff --git a/web-common/src/features/templates/JSONSchemaFormRenderer.svelte b/web-common/src/features/templates/JSONSchemaFormRenderer.svelte
new file mode 100644
index 00000000000..96e218baf7c
--- /dev/null
+++ b/web-common/src/features/templates/JSONSchemaFormRenderer.svelte
@@ -0,0 +1,673 @@
+
+
+{#if schema}
+ {#each regularFields as [key, prop]}
+ {#if isRadioEnum(prop)}
+
+ {#if prop.title}
+
{prop.title}
+ {/if}
+
+
+ {#if groupedFields.get(key)}
+ {@const allGroupedForOption = getGroupedFieldsForOption(
+ key,
+ option.value,
+ )}
+ {@const regularGrouped = allGroupedForOption.filter(
+ ([_, prop]) => !prop["x-advanced"],
+ )}
+ {@const advancedGrouped = allGroupedForOption.filter(
+ ([_, prop]) => prop["x-advanced"],
+ )}
+ {#each regularGrouped as [childKey, childProp]}
+ {#if isTabsEnum(childProp)}
+
+ {#if childProp.title}
+
+ {childProp.title}
+
+ {/if}
+
+ {#if groupedFields.get(childKey)}
+ {#each tabOptions(childProp) as tabOption}
+
+ {#each getGroupedFieldsForOption(childKey, tabOption.value) as [grandchildKey, grandchildProp]}
+
+
+
+ {/each}
+
+ {/each}
+ {/if}
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+ {#if advancedGrouped.length > 0}
+
+
+ Advanced Configuration
+
+
+ {#each advancedGrouped as [childKey, childProp]}
+
+
+
+ {/each}
+
+
+ {/if}
+ {/if}
+
+
+
+ {:else if isTabsEnum(prop)}
+
+ {#if prop.title}
+
{prop.title}
+ {/if}
+
+ {#if groupedFields.get(key)}
+ {#each tabOptions(prop) as option}
+
+ {@const allGroupedForTabOption = getGroupedFieldsForOption(
+ key,
+ option.value,
+ )}
+ {@const regularGroupedTab = allGroupedForTabOption.filter(
+ ([_, prop]) => !prop["x-advanced"],
+ )}
+ {@const advancedGroupedTab = allGroupedForTabOption.filter(
+ ([_, prop]) => prop["x-advanced"],
+ )}
+ {#each regularGroupedTab as [childKey, childProp]}
+ {#if isRadioEnum(childProp)}
+
+ {#if childProp.title}
+
+ {childProp.title}
+
+ {/if}
+
+
+ {#if groupedFields.get(childKey)}
+ {#each getGroupedFieldsForOption(childKey, radioOption.value) as [grandchildKey, grandchildProp]}
+
+
+
+ {/each}
+ {/if}
+
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+ {#if advancedGroupedTab.length > 0}
+
+
+ Advanced Configuration
+
+
+ {#each advancedGroupedTab as [childKey, childProp]}
+
+
+
+ {/each}
+
+
+ {/if}
+
+ {/each}
+ {/if}
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+
+ {#if advancedFields.length > 0}
+
+
+ Advanced Configuration
+
+
+ {#each advancedFields as [key, prop]}
+ {#if isRadioEnum(prop)}
+
+ {#if prop.title}
+
{prop.title}
+ {/if}
+
+
+ {#if groupedFields.get(key)}
+ {#each getGroupedFieldsForOption(key, option.value) as [childKey, childProp]}
+ {#if isTabsEnum(childProp)}
+
+ {#if childProp.title}
+
+ {childProp.title}
+
+ {/if}
+
+ {#if groupedFields.get(childKey)}
+ {#each tabOptions(childProp) as tabOption}
+
+ {#each getGroupedFieldsForOption(childKey, tabOption.value) as [grandchildKey, grandchildProp]}
+
+
+
+ {/each}
+
+ {/each}
+ {/if}
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+ {/if}
+
+
+
+ {:else if isTabsEnum(prop)}
+
+ {#if prop.title}
+
{prop.title}
+ {/if}
+
+ {#if groupedFields.get(key)}
+ {#each tabOptions(prop) as option}
+
+ {#each getGroupedFieldsForOption(key, option.value) as [childKey, childProp]}
+ {#if isRadioEnum(childProp)}
+
+ {#if childProp.title}
+
+ {childProp.title}
+
+ {/if}
+
+
+ {#if groupedFields.get(childKey)}
+ {#each getGroupedFieldsForOption(childKey, radioOption.value) as [grandchildKey, grandchildProp]}
+
+
+
+ {/each}
+ {/if}
+
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+
+ {/each}
+ {/if}
+
+
+ {:else}
+
+
+
+ {/if}
+ {/each}
+
+
+ {/if}
+{/if}
diff --git a/web-common/src/features/templates/schema-utils.ts b/web-common/src/features/templates/schema-utils.ts
new file mode 100644
index 00000000000..30ea9436346
--- /dev/null
+++ b/web-common/src/features/templates/schema-utils.ts
@@ -0,0 +1,162 @@
+import type {
+ JSONSchemaConditional,
+ MultiStepFormSchema,
+} from "./schemas/types";
+
+export type RadioEnumOption = {
+ value: string;
+ label: string;
+ description: string;
+ hint?: string;
+};
+
+export function isVisibleForValues(
+ schema: MultiStepFormSchema,
+ key: string,
+ values: Record,
+): boolean {
+ const prop = schema.properties?.[key];
+ if (!prop) return false;
+ const conditions = prop["x-visible-if"];
+ if (!conditions) return true;
+
+ return Object.entries(conditions).every(([depKey, expected]) => {
+ const actual = values?.[depKey];
+ if (Array.isArray(expected)) {
+ return expected.map(String).includes(String(actual));
+ }
+ return String(actual) === String(expected);
+ });
+}
+
+export function getFieldLabel(
+ schema: MultiStepFormSchema,
+ key: string,
+): string {
+ return schema.properties?.[key]?.title || key;
+}
+
+/**
+ * Find all enum keys (radio or tabs) that have grouped fields.
+ * These are UI control fields (like auth_method, connection_method) that shouldn't appear in YAML.
+ */
+export function findGroupedEnumKeys(schema: MultiStepFormSchema): string[] {
+ if (!schema.properties) return [];
+ const keys: string[] = [];
+ for (const [key, value] of Object.entries(schema.properties)) {
+ if (value.enum && value["x-grouped-fields"]) {
+ keys.push(key);
+ }
+ }
+ return keys;
+}
+
+export function findRadioEnumKey(schema: MultiStepFormSchema): string | null {
+ if (!schema.properties) return null;
+ for (const [key, value] of Object.entries(schema.properties)) {
+ // Return radio or tabs fields that have grouped fields - those are auth/connection method selectors
+ // Standalone radio fields (like "mode") should not be considered auth method keys
+ const display = value["x-display"];
+ if (
+ value.enum &&
+ (display === "radio" || display === "tabs") &&
+ value["x-grouped-fields"]
+ ) {
+ return key;
+ }
+ }
+ return schema.properties.auth_method ? "auth_method" : null;
+}
+
+export function getRadioEnumOptions(schema: MultiStepFormSchema): {
+ key: string;
+ options: RadioEnumOption[];
+ defaultValue?: string;
+} | null {
+ const enumKey = findRadioEnumKey(schema);
+ if (!enumKey) return null;
+ const enumProperty = schema.properties?.[enumKey];
+ if (!enumProperty?.enum) return null;
+
+ const labels = enumProperty["x-enum-labels"] ?? [];
+ const descriptions = enumProperty["x-enum-descriptions"] ?? [];
+ const options =
+ enumProperty.enum?.map((value, idx) => ({
+ value: String(value),
+ label: labels[idx] ?? String(value),
+ description:
+ descriptions[idx] ?? enumProperty.description ?? "Choose an option",
+ hint: enumProperty["x-hint"],
+ })) ?? [];
+
+ const defaultValue =
+ enumProperty.default !== undefined && enumProperty.default !== null
+ ? String(enumProperty.default)
+ : options[0]?.value;
+
+ return {
+ key: enumKey,
+ options,
+ defaultValue: defaultValue || undefined,
+ };
+}
+
+export function getRequiredFieldsByEnumValue(
+ schema: MultiStepFormSchema,
+ opts?: { step?: "connector" | "source" | string },
+): Record {
+ const enumInfo = getRadioEnumOptions(schema);
+ if (!enumInfo) return {};
+
+ const conditionals = schema.allOf ?? [];
+ const baseRequired = new Set(schema.required ?? []);
+ const result: Record = {};
+
+ const matchesStep = (field: string) => {
+ if (!opts?.step) return true;
+ const prop = schema.properties?.[field];
+ if (!prop) return false;
+ const propStep = prop["x-step"];
+ if (!propStep) return true;
+ return propStep === opts.step;
+ };
+
+ for (const option of enumInfo.options) {
+ const required = new Set();
+
+ baseRequired.forEach((field) => {
+ if (matchesStep(field)) {
+ required.add(field);
+ }
+ });
+
+ for (const conditional of conditionals) {
+ const matches = matchesEnumCondition(
+ conditional,
+ enumInfo.key,
+ option.value,
+ );
+ const target = matches ? conditional.then : conditional.else;
+ target?.required?.forEach((field) => {
+ if (matchesStep(field)) {
+ required.add(field);
+ }
+ });
+ }
+
+ result[option.value] = Array.from(required);
+ }
+
+ return result;
+}
+
+function matchesEnumCondition(
+ conditional: JSONSchemaConditional,
+ enumKey: string,
+ value: string,
+) {
+ const conditionProps = conditional.if?.properties;
+ const constValue = conditionProps?.[enumKey]?.const;
+ if (constValue === undefined || constValue === null) return false;
+ return String(constValue) === value;
+}
diff --git a/web-common/src/features/templates/schemas/athena.ts b/web-common/src/features/templates/schemas/athena.ts
new file mode 100644
index 00000000000..245ee2a842c
--- /dev/null
+++ b/web-common/src/features/templates/schemas/athena.ts
@@ -0,0 +1,107 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const athenaSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ aws_access_key_id: {
+ type: "string",
+ title: "AWS Access Key ID",
+ description: "AWS access key ID",
+ "x-placeholder": "Enter AWS access key ID",
+ "x-secret": true,
+ "x-step": "connector",
+ },
+ aws_secret_access_key: {
+ type: "string",
+ title: "AWS Secret Access Key",
+ description: "AWS secret access key",
+ "x-placeholder": "Enter AWS secret access key",
+ "x-secret": true,
+ "x-step": "connector",
+ },
+ output_location: {
+ type: "string",
+ title: "S3 Output Location",
+ description: "S3 URI for query results",
+ "x-placeholder": "s3://my-bucket/athena-results/",
+ "x-step": "connector",
+ },
+ region: {
+ type: "string",
+ title: "AWS Region",
+ description: "AWS region where Athena is configured",
+ "x-placeholder": "us-east-1",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ role_session_name: {
+ type: "string",
+ title: "Role Session Name",
+ description:
+ "Optional session name to use when assuming an AWS role. Defaults to 'rill-session'.",
+ "x-placeholder": "my-session-name",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ external_id: {
+ type: "string",
+ title: "External ID",
+ description:
+ "Optional external ID to use when assuming an AWS role for cross-account access.",
+ "x-placeholder": "external-id-123",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+
+ workgroup: {
+ type: "string",
+ title: "Workgroup",
+ description: "Athena workgroup name (optional)",
+ "x-placeholder": "primary",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ aws_access_token: {
+ type: "string",
+ title: "AWS Access Token",
+ description: "AWS access token for authentication (optional)",
+ "x-placeholder": "Enter AWS access token",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ allow_host_access: {
+ type: "boolean",
+ title: "Allow Host Access",
+ description:
+ "Allow the connector to access the host's network (optional)",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from Athena",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: [
+ "aws_access_key_id",
+ "aws_secret_access_key",
+ "output_location",
+ "sql",
+ "name",
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/azure.ts b/web-common/src/features/templates/schemas/azure.ts
new file mode 100644
index 00000000000..5c0d144b28b
--- /dev/null
+++ b/web-common/src/features/templates/schemas/azure.ts
@@ -0,0 +1,143 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const azureSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Authentication method",
+ enum: ["connection_string", "account_key", "sas_token", "public"],
+ default: "connection_string",
+ description: "Choose how to authenticate to Azure Blob Storage",
+ "x-display": "radio",
+ "x-enum-labels": [
+ "Connection String",
+ "Storage Account Key",
+ "SAS Token",
+ "Public",
+ ],
+ "x-enum-descriptions": [
+ "Provide a full Azure Storage connection string.",
+ "Provide the storage account name and access key.",
+ "Provide the storage account name and SAS token.",
+ "Access publicly readable blobs without credentials.",
+ ],
+ "x-grouped-fields": {
+ connection_string: ["azure_storage_connection_string"],
+ account_key: ["azure_storage_account", "azure_storage_key"],
+ sas_token: ["azure_storage_account", "azure_storage_sas_token"],
+ public: [],
+ },
+ "x-step": "connector",
+ },
+ azure_storage_connection_string: {
+ type: "string",
+ title: "Connection string",
+ description: "Paste an Azure Storage connection string",
+ "x-placeholder": "Enter Azure storage connection string",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "connection_string" },
+ },
+ azure_storage_account: {
+ type: "string",
+ title: "Storage account",
+ description: "The name of the Azure storage account",
+ "x-placeholder": "Enter Azure storage account",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: ["account_key", "sas_token"] },
+ },
+ azure_storage_key: {
+ type: "string",
+ title: "Access key",
+ description: "Primary or secondary access key for the storage account",
+ "x-placeholder": "Enter Azure storage access key",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "account_key" },
+ },
+ azure_storage_sas_token: {
+ type: "string",
+ title: "SAS token",
+ description:
+ "Shared Access Signature token for the storage account (starting with ?sv=)",
+ "x-placeholder": "Enter Azure SAS token",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "sas_token" },
+ },
+ path_prefixes: {
+ type: "string",
+ title: "Prefixes",
+ description:
+ "List of prefixes to filter the blobs (e.g., ['logs/', 'data/'])",
+ "x-placeholder": "['logs/', 'data/']",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ allow_host_access: {
+ type: "boolean",
+ title: "Allow host access",
+ description:
+ "Allow access to the source from the host, useful for local development",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ path: {
+ type: "string",
+ title: "Blob URI",
+ description:
+ "URI to the Azure blob container or directory (e.g., https://.blob.core.windows.net/container)",
+ pattern: "^https?://[A-Za-z0-9.-]+\\.blob\\.core\\.windows\\.net/.+",
+ errorMessage: {
+ pattern:
+ "Enter a blob URL like https://account.blob.core.windows.net/container/path",
+ },
+ "x-placeholder": "https://account.blob.core.windows.net/container",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["path", "name"],
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "connection_string" } } },
+ then: { required: ["azure_storage_connection_string", "path", "name"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "account_key" } } },
+ then: {
+ required: [
+ "azure_storage_account",
+ "azure_storage_key",
+ "path",
+ "name",
+ ],
+ },
+ },
+ {
+ if: { properties: { auth_method: { const: "sas_token" } } },
+ then: {
+ required: [
+ "azure_storage_account",
+ "azure_storage_sas_token",
+ "path",
+ "name",
+ ],
+ },
+ },
+ {
+ if: { properties: { auth_method: { const: "public" } } },
+ then: { required: ["path", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/bigquery.ts b/web-common/src/features/templates/schemas/bigquery.ts
new file mode 100644
index 00000000000..cc0389c1b4a
--- /dev/null
+++ b/web-common/src/features/templates/schemas/bigquery.ts
@@ -0,0 +1,59 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const bigquerySchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ google_application_credentials: {
+ type: "string",
+ title: "GCP Credentials",
+ description:
+ "Upload a JSON key file for a service account with BigQuery access",
+ format: "file",
+ "x-display": "file",
+ "x-accept": ".json",
+ "x-step": "connector",
+ },
+ project_id: {
+ type: "string",
+ title: "Project ID",
+ description:
+ "Google Cloud project ID (optional if specified in credentials)",
+ "x-placeholder": "my-project-id",
+ "x-step": "connector",
+ },
+ allow_host_access: {
+ type: "boolean",
+ title: "Allow Host Access",
+ description:
+ "Allow the connector to access the host machine (useful for debugging)",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ log_queries: {
+ type: "boolean",
+ title: "Log Queries",
+ description: "Enable logging of all SQL queries (useful for debugging)",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from BigQuery",
+ "x-placeholder": "SELECT * FROM `project.dataset.table`;",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["google_application_credentials", "sql", "name"],
+};
diff --git a/web-common/src/features/templates/schemas/clickhouse-cloud.ts b/web-common/src/features/templates/schemas/clickhouse-cloud.ts
new file mode 100644
index 00000000000..cc25b09f72b
--- /dev/null
+++ b/web-common/src/features/templates/schemas/clickhouse-cloud.ts
@@ -0,0 +1,255 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const clickhouseCloudSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to connect to ClickHouse Cloud",
+ "x-display": "tabs",
+ "x-enum-labels": ["Username & Password", "Connection String"],
+ "x-grouped-fields": {
+ parameters: [
+ "host",
+ "port",
+ "database",
+ "username",
+ "password",
+ "ssl",
+ "cluster",
+ "mode",
+ ],
+ connection_string: ["dsn", "mode"],
+ },
+ "x-step": "connector",
+ },
+ host: {
+ type: "string",
+ title: "Host",
+ description: "Hostname or IP address of the ClickHouse Cloud server",
+ "x-placeholder": "your-instance.clickhouse.cloud",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ port: {
+ type: "string",
+ title: "Port",
+ description: "Port number of the ClickHouse Cloud server",
+ enum: ["8443", "9440"],
+ default: "8443",
+ "x-display": "select",
+ "x-enum-labels": ["8443 (HTTPS)", "9440 (Native Secure)"],
+ "x-hint": "ClickHouse Cloud uses secure ports only",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ username: {
+ type: "string",
+ title: "Username",
+ description: "Username to connect to the ClickHouse Cloud server",
+ "x-placeholder": "default",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Password to connect to the ClickHouse Cloud server",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ database: {
+ type: "string",
+ title: "Database",
+ description: "Name of the ClickHouse database to connect to",
+ "x-placeholder": "default",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ cluster: {
+ type: "string",
+ title: "Cluster",
+ description: "Cluster name for distributed tables",
+ "x-placeholder": "Cluster name",
+ "x-hint":
+ "If set, Rill will create models as distributed tables in the cluster",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ ssl: {
+ type: "boolean",
+ title: "SSL",
+ description: "Use SSL to connect to the ClickHouse server",
+ default: true,
+ "x-hint": "ClickHouse Cloud always uses SSL",
+ "x-readonly": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ database_whitelist: {
+ type: "string",
+ title: "Database Whitelist",
+ description: "List of allowed databases",
+ "x-placeholder": "db1,db2",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ "x-advanced": true,
+ },
+ optimize_temporary_tables_before_partition_replace: {
+ type: "string",
+ title: "Optimize Temporary Tables",
+ description: "Optimize temporary tables before partition replace",
+ "x-placeholder": "true",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ log_queries: {
+ type: "string",
+ title: "Log Queries",
+ description: "Log all queries executed by Rill",
+ "x-placeholder": "false",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ query_settings_override: {
+ type: "object",
+ title: "Query Settings Override",
+ description: "Override default query settings",
+ "x-placeholder": "key1=value1,key2=value2",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ query_settings: {
+ type: "object",
+ title: "Query Settings",
+ description: "Custom query settings",
+ "x-placeholder": "key1=value1,key2=value2",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ embed_port: {
+ type: "string",
+ title: "Embed Port",
+ description: "Port number for embedding the ClickHouse Cloud server",
+ "x-placeholder": "8443",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ can_scale_to_zero: {
+ type: "string",
+ title: "Can Scale to Zero",
+ description: "Enable scaling to zero",
+ "x-placeholder": "false",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ max_open_conns: {
+ type: "string",
+ title: "Max Open Connections",
+ description: "Maximum number of open connections",
+ "x-placeholder": "100",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ max_idle_conns: {
+ type: "string",
+ title: "Max Idle Connections",
+ description: "Maximum number of idle connections",
+ "x-placeholder": "10",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ dial_timeout: {
+ type: "string",
+ title: "Dial Timeout",
+ description: "Timeout for establishing a connection",
+ "x-placeholder": "30s",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ conn_max_lifetime: {
+ type: "string",
+ title: "Connection Max Lifetime",
+ description: "Maximum lifetime of a connection",
+ "x-placeholder": "30m",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ read_timeout: {
+ type: "string",
+ title: "Read Timeout",
+ description: "Timeout for reading from the connection",
+ "x-placeholder": "30s",
+ "x-advanced": true,
+ "x-step": "connector",
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "ClickHouse connection string (DSN)",
+ "x-placeholder":
+ "https://default@your-instance.clickhouse.cloud:8443/default",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "connection_string" },
+ },
+ mode: {
+ type: "string",
+ title: "Connection Mode",
+ description: "Database access mode",
+ enum: ["read", "readwrite"],
+ default: "read",
+ "x-display": "radio",
+ "x-enum-labels": ["Read-only", "Read-write"],
+ "x-enum-descriptions": [
+ "Only read operations are allowed (recommended for security)",
+ "Enable model creation and table mutations",
+ ],
+ "x-step": "connector",
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from ClickHouse",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ explorer_table: {
+ type: "string",
+ title: "Select a table",
+ description: "Select a table to generate metrics from",
+ "x-step": "explorer",
+ "x-visible-if": { mode: "read" },
+ },
+ },
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "parameters" } } },
+ then: { required: ["host", "database", "username", "password", "ssl"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "connection_string" } } },
+ then: { required: ["dsn"] },
+ },
+ {
+ if: { properties: { mode: { const: "readwrite" } } },
+ then: { required: ["sql", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/clickhouse.ts b/web-common/src/features/templates/schemas/clickhouse.ts
new file mode 100644
index 00000000000..7793493a28e
--- /dev/null
+++ b/web-common/src/features/templates/schemas/clickhouse.ts
@@ -0,0 +1,319 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const clickhouseSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection type",
+ enum: ["self-managed", "rill-managed"],
+ default: "self-managed",
+ description: "Choose how to connect to ClickHouse",
+ "x-display": "radio",
+ "x-enum-labels": ["Self-managed", "Rill-managed"],
+ "x-enum-descriptions": [
+ "Connect to your own self-hosted ClickHouse server.",
+ "Use a managed ClickHouse instance (starts embedded ClickHouse in development).",
+ ],
+ "x-grouped-fields": {
+ "self-managed": ["connection_method"],
+ "rill-managed": [],
+ },
+ "x-step": "connector",
+ },
+ connection_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to provide connection details",
+ "x-display": "tabs",
+ "x-enum-labels": ["Enter parameters", "Enter connection string"],
+ "x-grouped-fields": {
+ parameters: [
+ "host",
+ "port",
+ "username",
+ "password",
+ "database",
+ "ssl",
+ "cluster",
+ "mode",
+ ],
+ connection_string: ["dsn", "mode"],
+ },
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "self-managed" },
+ },
+ host: {
+ type: "string",
+ title: "Host",
+ description: "Hostname or IP address of the ClickHouse server",
+ "x-placeholder": "your-server.clickhouse.com",
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ },
+ port: {
+ type: "number",
+ title: "Port",
+ description: "Port number of the ClickHouse server",
+ default: 9000,
+ "x-placeholder": "9000",
+ "x-hint":
+ "Default: 9000 (native TCP), 8123 (HTTP). Secure: 9440 (TCP+TLS), 8443 (HTTPS)",
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ },
+ username: {
+ type: "string",
+ title: "Username",
+ description: "Username to connect to the ClickHouse server",
+ "x-placeholder": "default",
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Password to connect to the ClickHouse server",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ },
+ database: {
+ type: "string",
+ title: "Database",
+ description: "Name of the ClickHouse database to connect to",
+ default: "default",
+ "x-placeholder": "default",
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ },
+ ssl: {
+ type: "boolean",
+ title: "SSL",
+ description: "Use SSL to connect to the ClickHouse server",
+ default: true,
+ "x-hint": "Enable SSL for secure connections",
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ },
+ cluster: {
+ type: "string",
+ title: "Cluster",
+ description: "Cluster name for distributed tables",
+ "x-placeholder": "Cluster name",
+ "x-hint":
+ "If set, Rill will create models as distributed tables in the cluster",
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "parameters",
+ },
+ "x-advanced": true,
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "ClickHouse connection string (DSN)",
+ "x-placeholder": "clickhouse://username:password@host:port/database",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": {
+ auth_method: "self-managed",
+ connection_method: "connection_string",
+ },
+ },
+ mode: {
+ type: "string",
+ title: "Connection Mode",
+ description: "Database access mode",
+ enum: ["read", "readwrite"],
+ default: "read",
+ "x-display": "radio",
+ "x-enum-labels": ["Read-only", "Read-write"],
+ "x-enum-descriptions": [
+ "Only read operations are allowed (recommended for security)",
+ "Enable model creation and table mutations",
+ ],
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "self-managed" },
+ },
+ managed: {
+ type: "boolean",
+ title: "Managed",
+ description: "Enable managed mode for the ClickHouse server",
+ default: true,
+ "x-readonly": true,
+ "x-hint": "Enable managed mode to manage the server automatically",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "rill-managed" },
+ },
+ database_whitelist: {
+ type: "string",
+ title: "Database Whitelist",
+ description: "List of allowed databases",
+ "x-placeholder": "db1,db2",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ "x-advanced": true,
+ },
+ optimize_temporary_tables_before_partition_replace: {
+ type: "string",
+ title: "Optimize Temporary Tables",
+ description: "Optimize temporary tables before partition replace",
+ "x-placeholder": "true",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ log_queries: {
+ type: "string",
+ title: "Log Queries",
+ description: "Log all queries executed by Rill",
+ "x-placeholder": "false",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ query_settings_override: {
+ type: "object",
+ title: "Query Settings Override",
+ description: "Override default query settings",
+ "x-placeholder": "key1=value1,key2=value2",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ query_settings: {
+ type: "object",
+ title: "Query Settings",
+ description: "Custom query settings",
+ "x-placeholder": "key1=value1,key2=value2",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ embed_port: {
+ type: "string",
+ title: "Embed Port",
+ description: "Port number for embedding the ClickHouse Cloud server",
+ "x-placeholder": "8443",
+ "x-step": "connector",
+ "x-advanced": true,
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ can_scale_to_zero: {
+ type: "string",
+ title: "Can Scale to Zero",
+ description: "Enable scaling to zero",
+ "x-placeholder": "false",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ max_open_conns: {
+ type: "string",
+ title: "Max Open Connections",
+ description: "Maximum number of open connections",
+ "x-placeholder": "100",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ max_idle_conns: {
+ type: "string",
+ title: "Max Idle Connections",
+ description: "Maximum number of idle connections",
+ "x-placeholder": "10",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ dial_timeout: {
+ type: "string",
+ title: "Dial Timeout",
+ description: "Timeout for establishing a connection",
+ "x-placeholder": "30s",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ conn_max_lifetime: {
+ type: "string",
+ title: "Connection Max Lifetime",
+ description: "Maximum lifetime of a connection",
+ "x-placeholder": "30m",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ read_timeout: {
+ type: "string",
+ title: "Read Timeout",
+ description: "Timeout for reading from the connection",
+ "x-placeholder": "30s",
+ "x-advanced": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "self-managed" },
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from ClickHouse",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ explorer_table: {
+ type: "string",
+ title: "Select a table",
+ description: "Select a table to generate metrics from",
+ "x-step": "explorer",
+ "x-visible-if": { mode: "read" },
+ },
+ },
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "self-managed" } } },
+ then: { required: [] },
+ },
+ {
+ if: { properties: { auth_method: { const: "rill-managed" } } },
+ then: { required: [] },
+ },
+ {
+ if: { properties: { mode: { const: "readwrite" } } },
+ then: { required: ["sql", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/druid.ts b/web-common/src/features/templates/schemas/druid.ts
new file mode 100644
index 00000000000..6214ab0da8f
--- /dev/null
+++ b/web-common/src/features/templates/schemas/druid.ts
@@ -0,0 +1,114 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const druidSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to connect to Druid",
+ "x-display": "tabs",
+ "x-enum-labels": ["Enter parameters", "Enter connection string"],
+ "x-grouped-fields": {
+ parameters: ["host", "port", "username", "password", "ssl"],
+ connection_string: ["dsn"],
+ },
+ "x-step": "connector",
+ },
+ host: {
+ type: "string",
+ title: "Host",
+ description: "Hostname or IP address of the Druid server",
+ "x-placeholder": "localhost",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ port: {
+ type: "number",
+ title: "Port",
+ description: "Port number of the Druid server",
+ "x-placeholder": "8888",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ username: {
+ type: "string",
+ title: "Username",
+ description: "Username to connect to the Druid server (optional)",
+ "x-placeholder": "default",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Password to connect to the Druid server (optional)",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ max_open_conns: {
+ type: "number",
+ title: "Maximum open connections",
+ description: "Maximum number of open connections to the Druid server",
+ default: 10,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ skip_version_check: {
+ type: "boolean",
+ title: "Skip version check",
+ description: "Skip the version check when connecting to the Druid server",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ skip_query_priority: {
+ type: "boolean",
+ title: "Skip query priority",
+ description:
+ "Skip the query priority when connecting to the Druid server",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ ssl: {
+ type: "boolean",
+ title: "Use SSL",
+ description: "Use SSL to connect to the Druid server",
+ default: true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "Druid connection string (DSN)",
+ "x-placeholder":
+ "https://example.com/druid/v2/sql/avatica-protobuf?authentication=BASIC&avaticaUser=username&avaticaPassword=password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "connection_string" },
+ },
+ explorer_table: {
+ type: "string",
+ title: "Select a table",
+ description: "Select a table to generate metrics from",
+ "x-step": "explorer",
+ },
+ },
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "parameters" } } },
+ then: { required: ["host", "ssl"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "connection_string" } } },
+ then: { required: ["dsn"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/duckdb.ts b/web-common/src/features/templates/schemas/duckdb.ts
new file mode 100644
index 00000000000..679f6b3256a
--- /dev/null
+++ b/web-common/src/features/templates/schemas/duckdb.ts
@@ -0,0 +1,101 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const duckdbSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection type",
+ enum: ["self-managed", "rill-managed"],
+ default: "self-managed",
+ description: "Choose how to connect to DuckDB",
+ "x-display": "radio",
+ "x-enum-labels": ["Self-managed", "Rill-managed"],
+ "x-enum-descriptions": [
+ "Connect to your own self-hosted DuckDB server.",
+ "Use a managed DuckDB instance hosted by Rill.",
+ ],
+ "x-grouped-fields": {
+ "self-managed": ["path", "attach", "mode"],
+ "rill-managed": ["managed"],
+ },
+ "x-step": "connector",
+ },
+ path: {
+ type: "string",
+ title: "Database Path",
+ description: "Path to external DuckDB database file",
+ "x-placeholder": "/path/to/main.db",
+ "x-step": "connector",
+ },
+ attach: {
+ type: "string",
+ title: "Attach",
+ description:
+ "Attach to an existing DuckDB database with options (alternative to path)",
+ "x-placeholder":
+ "'ducklake:metadata.ducklake' AS my_ducklake(DATA_PATH 'datafiles')",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ mode: {
+ type: "string",
+ title: "Connection Mode",
+ description: "Database access mode",
+ enum: ["read", "readwrite"],
+ default: "readwrite",
+ "x-display": "radio",
+ "x-enum-labels": ["Read-only", "Read-write"],
+ "x-enum-descriptions": [
+ "Only read operations are allowed (recommended for security)",
+ "Enable model creation and table mutations",
+ ],
+ "x-step": "connector",
+ },
+ managed: {
+ type: "boolean",
+ title: "Managed",
+ description: "Enable managed mode for the ClickHouse server",
+ default: true,
+ "x-readonly": true,
+ "x-hint": "Enable managed mode to manage the server automatically",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "rill-managed" },
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from DuckDB",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ explorer_table: {
+ type: "string",
+ title: "Select a table",
+ description: "Select a table to generate metrics from",
+ "x-step": "explorer",
+ "x-visible-if": { mode: "read" },
+ },
+ },
+ allOf: [
+ {
+ if: { properties: { mode: { const: "readwrite" } } },
+ then: { required: ["path", "sql", "name"] },
+ },
+ {
+ if: { properties: { mode: { const: "read" } } },
+ then: { required: ["path"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/gcs.ts b/web-common/src/features/templates/schemas/gcs.ts
new file mode 100644
index 00000000000..c5586dbe34f
--- /dev/null
+++ b/web-common/src/features/templates/schemas/gcs.ts
@@ -0,0 +1,109 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const gcsSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Authentication method",
+ enum: ["credentials", "hmac", "public"],
+ default: "credentials",
+ description: "Choose how to authenticate to GCS",
+ "x-display": "radio",
+ "x-enum-labels": ["GCP credentials", "HMAC keys", "Public"],
+ "x-enum-descriptions": [
+ "Upload a JSON key file for a service account with GCS access.",
+ "Use HMAC access key and secret for S3-compatible authentication.",
+ "Access publicly readable buckets without credentials.",
+ ],
+ "x-grouped-fields": {
+ credentials: ["google_application_credentials"],
+ hmac: ["key_id", "secret"],
+ public: [],
+ },
+ "x-step": "connector",
+ },
+ google_application_credentials: {
+ type: "string",
+ title: "Service account key",
+ description:
+ "Upload a JSON key file for a service account with GCS access.",
+ format: "file",
+ "x-display": "file",
+ "x-accept": ".json",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "credentials" },
+ },
+ key_id: {
+ type: "string",
+ title: "Access Key ID",
+ description: "HMAC access key ID for S3-compatible authentication",
+ "x-placeholder": "Enter your HMAC access key ID",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "hmac" },
+ },
+ secret: {
+ type: "string",
+ title: "Secret Access Key",
+ description: "HMAC secret access key for S3-compatible authentication",
+ "x-placeholder": "Enter your HMAC secret access key",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "hmac" },
+ },
+ path_prefixes: {
+ type: "string",
+ title: "Path prefixes",
+ description:
+ "List of prefixes to filter the files in the GCS bucket. Leave empty to include all files.",
+ "x-placeholder": "['logs/', 'data/']",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ allow_host_access: {
+ type: "boolean",
+ title: "Allow host access",
+ description:
+ "Allow access to the GCS bucket from the host machine. This is useful for debugging and testing.",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ path: {
+ type: "string",
+ title: "GCS URI",
+ description: "Path to your GCS bucket or prefix",
+ pattern: "^gs://[^/]+(/.*)?$",
+ errorMessage: {
+ pattern: "Enter a GCS URI like gs://bucket or gs://bucket/path",
+ },
+ "x-placeholder": "gs://bucket/path",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["path", "name"],
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "credentials" } } },
+ then: { required: ["google_application_credentials", "path", "name"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "hmac" } } },
+ then: { required: ["key_id", "secret", "path", "name"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "public" } } },
+ then: { required: ["path", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/https.ts b/web-common/src/features/templates/schemas/https.ts
new file mode 100644
index 00000000000..ca3299876d0
--- /dev/null
+++ b/web-common/src/features/templates/schemas/https.ts
@@ -0,0 +1,72 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const httpsSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Authentication method",
+ enum: ["headers", "public"],
+ default: "headers",
+ description: "Choose how to authenticate to the REST API",
+ "x-display": "radio",
+ "x-enum-labels": ["Custom Headers", "Public"],
+ "x-enum-descriptions": [
+ "Access publicly available APIs without authentication.",
+ "Provide custom HTTP headers for authentication (e.g., Authorization, API keys).",
+ ],
+ "x-grouped-fields": {
+ headers: ["headers"],
+ },
+ "x-step": "connector",
+ },
+ headers: {
+ type: "string",
+ title: "HTTP Headers (JSON)",
+ description:
+ 'HTTP headers as JSON object. Example: {"Authorization": "Bearer my-token", "X-API-Key": "value"}',
+ "x-placeholder": '{"Authorization": "Bearer my-token"}',
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "headers" },
+ },
+ path: {
+ type: "string",
+ title: "URL",
+ description: "HTTP(S) URL to fetch data from",
+ pattern: "^https?://",
+ "x-placeholder": "https://api.example.com/data",
+ "x-step": "source",
+ },
+ format: {
+ type: "string",
+ title: "Data Format",
+ description: "Format of the data returned by the API",
+ enum: ["json", "csv"],
+ default: "json",
+ "x-display": "radio",
+ "x-enum-labels": ["JSON", "CSV"],
+ "x-enum-descriptions": ["JSON format", "CSV format"],
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["path", "name"],
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "headers" } } },
+ then: { required: ["headers", "path", "name"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "public" } } },
+ then: { required: ["path", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/local_file.ts b/web-common/src/features/templates/schemas/local_file.ts
new file mode 100644
index 00000000000..aba507a9503
--- /dev/null
+++ b/web-common/src/features/templates/schemas/local_file.ts
@@ -0,0 +1,34 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const localFileSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ path: {
+ type: "string",
+ title: "Path",
+ description: "Path or URL to file",
+ "x-placeholder": "/path/to/file.csv",
+ "x-step": "source",
+ },
+ format: {
+ type: "string",
+ title: "Format",
+ description: "File format. Inferred from extension if not set.",
+ enum: ["csv", "parquet", "json", "ndjson"],
+ "x-display": "select",
+ "x-enum-labels": ["CSV", "Parquet", "JSON", "NDJSON"],
+ "x-placeholder": "csv",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Source name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_local_source",
+ "x-step": "source",
+ },
+ },
+ required: ["path", "name"],
+};
diff --git a/web-common/src/features/templates/schemas/motherduck.ts b/web-common/src/features/templates/schemas/motherduck.ts
new file mode 100644
index 00000000000..6f91a841be9
--- /dev/null
+++ b/web-common/src/features/templates/schemas/motherduck.ts
@@ -0,0 +1,78 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const motherduckSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ path: {
+ type: "string",
+ title: "Database Path",
+ description: "Path to MotherDuck database (must be prefixed with 'md:')",
+ "x-placeholder": "md:my_db",
+ "x-step": "connector",
+ },
+ token: {
+ type: "string",
+ title: "MotherDuck Token",
+ description: "Your MotherDuck authentication token",
+ "x-placeholder": "Enter your MotherDuck token",
+ "x-secret": true,
+ "x-step": "connector",
+ },
+ schema_name: {
+ type: "string",
+ title: "Schema Name",
+ description: "Default schema used by the MotherDuck database",
+ "x-placeholder": "main",
+ "x-step": "connector",
+ },
+ mode: {
+ type: "string",
+ title: "Connection Mode",
+ description: "Database access mode",
+ enum: ["read", "readwrite"],
+ default: "read",
+ "x-display": "radio",
+ "x-enum-labels": ["Read-only", "Read-write"],
+ "x-enum-descriptions": [
+ "Only read operations are allowed (recommended for security)",
+ "Enable model creation and table mutations",
+ ],
+ "x-step": "connector",
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from MotherDuck",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ "x-visible-if": { mode: "readwrite" },
+ },
+ explorer_table: {
+ type: "string",
+ title: "Select a table",
+ description: "Select a table to generate metrics from",
+ "x-step": "explorer",
+ "x-visible-if": { mode: "read" },
+ },
+ },
+ allOf: [
+ {
+ if: { properties: { mode: { const: "readwrite" } } },
+ then: { required: ["path", "token", "schema_name", "sql", "name"] },
+ },
+ {
+ if: { properties: { mode: { const: "read" } } },
+ then: { required: ["path", "token", "schema_name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/mysql.ts b/web-common/src/features/templates/schemas/mysql.ts
new file mode 100644
index 00000000000..33cafa3326f
--- /dev/null
+++ b/web-common/src/features/templates/schemas/mysql.ts
@@ -0,0 +1,129 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const mysqlSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to connect to MySQL",
+ "x-display": "tabs",
+ "x-enum-labels": ["Enter parameters", "Enter connection string"],
+ "x-grouped-fields": {
+ parameters: [
+ "",
+ "host",
+ "port",
+ "database",
+ "user",
+ "password",
+ "ssl-mode",
+ "log_queries",
+ ],
+ connection_string: ["dsn", "log_queries"],
+ },
+ "x-step": "connector",
+ },
+ host: {
+ type: "string",
+ title: "Host",
+ description: "Database server hostname or IP address",
+ "x-placeholder": "localhost",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ port: {
+ type: "number",
+ title: "Port",
+ description: "Database server port",
+ default: 3306,
+ "x-placeholder": "3306",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ database: {
+ type: "string",
+ title: "Database",
+ description: "Database name",
+ "x-placeholder": "my_database",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ user: {
+ type: "string",
+ title: "Username",
+ description: "Database user",
+ "x-placeholder": "root",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Database password",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ "ssl-mode": {
+ type: "string",
+ title: "SSL Mode",
+ description: "SSL connection mode",
+ enum: ["", "disabled", "preferred", "required"],
+ default: "",
+ "x-display": "select",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ "x-advanced": true,
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "MySQL connection string (DSN)",
+ "x-placeholder": "user:password@tcp(host:3306)/dbname?tls=preferred",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "connection_string" },
+ },
+ log_queries: {
+ type: "boolean",
+ title: "Log Queries",
+ description: "Enable logging of all SQL queries (useful for debugging)",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from MySQL",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["sql", "name"],
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "parameters" } } },
+ then: {
+ required: ["host", "database", "user", "password", "sql", "name"],
+ },
+ },
+ {
+ if: { properties: { auth_method: { const: "connection_string" } } },
+ then: { required: ["dsn", "sql", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/pinot.ts b/web-common/src/features/templates/schemas/pinot.ts
new file mode 100644
index 00000000000..800321b7761
--- /dev/null
+++ b/web-common/src/features/templates/schemas/pinot.ts
@@ -0,0 +1,115 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const pinotSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to connect to Pinot",
+ "x-display": "tabs",
+ "x-enum-labels": ["Enter parameters", "Enter connection string"],
+ "x-grouped-fields": {
+ parameters: [
+ "broker_host",
+ "broker_port",
+ "controller_host",
+ "controller_port",
+ "username",
+ "password",
+ "ssl",
+ ],
+ connection_string: ["dsn"],
+ },
+ "x-step": "connector",
+ },
+ broker_host: {
+ type: "string",
+ title: "Broker Host",
+ description: "Hostname or IP address of the Pinot broker server",
+ "x-placeholder": "localhost",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ broker_port: {
+ type: "number",
+ title: "Broker Port",
+ description: "Port number of the Pinot broker server",
+ "x-placeholder": "8000",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ controller_host: {
+ type: "string",
+ title: "Controller Host",
+ description: "Hostname or IP address of the Pinot controller server",
+ "x-placeholder": "localhost",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ "x-advanced": true,
+ },
+ controller_port: {
+ type: "number",
+ title: "Controller Port",
+ description: "Port number of the Pinot controller server",
+ "x-placeholder": "9000",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ "x-advanced": true,
+ },
+ username: {
+ type: "string",
+ title: "Username",
+ description: "Username to connect to the Pinot server (optional)",
+ "x-placeholder": "default",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Password to connect to the Pinot server (optional)",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ ssl: {
+ type: "boolean",
+ title: "Use SSL",
+ description: "Use SSL to connect to the Pinot server",
+ default: true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "Pinot connection string (DSN)",
+ "x-placeholder":
+ "http(s)://username:password@localhost:8000?controller=localhost:9000",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "connection_string" },
+ },
+ explorer_table: {
+ type: "string",
+ title: "Select a table",
+ description: "Select a table to generate metrics from",
+ "x-step": "explorer",
+ },
+ },
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "parameters" } } },
+ then: { required: ["broker_host", "controller_host", "ssl"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "connection_string" } } },
+ then: { required: ["dsn"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/postgres.ts b/web-common/src/features/templates/schemas/postgres.ts
new file mode 100644
index 00000000000..6629a505ed2
--- /dev/null
+++ b/web-common/src/features/templates/schemas/postgres.ts
@@ -0,0 +1,127 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const postgresSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to connect to PostgreSQL",
+ "x-display": "tabs",
+ "x-enum-labels": ["Enter parameters", "Enter connection string"],
+ "x-grouped-fields": {
+ parameters: [
+ "host",
+ "port",
+ "dbname",
+ "user",
+ "password",
+ "sslmode",
+ "log_queries",
+ ],
+ connection_string: ["dsn", "log_queries"],
+ },
+ "x-step": "connector",
+ },
+ host: {
+ type: "string",
+ title: "Host",
+ description: "Database server hostname or IP address",
+ "x-placeholder": "localhost",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ port: {
+ type: "number",
+ title: "Port",
+ description: "Database server port",
+ default: 5432,
+ "x-placeholder": "5432",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ dbname: {
+ type: "string",
+ title: "Database",
+ description: "Database name",
+ "x-placeholder": "my_database",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ user: {
+ type: "string",
+ title: "Username",
+ description: "Database user",
+ "x-placeholder": "postgres",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Database password",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ },
+ sslmode: {
+ type: "string",
+ title: "SSL Mode",
+ description: "SSL connection mode",
+ enum: ["", "disable", "allow", "prefer", "require"],
+ default: "",
+ "x-display": "select",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "parameters" },
+ "x-advanced": true,
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "PostgreSQL connection string (DSN)",
+ "x-placeholder":
+ "postgres://user:password@host:5432/dbname?sslmode=require",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "connection_string" },
+ },
+ log_queries: {
+ type: "boolean",
+ title: "Log Queries",
+ description: "Enable logging of all SQL queries (useful for debugging)",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from PostgreSQL",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["sql", "name"],
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "parameters" } } },
+ then: { required: ["host", "dbname", "user", "password", "sql", "name"] },
+ },
+ {
+ if: { properties: { auth_method: { const: "connection_string" } } },
+ then: { required: ["dsn", "sql", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/redshift.ts b/web-common/src/features/templates/schemas/redshift.ts
new file mode 100644
index 00000000000..94a20829f7c
--- /dev/null
+++ b/web-common/src/features/templates/schemas/redshift.ts
@@ -0,0 +1,97 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const redshiftSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ aws_access_key_id: {
+ type: "string",
+ title: "AWS Access Key ID",
+ description: "AWS access key ID for Redshift Data API",
+ "x-placeholder": "Enter AWS access key ID",
+ "x-secret": true,
+ "x-step": "connector",
+ },
+ aws_secret_access_key: {
+ type: "string",
+ title: "AWS Secret Access Key",
+ description: "AWS secret access key for Redshift Data API",
+ "x-placeholder": "Enter AWS secret access key",
+ "x-secret": true,
+ "x-step": "connector",
+ },
+ region: {
+ type: "string",
+ title: "AWS Region",
+ description: "AWS region where the Redshift cluster is located",
+ "x-placeholder": "us-east-1",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ database: {
+ type: "string",
+ title: "Database",
+ description: "Redshift database name",
+ "x-placeholder": "dev",
+ "x-step": "connector",
+ },
+ workgroup: {
+ type: "string",
+ title: "Workgroup",
+ description: "Redshift Serverless workgroup name (for serverless)",
+ "x-placeholder": "default-workgroup",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ cluster_identifier: {
+ type: "string",
+ title: "Cluster Identifier",
+ description:
+ "Redshift provisioned cluster identifier (for provisioned clusters)",
+ "x-placeholder": "my-redshift-cluster",
+ "x-hint":
+ "Provide either workgroup (for serverless) or cluster identifier (for provisioned)",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ allow_host_access: {
+ type: "boolean",
+ title: "Allow Host Access",
+ description:
+ "Allow the connector to access the host's network (useful for private clusters)",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ log_queries: {
+ type: "boolean",
+ title: "Log Queries",
+ description: "Enable logging of all SQL queries (useful for debugging)",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from Redshift",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: [
+ "aws_access_key_id",
+ "aws_secret_access_key",
+ "database",
+ "sql",
+ "name",
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/s3.ts b/web-common/src/features/templates/schemas/s3.ts
new file mode 100644
index 00000000000..61d87f14103
--- /dev/null
+++ b/web-common/src/features/templates/schemas/s3.ts
@@ -0,0 +1,163 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const s3Schema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ auth_method: {
+ type: "string",
+ title: "Authentication method",
+ description: "Choose how to authenticate to S3",
+ enum: ["access_keys", "public"],
+ default: "access_keys",
+ "x-display": "radio",
+ "x-enum-labels": ["Access keys", "Public"],
+ "x-enum-descriptions": [
+ "Use AWS access key ID and secret access key.",
+ "Access publicly readable buckets without credentials.",
+ ],
+ "x-grouped-fields": {
+ access_keys: [
+ "aws_access_key_id",
+ "aws_secret_access_key",
+ "region",
+ "endpoint",
+ "aws_role_arn",
+ "aws_role_session_name",
+ "aws_external_id",
+ "path_prefixes",
+ "allow_host_access",
+ ],
+ public: [],
+ },
+ "x-step": "connector",
+ },
+ aws_access_key_id: {
+ type: "string",
+ title: "Access Key ID",
+ description: "AWS access key ID for the bucket",
+ "x-placeholder": "Enter AWS access key ID",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ },
+ aws_secret_access_key: {
+ type: "string",
+ title: "Secret Access Key",
+ description: "AWS secret access key for the bucket",
+ "x-placeholder": "Enter AWS secret access key",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ },
+ region: {
+ type: "string",
+ title: "Region",
+ description:
+ "Rill uses your default AWS region unless you set it explicitly.",
+ "x-placeholder": "us-east-1",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ },
+ endpoint: {
+ type: "string",
+ title: "Endpoint",
+ description:
+ "Override the S3 endpoint (for S3-compatible services like R2/MinIO).",
+ "x-placeholder": "https://s3.example.com",
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ "x-advanced": true,
+ },
+ aws_role_arn: {
+ type: "string",
+ title: "AWS Role ARN",
+ description: "AWS Role ARN to assume",
+ "x-placeholder": "arn:aws:iam::123456789012:role/MyRole",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ "x-advanced": true,
+ },
+ aws_role_session_name: {
+ type: "string",
+ title: "Role Session Name",
+ description:
+ "Optional session name to use when assuming an AWS role. Defaults to 'rill-session'.",
+ "x-placeholder": "my-session-name",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ "x-advanced": true,
+ },
+ aws_external_id: {
+ type: "string",
+ title: "External ID",
+ description:
+ "Optional external ID to use when assuming an AWS role for cross-account access.",
+ "x-placeholder": "external-id-123",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { auth_method: "access_keys" },
+ "x-advanced": true,
+ },
+ path_prefixes: {
+ type: "string",
+ title: "Path prefixes",
+ description: "Prefixes to include in the model",
+ "x-placeholder": "['logs/', 'data/']",
+ "x-step": "connector",
+ "x-advanced": true,
+ "x-visible-if": { auth_method: "access_keys" },
+ },
+ allow_host_access: {
+ type: "boolean",
+ title: "Allow host access",
+ description:
+ "Allow the Rill instance to access the S3 bucket from the host machine.",
+ default: false,
+ "x-step": "connector",
+ "x-advanced": true,
+ "x-visible-if": { auth_method: "access_keys" },
+ },
+ path: {
+ type: "string",
+ title: "S3 URI",
+ description: "Path to your S3 bucket or prefix",
+ pattern: "^s3://[^/]+(/.*)?$",
+ errorMessage: {
+ pattern: "Enter an S3 URI like s3://bucket or s3://bucket/path",
+ },
+ "x-placeholder": "s3://bucket/path",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["path", "name"],
+ allOf: [
+ {
+ if: { properties: { auth_method: { const: "access_keys" } } },
+ then: {
+ required: [
+ "aws_access_key_id",
+ "aws_secret_access_key",
+ "path",
+ "name",
+ ],
+ },
+ },
+ {
+ if: { properties: { auth_method: { const: "public" } } },
+ then: {
+ required: ["path", "name"],
+ },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/salesforce.ts b/web-common/src/features/templates/schemas/salesforce.ts
new file mode 100644
index 00000000000..429279a584c
--- /dev/null
+++ b/web-common/src/features/templates/schemas/salesforce.ts
@@ -0,0 +1,74 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const salesforceSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ username: {
+ type: "string",
+ title: "Username",
+ description: "Salesforce username",
+ "x-placeholder": "Enter your Salesforce username",
+ "x-step": "source",
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Salesforce password",
+ "x-placeholder": "Enter your Salesforce password",
+ "x-secret": true,
+ "x-step": "source",
+ },
+ key: {
+ type: "string",
+ title: "Security Token",
+ description: "Salesforce security token",
+ "x-placeholder": "Enter your security token",
+ "x-secret": true,
+ "x-step": "source",
+ },
+ endpoint: {
+ type: "string",
+ title: "Endpoint",
+ description: "Salesforce endpoint URL (optional, defaults to production)",
+ "x-placeholder": "https://login.salesforce.com",
+ "x-step": "source",
+ "x-advanced": true,
+ },
+ client_id: {
+ type: "string",
+ title: "Client ID",
+ description: "Connected App client ID (optional)",
+ "x-placeholder": "Enter client ID",
+ "x-step": "source",
+ "x-advanced": true,
+ },
+ soql: {
+ type: "string",
+ title: "SOQL Query",
+ description: "SOQL Query to extract data from Salesforce",
+ "x-placeholder": "SELECT Id, CreatedDate, Name FROM Opportunity",
+ "x-hint":
+ "Write a SOQL query to retrieve data from your Salesforce object.",
+ "x-step": "source",
+ },
+ sobject: {
+ type: "string",
+ title: "SObject",
+ description: "SObject to query in Salesforce",
+ "x-placeholder": "Opportunity",
+ "x-hint":
+ "Enter the name of the Salesforce object you want to query (e.g., Opportunity, Lead, Account).",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Source name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_salesforce_source",
+ "x-step": "source",
+ },
+ },
+ required: ["soql", "sobject", "name"],
+};
diff --git a/web-common/src/features/templates/schemas/snowflake.ts b/web-common/src/features/templates/schemas/snowflake.ts
new file mode 100644
index 00000000000..f064c97a58e
--- /dev/null
+++ b/web-common/src/features/templates/schemas/snowflake.ts
@@ -0,0 +1,205 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const snowflakeSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ connection_method: {
+ type: "string",
+ title: "Connection method",
+ enum: ["parameters", "connection_string"],
+ default: "parameters",
+ description: "Choose how to connect to Snowflake",
+ "x-display": "tabs",
+ "x-enum-labels": ["Enter parameters", "Enter connection string"],
+ "x-grouped-fields": {
+ parameters: ["auth_method"],
+ connection_string: ["dsn"],
+ },
+ "x-step": "connector",
+ },
+ auth_method: {
+ type: "string",
+ title: "Authentication method",
+ enum: ["password", "keypair"],
+ default: "password",
+ description: "Choose how to authenticate to Snowflake",
+ "x-display": "radio",
+ "x-enum-labels": ["Username & Password", "Key Pair"],
+ "x-enum-descriptions": [
+ "Authenticate with username and password.",
+ "Authenticate with RSA key pair (more secure).",
+ ],
+ "x-grouped-fields": {
+ password: [
+ "account",
+ "user",
+ "password",
+ "warehouse",
+ "database",
+ "schema",
+ "role",
+ ],
+ keypair: [
+ "account",
+ "user",
+ "private_key",
+ "private_key_passphrase",
+ "warehouse",
+ "database",
+ "schema",
+ "role",
+ ],
+ },
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ account: {
+ type: "string",
+ title: "Account",
+ description: "Snowflake account identifier (e.g., abc12345.us-east-1)",
+ "x-placeholder": "abc12345.us-east-1",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ user: {
+ type: "string",
+ title: "Username",
+ description: "Snowflake username",
+ "x-placeholder": "Enter username",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ password: {
+ type: "string",
+ title: "Password",
+ description: "Snowflake password",
+ "x-placeholder": "Enter password",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": {
+ connection_method: "parameters",
+ auth_method: "password",
+ },
+ },
+ private_key: {
+ type: "string",
+ title: "Private Key",
+ description: "RSA private key in PEM format",
+ "x-placeholder":
+ "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": {
+ connection_method: "parameters",
+ auth_method: "keypair",
+ },
+ },
+ warehouse: {
+ type: "string",
+ title: "Warehouse",
+ description: "Snowflake warehouse name",
+ "x-placeholder": "COMPUTE_WH",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ database: {
+ type: "string",
+ title: "Database",
+ description: "Snowflake database name",
+ "x-placeholder": "MY_DATABASE",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ schema: {
+ type: "string",
+ title: "Schema",
+ description: "Snowflake schema name",
+ "x-placeholder": "PUBLIC",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ role: {
+ type: "string",
+ title: "Role",
+ description: "Optional Snowflake role to assume",
+ "x-placeholder": "ANALYST",
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ authenticator: {
+ type: "string",
+ title: "Authenticator",
+ description: "Optional authenticator method (e.g., 'externalbrowser')",
+ "x-placeholder": "externalbrowser",
+ "x-step": "connector",
+ "x-advanced": true,
+ "x-visible-if": { connection_method: "parameters" },
+ },
+ dsn: {
+ type: "string",
+ title: "Connection String",
+ description: "Snowflake connection string (DSN)",
+ "x-placeholder": "user:password@account/database/schema?warehouse=wh",
+ "x-secret": true,
+ "x-step": "connector",
+ "x-visible-if": { connection_method: "connection_string" },
+ },
+ parallel_fetch_limit: {
+ type: "number",
+ title: "Parallel Fetch Limit",
+ description: "Maximum number of parallel fetches for query results",
+ "x-placeholder": "10",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ log_queries: {
+ type: "string",
+ title: "Log Queries",
+ description: "Enable logging of all SQL queries (useful for debugging)",
+ "x-placeholder": "false",
+ "x-step": "connector",
+ "x-advanced": true,
+ },
+ sql: {
+ type: "string",
+ title: "SQL Query",
+ description: "SQL query to extract data from Snowflake",
+ "x-placeholder": "SELECT * FROM my_table;",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Model Name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_model",
+ "x-step": "source",
+ },
+ },
+ required: ["sql", "name"],
+ allOf: [
+ {
+ if: {
+ properties: {
+ connection_method: { const: "parameters" },
+ auth_method: { const: "password" },
+ },
+ },
+ then: { required: ["account", "user", "password", "sql", "name"] },
+ },
+ {
+ if: {
+ properties: {
+ connection_method: { const: "parameters" },
+ auth_method: { const: "keypair" },
+ },
+ },
+ then: { required: ["account", "user", "private_key", "sql", "name"] },
+ },
+ {
+ if: { properties: { connection_method: { const: "connection_string" } } },
+ then: { required: ["dsn", "sql", "name"] },
+ },
+ ],
+};
diff --git a/web-common/src/features/templates/schemas/sqlite.ts b/web-common/src/features/templates/schemas/sqlite.ts
new file mode 100644
index 00000000000..6d4b63b349a
--- /dev/null
+++ b/web-common/src/features/templates/schemas/sqlite.ts
@@ -0,0 +1,31 @@
+import type { MultiStepFormSchema } from "./types";
+
+export const sqliteSchema: MultiStepFormSchema = {
+ $schema: "http://json-schema.org/draft-07/schema#",
+ type: "object",
+ properties: {
+ db: {
+ type: "string",
+ title: "Database Path",
+ description: "Path to SQLite database file",
+ "x-placeholder": "/path/to/database.db",
+ "x-step": "source",
+ },
+ table: {
+ type: "string",
+ title: "Table",
+ description: "SQLite table name",
+ "x-placeholder": "my_table",
+ "x-step": "source",
+ },
+ name: {
+ type: "string",
+ title: "Source name",
+ description: "Name for the source model",
+ pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$",
+ "x-placeholder": "my_sqlite_source",
+ "x-step": "source",
+ },
+ },
+ required: ["db", "table", "name"],
+};
diff --git a/web-common/src/features/templates/schemas/types.ts b/web-common/src/features/templates/schemas/types.ts
new file mode 100644
index 00000000000..6366cdad2fa
--- /dev/null
+++ b/web-common/src/features/templates/schemas/types.ts
@@ -0,0 +1,68 @@
+export type JSONSchemaVisibleIfValue =
+ | string
+ | number
+ | boolean
+ | Array;
+
+export type JSONSchemaField = {
+ type?: "string" | "number" | "boolean" | "object";
+ title?: string;
+ description?: string;
+ enum?: Array;
+ const?: string | number | boolean;
+ default?: string | number | boolean;
+ pattern?: string;
+ format?: string;
+ /**
+ * Custom error messages for validation failures.
+ * Use `pattern` key to provide a human-readable message when pattern validation fails.
+ */
+ errorMessage?: {
+ pattern?: string;
+ format?: string;
+ };
+ properties?: Record;
+ required?: string[];
+ "x-display"?: "radio" | "select" | "textarea" | "file" | "tabs";
+ "x-step"?: "connector" | "source" | "explorer";
+ "x-secret"?: boolean;
+ "x-readonly"?: boolean;
+ "x-visible-if"?: Record;
+ "x-enum-labels"?: string[];
+ "x-enum-descriptions"?: string[];
+ "x-placeholder"?: string;
+ "x-hint"?: string;
+ "x-accept"?: string;
+ "x-advanced"?: boolean;
+ /**
+ * Explicit grouping for radio/select options: maps an option value to the
+ * child field keys that should render beneath that option.
+ */
+ "x-grouped-fields"?: Record;
+};
+
+export type JSONSchemaCondition = {
+ properties?: Record;
+};
+
+export type JSONSchemaConstraint = {
+ required?: string[];
+};
+
+export type JSONSchemaConditional = {
+ if?: JSONSchemaCondition;
+ then?: JSONSchemaConstraint;
+ else?: JSONSchemaConstraint;
+};
+
+export type JSONSchemaObject = {
+ $schema?: string;
+ type: "object";
+ title?: string;
+ description?: string;
+ properties?: Record;
+ required?: string[];
+ allOf?: JSONSchemaConditional[];
+};
+
+export type MultiStepFormSchema = JSONSchemaObject;
diff --git a/web-local/tests/connectors/test-connection.spec.ts b/web-local/tests/connectors/test-connection.spec.ts
index e571c62d3bf..1ed49d8276c 100644
--- a/web-local/tests/connectors/test-connection.spec.ts
+++ b/web-local/tests/connectors/test-connection.spec.ts
@@ -4,6 +4,73 @@ import { test } from "../setup/base";
test.describe("Test Connection", () => {
test.use({ project: "Blank" });
+ test("Azure connector - auth method specific required fields", async ({
+ page,
+ }) => {
+ await page.getByRole("button", { name: "Add Asset" }).click();
+ await page.getByRole("menuitem", { name: "Add Data" }).click();
+ await page.locator("#azure").click();
+ await page.waitForSelector('form[id*="azure"]');
+
+ const button = page.getByRole("dialog").getByRole("button", {
+ name: /(Test and Connect|Continue)/,
+ });
+
+ // Select Storage Account Key (default may be different) -> requires account + key.
+ await page.getByRole("radio", { name: "Storage Account Key" }).click();
+ await expect(button).toBeDisabled();
+
+ await page.getByRole("textbox", { name: "Storage account" }).fill("acct");
+ await expect(button).toBeDisabled();
+
+ await page.getByRole("textbox", { name: "Access key" }).fill("key");
+ await expect(button).toBeEnabled();
+
+ // Switch to Public (no required fields) -> button should stay enabled.
+ await page.getByRole("radio", { name: "Public" }).click();
+ await expect(button).toBeEnabled();
+
+ // Switch to Connection String -> requires connection string, so disabled until filled.
+ await page.getByRole("radio", { name: "Connection String" }).click();
+ await expect(button).toBeDisabled();
+ await page
+ .getByRole("textbox", { name: "Connection string" })
+ .fill("DefaultEndpointsProtocol=https;");
+ await expect(button).toBeEnabled();
+ });
+
+ test("S3 connector - auth method specific required fields", async ({
+ page,
+ }) => {
+ await page.getByRole("button", { name: "Add Asset" }).click();
+ await page.getByRole("menuitem", { name: "Add Data" }).click();
+ await page.locator("#s3").click();
+ await page.waitForSelector('form[id*="s3"]');
+
+ const button = page.getByRole("dialog").getByRole("button", {
+ name: /(Test and Connect|Continue)/,
+ });
+
+ // Default method is Access keys -> requires access key id + secret.
+ await expect(button).toBeDisabled();
+ await page
+ .getByRole("textbox", { name: "Access Key ID" })
+ .fill("AKIA_TEST");
+ await expect(button).toBeDisabled();
+ await page
+ .getByRole("textbox", { name: "Secret Access Key" })
+ .fill("SECRET");
+ await expect(button).toBeEnabled();
+
+ // Switch to Public (no required fields) -> button should stay enabled.
+ await page.getByRole("radio", { name: "Public" }).click();
+ await expect(button).toBeEnabled();
+
+ // Switch back to Access keys -> fields cleared, so disabled until refilled.
+ await page.getByRole("radio", { name: "Access keys" }).click();
+ await expect(button).toBeDisabled();
+ });
+
test("GCS connector - HMAC", async ({ page }) => {
// Skip test if environment variables are not set
if (