From 1ba921bcd3b539f476ad0f6992499b06bcd7662b Mon Sep 17 00:00:00 2001 From: Michelle Perkins Date: Sat, 6 Jun 2026 20:18:09 -0400 Subject: [PATCH] Add Tailcall config generator page --- src/pages/app/config.tsx | 319 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 src/pages/app/config.tsx diff --git a/src/pages/app/config.tsx b/src/pages/app/config.tsx new file mode 100644 index 0000000000..99a7ef7a12 --- /dev/null +++ b/src/pages/app/config.tsx @@ -0,0 +1,319 @@ +import React, {useEffect, useMemo, useState} from "react" +import Layout from "@theme/Layout" + +const SCHEMA_URL = "https://raw.githubusercontent.com/tailcallhq/tailcall/main/generated/.tailcallrc.schema.json" + +type JsonSchema = { + title?: string + description?: string + type?: string | string[] + properties?: Record + definitions?: Record + required?: string[] + default?: unknown + enum?: unknown[] + items?: JsonSchema + allOf?: JsonSchema[] + anyOf?: JsonSchema[] + oneOf?: JsonSchema[] + $ref?: string +} + +type ConfigValue = string | number | boolean | null | ConfigObject | ConfigValue[] + +interface ConfigObject { + [key: string]: ConfigValue +} + +const isObject = (value: unknown): value is ConfigObject => + typeof value === "object" && value !== null && !Array.isArray(value) + +const getSchemaType = (schema: JsonSchema): string => { + if (Array.isArray(schema.type)) { + return schema.type.find((type) => type !== "null") || "string" + } + + return schema.type || "object" +} + +const resolveSchema = (schema: JsonSchema, root: JsonSchema): JsonSchema => { + if (schema.$ref?.startsWith("#/definitions/")) { + const definitionName = schema.$ref.replace("#/definitions/", "") + return root.definitions?.[definitionName] || schema + } + + const composedSchema = schema.allOf?.[0] || schema.anyOf?.find((item) => item.type !== "null") || schema.oneOf?.[0] + if (composedSchema) { + return {...schema, ...resolveSchema(composedSchema, root)} + } + + return schema +} + +const coerceValue = (rawValue: string, schema: JsonSchema): ConfigValue | undefined => { + if (rawValue.trim() === "") return undefined + + const schemaType = getSchemaType(schema) + + if (schemaType === "integer" || schemaType === "number") { + const parsed = Number(rawValue) + return Number.isFinite(parsed) ? parsed : undefined + } + + if (schemaType === "boolean") { + return rawValue === "true" + } + + if (schemaType === "array") { + return rawValue + .split(",") + .map((value) => value.trim()) + .filter(Boolean) + } + + return rawValue +} + +const pruneEmpty = (value: ConfigValue | undefined): ConfigValue | undefined => { + if (Array.isArray(value)) { + const nextValue = value.map(pruneEmpty).filter((item): item is ConfigValue => item !== undefined) + return nextValue.length > 0 ? nextValue : undefined + } + + if (isObject(value)) { + const nextValue = Object.entries(value).reduce((result, [key, entry]) => { + const pruned = pruneEmpty(entry) + if (pruned !== undefined) result[key] = pruned + return result + }, {}) + + return Object.keys(nextValue).length > 0 ? nextValue : undefined + } + + return value +} + +const setPathValue = (source: ConfigObject, path: string[], value: ConfigValue | undefined): ConfigObject => { + const [head, ...rest] = path + if (!head) return source + + if (rest.length === 0) { + const next = {...source} + if (value === undefined) { + delete next[head] + } else { + next[head] = value + } + return next + } + + const child = isObject(source[head]) ? (source[head] as ConfigObject) : {} + const nextChild = setPathValue(child, rest, value) + const next = {...source} + + if (Object.keys(nextChild).length === 0) { + delete next[head] + } else { + next[head] = nextChild + } + + return next +} + +const getPathValue = (source: ConfigObject, path: string[]): ConfigValue | undefined => { + return path.reduce((value, key) => { + if (!isObject(value)) return undefined + return value[key] + }, source) +} + +const FieldControl = ({ + config, + rootSchema, + schema, + path, + onChange, +}: { + config: ConfigObject + rootSchema: JsonSchema + schema: JsonSchema + path: string[] + onChange: (nextConfig: ConfigObject) => void +}) => { + const resolvedSchema = resolveSchema(schema, rootSchema) + const schemaType = getSchemaType(resolvedSchema) + const id = path.join(".") + const value = getPathValue(config, path) + const stringValue = Array.isArray(value) + ? value.join(", ") + : value === undefined || isObject(value) + ? "" + : String(value) + + if (schemaType === "object" && resolvedSchema.properties) { + return ( +
+ {path[path.length - 1]} +
+ {Object.entries(resolvedSchema.properties).map(([propertyName, propertySchema]) => ( + + ))} +
+
+ ) + } + + const handleValueChange = (rawValue: string) => { + onChange(setPathValue(config, path, coerceValue(rawValue, resolvedSchema))) + } + + return ( + + ) +} + +const ConfigGeneratorPage = () => { + const [schema, setSchema] = useState(null) + const [config, setConfig] = useState({}) + const [activeSection, setActiveSection] = useState("") + const [status, setStatus] = useState("Loading Tailcall schema...") + + useEffect(() => { + const loadSchema = async () => { + try { + const response = await fetch(SCHEMA_URL) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + const nextSchema = (await response.json()) as JsonSchema + setSchema(nextSchema) + setActiveSection(Object.keys(nextSchema.properties || {})[0] || "") + setStatus("Schema loaded from Tailcall main branch.") + } catch (error) { + setStatus(`Unable to load schema: ${error instanceof Error ? error.message : "Unknown error"}`) + } + } + + loadSchema() + }, []) + + const sectionNames = useMemo(() => Object.keys(schema?.properties || {}), [schema]) + const generatedConfig = pruneEmpty(config) || {} + const generatedJson = JSON.stringify(generatedConfig, null, 2) + + const downloadConfig = () => { + const blob = new Blob([generatedJson], {type: "application/json"}) + const url = URL.createObjectURL(blob) + const link = document.createElement("a") + link.href = url + link.download = ".tailcallrc.json" + link.click() + URL.revokeObjectURL(url) + } + + return ( + +
+
+

+ Tailcall configuration +

+

Config generator

+

+ Build a runtime config from the latest Tailcall JSON schema, preview the generated file, and download a + ready-to-edit `.tailcallrc.json`. +

+

{status}

+
+ + {schema && ( +
+
+ + + {activeSection && schema.properties?.[activeSection] && ( + + )} +
+ + +
+ )} +
+
+ ) +} + +export default ConfigGeneratorPage