diff --git a/runtime/parser/schema/project.schema.yaml b/runtime/parser/schema/project.schema.yaml index 81c3387a7d7d..8c8b91777cc8 100644 --- a/runtime/parser/schema/project.schema.yaml +++ b/runtime/parser/schema/project.schema.yaml @@ -3417,11 +3417,11 @@ definitions: - path required: - glob - examples: + examples: - type: api + glob: data/*.csv + args: - glob: "data/*.csv" - - title: Resource Status Check type: object description: Uses the status of a resource as data. diff --git a/web-common/src/features/apis/editor/APIEditor.svelte b/web-common/src/features/apis/editor/APIEditor.svelte new file mode 100644 index 000000000000..b203b3226989 --- /dev/null +++ b/web-common/src/features/apis/editor/APIEditor.svelte @@ -0,0 +1,59 @@ + + +
+
+ + { + if (!content?.length) { + setLineStatuses([], editor); + } + }} + {fileArtifact} + extensions={[customYAMLwithJSONandSQL]} + /> + +
+ + +
+ + diff --git a/web-common/src/features/apis/editor/APIResponsePreview.svelte b/web-common/src/features/apis/editor/APIResponsePreview.svelte new file mode 100644 index 000000000000..0ad3fdc52eb6 --- /dev/null +++ b/web-common/src/features/apis/editor/APIResponsePreview.svelte @@ -0,0 +1,150 @@ + + +
+
+ Response Preview + {#if response && response.length > 0} +
+ {response.length} {response.length === 1 ? "row" : "rows"} + +
+ {/if} +
+
+ {#if isLoading} + + {:else if error} + + {:else if !response} +
Click "Test API" to see the response
+ {:else if response.length === 0} +
API returned an empty response
+ {:else if viewMode === "json"} +
{JSON.stringify(response, null, 2)}
+ {:else} + + {/if} +
+
+ + diff --git a/web-common/src/features/apis/editor/APITestPanel.svelte b/web-common/src/features/apis/editor/APITestPanel.svelte new file mode 100644 index 000000000000..56750d9f5dbd --- /dev/null +++ b/web-common/src/features/apis/editor/APITestPanel.svelte @@ -0,0 +1,260 @@ + + +
+ + +
+ URL Preview: + + {fullUrl} +
+ + + Copy URL + + + +
+
+ + {#if argsOpen} +
+
+ Query Parameters + +
+ +
+ {#if args.length === 0} +

+ No query parameters. Click "Add" to create one. +

+ {:else} +
+ Key + Value + + {#each args as arg (arg.id)} + + + + {/each} +
+ {/if} +
+
+ {/if} + +
+ +
+
+ + diff --git a/web-common/src/features/apis/editor/template-utils.ts b/web-common/src/features/apis/editor/template-utils.ts new file mode 100644 index 000000000000..7c5336d5fc17 --- /dev/null +++ b/web-common/src/features/apis/editor/template-utils.ts @@ -0,0 +1,98 @@ +export interface Template { + label: string; + description: string; + content: string; +} + +const header = `# API YAML +# Reference documentation: https://docs.rilldata.com/reference/project-files/apis +# Test your API endpoint at http://localhost:9009/v1/instances/default/api/ + +`; + +export const templates: Template[] = [ + { + label: "SQL Query", + description: + "Query a model or source using SQL. Use {{ .args.param }} to accept dynamic arguments.", + content: `${header}type: api +sql: | + SELECT * FROM model_name +`, + }, + { + label: "SQL Query with Limits", + description: + "Query a model with pagination controls. Pass 'limit' and 'offset' arguments to control page size and position.", + content: `${header}type: api +sql: | + SELECT * FROM model_name + LIMIT {{ .args.limit }} + OFFSET {{ .args.offset }} +`, + }, + { + label: "Metrics SQL", + description: + "Query a metrics view using Metrics SQL. Reference measures and dimensions defined in your metrics view.", + content: `${header}type: api +metrics_sql: | + SELECT measure, dimension FROM metrics_view_name +`, + }, + { + label: "Metrics SQL with Args", + description: + "Query a metrics view with dynamic filtering. Pass arguments to filter results at query time.", + content: `${header}type: api +metrics_sql: | + SELECT measure, dimension FROM metrics_view_name + WHERE dimension = '{{ .args.filter }}' +`, + }, + { + label: "Metrics SQL with Pagination", + description: + "Query a metrics view with pagination controls. Pass 'limit' and 'offset' arguments to page through results.", + content: `${header}type: api +metrics_sql: | + SELECT measure, dimension FROM metrics_view_name + LIMIT {{ .args.limit }} + OFFSET {{ .args.offset }} +`, + }, + { + label: "OpenAPI", + description: + "Define an API with an OpenAPI specification. Includes request and response schemas for documentation and validation.", + content: `${header}type: api +metrics_sql: | + SELECT measure, dimension FROM metrics_view_name + WHERE dimension = '{{ .args.filter }}' +openapi: + summary: Describe your API endpoint + request_schema: + type: object + properties: + filter: + type: string + description: Filter by dimension value + response_schema: + type: object + properties: + measure: + type: number + dimension: + type: string +`, + }, + { + label: "Resource Status", + description: + "Return the reconciliation status of resources in the project. Useful for health-check and monitoring endpoints.", + content: `${header}type: api +resource_status: + where_error: true +`, + }, +]; diff --git a/web-common/src/features/apis/editor/types.ts b/web-common/src/features/apis/editor/types.ts new file mode 100644 index 000000000000..2f828651d3fb --- /dev/null +++ b/web-common/src/features/apis/editor/types.ts @@ -0,0 +1,5 @@ +export interface Arg { + id: string; + key: string; + value: string; +} diff --git a/web-common/src/features/metrics-views/errors.ts b/web-common/src/features/metrics-views/errors.ts index 5beac0621443..ad45ac8424d9 100644 --- a/web-common/src/features/metrics-views/errors.ts +++ b/web-common/src/features/metrics-views/errors.ts @@ -79,3 +79,12 @@ export function mapParseErrorToLine( } return runtimeErrorToLine(error.message, yaml); } + +export function mapParseErrorsToLines( + errors: V1ParseError[], + yaml: string, +): LineStatus[] { + return errors + .map((e) => mapParseErrorToLine(e, yaml)) + .filter((s): s is LineStatus => s !== undefined); +} diff --git a/web-common/src/features/workspaces/APIWorkspace.svelte b/web-common/src/features/workspaces/APIWorkspace.svelte new file mode 100644 index 000000000000..198ac50520c2 --- /dev/null +++ b/web-common/src/features/workspaces/APIWorkspace.svelte @@ -0,0 +1,168 @@ + + + + + + + + {#snippet child({ props })} + + + + Insert a starter API template + + + {/snippet} + + + {#each templates as template} + selectTemplate(template)}> + {template.label} + + {/each} + + + + + + + + + + + + + + {pendingTemplate?.label ?? "Template"} + + {pendingTemplate?.description ?? ""} +

+ Warning: this will overwrite your current file. +
+
+ + + + +
+
diff --git a/web-common/src/layout/workspace/WorkspaceEditorContainer.svelte b/web-common/src/layout/workspace/WorkspaceEditorContainer.svelte index b15d8869b615..0cbad168dd5a 100644 --- a/web-common/src/layout/workspace/WorkspaceEditorContainer.svelte +++ b/web-common/src/layout/workspace/WorkspaceEditorContainer.svelte @@ -21,7 +21,7 @@
{error} diff --git a/web-local/src/routes/(application)/(workspace)/files/[...file]/+page.svelte b/web-local/src/routes/(application)/(workspace)/files/[...file]/+page.svelte index e3a5f886a007..e61033c936d6 100644 --- a/web-local/src/routes/(application)/(workspace)/files/[...file]/+page.svelte +++ b/web-local/src/routes/(application)/(workspace)/files/[...file]/+page.svelte @@ -10,6 +10,7 @@ import { getExtensionsForFile } from "@rilldata/web-common/features/editor/getExtensionsForFile"; import { ResourceKind } from "@rilldata/web-common/features/entity-management/resource-selectors"; import { directoryState } from "@rilldata/web-common/features/file-explorer/directory-store"; + import APIWorkspace from "@rilldata/web-common/features/workspaces/APIWorkspace.svelte"; import CanvasWorkspace from "@rilldata/web-common/features/workspaces/CanvasWorkspace.svelte"; import ExploreWorkspace from "@rilldata/web-common/features/workspaces/ExploreWorkspace.svelte"; import MetricsWorkspace from "@rilldata/web-common/features/workspaces/MetricsWorkspace.svelte"; @@ -26,6 +27,7 @@ [ResourceKind.MetricsView, MetricsWorkspace], [ResourceKind.Explore, ExploreWorkspace], [ResourceKind.Canvas, CanvasWorkspace], + [ResourceKind.API, APIWorkspace], [null, null], [undefined, null], ]);