Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 11 additions & 7 deletions docs/design/json-editor-standard.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# JSON Editor Standard

The dashboard has three JSON editing contexts:
The dashboard has four named JSON editing contexts:

- Create forms: edit the payload that the visual form will submit.
- Resource RAW editors: PATCH an existing resource and verify the saved state.
- API Console: build direct Admin API requests and inspect responses.
- Payload JSON: edit the create payload that the Visual Editor will submit.
- Admin API JSON: PATCH an existing resource and verify the saved state.
- Plugin JSON: edit one plugin config inside the plugin Fields drawer.
- Request JSON and Response JSON: build direct Admin API requests and inspect
responses in the API Console.

These contexts have different actions, but they share one editing standard.

Expand All @@ -20,12 +22,14 @@ These contexts have different actions, but they share one editing standard.

## Contextual Actions

- Create forms submit the complete create payload through the form workflow.
- Resource RAW editors show identity fields separately as values managed by the
- Payload JSON submits the complete create payload through the form workflow.
- Admin API JSON editors show identity fields separately as values managed by the
Admin API path. The editable JSON excludes read-only fields, sends changed
editable fields with PATCH, and verifies the saved resource with a follow-up
read.
- API Console sends the selected method and payload exactly as configured.
- Plugin JSON edits only the selected plugin config object, not the full APISIX
resource payload.
- Request JSON sends the selected method and payload exactly as configured.
- Read-only response editors use the same JSON presentation without editing
controls.

Expand Down
11 changes: 6 additions & 5 deletions docs/en/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ From this screen you can:
* See the upstream or service target.
* Review applied plugins without opening every route.
* Toggle route status.
* Open the raw payload for quick inspection.
* Open the Admin API payload for quick inspection.

Routes are still APISIX resources. The UI does not create dashboard-only fields;
it presents the Admin API payload with friendlier controls.
Expand All @@ -101,11 +101,12 @@ The detail page has three working modes:

* **Overview**: scan the resource without editing it.
* **Configuration**: update the resource through structured form controls.
* **Raw JSON**: edit the Admin API payload directly with schema guidance.
* **Admin API JSON**: patch the saved APISIX resource directly with schema guidance.

Use the structured form for routine edits and the Raw JSON editor when you need
to paste a known APISIX payload, inspect the exact object, or work with fields
that are easier to reason about as JSON.
Use the structured form for routine edits and the Admin API JSON editor when you
need to inspect or patch the saved APISIX object directly. When creating a new
resource, the **Payload JSON** tab edits the same draft payload that the visual
editor will validate and submit.

## Understand Traffic Relationships

Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/admin-ux-helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
getResourceIdentityPaths,
} from '@/utils/resourceJsonSchema';

