From a1529e41b9b85d8ced2aba01f1f754b0b7db0c3a Mon Sep 17 00:00:00 2001 From: Yi Yang Date: Tue, 26 Mar 2024 16:17:54 +0800 Subject: [PATCH] feat(entities-plugins): support array fields in custom plugins (#1289) --- .../entities-plugins/fixtures/mockData.ts | 78 +++++++++++++++++++ .../src/components/PluginForm.cy.ts | 48 ++++++++++++ .../src/components/PluginForm.vue | 37 +++++++++ 3 files changed, 163 insertions(+) diff --git a/packages/entities/entities-plugins/fixtures/mockData.ts b/packages/entities/entities-plugins/fixtures/mockData.ts index 1f70e87a15..3f056d73f0 100644 --- a/packages/entities/entities-plugins/fixtures/mockData.ts +++ b/packages/entities/entities-plugins/fixtures/mockData.ts @@ -831,6 +831,84 @@ export const credentialSchema = { ], } +// custom plugin with array of custom schema objects +export const customPluginSchema = { + fields: [ + { + consumer: { + description: 'Custom type for representing a foreign key with a null value allowed.', + eq: null, + reference: 'consumers', + type: 'foreign', + }, + }, + { + protocols: { + default: [ + 'grpc', + 'grpcs', + 'http', + 'https', + ], + description: 'A set of strings representing HTTP protocols.', + elements: { + one_of: [ + 'grpc', + 'grpcs', + 'http', + 'https', + ], + type: 'string', + }, + required: true, + type: 'set', + }, + }, + { + config: { + fields: [ + { + discovery_uris: { + elements: { + fields: [ + { + issuer: { + required: true, + type: 'string', + }, + }, + { + requires_proxy: { + default: true, + type: 'boolean', + }, + }, + { + ssl_verify: { + default: false, + type: 'boolean', + }, + }, + { + timeout_ms: { + default: 5000, + type: 'number', + }, + }, + ], + type: 'record', + }, + type: 'array', + }, + }, + ], + required: true, + type: 'record', + }, + }, + ], +} + const serviceId = '6ecce9f2-4f3e-45aa-af18-a0553d354845' // CORS plugin export const plugin1 = { diff --git a/packages/entities/entities-plugins/src/components/PluginForm.cy.ts b/packages/entities/entities-plugins/src/components/PluginForm.cy.ts index c1a0a794dc..18395c2d15 100644 --- a/packages/entities/entities-plugins/src/components/PluginForm.cy.ts +++ b/packages/entities/entities-plugins/src/components/PluginForm.cy.ts @@ -10,6 +10,7 @@ import { schema2, scopedService, scopedConsumer, + customPluginSchema, } from '../../fixtures/mockData' import PluginForm from './PluginForm.vue' import { VueFormGenerator } from '../../src' @@ -297,6 +298,29 @@ describe('', () => { cy.get('@advancedFields').find('#config-random_status_code').should('be.visible') }) + it('should show correct form components for custom plugin with arrays of objects', () => { + interceptKMSchema({ mockData: customPluginSchema }) + + cy.mount(PluginForm, { + global: { components: { VueFormGenerator } }, + props: { + config: baseConfigKM, + pluginType: 'custom', + }, + router, + }) + + cy.wait('@getPluginSchema') + cy.get('.kong-ui-entities-plugin-form-container').should('be.visible') + + // array field + cy.getTestId('add-config-discovery_uris').click() + cy.get('#config-discovery_uris-issuer-0').should('have.attr', 'required') + cy.get('#config-discovery_uris-requires_proxy-0').should('have.attr', 'type', 'checkbox').and('be.checked') + cy.get('#config-discovery_uris-ssl_verify-0').should('have.attr', 'type', 'checkbox').and('not.be.checked') + cy.get('#config-discovery_uris-timeout_ms-0').should('have.attr', 'type', 'number').and('have.value', '5000') + }) + it('should hide scope selection when hideScopeSelection is true', () => { interceptKMSchema() @@ -1020,6 +1044,30 @@ describe('', () => { cy.get('@advancedFields').find('#config-random_status_code').should('be.visible') }) + it('should show correct form components for custom plugin with arrays of objects', () => { + interceptKonnectSchema({ mockData: customPluginSchema }) + + cy.mount(PluginForm, { + global: { components: { VueFormGenerator } }, + props: { + config: baseConfigKonnect, + pluginType: 'custom', + useCustomNamesForPlugin: true, + }, + router, + }) + + cy.wait('@getPluginSchema') + cy.get('.kong-ui-entities-plugin-form-container').should('be.visible') + + // array field + cy.getTestId('add-config-discovery_uris').click() + cy.get('#config-discovery_uris-issuer-0').should('have.attr', 'required') + cy.get('#config-discovery_uris-requires_proxy-0').should('have.attr', 'type', 'checkbox').and('be.checked') + cy.get('#config-discovery_uris-ssl_verify-0').should('have.attr', 'type', 'checkbox').and('not.be.checked') + cy.get('#config-discovery_uris-timeout_ms-0').should('have.attr', 'type', 'number').and('have.value', '5000') + }) + it('should hide scope selection when hideScopeSelection is true', () => { interceptKonnectSchema() diff --git a/packages/entities/entities-plugins/src/components/PluginForm.vue b/packages/entities/entities-plugins/src/components/PluginForm.vue index 1aa54066dc..e0537f0392 100644 --- a/packages/entities/entities-plugins/src/components/PluginForm.vue +++ b/packages/entities/entities-plugins/src/components/PluginForm.vue @@ -647,6 +647,43 @@ const buildFormSchema = (parentKey: string, response: Record, initi } } + // If itemFields is not defined, it means no custom schema for this field is defined + // This usually happens for a custom plugin, so we need to build the schema + if (!itemFields) { + initialFormSchema[field].fieldClasses = 'array-card-container-wrapper' + initialFormSchema[field].itemContainerComponent = 'FieldArrayCardContainer' + initialFormSchema[field].items = { + type: 'object', + schema: { + fields: Object.values(buildFormSchema(field, scheme.elements, {})), + }, + } + initialFormSchema[field].type = 'array' + initialFormSchema[field].newElementButtonLabelClasses = 'kong-form-new-element-button-label' + // Set the model to the field name, and the label to the formatted field name + initialFormSchema[field].items.schema.fields.forEach( + (field: { id?: string, model?: string, label?: string }) => { + for (const f of scheme.elements.fields) { + const modelName = Object.keys(f)[0] + const idParts = field.id?.split?.('-') ?? [] + if (idParts[idParts.length - 1] === modelName) { + field.model = modelName + field.label = formatPluginFieldLabel(modelName) + break + } + } + }, + ) + } + + // If the field is an array of objects, set the default value to an object + // with the default values of the nested fields + initialFormSchema[field].items.default = () => + scheme.elements.fields.reduce((acc: Record, current: Record) => { + const key = Object.keys(current)[0] + acc[key] = current[key].default + return acc + }, {}) } if (treatAsCredential.value && props.config.app === 'kongManager' && credentialSchema) {