Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
170b16e
API visual first pass
royendo Feb 3, 2026
b50943c
nit
royendo Feb 3, 2026
737d298
args
royendo Feb 3, 2026
05a5b87
simpler, prettier
royendo Feb 3, 2026
8614d13
web code qual
royendo Feb 3, 2026
74ad9bb
code qual
royendo Feb 3, 2026
cd82c30
Update VisualAPIEditor.svelte
royendo Feb 3, 2026
7cc6139
Update VisualAPIEditor.svelte
royendo Feb 3, 2026
a96efa5
fixed heights and JSON view
royendo Feb 3, 2026
08b2ef7
prettier
royendo Feb 3, 2026
2d1f1f5
code qual
royendo Feb 3, 2026
0a45bdc
Update APIWorkspace.svelte
royendo Feb 3, 2026
7d6e002
Merge branch 'main' into feat/visual-editor-api
royendo Feb 20, 2026
425ecfb
Update errors.ts
royendo Feb 20, 2026
83779f2
themeing css
royendo Feb 20, 2026
05d2383
remove viz editor, consolidate tests into text editor
royendo Mar 5, 2026
563a773
Merge branch 'main' into feat/visual-editor-api
royendo Mar 5, 2026
813ad67
rename
royendo Mar 5, 2026
c34565b
reset review on nav
royendo Mar 5, 2026
8272f9b
reset
royendo Mar 5, 2026
1dbc8f3
template overwrites
royendo Mar 5, 2026
b04e4b9
local PR reviews
royendo Mar 5, 2026
c63a253
Remove unused `mapParseErrorsToLines` import in `+page.svelte`
royendo Mar 11, 2026
a6f6529
Merge branch 'main' into feat/visual-editor-api
royendo Mar 11, 2026
c1ff4d6
importing runtime changes
royendo Mar 11, 2026
2c5ec94
Merge remote-tracking branch 'origin/main' into feat/visual-editor-api
royendo Mar 30, 2026
4b68698
migration svelte/vite
royendo Mar 30, 2026
08af507
prettier
royendo Mar 30, 2026
e670982
prettier
royendo Mar 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
template overwrites
  • Loading branch information
royendo committed Mar 5, 2026
commit 1dbc8f32b2c4e28396ef6120acce498586de8f34
27 changes: 24 additions & 3 deletions web-common/src/features/apis/editor/APIResponsePreview.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@
];

function handleViewModeChange(id: string) {
viewMode = id as ViewMode;
if (id === "table" || id === "json") {
viewMode = id;
}
}

$: columns = extractColumns(response);
Expand All @@ -33,12 +35,31 @@
return [{ name: "value", type: "VARCHAR" }];
}

return Object.keys(firstRow).map((key) => ({
const keys = Object.keys(firstRow);
const sampleSize = Math.min(data.length, 10);

return keys.map((key) => ({
name: key,
type: inferType(firstRow[key as keyof typeof firstRow]),
type: inferTypeFromSample(data, key, sampleSize),
}));
}

function inferTypeFromSample(
rows: unknown[],
key: string,
sampleSize: number,
): string {
for (let i = 0; i < sampleSize; i++) {
const row = rows[i];
if (typeof row !== "object" || row === null) continue;
const value = (row as Record<string, unknown>)[key];
if (value !== null && value !== undefined) {
return inferType(value);
}
}
return "VARCHAR";
}

function inferType(value: unknown): string {
if (value === null || value === undefined) return "VARCHAR";
if (typeof value === "number") {
Expand Down
65 changes: 57 additions & 8 deletions web-common/src/features/apis/editor/APITestPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,17 @@
$: isDisabled = hasErrors || isReconciling;

function buildFullUrl(base: string, params: Arg[]): string {
const url = new URL(base);
params.forEach((arg) => {
if (arg.key.trim()) {
url.searchParams.set(arg.key, arg.value);
}
});
return url.toString();
try {
const url = new URL(base);
params.forEach((arg) => {
if (arg.key.trim()) {
url.searchParams.set(arg.key, arg.value);
}
});
return url.toString();
} catch {
return base;
}
}

function addArg() {
Expand All @@ -55,8 +59,49 @@
copyToClipboard(fullUrl, "Copied endpoint URL to clipboard");
}

function handleArgsKeydown(e: KeyboardEvent) {
if (e.key !== "Tab") return;

const container = (e.target as HTMLElement)?.closest(".args-container");
if (!container) return;

const inputs = Array.from(
container.querySelectorAll<HTMLInputElement>("input"),
);
const currentIndex = inputs.indexOf(e.target as HTMLInputElement);
if (currentIndex === -1) return;

if (e.shiftKey) {
// Shift+Tab: move to previous input, or let dropdown handle if at first
if (currentIndex > 0) {
e.preventDefault();
inputs[currentIndex - 1].focus();
}
} else {
// Tab: move to next input, or add a new arg row if at the last one
if (currentIndex < inputs.length - 1) {
e.preventDefault();
inputs[currentIndex + 1].focus();
} else {
e.preventDefault();
addArg();
// Focus the new row's key input after Svelte updates the DOM
requestAnimationFrame(() => {
const updatedInputs = Array.from(
container.querySelectorAll<HTMLInputElement>("input"),
);
updatedInputs[updatedInputs.length - 2]?.focus();
});
}
}
}

function handleKeydown(e: KeyboardEvent) {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && !isDisabled) {
// Don't fire when focus is in a dialog, modal, or navigation sidebar
const target = e.target as HTMLElement;
if (target?.closest?.('[role="dialog"], nav, [role="alertdialog"]'))
return;
e.preventDefault();
testAPI();
}
Expand Down Expand Up @@ -127,7 +172,11 @@
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end" class="w-72 p-2">
<div class="flex flex-col gap-y-2">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="args-container flex flex-col gap-y-2"
on:keydown={handleArgsKeydown}
>
{#if args.length === 0}
<p class="text-xs text-fg-muted px-1 py-2">
No arguments. Click "Add" below.
Expand Down
98 changes: 98 additions & 0 deletions web-common/src/features/apis/editor/template-utils.ts
Original file line number Diff line number Diff line change
@@ -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/<filename>

`;

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
`,
},
];
Loading