test('uses one resource schema standard across raw JSON surfaces', () => {
test('uses one resource schema standard across Admin API JSON surfaces', () => {
expect(getResourceIdentityPaths('/consumers/alice')).toEqual(['username']);
expect(getResourceIdentityPaths('/secrets/vault/demo')).toEqual(['manager', 'id']);

Expand All @@ -44,7 +44,7 @@ test('uses one resource schema standard across raw JSON surfaces', () => {
.toEqual([['uri'], ['uris']]);
});

test('keeps resource identity outside editable raw JSON', () => {
test('keeps resource identity outside editable Admin API JSON', () => {
const resource = {
id: 'route-1',
create_time: 1,
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/api-console.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ test('keeps blocked request body errors visible with recovery actions', async ({
});
await expect(requestBodyError).toBeVisible();
await expect(
requestBodyError.getByRole('button', { name: 'Format JSON' })
requestBodyError.getByRole('button', { name: 'Format Request JSON' })
).toBeVisible();
await expect(
requestBodyError.getByRole('button', { name: 'Reset to template' })
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/consumer_groups.crud-all-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ test('should CRUD Consumer Group with all fields', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: 'Add Plugin: basic-auth',
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
await uiFillMonacoEditor(
page,
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/consumer_groups.crud-required-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ test('should CRUD Consumer Group with required fields', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: 'Add Plugin: basic-auth',
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
await uiFillMonacoEditor(page, pluginEditor, '{"hide_credentials": true}');

Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/consumers.credentials.list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ test('should create credential from the UI', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: 'Add Plugin: key-auth',
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
await uiFillMonacoEditor(
page,
Expand Down
6 changes: 3 additions & 3 deletions e2e/tests/global_rules.crud-all-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test('should CRUD global rule with multiple plugins', async ({ page }) => {
name: 'Add Plugin: response-rewrite',
});
await expect(pluginDialog).toBeVisible();
await pluginDialog.getByRole('tab', { name: 'JSON' }).click();
await pluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

// Configure response-rewrite with custom configuration using Monaco editor
const pluginEditor = await uiGetMonacoEditor(page, pluginDialog);
Expand Down Expand Up @@ -112,7 +112,7 @@ test('should CRUD global rule with multiple plugins', async ({ page }) => {
name: 'Add Plugin: cors',
});
await expect(corsPluginDialog).toBeVisible();
await corsPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await corsPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

// Submit with simple configuration for cors
const corsEditor = await uiGetMonacoEditor(page, corsPluginDialog);
Expand Down Expand Up @@ -149,7 +149,7 @@ test('should CRUD global rule with multiple plugins', async ({ page }) => {
const editPluginDialog = page.getByRole('dialog', {
name: 'Edit Plugin: response-rewrite',
});
await editPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await editPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog);
await uiFillMonacoEditor(
page,
Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/global_rules.crud-required-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test('should CRUD global rule with required fields only', async ({ page }) => {
name: 'Add Plugin: response-rewrite',
});
await expect(pluginDialog).toBeVisible();
await pluginDialog.getByRole('tab', { name: 'JSON' }).click();
await pluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

// Add minimal plugin configuration using Monaco editor
const pluginEditor = await uiGetMonacoEditor(page, pluginDialog);
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/hot-path.upstream-service-route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ test('can create upstream -> service -> route', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: `Add Plugin: ${servicePluginName}`,
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);

// Add plugin configuration
Expand Down Expand Up @@ -343,7 +343,7 @@ test('can create upstream -> service -> route', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: `Add Plugin: ${routePluginName}`,
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);

// Add plugin configuration
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/plugin_configs.crud-all-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ test('should CRUD plugin config with all fields', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: 'Add Plugin: response-rewrite',
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
await uiFillMonacoEditor(
page,
Expand Down Expand Up @@ -196,7 +196,7 @@ test('should CRUD plugin config with all fields', async ({ page }) => {
const editPluginDialog = page.getByRole('dialog', {
name: 'Edit Plugin: response-rewrite',
});
await editPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await editPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog);
await uiFillMonacoEditor(
page,
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/plugin_configs.crud-required-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ test('should CRUD plugin config with required fields', async ({ page }) => {
const addPluginDialog = page.getByRole('dialog', {
name: 'Add Plugin: response-rewrite',
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
await uiFillMonacoEditor(page, pluginEditor, '{"body": "test response"}');
// add plugin
Expand Down Expand Up @@ -159,7 +159,7 @@ test('should CRUD plugin config with required fields', async ({ page }) => {
const editPluginDialog = page.getByRole('dialog', {
name: 'Edit Plugin: response-rewrite',
});
await editPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await editPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog);
await uiFillMonacoEditor(
page,
Expand Down
20 changes: 10 additions & 10 deletions e2e/tests/plugin_metadata.crud-all-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test('should CRUD plugin metadata with all fields', async ({ page }) => {
name: 'Add Plugin: http-logger',
});
await expect(addPluginDialog).toBeVisible();
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
await page.evaluate(() => {
Expand All @@ -85,18 +85,18 @@ test('should CRUD plugin metadata with all fields', async ({ page }) => {
await pluginEditor.blur();
await expect(pluginEditor.getByText('{')).toBeVisible();
await expect(
addPluginDialog.getByText('Fix JSON syntax before saving.')
addPluginDialog.getByText('Fix Plugin JSON syntax before saving.')
).toBeVisible();
await addPluginDialog.getByRole('button', { name: 'Add Plugin' }).click();
const jsonErrorAlert = addPluginDialog.getByRole('alert').filter({
hasText: /Invalid JSON:|JSON format is not valid/,
});
await expect(jsonErrorAlert).toBeVisible();
await expect(
addPluginDialog.getByRole('button', { name: 'Format JSON after error' })
addPluginDialog.getByRole('button', { name: 'Format Plugin JSON after error' })
).toBeVisible();
await addPluginDialog
.getByRole('button', { name: 'Reset JSON after error' })
.getByRole('button', { name: 'Reset Plugin JSON after error' })
.click();
await expect(jsonErrorAlert).toBeHidden();

Expand All @@ -118,16 +118,16 @@ test('should CRUD plugin metadata with all fields', async ({ page }) => {
})
);
await expect(
addPluginDialog.getByRole('button', { name: 'Format plugin JSON' })
addPluginDialog.getByRole('button', { name: 'Format Plugin JSON' })
).toBeVisible();
await expect(
addPluginDialog.getByRole('button', { name: 'Copy plugin JSON' })
addPluginDialog.getByRole('button', { name: 'Copy Plugin JSON' })
).toBeVisible();
await expect(
addPluginDialog.getByRole('button', { name: 'Reset plugin JSON' })
addPluginDialog.getByRole('button', { name: 'Reset Plugin JSON' })
).toBeVisible();
await addPluginDialog
.getByRole('button', { name: 'Format plugin JSON' })
.getByRole('button', { name: 'Format Plugin JSON' })
.click();
await expect(pluginEditor.getByText('"log_format": {')).toBeVisible();

Expand Down Expand Up @@ -185,7 +185,7 @@ test('should CRUD plugin metadata with all fields', async ({ page }) => {
name: 'Edit Plugin: http-logger',
});
await expect(editPluginDialog).toBeVisible();
await editPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await editPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog, false);

Expand All @@ -207,7 +207,7 @@ test('should CRUD plugin metadata with all fields', async ({ page }) => {
name: 'Edit Plugin: http-logger',
});
await expect(editPluginDialog).toBeVisible();
await editPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await editPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

const pluginEditor = await uiGetMonacoEditor(page, editPluginDialog);
await uiFillMonacoEditor(
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/plugin_metadata.crud-required-fields.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test('should CRUD plugin metadata with required fields only', async ({
name: 'Add Plugin: syslog',
});
await expect(addPluginDialog).toBeVisible();
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

// Fill in minimal required configuration
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog);
Expand Down Expand Up @@ -124,7 +124,7 @@ test('should CRUD plugin metadata with required fields only', async ({
name: 'Edit Plugin: syslog',
});
await expect(editPluginDialog).toBeVisible();
await editPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await editPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();

// Verify existing configuration is shown
await expect(editPluginDialog.getByText('host')).toBeVisible();
Expand Down
8 changes: 4 additions & 4 deletions e2e/tests/resource-required-templates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test('create forms show conditional required fields and minimal JSON templates',
expect.arrayContaining(['uri', 'uris'])
);

await page.getByRole('tab', { name: 'Raw JSON' }).click();
await page.getByRole('tab', { name: 'Payload JSON' }).click();
const routeJsonEditor = page.locator(
'.ant-tabs-tabpane-active .monaco-editor'
);
Expand All @@ -56,7 +56,7 @@ test('create forms show conditional required fields and minimal JSON templates',
await expect.poll(() => requiredFields(page)).not.toContain('uris');

await page.goto('/ui/services/add');
await page.getByRole('tab', { name: 'Raw JSON' }).click();
await page.getByRole('tab', { name: 'Payload JSON' }).click();
await expect
.poll(() =>
readMonacoValue(
Expand All @@ -68,7 +68,7 @@ test('create forms show conditional required fields and minimal JSON templates',

await page.goto('/ui/upstreams/add');
await expect.poll(() => requiredFields(page)).toContain('nodes');
await page.getByRole('tab', { name: 'Raw JSON' }).click();
await page.getByRole('tab', { name: 'Payload JSON' }).click();
await expect
.poll(() =>
readMonacoValue(
Expand Down Expand Up @@ -155,7 +155,7 @@ test('plugin add JSON prefills required fields from APISIX schema', async ({
const addPluginDialog = page.getByRole('dialog', {
name: 'Add Plugin: limit-count',
});
await addPluginDialog.getByRole('tab', { name: 'JSON' }).click();
await addPluginDialog.getByRole('tab', { name: 'Plugin JSON' }).click();
const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog, false);

await expect
Expand Down
10 changes: 5 additions & 5 deletions e2e/tests/routes.edit-payload-preservation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { API_ROUTES } from '@/config/constant';

const routeId = randomId('route-payload-preserve');
const routeUri = '/route-payload-preserve';
const updatedDesc = 'updated through form while preserving raw payload';
const updatedDesc = 'updated through form while preserving Admin API payload';
const rawDraftDesc = 'raw json draft that will fail once';
const rawLatestDesc = 'latest server value after failed raw save';

Expand Down Expand Up @@ -53,7 +53,7 @@ test.afterAll(async () => {
await deleteAllRoutes(e2eReq);
});

test('route form save preserves raw payload and strips readonly fields', async ({
test('route form save preserves Admin API payload and strips readonly fields', async ({
page,
}) => {
await uiGoto(page, '/routes/detail/$id', { id: routeId });
Expand Down Expand Up @@ -113,13 +113,13 @@ test('route form save preserves raw payload and strips readonly fields', async (
});
});

test('raw JSON save failure offers reset and reload recovery actions', async ({
test('Admin API JSON save failure offers reset and reload recovery actions', async ({
page,
}) => {
await uiGoto(page, '/routes/detail/$id', { id: routeId });
await page.getByRole('tab', { name: 'Raw JSON' }).click();
await page.getByRole('tab', { name: 'Admin API JSON' }).click();

const rawJsonPanel = page.getByRole('tabpanel', { name: 'Raw JSON' });
const rawJsonPanel = page.getByRole('tabpanel', { name: 'Admin API JSON' });
const editor = rawJsonPanel.locator('.monaco-editor').first();
await expect(editor).toBeVisible();
await uiFillMonacoEditor(
Expand Down
Loading