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
29 changes: 29 additions & 0 deletions docs/design/schema-ux-todo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Schema UX TODO

This checklist tracks usability problems where the dashboard already knows the
Admin API schema, but the create/edit experience does not fully use that
knowledge yet.

## Completed

- [x] Prefill plugin Add JSON with required fields, not only schema defaults.
- [x] Keep primitive `oneOf` / `anyOf` required fields from becoming `{}` when a
plugin schema offers scalar alternatives.
- [x] Centralize JSON Schema template generation so plugin JSON and schema form
defaults share the same placeholder rules.
- [x] Cover union required fields, conditional required fields, and required
array items with regression tests.

## Next

- [ ] Audit complex plugin schemas in the live APISIX catalog:
`openid-connect`, `ai-proxy`, `ai-proxy-multi`, `saml-auth`, `proxy-cache`,
and Redis-backed `limit-count` variants.
- [ ] Verify Fields to JSON to Fields round trips for plugin schemas with
nested `oneOf`, `anyOf`, `if` / `then`, and `minItems`.
- [ ] Expand save-failure recovery checks across create Raw JSON, resource Raw
JSON, and plugin JSON drawers.
- [ ] Add clone-flow payload checks so cloned Routes, Services, and Upstreams
never submit read-only fields such as `id`, `create_time`, or `update_time`.
- [ ] Compare conditional required markers with generated JSON templates for
Routes, SSLs, Upstreams, Secrets, and plugin configs.
157 changes: 157 additions & 0 deletions e2e/tests/schema-template.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, test } from '@playwright/test';

import { buildJsonSchemaTemplate } from '@/components/schema-form/schemaTemplate';
import {
type JSONSchema,
validateSchemaValue,
} from '@/components/schema-form/schemaValidation';

const positiveIntegerOrString: JSONSchema = {
oneOf: [
{ type: 'integer', exclusiveMinimum: 0 },
{ type: 'string' },
],
};

test('builds a minimal template for union-based plugin required fields', () => {
const limitCountSchema: JSONSchema = {
type: 'object',
oneOf: [
{ required: ['count', 'time_window'] },
{ required: ['rules'] },
],
properties: {
count: positiveIntegerOrString,
time_window: positiveIntegerOrString,
rules: {
type: 'array',
minItems: 1,
items: {
type: 'object',
required: ['count', 'time_window', 'key'],
properties: {
count: positiveIntegerOrString,
time_window: positiveIntegerOrString,
key: { type: 'string', minLength: 1 },
},
},
},
},
};

const template = buildJsonSchemaTemplate(limitCountSchema);

expect(template).toEqual({
count: 1,
time_window: 1,
});
expect(validateSchemaValue(limitCountSchema, template)).toEqual([]);
});

test('fills conditional plugin requirements from the selected branch', () => {
const redisLimitCountSchema: JSONSchema = {
type: 'object',
properties: {
policy: {
type: 'string',
enum: ['local', 'redis-sentinel'],
default: 'local',
},
},
if: {
properties: {
policy: { enum: ['redis-sentinel'] },
},
},
then: {
required: ['redis_sentinels', 'redis_master_name'],
properties: {
redis_sentinels: {
type: 'array',
minItems: 1,
items: {
type: 'object',
required: ['host', 'port'],
properties: {
host: { type: 'string', minLength: 2 },
port: { type: 'integer', minimum: 1 },
},
},
},
redis_master_name: { type: 'string', minLength: 1 },
},
},
};

const template = buildJsonSchemaTemplate(redisLimitCountSchema, {
policy: 'redis-sentinel',
});

expect(template).toEqual({
policy: 'redis-sentinel',
redis_sentinels: [
{
host: 'value',
port: 1,
},
],
redis_master_name: 'value',
});
expect(validateSchemaValue(redisLimitCountSchema, template)).toEqual([]);
});

test('creates required array items for multi-instance plugin schemas', () => {
const aiProxyMultiSchema: JSONSchema = {
type: 'object',
required: ['instances'],
properties: {
instances: {
type: 'array',
minItems: 1,
items: {
type: 'object',
required: ['name', 'provider', 'auth', 'weight'],
properties: {
name: { type: 'string', minLength: 1 },
provider: { type: 'string', minLength: 1 },
auth: {
type: 'object',
additionalProperties: false,
},
weight: { type: 'integer', minimum: 0 },
},
},
},
},
};

const template = buildJsonSchemaTemplate(aiProxyMultiSchema);

expect(template).toEqual({
instances: [
{
name: 'value',
provider: 'value',
auth: {},
weight: 0,
},
],
});
expect(validateSchemaValue(aiProxyMultiSchema, template)).toEqual([]);
});
Loading