From a754506d9bf77e0f51686149018a1d484542a0ad Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 20 Oct 2023 12:45:25 -0400 Subject: [PATCH 01/40] feat(plugins): form unification [khcp-9487] --- packages/entities/entities-plugins/README.md | 4 +- .../entities-plugins/docs/plugin-form.md | 113 ++++++++++++++++++ .../entities-plugins/src/plugins-endpoints.ts | 4 + 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 packages/entities/entities-plugins/docs/plugin-form.md diff --git a/packages/entities/entities-plugins/README.md b/packages/entities/entities-plugins/README.md index 8d9ec8b01b..8711178ee5 100644 --- a/packages/entities/entities-plugins/README.md +++ b/packages/entities/entities-plugins/README.md @@ -37,11 +37,13 @@ yarn add @kong-ui-public/entities-plugins Import the component(s) in your host application as well as the package styles ```ts -import { PluginList, PluginConfigCard } from '@kong-ui-public/entities-plugins' +import { PluginList, PluginSelect, PluginForm, PluginConfigCard } from '@kong-ui-public/entities-plugins' import '@kong-ui-public/entities-plugins/dist/style.css' ``` ## Individual component documentation - [``](docs/plugin-list.md) +- [``](docs/plugin-select.md) +- [``](docs/plugin-form.md) - [``](docs/plugin-config-card.md) diff --git a/packages/entities/entities-plugins/docs/plugin-form.md b/packages/entities/entities-plugins/docs/plugin-form.md new file mode 100644 index 0000000000..dd8d621881 --- /dev/null +++ b/packages/entities/entities-plugins/docs/plugin-form.md @@ -0,0 +1,113 @@ +# PluginForm.vue + +A form component for Plugins. + +- [Requirements](#requirements) +- [Usage](#usage) + - [Install](#install) + - [Props](#props) + - [Events](#events) + - [Usage example](#usage-example) +- [TypeScript interfaces](#typescript-interfaces) + +## Requirements + +- `vue` and `vue-router` must be initialized in the host application +- `@kong/kongponents` must be added as a dependency in the host application, globally available via the Vue Plugin installation, and the package's style imports must be added in the app entry file. [See here for instructions on installing Kongponents](https://kongponents.konghq.com/#globally-install-all-kongponents). +- `@kong-ui-public/i18n` must be available as a `dependency` in the host application. +- `axios` must be installed as a dependency in the host application + +## Usage + +### Install + +[See instructions for installing the `@kong-ui-public/entities-plugins` package.](../README.md#install) + +### Props + +#### `config` + +- type: `Object as PropType` +- required: `true` +- default: `undefined` +- properties: + - `app`: + - type: `'konnect' | 'kongManager'` + - required: `true` + - default: `undefined` + - App name. + + - `apiBaseUrl`: + - type: `string` + - required: `true` + - default: `undefined` + - Base URL for API requests. + + - `requestHeaders`: + - type: `RawAxiosRequestHeaders | AxiosHeaders` + - required: `false` + - default: `undefined` + - Additional headers to send with all Axios requests. + + - `cancelRoute`: + - type: `RouteLocationRaw` + - required: `true` + - default: `undefined` + - Route to return to when canceling creation of a plugin. + + - `workspace`: + - type: `string` + - required: `true` + - default: `undefined` + - *Specific to Kong Manager*. Name of the current workspace. + + - `controlPlaneId`: + - type: `string` + - required: `true` + - default: `undefined` + - *Specific to Konnect*. Name of the current control plane. + + - `consumerId`: + - type: `string` + - required: `false` + - default: `''` + - Consumer to bind the plugin to on creation. This is used when creating a consumer credential. + +The base konnect or kongManger config. + +#### `pluginId` + +- type: `String` +- required: `false` +- default: `''` + +If showing the `Edit` type form, the ID of the plugin. + +### Events + +#### error + +An `@error` event is emitted when form validation fails. The event payload is the response error. + +#### loading + +A `@loading` event is emitted when loading state changes. The event payload is a boolean. + +#### update + +A `@update` event is emitted when the form is saved. The event payload is the plugin object. + +### Usage example + +Please refer to the [sandbox](../sandbox/pages/PluginListPage.vue). The form is accessible by clicking the `+ New Plugin` button or `Edit` action of an existing plugin. + +## TypeScript interfaces + +TypeScript interfaces [are available here](https://github.com/Kong/public-ui-components/blob/main/packages/entities/entities-plugins/src/types/plugin-form.ts) and can be directly imported into your host application. The following type interfaces are available for import: + +```ts +import type { + KonnectPluginFormConfig, + KongManagerPluginFormConfig, +} from '@kong-ui-public/entities-plugins' +``` diff --git a/packages/entities/entities-plugins/src/plugins-endpoints.ts b/packages/entities/entities-plugins/src/plugins-endpoints.ts index 432723bf26..b495a80148 100644 --- a/packages/entities/entities-plugins/src/plugins-endpoints.ts +++ b/packages/entities/entities-plugins/src/plugins-endpoints.ts @@ -11,12 +11,16 @@ export default { }, form: { konnect: { + create: '/api/runtime_groups/{controlPlaneId}/plugins', edit: '/api/runtime_groups/{controlPlaneId}/plugins/{id}', pluginSchema: '/api/runtime_groups/{controlPlaneId}/schemas/plugins/{plugin}', + validate: '/api/runtime_groups/{controlPlaneId}/v1/schemas/json/plugin/validate', }, kongManager: { + create: '/{workspace}/plugins', edit: '/{workspace}/plugins/{id}', pluginSchema: '/{workspace}/schemas/plugins/{plugin}', + validate: '/{workspace}/schemas/plugins/validate', }, }, item: { From 65d781592d04e638f3fba5cdba5cc8a74f802b74 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Sun, 22 Oct 2023 21:58:31 -0400 Subject: [PATCH 02/40] fix(*): second drop --- .../src/components/PluginCardSkeleton.vue | 62 +++ .../src/components/PluginList.vue | 3 +- .../src/components/PluginSelect.vue | 239 +++++++++++ .../src/components/PluginSelectCard.vue | 338 +++++++++++++++ .../src/components/PluginSelectGrid.vue | 402 ++++++++++++++++++ .../entities/entities-plugins/src/index.ts | 2 + .../entities-plugins/src/locales/en.json | 33 +- .../entities-plugins/src/types/index.ts | 1 + .../entities-plugins/src/types/plugin-form.ts | 35 ++ .../entities-plugins/src/types/plugin.ts | 20 +- 10 files changed, 1129 insertions(+), 6 deletions(-) create mode 100644 packages/entities/entities-plugins/src/components/PluginCardSkeleton.vue create mode 100644 packages/entities/entities-plugins/src/components/PluginSelect.vue create mode 100644 packages/entities/entities-plugins/src/components/PluginSelectCard.vue create mode 100644 packages/entities/entities-plugins/src/components/PluginSelectGrid.vue create mode 100644 packages/entities/entities-plugins/src/types/plugin-form.ts diff --git a/packages/entities/entities-plugins/src/components/PluginCardSkeleton.vue b/packages/entities/entities-plugins/src/components/PluginCardSkeleton.vue new file mode 100644 index 0000000000..1b5caff480 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginCardSkeleton.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/components/PluginList.vue b/packages/entities/entities-plugins/src/components/PluginList.vue index 95328e1a30..85aa7fa53f 100644 --- a/packages/entities/entities-plugins/src/components/PluginList.vue +++ b/packages/entities/entities-plugins/src/components/PluginList.vue @@ -237,6 +237,7 @@ import { useDeleteUrlBuilder, useGatewayFeatureSupported, } from '@kong-ui-public/entities-shared' +import '@kong-ui-public/entities-shared/dist/style.css' import type { BaseTableHeaders, @@ -259,8 +260,6 @@ import type { import PluginIcon from './PluginIcon.vue' -import '@kong-ui-public/entities-shared/dist/style.css' - const pluginMetaData = composables.usePluginMetaData() const emit = defineEmits<{ diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue new file mode 100644 index 0000000000..78b59dfa65 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -0,0 +1,239 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue new file mode 100644 index 0000000000..4c269ec76f --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue new file mode 100644 index 0000000000..e5675e4222 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/index.ts b/packages/entities/entities-plugins/src/index.ts index 55d3d14213..c7b5a5df64 100644 --- a/packages/entities/entities-plugins/src/index.ts +++ b/packages/entities/entities-plugins/src/index.ts @@ -1,5 +1,6 @@ import PluginIcon from './components/PluginIcon.vue' import PluginList from './components/PluginList.vue' +import PluginForm from './components/PluginForm.vue' import PluginConfigCard from './components/PluginConfigCard.vue' import composables from './composables' @@ -8,6 +9,7 @@ const { getPluginIconURL, usePluginMetaData } = composables export { PluginIcon, PluginList, + PluginForm, PluginConfigCard, getPluginIconURL, usePluginMetaData, diff --git a/packages/entities/entities-plugins/src/locales/en.json b/packages/entities/entities-plugins/src/locales/en.json index 1d64dedb23..bbf00f2389 100644 --- a/packages/entities/entities-plugins/src/locales/en.json +++ b/packages/entities/entities-plugins/src/locales/en.json @@ -1,9 +1,11 @@ { "actions": { "create": "New Plugin", + "create_custom": "Create", "copy_id": "Copy ID", "copy_json": "Copy JSON", "edit": "Edit", + "enable": "Enable", "delete": "Delete", "view": "View Details", "configure_dynamic_ordering": "Configure Dynamic Ordering" @@ -21,17 +23,20 @@ "errors": { "general": "Plugins could not be retrieved", "delete": "The plugin could not be deleted at this time.", - "copy": "Failed to copy to clipboard" + "copy": "Failed to copy to clipboard", + "load_results": "Error loading available plugins" }, "search": { "placeholder": { - "konnect": "Filter by exact instance name or ID" + "konnect": "Filter by exact instance name or ID", + "select": "Filter plugins" }, "filter": { "field": { "enabled": "Enabled" } - } + }, + "no_results": "No results found for \"{filter}\"" }, "plugins": { "title": "Plugins", @@ -391,6 +396,28 @@ "route": "Route ID", "consumer": "Consumer ID", "consumer_group": "Consumer Group ID" + }, + "select": { + "custom_badge_text": "Data Plane Node Specific", + "unavailable_tooltip": "This plugin is not available", + "misc_plugins": "Other Plugins", + "tabs": { + "kong": { + "title": "Kong Plugins", + "description": "Kong plugins are bundled by default with Kong Gateway and are available across all control planes.", + "app_reg_tooltip": "This plugin is not available because App Registration is enabled for this Service." + }, + "custom": { + "title": "Custom Plugins", + "description": "Custom plugins will be available in this control plane only.", + "empty_title": "No Custom Plugins", + "empty_description": "No custom plugins have been added to this Control Plane.", + "create": { + "name": "Custom Plugin", + "description": "Upload schema file to create custom plugin" + } + } + } } }, "glossary": { diff --git a/packages/entities/entities-plugins/src/types/index.ts b/packages/entities/entities-plugins/src/types/index.ts index 323770ab02..50c9c2cd36 100644 --- a/packages/entities/entities-plugins/src/types/index.ts +++ b/packages/entities/entities-plugins/src/types/index.ts @@ -1,3 +1,4 @@ export * from './plugin-config-card' +export * from './plugin-form' export * from './plugin-list' export * from './plugin' diff --git a/packages/entities/entities-plugins/src/types/plugin-form.ts b/packages/entities/entities-plugins/src/types/plugin-form.ts new file mode 100644 index 0000000000..eb4beb2be4 --- /dev/null +++ b/packages/entities/entities-plugins/src/types/plugin-form.ts @@ -0,0 +1,35 @@ +import type { RouteLocationRaw } from 'vue-router' +import type { KonnectBaseFormConfig, KongManagerBaseFormConfig } from '@kong-ui-public/entities-shared' + +export interface BasePluginFormConfig { + /** Route for creating a plugin */ + createRoute: RouteLocationRaw + /** Consumer to bind the Plugin to on creation if Consumer Credential */ + consumerId?: string +} + +/** Konnect Plugin form config */ +export interface KonnectPluginFormConfig extends BasePluginFormConfig, KonnectBaseFormConfig { + /** Route for creating a custom plugin */ + createCustomRoute: RouteLocationRaw + /** A function that returns the route for editing a custom plugin */ + getCustomEditRoute: (id: string) => RouteLocationRaw +} + +/** Kong Manager Plugin form config */ +export interface KongManagerPluginFormConfig extends BasePluginFormConfig, KongManagerBaseFormConfig {} + +export interface PluginFormFields { + name: string + tags: string + consumer_id: string +} + +export interface PluginFormState { + /** Form fields */ + fields: PluginFormFields + /** Form readonly state (only used when saving entity details) */ + isReadonly: boolean + /** The error message to show on the form */ + errorMessage: string +} diff --git a/packages/entities/entities-plugins/src/types/plugin.ts b/packages/entities/entities-plugins/src/types/plugin.ts index c8777722a9..f834c4e7b4 100644 --- a/packages/entities/entities-plugins/src/types/plugin.ts +++ b/packages/entities/entities-plugins/src/types/plugin.ts @@ -8,7 +8,19 @@ export enum PluginGroup { LOGGING = 'Logging', DEPLOYMENT = 'Deployment', WEBSOCKET = 'WebSocket Plugins', - CUSTOM_PLUGINS = 'Other Plugins', + CUSTOM_PLUGINS = 'Custom Plugins', +} + +export const PLUGIN_GROUPS_COLLAPSE_STATUS = { + AUTHENTICATION: true, + SECURITY: true, + TRAFFIC_CONTROL: true, + SERVERLESS: true, + ANALYTICS_AND_MONITORING: true, + TRANSFORMATIONS: true, + LOGGING: true, + DEPLOYMENT: true, + CUSTOM_PLUGINS: true, } export enum PluginScope { @@ -28,3 +40,9 @@ export type PluginMetaData = { name: string // A display name of the Plugin. scope: PluginScope[] // The scope supported by the Plugin. } + +export interface PluginType extends PluginMetaData { + available: boolean // whether the plugin is available or not + disabledMessage?: string // An optional field for plugin's disabled message. + id: string // the plugin schema name +} From 742b9ddeb780399e39b89601518477be3c0e574e Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Thu, 26 Oct 2023 14:30:26 -0400 Subject: [PATCH 03/40] fix(*): third drop --- .../entities-plugins/docs/plugin-form.md | 6 +- .../entities-plugins/docs/plugin-select.md | 95 +++ .../entities-plugins/sandbox/index.ts | 7 +- .../sandbox/pages/PluginListPage.vue | 4 +- .../sandbox/pages/PluginSelectPage.vue | 59 ++ .../DeleteCustomPluginSchemaModal.vue | 180 +++++ .../src/components/PluginCustomGrid.vue | 98 +++ .../src/components/PluginForm.cy.ts | 632 ++++++++++++++++++ .../src/components/PluginForm.vue | 233 +++++++ .../src/components/PluginSelect.vue | 105 +-- .../src/components/PluginSelectCard.vue | 59 +- .../src/components/PluginSelectGrid.vue | 294 ++++---- .../entities-plugins/src/constants.ts | 1 + .../entities/entities-plugins/src/index.ts | 6 +- .../entities-plugins/src/locales/en.json | 2 + .../entities-plugins/src/plugins-endpoints.ts | 8 + .../entities-plugins/src/types/plugin-form.ts | 10 +- .../entities-plugins/src/types/plugin.ts | 25 + .../src/composables/useHelpers.ts | 40 ++ 19 files changed, 1622 insertions(+), 242 deletions(-) create mode 100644 packages/entities/entities-plugins/docs/plugin-select.md create mode 100644 packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue create mode 100644 packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue create mode 100644 packages/entities/entities-plugins/src/components/PluginCustomGrid.vue create mode 100644 packages/entities/entities-plugins/src/components/PluginForm.cy.ts create mode 100644 packages/entities/entities-plugins/src/components/PluginForm.vue create mode 100644 packages/entities/entities-plugins/src/constants.ts diff --git a/packages/entities/entities-plugins/docs/plugin-form.md b/packages/entities/entities-plugins/docs/plugin-form.md index dd8d621881..bbcda141e0 100644 --- a/packages/entities/entities-plugins/docs/plugin-form.md +++ b/packages/entities/entities-plugins/docs/plugin-form.md @@ -67,11 +67,11 @@ A form component for Plugins. - default: `undefined` - *Specific to Konnect*. Name of the current control plane. - - `consumerId`: + - `entityId`: - type: `string` - required: `false` - default: `''` - - Consumer to bind the plugin to on creation. This is used when creating a consumer credential. + - Id of the entity to bind the plugin to on creation. The base konnect or kongManger config. @@ -99,7 +99,7 @@ A `@update` event is emitted when the form is saved. The event payload is the pl ### Usage example -Please refer to the [sandbox](../sandbox/pages/PluginListPage.vue). The form is accessible by clicking the `+ New Plugin` button or `Edit` action of an existing plugin. +Please refer to the [sandbox](../sandbox/pages/PluginListPage.vue). The form is accessible by clicking the `Edit` action of an existing plugin or after selecting a plugin when creating a new one. ## TypeScript interfaces diff --git a/packages/entities/entities-plugins/docs/plugin-select.md b/packages/entities/entities-plugins/docs/plugin-select.md new file mode 100644 index 0000000000..0e15d299bf --- /dev/null +++ b/packages/entities/entities-plugins/docs/plugin-select.md @@ -0,0 +1,95 @@ +# PluginSelect.vue + +A grid component for selecting Plugins. + +- [Requirements](#requirements) +- [Usage](#usage) + - [Install](#install) + - [Props](#props) + - [Events](#events) + - [Usage example](#usage-example) +- [TypeScript interfaces](#typescript-interfaces) + +## Requirements + +- `vue` and `vue-router` must be initialized in the host application +- `@kong/kongponents` must be added as a dependency in the host application, globally available via the Vue Plugin installation, and the package's style imports must be added in the app entry file. [See here for instructions on installing Kongponents](https://kongponents.konghq.com/#globally-install-all-kongponents). +- `@kong-ui-public/i18n` must be available as a `dependency` in the host application. +- `axios` must be installed as a dependency in the host application + +## Usage + +### Install + +[See instructions for installing the `@kong-ui-public/entities-plugins` package.](../README.md#install) + +### Props + +#### `config` + +- type: `Object as PropType` +- required: `true` +- default: `undefined` +- properties: + - `app`: + - type: `'konnect' | 'kongManager'` + - required: `true` + - default: `undefined` + - App name. + + - `apiBaseUrl`: + - type: `string` + - required: `true` + - default: `undefined` + - Base URL for API requests. + + - `requestHeaders`: + - type: `RawAxiosRequestHeaders | AxiosHeaders` + - required: `false` + - default: `undefined` + - Additional headers to send with all Axios requests. + + - `getCreateRoute`: + - type: `(plugin: string) => RouteLocationRaw` + - required: `true` + - default: `undefined` + - A function that returns the route for creating a specific plugin type. + + - `workspace`: + - type: `string` + - required: `true` + - default: `undefined` + - *Specific to Kong Manager*. Name of the current workspace. + + - `controlPlaneId`: + - type: `string` + - required: `true` + - default: `undefined` + - *Specific to Konnect*. Name of the current control plane. + +The base konnect or kongManger config. + +### Events + +#### plugin-clicked + +An `@plugin-clicked` event is emitted when a plugin in the selection grid is clicked. The event payload is the plugin object. + +#### loading + +A `@loading` event is emitted when loading state changes. The event payload is a boolean. + +### Usage example + +Please refer to the [sandbox](../sandbox/pages/PluginListPage.vue). The form is accessible by clicking the `+ New Plugin` button. + +## TypeScript interfaces + +TypeScript interfaces [are available here](https://github.com/Kong/public-ui-components/blob/main/packages/entities/entities-plugins/src/types/plugin-form.ts) and can be directly imported into your host application. The following type interfaces are available for import: + +```ts +import type { + KonnectPluginFormConfig, + KongManagerPluginFormConfig, +} from '@kong-ui-public/entities-plugins' +``` diff --git a/packages/entities/entities-plugins/sandbox/index.ts b/packages/entities/entities-plugins/sandbox/index.ts index bc99ab7b01..cfd1e30b1a 100644 --- a/packages/entities/entities-plugins/sandbox/index.ts +++ b/packages/entities/entities-plugins/sandbox/index.ts @@ -16,7 +16,12 @@ const init = async () => { component: () => import('./pages/PluginListPage.vue'), }, { - path: '/plugin/create', + path: '/plugin/select', + name: 'select-plugin', + component: () => import('./pages/PluginSelectPage.vue'), + }, + { + path: '/plugin/create/:plugin', name: 'create-plugin', component: () => import('./pages/FallbackPage.vue'), }, diff --git a/packages/entities/entities-plugins/sandbox/pages/PluginListPage.vue b/packages/entities/entities-plugins/sandbox/pages/PluginListPage.vue index c260517b62..591263b8c7 100644 --- a/packages/entities/entities-plugins/sandbox/pages/PluginListPage.vue +++ b/packages/entities/entities-plugins/sandbox/pages/PluginListPage.vue @@ -56,7 +56,7 @@ const konnectConfig = ref({ apiBaseUrl: '/us/kong-api/konnect-api', // `/{geo}/kong-api/konnect-api`, with leading slash and no trailing slash // Set the root `.env.development.local` variable to a control plane your PAT can access controlPlaneId, - createRoute: { name: 'create-plugin' }, + createRoute: { name: 'select-plugin' }, getViewRoute: (plugin: EntityRow) => ({ name: 'view-plugin', params: { id: plugin.id, plugin: plugin.name } }), getEditRoute: (plugin: EntityRow) => ({ name: 'edit-plugin', params: { id: plugin.id } }), getScopedEntityViewRoute: (type: ViewRouteType, id: string) => ({ name: `view-${type}`, params: { id } }), @@ -73,7 +73,7 @@ const kongManagerConfig = ref({ workspace: 'default', apiBaseUrl: '/kong-manager', // For local dev server proxy isExactMatch: false, - createRoute: { name: 'create-plugin' }, + createRoute: { name: 'select-plugin' }, getViewRoute: (plugin: EntityRow) => ({ name: 'view-plugin', params: { id: plugin.id, plugin: plugin.name } }), getEditRoute: (plugin: EntityRow) => ({ name: 'edit-plugin', params: { id: plugin.id } }), getScopedEntityViewRoute: (type: ViewRouteType, id: string) => ({ name: `view-${type}`, params: { id } }), diff --git a/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue b/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue new file mode 100644 index 0000000000..81ff226baa --- /dev/null +++ b/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue b/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue new file mode 100644 index 0000000000..2bb7e4c824 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue @@ -0,0 +1,180 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue new file mode 100644 index 0000000000..03d6ed9396 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/components/PluginForm.cy.ts b/packages/entities/entities-plugins/src/components/PluginForm.cy.ts new file mode 100644 index 0000000000..ec13322d29 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginForm.cy.ts @@ -0,0 +1,632 @@ +// Cypress component test spec file +import type { KongManagerPluginFormConfig, KonnectPluginFormConfig } from '../types' +import { plugin1 } from '../../fixtures/mockData' +import PluginForm from './PluginForm.vue' +import { EntityBaseForm } from '@kong-ui-public/entities-shared' + +const cancelRoute = { name: 'plugins-list' } + +const baseConfigKonnect:KonnectPluginFormConfig = { + app: 'konnect', + controlPlaneId: '1234-abcd-ilove-cats', + apiBaseUrl: '/us/kong-api/konnect-api', + cancelRoute, +} + +const baseConfigKM:KongManagerPluginFormConfig = { + app: 'kongManager', + workspace: 'default', + apiBaseUrl: '/kong-manager', + cancelRoute, +} + +describe('', () => { + describe('Kong Manager', () => { + const interceptKM = (params?: { + mockData?: object + alias?: string + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/plugins/*`, + }, + { + statusCode: 200, + body: params?.mockData ?? plugin1, + }, + ).as(params?.alias ?? 'getPlugin') + } + + const interceptUpdate = (status = 200): void => { + cy.intercept( + { + method: 'POST', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/schemas/plugins/validate`, + }, + { + statusCode: status, + body: { }, + }, + ).as('validatePlugin') + + cy.intercept( + { + method: 'PATCH', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/plugins/*`, + }, + { + statusCode: status, + body: { ...plugin1, tags: ['tag1', 'tag2'] }, + }, + ).as('updatePlugin') + } + + it('should show create form', () => { + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.get('.kong-ui-entities-plugins-form form').should('be.visible') + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // form fields + cy.getTestId('plugin-form-name').should('be.visible') + cy.getTestId('plugin-form-tags').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('be.visible') + // certs load in select + cy.getTestId('plugin-form-certificate-id').click() + cy.get('.k-select-list .k-select-item').should('have.length', certificates.data.length) + }) + + it('should correctly handle button state - create', () => { + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // enables save when required fields have values + cy.getTestId('plugin-form-name').type('tk-meowstersmith') + cy.getTestId('plugin-form-certificate-id').click() + cy.get(`[data-testid="k-select-item-${certificates.data[0].id}"] button`).click() + cy.getTestId('form-submit').should('be.enabled') + // disables save when required field is cleared + cy.getTestId('plugin-form-name').clear() + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should allow exact match filtering of certs', () => { + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // search + cy.getTestId('plugin-form-certificate-id').should('be.visible') + cy.getTestId('plugin-form-certificate-id').type(plugin1.certificate.id) + // click kselect item + cy.getTestId(`k-select-item-${plugin1.certificate.id}`).should('be.visible') + cy.get(`[data-testid="k-select-item-${plugin1.certificate.id}"] button`).click() + cy.getTestId('plugin-form-certificate-id').should('have.value', plugin1.certificate.id) + }) + + it('should set cert selection as readonly if provided in config - on create', () => { + + cy.mount(PluginForm, { + props: { + config: { + ...baseConfigKM, + certificateId: '1234-cats-beat-certs', + }, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // search + cy.getTestId('plugin-form-certificate-id').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('have.attr', 'readonly') + }) + + it('should not set cert selection as readonly if provided in config - on edit', () => { + + interceptKM() + + cy.mount(PluginForm, { + props: { + config: { + ...baseConfigKM, + certificateId: '1234-cats-beat-certs', + }, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('not.have.attr', 'readonly') + }) + + it('should show edit form', () => { + + interceptKM() + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // form fields + cy.getTestId('plugin-form-name').should('have.value', plugin1.name) + plugin1.tags.forEach((tag: string) => { + cy.getTestId('plugin-form-tags').invoke('val').then((val: string) => { + expect(val).to.contain(tag) + }) + }) + cy.getTestId('plugin-form-certificate-id').should('have.value', plugin1.certificate.id) + }) + + it('should correctly handle button state - edit', () => { + + interceptKM() + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // enables save when form has changes + cy.getTestId('plugin-form-name').type('-edited') + cy.getTestId('form-submit').should('be.enabled') + // disables save when form changes are undone + cy.getTestId('plugin-form-name').clear() + cy.getTestId('plugin-form-name').type(plugin1.name) + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should handle error state - failed to load plugin', () => { + + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/plugins/*`, + }, + { + statusCode: 404, + body: {}, + }, + ).as('getPlugin') + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // error state is displayed + cy.getTestId('form-fetch-error').should('be.visible') + // buttons and form hidden + cy.getTestId('form-cancel').should('not.exist') + cy.getTestId('form-submit').should('not.exist') + cy.get('.kong-ui-entities-plugins-form form').should('not.exist') + }) + + it('should handle error state - failed to load certs', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/certificates*`, + }, + { + statusCode: 500, + body: {}, + }, + ).as('getCertificates') + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('form-error').should('be.visible') + }) + + it('should handle error state - invalid cert id', () => { + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('plugin-form-certificate-id').type('xxxx') + cy.getTestId('no-search-results').should('be.visible') + cy.getTestId('invalid-certificate-message').should('exist') + }) + + it('update event should be emitted when plugin was edited', () => { + + interceptKM() + interceptUpdate() + + cy.mount(PluginForm, { + props: { + config: baseConfigKM, + pluginId: plugin1.id, + onUpdate: cy.spy().as('onUpdateSpy'), + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') + + cy.wait('@getPlugin') + cy.getTestId('plugin-form-tags').clear() + cy.getTestId('plugin-form-tags').type('tag1,tag2') + + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) + + cy.wait('@validatePlugin') + cy.wait('@updatePlugin') + + cy.get('@onUpdateSpy').should('have.been.calledOnce') + }) + }) + + describe('Konnect', () => { + const interceptKonnectCerts = (params?: { + mockData?: object + alias?: string + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/certificates*`, + }, + { + statusCode: 200, + body: params?.mockData ?? certificates, + }, + ).as(params?.alias ?? 'getCertificates') + } + + const interceptKonnect = (params?: { + mockData?: object + alias?: string + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/plugins/*`, + }, + { + statusCode: 200, + body: params?.mockData ?? plugin1, + }, + ).as(params?.alias ?? 'getPlugin') + } + + const interceptUpdate = (status = 200): void => { + cy.intercept( + { + method: 'POST', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/v1/schemas/json/plugin/validate`, + }, + { + statusCode: status, + body: { }, + }, + ).as('validatePlugin') + + cy.intercept( + { + method: 'PUT', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/plugins/*`, + }, + { + statusCode: status, + body: { ...plugin1, tags: ['tag1', 'tag2'] }, + }, + ).as('updatePlugin') + } + + it('should show create form', () => { + interceptKonnectCerts() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.get('.kong-ui-entities-plugins-form form').should('be.visible') + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // form fields + cy.getTestId('plugin-form-name').should('be.visible') + cy.getTestId('plugin-form-tags').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('be.visible') + // certs load in select + cy.getTestId('plugin-form-certificate-id').click() + cy.get('.k-select-list .k-select-item').should('have.length', certificates.data.length) + }) + + it('should correctly handle button state - create', () => { + interceptKonnectCerts() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // enables save when required fields have values + cy.getTestId('plugin-form-name').type('tk-meowstersmith') + cy.getTestId('plugin-form-certificate-id').click() + cy.get(`[data-testid="k-select-item-${certificates.data[0].id}"] button`).click() + cy.getTestId('form-submit').should('be.enabled') + // disables save when required field is cleared + cy.getTestId('plugin-form-name').clear() + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should allow exact match filtering of certs', () => { + interceptKonnectCerts() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // search + cy.getTestId('plugin-form-certificate-id').should('be.visible') + cy.getTestId('plugin-form-certificate-id').type(plugin1.certificate.id) + // click kselect item + cy.getTestId(`k-select-item-${plugin1.certificate.id}`).should('be.visible') + cy.get(`[data-testid="k-select-item-${plugin1.certificate.id}"] button`).click() + cy.getTestId('plugin-form-certificate-id').should('have.value', plugin1.certificate.id) + }) + + it('should set cert selection as readonly if provided in config - on create', () => { + interceptKonnectCerts() + + cy.mount(PluginForm, { + props: { + config: { + ...baseConfigKonnect, + certificateId: '1234-cats-beat-certs', + }, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('have.attr', 'readonly') + }) + + it('should not set cert selection as readonly if provided in config - on edit', () => { + interceptKonnectCerts() + interceptKonnect() + + cy.mount(PluginForm, { + props: { + config: { + ...baseConfigKonnect, + certificateId: '1234-cats-beat-certs', + }, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('be.visible') + cy.getTestId('plugin-form-certificate-id').should('not.have.attr', 'readonly') + }) + + it('should show edit form', () => { + interceptKonnectCerts() + interceptKonnect() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // form fields + cy.getTestId('plugin-form-name').should('have.value', plugin1.name) + plugin1.tags.forEach((tag: string) => { + cy.getTestId('plugin-form-tags').invoke('val').then((val: string) => { + expect(val).to.contain(tag) + }) + }) + cy.getTestId('plugin-form-certificate-id').should('have.value', plugin1.certificate.id) + }) + + it('should correctly handle button state - edit', () => { + interceptKonnectCerts() + interceptKonnect() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // default button state + cy.getTestId('form-cancel').should('be.visible') + cy.getTestId('form-submit').should('be.visible') + cy.getTestId('form-cancel').should('be.enabled') + cy.getTestId('form-submit').should('be.disabled') + // enables save when form has changes + cy.getTestId('plugin-form-name').type('-edited') + cy.getTestId('form-submit').should('be.enabled') + // disables save when form changes are undone + cy.getTestId('plugin-form-name').clear() + cy.getTestId('plugin-form-name').type(plugin1.name) + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should handle error state - failed to load plugin', () => { + interceptKonnectCerts() + + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/plugins/*`, + }, + { + statusCode: 404, + body: {}, + }, + ).as('getPlugin') + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + pluginId: plugin1.id, + }, + }) + + cy.wait('@getPlugin') + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + // error state is displayed + cy.getTestId('form-fetch-error').should('be.visible') + // buttons and form hidden + cy.getTestId('form-cancel').should('not.exist') + cy.getTestId('form-submit').should('not.exist') + cy.get('.kong-ui-entities-plugins-form form').should('not.exist') + }) + + it('should handle error state - failed to load certs', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/certificates*`, + }, + { + statusCode: 500, + body: {}, + }, + ).as('getCertificates') + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('form-error').should('be.visible') + }) + + it('should handle error state - invalid cert id', () => { + interceptKonnectCerts() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-plugins-form').should('be.visible') + cy.getTestId('plugin-form-certificate-id').type('xxxx') + cy.getTestId('no-search-results').should('be.visible') + cy.getTestId('invalid-certificate-message').should('exist') + }) + + it('update event should be emitted when plugin was edited', () => { + interceptKonnectCerts() + interceptKonnect() + interceptUpdate() + + cy.mount(PluginForm, { + props: { + config: baseConfigKonnect, + pluginId: plugin1.id, + onUpdate: cy.spy().as('onUpdateSpy'), + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') + + cy.wait('@getPlugin') + cy.getTestId('plugin-form-tags').clear() + cy.getTestId('plugin-form-tags').type('tag1,tag2') + + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) + + cy.wait('@validatePlugin') + cy.wait('@updatePlugin') + + cy.get('@onUpdateSpy').should('have.been.calledOnce') + }) + }) +}) diff --git a/packages/entities/entities-plugins/src/components/PluginForm.vue b/packages/entities/entities-plugins/src/components/PluginForm.vue new file mode 100644 index 0000000000..9fcf18dd77 --- /dev/null +++ b/packages/entities/entities-plugins/src/components/PluginForm.vue @@ -0,0 +1,233 @@ + + + + + diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index 78b59dfa65..657fb040e5 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -10,27 +10,7 @@ /> -
- - - - - -
-
@@ -46,8 +26,15 @@ {{ t('plugins.select.tabs.kong.description') }}

@@ -57,10 +44,10 @@ {{ t('plugins.select.tabs.custom.description') }}

- + + @@ -103,8 +90,16 @@ diff --git a/packages/entities/entities-plugins/src/constants.ts b/packages/entities/entities-plugins/src/constants.ts new file mode 100644 index 0000000000..5a86f4f285 --- /dev/null +++ b/packages/entities/entities-plugins/src/constants.ts @@ -0,0 +1 @@ +export const PLUGINS_PER_ROW = 3 diff --git a/packages/entities/entities-plugins/src/index.ts b/packages/entities/entities-plugins/src/index.ts index c7b5a5df64..cb0f154817 100644 --- a/packages/entities/entities-plugins/src/index.ts +++ b/packages/entities/entities-plugins/src/index.ts @@ -1,6 +1,7 @@ import PluginIcon from './components/PluginIcon.vue' import PluginList from './components/PluginList.vue' -import PluginForm from './components/PluginForm.vue' +import PluginSelect from './components/PluginSelect.vue' +import PluginSelectGrid from './components/PluginSelectGrid.vue' import PluginConfigCard from './components/PluginConfigCard.vue' import composables from './composables' @@ -9,7 +10,8 @@ const { getPluginIconURL, usePluginMetaData } = composables export { PluginIcon, PluginList, - PluginForm, + PluginSelect, + PluginSelectGrid, PluginConfigCard, getPluginIconURL, usePluginMetaData, diff --git a/packages/entities/entities-plugins/src/locales/en.json b/packages/entities/entities-plugins/src/locales/en.json index bbf00f2389..f3b4d0e204 100644 --- a/packages/entities/entities-plugins/src/locales/en.json +++ b/packages/entities/entities-plugins/src/locales/en.json @@ -401,6 +401,8 @@ "custom_badge_text": "Data Plane Node Specific", "unavailable_tooltip": "This plugin is not available", "misc_plugins": "Other Plugins", + "view_more": "View {count} more", + "view_less": "View less", "tabs": { "kong": { "title": "Kong Plugins", diff --git a/packages/entities/entities-plugins/src/plugins-endpoints.ts b/packages/entities/entities-plugins/src/plugins-endpoints.ts index b495a80148..c6fcfb1d44 100644 --- a/packages/entities/entities-plugins/src/plugins-endpoints.ts +++ b/packages/entities/entities-plugins/src/plugins-endpoints.ts @@ -9,6 +9,14 @@ export default { forEntity: '/{workspace}/{entityType}/{entityId}/plugins', }, }, + select: { + konnect: { + availablePlugins: '/api/runtime_groups/{controlPlaneId}/v1/available-plugins', + }, + kongManager: { + availablePlugins: '/{workspace}/kong', + }, + }, form: { konnect: { create: '/api/runtime_groups/{controlPlaneId}/plugins', diff --git a/packages/entities/entities-plugins/src/types/plugin-form.ts b/packages/entities/entities-plugins/src/types/plugin-form.ts index eb4beb2be4..6d9c4ee3c4 100644 --- a/packages/entities/entities-plugins/src/types/plugin-form.ts +++ b/packages/entities/entities-plugins/src/types/plugin-form.ts @@ -2,10 +2,10 @@ import type { RouteLocationRaw } from 'vue-router' import type { KonnectBaseFormConfig, KongManagerBaseFormConfig } from '@kong-ui-public/entities-shared' export interface BasePluginFormConfig { - /** Route for creating a plugin */ - createRoute: RouteLocationRaw - /** Consumer to bind the Plugin to on creation if Consumer Credential */ - consumerId?: string + /** A function that returns the route for creating a plugin */ + getCreateRoute: (id: string) => RouteLocationRaw + /** Entity to bind the Plugin to on creation */ + entityId?: string } /** Konnect Plugin form config */ @@ -22,7 +22,7 @@ export interface KongManagerPluginFormConfig extends BasePluginFormConfig, KongM export interface PluginFormFields { name: string tags: string - consumer_id: string + entity_id: string } export interface PluginFormState { diff --git a/packages/entities/entities-plugins/src/types/plugin.ts b/packages/entities/entities-plugins/src/types/plugin.ts index f834c4e7b4..992cb36196 100644 --- a/packages/entities/entities-plugins/src/types/plugin.ts +++ b/packages/entities/entities-plugins/src/types/plugin.ts @@ -11,6 +11,19 @@ export enum PluginGroup { CUSTOM_PLUGINS = 'Custom Plugins', } +export const PluginGroupArray = [ + PluginGroup.AUTHENTICATION, + PluginGroup.SECURITY, + PluginGroup.TRAFFIC_CONTROL, + PluginGroup.SERVERLESS, + PluginGroup.ANALYTICS_AND_MONITORING, + PluginGroup.TRANSFORMATIONS, + PluginGroup.LOGGING, + PluginGroup.DEPLOYMENT, + PluginGroup.WEBSOCKET, + PluginGroup.CUSTOM_PLUGINS, +] + export const PLUGIN_GROUPS_COLLAPSE_STATUS = { AUTHENTICATION: true, SECURITY: true, @@ -46,3 +59,15 @@ export interface PluginType extends PluginMetaData { disabledMessage?: string // An optional field for plugin's disabled message. id: string // the plugin schema name } + +export type DisabledPlugin = { + [key: string]: string // [plugin.id]: plugin.disabledMessage +} + +export type PluginCardList = { + [key in PluginGroup]?: PluginType[] +} + +export type TriggerLabels = { + [key in PluginGroup]?: string // [plugin.group]: label +} diff --git a/packages/entities/entities-shared/src/composables/useHelpers.ts b/packages/entities/entities-shared/src/composables/useHelpers.ts index ebfd40a499..8979a00552 100644 --- a/packages/entities/entities-shared/src/composables/useHelpers.ts +++ b/packages/entities/entities-shared/src/composables/useHelpers.ts @@ -10,7 +10,47 @@ export default function useHelpers() { return slotProps?.[propName] ?? undefined } + /** + * Check if 2 objects are equal + * @param {Object} a first object to compare + * @param {Object} b second object to compare + * @returns {Boolean} whether or not the objects are equal + */ + const objectsAreEqual = (a: Record, b: Record) => { + try { + return JSON.stringify(a) === JSON.stringify(b) + } catch (e) { + return false + } + } + + /** + * A comparator function that given a key, compares object values with that key, and returns the results of + * localCompare on those values (see: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare for reference) + * Also checks for undefined, nulls and sub-Arrays. + * @param {String} property the key to sort on + * @returns {Function} a comparator function + */ + const sortAlpha = (property: string) => { + return (a: Record, b: Record) => { + let propertyA = a[property] === undefined || a[property] === null ? '' : a[property] + let propertyB = b[property] === undefined || b[property] === null ? '' : b[property] + + if (Array.isArray(a[property])) { + propertyA = a[property][0] + } + + if (Array.isArray(b[property])) { + propertyB = b[property][0] + } + + return propertyA.localeCompare(propertyB) + } + } + return { getPropValue, + objectsAreEqual, + sortAlpha, } } From b83ae969bed45f7f57c0666c82c0745759219b7b Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Thu, 26 Oct 2023 17:11:17 -0400 Subject: [PATCH 04/40] fix(*): ts & lint errors --- .../DeleteCustomPluginSchemaModal.vue | 18 +- .../src/components/PluginCardSkeleton.vue | 2 +- .../src/components/PluginForm.cy.ts | 632 ------------------ .../src/components/PluginForm.vue | 27 +- .../src/components/PluginSelect.vue | 10 +- .../src/components/PluginSelectGrid.vue | 166 +++-- 6 files changed, 126 insertions(+), 729 deletions(-) delete mode 100644 packages/entities/entities-plugins/src/components/PluginForm.cy.ts diff --git a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue b/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue index 2bb7e4c824..e6e93b97ee 100644 --- a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue +++ b/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue @@ -92,6 +92,7 @@ From af36bdd88f2662b61eeeab92643f15da9057e399 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Thu, 26 Oct 2023 17:43:00 -0400 Subject: [PATCH 05/40] fix(*): custom drop 1 --- .../DeleteCustomPluginSchemaModal.vue | 75 ++++++++++++------- .../src/components/PluginCustomGrid.vue | 11 ++- .../src/components/PluginForm.vue | 4 +- .../entities-plugins/src/locales/en.json | 15 +++- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue b/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue index e6e93b97ee..11702463fd 100644 --- a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue +++ b/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue @@ -7,7 +7,8 @@ @canceled="handleCancel" > @@ -108,10 +125,11 @@ const { getMessageFromError } = useErrors() const title = computed((): string => { return isPluginSchemaInUse.value - ? t('configuration.plugins.list.deleteCustomPlugin.pluginSchemaInUseTitle', { name: props.plugin?.name }) - : `${t('actions.delete')} ${props.plugin?.name || 'plugin'}` + ? t('delete.plugin_schema_in_use_title') + : t('delete.title', { name: props.plugin?.name || t('delete.custom_plugin') }) }) +const isLoading = ref(false) const errorMessage = ref('') const customPluginNameFormValue = ref('') const isPluginSchemaInUse = ref(false) @@ -121,6 +139,9 @@ const handleCancel = (): void => { } const handleSubmit = async (): Promise => { + isLoading.value = true + errorMessage.value = '' + if (!props.plugin?.id) { return } @@ -131,21 +152,25 @@ const handleSubmit = async (): Promise => { emit('proceed') + // TODO: /* notify({ appearance: 'success', - message: i18n.t('configuration.plugins.list.deleteCustomPlugin.successMessage', { name: props.plugin?.name }), + message: t('delete.success_message', { name: props.plugin?.name || t('glossary.plugin') }), }) */ - } catch (err) { + } catch (err: any) { + // TODO: refactor + const { response } = err if ( - err.response?.status === 400 && - err.response.data?.message && - err.response.data.message.includes('plugin schema is currently in use') + response?.status === 400 && + response.data?.message && + response.data.message.includes('plugin schema is currently in use') ) { isPluginSchemaInUse.value = true - } else { errorMessage.value = getMessageFromError(err) } + } finally { + isLoading.value = false } } diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue index 03d6ed9396..67a6b214ac 100644 --- a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue @@ -1,12 +1,11 @@ + + +
{ const customPlugins = filteredPlugins.value?.[PluginGroup.CUSTOM_PLUGINS] || [] // ADD CUSTOM_PLUGIN_CREATE as the first card if allowed creation - return userCanCreate.value && !props.noRouteChange + return userCanCreate.value && !props.noRouteChange && props.config.createCustomRoute ? [{ id: 'custom-plugin-create', name: t('plugins.select.tabs.custom.create.name'), diff --git a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue index d584ce45a6..baefc61b2e 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue @@ -179,25 +179,8 @@ const emitPluginData = (plugin: PluginType) => { } // used for scoped plugins -// entityType is currently determined off of the route query or params -const entityType = computed((): string => { - const entity = String(route.query.entity_type || '') - - if (entity) { - return entity - } else if (route.params.gateway_service) { - return 'service_id' - } else if (route.params.route) { - return 'route_id' - } else if (route.params.consumer) { - return 'consumer_id' - } else if (route.params.consumer_group) { - return 'consumer_group_id' - } - - // global - return '' -}) +// entityType is currently determined off of the route query +const entityType = computed((): string => String(route.query.entity_type || '')) // remove custom plugin from original filteredPlugins const nonCustomPlugins = computed((): PluginCardList => { diff --git a/packages/entities/entities-plugins/src/types/plugin-form.ts b/packages/entities/entities-plugins/src/types/plugin-form.ts index 6d9c4ee3c4..c5637b5465 100644 --- a/packages/entities/entities-plugins/src/types/plugin-form.ts +++ b/packages/entities/entities-plugins/src/types/plugin-form.ts @@ -11,9 +11,9 @@ export interface BasePluginFormConfig { /** Konnect Plugin form config */ export interface KonnectPluginFormConfig extends BasePluginFormConfig, KonnectBaseFormConfig { /** Route for creating a custom plugin */ - createCustomRoute: RouteLocationRaw + createCustomRoute?: RouteLocationRaw /** A function that returns the route for editing a custom plugin */ - getCustomEditRoute: (id: string) => RouteLocationRaw + getCustomEditRoute?: (id: string) => RouteLocationRaw } /** Kong Manager Plugin form config */ From 025f7499173849f0fed0f56fce333a1264a50e71 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 11:14:56 -0400 Subject: [PATCH 09/40] fix(*): onlyAvailablePlugins --- .../src/components/PluginSelect.vue | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index f77e7df1ec..b19c92951d 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -33,7 +33,7 @@ :filtered-plugins="filteredPlugins" :ignored-plugins="ignoredPlugins" :no-route-change="noRouteChange" - only-available-plugins + :only-available-plugins="onlyAvailablePlugins" @loading="(val: boolean) => $emit('loading', val)" @plugin-clicked="(val: PluginType) => $emit('plugin-clicked', val)" @plugin-list-updated="(val: PluginCardList) => pluginsList = val" @@ -87,7 +87,7 @@ :filtered-plugins="filteredPlugins" :ignored-plugins="ignoredPlugins" :no-route-change="noRouteChange" - only-available-plugins + :only-available-plugins="onlyAvailablePlugins" @loading="(val: boolean) => $emit('loading', val)" @plugin-clicked="(val: PluginType) => $emit('plugin-clicked', val)" @plugin-list-updated="(val: PluginCardList) => pluginsList = val" @@ -157,6 +157,15 @@ const props = defineProps({ type: Boolean, default: false, }, + /** + * @param {boolean} onlyAvailablePlugins checks kong config plugins.available_on_server and if + * onlyAvailablePlugins = true, then it will not show plugins from PluginMeta that are outside + * of the available_on_server array. + */ + onlyAvailablePlugins: { + type: Boolean, + default: false, + }, ignoredPlugins: { type: Array as PropType, default: () => [], From 39e3fbf1125f112addb744b0c9710c5d5a1ac8f7 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 11:19:41 -0400 Subject: [PATCH 10/40] fix(*): example updates --- .../sandbox/pages/PluginSelectPage.vue | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue b/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue index 81ff226baa..685d8c0623 100644 --- a/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue +++ b/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue @@ -12,9 +12,11 @@ From bfdb75fa2808dbd446b7293b4da8e4c8de6e25fd Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 11:32:40 -0400 Subject: [PATCH 11/40] fix(plugins): build error --- .../entities-plugins/src/components/PluginSelectCard.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue index b1e32a6bef..05545a7f32 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue @@ -183,17 +183,17 @@ const handleCustomDelete = (): void => { } const handleCustomEdit = (pluginName: string): void => { - if (props.config.app === 'konnect') { + if (props.config.app === 'konnect' && props.config.getCustomEditRoute) { router.push(props.config.getCustomEditRoute(pluginName)) } } -// TODO: shouldn't this be router.push? const handleCustomClick = (): void => { + // TODO: verify // handle custom plugin card click only // regular plugin card render as 'router-link' component which don't need this - if (isCustomPlugin.value) { - emit('custom-plugin-create') + if (props.config.app === 'konnect' && props.config.createCustomRoute && isCustomPlugin.value) { + router.push(props.config.createCustomRoute) } } From 02a08ec9c7c140192fce698bc66746cff80bf2c3 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 11:37:06 -0400 Subject: [PATCH 12/40] fix(*): cleanup --- .../entities-plugins/src/components/PluginSelectCard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue index 05545a7f32..db3e5a346f 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue @@ -166,7 +166,7 @@ const handleCreateClick = (pluginName: string): void => { router.push(props.config.getCreateRoute(pluginName)) } -const emitPluginData = () => { +const emitPluginData = (): void => { emit('plugin-clicked', props.plugin) } From 99bfff1dab2dde2b04690ab2bae7b3281668306b Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 12:56:11 -0400 Subject: [PATCH 13/40] fix(*): display logic --- .../src/components/PluginSelectGrid.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue index baefc61b2e..8caf6eb4b4 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue @@ -197,7 +197,7 @@ const buildPluginList = (): PluginCardList => { // either grab all plugins from metadata file or use list of available plugins provided by API return [...new Set( Object.assign( - Object.keys({ ...(!props.onlyAvailablePlugins && pluginMetaData) }), + Object.keys({ ...(!props.onlyAvailablePlugins ? pluginMetaData : {}) }), availablePlugins.value, ), )] @@ -269,6 +269,8 @@ const buildPluginList = (): PluginCardList => { plugins.push(plugin) plugins.sort(sortAlpha('name')) + list[groupName] = plugins + return list }, {}) } @@ -294,11 +296,11 @@ const shouldCollapsed = ref>(PLUGIN_GROUPS_COLLAPSE_STAT // text for plugin group "view x more" label const triggerLabels = computed(() => { return Object.keys(pluginsList.value).reduce((acc: TriggerLabels, pluginGroup: string): TriggerLabels => { - const totalCount = getPluginCards(pluginGroup, 'all').length - const hiddenCount = getPluginCards(pluginGroup, 'hidden').length + const totalCount = getPluginCards(pluginGroup, 'all')?.length || 0 + const hiddenCount = getPluginCards(pluginGroup, 'hidden')?.length || 0 if (totalCount > PLUGINS_PER_ROW) { - acc[pluginGroup as keyof TriggerLabels] = t('plugins.select.view_more', { hiddenCount }) + acc[pluginGroup as keyof TriggerLabels] = t('plugins.select.view_more', { count: hiddenCount }) } return acc From 3c125a291300c71e7224a59d3ebd33df45b96cb3 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 12:58:24 -0400 Subject: [PATCH 14/40] fix(*): fetch condition --- .../src/components/PluginSelectGrid.vue | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue index 8caf6eb4b4..264ac7b815 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue @@ -344,28 +344,22 @@ onMounted(async () => { isLoading.value = true emit('loading', isLoading.value) - // only fetch available plugins if needed - if (props.onlyAvailablePlugins) { - try { - const res = await axiosInstance.get(availablePluginsUrl.value) - - // TODO: endpoints temporarily return different formats - if (props.config.app === 'konnect') { - const { names: available } = res.data - availablePlugins.value = available || [] - } else if (props.config.app === 'kongManager') { - const { plugins: { available_on_server: aPlugins } } = res.data - availablePlugins.value = aPlugins ? Object.keys(aPlugins) : [] - } - - pluginsList.value = buildPluginList() - emit('plugin-list-updated', pluginsList.value) - } catch (error: any) { - fetchErrorMessage.value = getMessageFromError(error) + try { + const res = await axiosInstance.get(availablePluginsUrl.value) + + // TODO: endpoints temporarily return different formats + if (props.config.app === 'konnect') { + const { names: available } = res.data + availablePlugins.value = available || [] + } else if (props.config.app === 'kongManager') { + const { plugins: { available_on_server: aPlugins } } = res.data + availablePlugins.value = aPlugins ? Object.keys(aPlugins) : [] } - } else { + pluginsList.value = buildPluginList() emit('plugin-list-updated', pluginsList.value) + } catch (error: any) { + fetchErrorMessage.value = getMessageFromError(error) } isLoading.value = false From 2bd4f355a9e0b852f793dfdbf59f085c97e63589 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 13:23:54 -0400 Subject: [PATCH 15/40] fix(*): customize visible plugins --- .../src/components/PluginCustomGrid.vue | 2 +- .../src/components/PluginSelect.vue | 9 +++++++++ .../src/components/PluginSelectGrid.vue | 17 +++++++++++------ .../entities/entities-plugins/src/constants.ts | 1 - 4 files changed, 21 insertions(+), 8 deletions(-) delete mode 100644 packages/entities/entities-plugins/src/constants.ts diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue index a27b3b2d9b..150223b635 100644 --- a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue @@ -7,7 +7,7 @@ :trigger-label="shouldCollapsedCustomPlugins ? triggerLabels[PLUGIN_GROUPS.CUSTOM_PLUGINS] : pluginHelpText.viewLess" > @@ -82,47 +101,32 @@ - - - - diff --git a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue index db3e5a346f..8867a03593 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue @@ -6,13 +6,13 @@ }" :label="plugin.disabledMessage" position-fixed - @click="noRouteChange ? emitPluginData() : handleCreateClick(plugin.id)" > @@ -116,12 +113,10 @@ import { useRouter } from 'vue-router' import { PluginGroup, type KongManagerPluginFormConfig, type KonnectPluginFormConfig, type PluginType } from '../types' import { KUI_ICON_SIZE_30, KUI_COLOR_TEXT_NEUTRAL_STRONGER } from '@kong/design-tokens' import composables from '../composables' -import { PermissionsWrapper } from '@kong-ui-public/entities-shared' import PluginIcon from './PluginIcon.vue' const emit = defineEmits<{ (e: 'plugin-clicked', plugin: PluginType) : void, - (e: 'custom-plugin-create'): void, (e: 'custom-plugin-delete'): void, }>() @@ -136,17 +131,19 @@ const props = defineProps({ return true }, }, - /** A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can delete a given entity */ + /** + * Whether or not user has rights to delete custom plugins + */ canDeleteCustom: { - type: Function as PropType<() => boolean | Promise>, - required: false, - default: async () => true, + type: Boolean, + default: false, }, - /** A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can edit a given entity */ + /** + * Whether or not user has rights to edit custom plugins + */ canEditCustom: { - type: Function as PropType<() => boolean | Promise>, - required: false, - default: async () => true, + type: Boolean, + default: false, }, plugin: { type: Object as PropType, @@ -162,8 +159,8 @@ const router = useRouter() const { i18n: { t } } = composables.useI18n() const controlPlaneId = computed((): string => props.config.app === 'konnect' ? props.config.controlPlaneId : '') -const handleCreateClick = (pluginName: string): void => { - router.push(props.config.getCreateRoute(pluginName)) +const handleCreateClick = (): void => { + router.push(props.config.getCreateRoute(props.plugin.id)) } const emitPluginData = (): void => { @@ -191,9 +188,12 @@ const handleCustomEdit = (pluginName: string): void => { const handleCustomClick = (): void => { // TODO: verify // handle custom plugin card click only - // regular plugin card render as 'router-link' component which don't need this - if (props.config.app === 'konnect' && props.config.createCustomRoute && isCustomPlugin.value) { - router.push(props.config.createCustomRoute) + if (props.config.app === 'konnect') { + if (isCreateCustomPlugin.value && props.config.createCustomRoute) { + router.push(props.config.createCustomRoute) + } else if (isCustomPlugin.value) { + handleCreateClick() + } } } @@ -210,7 +210,7 @@ const handleCustomClick = (): void => { cursor: pointer; } - .header-wrapper { // TODO: had to change from 2rem, does it work? + .header-wrapper { // maintain the specified height if slot has no content min-height: 25px; } @@ -232,6 +232,13 @@ const handleCustomClick = (): void => { font-weight: $kui-font-weight-regular; } + // TODO: do I still need this? + :deep(.k-card-body) { + display: flex; + flex: 1; + flex-direction: column; + } + &-body { background-color: $kui-color-background; flex: 1; diff --git a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue index 8881f8ea25..2169efae72 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectGrid.vue @@ -1,32 +1,23 @@ From 5267dbf3d4fcccbf73b53168d6c1aa60651df8e6 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 19:53:15 -0400 Subject: [PATCH 21/40] fix(*): docs --- .../entities-plugins/docs/plugin-select.md | 83 +++++++++++++++++++ .../src/components/PluginSelect.vue | 5 +- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/entities/entities-plugins/docs/plugin-select.md b/packages/entities/entities-plugins/docs/plugin-select.md index b0adfb828e..7ac8d652e2 100644 --- a/packages/entities/entities-plugins/docs/plugin-select.md +++ b/packages/entities/entities-plugins/docs/plugin-select.md @@ -55,6 +55,18 @@ A grid component for selecting Plugins. - default: `undefined` - A function that returns the route for creating a specific plugin type. + - `createCustomRoute`: + - type: RouteLocationRaw + - required: `false` + - default: `undefined` + - The route for creating a custom plugin. + + - `getCustomEditRoute`: + - type: `(plugin: string) => RouteLocationRaw` + - required: `false` + - default: `undefined` + - A function that returns the route for editing a custom plugin. + - `workspace`: - type: `string` - required: `true` @@ -81,6 +93,77 @@ A grid component for selecting Plugins. The base konnect or kongManger config. +#### `canCreate` + +- type: `Function as PropType<() => boolean | Promise>` +- required: `false` +- default: `async () => true` + +A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can create a new plugin. + +#### `canDeleteCustom` + +- type: `Function as PropType<(row: object) => boolean | Promise>` +- required: `false` +- default: `async () => true` + +A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can delete a given custom plugin. + +#### `canEditCustom` + +- type: `Function as PropType<(row: object) => boolean | Promise>` +- required: `false` +- default: `async () => true` + +A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can edit a given custom plugin. + +#### `noRouteChange` + +- type: `boolean` +- required: `false` +- default: `false` + +If true, let consuming component handle event when clicking on a plugin. Used in conjunction with `@plugin-clicked` event. + +#### `onlyAvailablePlugins` + +- type: `boolean` +- required: `false` +- default: `false` + +Checks the kong config plugins.available_on_server and if true, then it will not show plugins from PluginMeta that are outside of the available_on_server array. + +#### `ignoredPlugins` + +- type: `string[]` +- required: `false` +- default: '[]' + +An array of the plugin names. These are plugins that should not be displayed. + +#### `disabledPlugins` + +- type: `DisabledPlugin` +- required: `false` +- default: `{}` + +Plugins that should be disabled and their disabled messages. +Example: + +```json +{ + 'acl': 'ACL is not supported for this entity type', +} +``` + +#### `pluginsPerRow` + +- type: `number` +- required: `false` +- default: `4` + +Number of plugins to always have visible (never will be collapsed). + ### Events #### plugin-clicked diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index dd7ad293e4..7ab141b3ca 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -184,7 +184,10 @@ const props = defineProps({ default: () => [], }, /** - * Plugins that should be disabled and their disabled messages + * Plugins that should be disabled and their disabled messages. + * ex. { + * 'acl': 'ACL is not supported for this entity type', + * } */ disabledPlugins: { type: Object as PropType, From dec4e2527b3870f12e44c65d6326f46185d0ccf3 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Fri, 27 Oct 2023 20:07:08 -0400 Subject: [PATCH 22/40] fix(*): custom create --- .../entities-plugins/docs/plugin-select.md | 4 ++-- .../src/components/PluginCustomGrid.vue | 4 ++-- .../src/components/PluginSelect.vue | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/entities/entities-plugins/docs/plugin-select.md b/packages/entities/entities-plugins/docs/plugin-select.md index 7ac8d652e2..ff78d9e980 100644 --- a/packages/entities/entities-plugins/docs/plugin-select.md +++ b/packages/entities/entities-plugins/docs/plugin-select.md @@ -93,13 +93,13 @@ A grid component for selecting Plugins. The base konnect or kongManger config. -#### `canCreate` +#### `canCreateCustom` - type: `Function as PropType<() => boolean | Promise>` - required: `false` - default: `async () => true` -A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can create a new plugin. +A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can create a new custom plugin. #### `canDeleteCustom` diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue index 3bc148cb3e..0c0ee94928 100644 --- a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue @@ -102,7 +102,7 @@ const props = defineProps({ /** * Whether or not user has rights to create custom plugins */ - canCreate: { + canCreateCustom: { type: Boolean, default: false, }, @@ -164,7 +164,7 @@ const modifiedCustomPlugins = computed((): PluginType[] => { const customPlugins: PluginType[] = JSON.parse(JSON.stringify(props.pluginList))[PluginGroup.CUSTOM_PLUGINS] || [] // ADD CUSTOM_PLUGIN_CREATE as the first card if allowed creation - return props.canCreate && !props.noRouteChange && props.config.createCustomRoute + return props.canCreateCustom && !props.noRouteChange && props.config.createCustomRoute ? [{ id: 'custom-plugin-create', name: t('plugins.select.tabs.custom.create.name'), diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index 7ab141b3ca..a8e20e9dea 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -84,7 +84,7 @@

boolean | Promise>, required: false, default: async () => true, }, - /** A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can delete a given entity */ + /** A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can delete a custom plugin */ canDeleteCustom: { type: Function as PropType<() => boolean | Promise>, required: false, default: async () => true, }, - /** A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can edit a given entity */ + /** A synchronous or asynchronous function, that returns a boolean, that evaluates if the user can edit custom plugin */ canEditCustom: { type: Function as PropType<() => boolean | Promise>, required: false, @@ -405,12 +405,12 @@ watch(() => props.ignoredPlugins, (val, oldVal) => { } }) -const userCanCreate = ref(false) +const userCanCreateCustom = ref(false) const userCanEditCustom = ref(false) const userCanDeleteCustom = ref(false) onBeforeMount(async () => { // Evaluate the user permissions - userCanCreate.value = await props.canCreate() + userCanCreateCustom.value = await props.canCreateCustom() userCanEditCustom.value = await props.canEditCustom() userCanDeleteCustom.value = await props.canDeleteCustom() }) From 76aa43a36c87a1d5261e99570c30bd92d2ff6135 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Mon, 30 Oct 2023 12:51:21 -0400 Subject: [PATCH 23/40] fix(*): custom delete modal --- .../sandbox/pages/PluginSelectPage.vue | 10 +- .../DeleteCustomPluginSchemaModal.vue | 146 ++++++------------ .../src/components/PluginCustomGrid.vue | 3 + .../src/components/PluginSelect.vue | 2 + .../src/components/PluginSelectCard.vue | 2 +- .../src/components/PluginSelectGrid.vue | 1 - .../entities-plugins/src/locales/en.json | 1 + 7 files changed, 60 insertions(+), 105 deletions(-) diff --git a/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue b/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue index e7c3e21d1a..93f14486a6 100644 --- a/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue +++ b/packages/entities/entities-plugins/sandbox/pages/PluginSelectPage.vue @@ -3,6 +3,7 @@

Konnect API

Kong Manager API

@@ -25,8 +26,8 @@ const konnectConfig = ref({ // Set the root `.env.development.local` variable to a control plane your PAT can access controlPlaneId, // force the scope - entityType: 'services', - entityId: '6f1ef200-d3d4-4979-9376-726f2216d90c', + // entityType: 'services', + // entityId: '6f1ef200-d3d4-4979-9376-726f2216d90c', getCreateRoute: (plugin: string) => ({ name: 'create-plugin', params: { @@ -56,11 +57,14 @@ const kongManagerConfig = ref({ name: 'create-plugin', params: { control_plane_id: controlPlaneId.value, - // TODO: is this right for KM? plugin, }, }), }) + +const handleDeleteSuccess = (): void => { + console.log('Custom plugin deleted') +} diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue index 143c6b31a3..bc8301b25d 100644 --- a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue @@ -251,7 +251,6 @@ const handleClose = (revalidate?: boolean): void => { margin-top: $kui-space-90; row-gap: $kui-space-90; - // TODO: do I still need this? :deep(.kong-card) { display: flex; flex: 1 0 0; diff --git a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue index 225c22dc74..81d1f20c9c 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelectCard.vue @@ -186,7 +186,6 @@ const handleCustomEdit = (pluginName: string): void => { } const handleCustomClick = (): void => { - // TODO: verify // handle custom plugin card click only if (props.config.app === 'konnect') { if (isCreateCustomPlugin.value && props.config.createCustomRoute) { @@ -232,7 +231,6 @@ const handleCustomClick = (): void => { font-weight: $kui-font-weight-regular; } - // TODO: do I still need this? :deep(.k-card-body) { display: flex; flex: 1; From 62a2b6c1401becbd10fdee26202a3a408620fd38 Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Mon, 30 Oct 2023 13:35:12 -0400 Subject: [PATCH 26/40] fix(*): custom delete modal docs --- packages/entities/entities-plugins/docs/plugin-select.md | 4 ++++ .../entities-plugins/src/components/PluginCustomGrid.vue | 4 ++-- .../entities/entities-plugins/src/components/PluginSelect.vue | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/entities/entities-plugins/docs/plugin-select.md b/packages/entities/entities-plugins/docs/plugin-select.md index ff78d9e980..ca30d8a90f 100644 --- a/packages/entities/entities-plugins/docs/plugin-select.md +++ b/packages/entities/entities-plugins/docs/plugin-select.md @@ -174,6 +174,10 @@ An `@plugin-clicked` event is emitted when a plugin in the selection grid is cli A `@loading` event is emitted when loading state changes. The event payload is a boolean. +#### delete-custom:success + +A `@delete-custom:success` event is emitted when custom plugin is successfully deleted. The event payload is the deleted plugin's name. + ### Usage example Please refer to the [sandbox](../sandbox/pages/PluginListPage.vue). The form is accessible by clicking the `+ New Plugin` button. diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue index bc8301b25d..c192440934 100644 --- a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue +++ b/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue @@ -148,7 +148,7 @@ const props = defineProps({ const emit = defineEmits<{ (e: 'plugin-clicked', plugin: PluginType): void, (e: 'revalidate'): void, - (e: 'delete:success'): void, + (e: 'delete:success', pluginName: string): void, }>() const { i18n: { t } } = composables.useI18n() @@ -213,7 +213,7 @@ const handleCustomPluginDelete = (plugin: PluginType): void => { const handleClose = (revalidate?: boolean): void => { if (revalidate) { emit('revalidate') - emit('delete:success') + emit('delete:success', selectedPlugin.value?.name || '') } openDeleteModal.value = false diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index 86d343c726..e36c3d1068 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -91,7 +91,7 @@ :no-route-change="noRouteChange" :plugin-list="filteredPlugins" :plugins-per-row="pluginsPerRow" - @delete:success="$emit('delete-custom:success')" + @delete:success="(name: string) => $emit('delete-custom:success', name)" @plugin-clicked="(val: PluginType) => $emit('plugin-clicked', val)" @revalidate="() => pluginsList = buildPluginList()" /> @@ -206,7 +206,7 @@ const props = defineProps({ const emit = defineEmits<{ (e: 'loading', isLoading: boolean): void, (e: 'plugin-clicked', plugin: PluginType): void, - (e: 'delete-custom:success'): void, + (e: 'delete-custom:success', pluginName: string): void, }>() const route = useRoute() From 353e116841b9fb420784f210cb141060c712cc0b Mon Sep 17 00:00:00 2001 From: "kai.arrowood" Date: Mon, 30 Oct 2023 13:40:34 -0400 Subject: [PATCH 27/40] fix(*): organize files --- .../entities-plugins/src/components/PluginSelect.vue | 6 +++--- .../DeleteCustomPluginSchemaModal.vue | 4 ++-- .../{ => custom-plugins}/PluginCustomGrid.vue | 6 +++--- .../components/{ => select}/PluginCardSkeleton.vue | 0 .../src/components/{ => select}/PluginSelectCard.vue | 11 ++++++++--- .../src/components/{ => select}/PluginSelectGrid.vue | 4 ++-- 6 files changed, 18 insertions(+), 13 deletions(-) rename packages/entities/entities-plugins/src/components/{ => custom-plugins}/DeleteCustomPluginSchemaModal.vue (98%) rename packages/entities/entities-plugins/src/components/{ => custom-plugins}/PluginCustomGrid.vue (98%) rename packages/entities/entities-plugins/src/components/{ => select}/PluginCardSkeleton.vue (100%) rename packages/entities/entities-plugins/src/components/{ => select}/PluginSelectCard.vue (97%) rename packages/entities/entities-plugins/src/components/{ => select}/PluginSelectGrid.vue (98%) diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index e36c3d1068..d3b9a417c1 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -127,9 +127,9 @@ import { import { useAxios, useHelpers, useErrors } from '@kong-ui-public/entities-shared' import composables from '../composables' import endpoints from '../plugins-endpoints' -import PluginCardSkeleton from './PluginCardSkeleton.vue' -import PluginCustomGrid from './PluginCustomGrid.vue' -import PluginSelectGrid from './PluginSelectGrid.vue' +import PluginCardSkeleton from './select/PluginCardSkeleton.vue' +import PluginCustomGrid from './custom-plugins/PluginCustomGrid.vue' +import PluginSelectGrid from './select/PluginSelectGrid.vue' const props = defineProps({ /** The base konnect or kongManger config. Pass additional config props in the shared entity component as needed. */ diff --git a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue b/packages/entities/entities-plugins/src/components/custom-plugins/DeleteCustomPluginSchemaModal.vue similarity index 98% rename from packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue rename to packages/entities/entities-plugins/src/components/custom-plugins/DeleteCustomPluginSchemaModal.vue index 128b14b5ee..1558ff7832 100644 --- a/packages/entities/entities-plugins/src/components/DeleteCustomPluginSchemaModal.vue +++ b/packages/entities/entities-plugins/src/components/custom-plugins/DeleteCustomPluginSchemaModal.vue @@ -57,8 +57,8 @@ import { ref, computed, type PropType } from 'vue' import { type KongManagerPluginFormConfig, type KonnectPluginFormConfig, -} from '../types' -import composables from '../composables' +} from '../../types' +import composables from '../../composables' import { useAxios, useErrors, EntityTypes, EntityDeleteModal } from '@kong-ui-public/entities-shared' const props = defineProps({ diff --git a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue b/packages/entities/entities-plugins/src/components/custom-plugins/PluginCustomGrid.vue similarity index 98% rename from packages/entities/entities-plugins/src/components/PluginCustomGrid.vue rename to packages/entities/entities-plugins/src/components/custom-plugins/PluginCustomGrid.vue index c192440934..f0f133a504 100644 --- a/packages/entities/entities-plugins/src/components/PluginCustomGrid.vue +++ b/packages/entities/entities-plugins/src/components/custom-plugins/PluginCustomGrid.vue @@ -84,9 +84,9 @@ import { type KonnectPluginFormConfig, type PluginType, type PluginCardList, -} from '../types' -import composables from '../composables' -import PluginSelectCard from './PluginSelectCard.vue' +} from '../../types' +import composables from '../../composables' +import PluginSelectCard from '../select/PluginSelectCard.vue' import DeleteCustomPluginSchemaModal from './DeleteCustomPluginSchemaModal.vue' const props = defineProps({ diff --git a/packages/entities/entities-plugins/src/components/PluginCardSkeleton.vue b/packages/entities/entities-plugins/src/components/select/PluginCardSkeleton.vue similarity index 100% rename from packages/entities/entities-plugins/src/components/PluginCardSkeleton.vue rename to packages/entities/entities-plugins/src/components/select/PluginCardSkeleton.vue diff --git a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue b/packages/entities/entities-plugins/src/components/select/PluginSelectCard.vue similarity index 97% rename from packages/entities/entities-plugins/src/components/PluginSelectCard.vue rename to packages/entities/entities-plugins/src/components/select/PluginSelectCard.vue index 81d1f20c9c..76cf726351 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelectCard.vue +++ b/packages/entities/entities-plugins/src/components/select/PluginSelectCard.vue @@ -110,10 +110,15 @@ diff --git a/packages/entities/entities-plugins/src/components/PluginSelect.vue b/packages/entities/entities-plugins/src/components/PluginSelect.vue index d3b9a417c1..3e36bae05b 100644 --- a/packages/entities/entities-plugins/src/components/PluginSelect.vue +++ b/packages/entities/entities-plugins/src/components/PluginSelect.vue @@ -65,20 +65,22 @@ @changed="(hash: string) => $router.replace({ hash })" >