web/libs/app-common/src/blocks/StorageProviderForm/README.md
This system allows you to easily add new storage providers by defining their configuration in a declarative way.
Create a new file in providers/ directory (e.g., providers/myProvider.ts):
import { z } from "zod";
import { ProviderConfig } from "../types/provider";
export const myProvider: ProviderConfig = {
name: "myprovider",
title: "My Storage Provider",
description: "Configure your My Storage Provider connection",
fields: [
{
name: "api_key",
type: "password",
label: "API Key",
required: true,
placeholder: "Enter your API key",
schema: z.string().min(1, "API Key is required"),
},
{
name: "endpoint",
type: "text",
label: "API Endpoint",
required: true,
placeholder: "https://api.mystorage.com",
schema: z.string().url("Must be a valid URL"),
},
{
name: "use_ssl",
type: "toggle",
label: "Use SSL",
description: "Enable SSL for secure connections",
schema: z.boolean().default(true), // Default value defined in schema
},
{
name: "timeout",
type: "counter",
label: "Connection Timeout (seconds)",
min: 1,
max: 300,
step: 5,
schema: z.number().min(1).max(300).default(30), // Default value defined in schema
},
],
layout: [
{
fields: ["api_key"],
},
{
fields: ["endpoint"],
},
{
fields: ["use_ssl", "timeout"],
},
],
};
Add your provider to the registry in providers/index.ts:
import { myProvider } from "./myProvider";
export const providerRegistry: Record<string, ProviderConfig> = {
s3: s3Provider,
gcp: gcpProvider,
azure: azureProvider,
redis: redisProvider,
localfiles: localFilesProvider,
myprovider: myProvider, // Add your provider here
};
text: Regular text inputpassword: Password input (hidden)number: Numeric inputselect: Dropdown selectiontoggle: Boolean toggle switchcounter: Numeric counter with min/maxtextarea: Multi-line text inputhidden: Hidden input field (no visual styling or layout){
name: string; // Field name (used in form data)
type: FieldType; // Field type (see above)
label: string; // Display label
description?: string; // Help text
placeholder?: string; // Placeholder text
required?: boolean; // Whether field is required
schema: z.ZodTypeAny; // Zod validation schema with defaults
options?: Array<{ value: string; label: string }>; // For select fields
min?: number; // For number/counter fields
max?: number; // For number/counter fields
step?: number; // For number/counter fields
autoComplete?: string; // For input fields
gridCols?: number; // How many columns this field should span (1-12)
readOnly?: boolean; // Whether field is read-only (not editable by user)
dependsOn?: { // Field dependency configuration
field: string; // Name of the field this depends on
value: any | ((dependencyValue: any, formData: Record<string, any>) => boolean); // The value or function to check if field should be enabled
};
}
Default values can be defined in two ways:
Default values are defined directly in the Zod schema using .default():
{
name: "use_ssl",
type: "toggle",
label: "Use SSL",
schema: z.boolean().default(true), // Default: true
},
{
name: "timeout",
type: "counter",
label: "Connection Timeout",
schema: z.number().min(1).max(300).default(30), // Default: 30
},
{
name: "region",
type: "select",
label: "Region",
schema: z.string().default("us-east-1"), // Default: "us-east-1"
},
The system automatically extracts these default values from the schemas using the extractDefaultValues() function.
You can also provide custom default values when using the StorageProviderForm component:
const customDefaults = {
s3: {
region: "us-west-2",
bucket: "my-default-bucket",
},
gcp: {
project_id: "my-default-project",
bucket: "my-default-bucket",
},
};
<StorageProviderForm
// ... other props
defaultValues={customDefaults}
/>
Custom defaults take precedence over schema defaults. The structure is:
{
[providerName: string]: {
[fieldName: string]: any
}
}
You can mark fields as read-only by setting the readOnly property to true. Read-only fields will be displayed but cannot be edited by users:
{
name: "api_endpoint",
type: "text",
label: "API Endpoint",
description: "The endpoint URL for the API",
schema: z.string().url("Must be a valid URL"),
readOnly: true, // This field cannot be edited
},
{
name: "region",
type: "select",
label: "Region",
schema: z.string().default("us-east-1"),
options: [
{ value: "us-east-1", label: "US East (N. Virginia)" },
{ value: "us-west-2", label: "US West (Oregon)" },
],
readOnly: true, // This field cannot be edited
},
Read-only fields are useful for:
Hidden fields are rendered as <input type="hidden"> elements with no visual styling or layout. They're useful for storing values that shouldn't be visible to users but need to be included in form submissions:
{
name: "api_version",
type: "hidden",
schema: z.string().default("v1"),
},
{
name: "client_id",
type: "hidden",
schema: z.string().default("my-client"),
},
Hidden fields:
You can create field dependencies using the dependsOn property. This allows you to disable fields based on the state of other fields. The value property can be either a static value or a dynamic function.
{
name: "presigned_urls",
type: "toggle",
label: "Use Presigned URLs",
description: "Generate presigned URLs for file access",
schema: z.boolean().default(false),
},
{
name: "ttl",
type: "counter",
label: "TTL (seconds)",
description: "Time to live for presigned URLs",
schema: z.number().min(1).max(3600).default(3600),
dependsOn: {
field: "presigned_urls",
value: true, // TTL field is only enabled when presigned_urls is true
},
},
For more complex logic, you can use a function that receives the dependency value and the entire form data:
{
name: "encryption_type",
type: "select",
label: "Encryption Type",
schema: z.string().default("none"),
options: [
{ value: "none", label: "No Encryption" },
{ value: "aes", label: "AES-256" },
{ value: "custom", label: "Custom Encryption" },
],
},
{
name: "encryption_key",
type: "password",
label: "Encryption Key",
description: "Key for encryption",
schema: z.string().min(1, "Encryption key is required"),
dependsOn: {
field: "encryption_type",
value: (encryptionType, formData) => {
// Enable only when encryption type is not "none"
return encryptionType !== "none";
},
},
},
{
name: "custom_algorithm",
type: "text",
label: "Custom Algorithm",
description: "Custom encryption algorithm",
schema: z.string().min(1, "Algorithm is required"),
dependsOn: {
field: "encryption_type",
value: (encryptionType, formData) => {
// Enable only when encryption type is "custom"
return encryptionType === "custom";
},
},
},
For toggle fields, you can check the checked state:
{
name: "use_ssl",
type: "toggle",
label: "Use SSL",
description: "Enable SSL for secure connections",
schema: z.boolean().default(true),
},
{
name: "ssl_certificate",
type: "textarea",
label: "SSL Certificate",
description: "Paste your SSL certificate",
schema: z.string().min(1, "SSL certificate is required"),
dependsOn: {
field: "use_ssl",
value: (useSsl, formData) => {
// Enable only when SSL is enabled
return useSsl === true;
},
},
},
In these examples:
Field dependencies are useful for:
The layout array defines how fields are arranged in rows:
layout: [
{
fields: ["field1"], // Single field on one row
},
{
fields: ["field2", "field3"], // Two fields on the same row
},
{
fields: ["field4"], // Another single field
},
]
Each field includes a Zod schema for validation:
{
name: "api_key",
type: "password",
label: "API Key",
required: true,
schema: z.string().min(1, "API Key is required"),
}
The system automatically assembles all field schemas into a complete validation schema for the entire form.
The system provides several helper functions:
getProviderConfig(providerName): Get provider configurationgetProviderSchema(providerName): Get validation schema for providergetProviderDefaultValues(providerName): Get default values for providerextractDefaultValues(fields): Extract defaults from field schemasSee providers/example.ts for a complete example of a new provider configuration.
Here's a complete example of how to use the StorageProviderForm with custom default values:
import { StorageProviderForm } from '@humansignal/app-common';
// Define custom default values for different providers
const customDefaults = {
s3: {
region: "us-west-2",
bucket: "my-default-bucket",
prefix: "data/",
},
gcp: {
project_id: "my-default-project",
bucket: "my-default-bucket",
prefix: "annotations/",
},
azure: {
container: "my-default-container",
account_name: "myaccount",
prefix: "exports/",
},
};
// Use the form with custom defaults
function MyStorageForm() {
const handleSubmit = () => {
// Handle form submission
};
return (
<StorageProviderForm
onSubmit={handleSubmit}
target="import"
project={123}
storageTypes={[
{ title: "Amazon S3", name: "s3" },
{ title: "Google Cloud Storage", name: "gcp" },
{ title: "Azure Blob Storage", name: "azure" },
]}
providers={providerRegistry}
defaultValues={customDefaults}
title="Configure Storage Provider"
/>
);
}
In this example:
region: "us-west-2", bucket: "my-default-bucket", and prefix: "data/"project_id: "my-default-project", bucket: "my-default-bucket", and prefix: "annotations/"Here's an example of how to use read-only fields in a provider configuration:
export const myProvider: ProviderConfig = {
name: "myprovider",
title: "My Storage Provider",
description: "Configure your My Storage Provider connection",
fields: [
{
name: "api_endpoint",
type: "text",
label: "API Endpoint",
description: "The endpoint URL for the API",
schema: z.string().url("Must be a valid URL"),
readOnly: true, // This field cannot be edited
},
{
name: "api_key",
type: "password",
label: "API Key",
required: true,
placeholder: "Enter your API key",
schema: z.string().min(1, "API Key is required"),
},
{
name: "region",
type: "select",
label: "Region",
schema: z.string().default("us-east-1"),
options: [
{ value: "us-east-1", label: "US East (N. Virginia)" },
{ value: "us-west-2", label: "US West (Oregon)" },
],
readOnly: true, // This field cannot be edited
},
{
name: "use_ssl",
type: "toggle",
label: "Use SSL",
description: "Enable SSL for secure connections",
schema: z.boolean().default(true),
},
],
layout: [
{
fields: ["api_endpoint"],
},
{
fields: ["api_key"],
},
{
fields: ["region", "use_ssl"],
},
],
};
In this example:
api_endpoint field is read-only and will be pre-filled but cannot be changed by usersregion field is also read-only and will show the default value but cannot be modifiedapi_key and use_ssl fields are editable as normalHere's an example that combines both read-only fields and field dependencies:
export const advancedProvider: ProviderConfig = {
name: "advancedprovider",
title: "Advanced Storage Provider",
description: "Configure your advanced storage provider with conditional features",
fields: [
{
name: "api_endpoint",
type: "text",
label: "API Endpoint",
description: "The endpoint URL for the API",
schema: z.string().url("Must be a valid URL"),
readOnly: true, // This field cannot be edited
},
{
name: "api_key",
type: "password",
label: "API Key",
required: true,
placeholder: "Enter your API key",
schema: z.string().min(1, "API Key is required"),
},
{
name: "use_presigned_urls",
type: "toggle",
label: "Use Presigned URLs",
description: "Generate presigned URLs for secure file access",
schema: z.boolean().default(false),
},
{
name: "ttl_seconds",
type: "counter",
label: "TTL (seconds)",
description: "Time to live for presigned URLs",
schema: z.number().min(1).max(3600).default(3600),
dependsOn: {
field: "use_presigned_urls",
value: (usePresignedUrls, formData) => {
// Only enabled when presigned URLs are enabled
return usePresignedUrls === true;
},
},
},
{
name: "encryption_enabled",
type: "toggle",
label: "Enable Encryption",
description: "Enable client-side encryption",
schema: z.boolean().default(false),
},
{
name: "encryption_key",
type: "password",
label: "Encryption Key",
description: "Key for client-side encryption",
schema: z.string().min(1, "Encryption key is required"),
dependsOn: {
field: "encryption_enabled",
value: (encryptionEnabled, formData) => {
// Only enabled when encryption is enabled
return encryptionEnabled === true;
},
},
},
],
layout: [
{
fields: ["api_endpoint"],
},
{
fields: ["api_key"],
},
{
fields: ["use_presigned_urls", "ttl_seconds"],
},
{
fields: ["encryption_enabled", "encryption_key"],
},
],
};
In this example:
api_endpoint is read-only and cannot be changedttl_seconds is only enabled when use_presigned_urls is trueencryption_key is only enabled when encryption_enabled is trueThe old system used hardcoded React components for each provider. The new system:
ProviderForm componentTo migrate an existing provider: