From 1bf4f729dea8f5880e1fe265003495ca08bf530f Mon Sep 17 00:00:00 2001 From: Yi S Date: Thu, 10 Aug 2023 15:07:45 +0800 Subject: [PATCH] feat(entities-key-sets): migrate `entities-key-sets` from `shared-ui-components` (#686) * feat(entities-key-sets): copy & paste from the private repo ... and remove `CHANGELOG.md` * chore(entites-key-sets): go through checklist * chore(entities-key-sets): rename local dependencies in entities-key-sets * chore(entities-key-sets): correct --- packages/entities/entities-key-sets/LICENSE | 201 +++++ packages/entities/entities-key-sets/README.md | 51 ++ .../docs/key-set-config-card.md | 112 +++ .../entities-key-sets/docs/key-set-form.md | 107 +++ .../entities-key-sets/docs/key-set-list.md | 162 ++++ .../entities-key-sets/fixtures/mockData.ts | 47 ++ .../entities/entities-key-sets/package.json | 73 ++ .../entities-key-sets/sandbox/App.vue | 21 + .../entities-key-sets/sandbox/index.html | 27 + .../entities-key-sets/sandbox/index.ts | 55 ++ .../sandbox/pages/KeySetConfigCardPage.vue | 55 ++ .../sandbox/pages/KeySetFormPage.vue | 55 ++ .../sandbox/pages/KeySetListPage.vue | 95 +++ .../entities-key-sets/sandbox/tsconfig.json | 18 + .../src/components/KeySetConfigCard.vue | 71 ++ .../src/components/KeySetForm.cy.ts | 377 +++++++++ .../src/components/KeySetForm.vue | 207 +++++ .../src/components/KeySetList.cy.ts | 767 ++++++++++++++++++ .../src/components/KeySetList.vue | 463 +++++++++++ .../src/composables/index.ts | 6 + .../src/composables/useI18n.ts | 11 + .../src/global-components.d.ts | 2 + .../entities/entities-key-sets/src/index.ts | 7 + .../src/key-sets-endpoints.ts | 16 + .../entities-key-sets/src/locales/en.json | 64 ++ .../entities-key-sets/src/types/index.ts | 6 + .../src/types/key-set-config-card.ts | 16 + .../src/types/key-set-form.ts | 28 + .../src/types/key-set-list.ts | 36 + .../entities-key-sets/tsconfig.build.json | 12 + .../entities/entities-key-sets/tsconfig.json | 23 + .../entities/entities-key-sets/vite.config.ts | 35 + pnpm-lock.yaml | 58 +- 33 files changed, 3253 insertions(+), 31 deletions(-) create mode 100644 packages/entities/entities-key-sets/LICENSE create mode 100644 packages/entities/entities-key-sets/README.md create mode 100644 packages/entities/entities-key-sets/docs/key-set-config-card.md create mode 100644 packages/entities/entities-key-sets/docs/key-set-form.md create mode 100644 packages/entities/entities-key-sets/docs/key-set-list.md create mode 100644 packages/entities/entities-key-sets/fixtures/mockData.ts create mode 100644 packages/entities/entities-key-sets/package.json create mode 100644 packages/entities/entities-key-sets/sandbox/App.vue create mode 100644 packages/entities/entities-key-sets/sandbox/index.html create mode 100644 packages/entities/entities-key-sets/sandbox/index.ts create mode 100644 packages/entities/entities-key-sets/sandbox/pages/KeySetConfigCardPage.vue create mode 100644 packages/entities/entities-key-sets/sandbox/pages/KeySetFormPage.vue create mode 100644 packages/entities/entities-key-sets/sandbox/pages/KeySetListPage.vue create mode 100644 packages/entities/entities-key-sets/sandbox/tsconfig.json create mode 100644 packages/entities/entities-key-sets/src/components/KeySetConfigCard.vue create mode 100644 packages/entities/entities-key-sets/src/components/KeySetForm.cy.ts create mode 100644 packages/entities/entities-key-sets/src/components/KeySetForm.vue create mode 100644 packages/entities/entities-key-sets/src/components/KeySetList.cy.ts create mode 100644 packages/entities/entities-key-sets/src/components/KeySetList.vue create mode 100644 packages/entities/entities-key-sets/src/composables/index.ts create mode 100644 packages/entities/entities-key-sets/src/composables/useI18n.ts create mode 100644 packages/entities/entities-key-sets/src/global-components.d.ts create mode 100644 packages/entities/entities-key-sets/src/index.ts create mode 100644 packages/entities/entities-key-sets/src/key-sets-endpoints.ts create mode 100644 packages/entities/entities-key-sets/src/locales/en.json create mode 100644 packages/entities/entities-key-sets/src/types/index.ts create mode 100644 packages/entities/entities-key-sets/src/types/key-set-config-card.ts create mode 100644 packages/entities/entities-key-sets/src/types/key-set-form.ts create mode 100644 packages/entities/entities-key-sets/src/types/key-set-list.ts create mode 100644 packages/entities/entities-key-sets/tsconfig.build.json create mode 100644 packages/entities/entities-key-sets/tsconfig.json create mode 100644 packages/entities/entities-key-sets/vite.config.ts diff --git a/packages/entities/entities-key-sets/LICENSE b/packages/entities/entities-key-sets/LICENSE new file mode 100644 index 0000000000..47c89644eb --- /dev/null +++ b/packages/entities/entities-key-sets/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Kong, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/entities/entities-key-sets/README.md b/packages/entities/entities-key-sets/README.md new file mode 100644 index 0000000000..cbe4868d57 --- /dev/null +++ b/packages/entities/entities-key-sets/README.md @@ -0,0 +1,51 @@ +# @kong-ui-public/entities-key-sets + +Key set entity components. + +- [Requirements](#requirements) +- [Included components](#included-components) +- [Usage](#usage) + - [Install](#install) + - [Registration](#registration) +- [Individual component documentation](#individual-component-documentation) + +## 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/copy-uuid` must be available 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. +- `@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 + +## Included components + +- `KeySetList` +- `KeySetForm` +- `KeySetConfigCard` + +Reference the [individual component docs](#individual-component-documentation) for more info. + +## Usage + +### Install + +Install the component in your host application + +```sh +yarn add @kong-ui-public/entities-key-sets +``` + +### Registration + +Import the component(s) in your host application as well as the package styles + +```ts +import { KeySetList, KeySetForm, KeySetConfigCard } from '@kong-ui-public/entities-key-sets' +import '@kong-ui-public/entities-key-sets/dist/style.css' +``` + +## Individual component documentation + +- [``](docs/key-set-list.md) +- [``](docs/key-set-form.md) +- [``](docs/key-set-config-card.md) diff --git a/packages/entities/entities-key-sets/docs/key-set-config-card.md b/packages/entities/entities-key-sets/docs/key-set-config-card.md new file mode 100644 index 0000000000..f3f7e764a6 --- /dev/null +++ b/packages/entities/entities-key-sets/docs/key-set-config-card.md @@ -0,0 +1,112 @@ +# KeySetsConfigCard.vue + +A config card component for Key Sets. + +- [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-key-sets` 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. + + - `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. + + - `entityId`: + - type: `string` + - required: `true` + - default: `''` + - The ID of the Key Sets to display the config for + +The base konnect or kongManger config. + +#### `hideTitle` + +- type: `Boolean` +- required: `false` +- default: `false` + +Set this value to `true` to hide the card title. + +### Events + +#### fetch:error + +An `@fetch:error` event is emitted when the component fails to fetch the Key Sets. The event payload is the response error. + +#### fetch:success + +A `@fetch:success` event is emitted when the Key Sets is successfully fetched. The event payload is the Key Sets object. + +#### loading + +A `@loading` event is emitted when loading state changes. The event payload is a boolean. + +#### copy:success + +A `@copy:success` event is emitted when a user clicks the `Copy JSON` button and the JSON object is copied successfully. + +### Usage example + +Please refer to the [sandbox](../sandbox/pages/KeySetConfigCardPage.vue). The page is accessible by clicking on the row or `View details` button of an existing Ke Sets. + +## TypeScript interfaces + +TypeScript interfaces [are available here](https://github.com/Kong/public-ui-components/blob/main/packages/entities/entities-key-sets/src/types/key-set-config-card.ts) and can be directly imported into your host application. The following type interfaces are available for import: + +```ts +import type { + KeySetConfigurationSchema, + KonnectKeySetEntityConfig, + KongManagerKeySetEntityConfig, +} from '@kong-ui-public/entities-key-sets' +``` diff --git a/packages/entities/entities-key-sets/docs/key-set-form.md b/packages/entities/entities-key-sets/docs/key-set-form.md new file mode 100644 index 0000000000..79593865f1 --- /dev/null +++ b/packages/entities/entities-key-sets/docs/key-set-form.md @@ -0,0 +1,107 @@ +# KeySetForm.vue + +A form component for Key Sets. + +- [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-key-sets` 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 Key Set. + + - `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. + +#### `keySetId` + +- type: `String` +- required: `false` +- default: `''` + +If showing the `Edit` type form, the ID of the Key Set. + +### Events + +#### error + +An `@error` event is emitted when the form fails to fetch or save a Key Set. 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 Key Set object. + +### Usage example + +Please refer to the [sandbox](../sandbox/pages/KeySetFormPage.vue). The form is accessible by clicking the `+ New Set` button or `Edit` action of an existing Key Set. + +## TypeScript interfaces + +TypeScript interfaces [are available here](https://github.com/Kong/public-ui-components/blob/main/packages/entities/entities-key-sets/src/types/key-set-form.ts) and can be directly imported into your host application. The following type interfaces are available for import: + +```ts +import type { + KonnectKeySetFormConfig, + KongManagerKeySetFormConfig, +} from '@kong-ui-public/entities-key-sets' +``` \ No newline at end of file diff --git a/packages/entities/entities-key-sets/docs/key-set-list.md b/packages/entities/entities-key-sets/docs/key-set-list.md new file mode 100644 index 0000000000..ab028e1f53 --- /dev/null +++ b/packages/entities/entities-key-sets/docs/key-set-list.md @@ -0,0 +1,162 @@ +# KeySetList.vue + +A table component for key sets. + +- [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/copy-uuid` must be available 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. +- `@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-key-sets` 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. + + - `createRoute`: + - type: `RouteLocationRaw` + - required: `true` + - default: `undefined` + - Route for creating a key set. + + - `getEditRoute`: + - type: `(id: string) => RouteLocationRaw` + - required: `true` + - default: `undefined` + - A function that returns the route for editing a key set. + + - `workspace`: + - type: `string` + - required: `true` + - default: `undefined` + - *Specific to Kong Manager*. Name of the current workspace. + + - `isExactMatch`: + - type: `boolean` + - required: `false` + - default: `undefined` + - *Specific to Kong Manager*. Whether to use exact match. + + - `disableSorting`: + - type: `boolean` + - required: `false` + - default: `undefined` + - *Specific to Kong Manager*. Whether to disable table sorting. + + - `filterSchema`: + - type: `FilterSchema` + - required: `false` + - default: `undefined` + - *Specific to Kong Manager*. FilterSchema for fuzzy match. + + - `controlPlaneId`: + - type: `string` + - required: `true` + - default: `undefined` + - *Specific to Konnect*. Name of the current control plane. + +The base konnect or kongManger config. + +#### `cacheIdentifier` + +- type: `String` +- required: `false` +- default: `''` + +Used to override the default unique identifier for the table's entry in the cache. This should be unique across the Vue App. +Note: the default value is usually sufficient unless the app needs to support multiple separate instances of the table. + +#### `canCreate` + +- type: `Function as PropType<() => Promise>` +- required: `false` +- default: `async () => true` + +An asynchronous function, that returns a boolean, that evaluates if the user can create a new entity. + +#### `canDelete` + +- type: `Function as PropType<(row: object) => Promise>` +- required: `false` +- default: `async () => true` + +An asynchronous function, that returns a boolean, that evaluates if the user can delete a given entity. + +#### `canEdit` + +- type: `Function as PropType<(row: object) => Promise>` +- required: `false` +- default: `async () => true` + +An asynchronous function, that returns a boolean, that evaluates if the user can edit a given entity. + +### Events + +#### error + +An `@error` event is emitted when the table fails to fetch keys or delete a key set. The event payload is the response error. + +#### copy:success + +A `@copy:success` event is emitted when a key set ID or the entity JSON is successfully copied to clipboard. The event payload shape is CopyEventPayload. + +#### copy:error + +A `@copy:error` event is emitted when an error occurs when trying to copy a key set ID or the entity JSON. The event payload shape is CopyEventPayload. + +#### delete:success + +A `@delete:success` event is emitted when a key set is successfully deleted. The event payload is the key item data object. + +### Usage example + +Please refer to the [sandbox](../sandbox/pages/KeyListPage.vue). + +## TypeScript interfaces + +TypeScript interfaces [are available here](https://github.com/Kong/public-ui-components/blob/main/packages/entities/entities-key-sets/src/types/key-set-list.ts) and can be directly imported into your host application. The following type interfaces are available for import: + +```ts +import type { + BaseKeyListConfig, + KonnectKeySetListConfig, + KongManagerKeySetListConfig, +} from '@kong-ui-public/entities-key-sets' +``` diff --git a/packages/entities/entities-key-sets/fixtures/mockData.ts b/packages/entities/entities-key-sets/fixtures/mockData.ts new file mode 100644 index 0000000000..b91eaaa0ad --- /dev/null +++ b/packages/entities/entities-key-sets/fixtures/mockData.ts @@ -0,0 +1,47 @@ +// FetcherRawResponse is the raw format of the endpoint's response +export interface FetcherRawResponse { + data: any[]; + total: number; + offset?: string; +} + +export const keySet1 = { + id: '1', + name: 'key-set-1', + tags: ['tag1'], +} + +export const keySets: FetcherRawResponse = { + data: [ + keySet1, + { + id: '2', + name: 'key-set-2', + tags: ['tag1', 'tag2'], + }, + ], + total: 2, +} + +export const keySets100: any[] = Array(100) + .fill(null) + .map((_, i) => ({ + id: `${i + 1}`, + name: `key-set-${i + 1}`, + })) + +export const paginate = ( + routes: any[], + size: number, + _offset: number, +): FetcherRawResponse => { + const sliced = routes.slice(_offset, _offset + size) + const offset = + _offset + size < routes.length ? String(_offset + size) : undefined + + return { + data: sliced, + total: sliced.length, + offset, + } +} diff --git a/packages/entities/entities-key-sets/package.json b/packages/entities/entities-key-sets/package.json new file mode 100644 index 0000000000..48a3163c06 --- /dev/null +++ b/packages/entities/entities-key-sets/package.json @@ -0,0 +1,73 @@ +{ + "name": "@kong-ui-public/entities-key-sets", + "version": "1.0.0", + "type": "module", + "main": "./dist/entities-key-sets.umd.js", + "module": "./dist/entities-key-sets.es.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/entities-key-sets.es.js", + "require": "./dist/entities-key-sets.umd.js" + }, + "./package.json": "./package.json", + "./dist/*": "./dist/*" + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@kong-ui-public/copy-uuid": "workspace:^", + "@kong-ui-public/i18n": "workspace:^", + "@kong/kongponents": "^8.106.0", + "axios": "^1.4.0", + "vue": "^3.3.4", + "vue-router": "^4.2.4" + }, + "devDependencies": { + "@kong-ui-public/copy-uuid": "workspace:^", + "@kong-ui-public/i18n": "workspace:^", + "@kong/kongponents": "^8.106.0", + "axios": "^1.4.0", + "vue": "^3.3.4", + "vue-router": "^4.2.4" + }, + "scripts": { + "dev": "cross-env USE_SANDBOX=true vite", + "build": "run-s typecheck build:package build:types", + "build:package": "vite build -m production", + "build:analyzer": "BUILD_VISUALIZER='entities/entities-key-sets' vite build -m production", + "build:types": "vue-tsc -p './tsconfig.build.json' --emitDeclarationOnly", + "preview:package": "vite preview --port 4173", + "preview": "cross-env USE_SANDBOX=true PREVIEW_SANDBOX=true run-s build:package preview:package", + "lint": "eslint '**/*.{js,jsx,ts,tsx,vue}' --ignore-path '../../../.eslintignore'", + "lint:fix": "eslint '**/*.{js,jsx,ts,tsx,vue}' --ignore-path '../../../.eslintignore' --fix", + "stylelint": "stylelint --allow-empty-input './src/**/*.{css,scss,sass,less,styl,vue}'", + "stylelint:fix": "stylelint --allow-empty-input './src/**/*.{css,scss,sass,less,styl,vue}' --fix", + "typecheck": "vue-tsc -p './tsconfig.build.json' --noEmit", + "test:component": "BABEL_ENV=cypress cross-env FORCE_COLOR=1 cypress run --component -b chrome --spec './src/**/*.cy.ts' --project '../../../.'", + "test:component:open": "BABEL_ENV=cypress cross-env FORCE_COLOR=1 cypress open --component -b chrome --project '../../../.'", + "test:unit": "cross-env FORCE_COLOR=1 vitest run", + "test:unit:open": "cross-env FORCE_COLOR=1 vitest --ui" + }, + "repository": { + "type": "git", + "url": "https://github.com/Kong/public-ui-components.git", + "directory": "packages/entities/entities-key-sets" + }, + "homepage": "https://github.com/Kong/public-ui-components/tree/main/packages/entities/entities-key-sets", + "author": "Kong, Inc.", + "license": "Apache-2.0", + "volta": { + "extends": "../../../package.json" + }, + "distSizeChecker": { + "errorLimit": "500KB" + }, + "dependencies": { + "@kong-ui-public/entities-shared": "workspace:^" + } +} diff --git a/packages/entities/entities-key-sets/sandbox/App.vue b/packages/entities/entities-key-sets/sandbox/App.vue new file mode 100644 index 0000000000..656df1b4e6 --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/App.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/entities/entities-key-sets/sandbox/index.html b/packages/entities/entities-key-sets/sandbox/index.html new file mode 100644 index 0000000000..f19874e6b5 --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/index.html @@ -0,0 +1,27 @@ + + + + + + + EntitiesKeySets Component Sandbox + + + + + + + +
+ + + + diff --git a/packages/entities/entities-key-sets/sandbox/index.ts b/packages/entities/entities-key-sets/sandbox/index.ts new file mode 100644 index 0000000000..1bb2113b59 --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/index.ts @@ -0,0 +1,55 @@ +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import App from './App.vue' +import Kongponents from '@kong/kongponents' +import '@kong/kongponents/dist/style.css' +import CopyUuid from '@kong-ui-public/copy-uuid' +import '@kong-ui-public/copy-uuid/dist/style.css' + +const app = createApp(App) + +// Initializing in an async function in order to fetch session and permissions data for the sandbox ONLY. +// DO NOT DO THIS IN AN ACTUAL APP +const init = async () => { + const router = createRouter({ + history: createWebHistory(), + routes: [ + { + path: '/', + name: 'home', + redirect: { name: 'key-set-list' }, + }, + { + path: '/key-set-list', + name: 'key-set-list', + component: () => import('./pages/KeySetListPage.vue'), + }, + { + path: '/key-set/create', + name: 'create-key-set', + component: () => import('./pages/KeySetFormPage.vue'), + }, + { + path: '/key-set/:id', + name: 'view-key-set', + props: true, + component: () => import('./pages/KeySetConfigCardPage.vue'), + }, + { + path: '/key-set/:id/edit', + name: 'edit-key-set', + props: true, + component: () => import('./pages/KeySetFormPage.vue'), + }, + ], + }) + + app.use(Kongponents) + app.use(CopyUuid) + + app.use(router) + + app.mount('#app') +} + +init() diff --git a/packages/entities/entities-key-sets/sandbox/pages/KeySetConfigCardPage.vue b/packages/entities/entities-key-sets/sandbox/pages/KeySetConfigCardPage.vue new file mode 100644 index 0000000000..5fd26d4f38 --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/pages/KeySetConfigCardPage.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/entities/entities-key-sets/sandbox/pages/KeySetFormPage.vue b/packages/entities/entities-key-sets/sandbox/pages/KeySetFormPage.vue new file mode 100644 index 0000000000..cf3772960b --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/pages/KeySetFormPage.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/entities/entities-key-sets/sandbox/pages/KeySetListPage.vue b/packages/entities/entities-key-sets/sandbox/pages/KeySetListPage.vue new file mode 100644 index 0000000000..58d3690c26 --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/pages/KeySetListPage.vue @@ -0,0 +1,95 @@ + + + diff --git a/packages/entities/entities-key-sets/sandbox/tsconfig.json b/packages/entities/entities-key-sets/sandbox/tsconfig.json new file mode 100644 index 0000000000..6b0bff7930 --- /dev/null +++ b/packages/entities/entities-key-sets/sandbox/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@entities-shared-sandbox/*": [ + "../../entities-shared/sandbox/shared/*" + ] + } + }, + "include": [ + "**/*.ts", + "**/*.vue", + ], + "exclude": [ + "node_modules" + ] +} diff --git a/packages/entities/entities-key-sets/src/components/KeySetConfigCard.vue b/packages/entities/entities-key-sets/src/components/KeySetConfigCard.vue new file mode 100644 index 0000000000..3728ef73c3 --- /dev/null +++ b/packages/entities/entities-key-sets/src/components/KeySetConfigCard.vue @@ -0,0 +1,71 @@ + + + diff --git a/packages/entities/entities-key-sets/src/components/KeySetForm.cy.ts b/packages/entities/entities-key-sets/src/components/KeySetForm.cy.ts new file mode 100644 index 0000000000..0523c5963e --- /dev/null +++ b/packages/entities/entities-key-sets/src/components/KeySetForm.cy.ts @@ -0,0 +1,377 @@ +// Cypress component test spec file +import { KongManagerKeySetFormConfig, KonnectKeySetFormConfig } from '../types' +import { keySet1 } from '../../fixtures/mockData' +import KeySetForm from './KeySetForm.vue' +import { EntityBaseForm } from '@kong-ui-public/entities-shared' + +const cancelRoute = { name: 'keys-list' } + +const baseConfigKonnect: KonnectKeySetFormConfig = { + app: 'konnect', + controlPlaneId: '1234-abcd-ilove-cats', + apiBaseUrl: '/us/kong-api/konnect-api', + cancelRoute, +} + +const baseConfigKM: KongManagerKeySetFormConfig = { + 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}/key-sets/*`, + }, + { + statusCode: 200, + body: params?.mockData ?? keySet1, + }, + ).as(params?.alias ?? 'getKeySet') + } + + const interceptUpdate = (status = 200): void => { + cy.intercept( + { + method: 'PATCH', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/key-sets/*`, + }, + { + statusCode: status, + body: { ...keySet1, tags: ['tag1', 'tag2'] }, + }, + ).as('updateKeySet') + } + + it('should show create form', () => { + cy.mount(KeySetForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-key-sets-form').should('be.visible') + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').should('be.visible') + cy.getTestId('key-set-form-tags').should('be.visible') + }) + + it('should correctly handle button state - create', () => { + cy.mount(KeySetForm, { + props: { + config: baseConfigKM, + }, + }) + + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').type('my-key-set') + cy.getTestId('form-submit').should('be.enabled') + // disables save when required field is cleared + cy.getTestId('key-set-form-name').clear() + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should show edit form', () => { + interceptKM() + + cy.mount(KeySetForm, { + props: { + config: baseConfigKM, + keySetId: keySet1.id, + }, + }) + + cy.wait('@getKeySet') + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').should('have.value', keySet1.name) + keySet1.tags.forEach((tag: string) => { + cy.getTestId('key-set-form-tags').invoke('val').then((val: string) => { + expect(val).to.contain(tag) + }) + }) + }) + + it('should correctly handle button state - edit', () => { + interceptKM() + + cy.mount(KeySetForm, { + props: { + config: baseConfigKM, + keySetId: keySet1.id, + }, + }) + + cy.wait('@getKeySet') + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').type('-edited') + cy.getTestId('form-submit').should('be.enabled') + // disables save when form changes are undone + cy.getTestId('key-set-form-name').clear() + cy.getTestId('key-set-form-name').type(keySet1.name) + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should handle error state - failed to load Key Set', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/key-sets/*`, + }, + { + statusCode: 404, + body: {}, + }, + ).as('getKeySetError') + + cy.mount(KeySetForm, { + props: { + config: baseConfigKM, + keySetId: keySet1.id, + }, + }) + + cy.wait('@getKeySetError') + cy.get('.kong-ui-entities-key-sets-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-key-sets-form form').should('not.exist') + }) + + it('update event should be emitted when Key Set was edited', () => { + interceptKM() + interceptUpdate() + + cy.mount(KeySetForm, { + props: { + config: baseConfigKM, + keySetId: keySet1.id, + onUpdate: cy.spy().as('onUpdateSpy'), + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') + + cy.wait('@getKeySet') + cy.getTestId('key-set-form-tags').clear() + cy.getTestId('key-set-form-tags').type('tag1,tag2') + + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) + + cy.wait('@updateKeySet') + + cy.get('@onUpdateSpy').should('have.been.calledOnce') + }) + }) + + describe('Konnect', () => { + const interceptKonnect = (params?: { + mockData?: object + alias?: string + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets/*`, + }, + { + statusCode: 200, + body: params?.mockData ?? keySet1, + }, + ).as(params?.alias ?? 'getKeySet') + } + + const interceptUpdate = (status = 200): void => { + cy.intercept( + { + method: 'PUT', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets/*`, + }, + { + statusCode: status, + body: { ...keySet1, tags: ['tag1', 'tag2'] }, + }, + ).as('updateKeySet') + } + + it('should show create form', () => { + cy.mount(KeySetForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-key-sets-form').should('be.visible') + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').should('be.visible') + cy.getTestId('key-set-form-tags').should('be.visible') + }) + + it('should correctly handle button state - create', () => { + cy.mount(KeySetForm, { + props: { + config: baseConfigKonnect, + }, + }) + + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').type('my-key-set') + cy.getTestId('form-submit').should('be.enabled') + // disables save when required field is cleared + cy.getTestId('key-set-form-name').clear() + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should show edit form', () => { + interceptKonnect() + + cy.mount(KeySetForm, { + props: { + config: baseConfigKonnect, + keySetId: keySet1.id, + }, + }) + + cy.wait('@getKeySet') + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').should('have.value', keySet1.name) + keySet1.tags.forEach((tag: string) => { + cy.getTestId('key-set-form-tags').invoke('val').then((val: string) => { + expect(val).to.contain(tag) + }) + }) + }) + + it('should correctly handle button state - edit', () => { + interceptKonnect() + + cy.mount(KeySetForm, { + props: { + config: baseConfigKonnect, + keySetId: keySet1.id, + }, + }) + + cy.wait('@getKeySet') + cy.get('.kong-ui-entities-key-sets-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('key-set-form-name').type('-edited') + cy.getTestId('form-submit').should('be.enabled') + // disables save when form changes are undone + cy.getTestId('key-set-form-name').clear() + cy.getTestId('key-set-form-name').type(keySet1.name) + cy.getTestId('form-submit').should('be.disabled') + }) + + it('should handle error state - failed to load Key Set', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets/*`, + }, + { + statusCode: 404, + body: {}, + }, + ).as('getKeySetError') + + cy.mount(KeySetForm, { + props: { + config: baseConfigKonnect, + keySetId: keySet1.id, + }, + }) + + cy.wait('@getKeySetError') + cy.get('.kong-ui-entities-key-sets-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-key-sets-form form').should('not.exist') + }) + + it('update event should be emitted when Key Set was edited', () => { + interceptKonnect() + interceptUpdate() + + cy.mount(KeySetForm, { + props: { + config: baseConfigKonnect, + keySetId: keySet1.id, + onUpdate: cy.spy().as('onUpdateSpy'), + }, + }).then(({ wrapper }) => wrapper) + .as('vueWrapper') + + cy.wait('@getKeySet') + cy.getTestId('key-set-form-tags').clear() + cy.getTestId('key-set-form-tags').type('tag1,tag2') + + cy.get('@vueWrapper').then((wrapper: any) => wrapper.findComponent(EntityBaseForm) + .vm.$emit('submit')) + + cy.wait('@updateKeySet') + + cy.get('@onUpdateSpy').should('have.been.calledOnce') + }) + }) +}) diff --git a/packages/entities/entities-key-sets/src/components/KeySetForm.vue b/packages/entities/entities-key-sets/src/components/KeySetForm.vue new file mode 100644 index 0000000000..360c75997d --- /dev/null +++ b/packages/entities/entities-key-sets/src/components/KeySetForm.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/packages/entities/entities-key-sets/src/components/KeySetList.cy.ts b/packages/entities/entities-key-sets/src/components/KeySetList.cy.ts new file mode 100644 index 0000000000..51aedaba9d --- /dev/null +++ b/packages/entities/entities-key-sets/src/components/KeySetList.cy.ts @@ -0,0 +1,767 @@ +// Cypress component test spec file +import KeySetList from './KeySetList.vue' +import type { FetcherResponse } from '@kong-ui-public/entities-shared' +import { + FetcherRawResponse, + paginate, + keySets, + keySets100, +} from '../../fixtures/mockData' +import { KongManagerKeySetListConfig, KonnectKeySetListConfig } from '../types' +import { createMemoryHistory, createRouter, Router } from 'vue-router' +import { v4 as uuidv4 } from 'uuid' + +const viewRoute = 'view-key-set' +const editRoute = 'edit-key-set' +const createRoute = 'create-key-set' + +const baseConfigKonnect: KonnectKeySetListConfig = { + app: 'konnect', + controlPlaneId: '1234-abcd-ilove-cats', + apiBaseUrl: '/us/kong-api/konnect-api', + createRoute, + getViewRoute: () => viewRoute, + getEditRoute: () => editRoute, +} + +const baseConfigKM: KongManagerKeySetListConfig = { + app: 'kongManager', + workspace: 'default', + apiBaseUrl: '/kong-manager', + isExactMatch: false, + filterSchema: {}, + createRoute, + getViewRoute: () => viewRoute, + getEditRoute: () => editRoute, +} + +describe('', () => { + beforeEach(() => { + cy.on('uncaught:exception', err => !err.message.includes('ResizeObserver loop limit exceeded')) + }) + + describe('actions', () => { + // Create a new router instance for each test + let router: Router + + beforeEach(() => { + // Initialize a new router before each test + router = createRouter({ + routes: [ + { path: '/', name: 'list-key-set', component: { template: '
ListPage
' } }, + { path: `/${viewRoute}`, name: viewRoute, component: { template: '
DetailPage
' } }, + ], + history: createMemoryHistory(), + }) + + // Mock data for each test in this block; doesn't matter if we use KM or Konnect + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets*`, + }, + { + statusCode: 200, + body: keySets, + }, + ) + }) + + it('should always show the Copy ID action', () => { + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.getTestId('k-dropdown-trigger').eq(0).click() + cy.getTestId('action-entity-copy-id').should('be.visible') + }) + + for (const expected of [false, true]) { + describe(`${expected ? 'allowed' : 'denied'}`, () => { + it(`should ${expected ? 'allow' : 'prevent'} row click if canRetrieve evaluates to ${expected}`, () => { + expect(router.currentRoute.value.fullPath).not.to.include(viewRoute) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => expected, + }, + router, + }) + + // eslint-disable-next-line cypress/unsafe-to-chain-command + cy.get('table tbody td').eq(0).click().then(() => { + if (expected) { + expect(router.currentRoute.value.fullPath).to.include(viewRoute) + } else { + expect(router.currentRoute.value.fullPath).not.to.include(viewRoute) + } + }) + }) + + it(`should ${expected ? 'show' : 'hide'} the View Details action if canRetrieve evaluates to ${expected}`, () => { + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => expected, + }, + }) + + cy.getTestId('k-dropdown-trigger').eq(0).click() + cy.getTestId('action-entity-view').should(`${!expected ? 'not.' : ''}exist`) + }) + + it(`should ${expected ? '' : 'not'} include the Edit action if canEdit evaluates to ${expected}`, () => { + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => expected, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.getTestId('k-dropdown-trigger').eq(0).click() + cy.getTestId('action-entity-edit').should(`${expected ? '' : 'not.'}exist`) + }) + + it(`should ${expected ? '' : 'not'} include the Delete action if canDelete evaluates to ${expected}`, () => { + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => expected, + canRetrieve: () => {}, + }, + }) + + cy.getTestId('k-dropdown-trigger').eq(0).click() + cy.getTestId('action-entity-delete').should(`${expected ? '' : 'not.'}exist`) + }) + }) + } + }) + + describe('Kong Manager', () => { + const interceptKM = (params?: { + mockData?: FetcherRawResponse; + alias?: string; + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/key-sets*`, + }, + { + statusCode: 200, + body: params?.mockData ?? { + data: [], + total: 0, + }, + }, + ).as(params?.alias ?? 'getKeySets') + } + + const interceptKMMultiPage = (params?: { + mockData?: FetcherRawResponse[]; + alias?: string; + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/key-sets*`, + }, + (req) => { + const size = req.query.size ? Number(req.query.size) : 30 + const offset = req.query.offset ? Number(req.query.offset) : 0 + + req.reply({ + statusCode: 200, + body: paginate(params?.mockData ?? [], size, offset), + }) + }, + ).as(params?.alias ?? 'getKeySetsMultiPage') + } + + it('should show empty state and create key set cta', () => { + interceptKM() + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => true, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list').should('be.visible') + cy.get('.k-table-empty-state').should('be.visible') + cy.get('.k-table-empty-state .k-empty-state-cta .k-button').should('be.visible') + }) + + it('should hide empty state and create key set cta if user can not create', () => { + interceptKM() + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => false, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list').should('be.visible') + cy.get('.k-table-empty-state').should('be.visible') + cy.get('.k-table-empty-state .k-empty-state-cta .k-button').should('not.exist') + }) + + it('should handle error state', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKM.apiBaseUrl}/${baseConfigKM.workspace}/key-sets*`, + }, + { + statusCode: 500, + body: {}, + }, + ).as('getKeySets') + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list').should('be.visible') + cy.get('.k-table-error-state').should('be.visible') + }) + + it('should show key set items', () => { + interceptKM({ + mockData: keySets, + }) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list tr[data-testid="key-set-1"]').should( + 'exist', + ) + cy.get('.kong-ui-entities-key-sets-list tr[data-testid="key-set-2"]').should( + 'exist', + ) + }) + + it('should allow switching between pages', () => { + interceptKMMultiPage({ + mockData: keySets100, + }) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + const l = '.kong-ui-entities-key-sets-list' + const p = '[data-testid="k-table-pagination"]' + + cy.wait('@getKeySetsMultiPage') + + // Page #1 + cy.get(`${l} tbody tr`).should('have.length', 30) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-29"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-30"]`).should('exist') + + cy.get(`${l} ${p}`).should('exist') + cy.get(`${l} ${p} [data-testid="prev-btn"]`).should( + 'have.class', + 'disabled', + ) + cy.get(`${l} ${p} [data-testid="next-btn"]`) + .should('not.have.class', 'disabled') + .click() // next page + + cy.wait('@getKeySetsMultiPage') + + // Page #2 + cy.get(`${l} tbody tr`).should('have.length', 30) + cy.get(`${l} tbody tr[data-testid="key-set-31"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-32"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-59"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-60"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="prev-btn"]`).should( + 'not.have.class', + 'disabled', + ) + cy.get(`${l} ${p} [data-testid="next-btn"]`) + .should('not.have.class', 'disabled') + .click() // next page + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} ${p} [data-testid="next-btn"]`) + .should('not.have.class', 'disabled') + .click() // next page + + // Page #4 + cy.get(`${l} tbody tr`).should('have.length', 10) + cy.get(`${l} tbody tr[data-testid="key-set-91"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-92"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-99"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-100"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="prev-btn"]`).should( + 'not.have.class', + 'disabled', + ) + cy.get(`${l} ${p} [data-testid="next-btn"]`).should( + 'have.class', + 'disabled', + ) + }) + + it('should allow picking different page sizes and persist the preference', () => { + interceptKMMultiPage({ + mockData: keySets100, + }) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + .then(({ wrapper }) => wrapper) + .as('vueWrapper') + + const l = '.kong-ui-entities-key-sets-list' + const p = '[data-testid="k-table-pagination"]' + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 30) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-29"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-30"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).should( + 'contain.text', + '30 items per page', + ) + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).click() + cy.get( + `${l} ${p} [data-testid="page-size-dropdown"] [value="15"]`, + ).click() + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 15) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-14"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-15"]`).should('exist') + + // Unmount and mount + cy.get('@vueWrapper').then((wrapper: any) => wrapper.unmount()) + cy.mount(KeySetList, { + props: { + cacheIdentifie: `key-set-list-${uuidv4()}`, + config: baseConfigKM, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 15) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-14"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-15"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).should( + 'contain.text', + '15 items per page', + ) + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).click() + cy.get( + `${l} ${p} [data-testid="page-size-dropdown"] [value="50"]`, + ).click() + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 50) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-49"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-50"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).should( + 'contain.text', + '50 items per page', + ) + }) + }) + + describe('Konnect', () => { + const interceptKonnect = (params?: { + mockData?: FetcherResponse; + alias?: string; + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets*`, + }, + { + statusCode: 200, + body: params?.mockData ?? { + data: [], + total: 0, + }, + }, + ).as(params?.alias ?? 'getKeySets') + } + + const interceptKonnectMultiPage = (params?: { + mockData?: FetcherRawResponse[]; + alias?: string; + }) => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets*`, + }, + (req) => { + const size = req.query.size ? Number(req.query.size) : 30 + const offset = req.query.offset ? Number(req.query.offset) : 0 + + req.reply({ + statusCode: 200, + body: paginate(params?.mockData ?? [], size, offset), + }) + }, + ).as(params?.alias ?? 'getKeySetsMultiPage') + } + + it('should show empty state and create key set cta', () => { + interceptKonnect() + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => true, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list').should('be.visible') + cy.get('.k-table-empty-state').should('be.visible') + cy.get('.k-table-empty-state .k-empty-state-cta .k-button').should('be.visible') + }) + + it('should hide empty state and create key set cta if user can not create', () => { + interceptKonnect() + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => false, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list').should('be.visible') + cy.get('.k-table-empty-state').should('be.visible') + cy.get('.k-table-empty-state .k-empty-state-cta .k-button').should('not.exist') + }) + + it('should handle error state', () => { + cy.intercept( + { + method: 'GET', + url: `${baseConfigKonnect.apiBaseUrl}/api/runtime_groups/${baseConfigKonnect.controlPlaneId}/key-sets*`, + }, + { + statusCode: 500, + body: {}, + }, + ).as('getKeySets') + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list').should('be.visible') + cy.get('.k-table-error-state').should('be.visible') + }) + + it('should show key set items', () => { + interceptKonnect({ + mockData: keySets, + }) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySets') + cy.get('.kong-ui-entities-key-sets-list tr[data-testid="key-set-1"]').should( + 'exist', + ) + cy.get('.kong-ui-entities-key-sets-list tr[data-testid="key-set-2"]').should( + 'exist', + ) + }) + + it('should allow switching between pages', () => { + interceptKonnectMultiPage({ + mockData: keySets100, + }) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + const l = '.kong-ui-entities-key-sets-list' + const p = '[data-testid="k-table-pagination"]' + + cy.wait('@getKeySetsMultiPage') + + // Page #1 + cy.get(`${l} tbody tr`).should('have.length', 30) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-29"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-30"]`).should('exist') + + cy.get(`${l} ${p}`).should('exist') + cy.get(`${l} ${p} [data-testid="prev-btn"]`).should( + 'have.class', + 'disabled', + ) + cy.get(`${l} ${p} [data-testid="next-btn"]`) + .should('not.have.class', 'disabled') + .click() // next page + + cy.wait('@getKeySetsMultiPage') + + // Page #2 + cy.get(`${l} tbody tr`).should('have.length', 30) + cy.get(`${l} tbody tr[data-testid="key-set-31"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-32"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-59"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-60"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="prev-btn"]`).should( + 'not.have.class', + 'disabled', + ) + cy.get(`${l} ${p} [data-testid="next-btn"]`) + .should('not.have.class', 'disabled') + .click() // next page + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} ${p} [data-testid="next-btn"]`) + .should('not.have.class', 'disabled') + .click() // next page + + // Page #4 + cy.get(`${l} tbody tr`).should('have.length', 10) + cy.get(`${l} tbody tr[data-testid="key-set-91"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-92"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-99"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-100"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="prev-btn"]`).should( + 'not.have.class', + 'disabled', + ) + cy.get(`${l} ${p} [data-testid="next-btn"]`).should( + 'have.class', + 'disabled', + ) + }) + + it('should allow picking different page sizes and persist the preference', () => { + interceptKonnectMultiPage({ + mockData: keySets100, + }) + + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + .then(({ wrapper }) => wrapper) + .as('vueWrapper') + + const l = '.kong-ui-entities-key-sets-list' + const p = '[data-testid="k-table-pagination"]' + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 30) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-29"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-30"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).should( + 'contain.text', + '30 items per page', + ) + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).click() + cy.get( + `${l} ${p} [data-testid="page-size-dropdown"] [value="15"]`, + ).click() + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 15) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-14"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-15"]`).should('exist') + + // Unmount and mount + cy.get('@vueWrapper').then((wrapper: any) => wrapper.unmount()) + cy.mount(KeySetList, { + props: { + cacheIdentifier: `key-set-list-${uuidv4()}`, + config: baseConfigKonnect, + canCreate: () => {}, + canEdit: () => {}, + canDelete: () => {}, + canRetrieve: () => {}, + }, + }) + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 15) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-14"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-15"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).should( + 'contain.text', + '15 items per page', + ) + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).click() + cy.get( + `${l} ${p} [data-testid="page-size-dropdown"] [value="50"]`, + ).click() + + cy.wait('@getKeySetsMultiPage') + + cy.get(`${l} tbody tr`).should('have.length', 50) + cy.get(`${l} tbody tr[data-testid="key-set-1"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-2"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-49"]`).should('exist') + cy.get(`${l} tbody tr[data-testid="key-set-50"]`).should('exist') + + cy.get(`${l} ${p} [data-testid="page-size-dropdown"]`).should( + 'contain.text', + '50 items per page', + ) + }) + }) +}) diff --git a/packages/entities/entities-key-sets/src/components/KeySetList.vue b/packages/entities/entities-key-sets/src/components/KeySetList.vue new file mode 100644 index 0000000000..583afdcec1 --- /dev/null +++ b/packages/entities/entities-key-sets/src/components/KeySetList.vue @@ -0,0 +1,463 @@ + + + + + diff --git a/packages/entities/entities-key-sets/src/composables/index.ts b/packages/entities/entities-key-sets/src/composables/index.ts new file mode 100644 index 0000000000..3893dfc828 --- /dev/null +++ b/packages/entities/entities-key-sets/src/composables/index.ts @@ -0,0 +1,6 @@ +import useI18n from './useI18n' + +// All composables must be exported as part of the default object for Cypress test stubs +export default { + useI18n, +} diff --git a/packages/entities/entities-key-sets/src/composables/useI18n.ts b/packages/entities/entities-key-sets/src/composables/useI18n.ts new file mode 100644 index 0000000000..337ff02756 --- /dev/null +++ b/packages/entities/entities-key-sets/src/composables/useI18n.ts @@ -0,0 +1,11 @@ +import { createI18n, i18nTComponent } from '@kong-ui-public/i18n' +import english from '../locales/en.json' + +export default function useI18n() { + const i18n = createI18n('en-us', english) + + return { + i18n, + i18nT: i18nTComponent(i18n), // Translation component + } +} diff --git a/packages/entities/entities-key-sets/src/global-components.d.ts b/packages/entities/entities-key-sets/src/global-components.d.ts new file mode 100644 index 0000000000..2f0048d672 --- /dev/null +++ b/packages/entities/entities-key-sets/src/global-components.d.ts @@ -0,0 +1,2 @@ +// Import globally available components +import '@kong/kongponents/dist/types/global-components' diff --git a/packages/entities/entities-key-sets/src/index.ts b/packages/entities/entities-key-sets/src/index.ts new file mode 100644 index 0000000000..d11912d5a1 --- /dev/null +++ b/packages/entities/entities-key-sets/src/index.ts @@ -0,0 +1,7 @@ +import KeySetList from './components/KeySetList.vue' +import KeySetForm from './components/KeySetForm.vue' +import KeySetConfigCard from './components/KeySetConfigCard.vue' + +export { KeySetList, KeySetForm, KeySetConfigCard } + +export * from './types' diff --git a/packages/entities/entities-key-sets/src/key-sets-endpoints.ts b/packages/entities/entities-key-sets/src/key-sets-endpoints.ts new file mode 100644 index 0000000000..c51cec626b --- /dev/null +++ b/packages/entities/entities-key-sets/src/key-sets-endpoints.ts @@ -0,0 +1,16 @@ +export default { + list: { + konnect: '/api/runtime_groups/{controlPlaneId}/key-sets', + kongManager: '/{workspace}/key-sets', + }, + form: { + konnect: { + create: '/api/runtime_groups/{controlPlaneId}/key-sets', + edit: '/api/runtime_groups/{controlPlaneId}/key-sets/{id}', + }, + kongManager: { + create: '/{workspace}/key-sets', + edit: '/{workspace}/key-sets/{id}', + }, + }, +} diff --git a/packages/entities/entities-key-sets/src/locales/en.json b/packages/entities/entities-key-sets/src/locales/en.json new file mode 100644 index 0000000000..ef9dd2d50e --- /dev/null +++ b/packages/entities/entities-key-sets/src/locales/en.json @@ -0,0 +1,64 @@ +{ + "keySets": { + "title": "Key Sets", + "list": { + "toolbar_actions": { + "new_key_set": "New Key Set" + }, + "table_headers": { + "name": "Name", + "id": "ID", + "tags": "Tags" + }, + "empty_state": { + "title": "Configure a New Key Set", + "description": "A Key Set object holds a collection of asymmetric key objects. This entity allows to logically group keys by their purpose." + } + }, + "actions": { + "create": "New Key Set", + "copy_id": "Copy ID", + "copy_json": "Copy JSON", + "edit": "Edit", + "delete": "Delete", + "clear": "Clear", + "view": "View Details" + }, + "search": { + "placeholder": "Filter by exact name or ID" + }, + "delete": { + "title": "Delete a Key Set", + "description": "Deleting this key set will also delete keys associated. This action cannot be reversed so make sure to check the key set usage before deleting." + }, + "errors": { + "general": "Key sets could not be retrieved", + "delete": "The key set could not be deleted at this time.", + "copy": "Failed to copy to clipboard" + }, + "copy": { + "success": "Copied {val} to clipboard", + "success_brief": "Successfully copied to clipboard" + }, + "form": { + "sections": { + "general": { + "title": "General Information", + "description": "General information will help identify and manage this key set." + } + }, + "fields": { + "name": { + "label": "Name", + "placeholder": "Enter a unique name for this key set" + }, + "tags": { + "label": "Tags", + "placeholder": "Enter a list of tags separated by comma", + "help": "e.g. tag1, tag2, tag3", + "tooltip": "An optional set of strings for grouping and filtering, separated by commas." + } + } + } + } +} diff --git a/packages/entities/entities-key-sets/src/types/index.ts b/packages/entities/entities-key-sets/src/types/index.ts new file mode 100644 index 0000000000..5cb78794f8 --- /dev/null +++ b/packages/entities/entities-key-sets/src/types/index.ts @@ -0,0 +1,6 @@ +// Export all types and interfaces from this index.ts +// The actual types and interfaces should be contained in separate files within this folder. + +export * from './key-set-list' +export * from './key-set-form' +export * from './key-set-config-card' diff --git a/packages/entities/entities-key-sets/src/types/key-set-config-card.ts b/packages/entities/entities-key-sets/src/types/key-set-config-card.ts new file mode 100644 index 0000000000..e031fdecef --- /dev/null +++ b/packages/entities/entities-key-sets/src/types/key-set-config-card.ts @@ -0,0 +1,16 @@ +import type { KonnectBaseEntityConfig, KongManagerBaseEntityConfig, ConfigurationSchemaItem } from '@kong-ui-public/entities-shared' + +/** Konnect KeySet entity config */ +export interface KonnectKeySetEntityConfig extends KonnectBaseEntityConfig {} + +/** Kong Manager KeySet entity config */ +export interface KongManagerKeySetEntityConfig extends KongManagerBaseEntityConfig {} + +export interface KeySetConfigurationSchema { + // basic fields + id: ConfigurationSchemaItem + name: ConfigurationSchemaItem, + last_updated: ConfigurationSchemaItem, + created: ConfigurationSchemaItem, + tags: ConfigurationSchemaItem, +} diff --git a/packages/entities/entities-key-sets/src/types/key-set-form.ts b/packages/entities/entities-key-sets/src/types/key-set-form.ts new file mode 100644 index 0000000000..46c69be603 --- /dev/null +++ b/packages/entities/entities-key-sets/src/types/key-set-form.ts @@ -0,0 +1,28 @@ +import type { RouteLocationRaw } from 'vue-router' +import { KonnectBaseFormConfig, KongManagerBaseFormConfig } from '@kong-ui-public/entities-shared' + +/** Konnect Key Set form config */ +export interface KonnectKeySetFormConfig extends KonnectBaseFormConfig { + /** Route to return to if canceling create/edit a Key Set */ + cancelRoute: RouteLocationRaw +} + +/** Kong Manager Key Set form config */ +export interface KongManagerKeySetFormConfig extends KongManagerBaseFormConfig { + /** Route to return to if canceling create/edit a Key Set */ + cancelRoute: RouteLocationRaw +} + +export interface KeySetFormFields { + name: string + tags?: string +} + +export interface KeySetFormState { + /** Form fields */ + fields: KeySetFormFields + /** 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-key-sets/src/types/key-set-list.ts b/packages/entities/entities-key-sets/src/types/key-set-list.ts new file mode 100644 index 0000000000..c0505b1a4b --- /dev/null +++ b/packages/entities/entities-key-sets/src/types/key-set-list.ts @@ -0,0 +1,36 @@ +import type { RouteLocationRaw } from 'vue-router' +import { FilterSchema, KongManagerBaseTableConfig, KonnectBaseTableConfig } from '@kong-ui-public/entities-shared' + +export interface BaseKeySetListConfig { + /** Route for creating a key set */ + createRoute: RouteLocationRaw + /** A function that returns the route for viewing a key set */ + getViewRoute: (id: string) => RouteLocationRaw + /** A function that returns the route for editing a key set */ + getEditRoute: (id: string) => RouteLocationRaw +} + +/** Konnect key list config */ +export interface KonnectKeySetListConfig extends KonnectBaseTableConfig, BaseKeySetListConfig {} + +/** Kong Manager key list config */ +export interface KongManagerKeySetListConfig extends KongManagerBaseTableConfig, BaseKeySetListConfig { + /** FilterSchema for fuzzy match */ + filterSchema?: FilterSchema +} + +export interface EntityRow extends Record { + id: string + name?: string + kid: string +} + +/** Copy field event payload */ +export interface CopyEventPayload { + /** The entity row */ + entity: EntityRow + /** The field being copied. If omitted, the entity JSON is being copied. */ + field?: string + /** The toaster message */ + message: string +} diff --git a/packages/entities/entities-key-sets/tsconfig.build.json b/packages/entities/entities-key-sets/tsconfig.build.json new file mode 100644 index 0000000000..577de9d6ae --- /dev/null +++ b/packages/entities/entities-key-sets/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "exclude": [ + "src/**/*.cy.ts", + "src/**/*.spec.ts", + "sandbox", + "dist" + ] +} diff --git a/packages/entities/entities-key-sets/tsconfig.json b/packages/entities/entities-key-sets/tsconfig.json new file mode 100644 index 0000000000..e34e90e4e4 --- /dev/null +++ b/packages/entities/entities-key-sets/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "outDir": "dist", + "declarationDir": "dist/types", + "types": [ + "node", + "vite/client", + "cypress", + "cypress/vue", + "../../../cypress/support" + ] + }, + "include": [ + "src/**/*", + "sandbox/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/entities/entities-key-sets/vite.config.ts b/packages/entities/entities-key-sets/vite.config.ts new file mode 100644 index 0000000000..41dfcd3dec --- /dev/null +++ b/packages/entities/entities-key-sets/vite.config.ts @@ -0,0 +1,35 @@ +import sharedViteConfig, { getApiProxies, sanitizePackageName } from '../../../vite.config.shared' +import { resolve } from 'path' +import { defineConfig, mergeConfig } from 'vite' + +// Package name MUST always match the kebab-case package name inside the component's package.json file and the name of your `/packages/{package-name}` directory +const packageName = 'entities-key-sets' +const sanitizedPackageName = sanitizePackageName(packageName) + +// Merge the shared Vite config with the local one defined below +const config = mergeConfig(sharedViteConfig, defineConfig({ + build: { + lib: { + // The kebab-case name of the exposed global variable. MUST be in the format `kong-ui-public-{package-name}` + // Example: name: 'kong-ui-public-demo-component' + name: `kong-ui-public-${sanitizedPackageName}`, + entry: resolve(__dirname, './src/index.ts'), + fileName: (format) => `${sanitizedPackageName}.${format}.js`, + }, + }, + server: { + proxy: { + // Add the API proxies to inject the Authorization header + ...getApiProxies(), + }, + }, +})) + +// If we are trying to preview a build of the local `package/entities-key-sets/sandbox` directory, +// unset the external and lib properties +if (process.env.PREVIEW_SANDBOX) { + config.build.rollupOptions.external = undefined + config.build.lib = undefined +} + +export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f72c99bc6b..4bf479f8fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: false - excludeLinksFromLockfile: false - importers: .: @@ -418,6 +414,31 @@ importers: specifier: ^4.2.4 version: 4.2.4(vue@3.3.4) + packages/entities/entities-key-sets: + dependencies: + '@kong-ui-public/entities-shared': + specifier: workspace:^ + version: link:../entities-shared + devDependencies: + '@kong-ui-public/copy-uuid': + specifier: workspace:^ + version: link:../../core/copy-uuid + '@kong-ui-public/i18n': + specifier: workspace:^ + version: link:../../core/i18n + '@kong/kongponents': + specifier: ^8.106.0 + version: 8.116.2(vue-router@4.2.4)(vue@3.3.4) + axios: + specifier: ^1.4.0 + version: 1.4.0 + vue: + specifier: ^3.3.4 + version: 3.3.4 + vue-router: + specifier: ^4.2.4 + version: 4.2.4(vue@3.3.4) + packages/entities/entities-keys: dependencies: '@kong-ui-public/entities-shared': @@ -435,7 +456,7 @@ importers: version: 1.8.0 '@kong/kongponents': specifier: ^8.106.0 - version: 8.116.2(axios@1.4.0)(vue-router@4.2.4)(vue@3.3.4) + version: 8.116.2(vue-router@4.2.4)(vue@3.3.4) axios: specifier: ^1.4.0 version: 1.4.0 @@ -1721,36 +1742,10 @@ packages: - debug dev: false - /@kong/kongponents@8.116.2(axios@1.4.0)(vue-router@4.2.4)(vue@3.3.4): - resolution: {integrity: sha512-p+08/xu8PA8P/+eaaXKH+lBVG0irNCFH2BF24gXagyi/VKqNsVy1z+YYUe065MYnalsUgifCFp/xTW2XTLi6Lg==} - engines: {node: '>=16.19.0'} - peerDependencies: - axios: ^0.27.2 - vue: '>= 3.3.0' - vue-router: ^4.1.6 - dependencies: - axios: 1.4.0 - date-fns: 2.30.0 - date-fns-tz: 1.3.8(date-fns@2.30.0) - focus-trap: 7.5.2 - focus-trap-vue: 3.4.0(focus-trap@7.5.2)(vue@3.3.4) - popper.js: 1.16.1 - sortablejs: 1.15.0 - swrv: 1.0.4(vue@3.3.4) - uuid: 8.3.2 - v-calendar: 3.0.3(vue@3.3.4) - vue: 3.3.4 - vue-draggable-next: 2.2.0(sortablejs@1.15.0)(vue@3.3.4) - vue-router: 4.2.4(vue@3.3.4) - transitivePeerDependencies: - - '@popperjs/core' - dev: true - /@kong/kongponents@8.116.2(vue-router@4.2.4)(vue@3.3.4): resolution: {integrity: sha512-p+08/xu8PA8P/+eaaXKH+lBVG0irNCFH2BF24gXagyi/VKqNsVy1z+YYUe065MYnalsUgifCFp/xTW2XTLi6Lg==} engines: {node: '>=16.19.0'} peerDependencies: - axios: ^0.27.2 vue: '>= 3.3.0' vue-router: ^4.1.6 dependencies: @@ -1769,6 +1764,7 @@ packages: vue-router: 4.2.4(vue@3.3.4) transitivePeerDependencies: - '@popperjs/core' + - debug dev: true /@kong/kongponents@8.116.2(vue@3.3.4):