diff --git a/.github/workflows/block-apps.yml b/.github/workflows/block-apps.yml index 0ad038d64..dd68ba4bf 100644 --- a/.github/workflows/block-apps.yml +++ b/.github/workflows/block-apps.yml @@ -1,4 +1,4 @@ -name: Block PRs on following apps supernova, huereka, greenhouse and greenhouse-management +name: Block PRs on apps supernova, huereka, greenhouse and greenhouse-management and libs communicator, oauth and juno-ui-components on: pull_request: @@ -75,12 +75,15 @@ jobs: echo libs: ${{ needs.changes.outputs.libs }} echo app changes: ${{ needs.changes.outputs.app-changes }} echo lib changes: ${{ needs.changes.outputs.lib-changes }} - - name: Check if any of the apps are supernova, huereka, greenhouse or greenhouse-management + - name: Check if changes belongs to apps supernova, huereka, greenhouse or greenhouse-management or libs communicator, oauth or juno-ui-components if: | contains(needs.changes.outputs.app-changes, 'supernova') || contains(needs.changes.outputs.app-changes, 'huereka') || contains(needs.changes.outputs.app-changes, 'greenhouse') || - contains(needs.changes.outputs.app-changes, 'greenhouse-management') + contains(needs.changes.outputs.app-changes, 'greenhouse-management') || + contains(needs.changes.outputs.lib-changes, 'communicator') || + contains(needs.changes.outputs.lib-changes, 'oauth') || + contains(needs.changes.outputs.lib-changes, 'juno-ui-components') run: | - echo "::error not allowed to make changes to supernova, huereka, greenhouse or greenhouse-management apps" + echo "::error not allowed to make changes to supernova, huereka, greenhouse or greenhouse-management apps or communicator, oauth or juno-ui-components libs" exit 1 diff --git a/apps/assets-overview/jest.config.js b/apps/assets-overview/jest.config.js index d16f3da9d..fd60a61f4 100644 --- a/apps/assets-overview/jest.config.js +++ b/apps/assets-overview/jest.config.js @@ -8,7 +8,7 @@ module.exports = { testEnvironment: "jsdom", setupFilesAfterEnv: ["/setupTests.js"], transformIgnorePatterns: [ - "node_modules/(?!(juno-ui-components|messages-provider)/)", + "node_modules/(?!(juno-ui-components|messages-provider|url-state-router|utils)/)", ], moduleNameMapper: { // Jest currently doesn't support resources with query parameters. diff --git a/apps/assets-overview/package.json b/apps/assets-overview/package.json index 25b94a563..0316dd4c7 100644 --- a/apps/assets-overview/package.json +++ b/apps/assets-overview/package.json @@ -31,9 +31,9 @@ "github-markdown-css": "^5.1.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "3.3.0", - "messages-provider": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "15.8.1", @@ -45,10 +45,10 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "url-state-router": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "url-state-router": "https://assets.juno.global.cloud.sap/libs/url-state-router@1.0.3/package.tgz", "util": "^0.12.4", - "utils": "*", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "scripts": { @@ -60,15 +60,15 @@ "peerDependencies": { "@tanstack/react-query": "4.28.0", "custom-event-polyfill": "1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "3.3.0", - "messages-provider": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", "prop-types": "15.8.1", "react": "18.2.0", "react-dom": "18.2.0", - "url-state-provider": "*", - "url-state-router": "*", - "utils": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "url-state-router": "https://assets.juno.global.cloud.sap/libs/url-state-router@1.0.3/package.tgz", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "appProps": { diff --git a/apps/auth/package.json b/apps/auth/package.json index b2d1c80fa..0c3c4ac2a 100644 --- a/apps/auth/package.json +++ b/apps/auth/package.json @@ -25,14 +25,14 @@ "autoprefixer": "^10.4.2", "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "custom-event-polyfill": "^1.0.7", "esbuild": "^0.17.12", "interweave": "^13.0.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "oauth": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -52,7 +52,7 @@ "peerDependencies": { "custom-event-polyfill": "^1.0.7", "juno-ui-components": "latest", - "oauth": "*", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0" diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 258a3d5e0..8b6a12424 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -26,7 +26,7 @@ "babel-plugin-transform-import-meta": "^2.2.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "postcss": "^8.4.21", "postcss-url": "^10.1.3", @@ -47,7 +47,7 @@ }, "peerDependencies": { "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", diff --git a/apps/dashboard/src/store.js b/apps/dashboard/src/store.js index d906a33b2..558705b6c 100644 --- a/apps/dashboard/src/store.js +++ b/apps/dashboard/src/store.js @@ -42,6 +42,16 @@ const DOMAINS = { "HCM", "HDA", "HEC", + "IAAS-20e8bf", + "IAAS-45b91f", + "IAAS-de5955", + "IAAS-9d6a56", + "IAAS-b56735", + "IAAS-ec5a3e", + "IAAS-d3495f", + "IAAS-90876f", + "IAAS-7ff5dd", + "IAAS-34a24e", "KYMA", "NEO", "ORA", diff --git a/apps/exampleapp/jest.config.js b/apps/exampleapp/jest.config.js index 5f65d6a46..33ea7ddfe 100644 --- a/apps/exampleapp/jest.config.js +++ b/apps/exampleapp/jest.config.js @@ -7,7 +7,9 @@ module.exports = { transform: { "\\.[jt]sx?$": "babel-jest" }, testEnvironment: "jsdom", setupFilesAfterEnv: ["/setupTests.js"], - transformIgnorePatterns: ["node_modules/(?!(juno-ui-components)/)"], + transformIgnorePatterns: [ + "node_modules/(?!(juno-ui-components|oauth|messages-provider|utils)/)", + ], moduleNameMapper: { // Jest currently doesn't support resources with query parameters. // Therefore we add the optional query parameter matcher at the end diff --git a/apps/exampleapp/package.json b/apps/exampleapp/package.json index 1a1e48134..0fe86bbff 100644 --- a/apps/exampleapp/package.json +++ b/apps/exampleapp/package.json @@ -29,10 +29,10 @@ "esbuild": "^0.17.19", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -42,9 +42,9 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", - "utils": "*", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "scripts": { @@ -55,15 +55,15 @@ "peerDependencies": { "@tanstack/react-query": "4.28.0", "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "importmapExtras": { diff --git a/apps/greenhouse-management/LICENSE b/apps/greenhouse-management/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/apps/greenhouse-management/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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/apps/greenhouse-management/README.md b/apps/greenhouse-management/README.md deleted file mode 100644 index 5adc53806..000000000 --- a/apps/greenhouse-management/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Greenhouse Management App - -This is the shell app for Greenhouse Management Apps. It is the host for all management apps that are part of Greenhouse. diff --git a/apps/greenhouse-management/__mocks__/client.js b/apps/greenhouse-management/__mocks__/client.js deleted file mode 100644 index 84531d3c2..000000000 --- a/apps/greenhouse-management/__mocks__/client.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { JSDOM } from "jsdom" -const dom = new JSDOM() -global.document = dom.window.document -global.window = dom.window diff --git a/apps/greenhouse-management/__mocks__/fileMock.js b/apps/greenhouse-management/__mocks__/fileMock.js deleted file mode 100644 index 27ce65aca..000000000 --- a/apps/greenhouse-management/__mocks__/fileMock.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = "test-file-stub" diff --git a/apps/greenhouse-management/babel.config.js b/apps/greenhouse-management/babel.config.js deleted file mode 100644 index 0719e2fec..000000000 --- a/apps/greenhouse-management/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - env: { - test: { - presets: ["@babel/preset-env", "@babel/preset-react"], - plugins: [["babel-plugin-transform-import-meta", { module: "ES6" }]], - }, - }, -} diff --git a/apps/greenhouse-management/esbuild.config.js b/apps/greenhouse-management/esbuild.config.js deleted file mode 100644 index 2394388b8..000000000 --- a/apps/greenhouse-management/esbuild.config.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const esbuild = require("esbuild") -const fs = require("node:fs/promises") -const pkg = require("./package.json") -const postcss = require("postcss") -const sass = require("sass") -const { transform } = require("@svgr/core") -const url = require("postcss-url") -// this function generates app props based on package.json and propSecrets.json -const appProps = require("../../helpers/appProps") - -if (!/.+\/.+\.js/.test(pkg.module)) - throw new Error( - "module value is incorrect, use DIR/FILE.js like build/index.js" - ) - -const isProduction = process.env.NODE_ENV === "production" -// If the jspm server fails and we cannot use external packages -// in our import map then IGNORE_EXTERNALS (global env variable) -// should be set to true -const IGNORE_EXTERNALS = process.env.IGNORE_EXTERNALS === "true" -// in dev environment we prefix output file with public -let outfile = `${isProduction ? "" : "public/"}${pkg.main || pkg.module}` -// get output from outputfile -let outdir = outfile.slice(0, outfile.lastIndexOf("/")) -const args = process.argv.slice(2) -const watch = args.indexOf("--watch") >= 0 -const serve = args.indexOf("--serve") >= 0 - -// helpers for console log -const green = "\x1b[32m%s\x1b[0m" -const yellow = "\x1b[33m%s\x1b[0m" -const clear = "\033c" - -const build = async () => { - // delete build folder and re-create it as an empty folder - await fs.rm(outdir, { recursive: true, force: true }) - await fs.mkdir(outdir, { recursive: true }) - - // build app - let ctx = await esbuild.context({ - bundle: true, - minify: isProduction, - // target: ["es2020"], - target: ["es2020"], //["chrome64", "firefox67", "safari11.1", "edge79"], - format: "esm", - platform: "browser", - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - loader: { ".js": "jsx" }, - sourcemap: !isProduction, - // here we exclude package from bundle which are defined in peerDependencies - // our importmap generator uses also the peerDependencies to create the importmap - // it means all packages defined in peerDependencies are in browser available via the importmap - external: - isProduction && !IGNORE_EXTERNALS - ? Object.keys(pkg.peerDependencies || {}) - : [], - entryPoints: [pkg.source], - outdir, - // this step is important for performance reason. - // the main file (index.js) contains minimal code needed to - // load the app via dynamic import (splitting: true) - splitting: true, - // we suport only esm! - format: "esm", - plugins: [ - // minimal plugin to log the recompiling process. - { - name: "start/end", - setup(build) { - build.onStart(() => { - console.log(clear) - console.log(yellow, "Compiling...") - }) - build.onEnd(() => console.log(green, "Done!")) - }, - }, - - // this custom plugin rewrites SVG imports to - // dataurls, paths or react components based on the - // search param and size - { - name: "svg-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(svg)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - let loader = "text" - if (args.suffix === "?url") { - // as URL - const maxSize = 10240 // 10Kb - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - } else { - // as react component - // use react component loader (jsx) - loader = "jsx" - contents = await transform(contents, { - plugins: ["@svgr/plugin-jsx"], - }) - } - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin rewrites image imports to - // dataurls or urls based on the size - { - name: "image-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(png|jpg|jpeg|gif)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - const maxSize = 10240 // 10Kb - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin parses the style files - { - name: "parse-styles", - setup(build) { - build.onLoad( - // consider only .scss and .css files - { filter: /.\.(css|scss)$/, namespace: "file" }, - async (args) => { - let content - // handle scss, convert to css - if (args.path.endsWith(".scss")) { - const result = sass.renderSync({ file: args.path }) - content = result.css - } else { - // read file content - content = await fs.readFile(args.path) - } - - // postcss plugins - const plugins = [ - require("tailwindcss"), - require("autoprefixer"), - // rewrite urls inside css - url({ - url: "inline", - // maxSize: 10, // use dataurls if files are smaller than 10k - // fallback: "copy", // if files are bigger use copy method - // assetsPath: "./build/assets", - // useHash: true, - // optimizeSvgEncode: true, - }), - ] - - const { css } = await postcss(plugins).process(content, { - from: args.path, - to: outdir, - }) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - return { contents: css, loader: "text" } - } - ) - }, - }, - ], - }) - - // watch and serve - if (watch || serve) { - if (watch) await ctx.watch() - if (serve) { - // generate app props based on package.json and secretProps.json - await fs.writeFile( - `./${outdir}/appProps.js`, - `export default ${JSON.stringify(appProps())}` - ) - - let { host, port } = await ctx.serve({ - host: "0.0.0.0", - port: parseInt(process.env.APP_PORT || process.env.PORT || 3000), - servedir: "public", - }) - console.log("serve on", `${host}:${port}`) - } - } else { - await ctx.rebuild() - await ctx.dispose() - } -} - -build() diff --git a/apps/greenhouse-management/jest.config.js b/apps/greenhouse-management/jest.config.js deleted file mode 100644 index cdc33f045..000000000 --- a/apps/greenhouse-management/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - transform: { "\\.[jt]sx?$": "babel-jest" }, - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/setupTests.js"], - transformIgnorePatterns: [ - "node_modules/(?!(juno-ui-components|url-state-router|communicator|oauth|url-state-provider|messages-provider|policy-engine)/)", - ], - moduleNameMapper: { - // Jest currently doesn't support resources with query parameters. - // Therefore we add the optional query parameter matcher at the end - // https://github.com/facebook/jest/issues/4181 - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?.+)?$": - require.resolve("./__mocks__/fileMock"), - "\\.(css|less|scss)$": require.resolve("./__mocks__/styleMock"), - }, -} diff --git a/apps/greenhouse-management/package.json b/apps/greenhouse-management/package.json deleted file mode 100644 index 726518edb..000000000 --- a/apps/greenhouse-management/package.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "name": "greenhouse-management", - "version": "1.1.13", - "managementPluginConfig": { - "clusters": { - "label": "Clusters", - "name": "greenhouse-cluster-admin", - "version": "1.6.7" - }, - "teams": { - "label": "Teams", - "name": "greenhouse-team-admin", - "version": "1.5.2" - }, - "plugins": { - "label": "Plugins", - "name": "greenhouse-plugin-admin", - "version": "1.0.7" - } - }, - "author": "UI-Team", - "contributors": [ - "Arturo Reuschenbach Puncernau", - "Tillman Haupt" - ], - "repository": "https://github.com/sapcc/juno/tree/main/apps/greenhouse-management", - "license": "MIT", - "source": "src/index.js", - "module": "build/index.js", - "private": true, - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sapcc-k8sclient": "^1.0.2", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "utils": "*", - "zustand": "4.3.7", - "esbuild": "^0.19.5" - }, - "scripts": { - "test": "jest", - "start": "NODE_ENV=development node esbuild.config.js --serve --watch", - "build": "NODE_ENV=production node esbuild.config.js" - }, - "peerDependencies": { - "@tanstack/react-query": "4.28.0", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", - "zustand": "4.3.7" - }, - "importmapExtras": { - "zustand/middleware": "4.3.7" - }, - "appProps": { - "theme": { - "value": "theme-dark", - "type": "optional", - "description": "Override the default theme. Possible values are theme-light or theme-dark (default)" - }, - "assetsUrl": { - "value": "URL to the assets server", - "type": "required", - "description": "This value is usually set by the Widget Loader. However, if this app is loaded via import or importShim, then this props parameter should be set." - }, - "apiEndpoint": { - "value": "", - "type": "required", - "description": "Endpoint URL of the API" - }, - "embedded": { - "value": "false", - "type": "optional", - "description": "Set to true if app is to be embedded in another existing app or page, like e.g. Elektra. If set to true the app won't render a page header/footer and instead render only the content. The default value is false." - }, - "environment": { - "value": "production", - "type": "optional", - "description": "environment name, e.g. production, qa, development, etc. This property can be used to load different plugins for different environments." - } - }, - "appDependencies": { - "auth": "latest" - }, - "appPreview": true -} \ No newline at end of file diff --git a/apps/greenhouse-management/public/favicon-16x16.png b/apps/greenhouse-management/public/favicon-16x16.png deleted file mode 100644 index 7e9bcaa7f..000000000 Binary files a/apps/greenhouse-management/public/favicon-16x16.png and /dev/null differ diff --git a/apps/greenhouse-management/public/favicon-32x32.png b/apps/greenhouse-management/public/favicon-32x32.png deleted file mode 100644 index dfe0b8018..000000000 Binary files a/apps/greenhouse-management/public/favicon-32x32.png and /dev/null differ diff --git a/apps/greenhouse-management/public/favicon.ico b/apps/greenhouse-management/public/favicon.ico deleted file mode 100644 index 3effd3438..000000000 Binary files a/apps/greenhouse-management/public/favicon.ico and /dev/null differ diff --git a/apps/greenhouse-management/public/index.html b/apps/greenhouse-management/public/index.html deleted file mode 100644 index acf58ccd9..000000000 --- a/apps/greenhouse-management/public/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - Template Dev - - - - - -
- - diff --git a/apps/greenhouse-management/secretProps.template.json b/apps/greenhouse-management/secretProps.template.json deleted file mode 100644 index 1b12d4815..000000000 --- a/apps/greenhouse-management/secretProps.template.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "assetsUrl": "https://assets.server.com/", - "endpoint": "https://endpoint/api/v1", - "appDependencies": { - "auth": { - "authIssuerUrl": "https://auth.backend.com/", - "authClientId": "clientId" - } - } -} diff --git a/apps/greenhouse-management/setupTests.js b/apps/greenhouse-management/setupTests.js deleted file mode 100644 index db44c9038..000000000 --- a/apps/greenhouse-management/setupTests.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom" diff --git a/apps/greenhouse-management/src/App.js b/apps/greenhouse-management/src/App.js deleted file mode 100644 index f3172b51c..000000000 --- a/apps/greenhouse-management/src/App.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" - -import { - AppBody, - AppShellProvider, - MainContainer, - MainContainerInner, - ContentContainer, -} from "juno-ui-components" -import StoreProvider from "./components/StoreProvider" -import UrlState from "./components/UrlState" - -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import AppContent from "./AppContent" -import styles from "./styles.scss" -import OrgInfo from "./components/OrgInfo" -import SideNav from "./components/SideNav" -import AsyncWorker from "./components/AsyncWorker" -import { MessagesProvider, Messages } from "messages-provider" -import Auth from "./components/Auth" - -const App = (props = {}) => { - // to be deleted - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - meta: { - endpoint: props.endpoint || props.currentHost || "", - }, - }, - }, - }) - - // support only embeded mode for now. This will probably never be started standalone - // page layout is copied from juno-ui-components/src/components/AppShell/AppShell.component.js - return ( - - - - - - - - - - - - - - - - - - - - ) -} - -const StyledApp = (props) => { - return ( - - - - - - - - - ) -} - -export default StyledApp diff --git a/apps/greenhouse-management/src/App.test.js b/apps/greenhouse-management/src/App.test.js deleted file mode 100644 index 3605e4c6b..000000000 --- a/apps/greenhouse-management/src/App.test.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { render, act } from "@testing-library/react" -// support shadow dom queries -// https://reactjsexample.com/an-extension-of-dom-testing-library-to-provide-hooks-into-the-shadow-dom/ -import { screen } from "shadow-dom-testing-library" -import App from "./App" - -test("renders app", async () => { - await act(() => render()) - - expect(screen.getByShadowTestId("greenhouse-management")).toBeInTheDocument() - -}) diff --git a/apps/greenhouse-management/src/AppContent.js b/apps/greenhouse-management/src/AppContent.js deleted file mode 100644 index 585902d70..000000000 --- a/apps/greenhouse-management/src/AppContent.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useLayoutEffect } from "react" -import PluginContainer from "./components/PluginContainer" -import { useApiEndpoint, useAssetsUrl } from "./components/StoreProvider" -import { useActions as messageActions } from "messages-provider" -import { Container } from "juno-ui-components" - -const AppContent = () => { - const { addMessage } = messageActions() - const apiEndpoint = useApiEndpoint() - const assetsUrl = useAssetsUrl() - - useLayoutEffect(() => { - if (!apiEndpoint) { - addMessage({ - variant: "warning", - text: " required api endpoint not set", - }) - } - - if (!assetsUrl) { - addMessage({ - variant: "warning", - text: "required assets url not set", - }) - } - - // Make these two props required - // if a required prop is missing do not set the assetsUrl and no plugin will be loaded - if (!apiEndpoint || !assetsUrl) return - }, []) - - return ( - - - - ) -} - -export default AppContent diff --git a/apps/greenhouse-management/src/assets/.gitkeep b/apps/greenhouse-management/src/assets/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/greenhouse-management/src/assets/juno-danger.svg b/apps/greenhouse-management/src/assets/juno-danger.svg deleted file mode 100644 index cb8cfcd53..000000000 --- a/apps/greenhouse-management/src/assets/juno-danger.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/greenhouse-management/src/assets/map.svg b/apps/greenhouse-management/src/assets/map.svg deleted file mode 100644 index 936f0f29b..000000000 --- a/apps/greenhouse-management/src/assets/map.svg +++ /dev/null @@ -1,4421 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - EU-DE-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - EU-DE-2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - EU-NL-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-SA-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-SA-2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-AE-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-AU-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-CN-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-JP-2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - AP-JP-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NA-CA-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NA-US-1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NA-US-3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - NA-US-2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - LA-BR-1 - - - - - diff --git a/apps/greenhouse-management/src/assets/rocket.gif b/apps/greenhouse-management/src/assets/rocket.gif deleted file mode 100644 index 6edcdce74..000000000 Binary files a/apps/greenhouse-management/src/assets/rocket.gif and /dev/null differ diff --git a/apps/greenhouse-management/src/components/AsyncWorker.jsx b/apps/greenhouse-management/src/components/AsyncWorker.jsx deleted file mode 100644 index 637f47ed8..000000000 --- a/apps/greenhouse-management/src/components/AsyncWorker.jsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import useUrlState from "../hooks/useUrlState" -import useCommunication from "../hooks/useCommunication" - -const AsyncWorker = () => { - useUrlState() - useCommunication() - return null -} - -export default AsyncWorker diff --git a/apps/greenhouse-management/src/components/Auth.jsx b/apps/greenhouse-management/src/components/Auth.jsx deleted file mode 100644 index 3c730ffaa..000000000 --- a/apps/greenhouse-management/src/components/Auth.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { useIsLoggedIn } from "./StoreProvider" -import HintLoading from "./shared/HintLoading" - -// Adds a loading screen while during auth -// Shows children when auth is complete -const Auth = ({ children }) => { - const authLoggedIn = useIsLoggedIn() - - return ( - <> - {!!authLoggedIn ? ( - children - ) : ( - - )} - - ) -} - -export default Auth diff --git a/apps/greenhouse-management/src/components/OrgInfo.jsx b/apps/greenhouse-management/src/components/OrgInfo.jsx deleted file mode 100644 index 01e0206af..000000000 --- a/apps/greenhouse-management/src/components/OrgInfo.jsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo, useEffect, useState } from "react" -import { createClient } from "sapcc-k8sclient" -import { useApiEndpoint, useAuthData } from "./StoreProvider" -import { useActions } from "messages-provider" - -// Shows organization info - -const OrgInfo = () => { - const apiEndpoint = useApiEndpoint() - const [org, setOrg] = useState(null) - const { addMessage } = useActions() - - const authData = useAuthData() - - const orgName = useMemo(() => { - if (!authData?.raw?.groups) return null - const orgString = authData?.raw?.groups.find( - (g) => g.indexOf("organization:") === 0 - ) - if (!orgString) return null - return orgString.split(":")[1] - }, [authData?.raw?.groups]) - - const client = useMemo(() => { - if (!apiEndpoint || !authData?.JWT) return null - return createClient({ apiEndpoint, token: authData?.JWT }) - }, [apiEndpoint, authData?.JWT]) - - useEffect(() => { - if (!client || !orgName) return - // plugin configs - client - .get(`/apis/greenhouse.sap/v1alpha1/organizations/${orgName}`) - .then((res) => { - setOrg({ - name: res?.spec?.displayName, - description: res?.spec?.description, - }) - }) - .catch((err) => { - addMessage({ - variant: "error", - text: `Failed to fetch organization info. ${err.message}`, - }) - }) - }, [client, orgName]) - - return ( -
-
-

Organization

- {org?.name &&

{org?.name}

} - {!org?.name &&

Loading...

} -
- {org?.description && ( -

{org?.description}

- )} - {!org?.name &&

} - {/*
- -

Thing 1

-
- 23 -
-
- - -

Thing 2

-
- 42 -
-
- - -

Thing 3

-
- 4711 -
-
-
*/} -
- ) -} - -export default OrgInfo diff --git a/apps/greenhouse-management/src/components/Plugin.jsx b/apps/greenhouse-management/src/components/Plugin.jsx deleted file mode 100644 index 1f1e06d1d..000000000 --- a/apps/greenhouse-management/src/components/Plugin.jsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect, useMemo, useRef, useState } from "react" -import { useAppLoader } from "utils" -import { useAssetsUrl, usePluginActive } from "./StoreProvider" -import { Messages, useActions } from "messages-provider" -import { parseError } from "../lib/helpers" -import { Stack, Button } from "juno-ui-components" -import HintLoading from "./shared/HintLoading" - -const Plugin = ({ config }) => { - const { addMessage } = useActions() - const assetsUrl = useAssetsUrl() - const { mount } = useAppLoader(assetsUrl) - const holder = useRef() - const activePlugin = usePluginActive() - - // local state - const [displayReload, setDisplayReload] = useState(false) - const [reload, setReload] = useState(0) - const [isMountedApp, setIsMountedApp] = useState(false) - - // element to mount the app - const el = document.createElement("div") - el.classList.add("inline") - const app = useRef(el) - - // mount the app each time the component is reloaded losing the state - useEffect(() => { - if (!mount || !assetsUrl || !config) return - // mount the app - mount(app.current, config) - .then((loaded) => { - if (!loaded) return - setIsMountedApp(true) - }) - .catch((error) => { - setDisplayReload(true) - addMessage({ - variant: "error", - text: `${config?.name}: ` + parseError(error), - }) - }) - }, [mount, reload, config, assetsUrl]) - - const displayPluging = useMemo( - () => activePlugin === config?.name, - [activePlugin, config] - ) - - useEffect(() => { - // if assetsUrl still null when rendering for first time the component then mountApp also return null and we skip here - if (!isMountedApp) return - - if (displayPluging) { - // append to holder - holder.current.appendChild(app.current) - } else { - // remove from holder - if (holder.current.contains(app.current)) - holder.current.removeChild(app.current) - } - }, [isMountedApp, displayPluging]) - - return ( -
- {displayPluging && ( - <> - - {!isMountedApp && !displayReload && } - {displayReload && ( - -

- Uh-oh! Our plugin {config?.label} encountered a hiccup.{" "} -

-

- No worries, just give it a little nudge by clicking the{" "} - Reload button below. -

-
- ) -} - -export default Plugin diff --git a/apps/greenhouse-management/src/components/PluginContainer.jsx b/apps/greenhouse-management/src/components/PluginContainer.jsx deleted file mode 100644 index 45d7f8b04..000000000 --- a/apps/greenhouse-management/src/components/PluginContainer.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from "react" -import { Container } from "juno-ui-components" -import { usePluginConfig } from "./StoreProvider" -import Plugin from "./Plugin" -import { MessagesProvider } from "messages-provider" - -const PluginContainer = () => { - const pluginConfig = usePluginConfig() - - return ( - <> - {Object.keys(pluginConfig).map((key, index) => ( - - - - ))} - - ) -} - -export default PluginContainer diff --git a/apps/greenhouse-management/src/components/SideNav.js b/apps/greenhouse-management/src/components/SideNav.js deleted file mode 100644 index 26a9b14c5..000000000 --- a/apps/greenhouse-management/src/components/SideNav.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { SideNavigation, SideNavigationItem } from "juno-ui-components" -import { usePluginConfig, usePluginActive, useActions } from "./StoreProvider" - -const SideNav = () => { - const pluginConfig = usePluginConfig() - const pluginActive = usePluginActive() - const { setPluginActive } = useActions() - - return ( - - {Object.keys(pluginConfig).map((key, index) => ( - setPluginActive(pluginConfig[key]?.name)} - /> - ))} - - ) -} - -export default SideNav diff --git a/apps/greenhouse-management/src/components/StoreProvider.js b/apps/greenhouse-management/src/components/StoreProvider.js deleted file mode 100644 index 58596117a..000000000 --- a/apps/greenhouse-management/src/components/StoreProvider.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { createContext, useContext } from "react" -import { useStore as create } from "zustand" -import createStore from "../lib/store" - -const StoreContext = createContext() -const StoreProvider = ({ options, children }) => ( - - {children} - -) - -const useAppStore = (selector) => create(useContext(StoreContext), selector) - -export const useIsUrlStateSetup = () => - useAppStore((state) => state.isUrlStateSetup) -export const useAssetsUrl = () => useAppStore((state) => state.assetsUrl) -export const usePluginConfig = () => useAppStore((state) => state.pluginConfig) -export const usePluginActive = () => useAppStore((state) => state.pluginActive) -export const useApiEndpoint = () => useAppStore((state) => state.apiEndpoint) -export const useAuthData = () => useAppStore((state) => state.authData.data) -export const useAuthAppLoaded = () => - useAppStore((state) => state.authAppLoaded) -export const useIsLoggedIn = () => - useAppStore((state) => state.authData.loggedIn) - -export const useActions = () => useAppStore((state) => state.actions) - -export default StoreProvider diff --git a/apps/greenhouse-management/src/components/UrlState.jsx b/apps/greenhouse-management/src/components/UrlState.jsx deleted file mode 100644 index ba0316bdd..000000000 --- a/apps/greenhouse-management/src/components/UrlState.jsx +++ /dev/null @@ -1,20 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import HintLoading from "./shared/HintLoading" -import { useIsUrlStateSetup } from "./StoreProvider" - -const UrlState = ({ children }) => { - const isUrlStateSetup = useIsUrlStateSetup() - - return ( - <> - {isUrlStateSetup ? children : } - - ) -} - -export default UrlState diff --git a/apps/greenhouse-management/src/components/shared/HintLoading.js b/apps/greenhouse-management/src/components/shared/HintLoading.js deleted file mode 100644 index b4091b753..000000000 --- a/apps/greenhouse-management/src/components/shared/HintLoading.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { Stack, Spinner } from "juno-ui-components" - -const centeredProps = { - alignment: "center", - distribution: "center", - direction: "vertical", - className: "h-full", -} - -const HintLoading = ({ text, centered }) => { - const stackProps = useMemo(() => { - return centered ? centeredProps : {} - }, [centered]) - - return ( - - - - {text ? {text} : Loading...} - - - ) -} - -export default HintLoading diff --git a/apps/greenhouse-management/src/hooks/.gitkeep b/apps/greenhouse-management/src/hooks/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/greenhouse-management/src/hooks/useCommunication.js b/apps/greenhouse-management/src/hooks/useCommunication.js deleted file mode 100644 index 7e1e7390b..000000000 --- a/apps/greenhouse-management/src/hooks/useCommunication.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect } from "react" -import { get, watch } from "communicator" -import { useActions } from "../components/StoreProvider" - -const useCommunication = () => { - const { setAuthData: setAuthData } = useActions() - const { setAuthAppLoaded: setAuthAppLoaded } = useActions() - - useEffect(() => { - if (!setAuthData || !setAuthAppLoaded) return - get("AUTH_APP_LOADED", setAuthAppLoaded, { - consumerID: "greenhouse-management", - debug: true, - }) - const unwatchLoaded = watch("AUTH_APP_LOADED", setAuthAppLoaded, { - debug: true, - consumerID: "greenhouse-management", - }) - - get("AUTH_GET_DATA", setAuthData, { - consumerID: "greenhouse-management", - debug: true, - }) - const unwatchUpdate = watch("AUTH_UPDATE_DATA", setAuthData, { - debug: true, - consumerID: "greenhouse-management", - }) - - return () => { - if (unwatchLoaded) unwatchLoaded() - if (unwatchUpdate) unwatchUpdate() - } - }, [setAuthData, setAuthAppLoaded]) -} - -export default useCommunication diff --git a/apps/greenhouse-management/src/hooks/useUrlState.js b/apps/greenhouse-management/src/hooks/useUrlState.js deleted file mode 100644 index 22377a222..000000000 --- a/apps/greenhouse-management/src/hooks/useUrlState.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect, useLayoutEffect } from "react" -import { registerConsumer } from "url-state-provider" -import { - useActions, - useIsUrlStateSetup, - usePluginActive, - useIsLoggedIn, -} from "../components/StoreProvider" - -// url state manager -const URL_APP_STATE_KEY = "greenhouse-management" -const ACTIVE_APP_KEY = "a" -const urlStateManager = registerConsumer(URL_APP_STATE_KEY) - -const useUrlState = () => { - const { setPluginActive, setIsUrlStateSetup } = useActions() - const isUrlStateSetup = useIsUrlStateSetup() - const pluginActive = usePluginActive() - const isLoggedIn = useIsLoggedIn() - - // Initial state from URL AFTER - // WARNING. To get the right state from the URL do following: - // If this app is embbeded in another app with authentication - // - Mount this app after the login is success in the parent app - // or - // - Wait here until you get logged in - useLayoutEffect(() => { - if (!isLoggedIn || isUrlStateSetup) return - - let active = urlStateManager.currentState()?.[ACTIVE_APP_KEY] - if (active) setPluginActive(active) - setIsUrlStateSetup(true) - }, [isUrlStateSetup, isLoggedIn]) - - // sync URL state - useEffect(() => { - if (!isUrlStateSetup) return - - // if the current state is the same as the new state, don't push - // this prevents the history from being filled with the same state - // and therefore prevents the forward button from being disabled - // This small optimization allows the user to go back and forth! - if (urlStateManager.currentState()?.[ACTIVE_APP_KEY] === pluginActive) - return - - urlStateManager.push({ [ACTIVE_APP_KEY]: pluginActive }) - }, [isUrlStateSetup, pluginActive]) -} - -export default useUrlState diff --git a/apps/greenhouse-management/src/index.js b/apps/greenhouse-management/src/index.js deleted file mode 100644 index f6f988315..000000000 --- a/apps/greenhouse-management/src/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createRoot } from "react-dom/client" -import React from "react" - -// export mount and unmount functions -export const mount = (container, options = {}) => { - import("./App").then((App) => { - mount.root = createRoot(container) - mount.root.render(React.createElement(App.default, options?.props)) - }) -} - -export const unmount = () => mount.root && mount.root.unmount() diff --git a/apps/greenhouse-management/src/lib/helpers.js b/apps/greenhouse-management/src/lib/helpers.js deleted file mode 100644 index 531d03702..000000000 --- a/apps/greenhouse-management/src/lib/helpers.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const parseError = (error) => { - let errMsg = JSON.stringify(error) - if (error?.message) { - errMsg = error?.message - try { - errMsg = JSON.parse(error?.message).msg - } catch (error) {} - } - return errMsg -} diff --git a/apps/greenhouse-management/src/lib/store.js b/apps/greenhouse-management/src/lib/store.js deleted file mode 100644 index cd1a3c95b..000000000 --- a/apps/greenhouse-management/src/lib/store.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createStore } from "zustand" -import { devtools } from "zustand/middleware" -import { managementPluginConfig } from "../../package.json" -import { useActions as messageActions } from "messages-provider" - -export default (options) => { - // check the managementPluginConfig is an object and not array or string - const { addMessage } = messageActions() - let configs = managementPluginConfig - - // check if the managementPluginConfig is an object with key values - if ( - typeof configs !== "object" || - Array.isArray(configs) || - Object.keys(configs).length === 0 - ) { - configs = {} - addMessage({ - variant: "error", - text: "managementPluginConfig is not an object with key values in the package.json", - }) - } - - // set the endpoint and embedded props for the management plugin coming from the package.json - Object.keys(configs).forEach((key) => { - // pull latest version in dev and qa - configs[key].version = options.environment =='qa' || options.environment == 'development' ? 'latest' : configs[key].version - configs[key].props = { - endpoint: options.apiEndpoint, - embedded: true, - } - }) - - return createStore( - devtools((set, get) => ({ - isUrlStateSetup: false, - assetsUrl: options.assetsUrl, - apiEndpoint: options.apiEndpoint, - pluginConfig: configs, - authData: { - loggedIn: false, - error: null, - data: null, - }, - authAppLoaded: false, - pluginActive: "greenhouse-cluster-admin", // name of the active plugin default - - actions: { - setPluginActive: (pluginId) => { - // find the pluginConfig which plugin name matches the plugin id - const plugin = Object.values(get().pluginConfig).find( - (plugin) => plugin.name === pluginId - ) - if (!plugin) return - - set( - (state) => { - state.pluginActive = plugin.name - }, - false, - "setPluginActive" - ) - }, - setIsUrlStateSetup: (isSetup) => - set( - (state) => { - state.isUrlStateSetup = isSetup - }, - false, - "setIsUrlStateSetup" - ), - setAuthData: (data) => - set( - (state) => ({ - authData: { - ...state.auth, - loggedIn: data ? data.loggedIn : false, - error: data ? data.error : null, - data: data ? data.auth : null, - }, - }), - false, - "setAuthData" - ), - setAuthAppLoaded: (loaded) => - set( - (state) => { - state.authAppLoaded = loaded - }, - false, - "setAuthAppLoaded" - ), - }, - })) - ) -} diff --git a/apps/greenhouse-management/src/styles.scss b/apps/greenhouse-management/src/styles.scss deleted file mode 100644 index 8e3a75e65..000000000 --- a/apps/greenhouse-management/src/styles.scss +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors -// SPDX-License-Identifier: Apache-2.0 - -/* Do not remove these tailwind directives. Without them styles won't work as expected */ -@tailwind base; -@tailwind components; -@tailwind utilities; - - -/* If necessary, app styles can be added below */ - -.svg-bg-test { - background: url('assets/juno-danger.svg') -} - -// .svg-bg-test-big-file { -// background: left 80px no-repeat url('assets/map.svg') -// } - - diff --git a/apps/greenhouse-management/tailwind.config.js b/apps/greenhouse-management/tailwind.config.js deleted file mode 100644 index 81b1f8fef..000000000 --- a/apps/greenhouse-management/tailwind.config.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// opacity helper to make custom colors work with opacity -function withOpacity(variableName) { - return ({ opacityVariable, opacityValue }) => { - if (opacityValue !== undefined) { - return `rgba(var(${variableName}), ${opacityValue})` - } - if (opacityVariable !== undefined) { - return `rgba(var(${variableName}), var(${opacityVariable}, 1))` - } - return `rgb(var(${variableName}))` - } -} - -module.exports = { - presets: [ - require("juno-ui-components/build/lib/tailwind.config"), // important, do not change - ], - prefix: "", // important, do not change - content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], - corePlugins: { - preflight: false, // important, do not change - }, - theme: {}, - plugins: [], -} diff --git a/apps/greenhouse/.gitignore b/apps/greenhouse/.gitignore deleted file mode 100644 index dbaccf123..000000000 --- a/apps/greenhouse/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.plugin.config.yaml diff --git a/apps/greenhouse/LICENSE b/apps/greenhouse/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/apps/greenhouse/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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/apps/greenhouse/README.md b/apps/greenhouse/README.md deleted file mode 100644 index 055d183d9..000000000 --- a/apps/greenhouse/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Greenhouse App - -This is the shell app for Project Greenhouse. It is the host for all apps that are part of Greenhouse. diff --git a/apps/greenhouse/__mocks__/client.js b/apps/greenhouse/__mocks__/client.js deleted file mode 100644 index 84531d3c2..000000000 --- a/apps/greenhouse/__mocks__/client.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { JSDOM } from "jsdom" -const dom = new JSDOM() -global.document = dom.window.document -global.window = dom.window diff --git a/apps/greenhouse/__mocks__/styleMock.js b/apps/greenhouse/__mocks__/styleMock.js deleted file mode 100644 index d74516001..000000000 --- a/apps/greenhouse/__mocks__/styleMock.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = {} diff --git a/apps/greenhouse/babel.config.js b/apps/greenhouse/babel.config.js deleted file mode 100644 index 0719e2fec..000000000 --- a/apps/greenhouse/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - env: { - test: { - presets: ["@babel/preset-env", "@babel/preset-react"], - plugins: [["babel-plugin-transform-import-meta", { module: "ES6" }]], - }, - }, -} diff --git a/apps/greenhouse/esbuild.config.js b/apps/greenhouse/esbuild.config.js deleted file mode 100644 index 2394388b8..000000000 --- a/apps/greenhouse/esbuild.config.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const esbuild = require("esbuild") -const fs = require("node:fs/promises") -const pkg = require("./package.json") -const postcss = require("postcss") -const sass = require("sass") -const { transform } = require("@svgr/core") -const url = require("postcss-url") -// this function generates app props based on package.json and propSecrets.json -const appProps = require("../../helpers/appProps") - -if (!/.+\/.+\.js/.test(pkg.module)) - throw new Error( - "module value is incorrect, use DIR/FILE.js like build/index.js" - ) - -const isProduction = process.env.NODE_ENV === "production" -// If the jspm server fails and we cannot use external packages -// in our import map then IGNORE_EXTERNALS (global env variable) -// should be set to true -const IGNORE_EXTERNALS = process.env.IGNORE_EXTERNALS === "true" -// in dev environment we prefix output file with public -let outfile = `${isProduction ? "" : "public/"}${pkg.main || pkg.module}` -// get output from outputfile -let outdir = outfile.slice(0, outfile.lastIndexOf("/")) -const args = process.argv.slice(2) -const watch = args.indexOf("--watch") >= 0 -const serve = args.indexOf("--serve") >= 0 - -// helpers for console log -const green = "\x1b[32m%s\x1b[0m" -const yellow = "\x1b[33m%s\x1b[0m" -const clear = "\033c" - -const build = async () => { - // delete build folder and re-create it as an empty folder - await fs.rm(outdir, { recursive: true, force: true }) - await fs.mkdir(outdir, { recursive: true }) - - // build app - let ctx = await esbuild.context({ - bundle: true, - minify: isProduction, - // target: ["es2020"], - target: ["es2020"], //["chrome64", "firefox67", "safari11.1", "edge79"], - format: "esm", - platform: "browser", - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - loader: { ".js": "jsx" }, - sourcemap: !isProduction, - // here we exclude package from bundle which are defined in peerDependencies - // our importmap generator uses also the peerDependencies to create the importmap - // it means all packages defined in peerDependencies are in browser available via the importmap - external: - isProduction && !IGNORE_EXTERNALS - ? Object.keys(pkg.peerDependencies || {}) - : [], - entryPoints: [pkg.source], - outdir, - // this step is important for performance reason. - // the main file (index.js) contains minimal code needed to - // load the app via dynamic import (splitting: true) - splitting: true, - // we suport only esm! - format: "esm", - plugins: [ - // minimal plugin to log the recompiling process. - { - name: "start/end", - setup(build) { - build.onStart(() => { - console.log(clear) - console.log(yellow, "Compiling...") - }) - build.onEnd(() => console.log(green, "Done!")) - }, - }, - - // this custom plugin rewrites SVG imports to - // dataurls, paths or react components based on the - // search param and size - { - name: "svg-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(svg)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - let loader = "text" - if (args.suffix === "?url") { - // as URL - const maxSize = 10240 // 10Kb - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - } else { - // as react component - // use react component loader (jsx) - loader = "jsx" - contents = await transform(contents, { - plugins: ["@svgr/plugin-jsx"], - }) - } - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin rewrites image imports to - // dataurls or urls based on the size - { - name: "image-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(png|jpg|jpeg|gif)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - const maxSize = 10240 // 10Kb - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin parses the style files - { - name: "parse-styles", - setup(build) { - build.onLoad( - // consider only .scss and .css files - { filter: /.\.(css|scss)$/, namespace: "file" }, - async (args) => { - let content - // handle scss, convert to css - if (args.path.endsWith(".scss")) { - const result = sass.renderSync({ file: args.path }) - content = result.css - } else { - // read file content - content = await fs.readFile(args.path) - } - - // postcss plugins - const plugins = [ - require("tailwindcss"), - require("autoprefixer"), - // rewrite urls inside css - url({ - url: "inline", - // maxSize: 10, // use dataurls if files are smaller than 10k - // fallback: "copy", // if files are bigger use copy method - // assetsPath: "./build/assets", - // useHash: true, - // optimizeSvgEncode: true, - }), - ] - - const { css } = await postcss(plugins).process(content, { - from: args.path, - to: outdir, - }) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - return { contents: css, loader: "text" } - } - ) - }, - }, - ], - }) - - // watch and serve - if (watch || serve) { - if (watch) await ctx.watch() - if (serve) { - // generate app props based on package.json and secretProps.json - await fs.writeFile( - `./${outdir}/appProps.js`, - `export default ${JSON.stringify(appProps())}` - ) - - let { host, port } = await ctx.serve({ - host: "0.0.0.0", - port: parseInt(process.env.APP_PORT || process.env.PORT || 3000), - servedir: "public", - }) - console.log("serve on", `${host}:${port}`) - } - } else { - await ctx.rebuild() - await ctx.dispose() - } -} - -build() diff --git a/apps/greenhouse/jest.config.js b/apps/greenhouse/jest.config.js deleted file mode 100644 index 0cb80394c..000000000 --- a/apps/greenhouse/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - transform: { "\\.[jt]sx?$": "babel-jest" }, - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/setupTests.js"], - transformIgnorePatterns: [ - "node_modules/(?!(juno-ui-components|communicator)/)", - ], - moduleNameMapper: { - // Jest currently doesn't support resources with query parameters. - // Therefore we add the optional query parameter matcher at the end - // https://github.com/facebook/jest/issues/4181 - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?.+)?$": - require.resolve("./__mocks__/fileMock"), - "\\.(css|less|scss)$": require.resolve("./__mocks__/styleMock"), - }, -} diff --git a/apps/greenhouse/package.json b/apps/greenhouse/package.json deleted file mode 100644 index df0c1778c..000000000 --- a/apps/greenhouse/package.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "name": "greenhouse", - "version": "0.1.20", - "managementVersion": "1.1.13", - "author": "UI-Team", - "contributors": [ - "Andreas Pfau", - "Arturo Reuschenbach Puncernau", - "Esther Schmitz" - ], - "repository": "https://github.com/sapcc/juno/tree/main/apps/greenhouse", - "license": "Apache-2.0", - "source": "src/index.js", - "module": "build/index.js", - "private": true, - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tailwindui/react": "^0.1.1", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "messages-provider": "*", - "immer": "^9.0.21", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sapcc-k8sclient": "^1.0.2", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "utils": "*", - "zustand": "4.3.7" - }, - "scripts": { - "test": "jest", - "start": "NODE_ENV=development node esbuild.config.js --serve --watch", - "build": "NODE_ENV=production node esbuild.config.js" - }, - "peerDependencies": { - "juno-ui-components": "*", - "messages-provider": "*", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", - "zustand": "4.3.7" - }, - "importmapExtras": { - "zustand/middleware": "4.3.7" - }, - "appProps": { - "authIssuerUrl": { - "value": "https://endpoint_url_of_the_openid_provider.com", - "type": "required", - "description": "Endpoint URL of the OpenID provider" - }, - "authClientId": { - "value": "tbd", - "type": "required", - "description": "OIDC client id. " - }, - "theme": { - "value": "theme-dark", - "description": "Override the default theme. Possible values are theme-light or theme-dark (default)" - }, - "embedded": { - "value": "false", - "description": "Set to true if app is to be embedded in another existing app or page, like e.g. Elektra. If set to true the app won't render a page header/footer and instead render only the content" - }, - "currentHost": { - "value": "URL TO ASSETS SERVER", - "type": "required", - "description": "This value is usually set by the Widget Loader. However, if this app is loaded via import or importShim, then this props parameter should be set." - }, - "apiEndpoint": { - "value": "URL TO K8S API", - "type": "required", - "description": "This value is necessary to communicate with the Kubernetes API. All the information you need comes from this API." - }, - "mockAuth": { - "value": false, - "type": "optional", - "description": "mock the OIDC data, allowed values are 'true', 'false' (default), or json (pure or base64 encoded)" - }, - "demoOrg": { - "value": "demo", - "type": "optional", - "description": "if organization name is equal to this value, then the app will be in demo mode. That means that the authentication will be mocked and plugins are loaded from demo org." - }, - "demoUserToken": { - "value": "token for demo user", - "type": "optional", - "description": "if both demoOrg and demoUserToken are set and organization name is equal to demoOrg, then this token will be used for authentication." - }, - "environment": { - "value": "production", - "type": "optional", - "description": "environment name, e.g. production, qa, development, etc. This property can be used to load different plugins for different environments." - } - }, - "appPreview": true -} \ No newline at end of file diff --git a/apps/greenhouse/public/android-chrome-192x192.png b/apps/greenhouse/public/android-chrome-192x192.png deleted file mode 100644 index 5459a8ded..000000000 Binary files a/apps/greenhouse/public/android-chrome-192x192.png and /dev/null differ diff --git a/apps/greenhouse/public/android-chrome-512x512.png b/apps/greenhouse/public/android-chrome-512x512.png deleted file mode 100644 index fb06c927e..000000000 Binary files a/apps/greenhouse/public/android-chrome-512x512.png and /dev/null differ diff --git a/apps/greenhouse/public/apple-touch-icon.png b/apps/greenhouse/public/apple-touch-icon.png deleted file mode 100644 index c76354909..000000000 Binary files a/apps/greenhouse/public/apple-touch-icon.png and /dev/null differ diff --git a/apps/greenhouse/public/favicon.ico b/apps/greenhouse/public/favicon.ico deleted file mode 100644 index 7e28ca348..000000000 Binary files a/apps/greenhouse/public/favicon.ico and /dev/null differ diff --git a/apps/greenhouse/public/favicon.svg b/apps/greenhouse/public/favicon.svg deleted file mode 100644 index ebe3eb28d..000000000 --- a/apps/greenhouse/public/favicon.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/greenhouse/public/index.html b/apps/greenhouse/public/index.html deleted file mode 100644 index 02fcbdbba..000000000 --- a/apps/greenhouse/public/index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - Greenhouse Dev - - - - - -
- - diff --git a/apps/greenhouse/secretProps.template.json b/apps/greenhouse/secretProps.template.json deleted file mode 100644 index 825e7f86c..000000000 --- a/apps/greenhouse/secretProps.template.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "authIssuerUrl": "https://auth.backend.com/", - "authClientId": "client-id", - "currentHost": "https://assets.server.com/", - "apiEndpoint": "https://api.greenhouse.com/", - "environment": "dev" -} diff --git a/apps/greenhouse/setupTests.js b/apps/greenhouse/setupTests.js deleted file mode 100644 index db44c9038..000000000 --- a/apps/greenhouse/setupTests.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom" diff --git a/apps/greenhouse/src/Shell.js b/apps/greenhouse/src/Shell.js deleted file mode 100644 index 05102e02f..000000000 --- a/apps/greenhouse/src/Shell.js +++ /dev/null @@ -1,62 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useLayoutEffect } from "react" - -import ShellLayout from "./components/layout/ShellLayout" -import Auth from "./components/Auth" -import styles from "./styles.scss" -import { AppShellProvider } from "juno-ui-components" -import PluginContainer from "./components/PluginContainer" -import AsyncWorker from "./components/AsyncWorker" -import StoreProvider, { useGlobalsActions } from "./components/StoreProvider" -import { MessagesProvider } from "messages-provider" - -const Shell = (props = {}) => { - const { setApiEndpoint, setAssetsHost, setDemoUserToken, setEnvironment } = - useGlobalsActions() - - // INIT - // on app initial load save Endpoint and URL_STATE_KEY so it can be - // used from overall in the application - useLayoutEffect(() => { - if (!setApiEndpoint || !setAssetsHost || !setDemoUserToken) return - // set to empty string to fetch local test data in dev mode - setEnvironment(props.environment) - setApiEndpoint(props.apiEndpoint) - setAssetsHost(props.currentHost) - setDemoUserToken(props.demoUserToken) - }, [setApiEndpoint, setAssetsHost, setDemoUserToken]) - - return ( - - - - - - ) -} - -const StyledShell = (props) => { - return ( - - {/* load styles inside the shadow dom */} - - - - - - - - - ) -} - -export default StyledShell diff --git a/apps/greenhouse/src/Shell.test.js b/apps/greenhouse/src/Shell.test.js deleted file mode 100644 index e8b0e3ed5..000000000 --- a/apps/greenhouse/src/Shell.test.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { render, act } from "@testing-library/react" -// support shadow dom queries -// https://reactjsexample.com/an-extension-of-dom-testing-library-to-provide-hooks-into-the-shadow-dom/ -import { screen } from "shadow-dom-testing-library" -import Shell from "./Shell" -import Auth from "./components/Auth" -import StoreProvider from "./components/StoreProvider" - -jest.mock("communicator") -jest.mock("./components/Auth") - -test("renders app", async () => { - await act(() => - render( - - - - ) - ) - - expect(Auth).toHaveBeenCalled() -}) diff --git a/apps/greenhouse/src/actions.js b/apps/greenhouse/src/actions.js deleted file mode 100644 index 4c818af6b..000000000 --- a/apps/greenhouse/src/actions.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const ENDPOINT = "https://endpoint-url-here.com" - -class HTTPError extends Error { - constructor(code, message) { - super(message || code) - this.name = "HTTPError" - this.statusCode = code - } -} - -const encodeUrlParamsFromObject = (options) => { - if (!options) return "" - let encodedOptions = Object.keys(options) - .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(options[k])}`) - .join("&") - return `&${encodedOptions}` -} - -// Check response status -const checkStatus = (response) => { - if (response.status < 400) { - return response - } else { - return response.text().then((message) => { - var error = new HTTPError(response.status, message || response.statusText) - error.statusCode = response.status - return Promise.reject(error) - }) - } -} - -// Example fetch call. Adjust as needed for your API -export const exampleFetch = ({ queryKey }) => { - const [_key, endpoint, options] = queryKey - const query = encodeUrlParamsFromObject(options) - return fetch(`${endpoint}/colors.json?${query}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - }) - .then(checkStatus) - .then((response) => { - return response.json() - }) -} diff --git a/apps/greenhouse/src/assets/ccloud_shape.svg b/apps/greenhouse/src/assets/ccloud_shape.svg deleted file mode 100644 index 04216ebd0..000000000 --- a/apps/greenhouse/src/assets/ccloud_shape.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/apps/greenhouse/src/assets/greenhouse_logo.svg b/apps/greenhouse/src/assets/greenhouse_logo.svg deleted file mode 100644 index 785a0527a..000000000 --- a/apps/greenhouse/src/assets/greenhouse_logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/apps/greenhouse/src/assets/juno_default_app.svg b/apps/greenhouse/src/assets/juno_default_app.svg deleted file mode 100644 index cc350ee28..000000000 --- a/apps/greenhouse/src/assets/juno_default_app.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/greenhouse/src/assets/juno_doop.svg b/apps/greenhouse/src/assets/juno_doop.svg deleted file mode 100644 index ddf2c0308..000000000 --- a/apps/greenhouse/src/assets/juno_doop.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/greenhouse/src/assets/juno_heureka.svg b/apps/greenhouse/src/assets/juno_heureka.svg deleted file mode 100644 index 9ee687510..000000000 --- a/apps/greenhouse/src/assets/juno_heureka.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/apps/greenhouse/src/assets/juno_supernova.svg b/apps/greenhouse/src/assets/juno_supernova.svg deleted file mode 100644 index 16781ca3e..000000000 --- a/apps/greenhouse/src/assets/juno_supernova.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/greenhouse/src/components/AsyncWorker.jsx b/apps/greenhouse/src/components/AsyncWorker.jsx deleted file mode 100644 index de26545c4..000000000 --- a/apps/greenhouse/src/components/AsyncWorker.jsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from "react" -import useUrlState from "../hooks/useUrlState" -import useCommunication from "../hooks/useCommunication" -import { useAuthData, useAuthLoggedIn } from "../components/StoreProvider" - -const currentUrl = new URL(window.location.href) -let match = currentUrl.host.match(/^(.+)\.dashboard\..+/) -let orgName = match ? match[1] : currentUrl.searchParams.get("org") - -const AsyncWorker = () => { - const authData = useAuthData() - const authLoggedIn = useAuthLoggedIn() - - useCommunication() - useUrlState() - - // read org name from token and adjust url to contain the org name - useEffect(() => { - if (!authLoggedIn) return - - if (!orgName) { - const orgString = authData?.raw?.groups?.find( - (g) => g.indexOf("organization:") === 0 - ) - - if (orgString) { - const name = orgString.split(":")[1] - let url = new URL(window.location.href) - url.searchParams.set("org", name) - window.history.replaceState(null, null, url.href) - } - } - }, [authLoggedIn, authData]) - - return null -} - -export default AsyncWorker diff --git a/apps/greenhouse/src/components/Auth.jsx b/apps/greenhouse/src/components/Auth.jsx deleted file mode 100644 index 2405a930d..000000000 --- a/apps/greenhouse/src/components/Auth.jsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect, useState, createRef } from "react" -import { Button, LoadingIndicator, Spinner, Stack } from "juno-ui-components" -import { - useAuthAppLoaded, - useAuthLoggedIn, - useAuthIsProcessing, - useAuthError, - useAuthActions, - useGlobalsActions, - useGlobalsAssetsHost, -} from "../components/StoreProvider" -import { useAppLoader } from "utils" -import { useActions } from "messages-provider" - -const currentUrl = new URL(window.location.href) -let match = currentUrl.host.match(/^(.+)\.dashboard\..+/) -let orgName = match ? match[1] : currentUrl.searchParams.get("org") - -/** - * Auth Component: - * - * This component is responsible for managing user authentication and loading the authentication app dynamically. - * It receives the following props: - * - clientId: The client ID for authentication. - * - issuerUrl: The URL of the authentication issuer. - * - mock: A flag indicating whether to use mock authentication. - * - children: The content to be displayed when the user is logged in. - * - * The component uses custom hooks to handle authentication states and data. It dynamically loads the authentication - * app via the use of the useAppLoader hook. When mounted, the component connects to the authentication events, - * allowing seamless authentication experiences. - * - * The Auth component renders three main sections: - * 1. A div element with a data-app attribute set to "greenhouse-auth" and a ref for loading the authentication app. - * 2. If the user is logged in, the children are rendered. - * 3. If the user is not logged in, a stack containing loading indicators, messages, and a "Sign in" button is rendered. - * The component handles various loading states, shows a long loading indicator after 5 seconds, and displays specific - * messages based on the authentication status. - * - * Note: The component reads organization information from the token and adjusts the URL accordingly after the user is logged in. - */ -const Auth = ({ - clientId, - issuerUrl, - mock, - children, - demoOrg, - demoUserToken, -}) => { - const assetsHost = useGlobalsAssetsHost() - const authAppLoaded = useAuthAppLoaded() - const authLoggedIn = useAuthLoggedIn() - const authIsProcessing = useAuthIsProcessing() - const authError = useAuthError() - const { login } = useAuthActions() - const { setDemoMode } = useGlobalsActions() - const { addMessage } = useActions() - - const ref = createRef() - const { mount } = useAppLoader(assetsHost) - const [loading, setLoading] = useState(!authAppLoaded) - const [longLoading, setLongLoading] = useState(false) - - // in this useEffect we load the auth app via import (see mount) - // It should happen just once! - // The connection to the auth events happens in the useCommunication hook! - // wait until assetsHost is set to avoid a warning on mount - useEffect(() => { - if (!assetsHost || !clientId || !issuerUrl) return - - // if current orgName is the demo org, we mock the auth app - if (demoOrg === orgName) { - // we mock the auth app with default groups - mock = JSON.stringify({ - groups: ["organization:demo", "role:ccloud:admin"], - }) - // set demo mode - // see in useCommunication hook, there we redefine the authData.JWT wit demoUserToken if demo mode is set - setDemoMode(true) - } - - mount(ref.current, { - id: "auth", - name: "auth", - version: "latest", - props: { - issuerUrl: issuerUrl, - clientId: clientId, - mock: mock, - debug: true, - initialLogin: true, - requestParams: JSON.stringify({ - connector_id: !orgName ? undefined : orgName, - }), - }, - }) - // add mount to the dependencies since it changes depending on the assetsHost - }, [mount, clientId, issuerUrl, assetsHost]) - - // timeout for waiting for auth - useEffect(() => { - setLoading(!authAppLoaded) - if (authAppLoaded) return - // set timeout for waiting for auth app - let loadingTimer - if (!authAppLoaded) { - loadingTimer = setTimeout(() => { - if (!authAppLoaded) setLoading(false) - }, 30000) // 30 seconds - } - - return () => loadingTimer && clearTimeout(loadingTimer) - }, [authAppLoaded, setLoading]) - - // set long loading - useEffect(() => { - let longLoadingTimer = setTimeout(() => setLongLoading(true), 5000) // long loading if longer than 5 seconds - return () => longLoadingTimer && clearTimeout(longLoadingTimer) - }, []) - - return ( - <> -
- - {!!authLoggedIn && children} - - {!authLoggedIn && ( - - {loading || authIsProcessing ? ( - <> - {longLoading ? ( - - ) : ( - - )} - {loading ? "Loading..." : "Signing on..."} - - ) : ( - <> - {authAppLoaded ? ( - <> - - {authError - ? "You have been logged out. Please sign in again." - : "Please sign in before you can use Greenhouse."} - - - - ) : ( - "Looks like the auth app is missing!" - )} - - )} - - )} - - ) -} - -export default Auth diff --git a/apps/greenhouse/src/components/Avatar.jsx b/apps/greenhouse/src/components/Avatar.jsx deleted file mode 100644 index 6e41eb812..000000000 --- a/apps/greenhouse/src/components/Avatar.jsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack } from "juno-ui-components" - -const avatarCss = ` -h-8 -w-8 -bg-theme-background-lvl-2 -rounded-full -bg-cover -` - -const Avatar = ({ userName, url }) => { - return ( - - {url && ( -
- )} - {userName && {userName}} - - ) -} - -export default Avatar diff --git a/apps/greenhouse/src/components/NotificationsContainer.jsx b/apps/greenhouse/src/components/NotificationsContainer.jsx deleted file mode 100644 index 61afdffc8..000000000 --- a/apps/greenhouse/src/components/NotificationsContainer.jsx +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Messages } from "messages-provider" -import { useDemoMode } from "./StoreProvider" - -const NotificationsContainer = () => { - const demoMode = useDemoMode() - - return ( - <> - {demoMode && ( -
- Welcome to the Greenhouse demo system! We're glad you're here! Just a - quick heads up: you won't find any live data here. Enjoy exploring! -
- )} - {/* do not use a container here to align the messages to the ones coming from each plugin */} - - - ) -} - -export default NotificationsContainer diff --git a/apps/greenhouse/src/components/Plugin.jsx b/apps/greenhouse/src/components/Plugin.jsx deleted file mode 100644 index 45ac97e77..000000000 --- a/apps/greenhouse/src/components/Plugin.jsx +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect, useState, useMemo, useRef } from "react" -import { useAppLoader } from "utils" -import { usePlugin, useGlobalsAssetsHost } from "../components/StoreProvider" -import { Messages, useActions } from "messages-provider" -import { parseError } from "../lib/helpers" -import { Stack, Button } from "juno-ui-components" - -const Plugin = ({ id }) => { - const assetsHost = useGlobalsAssetsHost() - const { mount } = useAppLoader(assetsHost) - const holder = useRef() - const config = usePlugin().config() - const activeApps = usePlugin().active() - const { addMessage } = useActions() - - const [displayReload, setDisplayReload] = useState(false) - const [reload, setReload] = useState(0) - const [isMounted, setIsMounted] = useState(false) - - // element to mount the app - const el = document.createElement("div") - el.classList.add("inline") - const app = useRef(el) - - // mount the app each time the component is reloaded losing the state - useEffect(() => { - if (!mount || !assetsHost || !config) return - // mount the app - mount(app.current, { - ...config[id], - props: { ...config[id]?.props, embedded: true }, - }) - .then((loaded) => { - if (!loaded) return - setIsMounted(true) - }) - .catch((error) => { - setDisplayReload(true) - addMessage({ - variant: "error", - text: `${config?.name}: ` + parseError(error), - }) - }) - }, [mount, reload, config, assetsHost]) - - const displayPluging = useMemo( - () => activeApps.indexOf(id) >= 0, - [activeApps, config] - ) - - useEffect(() => { - if (!config[id] || !isMounted) return - - if (displayPluging) { - // add to holder - holder.current.appendChild(app.current) - } else { - // remove from holder - if (holder.current.contains(app.current)) - holder.current.removeChild(app.current) - } - }, [isMounted, displayPluging]) - - return ( -
- {displayPluging && ( - <> - - {displayReload && ( - -

- Uh-oh! Our plugin {config?.label} encountered a hiccup.{" "} -

-

- No worries, just give it a little nudge by clicking the{" "} - Reload button below. -

-
- ) -} - -export default Plugin diff --git a/apps/greenhouse/src/components/PluginContainer.jsx b/apps/greenhouse/src/components/PluginContainer.jsx deleted file mode 100644 index 70fe7e8d4..000000000 --- a/apps/greenhouse/src/components/PluginContainer.jsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import Plugin from "./Plugin" -import { usePlugin, useGlobalsEnvironment } from "../components/StoreProvider" -import useApi from "../hooks/useApi" -import { useLayoutEffect } from "react" -import HintLoading from "./shared/HintLoading" -import { parseError } from "../lib/helpers" -import { useActions, MessagesProvider } from "messages-provider" - -const PluginContainer = () => { - const { getPluginConfigs } = useApi() - const environment = useGlobalsEnvironment() - const config = usePlugin().config() - const isFetching = usePlugin().isFetching() - const { addMessage } = useActions() - - // prevent to load a plugin before the config is fetched to avoid rerendering do tue the default plugin greenhouse-mng - const [displayPlugin, setDisplayPlugin] = React.useState(false) - - const requestConfig = usePlugin().requestConfig - const receiveConfig = usePlugin().receiveConfig - const receiveError = usePlugin().receiveError - - const availableAppIds = React.useMemo(() => Object.keys(config), [config]) - - useLayoutEffect(() => { - if (!getPluginConfigs) return - requestConfig() - - // fetch configs from kubernetes - getPluginConfigs() - .then((kubernetesConfig) => { - receiveConfig(kubernetesConfig) - }) - .catch((error) => { - // error fetching configs - receiveError(error.message) - addMessage({ - variant: "error", - text: parseError(error), - }) - }) - .finally(() => { - setDisplayPlugin(true) - }) - }, [getPluginConfigs, environment]) - - return ( - <> - {displayPlugin && - availableAppIds.length > 0 && - availableAppIds.map((id, i) => ( - - - - ))} - {!isFetching && - !displayPlugin && - availableAppIds.length <= 0 && - "No plugins available."} - - ) -} - -export default PluginContainer diff --git a/apps/greenhouse/src/components/StoreProvider.jsx b/apps/greenhouse/src/components/StoreProvider.jsx deleted file mode 100644 index 15d54f269..000000000 --- a/apps/greenhouse/src/components/StoreProvider.jsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { createContext, useContext } from "react" -import { useStore as create } from "zustand" -import createStore from "../lib/store" - -const StoreContext = createContext() -const StoreProvider = ({ options, children }) => ( - - {children} - -) - -// build a hook from the store -const useStore = (selector) => create(useContext(StoreContext).store, selector) - -// AUTH -export const useAuthData = () => useStore((s) => s.auth.data) -export const useAuthIsProcessing = () => useStore((s) => s.auth.isProcessing) -export const useAuthLoggedIn = () => useStore((s) => s.auth.loggedIn) -export const useAuthError = () => useStore((s) => s.auth.error) -export const useAuthLastAction = () => useStore((s) => s.auth.lastAction) -export const useAuthAppLoaded = () => useStore((s) => s.auth.appLoaded) -export const useAuthAppIsLoading = () => useStore((s) => s.auth.appIsLoading) -export const useAuthActions = () => useStore((s) => s.auth.actions) - -// APPS -export const usePlugin = () => useContext(StoreContext).plugin - -// GLOBAL -export const useGlobalsApiEndpoint = () => - useStore((s) => s.globals.apiEndpoint) -export const useGlobalsAssetsHost = () => useStore((s) => s.globals.assetsHost) -export const useGlobalsIsUrlStateSetup = () => - useStore((state) => state.globals.isUrlStateSetup) -export const useGlobalsActions = () => useStore((s) => s.globals.actions) -export const useGlobalsEnvironment = () => - useStore((s) => s.globals.environment) -export const useDemoMode = () => useStore((s) => s.globals.demoMode) -export const useDemoUserToken = () => useStore((s) => s.globals.demoUserToken) - -export default StoreProvider diff --git a/apps/greenhouse/src/components/layout/ShellLayout.js b/apps/greenhouse/src/components/layout/ShellLayout.js deleted file mode 100644 index b4a7bb88b..000000000 --- a/apps/greenhouse/src/components/layout/ShellLayout.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import PluginNav from "../nav/PluginNav" -import NotificationsContainer from "../NotificationsContainer" - -const shellStyles = ` - grid - grid-cols-[max-content_auto] - grid-rows-[minmax(100vh,100%)] -` - -const mainStyles = ` - py-4 - pl-4 - bg-theme-global-bg - h-full -` - -const ShellLayout = ({ children }) => { - return ( -
- -
- -
{children}
-
-
- ) -} - -export default ShellLayout diff --git a/apps/greenhouse/src/components/layout/ShellLayout.test.js b/apps/greenhouse/src/components/layout/ShellLayout.test.js deleted file mode 100644 index b5d252a44..000000000 --- a/apps/greenhouse/src/components/layout/ShellLayout.test.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { render, act, renderHook } from "@testing-library/react" -// support shadow dom queries -// https://reactjsexample.com/an-extension-of-dom-testing-library-to-provide-hooks-into-the-shadow-dom/ -import { screen } from "shadow-dom-testing-library" -import ShellLayout from "./ShellLayout" -import StoreProvider from "../StoreProvider" -import { MessagesProvider } from "messages-provider" - -jest.mock("communicator") - -test("renders app", async () => { - await act(() => - render( - - - - - - ) - ) - - let logoTitle = await screen.queryAllByShadowTitle(/Greenhouse/i) - expect(logoTitle.length > 0).toBe(true) -}) diff --git a/apps/greenhouse/src/components/nav/PluginNav.js b/apps/greenhouse/src/components/nav/PluginNav.js deleted file mode 100644 index f76ff38f8..000000000 --- a/apps/greenhouse/src/components/nav/PluginNav.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import CCloudShape from "../../assets/ccloud_shape.svg" -import GreenhouseLogo from "../../assets/greenhouse_logo.svg" -import SupernovaIcon from "../../assets/juno_supernova.svg" -import DoopIcon from "../../assets/juno_doop.svg" -import HeurekaIcon from "../../assets/juno_heureka.svg" -import { Icon, Stack, Button } from "juno-ui-components" -import { - useAuthData, - useAuthLoggedIn, - useAuthActions, - usePlugin, -} from "../../components/StoreProvider" -import Avatar from "../Avatar" - -const AppIcon = ({ name }) => { - switch (name) { - case "supernova": - return - case "doop": - return - case "heureka": - return - default: - return - } -} - -const navStyles = ` -bg-juno-grey-blue-11 -py-4 -` - -const navItem = (active) => { - return ` - px-2 - py-3 - w-full - hover:text-theme-high - - ${ - active && - ` - bg-theme-global-bg - border-text-theme-light - border-l-4 - text-white - hover:text-white - ` - } -` -} - -const logoStyles = ` -pb-1 -` - -const logoText = ` -py-2 -font-bold -text-sm -leading-4 -` - -const appIconStyles = ` - -` - -const appNameStyles = ` -text-xs -break-all -` - -const PluginNav = () => { - const authData = useAuthData() - const loggedIn = useAuthLoggedIn() - const { login, logout } = useAuthActions() - const setActiveApps = usePlugin().setActive - const activeApps = usePlugin().active() - const appConfig = usePlugin().appConfig() - const mngConfig = usePlugin().mngConfig() - - return ( - - - - {appConfig.map((appConf, i) => ( - = 0 - )}`} - role="button" - tabIndex="0" - onClick={() => setActiveApps([appConf.id])} - > - - {appConf.displayName} - - ))} - - - {mngConfig.map((appConf, i) => ( - = 0 - )}`} - role="button" - tabIndex="0" - onClick={() => setActiveApps([appConf.id])} - > - - {appConf.displayName} - - ))} - - - {loggedIn ? ( - <> - - - - ) : ( - - )} - - - - ) -} - -export default PluginNav diff --git a/apps/greenhouse/src/components/shared/HintLoading.js b/apps/greenhouse/src/components/shared/HintLoading.js deleted file mode 100644 index 65a2f8eec..000000000 --- a/apps/greenhouse/src/components/shared/HintLoading.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack, Spinner } from "juno-ui-components" - -const HintLoading = ({ text }) => { - return ( - - - {text ? {text} : Loading...} - - ) -} - -export default HintLoading diff --git a/apps/greenhouse/src/hooks/useApi.js b/apps/greenhouse/src/hooks/useApi.js deleted file mode 100644 index 35226e05e..000000000 --- a/apps/greenhouse/src/hooks/useApi.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useCallback, useMemo } from "react" -import { createClient } from "sapcc-k8sclient" -import { - useAuthData, - useGlobalsApiEndpoint, - useGlobalsAssetsHost, -} from "../components/StoreProvider" -import { createPluginConfig } from "../lib/plugin" - -// get plugin configs from k8s api -const useApi = () => { - const authData = useAuthData() - // const token = useStoreByKey("auth.data?.JWT") - // const groups = useStoreByKey("auth.data?.raw?.groups") - const apiEndpoint = useGlobalsApiEndpoint() - const assetsHost = useGlobalsAssetsHost() - - const namespace = useMemo(() => { - if (!authData?.raw?.groups) return null - const orgString = authData?.raw?.groups.find( - (g) => g.indexOf("organization:") === 0 - ) - if (!orgString) return null - return orgString.split(":")[1] - }, [authData?.raw?.groups]) - - const client = useMemo(() => { - if (!apiEndpoint || !authData?.JWT) return null - return createClient({ apiEndpoint, token: authData?.JWT }) - }, [apiEndpoint, authData?.JWT]) - - const getPluginConfigs = useCallback(() => { - if (!client || !assetsHost || !namespace) return Promise.resolve({}) - - const manifestUrl = new URL("/manifest.json", assetsHost) - return Promise.all([ - // manifest - fetch(manifestUrl).then((r) => r.json()), - // plugin configs - client.get( - `/apis/greenhouse.sap/v1alpha1/namespaces/${namespace}/plugins`, - { - limit: 500, - } - ), - ]).then(([manifest, configs]) => { - // console.log("::::::::::::::::::::::::manifest", manifest) - // console.log("::::::::::::::::::::::::configs", configs.items) - - // create config map - const config = {} - configs.items.forEach((conf) => { - const id = conf.metadata?.name - const name = conf.status?.uiApplication?.name - const displayName = conf.spec?.displayName - const weight = conf.status?.weight - const version = conf.status?.uiApplication?.version - const url = conf.status?.uiApplication?.url - - // only add plugin if the url is from another host or the name with the given version is in the manifest! - if ((url && url.indexOf(assetsHost) < 0) || manifest[name]?.[version]) { - const newConf = createPluginConfig({ - id, - name, - displayName, - weight, - version, - url, - props: conf.spec?.optionValues?.reduce((map, item) => { - map[item.name] = item.value - return map - }, {}), - }) - if (newConf) config[id] = newConf - } - }) - - return config - }) - }, [client, assetsHost, namespace]) - - return { client, getPluginConfigs } -} - -export default useApi diff --git a/apps/greenhouse/src/hooks/useCommunication.js b/apps/greenhouse/src/hooks/useCommunication.js deleted file mode 100644 index 2826e4131..000000000 --- a/apps/greenhouse/src/hooks/useCommunication.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect, useCallback } from "react" -import { broadcast, get, watch } from "communicator" -import { - useAuthAppLoaded, - useAuthIsProcessing, - useAuthError, - useAuthLoggedIn, - useAuthLastAction, - useAuthActions, - useDemoMode, - useDemoUserToken, -} from "../components/StoreProvider" - -const useCommunication = () => { - const authAppLoaded = useAuthAppLoaded() - const authIsProcessing = useAuthIsProcessing() - const authLastAction = useAuthLastAction() - const { setData: authSetData, setAppLoaded: authSetAppLoaded } = - useAuthActions() - const demoMode = useDemoMode() - const demoUserToken = useDemoUserToken() - - const setAuthData = useCallback( - (data) => { - // If we're in demo mode, we need to make sure the JWT is set to the demo user's JWT - if (data?.auth && demoMode && demoUserToken) { - data.auth.JWT = demoUserToken - } - if (data?.auth?.error) - console.warn("Greenhouse: Auth error: ", data?.auth?.error) - authSetData(data) - }, - [authSetData, demoMode, demoUserToken] - ) - - useEffect(() => { - if (!authAppLoaded || authIsProcessing) return - if (authLastAction?.name === "signOn") { - broadcast("AUTH_LOGIN", "greenhouse", { - debug: true, - consumerID: "greenhouse", - }) - } else if (authLastAction?.name === "signOut") { - broadcast("AUTH_LOGOUT", "greenhouse", { - debug: true, - consumerID: "greenhouse", - }) - } - }, [authAppLoaded, authIsProcessing, authLastAction]) - - useEffect(() => { - if (!authSetData || !authSetAppLoaded) return - get("AUTH_APP_LOADED", authSetAppLoaded, { - consumerID: "greenhouse", - debug: true, - }) - const unwatchLoaded = watch("AUTH_APP_LOADED", authSetAppLoaded, { - debug: true, - consumerID: "greenhouse", - }) - - get("AUTH_GET_DATA", setAuthData, { consumerID: "greenhouse", debug: true }) - const unwatchUpdate = watch("AUTH_UPDATE_DATA", setAuthData, { - debug: true, - consumerID: "greenhouse", - }) - - return () => { - if (unwatchLoaded) unwatchLoaded() - if (unwatchUpdate) unwatchUpdate() - } - }, [setAuthData, authSetAppLoaded]) -} - -export default useCommunication diff --git a/apps/greenhouse/src/hooks/useUrlState.js b/apps/greenhouse/src/hooks/useUrlState.js deleted file mode 100644 index 24f34a29d..000000000 --- a/apps/greenhouse/src/hooks/useUrlState.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect, useLayoutEffect } from "react" -import { registerConsumer } from "url-state-provider" -import { - useAuthLoggedIn, - useGlobalsIsUrlStateSetup, - useGlobalsActions, - usePlugin, -} from "../components/StoreProvider" - -// url state manager -const GREENHOUSE_STATE_KEY = "greenhouse" -const ACTIVE_APPS_KEY = "a" -const urlStateManager = registerConsumer(GREENHOUSE_STATE_KEY) - -const useUrlState = () => { - // const { setActive: setActiveApps } = usePlugin.actions() - const setActiveApps = usePlugin().setActive - const activeApps = usePlugin().active() - const appsConfig = usePlugin().config() - const loggedIn = useAuthLoggedIn() - const isUrlStateSetup = useGlobalsIsUrlStateSetup() - const { setIsUrlStateSetup } = useGlobalsActions() - - // Initial state from URL (on login) - useLayoutEffect(() => { - if (!loggedIn || !appsConfig || isUrlStateSetup) return - - let active = urlStateManager.currentState()?.[ACTIVE_APPS_KEY] - if (active) setActiveApps(active.split(",")) - setIsUrlStateSetup(true) - }, [loggedIn, appsConfig, setActiveApps]) - - // sync URL state - useEffect(() => { - if (!loggedIn || !isUrlStateSetup) return - - const newActiveApps = activeApps?.join(",") - // if the current state is the same as the new state, don't push - // this prevents the history from being filled with the same state - // and therefore prevents the forward button from being disabled - // This small optimization allows the user to go back and forth! - if (urlStateManager.currentState()?.[ACTIVE_APPS_KEY] === newActiveApps) - return - - urlStateManager.push({ [ACTIVE_APPS_KEY]: activeApps.join(",") }) - }, [loggedIn, activeApps]) - - useEffect(() => { - const unregisterStateListener = urlStateManager.onChange((state) => { - const newActiveApps = state?.[ACTIVE_APPS_KEY]?.split(",") - setActiveApps(newActiveApps || []) - }) - - // disable this for now, it's annoying! - // This code sets the title of the page if URL changes. - // It was introduced to see different titles in the browser history. - // const unregisterGlobalChangeListener = urlStateManager.onGlobalChange( - // (state) => { - // const url = new URL(window.location) - // document.title = `Greenhouse - ${url.searchParams.get("__s")}` - // } - // ) - - return () => { - unregisterStateListener() - //unregisterGlobalChangeListener() - } - }, []) -} - -export default useUrlState diff --git a/apps/greenhouse/src/index.js b/apps/greenhouse/src/index.js deleted file mode 100644 index b16f8a89d..000000000 --- a/apps/greenhouse/src/index.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createRoot } from "react-dom/client" -import React from "react" -import { version } from "../package.json" - -const logAppName = (version) => { - const appName = `%c - ____ ____ _____ _____ _ _ _ _ ___ _ _ ____ _____ - / ___| _ \\| ____| ____| \\ | | | | |/ _ \\| | | / ___|| ____| -| | _| |_) | _| | _| | \\| | |_| | | | | | | \\___ \\| _| -| |_| | _ <| |___| |___| |\\ | _ | |_| | |_| |___) | |___ - \\____|_| \\_\\_____|_____|_| \\_|_| |_|\\___/ \\___/|____/|_____| v${version} -` - console.log(appName, "color:green") -} - -logAppName(version) - -// export mount and unmount functions -export const mount = (container, options = {}) => { - import("./Shell").then((App) => { - mount.root = createRoot(container) - mount.root.render(React.createElement(App.default, options?.props)) - }) -} - -export const unmount = () => mount.root && mount.root.unmount() diff --git a/apps/greenhouse/src/lib/helpers.js b/apps/greenhouse/src/lib/helpers.js deleted file mode 100644 index 531d03702..000000000 --- a/apps/greenhouse/src/lib/helpers.js +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const parseError = (error) => { - let errMsg = JSON.stringify(error) - if (error?.message) { - errMsg = error?.message - try { - errMsg = JSON.parse(error?.message).msg - } catch (error) {} - } - return errMsg -} diff --git a/apps/greenhouse/src/lib/plugin.js b/apps/greenhouse/src/lib/plugin.js deleted file mode 100644 index 4d3fc40a6..000000000 --- a/apps/greenhouse/src/lib/plugin.js +++ /dev/null @@ -1,242 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useStore, createStore } from "zustand" -import { devtools } from "zustand/middleware" -import produce from "immer" -import { managementVersion } from "../../package.json" - -export const NAV_TYPES = { - APP: "app", - MNG: "management", -} - -const pluginConfig = { - id: "", - name: "", - displayName: "", - version: "latest", - url: null, - weight: 0, - navType: NAV_TYPES.APP, - navigable: true, - props: { - id: "", - }, -} - -export const createPluginConfig = (config) => { - // check required attrs - if (!config?.id || !config?.name) { - console.warn( - `[greenhouse]::createPluginConfig: id and name are required. Skipping config: ${config}` - ) - return null - } - - // clone default pluginConfig - const newConfig = { ...pluginConfig } - // update just known attrs - Object.keys(newConfig).forEach((key) => { - // check agains type to update falsy booleans - if (typeof config?.[key] !== "undefined") newConfig[key] = config?.[key] - }) - // check displayName - if (!newConfig?.displayName) newConfig.displayName = newConfig.name - // update id to the props attr - newConfig.props = { ...newConfig.props, id: newConfig.id } - - return newConfig -} - -const filterAndSortConfigByType = (config, navtype) => { - if (typeof config !== "object" || config === null) return [] - return Object.values(config) - .filter((a) => a.navigable) - .filter((a) => a.navType === navtype) - .sort((a, b) => { - // sort by weight, then by name - // if weight is not defined, app is sorted to the end - const w1 = a.weight === undefined ? Infinity : a.weight - const w2 = b.weight === undefined ? Infinity : b.weight - let weightSort = w1 - w2 - weightSort = weightSort > 0 ? 1 : weightSort < 0 ? -1 : 0 - return weightSort || a.displayName.localeCompare(b.displayName) - }) -} - -// if no active app already set will set the app (no mng apps) with the lowest weight -const findActiveAppId = (appConfig) => { - if (!appConfig || appConfig.length === 0) return null - - // if there is no active app, then from appsConfig, get the app id of the app with the lowest weight and set it as active - const minWeightApp = appConfig.reduce((previous, current) => { - return current.weight < previous.weight ? current : previous - }) - - return [minWeightApp.id] -} - -const Plugin = ({ environment, apiEndpoint, currentHost }) => { - const store = createStore( - devtools((set, get) => ({ - active: [], - config: { - [`greenhouse-management`]: createPluginConfig({ - id: "greenhouse-management", - name: "greenhouse-management", - displayName: "Organization", - version: environment =='qa' || environment == 'development' ? 'latest' : managementVersion, // pull latest version in dev and qa - navType: NAV_TYPES.MNG, - props: { - assetsUrl: currentHost, - apiEndpoint: apiEndpoint, - environment: environment, - }, - }), - }, - appConfig: [], // kube app configs - mngConfig: [], // management app configs - isFetching: false, - error: null, - updatedAt: null, - })) - ) - const { getState, setState } = store - - const setIsFetching = (newState) => { - setState( - produce((state) => { - state.isFetching = newState - }), - false, - "plugin/setIsFetching" - ) - } - - const setError = (error) => - setState( - produce((state) => { - state.error = error - }), - false, - "plugin/setError" - ) - - const setActive = (active) => - setState( - produce((state) => { - if (!Array.isArray(active)) active = [active] - // if the current state is the same as the new state, don't update - if (JSON.stringify(state.active) === JSON.stringify(active)) return - state.active = active - }), - false, - "plugin/setActive" - ) - - // const addActive = (appName) => - // setState( - // produce((state) => { - // const index = getState().active.findIndex((i) => i === appName) - // if (index >= 0) return - // const newActive = getState().active.slice() - // newActive.push(appName) - // state.active = newActive - // }), - // false, - // "plugin/addActive" - // ) - - // const removeActive = (appName) => - // setState( - // produce((state) => { - // const index = getState().active.findIndex((i) => i === appName) - // if (index < 0) return - // let newActive = getState().active.slice() - // newActive.splice(index, 1) - // state.active = newActive - // }), - // false, - // "plugin/removeActive" - // ) - - const addConfig = (config) => - setState( - produce((state) => { - state.config = { ...getState().config, ...config } - }), - false, - "plugin/addConfig" - ) - - const splitApps = () => { - const allConfig = getState().config - const appConfig = filterAndSortConfigByType(allConfig, NAV_TYPES.APP) - setAppConfig(appConfig) - const mngConfig = filterAndSortConfigByType(allConfig, NAV_TYPES.MNG) - setMngConfig(mngConfig) - } - - const setAppConfig = (appConfig) => - setState( - produce((state) => { - state.appConfig = appConfig - }), - false, - "plugin/setAppConfig" - ) - - const setMngConfig = (mngConfig) => - setState( - produce((state) => { - state.mngConfig = mngConfig - }), - false, - "plugin/setMngConfig" - ) - - return { - active: () => useStore(store, (s) => s.active), - config: () => useStore(store, (s) => s.config), - appConfig: () => useStore(store, (s) => s.appConfig), - mngConfig: () => useStore(store, (s) => s.mngConfig), - isFetching: () => useStore(store, (s) => s.isFetching), - error: () => useStore(store, (s) => s.error), - updatedAt: () => useStore(store, (s) => s.updatedAt), - setActive: setActive, - requestConfig: () => { - setIsFetching(true) - setError(null) - }, - receiveError: (error) => { - setIsFetching(false) - setError(error) - // on api error split then the preconfigured - splitApps() - }, - receiveConfig: (kubeConfig) => { - // add config and other states - addConfig(kubeConfig) - setIsFetching(false) - setError(null) - - // split apps in mng and apps - splitApps() - - // if no config found in the active apps set a new one but from the apps and not mng - if ( - Object.keys(getState().config).filter((key) => - getState().active.includes(key) - ).length === 0 - ) { - const newActiveApp = findActiveAppId(getState().appConfig) - setActive(newActiveApp) - } - }, - } -} - -export default Plugin diff --git a/apps/greenhouse/src/lib/plugin.test.js b/apps/greenhouse/src/lib/plugin.test.js deleted file mode 100644 index 7a26ddd01..000000000 --- a/apps/greenhouse/src/lib/plugin.test.js +++ /dev/null @@ -1,173 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as React from "react" -import { createPluginConfig, NAV_TYPES } from "./plugin" -import StoreProvider, { usePlugin } from "../components/StoreProvider" -import { renderHook, act } from "@testing-library/react" - -describe("Plugin", () => { - describe("createPluginConfig", () => { - it("requires at least an id and name", () => { - const spy = jest.spyOn(console, "warn").mockImplementation(() => {}) - - createPluginConfig() - createPluginConfig({ id: "test" }) - createPluginConfig({ name: "test" }) - - expect(spy).toHaveBeenCalledTimes(3) - expect(spy).toHaveBeenCalledWith( - expect.stringContaining( - "[greenhouse]::createPluginConfig: id and name are required." - ) - ) - spy.mockRestore() - }) - - it("maps name to displayName if missing", () => { - expect(createPluginConfig({ id: "id_test", name: "name_test" })).toEqual( - expect.objectContaining({ displayName: "name_test" }) - ) - }) - - it("sets weight to default 0 if missing", () => { - expect(createPluginConfig({ id: "id_test", name: "name_test" })).toEqual( - expect.objectContaining({ weight: 0 }) - ) - }) - - it("sets version to latest if missing", () => { - expect(createPluginConfig({ id: "id_test", name: "name_test" })).toEqual( - expect.objectContaining({ version: "latest" }) - ) - }) - - it("sets navigable to true if missing", () => { - expect(createPluginConfig({ id: "id_test", name: "name_test" })).toEqual( - expect.objectContaining({ navigable: true }) - ) - }) - - it("sets navigation type to app", () => { - expect(createPluginConfig({ id: "id_test", name: "name_test" })).toEqual( - expect.objectContaining({ navType: NAV_TYPES.APP }) - ) - }) - - it("adds id to the props", () => { - expect(createPluginConfig({ id: "id_test", name: "name_test" })).toEqual( - expect.objectContaining({ - props: expect.objectContaining({ id: "id_test" }), - }) - ) - }) - - it("does not save not known keys", () => { - expect( - createPluginConfig({ - id: "id_test", - name: "name_test", - miau: "bup", - }) - ).not.toEqual( - expect.objectContaining({ - miau: "bup", - }) - ) - }) - - it("creates a plugin", () => { - const config = { - id: "id_test", - name: "name_test", - displayName: "displayName_Test", - version: "1.2.3", - url: "http://miau.bup", - weight: 9, - navigable: false, - navType: NAV_TYPES.MNG, - props: { - test1: "test1", - test2: "test2", - }, - } - expect(createPluginConfig(config)).toEqual({ - ...config, - props: { ...config.props, id: config.id }, - }) - }) - }) - - describe("savePlugin", () => { - describe("set active plugin", () => { - it("keeps active plugin if existing in the config", () => { - const wrapper = ({ children }) => ( - {children} - ) - - const store = renderHook( - () => ({ - setActive: usePlugin().setActive, - receiveConfig: usePlugin().receiveConfig, - active: usePlugin().active(), - }), - { wrapper } - ) - - const configs = { - plugin1: createPluginConfig({ - id: "plugin1", - name: "plugin1", - weight: 9, - }), - plugin2: createPluginConfig({ - id: "plugin2", - name: "plugin2", - weight: 0, - }), - } - - act(() => store.result.current.setActive(["plugin1"])) - act(() => store.result.current.receiveConfig(configs)) - expect(store.result.current.active).toEqual(["plugin1"]) - }) - it("sets a new active plugin (from apps and not from mng) with the lowest weight", () => { - const wrapper = ({ children }) => ( - {children} - ) - - const store = renderHook( - () => ({ - receiveConfig: usePlugin().receiveConfig, - active: usePlugin().active(), - }), - { wrapper } - ) - - const configs = { - plugin0: createPluginConfig({ - id: "plugin0", - name: "plugin0", - weight: 0, - navType: NAV_TYPES.MNG, - }), - plugin1: createPluginConfig({ - id: "plugin1", - name: "plugin1", - weight: 9, - }), - plugin2: createPluginConfig({ - id: "plugin2", - name: "plugin2", - weight: 1, - }), - } - - act(() => store.result.current.receiveConfig(configs)) - expect(store.result.current.active).toEqual(["plugin2"]) - }) - }) - }) -}) diff --git a/apps/greenhouse/src/lib/store/createAuthDataSlice.js b/apps/greenhouse/src/lib/store/createAuthDataSlice.js deleted file mode 100644 index 671517788..000000000 --- a/apps/greenhouse/src/lib/store/createAuthDataSlice.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const ACTIONS = { - SIGN_ON: "signOn", - SIGN_OUT: "signOut", -} - -const createAuthDataSlice = (set, get) => ({ - auth: { - data: null, - isProcessing: false, - loggedIn: false, - error: null, - lastAction: {}, - appLoaded: false, - appIsLoading: false, - - actions: { - setAppLoaded: (appLoaded) => { - set( - (state) => ({ auth: { ...state.auth, appLoaded } }), - false, - "auth/setAppLoaded" - ) - }, - setData: (data = {}) => { - set( - (state) => ({ - auth: { - ...state.auth, - isProcessing: data ? data.isProcessing : false, - loggedIn: data ? data.loggedIn : false, - error: data ? data.error : null, - data: data ? data.auth : null, - }, - }), - false, - "auth/setData" - ) - if (!data) get().auth.actions.setAction(ACTIONS.SIGN_OUT) - }, - setAction: (name) => - set( - (state) => ({ - auth: { - ...state.auth, - lastAction: { name: name, updatedAt: Date.now() }, - }, - }), - false, - "auth/setAction" - ), - login: () => { - // logout - get().auth.actions.setAction(ACTIONS.SIGN_OUT) - get().auth.actions.setAction(ACTIONS.SIGN_ON) - }, - logout: () => get().auth.actions.setAction(ACTIONS.SIGN_OUT), - }, - }, -}) - -export default createAuthDataSlice diff --git a/apps/greenhouse/src/lib/store/createGlobalsSlice.js b/apps/greenhouse/src/lib/store/createGlobalsSlice.js deleted file mode 100644 index 66e33fdbc..000000000 --- a/apps/greenhouse/src/lib/store/createGlobalsSlice.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const createGlobalsSlice = (set, get) => ({ - globals: { - apiEndpoint: "", - assetsHost: "", - environment: "", - isUrlStateSetup: false, - demoMode: false, - demoUserToken: null, - - actions: { - setDemoMode: (demoMode) => - set((state) => ({ globals: { ...state.globals, demoMode } })), - setDemoUserToken: (demoUserToken) => - set((state) => ({ globals: { ...state.globals, demoUserToken } })), - - setApiEndpoint: (value) => - set( - (state) => ({ globals: { ...state.globals, apiEndpoint: value } }), - false, - "globals/setApiEndpoint" - ), - - setEnvironment: (value) => - set( - (state) => ({ globals: { ...state.globals, environment: value } }), - false, - "globals/setEnvironment" - ), - setAssetsHost: (value) => - set( - (state) => ({ globals: { ...state.globals, assetsHost: value } }), - false, - "globals/setAssetsHost" - ), - setIsUrlStateSetup: (setup) => - set( - (state) => ({ - globals: { ...state.globals, isUrlStateSetup: setup }, - }), - false, - "globals/setIsUrlStateSetup" - ), - }, - }, -}) - -export default createGlobalsSlice diff --git a/apps/greenhouse/src/lib/store/index.js b/apps/greenhouse/src/lib/store/index.js deleted file mode 100644 index 75e693236..000000000 --- a/apps/greenhouse/src/lib/store/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createStore } from "zustand" -import { devtools } from "zustand/middleware" -import createAuthDataSlice from "./createAuthDataSlice" -import createGlobalsSlice from "./createGlobalsSlice" -import Plugin from "../plugin" - -export default (options) => { - const store = createStore( - devtools((set, get) => ({ - ...createAuthDataSlice(set, get), - ...createGlobalsSlice(set, get), - })) - ) - - const plugin = Plugin(options) - - return { store, plugin } -} diff --git a/apps/greenhouse/src/styles.scss b/apps/greenhouse/src/styles.scss deleted file mode 100644 index f3b5dd36d..000000000 --- a/apps/greenhouse/src/styles.scss +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors -// SPDX-License-Identifier: Apache-2.0 - -/* Do not remove these tailwind directives. Without them styles won't work as expected */ -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* If necessary, app styles can be added below */ - - diff --git a/apps/greenhouse/tailwind.config.js b/apps/greenhouse/tailwind.config.js deleted file mode 100644 index 81b1f8fef..000000000 --- a/apps/greenhouse/tailwind.config.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// opacity helper to make custom colors work with opacity -function withOpacity(variableName) { - return ({ opacityVariable, opacityValue }) => { - if (opacityValue !== undefined) { - return `rgba(var(${variableName}), ${opacityValue})` - } - if (opacityVariable !== undefined) { - return `rgba(var(${variableName}), var(${opacityVariable}, 1))` - } - return `rgb(var(${variableName}))` - } -} - -module.exports = { - presets: [ - require("juno-ui-components/build/lib/tailwind.config"), // important, do not change - ], - prefix: "", // important, do not change - content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], - corePlugins: { - preflight: false, // important, do not change - }, - theme: {}, - plugins: [], -} diff --git a/apps/heureka/LICENSE b/apps/heureka/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/apps/heureka/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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/apps/heureka/README.md b/apps/heureka/README.md deleted file mode 100644 index 9fd4ac161..000000000 --- a/apps/heureka/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Heureka - -This mircro frontent uses the Heureka POC API which aims to: - -- Track the overall state of our technology landscape -- Establish a unified & complete Patch Management Process: - - Maintain & track vulnerabilities and affected components - - Document the remediation, classification, and impact of vulnerabilities - - Document the changes corresponding with patching of vulnerabilities, as well as updating components diff --git a/apps/heureka/__mocks__/client.js b/apps/heureka/__mocks__/client.js deleted file mode 100644 index 84531d3c2..000000000 --- a/apps/heureka/__mocks__/client.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { JSDOM } from "jsdom" -const dom = new JSDOM() -global.document = dom.window.document -global.window = dom.window diff --git a/apps/heureka/__mocks__/fileMock.js b/apps/heureka/__mocks__/fileMock.js deleted file mode 100644 index 27ce65aca..000000000 --- a/apps/heureka/__mocks__/fileMock.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = "test-file-stub" diff --git a/apps/heureka/__mocks__/styleMock.js b/apps/heureka/__mocks__/styleMock.js deleted file mode 100644 index d74516001..000000000 --- a/apps/heureka/__mocks__/styleMock.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = {} diff --git a/apps/heureka/babel.config.js b/apps/heureka/babel.config.js deleted file mode 100644 index 0719e2fec..000000000 --- a/apps/heureka/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - env: { - test: { - presets: ["@babel/preset-env", "@babel/preset-react"], - plugins: [["babel-plugin-transform-import-meta", { module: "ES6" }]], - }, - }, -} diff --git a/apps/heureka/esbuild.config.js b/apps/heureka/esbuild.config.js deleted file mode 100644 index 2394388b8..000000000 --- a/apps/heureka/esbuild.config.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const esbuild = require("esbuild") -const fs = require("node:fs/promises") -const pkg = require("./package.json") -const postcss = require("postcss") -const sass = require("sass") -const { transform } = require("@svgr/core") -const url = require("postcss-url") -// this function generates app props based on package.json and propSecrets.json -const appProps = require("../../helpers/appProps") - -if (!/.+\/.+\.js/.test(pkg.module)) - throw new Error( - "module value is incorrect, use DIR/FILE.js like build/index.js" - ) - -const isProduction = process.env.NODE_ENV === "production" -// If the jspm server fails and we cannot use external packages -// in our import map then IGNORE_EXTERNALS (global env variable) -// should be set to true -const IGNORE_EXTERNALS = process.env.IGNORE_EXTERNALS === "true" -// in dev environment we prefix output file with public -let outfile = `${isProduction ? "" : "public/"}${pkg.main || pkg.module}` -// get output from outputfile -let outdir = outfile.slice(0, outfile.lastIndexOf("/")) -const args = process.argv.slice(2) -const watch = args.indexOf("--watch") >= 0 -const serve = args.indexOf("--serve") >= 0 - -// helpers for console log -const green = "\x1b[32m%s\x1b[0m" -const yellow = "\x1b[33m%s\x1b[0m" -const clear = "\033c" - -const build = async () => { - // delete build folder and re-create it as an empty folder - await fs.rm(outdir, { recursive: true, force: true }) - await fs.mkdir(outdir, { recursive: true }) - - // build app - let ctx = await esbuild.context({ - bundle: true, - minify: isProduction, - // target: ["es2020"], - target: ["es2020"], //["chrome64", "firefox67", "safari11.1", "edge79"], - format: "esm", - platform: "browser", - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - loader: { ".js": "jsx" }, - sourcemap: !isProduction, - // here we exclude package from bundle which are defined in peerDependencies - // our importmap generator uses also the peerDependencies to create the importmap - // it means all packages defined in peerDependencies are in browser available via the importmap - external: - isProduction && !IGNORE_EXTERNALS - ? Object.keys(pkg.peerDependencies || {}) - : [], - entryPoints: [pkg.source], - outdir, - // this step is important for performance reason. - // the main file (index.js) contains minimal code needed to - // load the app via dynamic import (splitting: true) - splitting: true, - // we suport only esm! - format: "esm", - plugins: [ - // minimal plugin to log the recompiling process. - { - name: "start/end", - setup(build) { - build.onStart(() => { - console.log(clear) - console.log(yellow, "Compiling...") - }) - build.onEnd(() => console.log(green, "Done!")) - }, - }, - - // this custom plugin rewrites SVG imports to - // dataurls, paths or react components based on the - // search param and size - { - name: "svg-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(svg)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - let loader = "text" - if (args.suffix === "?url") { - // as URL - const maxSize = 10240 // 10Kb - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - } else { - // as react component - // use react component loader (jsx) - loader = "jsx" - contents = await transform(contents, { - plugins: ["@svgr/plugin-jsx"], - }) - } - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin rewrites image imports to - // dataurls or urls based on the size - { - name: "image-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(png|jpg|jpeg|gif)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - const maxSize = 10240 // 10Kb - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin parses the style files - { - name: "parse-styles", - setup(build) { - build.onLoad( - // consider only .scss and .css files - { filter: /.\.(css|scss)$/, namespace: "file" }, - async (args) => { - let content - // handle scss, convert to css - if (args.path.endsWith(".scss")) { - const result = sass.renderSync({ file: args.path }) - content = result.css - } else { - // read file content - content = await fs.readFile(args.path) - } - - // postcss plugins - const plugins = [ - require("tailwindcss"), - require("autoprefixer"), - // rewrite urls inside css - url({ - url: "inline", - // maxSize: 10, // use dataurls if files are smaller than 10k - // fallback: "copy", // if files are bigger use copy method - // assetsPath: "./build/assets", - // useHash: true, - // optimizeSvgEncode: true, - }), - ] - - const { css } = await postcss(plugins).process(content, { - from: args.path, - to: outdir, - }) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - return { contents: css, loader: "text" } - } - ) - }, - }, - ], - }) - - // watch and serve - if (watch || serve) { - if (watch) await ctx.watch() - if (serve) { - // generate app props based on package.json and secretProps.json - await fs.writeFile( - `./${outdir}/appProps.js`, - `export default ${JSON.stringify(appProps())}` - ) - - let { host, port } = await ctx.serve({ - host: "0.0.0.0", - port: parseInt(process.env.APP_PORT || process.env.PORT || 3000), - servedir: "public", - }) - console.log("serve on", `${host}:${port}`) - } - } else { - await ctx.rebuild() - await ctx.dispose() - } -} - -build() diff --git a/apps/heureka/jest.config.js b/apps/heureka/jest.config.js deleted file mode 100644 index 58feee149..000000000 --- a/apps/heureka/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - transform: { "\\.[jt]sx?$": "babel-jest" }, - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/setupTests.js"], - transformIgnorePatterns: [ - "node_modules/(?!(juno-ui-components|url-state-router|communicator|oauth)/)", - ], - moduleNameMapper: { - // Jest currently doesn't support resources with query parameters. - // Therefore we add the optional query parameter matcher at the end - // https://github.com/facebook/jest/issues/4181 - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?.+)?$": - require.resolve("./__mocks__/fileMock"), - "\\.(css|less|scss)$": require.resolve("./__mocks__/styleMock"), - }, -} diff --git a/apps/heureka/package.json b/apps/heureka/package.json deleted file mode 100644 index 2bdc2cf80..000000000 --- a/apps/heureka/package.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "name": "heureka", - "version": "2.0.4", - "author": "UI-Team", - "contributors": [ - "Arturo Reuschenbach Pucernau" - ], - "repository": "https://github.com/sapcc/juno/tree/main/apps/heureka", - "license": "Apache-2.0", - "module": "build/index.js", - "source": "src/index.js", - "private": true, - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", - "graphql-request": "^6.0.0", - "graphql": "*", - "immer": "^9.0.21", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "zustand": "4.3.7" - }, - "scripts": { - "test": "jest", - "start": "NODE_ENV=development node esbuild.config.js --serve --watch", - "build": "NODE_ENV=production node esbuild.config.js" - }, - "peerDependencies": { - "@tanstack/react-query": "4.28.0", - "juno-ui-components": "*", - "messages-provider": "*", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "zustand": "4.3.7" - }, - "importmapExtras": { - "zustand/middleware": "4.3.7" - }, - "appProps": { - "theme": { - "value": "theme-dark", - "type": "optional", - "description": "Override the default theme. Possible values are theme-light or theme-dark (default)" - }, - "apiEndpoint": { - "value": "", - "type": "required", - "description": "Endpoint URL of the API" - }, - "embedded": { - "value": "false", - "type": "optional", - "description": "Set to true if app is to be embedded in another existing app or page, like e.g. Elektra. If set to true the app won't render a page header/footer and instead render only the content. The default value is false." - } - }, - "appPreview": true -} diff --git a/apps/heureka/public/favicon.ico b/apps/heureka/public/favicon.ico deleted file mode 100644 index 9ebc4bb2e..000000000 Binary files a/apps/heureka/public/favicon.ico and /dev/null differ diff --git a/apps/heureka/public/index.html b/apps/heureka/public/index.html deleted file mode 100644 index 3717575f0..000000000 --- a/apps/heureka/public/index.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - Heureka Dev - - - - - -
- - diff --git a/apps/heureka/setupTests.js b/apps/heureka/setupTests.js deleted file mode 100644 index db44c9038..000000000 --- a/apps/heureka/setupTests.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom" diff --git a/apps/heureka/src/App.js b/apps/heureka/src/App.js deleted file mode 100644 index 72feb1488..000000000 --- a/apps/heureka/src/App.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from "react" -import styles from "./styles.scss" -import { AppShell, AppShellProvider } from "juno-ui-components" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { MessagesProvider } from "messages-provider" -import AsyncWorker from "./components/AsyncWorker" -import StoreProvider, { useActions } from "./components/StoreProvider" -import TabContext from "./components/tabs/TabContext" - -const App = (props) => { - // Create a client - const queryClient = new QueryClient({ - defaultOptions: { - // global default options that apply to all queries - queries: { - // staleTime: Infinity, // if you wish to keep data from the keys until reload - keepPreviousData: true, // nice when paginating - refetchOnWindowFocus: false, // default: true - }, - }, - }) - - return ( - - - - - - - ) -} - -const StyledApp = (props) => { - return ( - - {/* load styles inside the shadow dom */} - - - - - - - - ) -} - -export default StyledApp diff --git a/apps/heureka/src/App.test.js b/apps/heureka/src/App.test.js deleted file mode 100644 index df4eb46ea..000000000 --- a/apps/heureka/src/App.test.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { render } from "@testing-library/react" -// support shadow dom queries -// https://reactjsexample.com/an-extension-of-dom-testing-library-to-provide-hooks-into-the-shadow-dom/ -import { screen } from "shadow-dom-testing-library" -import App from "./App" - -jest.mock("communicator") - -describe("logged in", () => { - test("renders app", async () => { - render() - const loginTitle = await screen.queryAllByShadowText(/Converged Cloud/i) - expect(loginTitle.length > 0).toBe(true) - }) -}) diff --git a/apps/heureka/src/actions.js b/apps/heureka/src/actions.js deleted file mode 100644 index 06410e52a..000000000 --- a/apps/heureka/src/actions.js +++ /dev/null @@ -1,157 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -class HTTPError extends Error { - constructor(code, message) { - super(message || code) - this.name = "HTTPError" - this.statusCode = code - } -} - -export const encodeUrlParamsFromObject = (options) => { - if (!options) return "" - let encodedOptions = Object.keys(options) - .map((k) => { - if (typeof options[k] === "object") { - const childOption = options[k] - return Object.keys(childOption).map( - (childKey) => - `${encodeURIComponent(childKey)}=${encodeURIComponent( - childOption[childKey] - )}` - ) - } - return `${encodeURIComponent(k)}=${encodeURIComponent(options[k])}` - }) - .join("&") - return `&${encodedOptions}` -} - -const checkStatus = (response) => { - if (response.status < 400) { - return response - } else { - return response.text().then((message) => { - var error = new HTTPError(response.status, message || response.statusText) - error.statusCode = response.status - return Promise.reject(error) - }) - } -} - -// -// SERVICES -// - -export const services = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/services", options) -} - -export const service = ({ queryKey }) => { - const [_key, bearerToken, endpoint, serviceId] = queryKey - return fetchFromAPI(bearerToken, endpoint, `/services/${serviceId}`) -} - -export const serviceFilters = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/services/filters", options) -} - -// -// COMPONENTS -// - -export const components = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/components", options) -} - -export const component = ({ queryKey }) => { - const [_key, bearerToken, endpoint, componentId] = queryKey - return fetchFromAPI(bearerToken, endpoint, `/components/${componentId}`) -} - -export const componentFilters = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/components/filters", options) -} - -// -// VULNERABILITIES -// - -export const vulnerabilities = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/vulnerabilities", options) -} - -export const vulnerability = ({ queryKey }) => { - const [_key, bearerToken, endpoint, vulnerabilityId] = queryKey - return fetchFromAPI( - bearerToken, - endpoint, - `/vulnerabilities/${vulnerabilityId}` - ) -} - -export const vulnerabilityFilters = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI( - bearerToken, - endpoint, - "/vulnerabilities/filters", - options - ) -} - -// -// USERS -// - -export const users = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/users", options) -} - -export const user = ({ queryKey }) => { - const [_key, bearerToken, endpoint, userId] = queryKey - return fetchFromAPI(bearerToken, endpoint, `/users/${userId}`) -} - -export const userFilters = ({ queryKey }) => { - const [_key, bearerToken, endpoint, options] = queryKey - return fetchFromAPI(bearerToken, endpoint, "/users/filters", options) -} - -// -// COMMONS -// - -const fetchFromAPI = (bearerToken, endpoint, path, options) => { - const query = encodeUrlParamsFromObject(options) - return fetch(`${endpoint}${path}?${query}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${bearerToken}`, - }, - }) - .then(checkStatus) - .then((response) => { - let isJSON = response.headers - .get("content-type") - .includes("application/json") - if (!isJSON) { - var error = new HTTPError( - 400, - "The response is not a valid JSON response" - ) - return Promise.reject(error) - } - return response.json() - }) -} diff --git a/apps/heureka/src/components/AsyncWorker.jsx b/apps/heureka/src/components/AsyncWorker.jsx deleted file mode 100644 index d9728e5e9..000000000 --- a/apps/heureka/src/components/AsyncWorker.jsx +++ /dev/null @@ -1,16 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import useUrlState from "../hooks/useUrlState" -import useQueryClientFn from "../hooks/useQueryClientFn" - -const AsyncWorker = () => { - useUrlState() - useQueryClientFn() - return null -} - -export default AsyncWorker diff --git a/apps/heureka/src/components/StoreProvider.jsx b/apps/heureka/src/components/StoreProvider.jsx deleted file mode 100644 index d601f3d69..000000000 --- a/apps/heureka/src/components/StoreProvider.jsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { createContext, useContext } from "react" -import { useStore as create } from "zustand" -import createStore from "../lib/store" - -const StoreContext = createContext() -const StoreProvider = ({ options, children }) => ( - - {children} - -) - -const useStore = (selector) => create(useContext(StoreContext), selector) - -export const useEndpoint = () => useStore((s) => s.endpoint) -export const useQueryClientFnReady = () => useStore((s) => s.queryClientFnReady) -export const useActiveTab = () => useStore((s) => s.activeTab) -export const useQueryOptions = (tab) => - useStore((s) => s.tabs[tab].queryOptions) -export const useActions = () => useStore((s) => s.actions) - -export default StoreProvider diff --git a/apps/heureka/src/components/backup/AppContainer.js b/apps/heureka/src/components/backup/AppContainer.js deleted file mode 100644 index 776ed1371..000000000 --- a/apps/heureka/src/components/backup/AppContainer.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - MainTabs, - TabList, - Tab, - TabPanel, - Icon, - Container, -} from "juno-ui-components" -import { useRouter } from "url-state-router" -import Breadcrumb from "./Breadcrumb" -import { Messages } from "messages-provider" - -const AppContainer = ({ tabsConfig, component, children }) => { - const { navigateTo, currentPath } = useRouter() - - const tabIndex = useMemo(() => { - if (!currentPath) return 0 - return tabsConfig.findIndex((tab) => currentPath.startsWith(tab.path)) - }, [currentPath]) - - return ( - <> - - - {tabsConfig.map((tab, index) => ( - navigateTo(tab.path)}> - - {tab.label} - - ))} - - {tabsConfig.map((tab, index) => ( - - ))} - - - - {component || children} - - - - ) -} - -export default AppContainer diff --git a/apps/heureka/src/components/backup/AppRouter.js b/apps/heureka/src/components/backup/AppRouter.js deleted file mode 100644 index 716a4a65f..000000000 --- a/apps/heureka/src/components/backup/AppRouter.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from "react" -import { Router, Route, Redirect, Switch } from "url-state-router" -import { useActions, Messages } from "messages-provider" - -import AppContainer from "./AppContainer" -import Services from "./Services" -import ServiceDetail from "./ServiceDetail" -import ChangesLogDetail from "./ChangesLogDetail" -import PatchLogNew from "./PatchLogNew" -import PatchLogDetail from "./PatchLogDetail" -import Components from "./Components" -import ComponentDetail from "./ComponentDetail" -import Vulnerabilities from "./Vulnerabilities" -import VulnerabilitiyDetails from "./VulnerabilitiyDetails" -import Users from "./Users" -import UserDetail from "./UserDetail" -import SupportGroups from "./SupportGroups" -import Home from "./Home" -import useStore from "../hooks/useStore" -import WelcomeView from "./WelcomeView" - -export const HOME_PATH = "/home" -export const SUPPORT_GROUP_PATH = "/support_group" -export const SERVICES_PATH = "/services" -export const COMPONENTS_PATH = "/components" -export const VULNERABILITIES_PATH = "/vulnerabilities" -export const USERS_PATH = "/users" -export const TABS_CONFIG = [ - { path: HOME_PATH, label: "Home", icon: "autoAwesomeMosaic" }, - { path: SERVICES_PATH, label: "Services", icon: "dns" }, - { path: COMPONENTS_PATH, label: "Components", icon: "widgets" }, - { - path: VULNERABILITIES_PATH, - label: "Vulnerabilities", - icon: "autoAwesomeMotion", - }, - { path: SUPPORT_GROUP_PATH, label: "Support group", icon: "manageAccounts" }, - { - path: USERS_PATH, - label: "Users", - icon: "accountCircle", - }, -] - -const AppRouter = (props) => { - const urlStateKey = useStore((state) => state.urlStateKey) - const auth = useStore((state) => state.auth) - const loggedIn = useStore((state) => state.loggedIn) - const login = useStore((state) => state.login) - const { addMessage } = useActions() - const embedded = useStore((state) => state.embedded) - - useEffect(() => { - if (auth?.error) { - addMessage({ - variant: "error", - text: parseError(auth?.error), - }) - } - }, [auth?.error]) - - return ( - <> - {/* wait util the user is logged in to avoid that url-state-router processes the wrong URL do tue Redirects in the login process*/} - {loggedIn && !auth?.error ? ( - <> - {/* wait util the urlStateKey is stored and retrieved to avoid to initialized the Router with nil stateID*/} - {urlStateKey && ( - - - - - - - - - - - - - - - - - - - - - - - - )} - - ) : embedded ? ( - "Authentication required!" - ) : ( - <> - - - - )} - - ) -} - -export default AppRouter diff --git a/apps/heureka/src/components/backup/Avatar.js b/apps/heureka/src/components/backup/Avatar.js deleted file mode 100644 index 6e41eb812..000000000 --- a/apps/heureka/src/components/backup/Avatar.js +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack } from "juno-ui-components" - -const avatarCss = ` -h-8 -w-8 -bg-theme-background-lvl-2 -rounded-full -bg-cover -` - -const Avatar = ({ userName, url }) => { - return ( - - {url && ( -
- )} - {userName && {userName}} - - ) -} - -export default Avatar diff --git a/apps/heureka/src/components/backup/Breadcrumb.js b/apps/heureka/src/components/backup/Breadcrumb.js deleted file mode 100644 index 0a12d4708..000000000 --- a/apps/heureka/src/components/backup/Breadcrumb.js +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" - -const Breadcrumb = ({}) => { - return "Breadcrumb" -} - -export default Breadcrumb diff --git a/apps/heureka/src/components/backup/ChangesLogDetail.js b/apps/heureka/src/components/backup/ChangesLogDetail.js deleted file mode 100644 index 19844211a..000000000 --- a/apps/heureka/src/components/backup/ChangesLogDetail.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - Button, - Panel, - PanelBody, - PanelFooter, - DataGrid, - DataGridRow, - DataGridCell, - Stack, - TextareaRow, -} from "juno-ui-components" -import { - DetailSection, - DetailSectionBox, - DetailContentHeading, - DetailSectionHeader, -} from "../styles" -import { changeLogExample1, changeLogExample2 } from "../helpers" -import { useRouter } from "url-state-router" -import { DateTime } from "luxon" -import ComponentsList from "./ComponentsList" -import { SERVICES_PATH } from "./AppRouter" - -const ChangesLogDetail = ({}) => { - const { options, routeParams, navigateTo } = useRouter() - const changeLogId = routeParams?.changeLogId - const serviceId = routeParams?.serviceId - - const change = useMemo(() => { - if (changeLogId === "4323") { - return changeLogExample1 - } - if (changeLogId === "1234") { - return changeLogExample2 - } - return {} - }, [changeLogId]) - - const createdAt = useMemo(() => { - if (change?.CreatedAt) { - return DateTime.fromSQL(change.CreatedAt).toLocaleString( - DateTime.DATETIME_SHORT - ) - } - }, [change?.CreatedAt]) - - const onPanelClose = () => { - navigateTo(`${SERVICES_PATH}/${serviceId}`) - } - - return ( - - -
-
- - - - ID: - - {change.ID} - - - - Date: - - {createdAt} - - -
-
- {change.Type === "manually" && ( - <> -
-

Components

-
- -
-
- -
- -
- - )} - {change.Type === "automatic" && ( - <> -
-

State before

-
- -
-
-
-

State after

-
- -
-
- - )} -
-
- ) -} - -export default ChangesLogDetail diff --git a/apps/heureka/src/components/backup/ChangesLogList.js b/apps/heureka/src/components/backup/ChangesLogList.js deleted file mode 100644 index e72dd39c4..000000000 --- a/apps/heureka/src/components/backup/ChangesLogList.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import ChangesLogListItem from "./ChangesLogListItem" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import HintNotFound from "./HintNotFound" - -const ChangesLogList = ({ changes, selectable }) => { - changes = useMemo(() => { - if (!changes) return [] - return changes - }, [changes]) - - const columnsLength = useMemo(() => { - const configurableCols = 4 - return selectable ? configurableCols + 1 : configurableCols - }, [selectable]) - - return ( - <> - - - {selectable && } - ID - Type - Date - Changed components - - {changes.length > 0 ? ( - <> - {changes.map((item, i) => ( - - ))} - - ) : ( - - - - - - )} - - - ) -} - -export default ChangesLogList diff --git a/apps/heureka/src/components/backup/ChangesLogListItem.js b/apps/heureka/src/components/backup/ChangesLogListItem.js deleted file mode 100644 index 8c304fbf4..000000000 --- a/apps/heureka/src/components/backup/ChangesLogListItem.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell, CheckboxRow } from "juno-ui-components" -import { DateTime } from "luxon" -import { Link } from "url-state-router" -import { useRouter } from "url-state-router" -import { SERVICES_PATH } from "./AppRouter" - -const ChangesLogListItem = ({ item, selectable }) => { - const { options, routeParams } = useRouter() - const serviceId = routeParams?.serviceId - - const createdAt = useMemo(() => { - if (item.CreatedAt) { - return DateTime.fromSQL(item.CreatedAt).toLocaleString( - DateTime.DATETIME_SHORT - ) - } - }, [item.CreatedAt]) - - return ( - - {selectable && ( - - - - )} - - - {item.ID} - - - {item.Type} - {createdAt} - - {item.Components.map((component) => component.Name).join(", ")} - - - ) -} - -export default ChangesLogListItem diff --git a/apps/heureka/src/components/backup/ComponentDetail.js b/apps/heureka/src/components/backup/ComponentDetail.js deleted file mode 100644 index ec843e4d9..000000000 --- a/apps/heureka/src/components/backup/ComponentDetail.js +++ /dev/null @@ -1,170 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useMemo } from "react" -import { - Icon, - DataGrid, - DataGridRow, - DataGridCell, - Container, -} from "juno-ui-components" -import { useRouter } from "url-state-router" -import { getComponent } from "../queries" -import { useActions } from "messages-provider" -import useStore from "../hooks/useStore" -import { - usersListToString, - componentDetailsByType, - parseError, -} from "../helpers" -import VulnerabilitiesList from "./VulnerabilitiesList" -import PackagesList from "./PackagesList" -import { - DetailSection, - DetailContentHeading, - DetailSectionHeader, -} from "../styles" -import HintLoading from "./HintLoading" -import HintNotFound from "./HintNotFound" -import ServicesList from "./ServicesList" - -const DetailSectionTop = ` -bg-theme-code-block -rounded-t -mb-0.5 -` - -const DetailSectionBottom = ` -bg-theme-code-block -rounded-b -pb-0.5 -` - -const ComponentDetail = () => { - const { options, routeParams } = useRouter() - - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const componentId = routeParams?.componentId - const { isLoading, isError, isFetching, data, error } = getComponent( - auth?.id_token, - endpoint, - componentId - ) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (error) { - addMessage({ - variant: "error", - text: parseError(error), - }) - } - }, [error]) - - const owners = useMemo(() => { - if (!data?.Owners) return [] - return usersListToString(data.Owners) - }, [data?.Owners]) - - const operators = useMemo(() => { - if (!data?.Operators) return [] - return usersListToString(data.Operators) - }, [data?.Operators]) - - return ( - - {isLoading && !data ? ( - - ) : ( - <> - {data ? ( - <> -

- {data.Name} -

- -
-
- - - - ID: - - {data.ID} - - - - Owners: - - {owners} - - - - Operators: - - {operators} - - - - Type: - - {data.Type} - - -
-
- - {componentDetailsByType(data).map((item, index) => ( - - - {`${item.label}: `} - - {item.value} - - ))} - -
-
- -
-

Belongs to

-
- -
-
- -
-

Vulnerabilities

-
- -
-
- -
-

Packages

-
- -
-
- - ) : ( - - )} - - )} -
- ) -} - -export default ComponentDetail diff --git a/apps/heureka/src/components/backup/Components.js b/apps/heureka/src/components/backup/Components.js deleted file mode 100644 index cf8e9ecb1..000000000 --- a/apps/heureka/src/components/backup/Components.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect, useCallback, useState } from "react" -import { getComponents, getComponentFilters } from "../queries" -import useStore from "../hooks/useStore" -import { useActions } from "messages-provider" -import { parseError } from "../helpers" -import { Container } from "juno-ui-components" -import Pagination from "./Pagination" -import ComponentsList from "./ComponentsList" -import FilterToolbar from "./FilterToolbar" -import HintLoading from "./HintLoading" - -const ITEMS_PER_PAGE = 10 - -const Components = () => { - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const [paginationOptions, setPaginationOptions] = useState({ - limit: ITEMS_PER_PAGE, - offset: 0, - }) - const [searchOptions, setSearchOptions] = useState({}) - const components = getComponents(auth?.id_token, endpoint, { - ...paginationOptions, - ...searchOptions, - }) - - const filters = getComponentFilters(auth?.id_token, endpoint) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (components.error) { - addMessage({ - variant: "error", - text: parseError(components.error), - }) - } - }, [components.error]) - - useEffect(() => { - if (filters.error) { - addMessage({ - variant: "error", - text: parseError(filters.error), - }) - } - }, [filters.error]) - - const onPaginationChanged = (offset) => { - setPaginationOptions({ ...paginationOptions, offset: offset }) - } - - const onSearchTerm = (options) => { - setSearchOptions(options) - } - - return ( - - {components.isLoading && !components.data ? ( - - ) : ( - <> - - - - - )} - - ) -} - -export default Components diff --git a/apps/heureka/src/components/backup/ComponentsList.js b/apps/heureka/src/components/backup/ComponentsList.js deleted file mode 100644 index a6bd54ccb..000000000 --- a/apps/heureka/src/components/backup/ComponentsList.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import ComponentsListItem from "./ComponentsListItem" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import HintNotFound from "./HintNotFound" -import { classifyVulnerabilities } from "../helpers" - -const ComponentsList = ({ - components, - columns, - sorted, - unlink, - selectable, -}) => { - components = useMemo(() => { - if (!components) return [] - // inforce input as array - if (!Array.isArray(components)) components = [components] - // sort components by threat level - if (sorted) { - return components - .sort((a, b) => { - const vulA = classifyVulnerabilities(a) - const vulB = classifyVulnerabilities(b) - return ( - vulA.critical - vulB.critical || - vulA.high - vulB.high || - vulA.medium - vulB.medium || - vulA.low - vulB.low - ) - }) - .reverse() - } - return components - }, [components]) - - columns = useMemo(() => { - if (!columns) { - return { - name: {}, - type: {}, - version: {}, - vulnerabilities: {}, - belongsTo: {}, - owners: {}, - operators: {}, - } - } - return columns - }, [columns]) - - const columnsLength = useMemo(() => { - const configurableCols = columns ? Object.keys(columns).length : 7 - return selectable ? configurableCols + 1 : configurableCols - }, [columns, selectable]) - - return ( - - - {selectable && } - {columns?.name && Name} - {columns?.type && Type} - {columns?.version && Version} - {columns?.vulnerabilities && ( - Vulnerabilities - )} - {columns?.belongsTo && Belongs to} - {columns?.owners && Owners} - {columns?.operators && Operators} - - {components.length > 0 ? ( - <> - {components.map((item, i) => ( - - ))} - - ) : ( - - - - - - )} - - ) -} - -export default ComponentsList diff --git a/apps/heureka/src/components/backup/ComponentsListItem.js b/apps/heureka/src/components/backup/ComponentsListItem.js deleted file mode 100644 index ef4c7bdec..000000000 --- a/apps/heureka/src/components/backup/ComponentsListItem.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell, CheckboxRow } from "juno-ui-components" -import { Link } from "url-state-router" -import VulnerabilitiesOverview from "./VulnerabilitiesOverview" -import { - classifyVulnerabilities, - usersListToString, - componentVersionByType, -} from "../helpers" -import { COMPONENTS_PATH } from "./AppRouter" -import CustomBadge from "./CustomBadge" - -const ComponentsListItem = ({ item, columns, unlink, selectable }) => { - const services = useMemo(() => { - if (!item.Services) return [] - return item.Services - }, [item.Services]) - - const vulnerabilities = useMemo(() => { - return classifyVulnerabilities(item) - }, [item]) - - const owners = useMemo(() => { - return usersListToString(item.Owners) - }, [item.Owners]) - - const operators = useMemo(() => { - return usersListToString(item.Operators) - }, [item.Operators]) - - return ( - - {selectable && ( - - - - )} - {columns?.name && ( - - {unlink ? ( - <>{item.Name} - ) : ( - - {item.Name} - - )} - - )} - {columns?.type && {item.Type}} - {columns?.version && ( - {componentVersionByType(item)} - )} - {columns?.vulnerabilities && ( - - - - )} - {columns?.belongsTo && ( - - - - )} - {columns?.owners && {owners}} - {columns?.operators && {operators}} - - ) -} - -export default ComponentsListItem diff --git a/apps/heureka/src/components/backup/CustomBadge.js b/apps/heureka/src/components/backup/CustomBadge.js deleted file mode 100644 index 1f6ff2620..000000000 --- a/apps/heureka/src/components/backup/CustomBadge.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Badge, Icon } from "juno-ui-components" - -const CustomBadge = ({ badgeVariant, icon, label, className }) => { - return ( - - - {label} - - ) -} - -export default CustomBadge diff --git a/apps/heureka/src/components/backup/CustomPageHeader.js b/apps/heureka/src/components/backup/CustomPageHeader.js deleted file mode 100644 index 5ca68b106..000000000 --- a/apps/heureka/src/components/backup/CustomPageHeader.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from "react" -import useStore from "../hooks/useStore" -import { PageHeader } from "juno-ui-components" -import HeaderUser from "./HeaderUser" - -const CustomPageHeader = () => { - const auth = useStore((state) => state.auth) - const logout = useStore((state) => state.logout) - const loggedIn = useStore((state) => state.loggedIn) - return ( - - {loggedIn && } - - ) -} - -export default CustomPageHeader diff --git a/apps/heureka/src/components/backup/EvidenceNew.js b/apps/heureka/src/components/backup/EvidenceNew.js deleted file mode 100644 index 070920ca0..000000000 --- a/apps/heureka/src/components/backup/EvidenceNew.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { Button, Panel, PanelBody, PanelFooter } from "juno-ui-components" - -const EvidenceNew = ({}) => { - return ( - - Panel Body - - ) -} - -export default EvidenceNew diff --git a/apps/heureka/src/components/backup/EvidencesList.js b/apps/heureka/src/components/backup/EvidencesList.js deleted file mode 100644 index 393e41bdd..000000000 --- a/apps/heureka/src/components/backup/EvidencesList.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import EvidencesListItem from "./EvidencesListItem" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import HintNotFound from "./HintNotFound" - -const EvidencesList = ({ evidences }) => { - evidences = useMemo(() => { - if (!evidences) return [] - return evidences - }, [evidences]) - - return ( - <> - - - ID - Date - - {evidences.length > 0 ? ( - <> - {evidences.map((item, i) => ( - - ))} - - ) : ( - - - - - - )} - - - ) -} - -export default EvidencesList diff --git a/apps/heureka/src/components/backup/EvidencesListItem.js b/apps/heureka/src/components/backup/EvidencesListItem.js deleted file mode 100644 index 1fcaa6a4b..000000000 --- a/apps/heureka/src/components/backup/EvidencesListItem.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell, Button } from "juno-ui-components" -import { DateTime } from "luxon" -import { Link } from "url-state-router" -import { useRouter } from "url-state-router" -import { SERVICES_PATH } from "./AppRouter" - -const EvidencesListItem = ({ item }) => { - const { options, routeParams } = useRouter() - const evidenceId = routeParams?.evidenceId - - const createdAt = useMemo(() => { - if (item.CreatedAt) { - return DateTime.fromSQL(item.CreatedAt).toLocaleString( - DateTime.DATETIME_SHORT - ) - } - }, [item.CreatedAt]) - - return ( - - {item.ID} - {createdAt} - - ) -} - -export default EvidencesListItem diff --git a/apps/heureka/src/components/backup/FilterToolbar.js b/apps/heureka/src/components/backup/FilterToolbar.js deleted file mode 100644 index a550f23d3..000000000 --- a/apps/heureka/src/components/backup/FilterToolbar.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { FilterToolbarStateProvider } from "./FilterToolbarStore" -import FilterToolbarCore from "./FilterToolbarCore" - -const FilterToolbar = ({ - filterTypes, - onSearchTerm, - isLoading, - filterLabels, - placeholders, -}) => { - return ( - - - - ) -} - -export default FilterToolbar diff --git a/apps/heureka/src/components/backup/FilterToolbarCore.js b/apps/heureka/src/components/backup/FilterToolbarCore.js deleted file mode 100644 index c1cdd4027..000000000 --- a/apps/heureka/src/components/backup/FilterToolbarCore.js +++ /dev/null @@ -1,109 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo, useState, useEffect, useCallback } from "react" -import { Filters, FilterPill } from "juno-ui-components" -import { useStore } from "./FilterToolbarStore" - -const toURLOptions = (items) => { - let options = [] - items.forEach((item) => { - options.push({ [item.key]: item.value }) - }) - return options -} - -// onSearchTerm (callback): returns key:value selected filters, ex: {name: "Elektra"} -// isLoading (boolean): sets the tool in loading state -// filterLabels (object): labels matching the filter key to be displayed in the dropdown. Ex: {name: "service name"} -// placeholders (object): placeholders matching the selected key -const FilterToolbarCore = ({ - onSearchTerm, - isLoading, - filterLabels, - placeholders, -}) => { - const selectedFilters = useStore(useCallback((state) => state.filters)) - const filterTypes = useStore(useCallback((state) => state.filterTypes)) - const addFilter = useStore((state) => state.addFilter) - const removeFilter = useStore((state) => state.removeFilter) - - const [placeholder, setPlaceholder] = useState("") - const [filterKey, setFilterKey] = useState("") - const [error, setError] = useState(null) - - const filterOptions = useMemo(() => { - if (typeof filterTypes !== "object") return [] - let result = [] - Object.keys(filterTypes).forEach((key) => { - // check if there is a label for the key - const label = - filterLabels && typeof filterLabels === "object" && filterLabels[key] - result.push({ label: label || key, key: key }) - }) - return result - }, [filterTypes, filterLabels]) - - useEffect(() => { - onSearchTerm(toURLOptions(selectedFilters)) - }, [selectedFilters]) - - const onSelectChange = (event) => { - const selectedValue = event.target.value - let label = `Please enter ${selectedValue}` - if (placeholders && placeholders[selectedValue]) { - label = placeholders[selectedValue] - } - // save the selected key - setFilterKey(selectedValue) - // set the new placeholder - setPlaceholder(label) - } - - const onPillClosed = (uid) => { - removeFilter(uid) - } - - const onFilter = (value) => { - setError(null) - if (filterKey === "") { - return setError("Please select a filter type") - } - if (value === "") { - return setError("Filter value can't be blank") - } - addFilter(filterKey, value) - } - - return ( - - {selectedFilters.map((item, index) => ( - - ))} - {error && ( -
- {error} -
- )} -
- ) -} - -export default FilterToolbarCore diff --git a/apps/heureka/src/components/backup/FilterToolbarStore.js b/apps/heureka/src/components/backup/FilterToolbarStore.js deleted file mode 100644 index 5b762bbeb..000000000 --- a/apps/heureka/src/components/backup/FilterToolbarStore.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import uniqueId from "lodash.uniqueid" -import { createStore, useStore as useZustandStore } from "zustand" -import React, { createContext, useContext, useRef } from "react" - -const StoreContext = createContext() - -export const SEARCH_STRING_TYPE = "string" -export const SEARCH_BOOL_TYPE = "bool" -export const SEARCH_ARRAY_TYPE = "[string]" - -// custom store for the selected filters -const initialStore = (filterTypes) => { - return createStore((set) => ({ - filterTypes: filterTypes || {}, - filters: [], // this is the initial state - addFilter: (key, value) => addFilter(set, key, value), - removeFilter: (key, value) => removeFilter(set, key, value), - })) -} - -const addFilter = (set, key, value) => - set((state) => { - // prevent to add duplicates - const index = state.filters.findIndex( - (item) => item.key === key && item.value === value - ) - if (index >= 0) return state - - // if key type is string or boolean do not add more than 1 filter, overwrite existing - if ( - state.filterTypes[key] === SEARCH_STRING_TYPE || - state.filterTypes[key] === SEARCH_BOOL_TYPE - ) { - const newFilters = state.filters.slice() - const foundItem = newFilters.find((element) => element.key === key) - if (foundItem) { - foundItem.value = value - return { ...state, filters: newFilters } - } - } - // add entry - let newFilters = state.filters - .slice() - .concat({ uid: uniqueId("filter-"), key: key, value: value }) - // sort entries - newFilters.sort((a, b) => a.key.localeCompare(b.key)) - return { ...state, filters: newFilters } - }) - -const removeFilter = (set, uid) => - set((state) => { - let newItems = state.filters.slice() - const index = newItems.findIndex((item) => item.uid === uid) - // if NOT found return - if (index < 0) return state - - newItems.splice(index, 1) - return { ...state, filters: newItems } - }) - -export const FilterToolbarStateProvider = ({ filterTypes, children }) => { - const store = useRef(initialStore(filterTypes)).current - return {children} -} - -const useStore = (selector) => { - const store = useContext(StoreContext) - return useZustandStore(store, selector) -} -export { useStore } diff --git a/apps/heureka/src/components/backup/HeaderUser.js b/apps/heureka/src/components/backup/HeaderUser.js deleted file mode 100644 index 8cdff6776..000000000 --- a/apps/heureka/src/components/backup/HeaderUser.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack, Button } from "juno-ui-components" -import Avatar from "./Avatar" - -const HeaderUser = ({ auth, logout }) => { - return ( - -
- -
- -
- -
- -

Evidences

-
- - - ) -} - -export default PatchLogDetail diff --git a/apps/heureka/src/components/backup/PatchLogNew.js b/apps/heureka/src/components/backup/PatchLogNew.js deleted file mode 100644 index 84929f36f..000000000 --- a/apps/heureka/src/components/backup/PatchLogNew.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useMemo } from "react" -import { - Button, - Panel, - PanelBody, - PanelFooter, - Form, - TextInputRow, -} from "juno-ui-components" -import { useRouter } from "url-state-router" -import { getService } from "../queries" -import useStore from "../hooks/useStore" -import ChangesLogList from "./ChangesLogList" -import { SERVICES_PATH } from "./AppRouter" -import { changeLogExample1, changeLogExample2 } from "../helpers" - -const PatchLogNew = ({}) => { - const { options, routeParams, navigateTo } = useRouter() - const serviceId = routeParams?.serviceId - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - - const changes = useMemo(() => { - return [changeLogExample1, changeLogExample2] - }, []) - - const onPanelClose = () => { - navigateTo(`${SERVICES_PATH}/${serviceId}`) - } - - const formPanelFooter = useMemo( - () => ( - - - - - ), - [] - ) - - return ( - - -
e.preventDefault()}> -

Components

- - - - - - -
-
- ) -} - -export default PatchLogNew diff --git a/apps/heureka/src/components/backup/PatchLogsList.js b/apps/heureka/src/components/backup/PatchLogsList.js deleted file mode 100644 index e1680c5f4..000000000 --- a/apps/heureka/src/components/backup/PatchLogsList.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import PatchLogsListItem from "./PatchLogsListItem" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import HintNotFound from "./HintNotFound" - -const PatchLogsList = ({ patches }) => { - patches = useMemo(() => { - if (!patches) return [] - return patches - }, [patches]) - - return ( - <> - - - ID - Date - Changes - Evidences - - {patches.length > 0 ? ( - <> - {patches.map((item, i) => ( - - ))} - - ) : ( - - - - - - )} - - - ) -} - -export default PatchLogsList diff --git a/apps/heureka/src/components/backup/PatchLogsListItem.js b/apps/heureka/src/components/backup/PatchLogsListItem.js deleted file mode 100644 index 4293a6cb3..000000000 --- a/apps/heureka/src/components/backup/PatchLogsListItem.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell, Button } from "juno-ui-components" -import { DateTime } from "luxon" -import CustomBadge from "./CustomBadge" -import { Link } from "url-state-router" -import { useRouter } from "url-state-router" -import { SERVICES_PATH } from "./AppRouter" - -const PatchLogsListItem = ({ item }) => { - const { options, routeParams } = useRouter() - const serviceId = routeParams?.serviceId - - const createdAt = useMemo(() => { - if (item.CreatedAt) { - return DateTime.fromSQL(item.CreatedAt).toLocaleString( - DateTime.DATETIME_SHORT - ) - } - }, [item.CreatedAt]) - - return ( - - - - {item.ID} - - - {createdAt} - - - - - - - - ) -} - -export default PatchLogsListItem diff --git a/apps/heureka/src/components/backup/ServiceDetail.js b/apps/heureka/src/components/backup/ServiceDetail.js deleted file mode 100644 index ff8b4f567..000000000 --- a/apps/heureka/src/components/backup/ServiceDetail.js +++ /dev/null @@ -1,202 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useMemo } from "react" -import { getService } from "../queries" -import useStore from "../hooks/useStore" -import { useActions } from "messages-provider" -import { useRouter } from "url-state-router" -import { - parseError, - patchExampl1, - patchExampl2, - changeLogExample1, - changeLogExample2, -} from "../helpers" -import { - Icon, - DataGrid, - DataGridRow, - DataGridCell, - Container, - Stack, - Button, -} from "juno-ui-components" -import { - DetailSection, - DetailSectionBox, - DetailContentHeading, - DetailSectionHeader, -} from "../styles" -import HintLoading from "./HintLoading" -import HintNotFound from "./HintNotFound" -import PatchLogsList from "./PatchLogsList" -import ComponentsList from "./ComponentsList" -import { SERVICES_PATH } from "./AppRouter" -import ChangesLogList from "./ChangesLogList" - -const listOfUsers = (users) => { - users = users || [] - return users.map((user, index) => ( - - {index ? ", " : ""} - {`${user.Name} `} - - ({user.SapID}) - - - )) -} -const ServiceDetail = () => { - const { options, routeParams, navigateTo } = useRouter() - - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const serviceId = routeParams?.serviceId - const { isLoading, isError, isFetching, data, error } = getService( - auth?.id_token, - endpoint, - serviceId - ) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (error) { - addMessage({ - variant: "error", - text: parseError(error), - }) - } - }, [error]) - - const owners = useMemo(() => { - if (!data?.Owners) return [] - return listOfUsers(data.Owners) - }, [data?.Owners]) - - const operators = useMemo(() => { - if (!data?.Operators) return [] - return listOfUsers(data.Operators) - }, [data?.Operators]) - - const components = useMemo(() => { - if (!data?.Components) return [] - return data.Components - }, [data]) - - const patches = useMemo(() => { - if (data?.Name && data?.Name === "Elektra") { - return [patchExampl2, patchExampl1] - } - return [] - }, [data]) - - const changes = useMemo(() => { - if (data?.Name && data?.Name === "Elektra") { - return [changeLogExample1, changeLogExample2] - } - return [] - }, [data]) - - return ( - - {isLoading && !data ? ( - - ) : ( - <> - {data ? ( - <> -

- {data.Name} -

- -
-
- - - - ID: - - {data.ID} - - - - Owners: - - {owners} - - - - Operators: - - {operators} - - -
-
- -
-

- Vulnerabilities in this service -

-
- -
-
- -
- -

Patches log

-
- -
- -

Changes log

-
- - ) : ( - - )} - - )} -
- ) -} - -export default ServiceDetail diff --git a/apps/heureka/src/components/backup/Services.js b/apps/heureka/src/components/backup/Services.js deleted file mode 100644 index f1863d3b4..000000000 --- a/apps/heureka/src/components/backup/Services.js +++ /dev/null @@ -1,93 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useState, useMemo } from "react" -import useStore from "../hooks/useStore" -import { useActions } from "messages-provider" -import { Container } from "juno-ui-components" -import { getServices, getServiceFilters } from "../queries" -import { parseError } from "../helpers" -import Pagination from "./Pagination" -import ServicesList from "./ServicesList" -import FilterToolbar from "./FilterToolbar" -import HintLoading from "./HintLoading" - -const ITEMS_PER_PAGE = 10 - -const Services = ({}) => { - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const addMessage = useActions() - const [paginationOptions, setPaginationOptions] = useState({ - limit: ITEMS_PER_PAGE, - offset: 0, - }) - const [searchOptions, setSearchOptions] = useState({}) - const services = getServices(auth?.id_token, endpoint, { - ...paginationOptions, - ...searchOptions, - }) - const filters = getServiceFilters(auth?.id_token, endpoint) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (services.error) { - addMessage({ - variant: "error", - text: parseError(services.error), - }) - } - }, [services.error]) - - useEffect(() => { - if (filters.error) { - addMessage({ - variant: "error", - text: parseError(filters.error), - }) - } - }, [filters.error]) - - const onPaginationChanged = (offset) => { - setPaginationOptions({ ...paginationOptions, offset: offset }) - } - - const onSearchTerm = (options) => { - setSearchOptions(options) - } - - return useMemo(() => { - return ( - - {services.isLoading && !services.data ? ( - - ) : ( - <> - - - - - )} - - ) - }, [services, filters]) -} - -export default Services diff --git a/apps/heureka/src/components/backup/ServicesList.js b/apps/heureka/src/components/backup/ServicesList.js deleted file mode 100644 index 397c780dd..000000000 --- a/apps/heureka/src/components/backup/ServicesList.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - Stack, - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import ServicesListItem from "./ServicesListItem" -import HintNotFound from "./HintNotFound" - -const ServicesList = ({ services, minimized }) => { - services = useMemo(() => { - if (!services) return [] - return services - }, [services]) - - return ( - <> - - - Name - {!minimized && ( - <> - Support group - Operators - Vulnerabilities - Components - - )} - - {services.length > 0 ? ( - <> - {services.map((item, i) => ( - - ))} - - ) : ( - - - - - - )} - - - ) -} - -export default ServicesList diff --git a/apps/heureka/src/components/backup/ServicesListItem.js b/apps/heureka/src/components/backup/ServicesListItem.js deleted file mode 100644 index ce73e5215..000000000 --- a/apps/heureka/src/components/backup/ServicesListItem.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell } from "juno-ui-components" -import { Link } from "url-state-router" -import { classifyVulnerabilities } from "../helpers" -import VulnerabilitiesOverview from "./VulnerabilitiesOverview" -import { SERVICES_PATH } from "./AppRouter" -import CustomBadge from "./CustomBadge" - -const cellClasses = ` -justify-start -` - -const listOfUsers = (users) => { - users = users || [] - return users.map((user, index) => ( - - {index ? ", " : ""} - {`${user.Name} `} - - ({user.SapID}) - - - )) -} - -const ServicesListItem = ({ item, minimized }) => { - const owners = useMemo(() => { - return listOfUsers(item.Owners) - }, [item.Owners]) - - const operators = useMemo(() => { - return listOfUsers(item.Operators) - }, [item.Operators]) - - const components = useMemo(() => { - if (!item?.Components) return [] - return item.Components - }, [item.Components]) - - const vulnerabilities = useMemo(() => { - return classifyVulnerabilities(components) - }, [components]) - - return ( - - - {item.Name} - - {!minimized && ( - <> - {owners} - {operators} - - - - -
- -
-
- - )} -
- ) -} - -export default ServicesListItem diff --git a/apps/heureka/src/components/backup/SupportGroups.js b/apps/heureka/src/components/backup/SupportGroups.js deleted file mode 100644 index 393a170f9..000000000 --- a/apps/heureka/src/components/backup/SupportGroups.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback } from "react" -import { Container } from "juno-ui-components" -import useStore from "../hooks/useStore" -import ServerGroupsList from "./SupportGroupsList" - -const ITEMS_PER_PAGE = 10 - -const SupportGroups = () => { - const endpoint = useStore(useCallback((state) => state.endpoint)) - - return ( - - - - ) -} - -export default SupportGroups diff --git a/apps/heureka/src/components/backup/SupportGroupsList.js b/apps/heureka/src/components/backup/SupportGroupsList.js deleted file mode 100644 index 437ad9404..000000000 --- a/apps/heureka/src/components/backup/SupportGroupsList.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import HintNotFound from "./HintNotFound" - -const ServerGroupsList = ({ supportGroups }) => { - supportGroups = useMemo(() => { - if (!supportGroups) return [] - return supportGroups - }, [supportGroups]) - - return ( - - - Name - Members - - {supportGroups.length > 0 ? ( - <> - No yet implemented - - ) : ( - - - - - - )} - - ) -} - -export default ServerGroupsList diff --git a/apps/heureka/src/components/backup/UserDetail.js b/apps/heureka/src/components/backup/UserDetail.js deleted file mode 100644 index 10c15893e..000000000 --- a/apps/heureka/src/components/backup/UserDetail.js +++ /dev/null @@ -1,116 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useMemo } from "react" -import { - Icon, - DataGrid, - DataGridRow, - DataGridCell, - Stack, - Spinner, - Container, -} from "juno-ui-components" -import { useRouter } from "url-state-router" -import { getUser } from "../queries" -import { useActions } from "messages-provider" -import useStore from "../hooks/useStore" -import { - DetailSection, - DetailSectionBox, - DetailContentHeading, - DetailSectionHeader, -} from "../styles" -import HintLoading from "./HintLoading" -import ServicesList from "./ServicesList" -import { parseError } from "../helpers" -import HintNotFound from "./HintNotFound" - -const UserDetail = ({}) => { - const { routeParams } = useRouter() - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const userId = routeParams?.userId - const { isLoading, isError, isFetching, data, error } = getUser( - auth?.id_token, - endpoint, - userId - ) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (error) { - addMessage({ - variant: "error", - text: parseError(error), - }) - } - }, [error]) - - return ( - - {isLoading && !data ? ( - - ) : ( - <> - {data ? ( - <> -

- {data.Name} -

- -
-
- - - - ID: - - {data.ID} - - - - Email: - - {data.Email} - - - - SAP ID: - - {data.SapID} - - - - Support team: - - Services team - - -
-
-
-

Owned services

-
- -
-
- -
-

Evidences

-
-
- - ) : ( - - )} - - )} -
- ) -} - -export default UserDetail diff --git a/apps/heureka/src/components/backup/Users.js b/apps/heureka/src/components/backup/Users.js deleted file mode 100644 index 5b33aa64d..000000000 --- a/apps/heureka/src/components/backup/Users.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useState } from "react" -import useStore from "../hooks/useStore" -import { useActions } from "messages-provider" -import { Container } from "juno-ui-components" -import { getUsers, getUserFilters } from "../queries" -import UsersList from "./UsersList" -import HintLoading from "./HintLoading" -import FilterToolbar from "./FilterToolbar" -import { parseError } from "../helpers" -import Pagination from "./Pagination" - -const ITEMS_PER_PAGE = 10 - -const Users = ({}) => { - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const [paginationOptions, setPaginationOptions] = useState({ - limit: ITEMS_PER_PAGE, - offset: 0, - }) - const [searchOptions, setSearchOptions] = useState({}) - const users = getUsers(auth?.id_token, endpoint, { - ...paginationOptions, - ...searchOptions, - }) - - const filters = getUserFilters(auth?.id_token, endpoint) - - useEffect(() => { - if (users.error) { - addMessage({ - variant: "error", - text: parseError(users.error), - }) - } - }, [users.error]) - - useEffect(() => { - if (filters.error) { - addMessage({ - variant: "error", - text: parseError(filters.error), - }) - } - }, [filters.error]) - - const onPaginationChanged = (offset) => { - setPaginationOptions({ ...paginationOptions, offset: offset }) - } - - const onSearchTerm = (options) => { - setSearchOptions(options) - } - - return ( - - {users.isLoading && !users.data ? ( - - ) : ( - <> - - - - - )} - - ) -} - -export default Users diff --git a/apps/heureka/src/components/backup/UsersList.js b/apps/heureka/src/components/backup/UsersList.js deleted file mode 100644 index e5bf4fdfe..000000000 --- a/apps/heureka/src/components/backup/UsersList.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - Stack, - DataGrid, - DataGridRow, - DataGridCell, - DataGridHeadCell, -} from "juno-ui-components" -import UserListItem from "./UsersListItem" -import HintNotFound from "./HintNotFound" - -const UsersList = ({ users }) => { - users = useMemo(() => { - if (!users) return [] - return users - }, [users]) - - return ( - <> - - - Name - SAP ID - Support group - Owned services - - {users.length > 0 ? ( - <> - {users.map((user, index) => ( - - ))} - - ) : ( - - - - - - )} - - - ) -} - -export default UsersList diff --git a/apps/heureka/src/components/backup/UsersListItem.js b/apps/heureka/src/components/backup/UsersListItem.js deleted file mode 100644 index b038b3581..000000000 --- a/apps/heureka/src/components/backup/UsersListItem.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell } from "juno-ui-components" -import { USERS_PATH } from "./AppRouter" -import { Link } from "url-state-router" -import Avatar from "./Avatar" -import CustomBadge from "./CustomBadge" - -const UserListItem = ({ item }) => { - const ownServices = useMemo(() => { - if (!item.OwnServices) return [] - return item.OwnServices - }, [item.OwnServices]) - - const evidences = useMemo(() => { - if (!item.Evidences) return [] - return item.Evidences - }, [item.Evidences]) - - return ( - - - - - - - {item.SapID} - Services team - - - - - ) -} - -export default UserListItem diff --git a/apps/heureka/src/components/backup/Vulnerabilities.js b/apps/heureka/src/components/backup/Vulnerabilities.js deleted file mode 100644 index 6fe1554ab..000000000 --- a/apps/heureka/src/components/backup/Vulnerabilities.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useState } from "react" -import useStore from "../hooks/useStore" -import { useActions } from "messages-provider" -import { Stack, Spinner, Container } from "juno-ui-components" -import { getVulnerabilities, getVulnerabilityFilters } from "../queries" -import { parseError } from "../helpers" -import Pagination from "./Pagination" -import VulnerabilitiesList from "./VulnerabilitiesList" -import FilterToolbar from "./FilterToolbar" -import HintLoading from "./HintLoading" - -const ITEMS_PER_PAGE = 10 - -const Vulnerabilities = ({}) => { - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const [paginationOptions, setPaginationOptions] = useState({ - limit: ITEMS_PER_PAGE, - offset: 0, - }) - const [searchOptions, setSearchOptions] = useState({}) - const vulnerabilities = getVulnerabilities(auth?.id_token, endpoint, { - ...paginationOptions, - ...searchOptions, - }) - const filters = getVulnerabilityFilters(auth?.id_token, endpoint) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (vulnerabilities.error) { - addMessage({ - variant: "error", - text: parseError(vulnerabilities.error), - }) - } - }, [vulnerabilities.error]) - - useEffect(() => { - if (filters.error) { - addMessage({ - variant: "error", - text: parseError(filters.error), - }) - } - }, [filters.error]) - - const onPaginationChanged = (offset) => { - setPaginationOptions({ ...paginationOptions, offset: offset }) - } - - const onSearchTerm = (options) => { - setSearchOptions(options) - } - - return ( - - {vulnerabilities.isLoading && !vulnerabilities.data ? ( - - ) : ( - <> - - - - - )} - - ) -} - -export default Vulnerabilities diff --git a/apps/heureka/src/components/backup/VulnerabilitiesList.js b/apps/heureka/src/components/backup/VulnerabilitiesList.js deleted file mode 100644 index d6bae33cf..000000000 --- a/apps/heureka/src/components/backup/VulnerabilitiesList.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, - Stack, -} from "juno-ui-components" -import VulnerabilitiesListItem from "./VulnerabilitiesListItem" -import { threadLevelToWeight } from "../helpers" -import HintNotFound from "./HintNotFound" - -const VulnerabilitiesList = ({ vulnerabilities, sortBy, minimized }) => { - vulnerabilities = useMemo(() => { - if (!vulnerabilities) return [] - if (!Array.isArray(vulnerabilities)) vulnerabilities = [vulnerabilities] - if (sortBy === "ThreatLevelOverall") { - return vulnerabilities - .sort( - (a, b) => - threadLevelToWeight(a[sortBy]) - threadLevelToWeight(b[sortBy]) - ) - .reverse() - } - return vulnerabilities - }, [vulnerabilities]) - - return ( - - - SCN/CVE - Threat level - {!minimized && Component} - Last modified - State - - {vulnerabilities.length > 0 ? ( - <> - {" "} - {vulnerabilities.map((item, index) => ( - - ))} - - ) : ( - - - - - - )} - - ) -} - -export default VulnerabilitiesList diff --git a/apps/heureka/src/components/backup/VulnerabilitiesListItem.js b/apps/heureka/src/components/backup/VulnerabilitiesListItem.js deleted file mode 100644 index 7e2403d0b..000000000 --- a/apps/heureka/src/components/backup/VulnerabilitiesListItem.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DataGridRow, DataGridCell } from "juno-ui-components" -import { DateTime } from "luxon" -import VulnerabilityBadge from "./VulnerabilityBadge" -import { Link } from "url-state-router" -import { VULNERABILITIES_PATH } from "./AppRouter" - -const IdClasses = ` -text-sm -pt-1 -whitespace-nowrap -text-theme-disabled -` -const VulnerabilityCss = ` -flex -` - -const VulnerabilitiesListItem = ({ item, minimized }) => { - const lastModifiedtString = useMemo(() => { - if (!item?.Scn?.ScnLastModified) return "No date available" - return DateTime.fromSQL(item.Scn.ScnLastModified).toLocaleString( - DateTime.DATETIME_SHORT - ) - }, [item?.Scn?.ScnLastModified]) - - return ( - - - - {item?.Scn?.Name} -
{item?.Scn?.CveID}
- -
- -
- -
-
- {!minimized && {item?.Component?.Name}} - {lastModifiedtString} - {item?.State} -
- ) -} - -export default VulnerabilitiesListItem diff --git a/apps/heureka/src/components/backup/VulnerabilitiesOverview.js b/apps/heureka/src/components/backup/VulnerabilitiesOverview.js deleted file mode 100644 index 99519c8cd..000000000 --- a/apps/heureka/src/components/backup/VulnerabilitiesOverview.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Icon, Badge } from "juno-ui-components" -import VulnerabilityBadge from "./VulnerabilityBadge" -import { - THREAD_LEVEL_LOW, - THREAD_LEVEL_MEDIUM, - THREAD_LEVEL_HIGH, - THREAD_LEVEL_CRITICAL, -} from "../helpers" - -const VulnerabilitiesOverview = ({ vulnerabilities }) => { - return ( -
- {vulnerabilities.low > 0 && ( - - )} - {vulnerabilities.medium > 0 && ( - - )} - {vulnerabilities.high > 0 && ( - - )} - {vulnerabilities.critical > 0 && ( - - )} -
- ) -} - -export default VulnerabilitiesOverview diff --git a/apps/heureka/src/components/backup/VulnerabilitiyDetails.js b/apps/heureka/src/components/backup/VulnerabilitiyDetails.js deleted file mode 100644 index c74a989bf..000000000 --- a/apps/heureka/src/components/backup/VulnerabilitiyDetails.js +++ /dev/null @@ -1,224 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useCallback, useEffect, useMemo } from "react" -import { - Icon, - DataGrid, - DataGridRow, - DataGridCell, - Container, -} from "juno-ui-components" -import { getVulnerability } from "../queries" -import useStore from "../hooks/useStore" -import { useActions } from "messages-provider" -import { useRouter } from "url-state-router" -import { parseError } from "../helpers" -import { - DetailSection, - DetailSectionBox, - DetailContentHeading, - DetailSectionHeader, -} from "../styles" -import HintLoading from "./HintLoading" -import HintNotFound from "./HintNotFound" -import { DateTime } from "luxon" - -const VulnerabilitiyDetails = () => { - const { options, routeParams } = useRouter() - - const endpoint = useStore(useCallback((state) => state.endpoint)) - const auth = useStore(useCallback((state) => state.auth)) - const { addMessage } = useActions() - const vulnerabilityId = routeParams?.vulnerabilityId - const { isLoading, isError, isFetching, data, error } = getVulnerability( - auth?.id_token, - endpoint, - vulnerabilityId - ) - - // dispatch error with useEffect because error variable will first set once all retries did not succeed - useEffect(() => { - if (error) { - addMessage({ - variant: "error", - text: parseError(error), - }) - } - }, [error]) - - const scnLastModified = useMemo(() => { - if (data?.Scn?.ScnLastModified) { - return DateTime.fromSQL(data?.Scn?.ScnLastModified).toLocaleString( - DateTime.DATETIME_SHORT - ) - } - }, [data?.Scn?.ScnLastModified]) - - const cveLastModified = useMemo(() => { - if (data?.Scn?.CveLastModified) { - return DateTime.fromSQL(data?.Scn?.CveLastModified).toLocaleString( - DateTime.DATETIME_SHORT - ) - } - }, [data?.Scn?.CveLastModified]) - - return ( - - {isLoading && !data ? ( - - ) : ( - <> - {data ? ( - <> -

- {" "} - {data?.Scn?.Name} -

-
-
- - - - ID: - - {data?.ID} - - - - CCScore: - - {data?.CCScore} - - - - CCScoreReason: - - {data?.CCScoreReason} - - - - State: - - {data?.State} - - -
-
- -
-

- SAP CERT Notifications (SCN) information -

-
-
- - - - Name: - - {data?.Scn?.Name} - - - - Last modified: - - {scnLastModified} - - - - Threat level client: - - - {data?.Scn?.ThreatLevelClient} - - - - - Threat level overall: - - - {data?.Scn?.ThreatLevelOverall} - - - - - Threat level server: - - - {data?.Scn?.ThreatLevelServer} - - - - - URL: - - - {data?.Scn?.URL && ( - - {data?.Scn?.URL} - - )} - - - -
-
-
- -
-

- Common Vulnerabilities and Exposures (CVE) information -

-
-
- - - - Name: - - {data?.Scn?.CveID} - - - - Last modified: - - {cveLastModified} - - - - URL: - - - {data?.Scn?.CveURL && ( - - {data?.Scn?.CveURL} - - )} - - - - - CVSS score: - - {data?.Scn?.CvssBase} - - -
-
-
- - ) : ( - - )} - - )} -
- ) -} - -export default VulnerabilitiyDetails diff --git a/apps/heureka/src/components/backup/VulnerabilityBadge.js b/apps/heureka/src/components/backup/VulnerabilityBadge.js deleted file mode 100644 index 1de5f4b99..000000000 --- a/apps/heureka/src/components/backup/VulnerabilityBadge.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { - THREAD_LEVEL_LOW, - THREAD_LEVEL_MEDIUM, - THREAD_LEVEL_HIGH, - THREAD_LEVEL_CRITICAL, -} from "../helpers" -import CustomBadge from "./CustomBadge" - -const badgeCss = ` - mr-2 -` - -const VulnerabilityBadge = ({ level, label }) => { - const icon = useMemo(() => { - const threadLevel = level || "" - switch (threadLevel.toLowerCase()) { - case THREAD_LEVEL_LOW: - return "severityLow" - case THREAD_LEVEL_MEDIUM: - return "severityMedium" - case THREAD_LEVEL_HIGH: - return "severityHigh" - case THREAD_LEVEL_CRITICAL: - return "severityCritical" - } - }, [level]) - - const badgeVariant = useMemo(() => { - const threadLevel = level || "" - switch (threadLevel.toLowerCase()) { - case THREAD_LEVEL_LOW: - return "default" - case THREAD_LEVEL_MEDIUM: - return "warning" - case THREAD_LEVEL_HIGH: - return "danger" - case THREAD_LEVEL_CRITICAL: - return "critical" - } - }, [level]) - - return ( - - ) -} - -export default VulnerabilityBadge diff --git a/apps/heureka/src/components/backup/WelcomeView.js b/apps/heureka/src/components/backup/WelcomeView.js deleted file mode 100644 index 40cbc73b0..000000000 --- a/apps/heureka/src/components/backup/WelcomeView.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack, Button } from "juno-ui-components" - -const WelcomeView = ({ loginCallback }) => { - return ( - -

- Welcome to the Converged Cloud Patch Management System -

-

Login to maintain & track vulnerabilities

-
-
- ) -} - -export default PaginationV2 diff --git a/apps/heureka/src/components/tabs/TabContext.jsx b/apps/heureka/src/components/tabs/TabContext.jsx deleted file mode 100644 index 034ba6abf..000000000 --- a/apps/heureka/src/components/tabs/TabContext.jsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { Container, TabNavigation, TabNavigationItem } from "juno-ui-components" -import TabPanel from "./TabPanel" -import { useActions, useActiveTab } from "../StoreProvider" - -import ServicesTab from "../services/ServicesTab" -import VulnerabilitiesTab from "../vulnerabilities/VulnerabilitiesTab" - -const TAB_CONFIG = [ - { - label: "Services", - value: "services", - icon: "dns", - component: ServicesTab, - }, - { - label: "Vulnerabilities", - value: "vulnerabilities", - icon: "autoAwesomeMotion", - component: VulnerabilitiesTab, - }, -] - -const TabContext = () => { - const { setActiveTab } = useActions() - const activeTab = useActiveTab() - - return ( - <> - setActiveTab(value)} - > - {TAB_CONFIG.map((tab) => ( - - ))} - - - {TAB_CONFIG.map((tab) => ( - - - - ))} - - - ) -} - -export default TabContext diff --git a/apps/heureka/src/components/tabs/TabPanel.jsx b/apps/heureka/src/components/tabs/TabPanel.jsx deleted file mode 100644 index 0c6655821..000000000 --- a/apps/heureka/src/components/tabs/TabPanel.jsx +++ /dev/null @@ -1,22 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo, useRef, useEffect } from "react" -import { useActiveTab } from "../StoreProvider" - -const TabPanel = ({ value, children }) => { - const activeTab = useActiveTab() - - // ATENTION!! compare with == since tabindex is int and value is string - const displayChildren = useMemo(() => activeTab == value, [activeTab, value]) - - return ( -
- {children} -
- ) -} - -export default TabPanel diff --git a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesList.jsx b/apps/heureka/src/components/vulnerabilities/VulnerabilitiesList.jsx deleted file mode 100644 index 08f9503d0..000000000 --- a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesList.jsx +++ /dev/null @@ -1,53 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { - DataGrid, - DataGridRow, - DataGridHeadCell, - DataGridCell, -} from "juno-ui-components" -import HintNotFound from "../shared/HintNotFound" -import HintLoading from "../shared/HintLoading" -import VulnerabilitiesListItem from "./VulnerabilitiesListItem" - -const VulnerabilitiesList = ({ vulnerabilities, isLoading }) => { - return ( - - - SCN/CVE - Threat level - Component - Last modified - State - - {isLoading && !vulnerabilities ? ( - - ) : ( - <> - {vulnerabilities?.length > 0 ? ( - <> - {vulnerabilities.map((item, index) => ( - - ))} - - ) : ( - - - - - - )} - - )} - - ) -} - -export default VulnerabilitiesList diff --git a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesListController.jsx b/apps/heureka/src/components/vulnerabilities/VulnerabilitiesListController.jsx deleted file mode 100644 index 67e676cdf..000000000 --- a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesListController.jsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { useQuery } from "@tanstack/react-query" -import { - useQueryClientFnReady, - useQueryOptions, - useActions, -} from "../StoreProvider" -import VulnerabilitiesList from "./VulnerabilitiesList" -import PaginationV2 from "../shared/PaginationV2" - -// targetRemediationDate -// discoveryDate -// severity -// remediationDate (detailview) - -const VulnerabilitiesListController = () => { - const queryClientFnReady = useQueryClientFnReady() - const queryOptions = useQueryOptions("vulnerabilities") - const { setQueryOptions } = useActions() - - const { isLoading, isFetching, isError, data, error } = useQuery({ - queryKey: [`vulnerabilities`, queryOptions], - enabled: !!queryClientFnReady, - }) - - const vulnerabilities = useMemo(() => { - if (!data) return null - return data?.VulnerabilityMatches?.edges - }, [data]) - - const pageInfo = useMemo(() => { - if (!data) return null - return data?.VulnerabilityMatches?.pageInfo - }, [data]) - - const { currentPage, totalPages } = useMemo(() => { - if (!data?.VulnerabilityMatches?.pageInfo?.pages) return {} - const pages = data?.VulnerabilityMatches?.pageInfo?.pages - let currentPage = null - const currentPageIndex = pages?.findIndex((page) => page?.isCurrent) - if (currentPageIndex > -1) { - currentPage = pages[currentPageIndex]?.pageNumber - } - const totalPages = pages?.length - return { currentPage, totalPages } - }, [data?.VulnerabilityMatches?.pageInfo]) - - const onPaginationChanged = (newPage) => { - if (!data?.VulnerabilityMatches?.pageInfo?.pages) return - const pages = data?.VulnerabilityMatches?.pageInfo?.pages - const currentPageIndex = pages?.findIndex( - (page) => page?.pageNumber === newPage - ) - if (currentPageIndex > -1) { - const after = pages[currentPageIndex]?.after - setQueryOptions("vulnerabilities", { - ...queryOptions, - after: `${after}`, - }) - } - } - - return ( - <> - - - - ) -} - -export default VulnerabilitiesListController diff --git a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesListItem.jsx b/apps/heureka/src/components/vulnerabilities/VulnerabilitiesListItem.jsx deleted file mode 100644 index 42e29eda9..000000000 --- a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesListItem.jsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { DataGridRow, DataGridCell } from "juno-ui-components" - -const IdClasses = ` -text-sm -pt-1 -whitespace-nowrap -text-theme-disabled -` -const VulnerabilityCss = ` -flex -` - -const VulnerabilitiesListItem = ({ item }) => { - return ( - - - {item?.node?.id} - - -
- {/* */} -
-
- {item?.Component?.Name} - - {item?.State} -
- ) -} - -export default VulnerabilitiesListItem diff --git a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesTab.jsx b/apps/heureka/src/components/vulnerabilities/VulnerabilitiesTab.jsx deleted file mode 100644 index f48d1cab5..000000000 --- a/apps/heureka/src/components/vulnerabilities/VulnerabilitiesTab.jsx +++ /dev/null @@ -1,19 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import VulnerabilitiesListController from "./VulnerabilitiesListController" -import Filters from "../filters/Filters" - -const VulnerabilitiesTab = () => { - return ( - <> - - - - ) -} - -export default VulnerabilitiesTab diff --git a/apps/heureka/src/helpers.js b/apps/heureka/src/helpers.js deleted file mode 100644 index 37a89b7d6..000000000 --- a/apps/heureka/src/helpers.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DateTime } from "luxon" - -export const parseError = (error) => { - if (!error || (typeof error === "object" && Object.keys(error).length === 0)) - return "An error occurred. There is no further information" - let errMsg = JSON.stringify(error) - if (error?.message) { - errMsg = error?.message - try { - const msgJson = JSON.parse(error?.message) - if (msgJson.error) errMsg = msgJson.error - if (msgJson.msg) errMsg = msgJson.msg - } catch (error) {} - } - return errMsg -} - -export const usersListToString = (users) => { - if (!users) users = [] - if (!Array.isArray(users)) users = [users] - - return users.map((user) => `${user.Name} (${user.SapID})`).join(", ") -} - -export const THREAD_LEVEL_LOW = "low" -export const THREAD_LEVEL_MEDIUM = "medium" -export const THREAD_LEVEL_HIGH = "high" -export const THREAD_LEVEL_CRITICAL = "critical" - -export const threadLevelToWeight = (level) => { - switch (level?.toLowerCase()) { - case THREAD_LEVEL_LOW: - return 3 - case THREAD_LEVEL_MEDIUM: - return 5 - case THREAD_LEVEL_HIGH: - return 8 - case THREAD_LEVEL_CRITICAL: - return 10 - } -} - -export const classifyVulnerabilitiesV2 = (vulnerabilites = []) => { - if (!vulnerabilites) vulnerabilites = [] - if (!Array.isArray(vulnerabilites)) vulnerabilites = [vulnerabilites] - let severities = { low: 0, medium: 0, high: 0, critical: 0 } - vulnerabilites.forEach((vulnerability) => { - // use of ThreatLevelOverall to get the index - switch (vulnerability?.ThreatLevelOverall?.toLowerCase()) { - case THREAD_LEVEL_LOW: - return (severities.low += 1) - case THREAD_LEVEL_MEDIUM: - return (severities.medium += 1) - case THREAD_LEVEL_HIGH: - return (severities.high += 1) - case THREAD_LEVEL_CRITICAL: - return (severities.critical += 1) - } - }) - return severities -} - -export const classifyVulnerabilities = (components = []) => { - if (!components) components = [] - if (!Array.isArray(components)) components = [components] - - let severities = { low: 0, medium: 0, high: 0, critical: 0 } - components.forEach((component) => { - // collect vulnerabilities from one component - if (component?.Vulnerabilities) { - const vulnerabilites = component?.Vulnerabilities - vulnerabilites.forEach((vulnerability) => { - // use of ThreatLevelOverall to get the index - switch (vulnerability?.ThreatLevelOverall?.toLowerCase()) { - case THREAD_LEVEL_LOW: - return (severities.low += 1) - case THREAD_LEVEL_MEDIUM: - return (severities.medium += 1) - case THREAD_LEVEL_HIGH: - return (severities.high += 1) - case THREAD_LEVEL_CRITICAL: - return (severities.critical += 1) - } - }) - } - }) - return severities -} - -export const COMPONENT_TYPE_KEPPEL = "KeppelImage" - -export const componentTypes = () => { - return [COMPONENT_TYPE_KEPPEL] -} - -export const componentDetailsByType = (component) => { - let detailKeys = [] - switch (component.Type) { - case COMPONENT_TYPE_KEPPEL: - detailKeys = [ - { - label: "Version", - value: componentVersionByType(component), - }, - { - label: "Maintainer", - value: component?.Details?.Maintainer, - }, - { - label: "Region", - value: component?.Details?.Region, - }, - { - label: "Source Repository", - value: component?.Details?.SourceRepository, - }, - ] - default: - } - return detailKeys -} - -export const componentVersionByType = (component) => { - let version = "" - switch (component.Type) { - case COMPONENT_TYPE_KEPPEL: - if (component?.Details?.PushedAt) { - version = DateTime.fromSeconds( - component?.Details?.PushedAt - ).toLocaleString(DateTime.DATETIME_SHORT) - } - default: - } - return version -} - -export const changeLogExample1 = { - ID: "4323", - Type: "automatic", - Components: [{ Name: "ubuntu" }, { Name: "alpine" }], - BeforeState: [ - { - ID: "333", - Name: "ubuntu", - Type: "KeppelImage", - Details: { PushedAt: 1543974164 }, - Vulnerabilities: [ - { - ID: 666, - ThreatLevelOverall: "Critical", - }, - ], - }, - ], - AfterState: [ - { - ID: "334", - Name: "alpine", - Type: "KeppelImage", - Details: { PushedAt: 1608021867 }, - Vulnerabilities: [], - }, - ], - CreatedAt: "2022-05-04 19:15:00.000", -} - -export const changeLogExample2 = { - ID: "1234", - Type: "manually", - Components: [ - { - Name: "absent-metrics-operator", - Type: "KeppelImage", - Details: { PushedAt: 1608021867 }, - }, - ], - BeforeState: [], - AfterState: [], - CreatedAt: "2022-04-29 14:15:00.000", -} - -export const patchExampl1 = { - ID: "123", - Type: "automatic", - Changes: [changeLogExample1], - CreatedAt: "2022-07-06 18:15:00.000", - Evidences: [], -} - -export const patchExampl2 = { - ID: "456", - Type: "automatic", - Changes: [changeLogExample2], - CreatedAt: "2022-05-04 19:15:00.000", - Evidences: [], -} diff --git a/apps/heureka/src/helpers.test.js b/apps/heureka/src/helpers.test.js deleted file mode 100644 index 691a20feb..000000000 --- a/apps/heureka/src/helpers.test.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { parseError, classifyVulnerabilities } from "./helpers" - -describe("Helpers", () => { - describe("parseError", () => { - test("return error as string if no object with message", () => { - expect(parseError({ error: "This is an error text" })).toEqual( - '{"error":"This is an error text"}' - ) - }) - test("return error message if object with message attr exists", () => { - expect(parseError({ message: "This is an error text" })).toEqual( - "This is an error text" - ) - }) - test("return error message if object message has attr msg", () => { - expect( - parseError({ message: '{ "msg": "This is an error text" }' }) - ).toEqual("This is an error text") - }) - test("return error message if object message has attr error", () => { - expect( - parseError({ message: '{ "error": "This is an error text" }' }) - ).toEqual("This is an error text") - }) - test("return standard error message if no object message available", () => { - expect(parseError({})).toEqual( - "An error occurred. There is no further information" - ) - }) - test("return standard error message if no object message available", () => { - expect(parseError()).toEqual( - "An error occurred. There is no further information" - ) - }) - test("return error text if just a string is available", () => { - expect(parseError("This is a mega error")).toEqual( - '"This is a mega error"' - ) - }) - }) - - describe("classifyVulnerabilities", () => { - test("return empty results if no components provided", () => { - expect(classifyVulnerabilities(classifyVulnerabilities)).toEqual({ - low: 0, - medium: 0, - high: 0, - critical: 0, - }) - }) - test("return empty results if null provided", () => { - expect(classifyVulnerabilities(null)).toEqual({ - low: 0, - medium: 0, - high: 0, - critical: 0, - }) - }) - test("map to array if object instead of array provided", () => { - expect( - classifyVulnerabilities({ - Vulnerabilities: [{ ThreatLevelOverall: "Medium" }], - }) - ).toEqual({ - low: 0, - medium: 1, - high: 0, - critical: 0, - }) - }) - test("classify all possibilities with and without capital letter", () => { - const component1 = { - Vulnerabilities: [ - { ThreatLevelOverall: "High" }, - { ThreatLevelOverall: "Low" }, - ], - } - const component2 = { - Vulnerabilities: [ - { ThreatLevelOverall: "Critical" }, - { ThreatLevelOverall: "Medium" }, - ], - } - const component3 = { - Vulnerabilities: [{ ThreatLevelOverall: "medium" }], - } - expect( - classifyVulnerabilities([component1, component2, component3]) - ).toEqual({ - low: 1, - medium: 2, - high: 1, - critical: 1, - }) - }) - }) -}) diff --git a/apps/heureka/src/hooks/useCommunication.js b/apps/heureka/src/hooks/useCommunication.js deleted file mode 100644 index a22ef054e..000000000 --- a/apps/heureka/src/hooks/useCommunication.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useReducer, useEffect, useCallback } from "react" -import { broadcast, get, watch } from "communicator" -import useStore from "./useStore" - -const useCommunication = () => { - console.log("[heureka] useCommunication setup") - - const setAuth = useStore((state) => state.setAuth) - const setLoggedIn = useStore((state) => state.setLoggedIn) - const setLoggedOut = useStore((state) => state.setLoggedOut) - const setLogin = useStore((state) => state.setLogin) - - useEffect(() => { - // get manually the current auth object in case the this app mist the first auth update message - // this is the case this app is loaded after the Auth app. - get( - "AUTH_GET_DATA", - (data) => { - setAuth(data.auth) - setLoggedIn(data.loggedIn) - }, - { debug: true } - ) - // watch for auth updates messages - // with the watcher we get the auth object when this app is loaded before the Auth app - const unwatch = watch( - "AUTH_UPDATE_DATA", - (data) => { - setAuth(data.auth) - setLoggedIn(data.loggedIn) - }, - { debug: true } - ) - return unwatch - }, [setAuth, setLoggedIn]) - - setLogin(() => { - broadcast("AUTH_LOGIN", "heureka", { debug: true }) - }) - - setLoggedOut(() => { - broadcast("AUTH_LOGOUT", "heureka") - }) -} - -export default useCommunication diff --git a/apps/heureka/src/hooks/useQueryClientFn.js b/apps/heureka/src/hooks/useQueryClientFn.js deleted file mode 100644 index 756353278..000000000 --- a/apps/heureka/src/hooks/useQueryClientFn.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect, useMemo } from "react" -import { useQueryClient } from "@tanstack/react-query" -import { useEndpoint, useActions } from "../components/StoreProvider" -import { request } from "graphql-request" -import sevicesQuery from "../lib/queries/services" -import vulnerabilityMatchesQuery from "../lib/queries/vulnerabilityMatches" -import ServiceFilterQuery from "../lib/queries/serviceFilters" - -// hook to register query defaults that depends on the queryClient and options -const useQueryClientFn = () => { - const queryClient = useQueryClient() - const endpoint = useEndpoint() - const { setQueryClientFnReady } = useActions() - - /* - As stated in getQueryDefaults, the order of registration of query defaults does matter. Since the first matching defaults are returned by getQueryDefaults, the registration should be made in the following order: from the least generic key to the most generic one. This way, in case of specific key, the first matching one would be the expected one. - */ - useEffect(() => { - if (!queryClient || !endpoint) return - console.log("useQueryClientFn::: setting defaults") - - queryClient.setQueryDefaults(["services"], { - queryFn: async ({ queryKey }) => { - const [_key, options] = queryKey - console.log("useQueryClientFn::: queryKey: ", queryKey, options) - return await request(endpoint, sevicesQuery(), options) - }, - }) - - queryClient.setQueryDefaults(["vulnerabilities"], { - queryFn: async ({ queryKey }) => { - const [_key, options] = queryKey - console.log("useQueryClientFn::: queryKey: ", queryKey) - return await request(endpoint, vulnerabilityMatchesQuery(), options) - }, - }) - - queryClient.setQueryDefaults(["serviceFilters"], { - queryFn: async ({ queryKey }) => { - console.log("useQueryClientFn::: queryKey: ", queryKey) - return await request(endpoint, ServiceFilterQuery()) - }, - staleTime: Infinity, // this do not change often keep it until reload - }) - - // set queryClientFnReady to true once - setQueryClientFnReady(true) - }, [queryClient, endpoint]) -} - -export default useQueryClientFn diff --git a/apps/heureka/src/hooks/useUrlState.js b/apps/heureka/src/hooks/useUrlState.js deleted file mode 100644 index af5835d23..000000000 --- a/apps/heureka/src/hooks/useUrlState.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useState, useEffect } from "react" -import { registerConsumer } from "url-state-provider" -import { useActions, useActiveTab } from "../components/StoreProvider" - -const DEFAULT_KEY = "heureka" -const ACTIVE_TAB = "t" - -const useUrlState = (key) => { - const [isURLRead, setIsURLRead] = useState(false) - // it is possible to have two apps instances on the same page - // int his case the key should be different per app - const urlStateManager = registerConsumer(key || DEFAULT_KEY) - - const { setActiveTab } = useActions() - const activeTab = useActiveTab() - - // Set initial state from URL (on login) - useEffect(() => { - if (isURLRead) return - console.log( - `HEUREKA: (${key || DEFAULT_KEY}) setting up state from url:`, - urlStateManager.currentState() - ) - - // READ the url state and set the state - const newTabIndex = urlStateManager.currentState()?.[ACTIVE_TAB] - // SAVE the state - if (newTabIndex) setActiveTab(newTabIndex) - setIsURLRead(true) - }, [isURLRead]) - - // SYNC states to URL state - useEffect(() => { - if (!isURLRead) return - urlStateManager.push({ - [ACTIVE_TAB]: activeTab, - }) - }, [isURLRead, activeTab]) -} - -export default useUrlState diff --git a/apps/heureka/src/img/app_bg_example.svg b/apps/heureka/src/img/app_bg_example.svg deleted file mode 100644 index b28325349..000000000 --- a/apps/heureka/src/img/app_bg_example.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - diff --git a/apps/heureka/src/index.js b/apps/heureka/src/index.js deleted file mode 100644 index f6f988315..000000000 --- a/apps/heureka/src/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createRoot } from "react-dom/client" -import React from "react" - -// export mount and unmount functions -export const mount = (container, options = {}) => { - import("./App").then((App) => { - mount.root = createRoot(container) - mount.root.render(React.createElement(App.default, options?.props)) - }) -} - -export const unmount = () => mount.root && mount.root.unmount() diff --git a/apps/heureka/src/lib/queries/serviceFilters.js b/apps/heureka/src/lib/queries/serviceFilters.js deleted file mode 100644 index 5ba6c2080..000000000 --- a/apps/heureka/src/lib/queries/serviceFilters.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { gql } from "graphql-request" - -// gql -// It is there for convenience so that you can get the tooling support -// like prettier formatting and IDE syntax highlighting. -// You can use gql from graphql-tag if you need it for some reason too. -export default () => gql` - { - __type(name: "ServiceFilter") { - name - inputFields { - name - type { - name - kind - ofType { - name - kind - enumValues { - name - } - } - } - } - } - } -` diff --git a/apps/heureka/src/lib/queries/services.js b/apps/heureka/src/lib/queries/services.js deleted file mode 100644 index ab294fb75..000000000 --- a/apps/heureka/src/lib/queries/services.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { gql } from "graphql-request" - -// gql -// It is there for convenience so that you can get the tooling support -// like prettier formatting and IDE syntax highlighting. -// You can use gql from graphql-tag if you need it for some reason too. -export default () => gql` - query ($filter: ServiceFilter, $first: Int, $after: String) { - Services(filter: $filter, first: $first, after: $after) { - __typename - totalCount - edges { - node { - id - name - owners { - totalCount - edges { - node { - id - sapID - name - } - cursor - } - pageInfo { - hasNextPage - nextPageAfter - } - } - supportGroups { - totalCount - edges { - node { - id - name - } - cursor - } - pageInfo { - hasNextPage - nextPageAfter - } - } - activities { - totalCount - edges { - node { - id - } - cursor - } - pageInfo { - hasNextPage - nextPageAfter - } - } - advisoryRepositories { - totalCount - edges { - node { - id - name - url - created_at - updated_at - } - cursor - priority - created_at - updated_at - } - pageInfo { - hasNextPage - nextPageAfter - } - } - } - cursor - } - pageInfo { - hasNextPage - nextPageAfter - } - } - } -` diff --git a/apps/heureka/src/lib/queries/vulnerabilityMatches.js b/apps/heureka/src/lib/queries/vulnerabilityMatches.js deleted file mode 100644 index 0c3b3d806..000000000 --- a/apps/heureka/src/lib/queries/vulnerabilityMatches.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { gql } from "graphql-request" - -// gql -// It is there for convenience so that you can get the tooling support -// like prettier formatting and IDE syntax highlighting. -// You can use gql from graphql-tag if you need it for some reason too. -export default () => gql` - query ($filter: VulnerabilityMatchFilter, $first: Int, $after: String) { - VulnerabilityMatches(filter: $filter, first: $first, after: $after) { - __typename - totalCount - edges { - node { - id - status - remediationDate - discoveryDate - targetRemediationDate - severity { - value - score - } - evidences { - totalCount - edges { - node { - id - description - } - cursor - } - pageInfo { - hasNextPage - nextPageAfter - } - } - vulnerabilityDisclosureId - vulnerabilityDisclosure { - id - lastModified - } - componentInstanceId - componentInstance { - id - ccrn - count - } - vulnerabilityMatchChanges { - totalCount - edges { - node { - id - action - vulnerabilityMatchId - activityId - } - cursor - } - pageInfo { - hasNextPage - nextPageAfter - } - } - } - cursor - } - pageInfo { - hasNextPage - hasPreviousPage - isValidPage - pageNumber - nextPageAfter - pages { - after - isCurrent - pageNumber - pageCount - } - } - } - } -` diff --git a/apps/heureka/src/lib/store.js b/apps/heureka/src/lib/store.js deleted file mode 100644 index b35ad2071..000000000 --- a/apps/heureka/src/lib/store.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createStore } from "zustand" -import { devtools } from "zustand/middleware" -import produce from "immer" - -export default (options) => - createStore( - devtools((set, get) => ({ - isUrlStateSetup: false, - queryClientFnReady: false, - endpoint: options?.apiEndpoint, - - activeTab: "services", - tabs: { - services: { - queryOptions: { - first: 20, - }, - }, - vulnerabilities: { - queryOptions: { - first: 20, - }, - }, - }, - - actions: { - setQueryClientFnReady: (readiness) => - set( - (state) => { - state.queryClientFnReady = readiness - }, - false, - "setQueryClientFnReady" - ), - setActiveTab: (index) => - set( - (state) => { - state.activeTab = index - }, - false, - "setActiveTab" - ), - setQueryOptions: (tab, options) => - set( - produce((state) => { - state.tabs[tab].queryOptions = options - }), - false, - "setQueryOptions" - ), - }, - })) - ) diff --git a/apps/heureka/src/queries.js b/apps/heureka/src/queries.js deleted file mode 100644 index 210c7fcf0..000000000 --- a/apps/heureka/src/queries.js +++ /dev/null @@ -1,205 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useQuery } from "@tanstack/react-query" -import { - services, - serviceFilters, - service, - components, - componentFilters, - component, - vulnerabilities, - vulnerability, - vulnerabilityFilters, - users, - user, - userFilters, -} from "./actions" - -// get all services -export const getServices = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["services", bearerToken, endpoint, options], - queryFn: services, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - }) -} - -export const getServiceFilters = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["serviceFilters", bearerToken, endpoint, options], - queryFn: serviceFilters, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - // If a user leaves your application and returns to stale data, React Query automatically requests fresh data for you in the background. - // You can disable this globally or per-query using the refetchOnWindowFocus option - refetchOnWindowFocus: false, - }) -} - -export const getService = ( - bearerToken, - endpoint, - serviceId, - placeholderData -) => { - return useQuery({ - queryKey: ["service", bearerToken, endpoint, serviceId], - queryFn: service, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // Placeholder data allows a query to behave as if it already has data, similar to the initialData option, - // but the data is not persisted to the cache. This comes in handy for situations where you have enough partial (or fake) - // data to render the query successfully while the actual data is fetched in the background. - placeholderData: placeholderData, - }) -} - -// get all components -export const getComponents = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["components", bearerToken, endpoint, options], - queryFn: components, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - }) -} - -export const getComponentFilters = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["componentFilters", bearerToken, endpoint, options], - queryFn: componentFilters, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - // If a user leaves your application and returns to stale data, React Query automatically requests fresh data for you in the background. - // You can disable this globally or per-query using the refetchOnWindowFocus option - refetchOnWindowFocus: false, - }) -} - -export const getComponent = ( - bearerToken, - endpoint, - componentId, - placeholderData -) => { - return useQuery({ - queryKey: ["component", bearerToken, endpoint, componentId], - queryFn: component, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // Placeholder data allows a query to behave as if it already has data, similar to the initialData option, - // but the data is not persisted to the cache. This comes in handy for situations where you have enough partial (or fake) - // data to render the query successfully while the actual data is fetched in the background. - placeholderData: placeholderData, - }) -} - -export const getVulnerabilities = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["vulnerabilities", bearerToken, endpoint, options], - queryFn: vulnerabilities, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - }) -} - -export const getVulnerability = ( - bearerToken, - endpoint, - vulnerabilityId, - placeholderData -) => { - return useQuery({ - queryKey: ["user", bearerToken, endpoint, vulnerabilityId], - queryFn: vulnerability, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // Placeholder data allows a query to behave as if it already has data, similar to the initialData option, - // but the data is not persisted to the cache. This comes in handy for situations where you have enough partial (or fake) - // data to render the query successfully while the actual data is fetched in the background. - placeholderData: placeholderData, - }) -} - -export const getVulnerabilityFilters = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["vulnerabilityFilters", bearerToken, endpoint, options], - queryFn: vulnerabilityFilters, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - // If a user leaves your application and returns to stale data, React Query automatically requests fresh data for you in the background. - // You can disable this globally or per-query using the refetchOnWindowFocus option - refetchOnWindowFocus: false, - }) -} - -export const getUsers = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["users", bearerToken, endpoint, options], - queryFn: users, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - }) -} - -export const getUser = (bearerToken, endpoint, userId, placeholderData) => { - return useQuery({ - queryKey: ["user", bearerToken, endpoint, userId], - queryFn: user, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // Placeholder data allows a query to behave as if it already has data, similar to the initialData option, - // but the data is not persisted to the cache. This comes in handy for situations where you have enough partial (or fake) - // data to render the query successfully while the actual data is fetched in the background. - placeholderData: placeholderData, - }) -} - -export const getUserFilters = (bearerToken, endpoint, options) => { - return useQuery({ - queryKey: ["userFilters", endpoint, options], - queryFn: userFilters, - // The query will not execute until the bearerToken exists - enabled: !!bearerToken, - // The data from the last successful fetch available while new data is being requested, even though the query key has changed. - // When the new data arrives, the previous data is seamlessly swapped to show the new data. - // isPreviousData is made available to know what data the query is currently providing you - keepPreviousData: true, - // If a user leaves your application and returns to stale data, React Query automatically requests fresh data for you in the background. - // You can disable this globally or per-query using the refetchOnWindowFocus option - refetchOnWindowFocus: false, - }) -} diff --git a/apps/heureka/src/styles.js b/apps/heureka/src/styles.js deleted file mode 100644 index d18e47105..000000000 --- a/apps/heureka/src/styles.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const DetailContentHeading = ` -jn-font-bold -jn-text-lg -jn-text-theme-high -jn-pb-2 - ` - -export const DetailSection = ` -mt-6 -` - -export const DetailSectionHeader = ` -font-bold -mt-4 -text-lg -` - -export const DetailSectionBox = ` -bg-theme-background-lvl-1 -rounded -pb-0.5 -` diff --git a/apps/heureka/src/styles.scss b/apps/heureka/src/styles.scss deleted file mode 100644 index 8c927175f..000000000 --- a/apps/heureka/src/styles.scss +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors -// SPDX-License-Identifier: Apache-2.0 - -/* Do not remove these tailwind directives. Without them styles won't work as expected */ -@tailwind base; -@tailwind components; -@tailwind utilities; - - -/* If necessary, app styles can be added below */ - - diff --git a/apps/heureka/tailwind.config.js b/apps/heureka/tailwind.config.js deleted file mode 100644 index 4ee65e3b0..000000000 --- a/apps/heureka/tailwind.config.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// opacity helper to make custom colors work with opacity -function withOpacity(variableName) { - return ({ opacityVariable, opacityValue }) => { - if (opacityValue !== undefined) { - return `rgba(var(${variableName}), ${opacityValue})` - } - if (opacityVariable !== undefined) { - return `rgba(var(${variableName}), var(${opacityVariable}, 1))` - } - return `rgb(var(${variableName}))` - } -} - -module.exports = { - presets: [require("juno-ui-components/build/lib/tailwind.config")], - prefix: "", // important, do not change - content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], - corePlugins: { - preflight: false, - }, - theme: {}, - plugins: [], -} diff --git a/apps/playground/package.json b/apps/playground/package.json index 1b2b95459..1b2255011 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -29,7 +29,7 @@ "esbuild": "^0.19.5", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -40,7 +40,7 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", "zustand": "4.3.7" }, @@ -50,11 +50,11 @@ "build": "NODE_ENV=production node esbuild.config.js" }, "peerDependencies": { - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "18.2.0", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "zustand": "4.3.7" }, "importmapExtras": { diff --git a/apps/supernova/LICENSE b/apps/supernova/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/apps/supernova/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - 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 [yyyy] [name of copyright owner] - - 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/apps/supernova/README.md b/apps/supernova/README.md deleted file mode 100644 index b6c41b801..000000000 --- a/apps/supernova/README.md +++ /dev/null @@ -1,134 +0,0 @@ -Supernova is an alternative UI for Prometheus Alertmanager with some quality of life improvements: - -- Micro frontend design based on [Juno UI components](https://ui.juno.global.cloud.sap) -- Predefined filter categories -- Easy filtering -- Autodiscover of the support group and added automatically as filter -- Aggregation and counting of alerts by region and severity -- Automatic URL linking for URLs in descriptions -- Parsing of alert labels for included external links -- Display of active and expired silences for a given alert -- Warning of an existing silence displaying the exact expiration time when creating new silences - -## Concepts - -### Alerts - -Alerting rules offer the capability to define alert conditions using expressions in the Prometheus expression language. These rules enable you to specify conditions that trigger alerts, and subsequently send notifications regarding the firing alerts to an external service. Whenever the alert expression results in one or more vector elements at a given point in time, the alert counts as active for these elements `label sets`. - -#### Labels - -The labels clause allows specifying a set of additional labels to be attached to the alert. Following is a live example of a set of labels from an alert of the `support group:containers` with `severity:info` in the `region:eu-de-2`. - -```js -{ - ... - "labels": { - "alertname": "PodOOMKilled", - "cluster": "eu-de-2", - "cluster_type": "metal", - "context": "memory", - "label_ccloud_support_group": "containers", - "meta": "Pod kube-system/kube-system-metal-owner-label-injector-28150200-2vgk5 OOMKilled", - "namespace": "kube-system", - "no_alert_on_absence": "true", - "playbook": "docs/support/playbook/kubernetes/k8s_pod_oomkilled", - "pod_name": "kube-system-metal-owner-label-injector-28150200-2vgk5", - "prometheus": "kube-monitoring/kubernetes", - "region": "eu-de-2", - "service": "resources", - "severity": "info", - "support_group": "containers", - "tier": "k8s", - "status": "active" - } - ... -} -``` - -### Silences - -Silences are a straightforward way to simply mute alerts for a given time. A silence is configured based on matchers. Incoming alerts are checked whether they match all the equality matchers of an active silence. If they do, no notifications will be sent out for that alert. - -#### Matchers - -A matcher is a string with a syntax inspired by PromQL and OpenMetrics. Matchers are ANDed together, meaning that all matchers must evaluate to "true" when tested against the labels on a given alert. - -When utilizing Supernova to add a silence, the matchers will be preselected based on the alert you selected. Moreover, through the advanced section, you have the option to include additional labels that are excluded by default. These exclusions are dependent on the configured excluded labels, which will be explained in detail in the section below. - -Given an alert with following labels: - -```js -{ - ... - fingerprint: "alert123", - labels: { - severity: "critical", - support_group: "containers", - service: "automation", - } - ... -} -``` - -In order to prevent the alert from continuing to trigger, we require a silence that includes the following matchers: - -```js -{ - ... - id: "silence123", - matchers: [ - { name: "severity", value: "critical" }, - { name: "support_group", value: "containers" }, - { name: "service", value: "automation" }, - ], - ... -} -``` - -## Configuration - -### Filter labels - -Filter labels are a set of labels that are utilized to define the criteria by which alerts will be filtered, if those labels exist within the fetched alerts. These filter labels enable you to selectively narrow down the alerts based on specific label values, resulting in a more targeted and refined alert filtering process. - -To set the filter labels: - -1. Utilize the app prop `filterLabels`, which is used during the setup of the script tag. For further information, please consult the [Get Started]() section. - -### Silence excluded alert labels - -Excluded labels are a collection of labels that are automatically excluded by default when configuring silence matchers. These labels, such as `pod`, `pod_name` or `instance`, often undergo frequent value changes, causing new alerts to be triggered that are not covered by the existing silence. - -Consider the following example: an alert is triggered when a pod runs out of memory and gets killed `Out Of Memory killed`. When this pod is recreated, it receives a different name. If the pod runs again out of memory because of the same issue, a new alarm will be triggered, but it won't be covered if we had used the `pod_name` as a matcher in the silence configuration. - -PodOOMKilled alarm labels example: - -```js -{ - "alertname": "PodOOMKilled", - "cluster": "eu-de-1", - "cluster_type": "metal", - "context": "memory", - "label_ccloud_service": "keppel", - "label_ccloud_support_group": "containers", - "meta": "Pod keppel/keppel-janitor-6dc777bcbf-5xrns OOMKilled", - "namespace": "keppel", - "no_alert_on_absence": "true", - "playbook": "docs/support/playbook/kubernetes/k8s_pod_oomkilled", - "pod_name": "keppel-janitor-6dc777bcbf-5xrns", - "prometheus": "kube-monitoring/kubernetes", - "region": "eu-de-1", - "service": "resources", - "severity": "info", - "support_group": "containers", - "tier": "k8s", - "status": "active" -} -``` - -If the end user wishes to include any excluded labels as matchers, they can easily do so by expanding the advanced section during the silence creation process. This allows for greater flexibility and customization when configuring the silence matchers. - -To set the excluded alert labels: - -1. Utilize the app prop `silenceExcludedLabels`, which is used during the setup of the script tag. For further information, please consult the [Get Started]() section. diff --git a/apps/supernova/__mocks__/client.js b/apps/supernova/__mocks__/client.js deleted file mode 100644 index 84531d3c2..000000000 --- a/apps/supernova/__mocks__/client.js +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { JSDOM } from "jsdom" -const dom = new JSDOM() -global.document = dom.window.document -global.window = dom.window diff --git a/apps/supernova/__mocks__/fileMock.js b/apps/supernova/__mocks__/fileMock.js deleted file mode 100644 index 27ce65aca..000000000 --- a/apps/supernova/__mocks__/fileMock.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = "test-file-stub" diff --git a/apps/supernova/__mocks__/styleMock.js b/apps/supernova/__mocks__/styleMock.js deleted file mode 100644 index d74516001..000000000 --- a/apps/supernova/__mocks__/styleMock.js +++ /dev/null @@ -1,6 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = {} diff --git a/apps/supernova/babel.config.js b/apps/supernova/babel.config.js deleted file mode 100644 index abc1af7d1..000000000 --- a/apps/supernova/babel.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - presets: ["@babel/preset-env", "@babel/preset-react"], - env: { - test: { - plugins: [["babel-plugin-transform-import-meta", { module: "ES6" }]], - }, - }, -} diff --git a/apps/supernova/esbuild.config.js b/apps/supernova/esbuild.config.js deleted file mode 100644 index 70ff2bed5..000000000 --- a/apps/supernova/esbuild.config.js +++ /dev/null @@ -1,211 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const esbuild = require("esbuild") -const fs = require("node:fs/promises") -const pkg = require("./package.json") -const postcss = require("postcss") -const sass = require("sass") -const { transform } = require("@svgr/core") -const url = require("postcss-url") -// this function generates app props based on package.json and propSecrets.json -const appProps = require("../../helpers/appProps") - -if (!/.+\/.+\.js/.test(pkg.module)) - throw new Error( - "module value is incorrect, use DIR/FILE.js like build/index.js" - ) - -const isProduction = process.env.NODE_ENV === "production" -const IGNORE_EXTERNALS = process.env.IGNORE_EXTERNALS === "true" -// in dev environment we prefix output file with public -let outfile = `${isProduction ? "" : "public/"}${pkg.main || pkg.module}` -// get output from outputfile -let outdir = outfile.slice(0, outfile.lastIndexOf("/")) -const args = process.argv.slice(2) -const watch = args.indexOf("--watch") >= 0 -const serve = args.indexOf("--serve") >= 0 - -const green = "\x1b[32m%s\x1b[0m" -const yellow = "\x1b[33m%s\x1b[0m" -const clear = "\033c" - -// shared config -const config = { - bundle: true, - minify: isProduction, - // target: ["es2020"], - target: ["es2020"], //["chrome64", "firefox67", "safari11.1", "edge79"], - format: "esm", - platform: "browser", - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - loader: { ".js": "jsx" }, - sourcemap: isProduction ? false : "both", - external: - isProduction && !IGNORE_EXTERNALS - ? Object.keys(pkg.peerDependencies || {}) - : [], -} - -const build = async () => { - // delete build folder - await fs.rm(outdir, { recursive: true, force: true }) - await fs.mkdir(outdir, { recursive: true }) - - // build web workers - try { - const workerFiles = await fs.readdir("src/workers") - for (let f of workerFiles) { - await esbuild.build({ - ...config, - entryPoints: [`src/workers/${f}`], - outfile: `${outdir}/workers/${f}`, - }) - } - } catch (e) { - console.log("WARNING: BUILD WEB WORKERS", e.message) - } - - // build app - let ctx = await esbuild.context({ - ...config, - entryPoints: [pkg.source], - outdir, - splitting: true, - format: "esm", - plugins: [ - { - name: "start/end", - setup(build) { - build.onStart(() => { - // console.log(clear) - console.log(yellow, "Compiling...") - }) - build.onEnd((result) => console.log(green, "Done!")) - }, - }, - // this custom plugin rewrites SVG imports to - // dataurls, paths or react components based on the - // search param and size - { - name: "svg-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(svg)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - let loader = "text" - if (args.suffix === "?url") { - // as URL - const maxSize = 10240 // 10Kb - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - } else { - // as react component - // use react component loader (jsx) - loader = "jsx" - contents = await transform(contents, { - plugins: ["@svgr/plugin-jsx"], - }) - } - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin rewrites image imports to - // dataurls or urls based on the size - { - name: "image-loader", - setup(build) { - build.onLoad( - // consider only .svg files - { filter: /.\.(png|jpg|jpeg|gif)$/, namespace: "file" }, - async (args) => { - let contents = await fs.readFile(args.path) - const maxSize = 10240 // 10Kb - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - // use dataurl loader for small files and file loader for big files! - loader = contents.length <= maxSize ? "dataurl" : "file" - - return { contents, loader } - } - ) - }, - }, - - // this custom plugin parses the style files - { - name: "parse-styles", - setup(build) { - build.onLoad( - // consider only .scss and .css files - { filter: /.\.(css|scss)$/, namespace: "file" }, - async (args) => { - let content - // handle scss, convert to css - if (args.path.endsWith(".scss")) { - const result = sass.renderSync({ file: args.path }) - content = result.css - } else { - // read file content - content = await fs.readFile(args.path) - } - - // postcss plugins - const plugins = [ - require("tailwindcss"), - require("autoprefixer"), - // rewrite urls inside css - url({ - url: "inline", - // maxSize: 10, // use dataurls if files are smaller than 10k - // fallback: "copy", // if files are bigger use copy method - // assetsPath: "./build/assets", - // useHash: true, - // optimizeSvgEncode: true, - }), - ] - - const { css } = await postcss(plugins).process(content, { - from: args.path, - to: outdir, - }) - // built-in loaders: js, jsx, ts, tsx, css, json, text, base64, dataurl, file, binary - return { contents: css, loader: "text" } - } - ) - }, - }, - ], - }) - - if (watch || serve) { - if (watch) await ctx.watch() - if (serve) { - // generate app props based on package.json and secretProps.json - await fs.writeFile( - `./${outdir}/appProps.js`, - `export default ${JSON.stringify(appProps())}` - ) - - let { host, port } = await ctx.serve({ - host: "0.0.0.0", - port: parseInt(process.env.APP_PORT || process.env.PORT || 3000), - servedir: "public", - }) - console.log("serve on", `${host}:${port}`) - } - } else { - await ctx.rebuild() - await ctx.dispose() - } -} - -build() diff --git a/apps/supernova/jest.config.js b/apps/supernova/jest.config.js deleted file mode 100644 index 0cb80394c..000000000 --- a/apps/supernova/jest.config.js +++ /dev/null @@ -1,21 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -module.exports = { - transform: { "\\.[jt]sx?$": "babel-jest" }, - testEnvironment: "jsdom", - setupFilesAfterEnv: ["/setupTests.js"], - transformIgnorePatterns: [ - "node_modules/(?!(juno-ui-components|communicator)/)", - ], - moduleNameMapper: { - // Jest currently doesn't support resources with query parameters. - // Therefore we add the optional query parameter matcher at the end - // https://github.com/facebook/jest/issues/4181 - "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)(\\?.+)?$": - require.resolve("./__mocks__/fileMock"), - "\\.(css|less|scss)$": require.resolve("./__mocks__/styleMock"), - }, -} diff --git a/apps/supernova/package.json b/apps/supernova/package.json deleted file mode 100644 index a17f9842a..000000000 --- a/apps/supernova/package.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "name": "supernova", - "version": "0.9.11", - "author": "UI-Team", - "contributors": [ - "Esther Schmitz", - "Arturo Reuschenbach Puncernau" - ], - "repository": "https://github.com/sapcc/juno/tree/main/apps/supernova", - "license": "Apache-2.0", - "source": "src/index.js", - "module": "build/index.js", - "private": true, - "devDependencies": { - "@babel/core": "^7.20.2", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", - "esbuild": "^0.17.11", - "esbuild-sass-plugin": "^2.6.0", - "immer": "^9.0.21", - "interweave": "^13.0.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "zustand": "4.3.7" - }, - "peerDependencies": { - "@tanstack/react-query": "4.28.0", - "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "zustand": "4.3.7" - }, - "importmapExtras": { - "zustand/middleware": "4.3.7" - }, - "scripts": { - "start": "NODE_ENV=development node esbuild.config.js --port=$APP_PORT --serve --watch", - "test": "jest", - "build": "NODE_ENV=production node esbuild.config.js" - }, - "appProps": { - "theme": { - "value": "theme-dark", - "type": "optional", - "description": "Override the default theme. Possible values are theme-light or theme-dark (default)" - }, - "embedded": { - "value": "false", - "type": "optional", - "description": "Set to true if app is to be embedded in another existing app or page, like e.g. Elektra. If set to true the app won't render a page header/footer and instead render only the content. The default value is false." - }, - "endpoint": { - "value": "", - "type": "required", - "description": "Alertmanager API Endpoint URL" - }, - "filterLabels": { - "value": null, - "type": "optional", - "description": "FilterLabels are the labels shown in the filter dropdown, enabling users to filter alerts based on specific criteria. The 'Status' label serves as a default filter, automatically computed from the alert status attribute and will be not overwritten. The labels must be an array of strings. Example: [\"app\", \"cluster\", \"cluster_type\"]" - }, - "silenceExcludedLabels": { - "value": null, - "type": "optional", - "description": "SilenceExcludedLabels are labels that are initially excluded by default when creating a silence. However, they can be added if necessary when utilizing the advanced options in the silence form. The labels must be an array of strings. Example: [\"pod\", \"pod_name\", \"instance\"]" - }, - "silenceTemplates": { - "value": null, - "type": "optional", - "description": "SilenceTemplates are pre-defined silence templates that can be used to scheduled Maintenance Windows. The format consists of a list of objects including description, editable_labels (array of strings specifying the labels that users can modify), fixed_labels (map containing fixed labels and their corresponding values), status, and title." - } - }, - "appDependencies": { - "auth": "latest" - }, - "appPreview": true -} diff --git a/apps/supernova/public/favicon.ico b/apps/supernova/public/favicon.ico deleted file mode 100644 index 9ebc4bb2e..000000000 Binary files a/apps/supernova/public/favicon.ico and /dev/null differ diff --git a/apps/supernova/public/index.html b/apps/supernova/public/index.html deleted file mode 100644 index 13beb9f20..000000000 --- a/apps/supernova/public/index.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - Supernova Dev - - - - - - -
- - diff --git a/apps/supernova/public/index_test.html b/apps/supernova/public/index_test.html deleted file mode 100644 index aa41d46a8..000000000 --- a/apps/supernova/public/index_test.html +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - Supernova Dev - - - - - - - -
- - diff --git a/apps/supernova/setupTests.js b/apps/supernova/setupTests.js deleted file mode 100644 index db44c9038..000000000 --- a/apps/supernova/setupTests.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import "@testing-library/jest-dom" diff --git a/apps/supernova/src/App.jsx b/apps/supernova/src/App.jsx deleted file mode 100644 index 1663c5e2d..000000000 --- a/apps/supernova/src/App.jsx +++ /dev/null @@ -1,108 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useLayoutEffect } from "react" - -import { AppShellProvider } from "juno-ui-components" -import AppContent from "./AppContent" -import styles from "./styles.scss" -import { - useGlobalsActions, - useFilterActions, - useSilencesActions, - useAlertsActions, - StoreProvider, -} from "./hooks/useAppStore" -import AsyncWorker from "./components/AsyncWorker" -import { MessagesProvider } from "messages-provider" -import CustomAppShell from "./components/CustomAppShell" - -function App(props = {}) { - const { setLabels, setPredefinedFilters, setActivePredefinedFilter } = - useFilterActions() - const { setEmbedded, setApiEndpoint } = useGlobalsActions() - const { setExcludedLabels } = useSilencesActions() - - useLayoutEffect(() => { - // filterLabels are the labels shown in the filter dropdown, enabling users to filter alerts based on specific criteria. Default is status. - if (props.filterLabels) setLabels(props.filterLabels) - - // silenceExcludedLabels are labels that are initially excluded by default when creating a silence. However, they can be added if necessary when utilizing the advanced options in the silence form. - if (props.silenceExcludedLabels) - setExcludedLabels(props.silenceExcludedLabels) - - // predefined filters config - const predefinedFilters = [ - { - name: "prod", - displayName: "Prod", - matchers: { - // regex that matches anything except regions that start with qa-de- - region: "^(?!qa-de-).*", - }, - }, - { - name: "prod-qa", - displayName: "Prod + QA", - matchers: { - // regex that matches anything except regions that start with qa-de- and end with a number that is not 1 - // regex is used in RegExp constructor, so we need to escape the backslashes for flags - region: "^(?!qa-de-(?!1$)\\d+).*", - }, - }, - { - name: "labs", - displayName: "Labs", - matchers: { - // regex that matches all regions that start with qa-de- and end with a number that is not 1 - // regex is used in RegExp constructor, so we need to escape the backslashes for flags - region: "^qa-de-(?!1$)\\d+", - }, - }, - { - name: "all", - displayName: "All", - matchers: { - region: ".*", - }, - }, - ] - setPredefinedFilters(predefinedFilters) - - // initially active predefined filter - const initialPredefinedFilter = "prod" - setActivePredefinedFilter(initialPredefinedFilter) - - // save the apiEndpoint. It is also used outside the alertManager hook - setApiEndpoint(props.endpoint) - }, []) - - useLayoutEffect(() => { - if (props.embedded === "true" || props.embedded === true) setEmbedded(true) - }, []) - - return ( - - - - - - - ) -} - -const StyledApp = (props) => { - return ( - - {/* load appstyles inside the shadow dom */} - - - - - - ) -} - -export default StyledApp diff --git a/apps/supernova/src/App.test.js b/apps/supernova/src/App.test.js deleted file mode 100644 index cb360db19..000000000 --- a/apps/supernova/src/App.test.js +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { render, act } from "@testing-library/react" - -// support shadow dom queries -// https://reactjsexample.com/an-extension-of-dom-testing-library-to-provide-hooks-into-the-shadow-dom/ -import { screen } from "shadow-dom-testing-library" - -jest.mock("./hooks/useCommunication", () => { - return jest.fn(() => ({})) -}) -jest.mock("./hooks/useAlertmanagerAPI", () => { - return jest.fn(() => ({})) -}) - -import App from "./App" - -test("renders app", async () => { - render() - - let loginTitle = await screen.queryAllByShadowText(/Supernova/i) - expect(loginTitle.length > 0).toBe(true) -}) diff --git a/apps/supernova/src/AppContent.jsx b/apps/supernova/src/AppContent.jsx deleted file mode 100644 index 0e365d145..000000000 --- a/apps/supernova/src/AppContent.jsx +++ /dev/null @@ -1,125 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useEffect } from "react" -import { useActions, Messages } from "messages-provider" -import { Container, Spinner, Stack } from "juno-ui-components" -import { - useAlertsError, - useAlertsIsLoading, - useAlertsIsUpdating, - useAlertsUpdatedAt, - useAlertsTotalCounts, - useAuthLoggedIn, - useAuthError, - useSilencesError, -} from "./hooks/useAppStore" -import AlertsList from "./components/alerts/AlertsList" -import RegionsList from "./components/regions/RegionsList" -import StatusBar from "./components/status/StatusBar" -import Filters from "./components/filters/Filters" -import WelcomeView from "./components/WelcomeView" -import { parseError } from "./helpers" -import AlertDetail from "./components/alerts/AlertDetail" -import PredefinedFilters from "./components/filters/PredefinedFilters" - -const AppContent = () => { - const { addMessage } = useActions() - const loggedIn = useAuthLoggedIn() - const authError = useAuthError() - - // alerts - const alertsError = useAlertsError() - const isAlertsLoading = useAlertsIsLoading() - const totalCounts = useAlertsTotalCounts() - const isAlertsUpdating = useAlertsIsUpdating() - const updatedAt = useAlertsUpdatedAt() - - // silences - const silencesError = useSilencesError() - - useEffect(() => { - if (!authError) return - addMessage({ - variant: "error", - text: parseError(authError), - }) - }, [authError]) - - useEffect(() => { - // since the API call is done in a web worker and not logging aware, we need to show the error just in case the user is logged in - if (!alertsError || !loggedIn) return - - // if user uses firefox warn to activate `allow_client_cert`. Should be enough to do it just here since the API call is done in a web worker and nothing else will be loaded until the alerts are loaded - const isFirefox = navigator.userAgent.toLowerCase().includes("firefox") - if (isFirefox) { - addMessage({ - variant: "warning", - text: ( -

- Firefox detected. Please ensure that you have activated{" "} - allow_client_cert to enable the retrieval of alerts and - silences from the API. -

    -
  • 1. Go to about:config (via address bar)
  • -
  • - 2. Change network.cors_preflight.allow_client_cert to{" "} - true -
  • -
  • 3. Reload Greenhouse
  • -
-

- ), - }) - } - - addMessage({ - variant: "error", - text: parseError(alertsError), - }) - }, [alertsError, loggedIn]) - - useEffect(() => { - // since the API call is done in a web worker and not logging aware, we need to show the error just in case the user is logged in - if (!silencesError || !loggedIn) return - addMessage({ - variant: "error", - text: parseError(silencesError), - }) - }, [silencesError, loggedIn]) - - return ( - - - {loggedIn && !authError ? ( - <> - - - {isAlertsLoading ? ( - - Loading - - - ) : ( - <> - - - - - - )} - - ) : ( - - )} - - ) -} - -export default AppContent diff --git a/apps/supernova/src/api/apiService.js b/apps/supernova/src/api/apiService.js deleted file mode 100644 index b81b93bc5..000000000 --- a/apps/supernova/src/api/apiService.js +++ /dev/null @@ -1,122 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * This module implements a service to retrieve information from an API - * @module apiService - */ - -// default value for watch interval -const DEFAULT_INTERVAL = 300000 - -/** - * This function implements the actual service. - * @param {object} initialConfig - */ -function ApiService(initialConfig) { - // default config - let config = { - serviceName: null, - initialFetch: true, // Set this to false to disable this service from automatically running. - fetchFn: null, // The promise function that the service will use to request data - watch: true, // if true runs the fetchFn periodically with an interval defined in watchInterval - watchInterval: DEFAULT_INTERVAL, // 5 min - onFetchStart: null, - onFetchEnd: null, - onFetchError: null, - debug: false, - } - - let initialFetchPerformed = false - - // get the allowed config keys from default config - const allowedOptions = Object.keys(config) - // variable to hold the watch timer created by setInterval - let watchTimer - - // This function performs the request to get the target data - const update = () => { - if (config.fetchFn) { - // call onFetchStart if defined - // This is useful to inform the listener that a new fetch is starting - if (config.onFetchStart) config.onFetchStart() - if (config?.debug) - console.info(`ApiService::${config.serviceName || ""}: start fetch`) - initialFetchPerformed = true - return config - .fetchFn() - .then(() => { - if (config.onFetchEnd) config.onFetchEnd() - }) - .catch((error) => { - if (error?.httperror) { - error.message = "API: " + error.message - } - if (error.message == "Failed to fetch") { - error.message = - "Could not reach endpoint. Possible causes could include network issues, incorrect URL, or server outages." - } - - console.warn(`ApiService::${config.serviceName || ""}:`, error) - if (config.onFetchError) config.onFetchError(error) - }) - } else { - if (config?.debug) - console.warn( - `ApiService::${config.serviceName || ""}: missing fetch function` - ) - return - } - } - - // update watcher if config has changed - const updateWatcher = (oldConfig) => { - // do nothing if watch and watchInterval are the same - if ( - initialFetchPerformed && - oldConfig.watch === config.watch && - oldConfig.watchInterval === config.watchInterval - ) - return - - // delete last watcher - clearInterval(watchTimer) - - // create a new watcher which calls the update method - if (config.watch) { - watchTimer = setInterval(update, config.watchInterval || DEFAULT_INTERVAL) - } - } - - // this function is public and used to update the config - this.configure = (newOptions) => { - const oldConfig = { ...config } - // update apiService config - config = { ...config, ...newOptions } - - // check for allowed keys - Object.keys(config).forEach( - (key) => allowedOptions.indexOf(key) < 0 && delete config[key] - ) - - if (config?.debug) - console.log( - `ApiService::${config.serviceName || ""}: new config: `, - config - ) - - // update watcher will check the config relevant attribute changed to update the watcher - updateWatcher(oldConfig) - if (config.initialFetch && !initialFetchPerformed) update() - } - - // make it possible to update explicitly, not by a watcher! - this.fetch = update - - // set the config initially - this.configure(initialConfig) -} - -export default ApiService diff --git a/apps/supernova/src/api/client.js b/apps/supernova/src/api/client.js deleted file mode 100644 index bfbcc8f49..000000000 --- a/apps/supernova/src/api/client.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -class HTTPError extends Error { - constructor(code, message) { - super(message || code) - this.name = "HTTPError" - this.statusCode = code - } -} - -// Check response status -const checkStatus = (response) => { - if (response.status < 400) { - return response - } else { - return response.text().then((message) => { - var error = new HTTPError(response.status, message || response.statusText) - error.statusCode = response.status - error.httperror = true - return Promise.reject(error) - }) - } -} - -const DEFAULT_HEADERS = { - "Content-Type": "application/json", - Accept: "application/json", -} - -const request = (url, options = {}) => { - const requestOptions = { headers: DEFAULT_HEADERS, ...options } - - return fetch(url, requestOptions) - .then(checkStatus) - .then((response) => response.json()) -} - -export const head = (url, options = {}) => - request(url, { method: "HEAD", ...options }) -export const get = (url, options = {}) => - request(url, { method: "GET", ...options }) -export const post = (url, options = {}) => - request(url, { method: "POST", ...options }) -export const put = (url, options = {}) => - request(url, { method: "PUT", ...options }) -export const patch = (url, options = {}) => - request(url, { method: "PATCH", ...options }) -export const del = (url, options = {}) => - request(url, { method: "DELETE", ...options }) -export const copy = (url, options = {}) => - request(url, { method: "COPY", ...options }) diff --git a/apps/supernova/src/components/AsyncWorker.jsx b/apps/supernova/src/components/AsyncWorker.jsx deleted file mode 100644 index 98cc4cc8e..000000000 --- a/apps/supernova/src/components/AsyncWorker.jsx +++ /dev/null @@ -1,18 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import useCommunication from "../hooks/useCommunication" -import useAlertmanagerAPI from "../hooks/useAlertmanagerAPI" -import useUrlState from "../hooks/useUrlState" - -const AsyncWorker = ({ endpoint }) => { - useCommunication() - useAlertmanagerAPI(endpoint) - useUrlState() - return null -} - -export default AsyncWorker diff --git a/apps/supernova/src/components/Avatar.jsx b/apps/supernova/src/components/Avatar.jsx deleted file mode 100644 index 6e41eb812..000000000 --- a/apps/supernova/src/components/Avatar.jsx +++ /dev/null @@ -1,34 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack } from "juno-ui-components" - -const avatarCss = ` -h-8 -w-8 -bg-theme-background-lvl-2 -rounded-full -bg-cover -` - -const Avatar = ({ userName, url }) => { - return ( - - {url && ( -
- )} - {userName && {userName}} - - ) -} - -export default Avatar diff --git a/apps/supernova/src/components/CustomAppShell.jsx b/apps/supernova/src/components/CustomAppShell.jsx deleted file mode 100644 index b35fceb8b..000000000 --- a/apps/supernova/src/components/CustomAppShell.jsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { AppShell, PageHeader } from "juno-ui-components" -import { - useAuthData, - useAuthLoggedIn, - useGlobalsEmbedded, - useAuthActions, -} from "../hooks/useAppStore" -import HeaderUser from "./HeaderUser" - -const CustomAppShell = ({ children }) => { - const embedded = useGlobalsEmbedded() - const authData = useAuthData() - const loggedIn = useAuthLoggedIn() - const { logout } = useAuthActions() - - const pageHeader = useMemo(() => { - return ( - - {loggedIn && } - - ) - }, [loggedIn, authData, logout]) - - return ( - - {children} - - ) -} - -export default CustomAppShell diff --git a/apps/supernova/src/components/HeaderUser.jsx b/apps/supernova/src/components/HeaderUser.jsx deleted file mode 100644 index 8cdff6776..000000000 --- a/apps/supernova/src/components/HeaderUser.jsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Stack, Button } from "juno-ui-components" -import Avatar from "./Avatar" - -const HeaderUser = ({ auth, logout }) => { - return ( - -
- -
- - - - - ))} - - - )} - - ) -} - -export default AlertSilences diff --git a/apps/supernova/src/components/alerts/AlertStatus.jsx b/apps/supernova/src/components/alerts/AlertStatus.jsx deleted file mode 100644 index dd7bb3d0e..000000000 --- a/apps/supernova/src/components/alerts/AlertStatus.jsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { Stack } from "juno-ui-components" -import { - useSilencesItemsHash, - useSilencesLocalItems, - useSilencesActions, -} from "../../hooks/useAppStore" - -const AlertStatus = ({ alert }) => { - const allSilences = useSilencesItemsHash() - const localSilences = useSilencesLocalItems() - const { getMappingSilences, getMappedState } = useSilencesActions() - - const silences = useMemo(() => { - if (!alert) return [] - return getMappingSilences(alert) - }, [alert, allSilences, localSilences]) - - const state = useMemo(() => { - if (!alert) return {} - return getMappedState(alert) - }, [alert, allSilences, localSilences]) - - return ( -
- {state && ( - <> - {state?.isProcessing ? ( - - {state.type} - processing - - ) : ( - {state.type} - )} - - )} - {alert?.status?.inhibitedBy?.length > 0 && ( -
- - Inhibited by: - {alert?.status?.inhibitedBy} - -
- )} - {silences && silences.length > 0 && ( -
- - Silenced by: - {silences.map((data) => ( - {data?.createdBy || data.id} - ))} - -
- )} -
- ) -} - -export default AlertStatus diff --git a/apps/supernova/src/components/alerts/AlertsList.jsx b/apps/supernova/src/components/alerts/AlertsList.jsx deleted file mode 100644 index 0fc750155..000000000 --- a/apps/supernova/src/components/alerts/AlertsList.jsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo, useState, useRef, useCallback } from "react" -import { - DataGrid, - DataGridHeadCell, - DataGridRow, - DataGridCell, - Icon, - Stack, -} from "juno-ui-components" -import Alert from "./Alert" -import { - useAlertsItemsFiltered, - useAlertsIsLoading, -} from "../../hooks/useAppStore" - -const AlertsList = () => { - const [visibleAmount, setVisibleAmount] = useState(20) - const [isAddingItems, setIsAddingItems] = useState(false) - const timeoutRef = React.useRef(null) - - const itemsFiltered = useAlertsItemsFiltered() - const alertsIsLoading = useAlertsIsLoading() - - const alertsSorted = useMemo(() => { - if (itemsFiltered) { - return itemsFiltered.slice(0, visibleAmount) - } - }, [itemsFiltered, visibleAmount]) - - React.useEffect(() => { - return () => clearTimeout(timeoutRef.current) // clear when component is unmounted - }, []) - - // endless scroll observer - const observer = useRef() - const lastListElementRef = useCallback( - (node) => { - // no fetch if loading original data - if (alertsIsLoading || isAddingItems) return - if (observer.current) observer.current.disconnect() - observer.current = new IntersectionObserver((entries) => { - console.log("IntersectionObserver: callback") - if (entries[0].isIntersecting && visibleAmount <= alertsSorted.length) { - // setVisibleAmount((prev) => prev + 10) - clearTimeout(timeoutRef.current) - setIsAddingItems(true) - timeoutRef.current = setTimeout(() => { - setIsAddingItems(false) - setVisibleAmount((prev) => prev + 10) - }, 500) - } - }) - if (node) observer.current.observe(node) - }, - [alertsIsLoading, isAddingItems] - ) - - return ( - - {!alertsIsLoading && ( - - - - - Region - Service - Description - Firing Since - Status - - - )} - {alertsSorted?.length > 0 ? ( - alertsSorted?.map((alert, index) => { - if (alertsSorted.length === index + 1) { - // DataRow in alerts muss implement forwardRef - return ( - - ) - } - return - }) - ) : ( - - - - -
- We couldn't find anything. It's possible that the matching - alerts are not active at the moment, or the chosen filters could - be overly limiting. -
-
-
-
- )} - {isAddingItems && ( - - Loading ... - - )} -
- ) -} - -export default AlertsList diff --git a/apps/supernova/src/components/alerts/shared/AlertDescription.jsx b/apps/supernova/src/components/alerts/shared/AlertDescription.jsx deleted file mode 100644 index e9ffbd49f..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertDescription.jsx +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { Markup } from "interweave" - -import { descriptionParsed } from "../../../lib/utils" - - -const AlertDescription = ({description, subdued}) => { - - return ( - $1" - ) - )} - tagName="div" - className={subdued ? "text-theme-light" : ""} - /> - ) -} - -export default AlertDescription \ No newline at end of file diff --git a/apps/supernova/src/components/alerts/shared/AlertIcon.jsx b/apps/supernova/src/components/alerts/shared/AlertIcon.jsx deleted file mode 100644 index e85826895..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertIcon.jsx +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { forwardRef } from "react" - -import { Icon } from "juno-ui-components" - - -const AlertIcon = ({severity}, ref) => { - - const iconColor = () => { - - switch (severity) { - case "critical": - return "text-theme-danger" - case "warning": - return "text-theme-warning" - case "info": - return "text-theme-info" - } - - } - - return ( - <> - {severity === "critical" ? ( - - ) : severity?.match(/^(warning|info)$/) ? ( - - ) : ( - - )} - - ) -} - -export default forwardRef(AlertIcon) \ No newline at end of file diff --git a/apps/supernova/src/components/alerts/shared/AlertLabels.jsx b/apps/supernova/src/components/alerts/shared/AlertLabels.jsx deleted file mode 100644 index cf172256d..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertLabels.jsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" - -import { Pill, Stack } from "juno-ui-components" - -import { - useActiveFilters, - useFilterLabels, - useFilterActions, -} from "../../../hooks/useAppStore" - -/** - * For each of the given alert's labels which is included in the configured filterLabels render a Pill showing filterLabel and filterValue - */ -const AlertLabels = ({ alert, showAll}) => { - const filterLabels = showAll ? Object.keys(alert?.labels) : useFilterLabels() - const activeFilters = useActiveFilters() - const { addActiveFilter, removeActiveFilter } = useFilterActions() - - const handleLabelClick = (e, filterLabel, filterValue) => { - // if filter isn't already active, add it - if (!activeFilters?.[filterLabel]?.includes(filterValue)) { - e.stopPropagation() - addActiveFilter(filterLabel, filterValue) - } else { - // otherwise remove it - handleRemoveFilter(e, filterLabel, filterValue) - } - } - - const handleRemoveFilter = (e, filterLabel, filterValue) => { - e.stopPropagation() - removeActiveFilter(filterLabel, filterValue) - } - - return ( - - {filterLabels.map((filterLabel) => { - let value = alert?.labels?.[filterLabel] - let isActive = activeFilters?.[filterLabel]?.includes(value) - - return ( - value && ( - handleLabelClick(e, filterLabel, value)} - closeable={isActive} - onClose={(e, _) => handleRemoveFilter(e, filterLabel, value)} - /> - ) - ) - })} - - ) -} - -export default AlertLabels diff --git a/apps/supernova/src/components/alerts/shared/AlertLinks.jsx b/apps/supernova/src/components/alerts/shared/AlertLinks.jsx deleted file mode 100644 index 0ba9a37e7..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertLinks.jsx +++ /dev/null @@ -1,118 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" - -import { Stack } from "juno-ui-components" - -const AlertLinks = ({ alert, className }) => { - - return ( - - {alert?.generatorURL && ( - e.stopPropagation()} - > - Prometheus - - )} - {alert?.labels?.playbook && ( - e.stopPropagation()} - > - Playbook - - )} - {alert?.labels?.kibana && ( - e.stopPropagation()} - > - Logs - - )} - {alert?.labels?.dashboard && ( - e.stopPropagation()} - > - Grafana - - )} - {alert?.labels?.spc && ( - e.stopPropagation()} - > - SPC Ticket - - )} - {alert?.labels?.sentry && ( - e.stopPropagation()} - > - Sentry - - )} - {alert?.labels?.cloudops && ( - e.stopPropagation()} - > - CloudOps - - )} - {alert?.labels?.report && ( - e.stopPropagation()} - > - Report - - )} - {alert?.annotations?.mail_subject && ( - e.stopPropagation()} - > - Email Owner - - )} - - ) -} - -export default AlertLinks diff --git a/apps/supernova/src/components/alerts/shared/AlertRegion.jsx b/apps/supernova/src/components/alerts/shared/AlertRegion.jsx deleted file mode 100644 index 04e4e2d14..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertRegion.jsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" - - -const AlertRegion = ({ region, cluster }) => { - - return ( - <> - {region} - {region !== cluster && ( - <> -
- {cluster} - - )} - - ) -} - -export default AlertRegion \ No newline at end of file diff --git a/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx b/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx deleted file mode 100644 index 1f3601cf8..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertSilencesList.jsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useMemo } from "react" -import { DateTime } from "luxon" - -import { - Badge, - DataGrid, - DataGridCell, - DataGridHeadCell, - DataGridRow, -} from "juno-ui-components" - -import { useSilencesActions } from "../../../hooks/useAppStore" - -const badgeVariant = (state) => { - switch (state) { - case "active": - return "info" - case "processing": - return "warning" - default: - return "default" - } -} - -const AlertSilencesList = ({ alert }) => { - const dateFormat = { ...DateTime.DATETIME_SHORT } - const timeFormat = { ...DateTime.TIME_24_WITH_SHORT_OFFSET } - - const formatDateTime = (timestamp) => { - const time = DateTime.fromISO(timestamp) - return time.toLocaleString(dateFormat) - } - - const { getMappingSilences, getExpiredSilences } = useSilencesActions() - - const activeSilences = getMappingSilences(alert) - const expiredSilences = getExpiredSilences(alert) - console.log("expiredSilences", expiredSilences) - const silenceList = activeSilences.concat(expiredSilences) - - return ( - <> - {silenceList.length > 0 && ( - <> -

Silences

- - - Status - Silence start - Silence end - Created by - Comment - - {silenceList.map((silence) => ( - - -
- - {silence.status?.state} - -
-
- {formatDateTime(silence.startsAt)} - {formatDateTime(silence.endsAt)} - {silence.createdBy} - - {silence.comment} - -
- ))} -
- - )} - - ) -} - -export default AlertSilencesList diff --git a/apps/supernova/src/components/alerts/shared/AlertTimestamp.jsx b/apps/supernova/src/components/alerts/shared/AlertTimestamp.jsx deleted file mode 100644 index dbb80c4f4..000000000 --- a/apps/supernova/src/components/alerts/shared/AlertTimestamp.jsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { DateTime } from "luxon" - -import { Icon, Stack, Tooltip, TooltipContent, TooltipTrigger } from "juno-ui-components" - - -const AlertTimestamp = ({startTimestamp}) => { - const dateFormat = { ...DateTime.DATE_MED } - const timeFormat = { ...DateTime.TIME_24_WITH_SHORT_OFFSET } - const startTime = DateTime.fromISO(startTimestamp) - const daysFiring = DateTime.now().diff(startTime, "days") - - return ( - -
{startTime.toLocaleString(dateFormat)}
-
{startTime.toLocaleString(timeFormat)}
- {daysFiring.days > 7 && ( - - - - - - {`Alert has been firing for ${Math.round( - daysFiring.days - )} days`} - - - )} -
- ) - -} -export default AlertTimestamp \ No newline at end of file diff --git a/apps/supernova/src/components/filters/FilterPills.jsx b/apps/supernova/src/components/filters/FilterPills.jsx deleted file mode 100644 index 160fdbdd2..000000000 --- a/apps/supernova/src/components/filters/FilterPills.jsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" - -import { Pill, Stack } from "juno-ui-components" -import { useActiveFilters, useFilterActions } from "../../hooks/useAppStore" - -const FilterPills = () => { - const activeFilters = useActiveFilters() - const { removeActiveFilter } = useFilterActions() - - return ( - - {Object.entries(activeFilters).map(([key, values]) => { - return values.map((value) => ( - removeActiveFilter(key, value)} - key={`${key}:${value}`} - /> - )) - })} - - ) -} - -export default FilterPills diff --git a/apps/supernova/src/components/filters/FilterSelect.jsx b/apps/supernova/src/components/filters/FilterSelect.jsx deleted file mode 100644 index 4feb52a90..000000000 --- a/apps/supernova/src/components/filters/FilterSelect.jsx +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState } from "react" - -import { - Button, - InputGroup, - SelectOption, - Select, - Stack, - SearchInput, -} from "juno-ui-components" -import { - useFilterLabels, - useFilterLabelValues, - useFilterActions, - useActiveFilters, - useSearchTerm, -} from "../../hooks/useAppStore" -import { humanizeString } from "../../lib/utils" - -const FilterSelect = () => { - const [filterLabel, setFilterLabel] = useState("") - const [filterValue, setFilterValue] = useState("") - const [resetKey, setResetKey] = useState(Date.now()) - - const { - addActiveFilter, - loadFilterLabelValues, - clearActiveFilters, - setSearchTerm, - } = useFilterActions() - const filterLabels = useFilterLabels() - const filterLabelValues = useFilterLabelValues() - const activeFilters = useActiveFilters() - const searchTerm = useSearchTerm() - - const handleFilterAdd = (value) => { - if (filterLabel && (filterValue || value)) { - // add active filter to store - addActiveFilter(filterLabel, filterValue || value) - - // reset filterValue - setFilterValue("") - } else { - // TODO: show error -> please select filter/value - } - } - - const handleFilterLabelChange = (value) => { - setFilterLabel(value) - // lazy loading of all possible values for this label (only load them if we haven't already) - if (!filterLabelValues[value]?.values) { - loadFilterLabelValues(value) - } - } - - const handleFilterValueChange = (value) => { - setFilterValue(value) - handleFilterAdd(value) - } - - const handleSearchChange = (value) => { - // debounce setSearchTerm to avoid unnecessary re-renders - const debouncedSearchTerm = setTimeout(() => { - setSearchTerm(value.target.value) - }, 500) - - // clear timeout if we have a new value - return () => clearTimeout(debouncedSearchTerm) - } - - // const handleKeyDown = (event) => { - // if (event.key === "Enter") { - // handleFilterValueChange() - // } - // } - - return ( - - - - - - {displayNewSilence && ( - setDisplayNewSilence(false)} - onConfirm={success ? null : onSubmitForm} - > - {error && } - - {success && ( - - A silence object with id {success?.silenceID} was created - successfully. Please note that it may take up to 5 minutes for the - alert to show up as silenced. - - )} - - {expirationDate && !success && ( - - There is already a silence for this alert that expires at{" "} - - {DateTime.fromISO(expirationDate).toLocaleString( - DateTime.DATETIME_SHORT - )} - - - )} - - {alert?.labels?.alertname} - - - - - - {!success && ( - <> - - -
- - - - - - - - - {formState?.editable_labels && - Object.keys(formState?.editable_labels).length > 0 && ( - - -

- Editable Labels are labels that are editable. You can - use regular expressions. -

-
- - -
- {Object.keys(formState.editable_labels).map( - (editable_label, index) => ( - - ) - )} -
-
-
- )} - - {Object.keys(formState?.fixed_labels).length > 0 && ( - - -

Fixed Labels are labels that are not editable.

-
- - - - {Object.keys(formState.fixed_labels).map( - (label, index) => ( - - ) - )} - - -
- )} -
- )} - - )} -
- ) -} - -export default SilenceScheduled diff --git a/apps/supernova/src/components/silences/SilenceScheduledWrapper.jsx b/apps/supernova/src/components/silences/SilenceScheduledWrapper.jsx deleted file mode 100644 index eacd31880..000000000 --- a/apps/supernova/src/components/silences/SilenceScheduledWrapper.jsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { useState, useEffect } from "react" -import SilenceScheduled from "./SilenceScheduled" - -import { MessagesProvider } from "messages-provider" -import { Button, Icon } from "juno-ui-components" -import { useSilenceTemplates } from "../../hooks/useAppStore.js" - -const SilenceScheduledWrapper = () => { - const templates = useSilenceTemplates() - const [displayNewScheduledSilence, setDisplayNewScheduledSilence] = - useState(false) - - // function which sets displayNewScheduledSilence to false - const callbackOnClose = () => { - setDisplayNewScheduledSilence(false) - } - - return ( - <> - {templates && templates?.length > 0 && ( - - - {displayNewScheduledSilence && ( - - )} - - )} - - ) -} - -export default SilenceScheduledWrapper diff --git a/apps/supernova/src/components/silences/silenceHelpers.js b/apps/supernova/src/components/silences/silenceHelpers.js deleted file mode 100644 index 525ca9fe1..000000000 --- a/apps/supernova/src/components/silences/silenceHelpers.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const DEFAULT_DURATION_OPTIONS = [ - { label: "2 hours", value: "2" }, - { label: "12 hours", value: "12" }, - { label: "1 day", value: "24" }, - { label: "3 days", value: "72" }, - { label: "7 days", value: "168" }, -] - -// get the "latest" expiration date from the given silences -export const latestExpirationDate = (silences) => { - if (silences?.length > 0) { - const sortedSilences = silences.sort((a, b) => { - return new Date(b.endsAt) - new Date(a.endsAt) - }) - return sortedSilences[0].endsAt - } -} - -// returns options for duration select dropdown. Options with which exceeds the expiration date are marked as "covered" -// return also the first option which is not covered by the expiration date -export const getSelectOptions = (expirationDate) => { - if (!expirationDate) return { items: DEFAULT_DURATION_OPTIONS } - const now = new Date() - const expiration = new Date(expirationDate) - const diff = expiration - now - const diffInHours = diff / (1000 * 60 * 60) - const options = DEFAULT_DURATION_OPTIONS.map((o) => { - if (o.value <= diffInHours) { - return { - ...o, - label: o.label + " (covered with the existing silence)", - covered: true, - } - } - return o - }) - // find the first option which is not covered by the expiration date - const firstNotCovered = options.find((o) => !o?.covered) - - return { items: options, firstNotCovered } -} - -// Setup the matchers for the silence removing the excluded labels -// These excluded labels are those that not included by default when generating a silence configuration. -// The enrichedLabels are those that are added by the worker just for UI purposes when the alert is received. -export const setupMatchers = ( - alertLabels, - excludedLabels = [], - enrichedLabels = [] -) => { - if (!alertLabels || !excludedLabels) return - let items = [] - - Object.keys(alertLabels).forEach((label) => { - const value = alertLabels?.[label] - if (value) { - const matcher = { - name: label, - value: value, - isRegex: false, // for now hardcode isRegex to false since we take the exact value - excluded: false, - configurable: false, - } - if (enrichedLabels.includes(label)) { - // do not add enriched labels, skip - } else if (excludedLabels.includes(label)) { - // mark excluded label - items.push({ ...matcher, excluded: true, configurable: true }) - } else { - items.push(matcher) - } - } - }) - return items -} diff --git a/apps/supernova/src/components/silences/silenceHelpers.test.js b/apps/supernova/src/components/silences/silenceHelpers.test.js deleted file mode 100644 index 98ce288c7..000000000 --- a/apps/supernova/src/components/silences/silenceHelpers.test.js +++ /dev/null @@ -1,51 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { setupMatchers } from "./silenceHelpers" - -describe("helpers", () => { - describe("setupMatchers", () => { - test("mark excluded labels and ignore enriched labels", () => { - const alertLables = { - region: "na-us-1", - service: "compute", - severity: "critical", - pod: "node014-bb164.cc.na-us-1.cloud.sap", - status: "active", - } - const matchers = setupMatchers(alertLables, ["pod"], ["status"]) - expect(matchers).toEqual([ - { - name: "region", - value: "na-us-1", - isRegex: false, - excluded: false, - configurable: false, - }, - { - name: "service", - value: "compute", - isRegex: false, - excluded: false, - configurable: false, - }, - { - name: "severity", - value: "critical", - isRegex: false, - excluded: false, - configurable: false, - }, - { - name: "pod", - value: "node014-bb164.cc.na-us-1.cloud.sap", - isRegex: false, - excluded: true, - configurable: true, - }, - ]) - }) - }) -}) diff --git a/apps/supernova/src/components/silences/silenceScheduledHelpers.js b/apps/supernova/src/components/silences/silenceScheduledHelpers.js deleted file mode 100644 index a8cd12a63..000000000 --- a/apps/supernova/src/components/silences/silenceScheduledHelpers.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const DEFAULT_FORM_VALUES = { - fixed_labels: {}, - editable_labels: {}, - comment: { - value: "", - error: null, - }, - createdBy: "", - date: { - start: null, - end: null, - error: null, - }, -} - -////// Form Validation - -export const validateForm = (state) => { - let formState = state - let errorexist = false - - // validate if comment is at least 3 characters long - if (formState.comment.value.length < 3) { - errorexist = true - formState = { - ...formState, - comment: { - ...formState.comment, // Only create the comment object if it exists - error: "Please enter at least 3 characters", - }, - } - } - - // checks if start date is before end date - if (new Date(formState.date.start) >= new Date(formState.date.end)) { - errorexist = true - formState = { - ...formState, - date: { - ...formState.date, // Only create the comment object if it exists - error: "The start date need to be before the end date", - }, - } - } - - // All editable labels are valid regular expressions - Object.keys(formState.editable_labels).map((editable_label) => { - if (!validateLabelValue(formState.editable_labels[editable_label].value)) { - errorexist = true - formState = { - ...formState, - editable_labels: { - ...formState.editable_labels, - [editable_label]: { - ...formState.editable_labels[editable_label], - error: `Value for ${editable_label} is not a valid regular expression`, - }, - }, - } - } - - if (!formState.editable_labels[editable_label]?.value) { - errorexist = true - formState = { - ...formState, - editable_labels: { - ...formState.editable_labels, - [editable_label]: { - ...formState.editable_labels[editable_label], - error: `Value for ${editable_label} is empty`, - }, - }, - } - } - }) - - if (!errorexist) { - return null - } - - return formState -} - -const validateLabelValue = (value) => { - try { - return !!new RegExp(value) - } catch (e) { - return false - } -} diff --git a/apps/supernova/src/components/status/StatusBar.jsx b/apps/supernova/src/components/status/StatusBar.jsx deleted file mode 100644 index 425273aa5..000000000 --- a/apps/supernova/src/components/status/StatusBar.jsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from "react" -import { DateTime } from "luxon" - -import { Spinner, Stack } from "juno-ui-components" - -const statusBarStyles = ` - bg-theme-background-lvl-1 - py-1.5 - px-4 - my-px - text-theme-light -` - -const StatusBar = ({totalCounts, isUpdating, updatedAt}) => { - - return ( - -
- {`${totalCounts.total} alerts`} - {`(${totalCounts.critical || 0} critical, ${totalCounts.warning || 0} warning, ${totalCounts.info || 0} info)`} -
- - {isUpdating && - - } - {updatedAt && - `updated ${DateTime.fromMillis(updatedAt).toLocaleString({...DateTime.TIME_24_WITH_SHORT_OFFSET})}` - } - -
- ) -} - -export default StatusBar \ No newline at end of file diff --git a/apps/supernova/src/helpers.js b/apps/supernova/src/helpers.js deleted file mode 100644 index 9d8953d9b..000000000 --- a/apps/supernova/src/helpers.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const parseError = (error) => { - let errMsg = error - - // check if error is JSON containing message or just string - if (typeof error === "string") { - errMsg = parseMessage(error) - } - - // check if the error is a object containing message - if (typeof error === "object") { - console.log("Error parsing error message::object") - if (error?.message) { - errMsg = parseMessage(error?.message) - } - } - return errMsg -} - -const parseMessage = (message) => { - let newMsg = message - try { - newMsg = JSON.parse(message) - if (newMsg?.message) { - newMsg = (newMsg?.code ? `${newMsg.code}, ` : "") + newMsg?.message - } - } catch (error) {} - - if (newMsg === "Failed to fetch") { - newMsg = - "Sorry, there was an issue fetching the data. Possible causes could include network issues, incorrect URL, or server outages. " - } - - return newMsg -} diff --git a/apps/supernova/src/hooks/useAlertmanagerAPI.js b/apps/supernova/src/hooks/useAlertmanagerAPI.js deleted file mode 100644 index e4b478cc6..000000000 --- a/apps/supernova/src/hooks/useAlertmanagerAPI.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect } from "react" -import { - useAlertsActions, - useUserIsActive, - useSilencesActions, - useSilencesLocalItems, -} from "./useAppStore" - -const createWorker = (path) => { - return fetch(new URL(path, import.meta.url)) - .then((r) => r.blob()) - .then((blob) => { - var blobUrl = window.URL.createObjectURL(blob) - let worker - - const createWorker = () => { - if (!worker) worker = new Worker(blobUrl, { type: "module" }) - return worker - } - - const stopWorker = () => { - if (!worker) return - worker.terminate() - worker = null - } - - return { createWorker, stopWorker } - }) -} - -// create workers -const alertsWorker = createWorker("workers/alerts.js") -const silencesWorker = createWorker("workers/silences.js") - -const useAlertmanagerAPI = (apiEndpoint) => { - const { - setAlertsData, - setIsLoading: setAlertsIsLoading, - setIsUpdating: setAlertsIsUpdating, - setError: setAlertsError, - } = useAlertsActions() - const isUserActive = useUserIsActive() - const { - setSilences, - setIsUpdating: setSilencesIsUpdating, - setIsLoading: setSilencesIsLoading, - setError: setSilencesError, - } = useSilencesActions() - - //Setup web workers - useEffect(() => { - let cleanupAlertsWorker - let cleanupSilencesWorker - - alertsWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - console.log("Worker::Setting up ALERTS worker", worker) - - // receive messages from worker - worker.onmessage = (e) => { - const action = e.data.action - switch (action) { - case "ALERTS_UPDATE": - console.log("Worker::ALERT_UPDATE::", e.data) - setAlertsData({ items: e.data.alerts, counts: e.data.counts }) - break - case "ALERTS_FETCH_START": - console.log("Worker::ALERTS_FETCH_START::") - setAlertsIsUpdating(true) - break - case "ALERTS_FETCH_END": - console.log("Worker::ALERTS_FETCH_END::") - setAlertsIsUpdating(false) - break - case "ALERTS_FETCH_ERROR": - console.log("Worker::ALERTS_FETCH_ERROR::", e.data.error) - setAlertsIsUpdating(false) - // error comes as object string and have to be parsed - setAlertsError(e.data.error) - break - } - } - - cleanupAlertsWorker = () => { - console.log("Worker::Terminating Alerts Worker") - return stopWorker() - } - }) - - silencesWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - console.log("Worker::Setting up SILENCES worker") - - // receive messages from worker - worker.onmessage = (e) => { - const action = e.data.action - switch (action) { - case "SILENCES_UPDATE": - console.log("Worker::SILENCES_UPDATE::", e.data) - setSilences({ - items: e.data?.silences, - itemsHash: e.data?.silencesHash, - itemsByState: e.data?.silencesBySate, - }) - break - case "SILENCES_FETCH_START": - console.log("Worker::SILENCES_FETCH_START::") - setSilencesIsUpdating(true) - break - case "SILENCES_FETCH_END": - console.log("Worker::SILENCES_FETCH_END::") - setSilencesIsUpdating(false) - break - case "SILENCES_FETCH_ERROR": - console.log("Worker::SILENCES_FETCH_ERROR::", e.data.error) - setSilencesIsUpdating(false) - // error comes as object string and have to be parsed - setSilencesError(e.data.error) - break - } - } - - cleanupSilencesWorker = () => { - console.log("Worker::Terminating Silences Worker") - return stopWorker() - } - }) - - return () => { - cleanupAlertsWorker && cleanupAlertsWorker() - cleanupSilencesWorker && cleanupSilencesWorker() - } - }, []) - - // Reconfigure the workers each time we get a new endpoint - useEffect(() => { - if (!apiEndpoint) return - - // set alerts state to loading - setAlertsIsLoading(true) - alertsWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - // initial config - worker.postMessage({ - action: "ALERTS_CONFIGURE", - fetchVars: { - apiEndpoint, - options: {}, - }, - debug: true, - }) - }) - - setSilencesIsLoading(true) - silencesWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - // initial config - worker.postMessage({ - action: "SILENCES_CONFIGURE", - apiEndpoint: apiEndpoint, - }) - }) - }, [apiEndpoint]) - - // enable/disable watching in the workers - useEffect(() => { - if (isUserActive === undefined) return - alertsWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - worker.postMessage({ - action: "ALERTS_CONFIGURE", - watch: isUserActive, - }) - }) - silencesWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - worker.postMessage({ - action: "SILENCES_CONFIGURE", - watch: isUserActive, - }) - }) - }, [isUserActive]) - - // as soon as we have locally some silences we refetch the them - useEffect(() => { - if (!useSilencesLocalItems || useSilencesLocalItems?.length <= 0) return - silencesWorker.then(({ createWorker, stopWorker }) => { - const worker = createWorker() - worker.postMessage({ - action: "SILENCES_FETCH", - }) - }) - }, [useSilencesLocalItems]) -} - -export default useAlertmanagerAPI diff --git a/apps/supernova/src/hooks/useAppStore.js b/apps/supernova/src/hooks/useAppStore.js deleted file mode 100644 index a84448c9e..000000000 --- a/apps/supernova/src/hooks/useAppStore.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React, { createContext, useContext } from "react" -import { createStore, useStore } from "zustand" -import { devtools } from "zustand/middleware" - -import createSilencesSlice from "../lib/createSilencesSlice" -import createAlertsSlice from "../lib/createAlertsSlice" -import createFiltersSlice from "../lib/createFiltersSlice" -import createAuthDataSlice from "../lib/createAuthDataSlice" -import createGlobalsSlice from "../lib/createGlobalsSlice" -import createUserActivitySlice from "../lib/createUserActivitySlice" - -const createAppStore = devtools((set, get) => ({ - ...createGlobalsSlice(set, get), - ...createAuthDataSlice(set, get), - ...createUserActivitySlice(set, get), - ...createAlertsSlice(set, get), - ...createFiltersSlice(set, get), - ...createSilencesSlice(set, get), -})) - -const StoreContext = createContext() - -export const StoreProvider = ({ options, children }) => { - return ( - ({ - ...createGlobalsSlice(set, get), - ...createAuthDataSlice(set, get), - ...createUserActivitySlice(set, get), - ...createAlertsSlice(set, get), - ...createFiltersSlice(set, get), - ...createSilencesSlice(set, get, options), - })) - )} - > - {children} - - ) -} - -const useAppStore = (selector) => useStore(useContext(StoreContext), selector) - -// atomic exports only instead of exporting whole store -// See reasoning here: https://tkdodo.eu/blog/working-with-zustand - -// Globals exports -export const useGlobalsEmbedded = () => - useAppStore((state) => state.globals.embedded) -export const useShowDetailsFor = () => - useAppStore((state) => state.globals.showDetailsFor) -export const useGlobalsApiEndpoint = () => - useAppStore((state) => state.globals.apiEndpoint) -export const useGlobalsActions = () => - useAppStore((state) => state.globals.actions) - -// AUTH -export const useAuthData = () => useAppStore((state) => state.auth.data) -export const useAuthIsProcessing = () => - useAppStore((state) => state.auth.isProcessing) -export const useAuthLoggedIn = () => useAppStore((state) => state.auth.loggedIn) -export const useAuthError = () => useAppStore((state) => state.auth.error) -export const useAuthLastAction = () => - useAppStore((state) => state.auth.lastAction) -export const useAuthAppLoaded = () => - useAppStore((state) => state.auth.appLoaded) -export const useAuthAppIsLoading = () => - useAppStore((state) => state.auth.appIsLoading) -export const useAuthActions = () => useAppStore((state) => state.auth.actions) - -// UserActivity exports -export const useUserIsActive = () => - useAppStore((state) => state.userActivity.isActive) - -export const useUserActivityActions = () => - useAppStore((state) => state.userActivity.actions) - -// Alert exports -export const useAlertsItems = () => useAppStore((state) => state.alerts.items) -export const useAlertsItemsFiltered = () => - useAppStore((state) => state.alerts.itemsFiltered) -export const useAlertsTotalCounts = () => - useAppStore((state) => state.alerts.totalCounts) -export const useAlertsSeverityCountsPerRegion = () => - useAppStore((state) => state.alerts.severityCountsPerRegion) -export const useAlertsRegions = () => - useAppStore((state) => state.alerts.regions) -export const useAlertsRegionsFiltered = () => - useAppStore((state) => state.alerts.regionsFiltered) -export const useAlertsIsLoading = () => - useAppStore((state) => state.alerts.isLoading) -export const useAlertsIsUpdating = () => - useAppStore((state) => state.alerts.isUpdating) -export const useAlertsUpdatedAt = () => - useAppStore((state) => state.alerts.updatedAt) -export const useAlertsError = () => useAppStore((state) => state.alerts.error) -export const useAlertEnrichedLabels = () => - useAppStore((state) => state.alerts.enrichedLabels) - -export const useAlertsActions = () => - useAppStore((state) => state.alerts.actions) - -// Filter exports -export const useFilterLabels = () => - useAppStore((state) => state.filters.labels) -export const useActiveFilters = () => - useAppStore((state) => state.filters.activeFilters) -export const useSearchTerm = () => - useAppStore((state) => state.filters.searchTerm) -export const useFilterLabelValues = () => - useAppStore((state) => state.filters.filterLabelValues) -export const usePredefinedFilters = () => - useAppStore((state) => state.filters.predefinedFilters) -export const useActivePredefinedFilter = () => - useAppStore((state) => state.filters.activePredefinedFilter) - -export const useFilterActions = () => - useAppStore((state) => state.filters.actions) - -// Silences exports -export const useSilencesItems = () => - useAppStore((state) => state.silences.items) -export const useSilencesItemsHash = () => - useAppStore((state) => state.silences.itemsHash) -export const useSilencesExcludedLabels = () => - useAppStore((state) => state.silences.excludedLabels) -export const useSilencesIsLoading = () => - useAppStore((state) => state.silences.isLoading) -export const useSilencesIsUpdating = () => - useAppStore((state) => state.silences.isUpdating) -export const useSilencesUpdatedAt = () => - useAppStore((state) => state.silences.updatedAt) -export const useSilencesError = () => - useAppStore((state) => state.silences.error) -export const useSilencesLocalItems = () => - useAppStore((state) => state.silences.localItems) - -export const useSilenceTemplates = () => - useAppStore((state) => state.silences.templates) - -export const useSilencesActions = () => - useAppStore((state) => state.silences.actions) diff --git a/apps/supernova/src/hooks/useCommunication.js b/apps/supernova/src/hooks/useCommunication.js deleted file mode 100644 index be3964e20..000000000 --- a/apps/supernova/src/hooks/useCommunication.js +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useEffect } from "react" -import { broadcast, get, watch } from "communicator" -import { - useUserActivityActions, - useAuthAppLoaded, - useAuthIsProcessing, - useAuthError, - useAuthLoggedIn, - useAuthLastAction, - useAuthActions, -} from "./useAppStore" -import { AUTH_ACTIONS } from "../lib/createAuthDataSlice" - -const useCommunication = () => { - console.log("[supernova] useCommunication setup") - const { setIsActive } = useUserActivityActions() - const authAppLoaded = useAuthAppLoaded() - const authIsProcessing = useAuthIsProcessing() - const authError = useAuthError() - const authLoggedIn = useAuthLoggedIn() - const authLastAction = useAuthLastAction() - const { setData: authSetData, setAppLoaded: authSetAppLoaded } = - useAuthActions() - - useEffect(() => { - // watch for user activity updates messages - // with the watcher we get the user activity object when this app is loaded before the Auth app - const unwatch = watch( - "USER_ACTIVITY_UPDATE_DATA", - (data) => { - console.log("got message USER_ACTIVITY_UPDATE_DATA: ", data) - setIsActive(data?.isActive) - }, - { debug: true } - ) - return unwatch - }, [setIsActive]) - - // allow supernova to login/logout the user. Visible when app is not in embedded mode - useEffect(() => { - if (!authAppLoaded || authIsProcessing || authError) return - if (authLastAction?.name === AUTH_ACTIONS.SIGN_ON && !authLoggedIn) { - broadcast("AUTH_LOGIN", "supernova", { debug: false }) - } else if (authLastAction?.name === AUTH_ACTIONS.SIGN_OUT && authLoggedIn) { - broadcast("AUTH_LOGOUT", "supernova") - } - }, [authAppLoaded, authIsProcessing, authError, authLoggedIn, authLastAction]) - - useEffect(() => { - if (!authSetData || !authSetAppLoaded) return - - get("AUTH_APP_LOADED", authSetAppLoaded) - const unwatchLoaded = watch("AUTH_APP_LOADED", authSetAppLoaded) - - get("AUTH_GET_DATA", authSetData) - const unwatchUpdate = watch("AUTH_UPDATE_DATA", authSetData) - - return () => { - if (unwatchLoaded) unwatchLoaded() - if (unwatchUpdate) unwatchUpdate() - } - }, [authSetData, authSetAppLoaded]) -} - -export default useCommunication diff --git a/apps/supernova/src/hooks/useUrlState.js b/apps/supernova/src/hooks/useUrlState.js deleted file mode 100644 index 7e20b6d1d..000000000 --- a/apps/supernova/src/hooks/useUrlState.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { useLayoutEffect, useEffect, useState } from "react" -import { registerConsumer } from "url-state-provider" -import { - useAuthLoggedIn, - useAuthData, - useFilterLabels, - useFilterActions, - useActiveFilters, - useActivePredefinedFilter, - useSearchTerm, - useShowDetailsFor, - useGlobalsActions, -} from "./useAppStore" - -const urlStateManager = registerConsumer("supernova") -const ACTIVE_FILTERS = "f" -const ACTIVE_PREDEFINED_FILTER = "p" -const DETAILS_FOR = "d" -const SEARCH_TERM = "s" - -const useUrlState = () => { - const [isURLRead, setIsURLRead] = useState(false) - const loggedIn = useAuthLoggedIn() - const authData = useAuthData() - const { setActiveFilters, setActivePredefinedFilter, setSearchTerm } = - useFilterActions() - const filterLabels = useFilterLabels() - const activeFilters = useActiveFilters() - const searchTerm = useSearchTerm() - const activePredefinedFilter = useActivePredefinedFilter() - const { setShowDetailsFor } = useGlobalsActions() - const detailsFor = useShowDetailsFor() - - // Set initial state from URL (on login) - // useLayoutEffect so this is done before rendering anything - useLayoutEffect(() => { - // do not read the url state until the user is logged in and do it just once - if (!loggedIn || isURLRead) return - - console.log( - "SUPERNOVA:: setting up state from url with state::", - urlStateManager.currentState() - ) - - // get active filters from url state - const activeFiltersFromURL = - urlStateManager.currentState()?.[ACTIVE_FILTERS] - - // check if there are active filters in the url state - if (activeFiltersFromURL && Object.keys(activeFiltersFromURL).length > 0) { - setActiveFilters(activeFiltersFromURL) - } else { - // otherwise set the support group filter - // we just add this default filter when no other filters are set via URL - const label = "support_group" - if ( - authData?.parsed?.supportGroups?.length > 0 && - filterLabels?.length > 0 && - filterLabels.includes(label) - ) { - // this will also trigger a filterItems() call from the store self - setActiveFilters({ [label]: authData.parsed.supportGroups }) - } - } - - const searchTermFromURL = urlStateManager.currentState()?.[SEARCH_TERM] - if (searchTermFromURL) { - // decode the search term from the url. It is base64 encoded to avoid issues with special characters - setSearchTerm(atob(searchTermFromURL)) - } - - // get active predefined filters from url state - const activePredefinedFilterFromURL = - urlStateManager.currentState()?.[ACTIVE_PREDEFINED_FILTER] - if (activePredefinedFilterFromURL) { - setActivePredefinedFilter(activePredefinedFilterFromURL) - } - - // get detail view target from url state - const detailsForFromURL = urlStateManager.currentState()?.[DETAILS_FOR] - if (detailsForFromURL) { - setShowDetailsFor(detailsForFromURL) - } - setIsURLRead(true) - }, [loggedIn, isURLRead, authData, filterLabels]) - - // sync URL with the desired states - useEffect(() => { - // do not synchronize the states until the url state is read and user logged in - if (!loggedIn || !isURLRead) return - - // encode searchTerm before pushing it to the URL to avoid missinterpretation of special characters - const encodedSearchTerm = btoa(searchTerm) - - const newState = { - ...urlStateManager.currentState(), - [ACTIVE_FILTERS]: activeFilters, - [SEARCH_TERM]: encodedSearchTerm, - [ACTIVE_PREDEFINED_FILTER]: activePredefinedFilter, - [DETAILS_FOR]: detailsFor, - } - - // do not push the state if it is the same as the current one - // otherwise it will reset the browser history and the forward button will not work - if ( - JSON.stringify(newState) === - JSON.stringify(urlStateManager.currentState()) - ) - return - - urlStateManager.push(newState) - }, [loggedIn, activeFilters, searchTerm, activePredefinedFilter, detailsFor]) - - // Support for back button - useEffect(() => { - const unregisterStateListener = urlStateManager.onChange((state) => { - setActiveFilters(state?.[ACTIVE_FILTERS]) - setSearchTerm(state?.[SEARCH_TERM]) - setActivePredefinedFilter(state?.[ACTIVE_PREDEFINED_FILTER]) - setShowDetailsFor(state?.[DETAILS_FOR]) - }) - - return () => { - unregisterStateListener() - } - }, []) -} - -export default useUrlState diff --git a/apps/supernova/src/index.js b/apps/supernova/src/index.js deleted file mode 100644 index f6f988315..000000000 --- a/apps/supernova/src/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { createRoot } from "react-dom/client" -import React from "react" - -// export mount and unmount functions -export const mount = (container, options = {}) => { - import("./App").then((App) => { - mount.root = createRoot(container) - mount.root.render(React.createElement(App.default, options?.props)) - }) -} - -export const unmount = () => mount.root && mount.root.unmount() diff --git a/apps/supernova/src/lib/createAlertsSlice.js b/apps/supernova/src/lib/createAlertsSlice.js deleted file mode 100644 index ae3add14b..000000000 --- a/apps/supernova/src/lib/createAlertsSlice.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import produce from "immer" -import { countAlerts } from "./utils" - -const initialAlertsState = { - items: [], - itemsFiltered: [], - totalCounts: {}, // { total: number, critical: number, ...}, - severityCountsPerRegion: {}, // {"eu-de-1": { total: number, critical: {total: number, suppressed: number}, warning: {...}, ...} - regions: [], // save all available regions from initial list here - regionsFiltered: [], // regions list filtered by active predefined filters - enrichedLabels: ["status"], // labels that are enriched by the alert worker - isLoading: false, - isUpdating: false, - updatedAt: null, - error: null, -} - -const createAlertsSlice = (set, get) => ({ - alerts: { - ...initialAlertsState, - actions: { - setAlertsData: ({ items, counts }) => { - set( - produce((state) => { - state.alerts.items = items - state.alerts.totalCounts = counts?.global - state.alerts.severityCountsPerRegion = counts?.regions - state.alerts.regions = Object.keys(counts?.regions).sort() - state.alerts.isLoading = false - state.alerts.isUpdating = false - state.alerts.updatedAt = Date.now() - state.alerts.error = null - - // on the initial fetch copy all items to the filtered items list once since - // most views operate on the filtered items list - if (state.alerts.itemsFiltered.length === 0) { - state.alerts.itemsFiltered = items - } - - // same with the filtered regions list - if (state.alerts.regionsFiltered.length === 0) { - state.alerts.regionsFiltered = state.alerts.regions - } - - // TODO: - // reload previously loaded filter label values (they might have changed since last load) - // state.filters.filterLabelValues = {} // -> do NOT just reset them, reload instead - }), - false, - "alerts.setAlertsData" - ) - // if there are already active filters or active predefined filters, filter the new list - if ( - Object.keys(get().filters.activeFilters)?.length > 0 || - get().filters.activePredefinedFilter - ) { - get().alerts.actions.filterItems() - } - }, - - filterItems: () => { - const activePredefinedFilter = get().filters.predefinedFilters.find( - (filter) => filter.name === get().filters.activePredefinedFilter - ) - - const filteredRegions = new Set() - - set( - produce((state) => { - state.alerts.itemsFiltered = state.alerts.items.filter((item) => { - let visible = true - - // test if the item has a label "visibility" with value "hidden" - // if it does, immediately return false, no further processing needed, these items are always filtered out - if (item.labels.visibility === "hidden") { - return false - } - - // if the item is still visible test if item labels match the regex matchers of the active predefined filter - // for each key and value pair in the filter matchers check if the key's value regex matches the item's label value for this key - // if it doesn't match, set visible to false and break out of the loop - activePredefinedFilter && - Object.entries(activePredefinedFilter.matchers).forEach( - ([key, value]) => { - if (!new RegExp(value, "i").test(item.labels[key])) { - visible = false - return - } else { - // if the item is visible, add the item's region to the filtered regions set - // this way the filtered Regions set will contain all regions that have at least one visible item - filteredRegions.add(item.labels.region) - } - } - ) - - // if the item is still visible after the predefined filters, check if it gets filtered out by the active filters - // active filters is an object where the keys correspond to labels and the value is an array of all selected values to be filtered by - // iterate over all active filter keys and then check if one of the selected values matches the item's value for this key - if (visible) { - Object.keys(state.filters.activeFilters).forEach((key) => { - // if the item's label value for the current label isn't included in the selected filters set visible to false, i.e. filter out item - // this automatically leads to different values for the same label to be OR concatenated, while different labels are AND concatenated - // so an item must have at least one of the selected values for each filtered label - if ( - state.filters.activeFilters[key].indexOf(item.labels[key]) < - 0 - ) { - // we can break out of the loop here since we already know the item is not visible - visible = false - return - } - }) - } - - // if the item is still visible check if it gets filtered out by a search term - // the search term is matched against the stringified item object via regex - // if the item object does not contain the search term, it is not visible - if ( - visible && - state.filters.searchTerm && - state.filters.searchTerm.length > 0 - ) { - const itemString = JSON.stringify(item) - const re = new RegExp(state.filters.searchTerm, "i") - if (!itemString.match(re)) { - visible = false - } - } - - return visible - }) - }), - false, - "alerts.filterItems" - ) - get().alerts.actions.updateFilteredCounts() - if (filteredRegions.size > 0) { - get().alerts.actions.setRegionsFiltered( - Array.from(filteredRegions).sort() - ) - } else { - // if nothing was filtered out, set the filtered regions to all available regions - get().alerts.actions.setRegionsFiltered(get().alerts.regions) - } - }, - - setFilteredItems: (items) => { - set( - produce((state) => { - state.alerts.itemsFiltered = items - }), - false, - "alerts.setFilteredItems" - ) - get().alerts.actions.updateFilteredCounts() - }, - - setRegionsFiltered: (regions) => { - set( - produce((state) => { - state.alerts.regionsFiltered = regions - }), - false, - "alerts.setRegionsFiltered" - ) - }, - - updateFilteredCounts: () => { - const counts = countAlerts(get().alerts.itemsFiltered) - set( - produce((state) => { - state.alerts.totalCounts = counts.global - state.alerts.severityCountsPerRegion = counts.regions - }), - false, - "alerts.updateFilteredCounts" - ) - }, - - setIsLoading: (value) => { - set( - (state) => ({ alerts: { ...state.alerts, isLoading: value } }), - false, - "alerts.setIsLoading" - ) - }, - - setIsUpdating: (value) => { - set( - (state) => ({ alerts: { ...state.alerts, isUpdating: value } }), - false, - "alerts.setIsUpdating" - ) - }, - - setError: (error) => { - set( - (state) => ({ alerts: { ...state.alerts, error, isLoading: false } }), - false, - "alerts.setError" - ) - }, - - getAlertByFingerprint: (fingerprint) => { - return get().alerts.items.find( - (alert) => alert.fingerprint === fingerprint - ) - }, - }, - }, -}) - -export default createAlertsSlice diff --git a/apps/supernova/src/lib/createAlertsSlice.test.js b/apps/supernova/src/lib/createAlertsSlice.test.js deleted file mode 100644 index a3197bb99..000000000 --- a/apps/supernova/src/lib/createAlertsSlice.test.js +++ /dev/null @@ -1,440 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as React from "react" -import { renderHook, act } from "@testing-library/react" -import { - useAlertsActions, - useAlertEnrichedLabels, - StoreProvider, - useAlertsItemsFiltered, -} from "../hooks/useAppStore" - -describe("createAlertsSlice", () => { - describe("setEnrichedLabels", () => { - it("return status as default", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useAlertsActions(), - enrichedLabels: useAlertEnrichedLabels(), - }), - { wrapper } - ) - expect(store.result.current.enrichedLabels).toEqual(["status"]) - }) - }) - - describe("setFilteredItems", () => { - it("return empty array as default", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useAlertsActions(), - itemsFiltered: useAlertsItemsFiltered(), - }), - { wrapper } - ) - expect(store.result.current.itemsFiltered).toEqual([]) - }) - - it("accepts and transforms to array of strings coma separated strings containing the labels to use", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useAlertsActions(), - itemsFiltered: useAlertsItemsFiltered(), - }), - { wrapper } - ) - - let mock - - act(() => { - store.result.current.actions.setFilteredItems(mock) - }) - - expect(store.result.current.enrichedLabels).toEqual(mock) - }) - }) -}) - -/** - * { - "action": "ALERTS_UPDATE", - "alerts": [ - { - "annotations": { - "description": "storage paths for `node003-bb322.cc.eu-de-1.cloud.sap` is less than other hosts in the `productionbb322`. (vc-d-4.cc.eu-de-1.cloud.sap)", - "summary": "storage paths for `node003-bb322.cc.eu-de-1.cloud.sap` is less than other hosts in the `productionbb322`. (vc-d-4.cc.eu-de-1.cloud.sap)" - }, - "endsAt": "2023-11-01T08:29:10.787Z", - "fingerprint": "662491f9dd5bc5eb", - "receivers": [ - { - "name": "elastic" - }, - { - "name": "awx" - }, - { - "name": "octobus" - }, - { - "name": "slack_vmware_info" - } - ], - "startsAt": "2023-10-23T09:29:40.787Z", - "status": { - "inhibitedBy": [], - "silencedBy": [ - "ed08c9ff-898a-49c9-9621-35f38191462e" - ], - "state": "suppressed" - }, - "updatedAt": "2023-11-01T08:25:11.606Z", - "generatorURL": "https://prometheus-vmware-vc-d-4.eu-de-1.cloud.sap/graph?g0.expr=vrops_hostsystem_storage_number_of_path+%3C+on+%28vccluster%29+group_left+%28%29+%28max+by+%28vccluster%29+%28vrops_hostsystem_storage_number_of_path%29%29&g0.tab=1", - "labels": { - "alertname": "HostStoragePathCheck", - "cluster": "s-eu-de-1", - "cluster_type": "scaleout", - "collector": "vrops-vc-d-4-host", - "context": "node003-bb322.cc.eu-de-1.cloud.sap storage paths", - "datacenter": "eu-de-1d", - "hostsystem": "node003-bb322.cc.eu-de-1.cloud.sap", - "internal_name": "host-120240", - "job": "vrops-exporter", - "meta": "storage paths for `node003-bb322.cc.eu-de-1.cloud.sap` is less than other hosts in the `productionbb322`. (vc-d-4.cc.eu-de-1.cloud.sap)", - "no_alert_on_absence": "true", - "prometheus": "vmware-monitoring/vmware-vc-d-4", - "region": "eu-de-1", - "service": "compute", - "severity": "info", - "support_group": "compute", - "tier": "vmware", - "vccluster": "productionbb322", - "vcenter": "vc-d-4.cc.eu-de-1.cloud.sap", - "status": "suppressed" - } - }, - { - "annotations": { - "description": "The certificate for kubevirt-operator-webhook,kubevirt-operator-webhook.kubevirt,kubevirt-operator-webhook.kubevirt.svc,kubevirt-operator-webhook.kubevirt.svc.cluster.local expires in 417ms. See secret kubevirt/kubevirt-operator-certs, key tls.crt.", - "summary": "Certificate expires" - }, - "endsAt": "2023-11-01T08:29:52.979Z", - "fingerprint": "dcff442a4c5301b6", - "receivers": [ - { - "name": "elastic" - } - ], - "startsAt": "2023-10-04T09:49:52.979Z", - "status": { - "inhibitedBy": [ - "82b6187e7c363b9a" - ], - "silencedBy": [], - "state": "suppressed" - }, - "updatedAt": "2023-11-01T08:25:52.998Z", - "generatorURL": "https://prometheus-kubernetes.qa-de-6.cloud.sap/graph?g0.expr=%28secrets_exporter_certificate_not_after+-+time%28%29%29+%2F+60+%2F+60+%2F+24+%3C%3D+30&g0.tab=1", - "labels": { - "alertname": "CertificateExpiresIn30Days", - "app": "k8s-secrets-certificate-exporter", - "ccloud_support_group": "observability", - "cluster": "qa-de-6", - "cluster_type": "metal", - "context": "availability", - "host": "kubevirt-operator-webhook,kubevirt-operator-webhook.kubevirt,kubevirt-operator-webhook.kubevirt.svc,kubevirt-operator-webhook.kubevirt.svc.cluster.local", - "instance": "100.90.11.32:9091", - "job": "pods", - "key": "tls.crt", - "kubernetes_namespace": "kube-monitoring", - "kubernetes_pod_name": "k8s-secrets-certificate-exporter-664b7d4d68-p28hp", - "metrics_path": "/metrics", - "pod_template_hash": "664b7d4d68", - "prometheus": "kube-monitoring/collector-kubernetes", - "region": "qa-de-6", - "secret": "kubevirt/kubevirt-operator-certs", - "service": "certificates", - "severity": "info", - "support_group": "containers", - "tier": "k8s", - "status": "suppressed" - } - } - ], - "counts": { - "global": { - "total": 3469, - "critical": 84, - "warning": 281, - "info": 3088, - "none": 16 - }, - "regions": { - "eu-de-2": { - "total": 640, - "critical": { - "total": 7, - "suppressed": 3 - }, - "warning": { - "total": 51, - "suppressed": 6 - }, - "info": { - "total": 578, - "suppressed": 5 - }, - "none": { - "total": 4 - } - }, - "qa-de-1": { - "total": 393, - "critical": { - "total": 46 - }, - "warning": { - "total": 28, - "suppressed": 12 - }, - "info": { - "total": 319, - "suppressed": 38 - } - }, - "ap-au-1": { - "total": 169, - "critical": { - "total": 11, - "suppressed": 2 - }, - "warning": { - "total": 34, - "suppressed": 8 - }, - "info": { - "total": 124, - "suppressed": 3 - } - }, - "qa-de-3": { - "total": 65, - "critical": { - "total": 1 - }, - "warning": { - "total": 6 - }, - "info": { - "total": 58 - } - }, - "ap-cn-1": { - "total": 173, - "critical": { - "total": 9, - "suppressed": 9 - }, - "warning": { - "total": 35, - "suppressed": 6 - }, - "info": { - "total": 126, - "suppressed": 1 - }, - "none": { - "total": 3 - } - }, - "eu-de-1": { - "total": 415, - "critical": { - "total": 3, - "suppressed": 3 - }, - "warning": { - "total": 29, - "suppressed": 1 - }, - "info": { - "total": 383, - "suppressed": 45 - } - }, - "na-us-1": { - "total": 273, - "critical": { - "total": 3, - "suppressed": 3 - }, - "warning": { - "total": 18 - }, - "info": { - "total": 252, - "suppressed": 16 - } - }, - "eu-nl-1": { - "total": 185, - "critical": { - "total": 1, - "suppressed": 1 - }, - "warning": { - "total": 6 - }, - "info": { - "total": 178, - "suppressed": 2 - } - }, - "na-us-2": { - "total": 148, - "critical": { - "total": 1, - "suppressed": 1 - }, - "warning": { - "total": 4 - }, - "info": { - "total": 143, - "suppressed": 1 - } - }, - "ap-sa-1": { - "total": 114, - "critical": { - "total": 2, - "suppressed": 2 - }, - "warning": { - "total": 2, - "suppressed": 2 - }, - "info": { - "total": 110, - "suppressed": 1 - } - }, - "qa-de-2": { - "total": 48, - "warning": { - "total": 4 - }, - "info": { - "total": 44 - } - }, - "qa-de-6": { - "total": 58, - "warning": { - "total": 12 - }, - "info": { - "total": 46, - "suppressed": 10 - } - }, - "ap-sa-2": { - "total": 115, - "warning": { - "total": 22 - }, - "info": { - "total": 93, - "suppressed": 1 - } - }, - "ap-ae-1": { - "total": 124, - "warning": { - "total": 10, - "suppressed": 6 - }, - "info": { - "total": 111, - "suppressed": 1 - }, - "none": { - "total": 3 - } - }, - "na-us-3": { - "total": 144, - "warning": { - "total": 1 - }, - "info": { - "total": 143, - "suppressed": 1 - } - }, - "ap-jp-1": { - "total": 133, - "warning": { - "total": 11, - "suppressed": 6 - }, - "info": { - "total": 119, - "suppressed": 1 - }, - "none": { - "total": 3 - } - }, - "la-br-1": { - "total": 85, - "warning": { - "total": 1 - }, - "info": { - "total": 84, - "suppressed": 1 - } - }, - "na-ca-1": { - "total": 94, - "warning": { - "total": 1, - "suppressed": 1 - }, - "info": { - "total": 93, - "suppressed": 1 - } - }, - "ap-jp-2": { - "total": 93, - "warning": { - "total": 6, - "suppressed": 6 - }, - "info": { - "total": 84, - "suppressed": 1 - }, - "none": { - "total": 3 - } - } - } - } -} - * - */ diff --git a/apps/supernova/src/lib/createAuthDataSlice.js b/apps/supernova/src/lib/createAuthDataSlice.js deleted file mode 100644 index e4b347e38..000000000 --- a/apps/supernova/src/lib/createAuthDataSlice.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const AUTH_ACTIONS = { - SIGN_ON: "signOn", - SIGN_OUT: "signOut", -} - -const createAuthDataSlice = (set, get) => ({ - auth: { - data: null, - isProcessing: false, - loggedIn: false, - error: null, - lastAction: {}, - appLoaded: false, - appIsLoading: false, - - actions: { - setAppLoaded: (appLoaded) => { - set( - (state) => ({ auth: { ...state.auth, appLoaded } }), - false, - "auth/setAppLoaded" - ) - }, - setData: (data) => { - if (!data) return - // check if data has changed before updating the state - if ( - data?.isProcessing === get().auth.isProcessing && - data?.loggedIn === get().auth.loggedIn && - data?.error === get().auth.error && - data?.auth === get().auth.data - ) - return - - set( - (state) => ({ - auth: { - ...state.auth, - isProcessing: data?.isProcessing, - loggedIn: data?.loggedIn, - error: data?.error, - data: data?.auth, - }, - }), - false, - "auth/setData" - ) - }, - setAction: (name) => - set( - (state) => ({ - auth: { - ...state.auth, - lastAction: { name: name, updatedAt: Date.now() }, - }, - }), - false, - "auth/setAction" - ), - login: () => get().auth.actions.setAction(AUTH_ACTIONS.SIGN_ON), - logout: () => get().auth.actions.setAction(AUTH_ACTIONS.SIGN_OUT), - }, - }, -}) - -export default createAuthDataSlice diff --git a/apps/supernova/src/lib/createFiltersSlice.js b/apps/supernova/src/lib/createFiltersSlice.js deleted file mode 100644 index 1b4822fab..000000000 --- a/apps/supernova/src/lib/createFiltersSlice.js +++ /dev/null @@ -1,255 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import produce from "immer" - -const initialFiltersState = { - labels: ["status"], // labels to be used for filtering: [ "label1", "label2", "label3"]. Default is status which is enriched by the worker - activeFilters: {}, // for each active filter key list the selected values: {key1: [value1], key2: [value2_1, value2_2], ...} - filterLabelValues: {}, // contains all possible values for filter labels: {label1: ["val1", "val2", "val3", ...], label2: [...]}, lazy loaded when a label is selected for filtering - predefinedFilters: [], // predefined complex filters that filter using regex: [{name: "filter1", displayName: "Filter 1", matchers: {"label1": "regex1", "label2": "regex2", ...}}, ...] - activePredefinedFilter: null, // the currently active predefined filter - searchTerm: "", // the search term used for full-text filtering -} - -const createFiltersSlice = (set, get) => ({ - filters: { - ...initialFiltersState, - actions: { - setLabels: (labels) => - set( - (state) => { - if (!labels) return state - - // check if labels is an array - if (!Array.isArray(labels)) { - console.warn( - "[supernova]::setLabels: labels object is not an array" - ) - return state - } - - // check if all elements in the array are strings delete the ones that are not - if (!labels.every((element) => typeof element === "string")) { - console.warn( - "[supernova]::setLabels: Some elements of the array are not strings." - ) - labels = labels.filter((element) => typeof element === "string") - } - - // merge given labels with the initial, make it unique and sort it alphabetically - const uniqueLabels = Array.from( - new Set(initialFiltersState.labels.concat(labels)) - ).sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) - - return { - filters: { - ...state.filters, - labels: uniqueLabels, - }, - } - }, - false, - "filters.setLabels" - ), - - setActiveFilters: (activeFilters) => { - set( - (state) => { - return { - filters: { - ...state.filters, - activeFilters, - }, - } - }, - false, - "filters.setActiveFilters" - ) - get().alerts.actions.filterItems() - }, - - clearActiveFilters: () => { - set( - produce((state) => { - state.filters.activeFilters = {} - }), - false, - "filters.clearActiveFilters" - ) - get().alerts.actions.filterItems() - }, - - addActiveFilter: (filterLabel, filterValue) => { - set( - produce((state) => { - // use Set to prevent duplicate values - state.filters.activeFilters[filterLabel] = [ - ...new Set([ - ...(state.filters.activeFilters[filterLabel] || []), - filterValue, - ]), - ] - }), - false, - "filters.addActiveFilter" - ) - // after adding a new filter key and value: filter items - get().alerts.actions.filterItems() - }, - - // add multiple values for a filter label - addActiveFilters: (filterLabel, filterValues) => { - set( - produce((state) => { - // use Set to prevent duplicate values - state.filters.activeFilters[filterLabel] = [ - ...new Set([ - ...(state.filters.activeFilters[filterLabel] || []), - ...filterValues, - ]), - ] - }), - false, - "filters.addActiveFilters" - ) - // after adding a new filter key and value: filter items - get().alerts.actions.filterItems() - }, - - removeActiveFilter: (filterLabel, filterValue) => { - set( - produce((state) => { - state.filters.activeFilters[filterLabel] = - state.filters.activeFilters[filterLabel].filter( - (value) => value !== filterValue - ) - // if this was the last selected value delete the whole label key - if (state.filters.activeFilters[filterLabel].length === 0) { - delete state.filters.activeFilters[filterLabel] - } - }), - false, - "filters.removeActiveFilter" - ) - // after removing a filter: filter items - get().alerts.actions.filterItems() - }, - - setPredefinedFilters: (predefinedFilters) => { - set( - produce((state) => { - state.filters.predefinedFilters = predefinedFilters - }), - false, - "filters.setPredefinedFilters" - ) - }, - - setActivePredefinedFilter: (filterName) => { - set( - produce((state) => { - state.filters.activePredefinedFilter = filterName - }), - false, - "filters.setActivePredefinedFilter" - ) - // after activating predefined filter: filter items - get().alerts.actions.filterItems() - }, - - clearActivePredefinedFilter: () => { - set( - produce((state) => { - state.filters.activePredefinedFilter = null - }), - false, - "filters.clearActivePredefinedFilter" - ) - // after clearing predefined filter: filter items - get().alerts.actions.filterItems() - }, - - togglePredefinedFilter: (filterName) => { - set( - produce((state) => { - // if active predefined filter is already set and equal to the one that was clicked, clear it - if (state.filters.activePredefinedFilter === filterName) { - state.filters.activePredefinedFilter = null - } else { - state.filters.activePredefinedFilter = filterName - } // otherwise set the clicked filter as active - }), - false, - "filters.togglePredefinedFilter" - ) - // after activating predefined filter: filter items - get().alerts.actions.filterItems() - }, - - // retieve all possible values for the given filter label from the list of items and add them to the list - loadFilterLabelValues: (filterLabel) => { - set( - produce((state) => { - state.filters.filterLabelValues[filterLabel] = { isLoading: true } - }), - false, - "filters.loadFilterLabelValues.isLoading" - ) - set( - produce((state) => { - // use Set to ensure unique values - const values = [ - ...new Set( - state.alerts.items.map((item) => item.labels[filterLabel]) - ), - ] - // remove any "blank" values from the list, then sort - state.filters.filterLabelValues[filterLabel].values = values - .filter((value) => (value ? true : false)) - .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) - - state.filters.filterLabelValues[filterLabel].isLoading = false - }), - false, - "filters.loadFilterLabelValues" - ) - }, - - // for each filter label where we already loaded the values, reload them - reloadFilterLabelValues: () => { - Object.keys(get().filters.filterLabelValues).map((label) => { - get().filters.actions.loadFilterLabelValues(label) - }) - }, - - setSearchTerm: (searchTerm) => { - set( - produce((state) => { - state.filters.searchTerm = searchTerm - }), - false, - "filters.setSearchTerm" - ) - // after setting the search term: filter items - get().alerts.actions.filterItems() - }, - - // TODO: - // update previously loaded filter label values (e.g. after new items were fetched, the possible values might have changed) - // updateFilterLabelValues: () => { - // set( - // produce((state) => { - // Object.keys(state.filters.filterLabelValues).map((label) => - - // ) - // }) - // ) - // } - }, - }, -}) - -export default createFiltersSlice diff --git a/apps/supernova/src/lib/createFiltersSlice.test.js b/apps/supernova/src/lib/createFiltersSlice.test.js deleted file mode 100644 index dede36679..000000000 --- a/apps/supernova/src/lib/createFiltersSlice.test.js +++ /dev/null @@ -1,192 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as React from "react" -import { renderHook, act } from "@testing-library/react" -import { - useFilterLabels, - useFilterActions, - useSearchTerm, - StoreProvider, -} from "../hooks/useAppStore" - -const originalConsoleError = global.console.warn - -describe("createFiltersSlice", () => { - describe("setLabels", () => { - it("return default status label", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - filterLabels: useFilterLabels(), - }), - { wrapper } - ) - expect(store.result.current.filterLabels).toEqual(["status"]) - }) - - it("Adds array with strings to select", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - filterLabels: useFilterLabels(), - }), - { wrapper } - ) - - act(() => { - store.result.current.actions.setLabels([ - "app", - "cluster", - "cluster_type", - "context", - "job", - "region", - "service", - "severity", - "support_group", - "tier", - "type", - ]) - }) - - expect(store.result.current.filterLabels).toEqual( - expect.arrayContaining([ - "app", - "status", - "cluster", - "cluster_type", - "context", - "job", - "region", - "service", - "severity", - "support_group", - "tier", - "type", - ]) - ) - }) - - it("Adds empty array to select", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - filterLabels: useFilterLabels(), - }), - { wrapper } - ) - - act(() => { - store.result.current.actions.setLabels([]) - }) - - expect(store.result.current.filterLabels).toEqual( - expect.arrayContaining(["status"]) - ) - }) - - it("warns the user if labels are not an array", () => { - const spy = jest.spyOn(console, "warn").mockImplementation(() => {}) - - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - filterLabels: useFilterLabels(), - }), - { wrapper } - ) - - act(() => - store.result.current.actions.setLabels( - "app,cluster,cluster_type,context,job,region,service,severity,status,support_group,tier,type" - ) - ) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - "[supernova]::setLabels: labels object is not an array" - ) - spy.mockRestore() - }) - - it("warns the user if labels array also includes non-strings and adds the valid labels", () => { - const spy = jest.spyOn(console, "warn").mockImplementation(() => {}) - - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - filterLabels: useFilterLabels(), - }), - { wrapper } - ) - - act(() => store.result.current.actions.setLabels(["app", 1, 9])) - - // Is the warning called? - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - "[supernova]::setLabels: Some elements of the array are not strings." - ) - spy.mockRestore() - - // Are valid labels still set? - expect(store.result.current.filterLabels).toEqual( - expect.arrayContaining(["app", "status"]) - ) - }) - }) - - describe("setSearchTerm", () => { - it("empty search term", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - searchTerm: useSearchTerm(), - }), - { wrapper } - ) - - expect(store.result.current.searchTerm).toEqual("") - }) - - it("Set a search term", () => { - const wrapper = ({ children }) => ( - {children} - ) - const store = renderHook( - () => ({ - actions: useFilterActions(), - searchTerm: useSearchTerm(), - }), - { wrapper } - ) - - act(() => { - store.result.current.actions.setSearchTerm("k8s") - }) - - expect(store.result.current.searchTerm).toEqual("k8s") - }) - }) -}) diff --git a/apps/supernova/src/lib/createGlobalsSlice.js b/apps/supernova/src/lib/createGlobalsSlice.js deleted file mode 100644 index f6a5cf667..000000000 --- a/apps/supernova/src/lib/createGlobalsSlice.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import produce from "immer" - -const createGlobalsSlice = (set, get) => ({ - globals: { - embedded: false, - showDetailsFor: null, - apiEndpoint: null, - - actions: { - setEmbedded: (embedded) => - set( - (state) => ({ globals: { ...state.globals, embedded: embedded } }), - false, - "globals/setEmbedded" - ), - setShowDetailsFor: (alertID) => - set( - (state) => ({ - // if the alertID is the same as the current one, we want to close the details panel again, - // otherwise set the new alertID to replace the details in the panel - globals: { - ...state.globals, - showDetailsFor: - get().globals.showDetailsFor === alertID ? null : alertID, - }, - }), - false, - "globals/setShowDetailsFor" - ), - setApiEndpoint: (endpoint) => - set( - (state) => ({ - globals: { ...state.globals, apiEndpoint: endpoint }, - }), - false, - "globals/setShowDetailsFor" - ), - }, - }, -}) - -export default createGlobalsSlice diff --git a/apps/supernova/src/lib/createSilencesSlice.js b/apps/supernova/src/lib/createSilencesSlice.js deleted file mode 100644 index e4ebe603f..000000000 --- a/apps/supernova/src/lib/createSilencesSlice.js +++ /dev/null @@ -1,334 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import produce from "immer" - -const initialSilencesState = { - items: [], - itemsHash: {}, - itemsByState: {}, - excludedLabels: [], - isLoading: false, - isUpdating: false, - updatedAt: null, - error: null, - localItems: {}, - - // silence templates for maintanance - templates: [], -} - -const validateTemplates = (templates) => { - // check if the templates are an array - if (!Array.isArray(templates)) { - console.warn( - "[supernova]::validateTemplates: templates object is not an array" - ) - return [ - { - id: "1", - title: "Invalid template object", - invalid: "Templates object is not an array", - }, - ] - } - - // check if every element in the array is an object - if (!templates.every((element) => typeof element === "object")) { - console.warn( - "[supernova]::validateTemplates: templates object is not an array of objects" - ) - return [ - { - id: "1", - title: "Invalid template object", - invalid: "Templates object is not an array of objects", - }, - ] - } - - // check if every object - return templates - ?.map((template, index) => { - // check if status is active - if (template?.status === "active") { - // check if title and discription is a string, fixed_labels is an object and editable_labels is an array of strings - if ( - typeof template?.title !== "string" || - typeof template?.description !== "string" || - typeof template?.fixed_labels !== "object" || - !Array.isArray(template?.editable_labels) || - !template?.editable_labels.every( - (element) => typeof element === "string" - ) - ) { - let brokenElement = "Following elements are not well formed: " - - ;(brokenElement += - typeof template?.title !== "string" ? "title " : ""), - (brokenElement += - typeof template?.description !== "string" ? "description " : ""), - (brokenElement += - typeof template?.fixed_labels !== "object" - ? "fixed_labels " - : ""), - (brokenElement += !Array.isArray(template?.editable_labels) - ? "editable_labels " - : "") - return { - id: "elem" + index, - title: - typeof template?.title === "string" - ? template?.title - : "Invalid template", - invalid: brokenElement, - } - } - // if all ok, return the template - return { - id: "elem" + index, - title: template?.title, - description: template?.description, - fixed_labels: template?.fixed_labels || {}, - editable_labels: template?.editable_labels || [], - invalid: false, - } - } - // if status is not active, return null to filter it out - return null - }) - .filter((template) => template !== null) -} - -const createSilencesSlice = (set, get, options) => ({ - silences: { - ...initialSilencesState, - - // silence templates for maintanance - templates: options?.silenceTemplates - ? validateTemplates(options?.silenceTemplates) - : [], - actions: { - setSilences: ({ items, itemsHash, itemsByState }) => { - if (!items) return - - set( - produce((state) => { - state.silences.items = items - state.silences.itemsHash = itemsHash - state.silences.itemsByState = itemsByState - state.silences.isLoading = false - state.silences.isUpdating = false - state.silences.updatedAt = Date.now() - state.silences.error = null - }), - false, - "silences.setSilencesData" - ) - - // check if any local item can be removed - get().silences.actions.updateLocalItems() - }, - /* - Save temporary created silences to be able to display which alert is silenced - and who silenced it until the next alert fetch contains the silencedBy reference - */ - addLocalItem: ({ silence, id, alertFingerprint }) => { - // enforce silences with id and alertFingerprint - if (!silence || !id || !alertFingerprint) return - return set( - produce((state) => { - state.silences.localItems = { - ...get().silences.localItems, - [id]: { - ...silence, - id, - alertFingerprint, - type: "local", - }, - } - }), - false, - "silences.addLocalItem" - ) - }, - /* - Remove local silences which are already referenced by an alert - */ - updateLocalItems: () => { - const allSilences = get().silences.itemsHash - let newLocalSilences = { ...get().silences.localItems } - Object.keys(newLocalSilences).forEach((key) => { - const alert = get().alerts.actions.getAlertByFingerprint( - newLocalSilences[key]?.alertFingerprint - ) - - // check if the alert has already the silence reference and if the extern silence already exists - const silencedBy = alert?.status?.silencedBy - if ( - silencedBy?.length > 0 && - silencedBy?.includes(newLocalSilences[key]?.id) && - allSilences[key] - ) { - // mark to remove silence - newLocalSilences[key] = { ...newLocalSilences[key], remove: true } - } - }) - - // remove silences marked to remove - const reducedLocalSilences = Object.keys(newLocalSilences) - .filter((key) => !newLocalSilences[key]?.remove) - .reduce((obj, key) => { - obj[key] = newLocalSilences[key] - return obj - }, {}) - - return set( - produce((state) => { - state.silences.localItems = reducedLocalSilences - }), - false, - "silences.updateLocalItems" - ) - }, - /* - Given an alert fingerprint, this function returns all silences referenced by silencingBy. It also - check if there are local silences with the same alert fingerprint and return them as well. - */ - getMappingSilences: (alert) => { - if (!alert) return - const externalSilences = get().silences.itemsHash - let silencedBy = alert?.status?.silencedBy || [] - - // ensure silencedBy is an array - if (!Array.isArray(silencedBy)) silencedBy = [silencedBy] - let mappingSilences = [] - silencedBy.forEach((id) => { - if (externalSilences[id]) { - mappingSilences.push(externalSilences[id]) - } - }) - - // add local silences - let localSilences = get().silences.localItems - Object.keys(localSilences).forEach((silenceID) => { - // if there is already a silence with the same id, skip it and exists as external silence - if (silencedBy.includes(silenceID) && externalSilences[silenceID]) - return - // if the local silence has the same alert fingerprint, add it to the mapping silences - if ( - localSilences[silenceID]?.alertFingerprint === alert?.fingerprint - ) { - mappingSilences.push(localSilences[silenceID]) - } - }) - return mappingSilences - }, - /* - Return the state of an alert. If the alert is silenced by a local silence, the state is suppressed (processing) - */ - getMappedState: (alert) => { - if (!alert) return - // get all silences (local and external) - const silences = get().silences.actions.getMappingSilences(alert) - // if there is a silence with type local, return suppressed (processing) - if (silences?.find((silence) => silence?.type === "local")) { - return { type: "suppressed", isProcessing: true } - } - return { type: alert?.status?.state, isProcessing: false } - }, - setExcludedLabels: (labels) => { - return set( - (state) => { - // check if labels is an array and if every element in the array is a string - if ( - !Array.isArray(labels) || - !labels.some((element) => typeof element === "string") - ) { - console.warn( - "[supernova]::setExcludedLabels: labels object is not an array of strings" - ) - return state - } - - return { - silences: { - ...state.silences, - excludedLabels: labels, - }, - } - }, - false, - "silences.setExcludedLabels" - ) - }, - /* - Find all silences in itemsByState with key expired that matches all labels (key&value) from the alert but omit the labels that are excluded (excludedLabels) - */ - getExpiredSilences: (alert) => { - if (!alert) return - const alertLabels = alert?.labels || {} - const silences = get().silences.itemsByState?.expired || [] - const excludedLabels = get().silences.excludedLabels || [] - const enrichedLabels = get().alerts.enrichedLabels || [] - // combine the arrays containing the labels that shouldn't be used for matching into one for easier checking - const labelsExcludedForMatching = [...excludedLabels, ...enrichedLabels] - - // find all expired silences that matches all labels from the alert excluding the excluded excludedLabels - return silences.filter((silence) => { - const silenceMatchers = silence?.matchers || [] - // check if all labels from the alert are included in the silence - return Object.keys(alertLabels).every((label) => { - // check if the label is excluded - if (labelsExcludedForMatching.includes(label)) return true - // check if the label is included in the silence - return silenceMatchers.some( - (silenceLabel) => - silenceLabel?.name === label && - silenceLabel?.value === alertLabels?.[label] - ) - }) - }) - }, - /* - Returns the silence (including the local ones) with the latest expiration time for an alert. Useful to display when the alert will be active again. - */ - getLatestMappingSilence: (alert) => { - if (!alert) return - const silences = get().silences.actions.getMappingSilences(alert) - if (!silences?.length) return - // return the latest expired silence - return silences.reduce((prev, current) => - prev.endsAt > current.endsAt ? prev : current - ) - }, - setIsLoading: (value) => - set( - (state) => ({ silences: { ...state.silences, isLoading: value } }), - false, - "silences.setIsLoading" - ), - setIsUpdating: (value) => - set( - (state) => ({ - silences: { ...state.silences, isUpdating: value }, - }), - false, - "silences.setIsUpdating" - ), - setError: (error) => { - set( - (state) => ({ - silences: { ...state.silences, error, isLoading: false }, - }), - false, - "silences.setError" - ) - }, - }, - }, -}) - -export default createSilencesSlice diff --git a/apps/supernova/src/lib/createSilencesSlice.test.js b/apps/supernova/src/lib/createSilencesSlice.test.js deleted file mode 100644 index cea24fca9..000000000 --- a/apps/supernova/src/lib/createSilencesSlice.test.js +++ /dev/null @@ -1,778 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import * as React from "react" -import { renderHook, act } from "@testing-library/react" -import { - useSilencesActions, - useSilencesLocalItems, - useAlertsActions, - useAlertsItems, - useSilencesExcludedLabels, - StoreProvider, -} from "../hooks/useAppStore" -import { - createFakeAlertStatustWith, - createFakeAlertWith, - createFakeSilenceWith, -} from "./fakeObjects" -import { countAlerts } from "../lib/utils" - -describe("addLocalItem", () => { - it("should append the object with key silence id and value the silence itself", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - actions: useSilencesActions(), - localSilences: useSilencesLocalItems(), - }), - { wrapper } - ) - - const silence = createFakeSilenceWith() - act(() => - store.result.current.actions.addLocalItem({ - silence, - id: "test", - alertFingerprint: "123", - }) - ) - - expect(Object.keys(store.result.current.localSilences).length).toEqual(1) - expect(store.result.current.localSilences["test"]["id"]).toEqual("test") - expect( - store.result.current.localSilences["test"]["alertFingerprint"] - ).toEqual("123") - }) - it("should avoid to add any silences without id or alertFingerprint", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - actions: useSilencesActions(), - localSilences: useSilencesLocalItems(), - }), - { wrapper } - ) - - const silence = createFakeSilenceWith() - act(() => store.result.current.actions.addLocalItem({ silence, id: "" })) - act(() => store.result.current.actions.addLocalItem({ silence, id: null })) - act(() => - store.result.current.actions.addLocalItem({ - silence, - id: "test", - alertFingerprint: "", - }) - ) - act(() => - store.result.current.actions.addLocalItem({ - silence, - id: "test", - alertFingerprint: null, - }) - ) - expect(Object.keys(store.result.current.localSilences).length).toEqual(0) - }) -}) - -describe("getMappingSilences", () => { - it("return all external silences referenced by silencedBy and all local silences with the same fingerprint which are not yet included", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create extern silences adding an id to the object - const silence = createFakeSilenceWith({ id: "external" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence], - itemsHash: { external: silence }, - itemsByState: { active: [silence] }, - }) - ) - // create local silence adding per attribute the id and the alert fingerprint - const silence2 = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence2, - id: "local", - alertFingerprint: "123", - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getMappingSilences(alert)) - ) - expect(mappingResult.length).toEqual(2) - expect(mappingResult.map((item) => item.id)).toContainEqual("external") - expect(mappingResult.map((item) => item.id)).toContainEqual("local") - expect(mappingResult.find((item) => item.id === "local").type).toEqual( - "local" - ) - }) - - it("return silences also when alert silencedBy is just a string", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert - const status = createFakeAlertStatustWith({ silencedBy: "external" }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create local silence - const silence = createFakeSilenceWith({ id: "external" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence], - itemsHash: { external: silence }, - itemsByState: { active: [silence] }, - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getMappingSilences(alert)) - ) - expect(mappingResult.length).toEqual(1) - expect(mappingResult.map((item) => item.id)).toContainEqual("external") - }) - - it("ignores 'local silences' which are already included in silencedBy and exist as external silence", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external", "externalAndLocal"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create external silences adding an id to the object - const silence = createFakeSilenceWith({ id: "external" }) - const silence2 = createFakeSilenceWith({ id: "externalAndLocal" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence, silence2], - itemsHash: { external: silence, externalAndLocal: silence2 }, - itemsByState: { active: [silence, silence2] }, - }) - ) - // create local silence which already exists as external silence - const silence3 = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence3, - id: "externalAndLocal", - alertFingerprint: "123", - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getMappingSilences(alert)) - ) - expect(mappingResult.length).toEqual(2) - // checking type to be undefined means that the silence is not local - expect(mappingResult[0].type).toEqual(undefined) - expect(mappingResult[1].type).toEqual(undefined) - }) - - it("returns local silences when the id exists in silencedBy but it does not exist as external silence", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external", "local"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create external silences adding an id to the object - const silence = createFakeSilenceWith({ id: "external" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence], - itemsHash: { external: silence }, - itemsByState: { active: [silence] }, - }) - ) - // create local silence which already exists as external silence - const silence2 = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence2, - id: "local", - alertFingerprint: "123", - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getMappingSilences(alert)) - ) - expect(mappingResult.length).toEqual(2) - // checking type to be undefined means that the silence is not local - expect(mappingResult[0].type).toEqual(undefined) - expect(mappingResult[1].type).toEqual("local") - }) -}) - -describe("updateLocalItems", () => { - it("removes local silences whose alert reference (defined by alertFingerprint) has in silencedBy the silence itself and a silence with same id exist also as external silences", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - savedLocalSilences: useSilencesLocalItems(), - savedAlerts: useAlertsItems(), - }), - { wrapper } - ) - - // create local silences - const silence = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence, - id: "test1local", - alertFingerprint: "12345", - }) - ) - const silence2 = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence2, - id: "test2local", - alertFingerprint: "non_existing_alert", - }) - ) - // check if the local silence are saved - expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual( - 2 - ) - // create an alert without any silencedBy so we just have the local silences - const status = createFakeAlertStatustWith({ - silencedBy: ["test1local"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "12345" }) - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // check if the alert is saved - expect(store.result.current.savedAlerts.length).toEqual(1) - // trigger update local items by setting new external silences - const externalSilence = createFakeSilenceWith({ id: "test1local" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [externalSilence], - itemsHash: { [externalSilence.id]: externalSilence }, - itemsByState: { active: [externalSilence] }, - }) - ) - // check local items - expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual( - 1 - ) - expect(store.result.current.savedLocalSilences["test2local"].id).toEqual( - "test2local" - ) - }) - - it("keeps local silences if silence with same id does not exist yet in external silences", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - savedLocalSilences: useSilencesLocalItems(), - savedAlerts: useAlertsItems(), - }), - { wrapper } - ) - - // create local silences - const silence = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence, - id: "test1local", - alertFingerprint: "12345", - }) - ) - const silence2 = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence2, - id: "test2local", - alertFingerprint: "non_existing_alert", - }) - ) - // check if the local silence are saved - expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual( - 2 - ) - // create an alert without any silencedBy so we just have the local silences - const status = createFakeAlertStatustWith({ - silencedBy: ["test1local"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "12345" }) - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // check if the alert is saved - expect(store.result.current.savedAlerts.length).toEqual(1) - // trigger update local items by setting new external silences - const externalSilence = createFakeSilenceWith({ - id: "different_id_then_test1local", - }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [externalSilence], - itemsHash: { [externalSilence.id]: externalSilence }, - itemsByState: { active: [externalSilence] }, - }) - ) - // check local items - expect(Object.keys(store.result.current.savedLocalSilences).length).toEqual( - 2 - ) - expect(store.result.current.savedLocalSilences["test1local"].id).toEqual( - "test1local" - ) - expect(store.result.current.savedLocalSilences["test2local"].id).toEqual( - "test2local" - ) - }) -}) - -describe("getMappedState", () => { - it("retuns supressed (processing) if a local silence is found", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create extern silences adding an id to the object - const silence = createFakeSilenceWith({ id: "external" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence], - itemsHash: { external: silence }, - itemsByState: { active: [silence] }, - }) - ) - // create local silence adding per attribute the id and the alert fingerprint - const silence2 = createFakeSilenceWith() - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence2, - id: "local", - alertFingerprint: "123", - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getMappedState(alert)) - ) - expect(mappingResult["type"]).toEqual("suppressed") - expect(mappingResult["isProcessing"]).toEqual(true) - }) - - it("retuns just the alert.status.state if no local silences found", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create extern silences adding an id to the object - const silence = createFakeSilenceWith({ id: "external" }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence], - itemsHash: { external: silence }, - itemsByState: { active: [silence] }, - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getMappedState(alert)) - ) - expect(mappingResult["type"]).toEqual(alert?.status?.state) - expect(mappingResult["isProcessing"]).toEqual(false) - }) -}) - -describe("getExpiredSilences", () => { - it("returns all silences which are expired matching the alert labels but omitting the excludeLabels", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // set the excluded labels - act(() => store.result.current.silenceActions.setExcludedLabels(["pod"])) - // create an alert with custom status - const alert = createFakeAlertWith({ - fingerprint: "123", - labels: { - severity: "critical", - support_group: "containers", - service: "automation", - pod: "test", - }, - }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create external silences with different labels (service compute) - const silence = createFakeSilenceWith({ - id: "test1", - status: { - state: "expired", - }, - matchers: [ - { name: "severity", value: "critical", isRegex: false }, - { name: "support_group", value: "compute", isRegex: false }, - { name: "service", value: "compute", isRegex: false }, - ], - }) - // create an external silences with matching labels - const silence2 = createFakeSilenceWith({ - id: "test2", - status: { - state: "expired", - }, - matchers: [ - { name: "severity", value: "critical", isRegex: false }, - { name: "support_group", value: "containers", isRegex: false }, - { name: "service", value: "automation", isRegex: false }, - ], - }) - // create an external silences with less labels but matching - const silence3 = createFakeSilenceWith({ - id: "test3", - status: { - state: "expired", - }, - matchers: [ - { name: "severity", value: "info", isRegex: false }, - { name: "support_group", value: "containers", isRegex: false }, - ], - }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence, silence2, silence3], - itemsHash: { test1: silence, test2: silence2, test3: silence3 }, - itemsByState: { expired: [silence, silence2, silence3] }, - }) - ) - // get mapping silences - let expResult = null - act( - () => - (expResult = - store.result.current.silenceActions.getExpiredSilences(alert)) - ) - expect(expResult.length).toEqual(1) - expect(expResult[0].id).toEqual("test2") - }) -}) - -describe("getLatestMappingSilence", () => { - it("returns the silence with the latest endsAt timestamp when local", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create extern silences adding an id to the object - const silence = createFakeSilenceWith({ - id: "external", - endsAt: "2023-06-21T15:17:28.327Z", - }) - const silence2 = createFakeSilenceWith({ - id: "external2", - endsAt: "2023-06-21T16:18:28.327Z", - }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence, silence2], - itemsHash: { external: silence, external2: silence2 }, - itemsByState: { active: [silence, silence2] }, - }) - ) - // create local silence adding per attribute the id and the alert fingerprint - const silence3 = createFakeSilenceWith({ - endsAt: "2023-06-21T19:17:28.327Z", - }) - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence3, - id: "local", - alertFingerprint: "123", - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getLatestMappingSilence(alert)) - ) - expect(mappingResult.id).toEqual("local") - }) - - it("returns the silence with the latest endsAt timestamp when external", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - alertActions: useAlertsActions(), - silenceActions: useSilencesActions(), - }), - { wrapper } - ) - - // create an alert with custom status - const status = createFakeAlertStatustWith({ - silencedBy: ["external", "external2"], - }) - const alert = createFakeAlertWith({ status: status, fingerprint: "123" }) - // set the alert - act(() => - store.result.current.alertActions.setAlertsData({ - items: [alert], - counts: countAlerts([alert]), - }) - ) - // create extern silences adding an id to the object - const silence = createFakeSilenceWith({ - id: "external", - endsAt: "2023-06-21T15:17:28.327Z", - }) - const silence2 = createFakeSilenceWith({ - id: "external2", - endsAt: "2023-06-21T20:18:28.327Z", - }) - act(() => - store.result.current.silenceActions.setSilences({ - items: [silence, silence2], - itemsHash: { external: silence, external2: silence2 }, - itemsByState: { active: [silence, silence2] }, - }) - ) - // create local silence adding per attribute the id and the alert fingerprint - const silence3 = createFakeSilenceWith({ - endsAt: "2023-06-21T19:17:28.327Z", - }) - act(() => - store.result.current.silenceActions.addLocalItem({ - silence: silence3, - id: "local", - alertFingerprint: "123", - }) - ) - // get mapping silences - let mappingResult = null - act( - () => - (mappingResult = - store.result.current.silenceActions.getLatestMappingSilence(alert)) - ) - expect(mappingResult.id).toEqual("external2") - }) -}) - -describe("setExcludedLabels", () => { - it("return empty array as default", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - actions: useSilencesActions(), - excludedLabels: useSilencesExcludedLabels(), - }), - { wrapper } - ) - expect(store.result.current.excludedLabels).toEqual([]) - }) - - it("accepts array of strings containing the labels to use", () => { - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - actions: useSilencesActions(), - excludedLabels: useSilencesExcludedLabels(), - }), - { wrapper } - ) - - act(() => { - store.result.current.actions.setExcludedLabels([ - "pod", - "pod_name", - "instance", - ]) - }) - - expect(store.result.current.excludedLabels).toEqual([ - "pod", - "pod_name", - "instance", - ]) - }) - - it("warn the user if labels are different then an array of strings", () => { - const spy = jest.spyOn(console, "warn").mockImplementation(() => {}) - - const wrapper = ({ children }) => {children} - const store = renderHook( - () => ({ - actions: useSilencesActions(), - excludedLabels: useSilencesExcludedLabels(), - }), - { wrapper } - ) - - act(() => - store.result.current.actions.setExcludedLabels("pod,pod_name,instance") - ) - - expect(spy).toHaveBeenCalledTimes(1) - expect(spy).toHaveBeenCalledWith( - "[supernova]::setExcludedLabels: labels object is not an array of strings" - ) - spy.mockRestore() - }) -}) diff --git a/apps/supernova/src/lib/createUserActivitySlice.js b/apps/supernova/src/lib/createUserActivitySlice.js deleted file mode 100644 index 903996ac7..000000000 --- a/apps/supernova/src/lib/createUserActivitySlice.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import produce from "immer" - -const createUserActivitySlice = (set, get) => ({ - userActivity: { - isActive: true, - - actions: { - setIsActive: (activity) => { - set( - (state) => ({ - userActivity: { ...state.userActivity, isActive: activity }, - }), - false, - "userActivity.setIsActive" - ) - }, - }, - }, -}) - -export default createUserActivitySlice diff --git a/apps/supernova/src/lib/fakeObjects.js b/apps/supernova/src/lib/fakeObjects.js deleted file mode 100644 index b8030d1d0..000000000 --- a/apps/supernova/src/lib/fakeObjects.js +++ /dev/null @@ -1,110 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -const refAlert = { - annotations: { - description: - "vSphere High Availability (HA) has detected a possible host failure for `node014-bb164.cc.na-us-1.cloud.sap`. (vc-a-0.cc.na-us-1.cloud.sap).", - summary: - "vSphere High Availability (HA) has detected a possible host failure for `node014-bb164.cc.na-us-1.cloud.sap`. (vc-a-0.cc.na-us-1.cloud.sap).", - }, - endsAt: "2023-06-21T13:04:01.855Z", - fingerprint: "62cab9a4fd5732ee", - receivers: [ - { name: "elastic" }, - { name: "awx" }, - { name: "pagerduty_compute" }, - { name: "octobus" }, - { name: "support_group_alerts_critical_compute" }, - { name: "pagerduty_vmware" }, - { name: "slack_vmware_critical" }, - ], - startsAt: "2023-06-21T12:13:31.855Z", - status: { - inhibitedBy: [], - silencedBy: [], - state: "active", - }, - updatedAt: "2023-06-21T13:00:01.969Z", - generatorURL: - "https://prometheus-vmware-vc-a-0.na-us-1.cloud.sap/graph?g0.expr=vrops_hostsystem_alert_info%7Balert_name%3D%22vSphere+High+Availability+%28HA%29+has+detected+a+possible+host+failure%22%7D+and+on+%28hostsystem%29+vrops_hostsystem_runtime_maintenancestate%7Bstate%21~%22inMaintenance%22%2Cvccluster%21~%22.%2Acontrolplane-swift%22%7D&g0.tab=1", - labels: { - alert_impact: "HEALTH", - alert_level: "CRITICAL", - alert_name: - "vSphere High Availability (HA) has detected a possible host failure", - alertname: "HADetectedAPossibleHostFailure", - cluster: "s-na-us-1", - cluster_type: "scaleout", - collector: "vrops-vc-a-0-host", - context: "node014-bb164.cc.na-us-1.cloud.sap failure", - datacenter: "na-us-1a", - description: - "A vSphere HA master agent considers a host to have failed if it loses contact with the vSphere HA agent on the host, the host does not respond to pings on any of the management interfaces, and the master does not observe any datastore heartbeats. This problem can occur when a computer on the network is configured to have the same IP address as one of the ESX/ESXi hosts in a HA cluster. In this situation, the HA agent receives invalid data and generates errors. The HA agent does not function properly until it is reconfigured. The frequency of this problem depends on how often the IP address conflict occurs.", - hostsystem: "node014-bb164.cc.na-us-1.cloud.sap", - job: "vrops-exporter", - meta: "vSphere High Availability (HA) has detected a possible host failure for `node014-bb164.cc.na-us-1.cloud.sap`. (vc-a-0.cc.na-us-1.cloud.sap).", - no_alert_on_absence: "true", - playbook: "docs/devops/alert/vcenter/#hadetectedapossiblehostfailure", - prometheus: "vmware-monitoring/vmware-vc-a-0", - recommendation_1: - "Find the computer that has the duplicate IP address and reconfigure it to have a different IP address. This fault will be cleared and the alert canceled when the underlying problem is resolved and the vSphere HA master agent is able to connect to the HA agent on the host. NOTE: You can use the Duplicate IP warning in the /var/log/vmkernel log file on an ESX host or the /var/log/messages log file on an ESXi host to identify the computer that has the duplicate IP address.", - region: "na-us-1", - service: "compute", - severity: "critical", - status: "active", - support_group: "compute", - symptom_1_data: - "{'condition': {'faultEvents': ['com.vmware.vc.HA.DasHostFailedEvent'], 'faultKey': 'fault|host|ha', 'type': 'CONDITION_FAULT'}, 'severity': 'CRITICAL'}", - symptom_1_name: "vSphere HA detected a host failure", - tier: "vmware", - vccluster: "productionbb164", - vcenter: "vc-a-0.cc.na-us-1.cloud.sap", - }, -} - -const refAlertStatus = { - inhibitedBy: [], - silencedBy: [], - state: "active", -} - -const refSilence = { - duration: "2", - comment: "Test description", - createdBy: "Jane Doe", - status: { - state: "active", - }, - matchers: [ - { name: "cluster", value: "s-na-us-1", isRegex: false }, - { name: "cluster_type", value: "scaleout", isRegex: false }, - { - name: "context", - value: "node014-bb164.cc.na-us-1.cloud.sap failure", - isRegex: false, - }, - { name: "job", value: "vrops-exporter", isRegex: false }, - { name: "region", value: "na-us-1", isRegex: false }, - { name: "service", value: "compute", isRegex: false }, - { name: "severity", value: "critical", isRegex: false }, - { name: "support_group", value: "compute", isRegex: false }, - { name: "tier", value: "vmware", isRegex: false }, - ], - startsAt: "2023-06-21T13:17:28.327Z", - endsAt: "2023-06-21T15:17:28.327Z", -} - -export const createFakeSilenceWith = (props = {}) => { - return { ...refSilence, ...props } -} - -export const createFakeAlertStatustWith = (props = {}) => { - return { ...refAlertStatus, ...props } -} - -export const createFakeAlertWith = (props = {}) => { - return { ...refAlert, ...props } -} diff --git a/apps/supernova/src/lib/utils.js b/apps/supernova/src/lib/utils.js deleted file mode 100644 index df609af76..000000000 --- a/apps/supernova/src/lib/utils.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -export const severityToSemanticName = (severity) => { - switch (severity) { - case "critical": - return "danger" - case "warning": - return "warning" - case "info": - return "info" - default: - return severity - } -} - -export const descriptionParsed = (text) => { - if (!text) return "" - // urls in descriptions follow the schema: - // Parse description and replace urls with a-tags - const regexUrl = /<(http[^>|]+)\|([^>]+)>/g - const urlParsed = text.replace(regexUrl, `$2`) - - // replace text wrapped in *..* by strong tags - const regexBold = /\*(.*)\*/g - const boldParsed = urlParsed.replace(regexBold, `$1`) - - const regexCode = /`(.*)`/g - return boldParsed.replace(regexCode, `$1`) -} - -// Capitalize first char, underscores to spaces, camel case to spaces, all words except the first to lower case -export const humanizeString = (value) => { - if (!value) { - return value - } - - const camelCaseMatch = /([A-Z])/g - const underscoreMatch = /_/g - - const camelCaseToSpaces = value.replace(camelCaseMatch, " $1") - const underscoresToSpaces = camelCaseToSpaces.replace(underscoreMatch, " ") - - // all together now (also capitalize first word and lowercase all other words) - const humanized = - underscoresToSpaces.charAt(0).toUpperCase() + - underscoresToSpaces.slice(1).toLowerCase() - - return humanized -} - -// sort silences by state -// { -// active: [...], pending: [...], expired:[...], ... -// } -export const sortSilencesByState = (silences) => { - const sortedSilences = {} - - if (!silences || silences.length === 0) return {} - - silences.forEach((silences) => { - const state = silences.status?.state - if (!sortedSilences[state]) sortedSilences[state] = [] // init - sortedSilences[state].push(silences) - }) - return sortedSilences -} - -// count alerts and create a map -// { -// global: { total: number, critical: number, ...}, -// regions: { -// "eu-de-1": { total: number, critical: {total: number, suppressed: number}, warning: {...}, ...} -// }, ... -// } -export const countAlerts = (alerts) => { - const counts = { global: { total: 0 }, regions: {} } - - if (!alerts || alerts.length === 0) return counts - - // run through each alert once and adjust different types of counts as necessary - alerts.forEach((alert) => { - // total number of alerts - counts.global.total += 1 - - const region = alert.labels?.region - const severity = alert.labels?.severity - const state = alert.status?.state - - // global count per severity - counts.global[severity] = counts.global[severity] || 0 // init - counts.global[severity] += 1 - - // count per region and severity - counts.regions[region] = counts.regions[region] || {} // init - counts.regions[region].total = counts.regions[region].total || 0 // init - counts.regions[region].total += 1 - - // total count per region and severity - counts.regions[region][severity] = counts.regions[region][severity] || {} // init - counts.regions[region][severity]["total"] = - counts.regions[region][severity]?.total || 0 // init - counts.regions[region][severity]["total"] += 1 - // suppressed per region and severity - if (state === "suppressed") { - counts.regions[region][severity].suppressed = - counts.regions[region][severity]?.suppressed || 0 // init - counts.regions[region][severity].suppressed += 1 - } - }) - - return counts -} - -/** - * This method sorts the alerts first by severity (critical -> warning -> others), then by status, then by startsAt timestamp and finally by region - * @param {array} items, a list of alerts - * @returns {array} sorted alerts - */ -export const sortAlerts = (items) => { - const importantSeverities = ["critical", "warning"] - - return items.sort((a, b) => { - if ( - (a.labels?.severity === "critical" && - b.labels?.severity !== "critical") || - (a.labels?.severity === "warning" && - importantSeverities.indexOf(b.labels?.severity) < 0) - ) - return -1 - else if ( - a.labels?.severity === b.labels?.severity && - a.status?.state !== b.status?.state && - a.status?.state - ) - return a.status?.state.localeCompare(b.status?.state) - else if ( - a.labels?.severity === b.labels?.severity && - a.status?.state === b.status?.state && - a.startsAt !== b.startsAt && - b.startsAt - ) - return b.startsAt?.localeCompare(a.startsAt) - else if ( - a.labels?.severity === b.labels?.severity && - a.status?.state === b.status?.state && - a.startsAt === b.startsAt && - a.labels?.region - ) - return a.labels?.region?.localeCompare(b.labels?.region) - else return 1 - }) -} diff --git a/apps/supernova/src/styles.scss b/apps/supernova/src/styles.scss deleted file mode 100644 index 77cc678a9..000000000 --- a/apps/supernova/src/styles.scss +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors -// SPDX-License-Identifier: Apache-2.0 - -/* Do not remove these tailwind directives. Without them styles won't work as expected */ -@tailwind base; -@tailwind components; -@tailwind utilities; - -/* If necessary, app styles can be added below */ - -.inline-code { - @apply bg-theme-badge-default; - @apply text-theme-default; - @apply text-sm; - @apply rounded; - @apply px-1; - @apply py-0.5; -} - -// datagrid row hover style -// REMOVE THIS ONCE DATAGRID COMPONENT SUPPORTS HOVER -.alerts { - .juno-datagrid-row:hover { - .juno-datagrid-cell { - @apply bg-theme-background-lvl-1; - } - } - - .juno-datagrid-row.active { - .juno-datagrid-cell { - @apply bg-theme-background-lvl-2; - } - } -} diff --git a/apps/supernova/src/workers/alerts.js b/apps/supernova/src/workers/alerts.js deleted file mode 100644 index 817149618..000000000 --- a/apps/supernova/src/workers/alerts.js +++ /dev/null @@ -1,90 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import ApiService from "../api/apiService" -import { get } from "../api/client" -import { sortAlerts, countAlerts } from "../lib/utils" - -let compareAlertString - -/** - * @param {string} endpoint - * @param {object} options - * @returns {function} fetch function - */ -const fetchAction = (endpoint, options = {}) => { - return get(`${endpoint}/alerts`, { params: options.params }).then((items) => { - // sort alerts - let alerts = sortAlerts(items) - - // copy additional filter options to labels for easier filter selection - // because the alert object is nested this makes it a lot easier to filter, since we only use what is present in alert.labels - alerts.forEach((alert) => { - if (alert.labels) { - alert.labels.status = alert.status?.state - } - }) - - // slice if limit provided - if (options?.limit) { - if (options?.debug) - console.info("Alerts service: limit set: ", options?.limit) - alerts = alerts.slice(0, options?.limit) - } - - // check if new loaded alerts are different from the last response - const newCompareString = JSON.stringify(alerts) - if (options?.debug) - console.info( - "Alerts service: any changes?", - compareAlertString !== newCompareString - ) - if (compareAlertString !== newCompareString) { - compareAlertString = newCompareString - - if (options?.debug) console.info("Alerts service: inform listener") - // inform listener to receive new alerts - self.postMessage({ - action: "ALERTS_UPDATE", - alerts, - counts: countAlerts(alerts), - }) - } else { - if (options?.debug) console.info("Alerts service: no change found") - } - }) -} - -const alertsService = new ApiService({ - serviceName: "alerts", - debug: true, - onFetchStart: () => self.postMessage({ action: "ALERTS_FETCH_START" }), - onFetchEnd: () => self.postMessage({ action: "ALERTS_FETCH_END" }), - onFetchError: (error) => { - self.postMessage({ action: "ALERTS_FETCH_ERROR", error: error.message }) - }, -}) - -self.onmessage = (e) => { - const action = e.data.action - - switch (action) { - case "ALERTS_CONFIGURE": - // require at least apiEndpoint to update the fetch method - if (e.data?.fetchVars?.apiEndpoint) { - // update the fetch function - e.data["fetchFn"] = () => - fetchAction( - e.data?.fetchVars.apiEndpoint, - e.data?.fetchVars.options || {} - ) - } - alertsService.configure(e.data) - break - case "ALERTS_FETCH": - alertService.fetch() - break - } -} diff --git a/apps/supernova/src/workers/silences.js b/apps/supernova/src/workers/silences.js deleted file mode 100644 index f6661150f..000000000 --- a/apps/supernova/src/workers/silences.js +++ /dev/null @@ -1,55 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import ApiService from "../api/apiService" -import { get } from "../api/client" -import { sortSilencesByState } from "../lib/utils" - -const fetchAction = (endpoint) => { - return get(`${endpoint}/silences`, {}).then((items) => { - // convert items to hash to easear access - const itemsHash = items.reduce((itemsHash, silence) => { - itemsHash[silence.id] = silence - return itemsHash - }, {}) - - // split items by state (active, pending and expired) - // https://github.com/prometheus/alertmanager/blob/main/types/types.go#L434 - const itemsByState = sortSilencesByState(items) - - self.postMessage({ - action: "SILENCES_UPDATE", - silences: items, - silencesHash: itemsHash, - silencesBySate: itemsByState, - }) - }) -} - -const silenceService = new ApiService({ - serviceName: "silences", - debug: true, - onFetchStart: () => self.postMessage({ action: "SILENCES_FETCH_START" }), - onFetchEnd: () => self.postMessage({ action: "SILENCES_FETCH_END" }), - onFetchError: (error) => { - self.postMessage({ action: "SILENCES_FETCH_ERROR", error: error.message }) - }, -}) - -self.onmessage = (e) => { - const action = e.data.action - - switch (action) { - case "SILENCES_CONFIGURE": - if (e.data?.apiEndpoint) { - e.data["fetchFn"] = () => fetchAction(e.data?.apiEndpoint) - } - silenceService.configure(e.data) - break - case "SILENCES_FETCH": - silenceService.fetch() - break - } -} diff --git a/apps/supernova/tailwind.config.js b/apps/supernova/tailwind.config.js deleted file mode 100644 index 81b1f8fef..000000000 --- a/apps/supernova/tailwind.config.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -// opacity helper to make custom colors work with opacity -function withOpacity(variableName) { - return ({ opacityVariable, opacityValue }) => { - if (opacityValue !== undefined) { - return `rgba(var(${variableName}), ${opacityValue})` - } - if (opacityVariable !== undefined) { - return `rgba(var(${variableName}), var(${opacityVariable}, 1))` - } - return `rgb(var(${variableName}))` - } -} - -module.exports = { - presets: [ - require("juno-ui-components/build/lib/tailwind.config"), // important, do not change - ], - prefix: "", // important, do not change - content: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"], - corePlugins: { - preflight: false, // important, do not change - }, - theme: {}, - plugins: [], -} diff --git a/apps/template/package.json b/apps/template/package.json index 4756b4db5..4f59f17c5 100644 --- a/apps/template/package.json +++ b/apps/template/package.json @@ -28,7 +28,7 @@ "babel-plugin-transform-import-meta": "^2.2.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "postcss": "^8.4.21", "postcss-url": "^10.1.3", @@ -40,7 +40,7 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", "zustand": "4.5.2", "esbuild": "^0.19.5" @@ -52,12 +52,12 @@ }, "peerDependencies": { "@tanstack/react-query": "4.28.0", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "zustand": "4.5.2" }, "importmapExtras": { diff --git a/apps/user-activity/package.json b/apps/user-activity/package.json index dca41ced2..1da877bb1 100644 --- a/apps/user-activity/package.json +++ b/apps/user-activity/package.json @@ -24,7 +24,7 @@ "autoprefixer": "^10.4.2", "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", "postcss": "^8.4.21", diff --git a/apps/volta/jest.config.js b/apps/volta/jest.config.js index d16f3da9d..cd58e0648 100644 --- a/apps/volta/jest.config.js +++ b/apps/volta/jest.config.js @@ -8,7 +8,7 @@ module.exports = { testEnvironment: "jsdom", setupFilesAfterEnv: ["/setupTests.js"], transformIgnorePatterns: [ - "node_modules/(?!(juno-ui-components|messages-provider)/)", + "node_modules/(?!(juno-ui-components|messages-provider|utils)/)", ], moduleNameMapper: { // Jest currently doesn't support resources with query parameters. diff --git a/apps/volta/package.json b/apps/volta/package.json index c57b058e8..1d3088461 100644 --- a/apps/volta/package.json +++ b/apps/volta/package.json @@ -27,14 +27,14 @@ "autoprefixer": "^10.4.2", "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "lodash.uniqueid": "^4.0.1", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -45,23 +45,23 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", - "utils": "*", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "peerDependencies": { "@tanstack/react-query": "4.28.0", "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "importmapExtras": { diff --git a/apps/whois/package.json b/apps/whois/package.json index 0160a7b69..1c223b74d 100644 --- a/apps/whois/package.json +++ b/apps/whois/package.json @@ -26,11 +26,11 @@ "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", "cidr-regex": "^3.1.1", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "ip-regex": "^5.0.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "postcss": "^8.4.21", "postcss-url": "^10.1.3", @@ -41,7 +41,7 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.8.0", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4" }, "scripts": { @@ -50,12 +50,12 @@ "build": "NODE_ENV=production node esbuild.config.js" }, "peerDependencies": { - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*" + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz" }, "appProps": { "theme": { diff --git a/ci/pipeline.yaml.erb b/ci/pipeline.yaml.erb index 1fe98c433..46b13a6be 100644 --- a/ci/pipeline.yaml.erb +++ b/ci/pipeline.yaml.erb @@ -21,13 +21,17 @@ "playground": { type: "app", path: "apps/playground"}, "juno-ui-components": { type: "lib", path: "libs/juno-ui-components"}, "messages-provider": { type: "lib", path: "libs/messages-provider"}, - "oauth": { type: "lib", path: "libs/oauth"}, "policy-engine": { type: "lib", path: "libs/policy-engine"}, - "communicator": { type: "lib", path: "libs/communicator"}, "url-state-provider": { type: "lib", path: "libs/url-state-provider"}, "url-state-router": { type: "lib", path: "libs/url-state-router"}, "utils": { type: "lib", path: "libs/utils"}, } + + CLOUDOPERATOR_ASSETS = { + "communicator": { type: "lib", path: "libs/communicator"}, + "oauth": { type: "lib", path: "libs/oauth"}, + } + HA_REGIONS = ["eu-de-1","eu-de-2","eu-nl-1","ap-ae-1","ap-jp-2","ap-au-1","la-br-1","na-us-1","na-us-2"] %> @@ -141,9 +145,6 @@ resources: uri: https://github.com/sapcc/juno.git branch: main paths: ["<%=details[:path]%>","libs","package.json","e2e", ".yarn"] - # "ci" - # "ci/scripts" - # "ci/shared" username: sapcc-bot password: ((github-access-token/sapcc-bot)) @@ -155,6 +156,32 @@ resources: <% end %> + <% CLOUDOPERATOR_ASSETS.each do |name,details| %> + - name: <%=details[:type]%>-<%= name %>.git + icon: github + type: git-proxy + webhook_token: bleep-bloop + source: + uri: https://github.com/cloudoperators/juno.git + username: ((github-access-token/sapcc-bot)) + password: x-oauth-basic + branch: main + paths: ["<%=details[:path]%>"] + + - name: <%= details[:type] %>-<%= name %>.version + type: time-version-resource + icon: lock + check_every: 525600h + source: { key: "<%= details[:type] %>-<%= name %>" } # disambiguate from other time-version resources + + <% end %> + + - name: run-manually.version + type: time-version-resource + icon: lock + check_every: 525600h + source: { key: "juno-run-manually-version" } # disambiguate from other time-version resources + - name: assets-server.version type: time-version-resource icon: lock @@ -186,7 +213,8 @@ resource_types: groups: - name: assets jobs: - <% ASSETS.each do |name,details| %> + - run-all-manually + <% ASSETS.merge(CLOUDOPERATOR_ASSETS).each do |name,details| %> - build-<%= details[:type] %>-<%= name %> <% end %> - build-assets-server-image @@ -235,6 +263,11 @@ jobs: - '\tThis pipeline is defined at: https://github.com/sapcc/juno/ci/\n\n' - '\tManage Juno images: https://keppel.eu-de-1.cloud.sap/ccloud/juno\n' + + - name: run-all-manually + plan: + - put: run-manually.version + # ============================================================= # BUILD BASE IMAGES - name: build-base-image @@ -269,12 +302,16 @@ jobs: # BUILD ASSET SERVER IMAGE # Assets build - <% ASSETS.each do |name,details| %> + <% ASSETS.merge(CLOUDOPERATOR_ASSETS).each do |name,details| %> - name: build-<%= details[:type] %>-<%= name %> public: true plan: + - get: run-manually.version + trigger: true + passed: [run-all-manually] - get: <%= details[:type] %>-<%= name %>.git trigger: true + - get: juno.git - get: base.image - get: ci-helper.image - put: <%= details[:type] %>-<%= name %>.version @@ -282,7 +319,7 @@ jobs: <% if details[:type] == "lib" %> # this is used to get the last version, only libs with new versions will be deployed - task: download-last-build - file: <%=details[:type]%>-<%= name %>.git/ci/shared/swift-download.yaml + file: juno.git/ci/shared/swift-download.yaml output_mapping: download: last_build params: @@ -315,13 +352,14 @@ jobs: # TODO: this need a lot of runtime, move that stuff into asset_build.sh after version check echo "sync all node_modules from /juno/ to ./latest/" rsync -am --include='*/' --include='node_modules/***' --exclude='*' /juno/ ./latest - cd ./latest + cd ./latest/<%=details[:type]%>s/<%=name%> echo "update node modules -> npm install --silent" - npm install --silent + npm install + cd ../../ echo "" - ./ci/scripts/asset_build.sh --asset-name <%= name %> --asset-type <%= details[:type] %> --output-path ../build_result <% if details[:type] == "lib" %> --last-build-path ../last_build <% end %> + /juno/ci/scripts/asset_build.sh --asset-name <%= name %> --asset-type <%= details[:type] %> --output-path ../build_result <% if details[:type] == "lib" %> --last-build-path ../last_build <% end %> - task: sync - file: <%=details[:type]%>-<%= name %>.git/ci/shared/swift-upload.yaml + file: juno.git/ci/shared/swift-upload.yaml input_mapping: upload: build_result params: @@ -358,7 +396,7 @@ jobs: - get: swift-juno-assets.version trigger: true - <% ASSETS.each do |name, details|%> + <% ASSETS.merge(CLOUDOPERATOR_ASSETS).each do |name, details|%> - get: <%= details[:type] %>-<%= name %>.version trigger: false passed: ["build-<%= details[:type] %>-<%= name %>"] @@ -392,7 +430,7 @@ jobs: set -e cd ./juno.git # 1) download our own assets - <% ASSETS.each do |name, details|%> + <% ASSETS.merge(CLOUDOPERATOR_ASSETS).each do |name, details|%> ./ci/scripts/asset_storage.sh --container juno-assets --asset-name <%= name %> --asset-path <%= details[:path] %> --action download --root-path ../juno-assets <% end %> # 2) download and check for name collission in juno-3rd-party assets diff --git a/ci/scripts/asset_build.sh b/ci/scripts/asset_build.sh index 832f32ae3..f947cd02e 100755 --- a/ci/scripts/asset_build.sh +++ b/ci/scripts/asset_build.sh @@ -3,10 +3,10 @@ # exit on error set -e -if [ ! -f "CODEOWNERS" ]; then - echo "This script must run from root of juno repo" - exit 1 -fi +# if [ ! -f "CODEOWNERS" ]; then +# echo "This script must run from root of juno repo" +# exit 1 +# fi function help() { echo "Usage: build_assets.sh --asset-path||-ap --asset-name||-sn --asset-type||-at --output-path||-op --last-build-path||-lbp @@ -21,6 +21,8 @@ if [[ "$1" == "--help" ]]; then help fi +SCRIPTS_FOLDER=$(dirname $0) + OUTPUT_PATH="./build-result" while [[ $# -gt 0 ]]; do case $1 in @@ -134,19 +136,24 @@ fi echo "----------------------------------" echo "generate COMMUNICATOR.md in $ASSET_PATH" -node ci/scripts/generate_communication_readme.mjs --path="$ASSET_PATH" +node "$SCRIPTS_FOLDER/generate_communication_readme.mjs" --path="$ASSET_PATH" # install and build libs -npm run build-libs +# npm run build-libs # TEST AND BUILD ASSET # IGNORE_EXTERNALS=true will results in a bundle which includes all dependencies. # This is the case if the jspm cdn is unreachable!!! echo "----------------------------------" echo "run Tests for ...." +# since we removed all local dependencies (*) we don't need to use --workspace +# instead we can use the local path ASSET_NAME=$(jq -r .name "$ASSET_PATH/package.json") -npm --workspace "$ASSET_NAME" run test --if-present -NODE_ENV=production IGNORE_EXTERNALS=false npm --workspace "$ASSET_NAME" run build --if-present +CURRENT_DIR=$(pwd) +cd "$ASSET_PATH" +npm run test --if-present +NODE_ENV=production IGNORE_EXTERNALS=false npm run build --if-present +cd "$CURRENT_DIR" # get BUILD_DIR from package.json # strip `leading` slash from BUILD_DIR and split by / and use first part diff --git a/ci/scripts/esm_build/generate_importmap.mjs b/ci/scripts/esm_build/generate_importmap.mjs index 4156290c6..084e5a714 100644 --- a/ci/scripts/esm_build/generate_importmap.mjs +++ b/ci/scripts/esm_build/generate_importmap.mjs @@ -162,10 +162,21 @@ for (let name in packageRegistry) { // if the package has peer dependencies, we need to add them to the importmap's scopes section for (let depName in pkg.peerDependencies) { - const depVersion = pkg.peerDependencies[depName] - const ownPackage = - packageRegistry[depName]?.[depVersion === "*" ? "latest" : depVersion] + let depVersion = pkg.peerDependencies[depName] + + let ownPackage = null + if (packageRegistry[depName]) { + depVersion = depVersion === "*" ? "latest" : depVersion + // support URL as version + if (depVersion.startsWith("http")) { + // extract version from url. The version start directly after @ and is a sem version + depVersion = depVersion.match(/@([0-9]+\.[0-9]+\.[0-9]+)/)[1] + } + + ownPackage = packageRegistry[depName]?.[depVersion] + } + //console.log("====", depName, depVersion, ownPackage) if (ownPackage) { log( yellow( diff --git a/docs/build_and_host_app.md b/docs/build_and_host_app.md index cc5c684aa..615ae0605 100644 --- a/docs/build_and_host_app.md +++ b/docs/build_and_host_app.md @@ -33,12 +33,12 @@ npm -v ```json "peerDependencies": { "@tanstack/react-query": "^4.28.0", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "zustand": "^4.1.1" }, ``` @@ -53,8 +53,8 @@ npm -v ```yaml "devDependencies": { ... - "juno-ui-components": "*", - "url-state-provider": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", ... }, ``` diff --git a/libs/juno-ui-components/package.json b/libs/juno-ui-components/package.json index 9d95639a3..c77e16232 100644 --- a/libs/juno-ui-components/package.json +++ b/libs/juno-ui-components/package.json @@ -5,7 +5,7 @@ "module": "build/index.js", "source": "src/index.js", "style": "build/lib/variables.css", - "version": "2.13.8", + "version": "2.14.0", "files": [ "src/colors.css", "tailwind.config.js" diff --git a/libs/juno-ui-components/src/components/Navigation/Navigation.component.js b/libs/juno-ui-components/src/components/Navigation/Navigation.component.js new file mode 100644 index 000000000..32c276c6e --- /dev/null +++ b/libs/juno-ui-components/src/components/Navigation/Navigation.component.js @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { createContext, useEffect, useState } from "react" +import PropTypes from "prop-types" +import { NavigationItem } from "../NavigationItem/" + +export const NavigationContext = createContext() + +/** A generic Navigation component providing all the necessary functionality for a navigation. For internal use only. Not to be used directly, but to be wrapped by more role-specific / semantic navigation components such as `TabNavigation`, `TopNavigation`, `SideNavigation`. */ +export const Navigation = ({ + activeItem, + ariaLabel, + children, + className, + disabled, + onActiveItemChange, + onChange, + ...props +}) => { + const [activeItm, setActiveItm] = useState("") + const [items, setItems] = useState(new Map()) + + const findItemIdByKeyValue = (valueToFind) => { + // The prioritized sequence of individual item keys to check for a value: + const prioritizedKeys = ["value", "children", "label"] + const itemsKeys = Array.from(items.keys()) + if (itemsKeys.includes(valueToFind)) { + // return the value if it is found in the keys of the items map + return valueToFind + } else { + // If the value is not found in the keys of the items map, search for the value in the individual items according to the sequence in prioritizedKeys. If a matching item is found, return its id or null: + let foundItemId + for (let [key, obj] of items.entries()) { + prioritizedKeys.forEach((pKey) => { + if (obj[pKey] === valueToFind) { + foundItemId = obj.id + } + }) + } + return foundItemId + } + } + + useEffect(() => { + if (activeItem) { + const activeItemId = findItemIdByKeyValue(activeItem) + setActiveItm(activeItemId) + } + }, [activeItem]) + + // Re-evaluate active item when items map changes (essential to set the active item properly on first render!): + useEffect(() => { + if (activeItem) { + const activeItemId = findItemIdByKeyValue(activeItem) + setActiveItm(activeItemId) + } + }, [items]) + + // Key is set as established by the child item according to priority: value || children || label + const addItem = (key, children, label, value) => { + setItems((oldMap) => + new Map(oldMap).set(key, { + id: key, // store the associated key of the item in the map inside the object, so we can easily get the key later if we have to find an object by any of its keys + value: value, + label: label, + children: children, + displayName: children || label || value, // priority of what to actually render in each item + }) + ) + } + + const handleActiveItemChange = (key) => { + setActiveItm(key) + onActiveItemChange && onActiveItemChange(key) + } + + return ( + +
    + {children} +
+
+ ) +} + +// TODO: validate whether children are instances of NavigationItem + +Navigation.propTypes = { + /** The currently active item. Pass the `value`, `label` prop, or the child string of the respective NavigationItem. */ + activeItem: PropTypes.string, + /** The aria label of the navigation */ + arialLabel: PropTypes.string, + /** The child navigation items of the navigation */ + children: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.arrayOf(PropTypes.node), + ]), + /** Pass a custom className to the navigation parent element */ + className: PropTypes.string, + /** Whether the navigation is disabled. Will disable all children. */ + disabled: PropTypes.bool, + /** Handler to execute when the active item changes. Alias to `onChange`. */ + onActiveItemChange: PropTypes.func, + /** Handler to execute when the active item changes. Alias to `onActiveItemChange`. */ + onChange: PropTypes.func, +} + +Navigation.defaultProps = { + activeItem: "", + ariaLabel: "", + children: null, + className: "", + disabled: false, + onActiveItemChange: undefined, + onChange: undefined, +} diff --git a/libs/juno-ui-components/src/components/Navigation/Navigation.stories.js b/libs/juno-ui-components/src/components/Navigation/Navigation.stories.js new file mode 100644 index 000000000..513e88236 --- /dev/null +++ b/libs/juno-ui-components/src/components/Navigation/Navigation.stories.js @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { Navigation } from "./index.js" +import { NavigationItem } from "../NavigationItem/" + +export default { + title: "Internal/Navigation", + component: Navigation, + argTypes: { + children: { + control: false, + }, + role: { + options: ["TabNavigation", "TopNavigation", "SideNavigation"], + control: { type: "select" }, + }, + }, +} + +const Template = ({ children, ...props }) => ( + {children} +) + +export const DefaultWithChildren = { + render: Template, + args: { + activeItem: "Item 1", + children: [ + Item 1, + Item 2, + Item 3, + + Item 4 + , + ], + }, +} + +export const WithValuesAndLabels = { + render: Template, + args: { + children: [ + , + , + , + ], + }, +} + +export const WithValuesLabelsAndChildren = { + render: Template, + args: { + children: [ + + Item 1 + , + + Item 2 + , + + Item 3 + , + ], + }, +} + +export const ValuesOnly = { + render: Template, + args: { + children: [ + , + , + , + ], + }, +} + +export const WithActiveItemByValue = { + render: Template, + args: { + activeItem: "item-2", + children: [ + , + , + , + ], + }, +} + +// TODO: +export const WithActiveItemByLabel = { + render: Template, + args: { + activeItem: "Item 2", + children: [ + , + , + , + ], + }, +} + +// TODO: +export const WithActiveItemByChild = { + render: Template, + args: { + activeItem: "Item 2", + children: [ + + Item 1 + , + + Item 2 + , + + Item 3 + , + ], + }, +} + +export const Disabled = { + render: Template, + args: { + disabled: true, + children: [ + Item 1, + + Item 2 + , + Item 3, + ], + }, +} + +export const ItemsAsLinks = { + render: Template, + args: { + children: [ + + Link 1 + , + + Link 2 + , + + Link 3 + , + ], + }, +} diff --git a/libs/juno-ui-components/src/components/Navigation/Navigation.test.js b/libs/juno-ui-components/src/components/Navigation/Navigation.test.js new file mode 100644 index 000000000..757038ee9 --- /dev/null +++ b/libs/juno-ui-components/src/components/Navigation/Navigation.test.js @@ -0,0 +1,1524 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as React from "react" +import { render, screen, waitFor, cleanup, act } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { Navigation } from "./index" +import { NavigationItem } from "../NavigationItem/index" + +const mockOnChange = jest.fn() +const mockOnActiveItemChange = jest.fn() + +describe("Navigation", () => { + afterEach(() => { + cleanup() + jest.clearAllMocks() + }) + + test("renders a Navigation", async () => { + render() + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.getByRole("navigation")).toHaveClass("juno-navigation") + }) + + test("renders children as passed", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + }) + + test("renders an aria-label as passed", async () => { + render() + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.getByRole("navigation")).toHaveAttribute( + "aria-label", + "describe the navigation" + ) + }) + + test("renders a disabled navigation as passed", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.getByRole("navigation")).toHaveAttribute( + "aria-disabled", + "true" + ) + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).toBeDisabled() + expect(screen.getByRole("button", { name: "Item 1" })).toHaveAttribute( + "aria-disabled", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toBeDisabled() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-disabled", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toBeDisabled() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-disabled", + "true" + ) + }) + + // Test setting the activeItem initially: + + test("renders an active item as passed to the parent by child content when only content is given", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child content when content and label are given", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child content when content and value are given", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child content when content, label, and value are given", async () => { + render( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child value when only value is given", async () => { + render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "item-1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child value when value and label are given", async () => { + render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child value when value and child content are given", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child value when value, label, and child content are given", async () => { + render( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child label when only label is given", async () => { + render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child label when value and label are given", async () => { + render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child label when label and child content are given", async () => { + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("renders an active item as passed to the parent by child label when value, label, and child content are given", async () => { + render( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + }) + + // Test re-rendering / updating the activeItem: + + test("re-renders the activeItem when passed by child content when only child content is given", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child content when child content and value are given", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child content when child content and child label are given", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child content when child content, value, and label are given", async () => { + const { rerender } = render( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child value when only child value is given", async () => { + const { rerender } = render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "item-1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "item-1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child value when child value and label are given", async () => { + const { rerender } = render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "item-1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "item-1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "item-3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "item-3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "item-3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child value when child value and child content are given", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child value when child value, label, and child content are given", async () => { + const { rerender } = render( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child label when only child label is given", async () => { + const { rerender } = render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).toHaveAttribute("aria-selected", "true") + expect(screen.getByRole("button", { name: "Item 2 Label" })).toHaveClass( + "juno-navigation-item-active" + ) + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + rerender( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).toHaveAttribute("aria-selected", "true") + expect(screen.getByRole("button", { name: "Item 3 Label" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child label when child label and child content are given", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("re-renders the activeItem when passed by child label when child label and child value are given", async () => { + const { rerender } = render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).toHaveAttribute("aria-selected", "true") + expect(screen.getByRole("button", { name: "Item 2 Label" })).toHaveClass( + "juno-navigation-item-active" + ) + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + rerender( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 1 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).not.toHaveAttribute("aria-selected", "true") + expect( + screen.getByRole("button", { name: "Item 2 Label" }) + ).not.toHaveClass("juno-navigation-item-active") + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).toBeInTheDocument() + expect( + screen.getByRole("button", { name: "Item 3 Label" }) + ).toHaveAttribute("aria-selected", "true") + expect(screen.getByRole("button", { name: "Item 3 Label" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + // + test("re-renders the activeItem when passed by child label when child label, child value, and child content are given", async () => { + const { rerender } = render( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( + "juno-navigation-item-active" + ) + rerender( + + + Item 1 + + + Item 2 + + + Item 3 + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(3) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 3" })).toHaveClass( + "juno-navigation-item-active" + ) + }) + + test("executes an onActiveItemChange handler when the user clicks an item and the active item changes", async () => { + // Use a callback to change a variable so we can double-check whether this was executed across context-/component borders: + let callbackWasExecuted = 0 + const onActiveItemChangeCallback = () => { + callbackWasExecuted = 1 + } + render( + + + + + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + const user = userEvent.setup() + const itemToClick = screen.getByRole("button", { name: "Item 2" }) + waitFor(() => { + user.click(itemToClick) + expect(onActiveItemChangeCallback).toHaveBeenCalled() + expect(callbackWasExecuted).toBe(1) + }) + }) + + test("executes an onChange handler when the user clicks an item", async () => { + // Use a callback to change a variable so we can double-check whether this was executed across context-/component borders: + let clickCallbackWasExecuted = 0 + const onChangeCallback = () => { + clickCallbackWasExecuted = 1 + } + render( + + Item 1 + Item 2 + Item 3 + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + const user = userEvent.setup() + const itemToClick = screen.getByRole("button", { name: "Item 2" }) + waitFor(() => { + user.click(itemToClick) + expect(onChangeCallback).toHaveBeenCalled() + expect(clickCallbackWasExecuted).toBe(1) + }) + }) + + test("executes an onChange handler when the active item was changed programmatically", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(mockOnChange).not.toHaveBeenCalled() + waitFor(() => { + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(mockOnChange).toHaveBeenCalled() + }) + }) + + test("executes an onActiveItemChange handler when the active item was changed programmatically", async () => { + const { rerender } = render( + + Item 1 + Item 2 + Item 3 + + ) + expect(mockOnChange).not.toHaveBeenCalled() + waitFor(() => { + rerender( + + Item 1 + Item 2 + Item 3 + + ) + expect(mockOnActiveItemChange).toHaveBeenCalled() + }) + }) + + test("renders custom classNames as passed", async () => { + render() + expect(screen.getByRole("navigation")).toHaveClass("my-custom-class") + }) + + test("renders all props as passed", async () => { + render() + expect(screen.getByRole("navigation")).toHaveAttribute( + "data-lol", + "Prop goes here" + ) + }) +}) diff --git a/apps/greenhouse-management/__mocks__/styleMock.js b/libs/juno-ui-components/src/components/Navigation/index.js similarity index 72% rename from apps/greenhouse-management/__mocks__/styleMock.js rename to libs/juno-ui-components/src/components/Navigation/index.js index d74516001..0ed30bd3f 100644 --- a/apps/greenhouse-management/__mocks__/styleMock.js +++ b/libs/juno-ui-components/src/components/Navigation/index.js @@ -3,4 +3,4 @@ * SPDX-License-Identifier: Apache-2.0 */ -module.exports = {} +export { Navigation } from "./Navigation.component" diff --git a/libs/juno-ui-components/src/components/NavigationItem/NavigationItem.component.js b/libs/juno-ui-components/src/components/NavigationItem/NavigationItem.component.js new file mode 100644 index 000000000..8b40794cc --- /dev/null +++ b/libs/juno-ui-components/src/components/NavigationItem/NavigationItem.component.js @@ -0,0 +1,216 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useContext, useEffect, useState } from "react" +import PropTypes from "prop-types" +import { NavigationContext } from "../Navigation/Navigation.component" +import { Icon } from "../Icon/index.js" +import { knownIcons } from "../Icon/Icon.component.js" + +const itemStyles = ` + jn-flex + jn-items-center +` + +const disabledStyles = ` + jn-opacity-50 + jn-cursor-not-allowed +` + +/** A generic Navigation Item component. For internal use only. Use to wrap more semantic, role-specific navigation item components such as `SidenavigationItem`, `TabNavigationItem` , `TopNavigationItem` around. */ +export const NavigationItem = ({ + active, + activeItemStyles, + ariaLabel, + children, + className, + disabled, + icon, + inactiveItemStyles, + label, + href, + onClick, + value, + wrapperClassName, + ...props +}) => { + const navigationContext = useContext(NavigationContext) + + // Create a unique Identifier to a) identify the active item with the parent, b) as a key in the map of items with the parent, and c) to be returned by interested event handlers. + const theKey = value || children || label + + const { + activeItem: activeItem, + addItem: addItem, + handleActiveItemChange: handleActiveItemChange, + navigationDisabled: navigationDisabled, + navigationRole: navigationRole, + } = navigationContext || {} + + // Determine whether the item is initially set to active via the parent navigation component or by its own devices: + const initialActive = () => { + if (navigationContext?.activeItem?.length > 0) { + return activeItem === theKey + } else { + return active + } + } + + const [isActive, setIsActive] = useState(() => initialActive()) + + useEffect(() => { + // only add the item to the parent if we are in a context and addItem method exists: + addItem ? addItem(theKey, children, label, value) : undefined + }, [children, label, value]) + + useEffect(() => { + if (activeItem) { + activeItem === theKey ? setIsActive(true) : setIsActive(false) + return + } + setIsActive(active) + }, [activeItem, active]) + + const handleClick = (event) => { + if (disabled) { + event.preventDefault() + } else { + if ( + !isActive && + handleActiveItemChange && + typeof handleActiveItemChange === "function" + ) { + handleActiveItemChange(theKey) + } + onClick && onClick(event) + } + } + + return ( +
  • + {href && href.length ? ( + + {icon ? : ""} + {children || label || value} + + ) : ( + + )} +
  • + ) +} + +NavigationItem.propTypes = { + /** Whether the navigation item is the currently active item. If an acitve item is set on the parent, the one on the parent will win. */ + active: PropTypes.bool, + /** Styles to apply to the active item*/ + activeItemStyles: PropTypes.string, + /** The aria-label of the item */ + ariaLabel: PropTypes.string, + /** Pass custom classNames to the item itself. */ + className: PropTypes.string, + /** The child string of the item. Will override `label` when passed. */ + children: PropTypes.string, + /** Whether the item is disabled */ + disabled: PropTypes.bool, + /** An icon to render in the item */ + icon: PropTypes.oneOf(knownIcons), + /* Pass styles that apply to IN-active items only, in the event activeStyles are overwritten by defaultStyles affecting the same CSS property*/ + inactiveItemStyles: PropTypes.string, + /** The label of the item. Will be rendered if no children are passed */ + label: PropTypes.string, + /** The href of the item. The item will be rendered as an `` element when passed, instead of a ` - )} - + + {children} + ) } diff --git a/libs/juno-ui-components/src/components/SideNavigationItem/SideNavigationItem.test.js b/libs/juno-ui-components/src/components/SideNavigationItem/SideNavigationItem.test.js index f6fc8185a..7fa6d175b 100644 --- a/libs/juno-ui-components/src/components/SideNavigationItem/SideNavigationItem.test.js +++ b/libs/juno-ui-components/src/components/SideNavigationItem/SideNavigationItem.test.js @@ -83,23 +83,23 @@ describe("SideNavigationItem", () => { expect(screen.getByRole("button")).toHaveClass("juno-sidenavigation-item") }) - test("renders an active ToppNavigationItem as passed", async () => { + test("renders an active NavigationItem as passed", async () => { render() expect(screen.getByRole("button")).toBeInTheDocument() expect(screen.getByRole("button")).toHaveClass("juno-sidenavigation-item") expect(screen.getByRole("button")).toHaveClass( - "juno-sidenavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button")).toHaveAttribute("aria-selected", "true") }) - test("rerenders the active attribute of the ToppNavigationItem", async () => { + test("rerenders the active attribute of a navigation item", async () => { const { rerender } = render( ) expect(screen.getByRole("button")).toBeInTheDocument() expect(screen.getByRole("button")).toHaveClass( - "juno-sidenavigation-item-active" + "juno-navigation-item-active" ) rerender() expect(screen.getByRole("button")).toBeInTheDocument() diff --git a/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.component.js b/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.component.js index 62cce32eb..f835954d6 100644 --- a/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.component.js +++ b/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.component.js @@ -3,15 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, useState, useEffect, useContext } from "react" +import React, { createContext } from "react" import PropTypes from "prop-types" - +import { Navigation } from "../Navigation/index" const tabNavStyles = ` jn-flex ` -export const NavigationContext = createContext() +export const TabNavigationContext = createContext() /** A Tab Navigation parent component. Use to wrap `` elements inside. For tabs with corresponding tab panels, use `` instead. */ export const TabNavigation = ({ @@ -24,51 +24,29 @@ export const TabNavigation = ({ tabStyle, ...props }) => { - - const [ activeItm, setActiveItm ] = useState("") - - // Update state whenever activeItem prop on parent changes: - useEffect(() => { - if (activeItem) { - setActiveItm(activeItem) - } - }, [activeItem]) - - // Callback to pass to the child tab navigation items to set the state on the parent. This is used only once when initializing to prevent any onChange handlers to run: - const updateActiveItem = (label) => { - setActiveItm(label) - } - - // Callback to pass to child tab navigation items to execute whenever they change: - const handleActiveItemChange = (label) => { - setActiveItm(label) - onActiveItemChange && onActiveItemChange(label) - } - return ( - -
      - { children } -
    -
    + {children} +
    + ) } @@ -97,4 +75,4 @@ TabNavigation.defaultProps = { disabled: false, onActiveItemChange: undefined, tabStyle: "main", -} \ No newline at end of file +} diff --git a/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.stories.js b/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.stories.js index 2e7df1eea..af120ff6c 100644 --- a/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.stories.js +++ b/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.stories.js @@ -3,12 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react' -import { TabNavigation } from './index.js' -import { TabNavigationItem } from '../TabNavigationItem/index.js' +import React from "react" +import { TabNavigation } from "./index.js" +import { TabNavigationItem } from "../TabNavigationItem/index.js" export default { - title: 'Navigation/TabNavigation/TabNavigation', + title: "Navigation/TabNavigation/TabNavigation", component: TabNavigation, argTypes: { children: { @@ -18,79 +18,111 @@ export default { control: false, }, tabStyle: { - options: ['main', 'content'], - control: { type: 'radio' }, + options: ["main", "content"], + control: { type: "radio" }, }, }, -}; +} -const Template = ({ children, ...args}) => ( - - { children } - +const Template = ({ children, ...args }) => ( + {children} ) export const Default = Template.bind({}) Default.args = { children: [ - , - , - , - - ] + , + , + , + , + ], } export const Disabled = Template.bind({}) -Disabled.parameters = { +;(Disabled.parameters = { docs: { description: { - story: "All navigation items can be disabled by passing `disabled` to the `TabNavigation`." - } - } -}, -Disabled.args = { - disabled: true, - children: - [ + story: + "All navigation items can be disabled by passing `disabled` to the `TabNavigation`.", + }, + }, +}), + (Disabled.args = { + disabled: true, + children: [ , , , - - ] -} + , + ], + }) export const WithValues = Template.bind({}) -WithValues.parameters = { +;(WithValues.parameters = { docs: { description: { - story: "When needed, navigation items can take a `value` prop as a technical identifier that is different form the human-readable `label`. When using `value` on the navigation items, the respective `value`must be used when setting the `activeItem` prop on the TabNavigation. Alternatively, an individual `TabNavigationItem` can be set to `active`." - } - } -}, -WithValues.args = { - activeItem: "item-3", - children: [ - , - , - , - - ] -} + story: + "When needed, navigation items can take a `value` prop as a technical identifier that is different form the human-readable `label`. You may use any of the provided props as an identifier to set an active item on the parent. Alternatively, an individual `SideNavigationItem` can be set to `active`. When both an individual item is set to active and an aciveItem is set on the parent, the latter will win.", + }, + }, +}), + (WithValues.args = { + activeItem: "item-3", + children: [ + , + , + , + , + ], + }) export const WithChildren = Template.bind({}) WithChildren.parameters = { docs: { description: { - story: "Alternatively, navigation items can render children passed to them. In order to get a working, self-managing navigation, each item must have a `value` or `label` prop." - } - } + story: + "Alternatively, navigation items can render children passed to them.", + }, + }, } WithChildren.args = { activeItem: "item-1", children: [ - Item 1, - Item 2, - Item 3, - Item 4 - ] + + Item 1 + , + + Item 2 + , + + Item 3 + , + + Item 4 + , + ], } diff --git a/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.test.js b/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.test.js index 90d1b1d27..81bd6a706 100644 --- a/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.test.js +++ b/libs/juno-ui-components/src/components/TabNavigation/TabNavigation.test.js @@ -80,12 +80,11 @@ describe("TabNavigation", () => { expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() - expect(screen.getByRole("button", { name: "Item 1" })).toHaveAttribute( - "aria-selected", - "false" + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected" ) expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( @@ -93,7 +92,7 @@ describe("TabNavigation", () => { "true" ) expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) }) @@ -107,12 +106,11 @@ describe("TabNavigation", () => { expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() - expect(screen.getByRole("button", { name: "Item 1" })).toHaveAttribute( - "aria-selected", - "false" + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected" ) expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( @@ -120,7 +118,7 @@ describe("TabNavigation", () => { "true" ) expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) }) @@ -134,9 +132,8 @@ describe("TabNavigation", () => { expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() - expect(screen.getByRole("button", { name: "Item 1" })).toHaveAttribute( - "aria-selected", - "false" + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected" ) expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( @@ -154,10 +151,10 @@ describe("TabNavigation", () => { ) expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) rerender( @@ -166,10 +163,10 @@ describe("TabNavigation", () => { ) expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 2" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) }) @@ -182,13 +179,13 @@ describe("TabNavigation", () => { ) expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) rerender( @@ -198,13 +195,13 @@ describe("TabNavigation", () => { ) expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button", { name: "Item 3" })).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) }) @@ -220,9 +217,9 @@ describe("TabNavigation", () => { const tab1 = screen.getByRole("button", { name: "Item 1" }) const tab2 = screen.getByRole("button", { name: "Item 2" }) expect(tab1).toHaveAttribute("aria-selected", "true") - expect(tab2).toHaveAttribute("aria-selected", "false") + expect(tab2).not.toHaveAttribute("aria-selected") await userEvent.click(tab2) - expect(tab1).toHaveAttribute("aria-selected", "false") + expect(tab1).not.toHaveAttribute("aria-selected") expect(tab2).toHaveAttribute("aria-selected", "true") }) diff --git a/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.component.js b/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.component.js index a20e77206..6dc515948 100644 --- a/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.component.js +++ b/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.component.js @@ -3,41 +3,28 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useState, useEffect, useContext } from "react" +import React, { useContext } from "react" import PropTypes from "prop-types" -import { NavigationContext } from "../TabNavigation/TabNavigation.component" -import { Icon } from "../Icon/index.js" +import { NavigationItem } from "../NavigationItem/index" +import { TabNavigationContext } from "../TabNavigation/TabNavigation.component" import { knownIcons } from "../Icon/Icon.component.js" -const itemStyles = ` +const tabNavItemStyles = ` jn-flex jn-items-center jn-text-theme-default jn-font-bold jn-py-[0.875rem] jn-px-[1.5625rem] + jn-border-b-[3px] focus-visible:jn-outline-none focus-visible:jn-ring-2 focus-visible:jn-ring-theme-focus ` -const defaultMainItemStyles = ` - jn-border-b-[3px] - jn-border-transparent -` - -const defaultContentItemStyles = ` - jn-border-b-[3px] - jn-border-theme-tab-content-inactive-bottom -` - -const disabledItemStyles = ` - jn-pointer-events-none - jn-opacity-50 - jn-cursor-not-allowed -` -const activeItemStyles = ` +const tabNavActiveItemStyles = ` jn-text-theme-high + jn-font-bold jn-border-b-[3px] jn-border-theme-tab-active-bottom ` @@ -56,112 +43,34 @@ export const TabNavigationItem = ({ value, ...props }) => { - const navigationContext = useContext(NavigationContext) - const { - activeItem: activeItem, - updateActiveItem: updateActiveItem, - handleActiveItemChange: handleActiveItemChange, - disabled: groupDisabled, - tabStyle: tabStyle, - } = navigationContext || {} - - // Use the value (if passed) or the label as identifying key or the tab: - const theKey = value || label - - // Lazily init depending on parent context or tab's own prop: - const initialActive = () => { - if (navigationContext?.activeItem?.length > 0) { - return activeItem === theKey - } else { - return active - } - } - - const [isActive, setIsActive] = useState(() => initialActive()) - - useEffect(() => { - if (activeItem) { - activeItem === theKey ? setIsActive(true) : setIsActive(false) - return - } - setIsActive(active) - }, [activeItem, active]) - - const handleItemClick = (event) => { - if (!isActive) { - handleActiveItemChange(theKey) - } - onClick && onClick(event) - } - + const tabNavigationContext = useContext(TabNavigationContext) + const { tabStyle: tabStyle } = tabNavigationContext || {} return ( -
  • - {href ? ( - - {icon ? : null} - {children || label || theKey} - - ) : ( - - )} -
  • + + {children} + ) } diff --git a/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.stories.js b/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.stories.js index 02d81eeb9..259b319ac 100644 --- a/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.stories.js +++ b/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.stories.js @@ -3,63 +3,63 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from 'react'; +import React from "react" import { TabNavigation } from "../TabNavigation/index" -import { TabNavigationItem } from './index.js' +import { TabNavigationItem } from "./index.js" import { knownIcons } from "../Icon/Icon.component.js" export default { - title: 'Navigation/TabNavigation/TabNavigationItem', + title: "Navigation/TabNavigation/TabNavigationItem", component: TabNavigationItem, argTypes: { icon: { options: [null, ...knownIcons], - control: { type: 'select' }, + control: { type: "select" }, }, onClick: { control: false, - } + }, }, decorators: [(story) => {story()}], -}; +} export const Default = { args: { label: "Tab 1", - } + }, } export const Active = { args: { label: "Active TabNavigationItem", active: true, - } + }, } export const Disabled = { args: { label: "Disabled TabNavigationItem", disabled: true, - } + }, } export const WithIcon = { args: { icon: "warning", label: "With Icon", - } + }, } export const AsLink = { args: { label: "Item as Link", href: "https://www.sap.com", - } + }, } export const WithChildren = { args: { value: "itm-1", - children: "Item 1" - } -} \ No newline at end of file + children: "Item 1", + }, +} diff --git a/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.test.js b/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.test.js index d2e15a90f..a91fd303f 100644 --- a/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.test.js +++ b/libs/juno-ui-components/src/components/TabNavigationItem/TabNavigationItem.test.js @@ -73,7 +73,7 @@ describe("TabNavigationItem", () => { expect(screen.getByRole("button")).toBeInTheDocument() expect(screen.getByRole("button")).toHaveClass("juno-tabnavigation-item") expect(screen.getByRole("button")).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) expect(screen.getByRole("button")).toHaveAttribute("aria-selected", "true") }) @@ -84,12 +84,12 @@ describe("TabNavigationItem", () => { ) expect(screen.getByRole("button")).toBeInTheDocument() expect(screen.getByRole("button")).toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) rerender() expect(screen.getByRole("button")).toBeInTheDocument() expect(screen.getByRole("button")).not.toHaveClass( - "juno-tabnavigation-item-active" + "juno-navigation-item-active" ) }) @@ -112,7 +112,7 @@ describe("TabNavigationItem", () => { ) expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass( - "juno-tabnavigation-item-main" + "juno-tabnavigation-main-item" ) }) @@ -124,7 +124,7 @@ describe("TabNavigationItem", () => { ) expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() expect(screen.getByRole("button", { name: "Item 1" })).toHaveClass( - "juno-tabnavigation-item-content" + "juno-tabnavigation-content-item" ) }) diff --git a/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.component.js b/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.component.js index 16f12c261..76cf065a7 100644 --- a/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.component.js +++ b/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.component.js @@ -3,10 +3,11 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { createContext, useEffect, useState } from "react"; -import PropTypes from "prop-types"; +import React from "react" +import PropTypes from "prop-types" +import { Navigation } from "../Navigation/index" -const topNavigationStyles = ` +const topNavStyles = ` jn-flex jn-bg-theme-global-bg jn-gap-6 @@ -14,8 +15,6 @@ const topNavigationStyles = ` jn-py-1.5 ` -export const NavigationContext = createContext() - /** A generic horizontal top level navigation component. To be placed below the application header but above application content. Place `TopNavigationItem` elements as children. @@ -29,45 +28,18 @@ export const TopNavigation = ({ onActiveItemChange, ...props }) => { - - const [activeItm, setActiveItm] = useState("") - - useEffect(() => { - if (activeItem) { - setActiveItm(activeItem) - } - }, [activeItem]) - - const updateActiveItem = (label) => { - setActiveItm(label) - } - - const handleActiveItemChange = (label) => { - setActiveItm(label) - onActiveItemChange && onActiveItemChange(label) - } - return ( - -
      - { children } -
    -
    + + {children} + ) } @@ -93,4 +65,4 @@ TopNavigation.defaultProps = { className: "", disabled: false, onActiveItemChange: undefined, -} \ No newline at end of file +} diff --git a/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.stories.js b/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.stories.js index 0df6a7cf0..251ccd937 100644 --- a/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.stories.js +++ b/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.stories.js @@ -13,19 +13,17 @@ export default { argTypes: { items: { table: { - disable: true - } + disable: true, + }, }, children: { - control: false + control: false, }, - } + }, } -const Template = ({children, ...args}) => ( - - { children } - +const Template = ({ children, ...args }) => ( + {children} ) export const Default = Template.bind({}) @@ -34,61 +32,71 @@ Default.args = { , , , - - ] + , + ], } export const Disabled = Template.bind({}) -Disabled.parameters = { +;(Disabled.parameters = { docs: { description: { - story: "All navigation items can be disabled by passing `disabled` to the `TabNavigation`." - } - } -}, -Disabled.args = { - disabled: true, - children: - [ + story: + "All navigation items can be disabled by passing `disabled` to the `TabNavigation`.", + }, + }, +}), + (Disabled.args = { + disabled: true, + children: [ , , , - - ] -} + , + ], + }) export const WithValues = Template.bind({}) WithValues.parameters = { docs: { description: { - story: "When needed, navigation items can take a `value` prop as a technical identifier that is different form the human-readable `label`. When using `value` on the navigation items, the respective `value`must be used when setting the `activeItem` prop on the TopNavigation. Alternatively, an individual `TopNavigationItem` can be set to `active`." - } - } + story: + "When needed, navigation items can take a `value` prop as a technical identifier that is different form the human-readable `label`. You may use any of the provided props as an identifier to set an active item on the parent. Alternatively, an individual `SideNavigationItem` can be set to `active`. When both an individual item is set to active and an aciveItem is set on the parent, the latter will win.", + }, + }, } WithValues.args = { activeItem: "i-3", children: [ - , - , - , - - ] + , + , + , + , + ], } export const WithChildren = Template.bind({}) WithChildren.parameters = { docs: { description: { - story: "Alternatively, navigation items can render children passed to them. In order to get a working, self-managing navigation, each item must have a `value` or `label` prop." - } - } + story: + "Alternatively, navigation items can render children passed to them.", + }, + }, } WithChildren.args = { activeItem: "item-1", children: [ - Item 1, - Item 2, - Item 3, - Item 4 - ] -} \ No newline at end of file + + Item 1 + , + + Item 2 + , + + Item 3 + , + + Item 4 + , + ], +} diff --git a/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.test.js b/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.test.js index 9ab6eec9c..732dc9526 100644 --- a/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.test.js +++ b/libs/juno-ui-components/src/components/TopNavigation/TopNavigation.test.js @@ -3,27 +3,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as React from 'react'; -import { cleanup, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TopNavigation } from './index'; -import { TopNavigationItem } from '../TopNavigationItem/index'; +import * as React from "react" +import { cleanup, render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { TopNavigation } from "./index" +import { TopNavigationItem } from "../TopNavigationItem/index" const mockOnActiveItemChange = jest.fn() -describe('TopNavigation', () => { - +describe("TopNavigation", () => { afterEach(() => { - cleanup(); - jest.clearAllMocks(); + cleanup() + jest.clearAllMocks() }) - - test('render a TopNavigation', async () => { - render(); - expect(screen.getByRole('navigation')).toBeInTheDocument(); - expect(screen.getByRole('navigation')).toHaveClass("juno-topnavigation"); + + test("render a TopNavigation", async () => { + render() + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.getByRole("navigation")).toHaveClass("juno-topnavigation") }) - + test("renders children as passed", async () => { render( @@ -34,51 +33,70 @@ describe('TopNavigation', () => { ) expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(3) - expect(screen.getByRole("button", {name: "Item 1"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 2"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 3"})).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 3" })).toBeInTheDocument() }) - + test("renders an aria-label as passed", async () => { render() expect(screen.getByRole("navigation")).toBeInTheDocument() - expect(screen.getByRole("navigation")).toHaveAttribute("aria-label", "describe the navigation") + expect(screen.getByRole("navigation")).toHaveAttribute( + "aria-label", + "describe the navigation" + ) }) - + test("renders disabled children as passed", async () => { render( - ) - expect(screen.getByRole("navigation")).toBeInTheDocument() - expect(screen.queryAllByRole("button")).toHaveLength(2) - expect(screen.getByRole("button", {name: "Item 1"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 1"})).toBeDisabled() - expect(screen.getByRole("button", {name: "Item 1"})).toHaveAttribute("aria-disabled", "true") - expect(screen.getByRole("button", {name: "Item 2"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 2"})).toBeDisabled() - expect(screen.getByRole("button", {name: "Item 2"})).toHaveAttribute("aria-disabled", "true") + + ) + expect(screen.getByRole("navigation")).toBeInTheDocument() + expect(screen.queryAllByRole("button")).toHaveLength(2) + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).toBeDisabled() + expect(screen.getByRole("button", { name: "Item 1" })).toHaveAttribute( + "aria-disabled", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toBeDisabled() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-disabled", + "true" + ) }) - + test("renders an active navigation item as passed", async () => { render( - ) + ) expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) - expect(screen.getByRole("button", {name: "Item 1"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 1"})).toHaveAttribute("aria-selected", "false") - expect(screen.getByRole("button", {name: "Item 1"})).not.toHaveClass("juno-topnavigation-item-active") - expect(screen.getByRole("button", {name: "Item 2"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 2"})).toHaveAttribute("aria-selected", "true") - expect(screen.getByRole("button", {name: "Item 2"})).toHaveClass("juno-topnavigation-item-active") + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) }) - - test("renders an active navigaiton item as passed by value", async () => { + + test("renders an active navigation item as passed by value", async () => { render( @@ -87,31 +105,48 @@ describe('TopNavigation', () => { ) expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) - expect(screen.getByRole("button", {name: "Item 1"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 1"})).toHaveAttribute("aria-selected", "false") - expect(screen.getByRole("button", {name: "Item 1"})).not.toHaveClass("juno-topnavigation-item-active") - expect(screen.getByRole("button", {name: "Item 2"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 2"})).toHaveAttribute("aria-selected", "true") - expect(screen.getByRole("button", {name: "Item 2"})).toHaveClass("juno-topnavigation-item-active") + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) }) - + test("renders the active item as passed to the parent if conflicting with active prop passed to child item", async () => { render( - + ) expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) - expect(screen.getByRole("button", {name: "Item 1"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 1"})).toHaveAttribute("aria-selected", "false") - expect(screen.getByRole("button", {name: "Item 1"})).not.toHaveClass("juno-topnavigation-item-active") - expect(screen.getByRole("button", {name: "Item 2"})).toBeInTheDocument() - expect(screen.getByRole("button", {name: "Item 2"})).toHaveAttribute("aria-selected", "true") - expect(screen.getByRole("button", {name: "Item 2"})).toHaveClass("juno-topnavigation-item-active") + expect(screen.getByRole("button", { name: "Item 1" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveAttribute( + "aria-selected" + ) + expect(screen.getByRole("button", { name: "Item 1" })).not.toHaveClass( + "juno-navigation-item-active" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toBeInTheDocument() + expect(screen.getByRole("button", { name: "Item 2" })).toHaveAttribute( + "aria-selected", + "true" + ) + expect(screen.getByRole("button", { name: "Item 2" })).toHaveClass( + "juno-navigation-item-active" + ) }) - + test("changes the active item when the user clicks", async () => { render( @@ -121,40 +156,46 @@ describe('TopNavigation', () => { ) expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) - const tab1 = screen.getByRole("button", {name: "Item 1"}) - const tab2 = screen.getByRole("button", {name: "Item 2"}) + const tab1 = screen.getByRole("button", { name: "Item 1" }) + const tab2 = screen.getByRole("button", { name: "Item 2" }) expect(tab1).toHaveAttribute("aria-selected", "true") - expect(tab1).toHaveClass("juno-topnavigation-item-active") - expect(tab2).toHaveAttribute("aria-selected", "false") - expect(tab2).not.toHaveClass("juno-topnavigation-item-active") + expect(tab1).toHaveClass("juno-navigation-item-active") + expect(tab2).not.toHaveAttribute("aria-selected") + expect(tab2).not.toHaveClass("juno-navigation-item-active") await userEvent.click(tab2) - expect(tab1).toHaveAttribute("aria-selected", "false") - expect(tab1).not.toHaveClass("juno-topnavigation-item-active") + expect(tab1).not.toHaveAttribute("aria-selected") + expect(tab1).not.toHaveClass("juno-navigation-item-active") expect(tab2).toHaveAttribute("aria-selected", "true") - expect(tab2).toHaveClass("juno-topnavigation-item-active") + expect(tab2).toHaveClass("juno-navigation-item-active") }) - + test("executes a handler as passed when the selected item changes", async () => { render( - + - ) + + ) expect(screen.getByRole("navigation")).toBeInTheDocument() expect(screen.queryAllByRole("button")).toHaveLength(2) - const item2 = screen.getByRole("button", {name: "Item 2"}) + const item2 = screen.getByRole("button", { name: "Item 2" }) await userEvent.click(item2) expect(mockOnActiveItemChange).toHaveBeenCalled() }) - test('renders custom classNames as passed', async () => { - render(); - expect(screen.getByRole("navigation")).toHaveClass('my-custom-class'); - }); + test("renders custom classNames as passed", async () => { + render() + expect(screen.getByRole("navigation")).toHaveClass("my-custom-class") + }) - test('renders all props as passed', async () => { - render(); - expect(screen.getByRole('navigation')).toHaveAttribute('data-lol', 'Prop goes here'); - }); - -}); + test("renders all props as passed", async () => { + render() + expect(screen.getByRole("navigation")).toHaveAttribute( + "data-lol", + "Prop goes here" + ) + }) +}) diff --git a/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.component.js b/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.component.js index 4d13476e7..71edf0537 100644 --- a/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.component.js +++ b/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.component.js @@ -3,13 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React, { useContext, useEffect, useState } from "react"; -import PropTypes from "prop-types"; -import { NavigationContext } from "../TopNavigation/TopNavigation.component" -import { Icon } from "../Icon/index.js"; +import React, { useContext, useEffect, useState } from "react" +import PropTypes from "prop-types" +import { NavigationItem } from "../NavigationItem/index" import { knownIcons } from "../Icon/Icon.component.js" -const itemStyles = ` +const topNavItemStyles = ` jn-flex jn-items-center jn-grow-0 @@ -30,16 +29,8 @@ const itemStyles = ` focus-visible:jn-ring-theme-focus ` -const disabledItemStyles = ` - jn-opacity-50 - jn-cursor-not-allowed -` -const nonActiveItemStyles = ` - hover:jn-text-theme-high - hover:jn-bg-transparent -` - -const activeItemStyles = ` +const topNavActiveItemStyles = ` + jn-font-bold jn-text-theme-high jn-bg-theme-topnavigation-item-active ` @@ -60,101 +51,22 @@ export const TopNavigationItem = ({ value, ...props }) => { - - const navigationContext = useContext(NavigationContext) - - const { - activeItem: activeItem, - updateActiveItem: updateActiveItem, - handleActiveItemChange: handleActiveItemChange, - disabled: groupDisabled, - } = navigationContext || {} - - const theKey = value || label - - const initialActive = () => { - if (navigationContext) { - activeItem === theKey ? true : false - } else { - return active - } - } - - const [isActive, setIsActive] = useState( () => initialActive() ) - - // Set the parent state once if not set on the parent, but a navigation item has been set to active via its own prop: - useEffect(() => { - if (active && navigationContext && !activeItem) { - updateActiveItem(theKey) - } - }, []) - - // Update the parent state when in a navigation context, otherwise update item state directly: - useEffect(() => { - if (activeItem) { - activeItem === theKey ? setIsActive(true) : setIsActive(false) - } else { - setIsActive(active) - } - }, [activeItem, active]) - - - const handleItemClick = (event) => { - if (!isActive) { - handleActiveItemChange(theKey) - } - onClick && onClick(event) - } - return ( - - + {children} +
    ) } @@ -177,7 +89,7 @@ TopNavigationItem.propTypes = { href: PropTypes.string, /** A handler to execute once the navigation item is clicked. Will render the item as a button element if passed */ onClick: PropTypes.func, - /** An optional technical identifier fort the tab. If not passed, the label will be used to identify the tab. NOTE: If value is passed, the value of the active tab MUST be used when setting the activeItem prop on the parent TabNavigation.*/ + /** An optional technical identifier fort the tab. If not passed, the label will be used to identify the tab. NOTE: If value is passed, the value of the active tab MUST be used when setting the activeItem prop on the parent TabNavigation.*/ value: PropTypes.string, } @@ -192,4 +104,4 @@ TopNavigationItem.defaultProps = { href: "", onClick: undefined, value: "", -} \ No newline at end of file +} diff --git a/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.test.js b/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.test.js index 2eff2054a..1da5163f7 100644 --- a/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.test.js +++ b/libs/juno-ui-components/src/components/TopNavigationItem/TopNavigationItem.test.js @@ -3,100 +3,127 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as React from 'react'; -import { cleanup, render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { TopNavigation } from '../TopNavigation/index'; -import { TopNavigationItem } from './index'; +import * as React from "react" +import { cleanup, render, screen, waitFor } from "@testing-library/react" +import userEvent from "@testing-library/user-event" +import { TopNavigation } from "../TopNavigation/index" +import { TopNavigationItem } from "./index" const mockOnClick = jest.fn() -describe('TopNavigationItem', () => { - +describe("TopNavigationItem", () => { afterEach(() => { - cleanup(); - jest.clearAllMocks(); + cleanup() + jest.clearAllMocks() }) - - test('renders a ToppNavigationItem', async () => { - render(); - expect(screen.getByTestId('top-nav-item')).toBeInTheDocument(); - expect(screen.getByTestId('top-nav-item')).toHaveClass("juno-topnavigation-item"); - }); - + + test("renders a ToppNavigationItem", async () => { + render() + expect(screen.getByTestId("top-nav-item")).toBeInTheDocument() + expect(screen.getByTestId("top-nav-item")).toHaveClass( + "juno-topnavigation-item" + ) + }) + test("renders a label as passed", async () => { render() - expect(screen.getByRole("button")).toBeInTheDocument(); - expect(screen.getByRole("button")).toHaveTextContent("My Label"); + expect(screen.getByRole("button")).toBeInTheDocument() + expect(screen.getByRole("button")).toHaveTextContent("My Label") }) - + test("renders children as passed", async () => { render(The Item Is A Child) expect(screen.getByRole("button")).toBeInTheDocument() expect(screen.getByRole("button")).toHaveTextContent("The Item Is A Child") }) - + test("redners an aria-label attribute as passed", async () => { render() expect(screen.getByRole("button")).toBeInTheDocument() - expect(screen.getByRole("button")).toHaveAttribute("aria-label", "My ARIA-Label") + expect(screen.getByRole("button")).toHaveAttribute( + "aria-label", + "My ARIA-Label" + ) }) - + test("renders a disabled item as passed", async () => { render() expect(screen.getByRole("button")).toBeInTheDocument() - expect(screen.getByRole("button")).toBeDisabled(); - expect(screen.getByRole("button")).toHaveAttribute("aria-disabled", "true"); + expect(screen.getByRole("button")).toBeDisabled() + expect(screen.getByRole("button")).toHaveAttribute("aria-disabled", "true") }) - + test("renders an icon as passed", async () => { render() - expect(screen.getByRole("img")).toBeInTheDocument(); - expect(screen.getByRole("img")).toHaveAttribute("alt", "warning"); + expect(screen.getByRole("img")).toBeInTheDocument() + expect(screen.getByRole("img")).toHaveAttribute("alt", "warning") }) - + test("renders as a link when a href prop is passed", async () => { - render(); - expect(screen.getByRole("link")).toBeInTheDocument(); - expect(screen.getByRole("link")).toHaveClass("juno-topnavigation-item"); + render() + expect(screen.getByRole("link")).toBeInTheDocument() + expect(screen.getByRole("link")).toHaveClass("juno-topnavigation-item") }) - + test("renders as a button when an onClick prop is passed", async () => { - render({console.log("click")}} />); - expect(screen.getByRole("button")).toBeInTheDocument(); - expect(screen.getByRole("button")).toHaveClass("juno-topnavigation-item"); + render( + { + console.log("click") + }} + /> + ) + expect(screen.getByRole("button")).toBeInTheDocument() + expect(screen.getByRole("button")).toHaveClass("juno-topnavigation-item") + }) + + test("renders an active ToppNavigationItem as passed", async () => { + render() + expect(screen.getByTestId("top-nav-item")).toBeInTheDocument() + expect(screen.getByTestId("top-nav-item")).toHaveClass( + "juno-topnavigation-item" + ) + expect(screen.getByTestId("top-nav-item")).toHaveClass( + "juno-navigation-item-active" + ) }) - - test('renders an active ToppNavigationItem as passed', async () => { - render(); - expect(screen.getByTestId('top-nav-item')).toBeInTheDocument(); - expect(screen.getByTestId('top-nav-item')).toHaveClass("juno-topnavigation-item"); - expect(screen.getByTestId('top-nav-item')).toHaveClass("juno-topnavigation-item-active"); - }); - - test('renders an aria-label as passed', async () => { - render(); - expect(screen.getByRole('link')).toHaveAttribute('aria-label', 'hey nav item!'); - }); - + + test("renders an aria-label as passed", async () => { + render() + expect(screen.getByRole("link")).toHaveAttribute( + "aria-label", + "hey nav item!" + ) + }) + test("executes an onClick handler as passed", async () => { render( - - ) - expect(screen.getByRole("button", {name: "My Item"})).toBeInTheDocument() - await userEvent.click(screen.getByRole("button", {name: "My Item"})) + + + ) + expect(screen.getByRole("button", { name: "My Item" })).toBeInTheDocument() + await userEvent.click(screen.getByRole("button", { name: "My Item" })) expect(mockOnClick).toHaveBeenCalled() }) - test('renders custom classNames as passed', async () => { - render(); - expect(screen.getByTestId('top-nav-item')).toHaveClass('my-custom-class'); - }); - - test('renders all props as passed', async () => { - render(); - expect(screen.getByTestId('top-nav-item')).toHaveAttribute('data-lol', 'Prop goes here'); - }); - -}); \ No newline at end of file + test("renders custom classNames as passed", async () => { + render( + + ) + expect(screen.getByTestId("top-nav-item")).toHaveClass("my-custom-class") + }) + + test("renders all props as passed", async () => { + render( + + ) + expect(screen.getByTestId("top-nav-item")).toHaveAttribute( + "data-lol", + "Prop goes here" + ) + }) +}) diff --git a/libs/messages-provider/package.json b/libs/messages-provider/package.json index 1d1649fd5..53d0557b1 100644 --- a/libs/messages-provider/package.json +++ b/libs/messages-provider/package.json @@ -12,7 +12,7 @@ "module": "build/index.js", "license": "Apache-2.0", "peerDependencies": { - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "react": "18.2.0", "zustand": "4.5.2" }, @@ -28,7 +28,7 @@ "@testing-library/user-event": "^14.4.3", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "prop-types": "15.8.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/libs/url-state-router/package.json b/libs/url-state-router/package.json index 7363cb44e..941fac89f 100644 --- a/libs/url-state-router/package.json +++ b/libs/url-state-router/package.json @@ -14,7 +14,7 @@ "peerDependencies": { "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "prop-types": "^15.8.1" }, "devDependencies": { @@ -39,7 +39,7 @@ "rollup": "^3.4.0", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-delete": "^2.0.0", - "url-state-provider": "*" + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz" }, "scripts": { "test": "jest", diff --git a/libs/utils/README.md b/libs/utils/README.md index 67dfb03f2..6bb18dd2c 100644 --- a/libs/utils/README.md +++ b/libs/utils/README.md @@ -19,7 +19,7 @@ Within juno monorepo ```json "dependencies": { - "utils": "*" + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz" }, ``` diff --git a/package-lock.json b/package-lock.json index b9a1ea49e..7555df944 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,9 +52,9 @@ "github-markdown-css": "^5.1.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "3.3.0", - "messages-provider": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "15.8.1", @@ -66,27 +66,79 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "url-state-router": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "url-state-router": "https://assets.juno.global.cloud.sap/libs/url-state-router@1.0.3/package.tgz", "util": "^0.12.4", - "utils": "*", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "peerDependencies": { "@tanstack/react-query": "4.28.0", "custom-event-polyfill": "1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "3.3.0", - "messages-provider": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", "prop-types": "15.8.1", "react": "18.2.0", "react-dom": "18.2.0", - "url-state-provider": "*", - "url-state-router": "*", - "utils": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "url-state-router": "https://assets.juno.global.cloud.sap/libs/url-state-router@1.0.3/package.tgz", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" } }, + "apps/assets-overview/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, + "apps/assets-overview/node_modules/messages-provider": { + "version": "0.1.12", + "resolved": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "integrity": "sha512-8XVowXV8wxBJ10FBEAO2AGMpdaf6C6UKQCAjJELJnJHYwPDBYOnMa4nOwHCwzurH6rh0QTmbAeFz4HpD8Yglrw==", + "dev": true, + "peerDependencies": { + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "react": "18.2.0", + "zustand": "4.5.2" + } + }, + "apps/assets-overview/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", + "dev": true, + "dependencies": { + "juri": "^1.0.3" + } + }, + "apps/assets-overview/node_modules/url-state-router": { + "version": "1.0.3", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-router@1.0.3/package.tgz", + "integrity": "sha512-U5UZakLojwqPIPu8WCuNclSuRfWG+UNDz/9zTdm3matet6tmnVOBnhwpIlLg2Wt0QZcZ6A4Z0MckHAjdgjgumA==", + "dev": true, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": "18.2.0", + "react-dom": "^18.2.0", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz" + } + }, + "apps/assets-overview/node_modules/utils": { + "version": "1.1.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", + "integrity": "sha512-EJY8lT7jRzwlOmHA4YLKdSA8caLVoR4m8giN0+A6wQ2tUFYkBKRbF+pC6On5557tZyi4M2/IHanY0gpwydH3MA==", + "dev": true, + "peerDependencies": { + "react": "^18.2.0" + } + }, "apps/auth": { "version": "1.0.7", "license": "Apache-2.0", @@ -104,14 +156,14 @@ "autoprefixer": "^10.4.2", "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "custom-event-polyfill": "^1.0.7", "esbuild": "^0.17.12", "interweave": "^13.0.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "oauth": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -126,12 +178,35 @@ "peerDependencies": { "custom-event-polyfill": "^1.0.7", "juno-ui-components": "latest", - "oauth": "*", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0" } }, + "apps/auth/node_modules/communicator": { + "version": "2.2.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", + "integrity": "sha512-DGjFFgbAr8gXGJlnfvxEYegqtY+ZflKj+4i3uLqDv6J58gIAavOQenfps+Db1m3vM570+h9ZG8do/SNMPIbnWQ==", + "dev": true + }, + "apps/auth/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, + "apps/auth/node_modules/oauth": { + "version": "1.2.1", + "resolved": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", + "integrity": "sha512-UekxB4dCX+TdKA+/EUAVRfWyQWSY2MVbKZri/tQSfmhHHFJHFUKoBDKCv+1mIMNhtQKYFxKqDSIQ0m8sYS/Flw==", + "dev": true + }, "apps/dashboard": { "version": "1.0.2", "license": "Apache-2.0", @@ -151,7 +226,7 @@ "babel-plugin-transform-import-meta": "^2.2.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "postcss": "^8.4.21", "postcss-url": "^10.1.3", @@ -167,7 +242,7 @@ }, "peerDependencies": { "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", @@ -175,6 +250,17 @@ "zustand": "^4.1.1" } }, + "apps/dashboard/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, "apps/dashboard/node_modules/luxon": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", @@ -184,54 +270,6 @@ "node": ">=12" } }, - "apps/doop": { - "version": "1.0.1", - "extraneous": true, - "license": "MIT", - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "^4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "zustand": "^4.1.1" - }, - "peerDependencies": { - "@tanstack/react-query": "^4.28.0", - "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "zustand": "^4.1.1" - } - }, "apps/exampleapp": { "version": "1.0.4", "license": "Apache-2.0", @@ -254,10 +292,10 @@ "esbuild": "^0.17.19", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -267,583 +305,79 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", - "utils": "*", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "peerDependencies": { "@tanstack/react-query": "4.28.0", "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" } }, - "apps/exampleapp/node_modules/luxon": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", - "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==", + "apps/exampleapp/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", "dev": true, - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse": { - "version": "0.1.20", - "license": "Apache-2.0", - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tailwindui/react": "^0.1.1", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", - "immer": "^9.0.21", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sapcc-k8sclient": "^1.0.2", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "utils": "*", - "zustand": "4.3.7" - }, - "peerDependencies": { - "juno-ui-components": "*", - "messages-provider": "*", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", - "zustand": "4.3.7" - } - }, - "apps/greenhouse-management": { - "version": "1.1.13", - "license": "MIT", - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "esbuild": "^0.19.5", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sapcc-k8sclient": "^1.0.2", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "utils": "*", - "zustand": "4.3.7" - }, "peerDependencies": { - "@tanstack/react-query": "4.28.0", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "prop-types": "^15.8.1", + "prop-types": "15.8.1", "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", - "zustand": "4.3.7" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/android-arm": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.5.tgz", - "integrity": "sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/android-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz", - "integrity": "sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/android-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.5.tgz", - "integrity": "sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "react-dom": "18.2.0" } }, - "apps/greenhouse-management/node_modules/@esbuild/darwin-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz", - "integrity": "sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw==", - "cpu": [ - "arm64" - ], + "apps/exampleapp/node_modules/luxon": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", + "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], "engines": { "node": ">=12" } }, - "apps/greenhouse-management/node_modules/@esbuild/darwin-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz", - "integrity": "sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA==", - "cpu": [ - "x64" - ], + "apps/exampleapp/node_modules/messages-provider": { + "version": "0.1.12", + "resolved": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "integrity": "sha512-8XVowXV8wxBJ10FBEAO2AGMpdaf6C6UKQCAjJELJnJHYwPDBYOnMa4nOwHCwzurH6rh0QTmbAeFz4HpD8Yglrw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz", - "integrity": "sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/freebsd-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz", - "integrity": "sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-arm": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz", - "integrity": "sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz", - "integrity": "sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-ia32": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz", - "integrity": "sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-loong64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz", - "integrity": "sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-mips64el": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz", - "integrity": "sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-ppc64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz", - "integrity": "sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-riscv64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz", - "integrity": "sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-s390x": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz", - "integrity": "sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/linux-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz", - "integrity": "sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/netbsd-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz", - "integrity": "sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/openbsd-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz", - "integrity": "sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/sunos-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz", - "integrity": "sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/win32-arm64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz", - "integrity": "sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/win32-ia32": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz", - "integrity": "sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/@esbuild/win32-x64": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz", - "integrity": "sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "apps/greenhouse-management/node_modules/esbuild": { - "version": "0.19.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.5.tgz", - "integrity": "sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.19.5", - "@esbuild/android-arm64": "0.19.5", - "@esbuild/android-x64": "0.19.5", - "@esbuild/darwin-arm64": "0.19.5", - "@esbuild/darwin-x64": "0.19.5", - "@esbuild/freebsd-arm64": "0.19.5", - "@esbuild/freebsd-x64": "0.19.5", - "@esbuild/linux-arm": "0.19.5", - "@esbuild/linux-arm64": "0.19.5", - "@esbuild/linux-ia32": "0.19.5", - "@esbuild/linux-loong64": "0.19.5", - "@esbuild/linux-mips64el": "0.19.5", - "@esbuild/linux-ppc64": "0.19.5", - "@esbuild/linux-riscv64": "0.19.5", - "@esbuild/linux-s390x": "0.19.5", - "@esbuild/linux-x64": "0.19.5", - "@esbuild/netbsd-x64": "0.19.5", - "@esbuild/openbsd-x64": "0.19.5", - "@esbuild/sunos-x64": "0.19.5", - "@esbuild/win32-arm64": "0.19.5", - "@esbuild/win32-ia32": "0.19.5", - "@esbuild/win32-x64": "0.19.5" - } - }, - "apps/greenhouse-management/node_modules/luxon": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", - "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "apps/heureka": { - "version": "2.0.4", - "license": "Apache-2.0", - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", - "graphql": "*", - "graphql-request": "^6.0.0", - "immer": "^9.0.21", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "zustand": "4.3.7" - }, "peerDependencies": { - "@tanstack/react-query": "4.28.0", "juno-ui-components": "*", - "messages-provider": "*", - "prop-types": "^15.8.1", "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "zustand": "4.3.7" + "zustand": "4.5.2" + } + }, + "apps/exampleapp/node_modules/oauth": { + "version": "1.2.1", + "resolved": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", + "integrity": "sha512-UekxB4dCX+TdKA+/EUAVRfWyQWSY2MVbKZri/tQSfmhHHFJHFUKoBDKCv+1mIMNhtQKYFxKqDSIQ0m8sYS/Flw==", + "dev": true + }, + "apps/exampleapp/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", + "dev": true, + "dependencies": { + "juri": "^1.0.3" + } + }, + "apps/exampleapp/node_modules/utils": { + "version": "1.1.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", + "integrity": "sha512-EJY8lT7jRzwlOmHA4YLKdSA8caLVoR4m8giN0+A6wQ2tUFYkBKRbF+pC6On5557tZyi4M2/IHanY0gpwydH3MA==", + "dev": true, + "peerDependencies": { + "react": "^18.2.0" } }, "apps/playground": { @@ -868,7 +402,7 @@ "esbuild": "^0.19.5", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -879,16 +413,16 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", "zustand": "4.3.7" }, "peerDependencies": { - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "18.2.0", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "zustand": "4.3.7" } }, @@ -1282,65 +816,24 @@ "@esbuild/win32-x64": "0.19.12" } }, - "apps/supernova": { - "version": "0.9.11", - "license": "Apache-2.0", - "devDependencies": { - "@babel/core": "^7.20.2", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", - "esbuild": "^0.17.11", - "esbuild-sass-plugin": "^2.6.0", - "immer": "^9.0.21", - "interweave": "^13.0.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "zustand": "4.3.7" - }, + "apps/playground/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, "peerDependencies": { - "@tanstack/react-query": "4.28.0", - "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "messages-provider": "*", - "prop-types": "^15.8.1", + "prop-types": "15.8.1", "react": "18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "zustand": "4.3.7" + "react-dom": "18.2.0" } }, - "apps/supernova/node_modules/luxon": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", - "integrity": "sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA==", + "apps/playground/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", "dev": true, - "engines": { - "node": ">=12" + "dependencies": { + "juri": "^1.0.3" } }, "apps/template": { @@ -1364,7 +857,7 @@ "esbuild": "^0.19.5", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "postcss": "^8.4.21", "postcss-url": "^10.1.3", @@ -1376,18 +869,18 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", "zustand": "4.5.2" }, "peerDependencies": { "@tanstack/react-query": "4.28.0", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "zustand": "4.5.2" } }, @@ -1780,6 +1273,17 @@ "@esbuild/win32-x64": "0.19.5" } }, + "apps/template/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, "apps/template/node_modules/luxon": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", @@ -1789,6 +1293,15 @@ "node": ">=12" } }, + "apps/template/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", + "dev": true, + "dependencies": { + "juri": "^1.0.3" + } + }, "apps/template/node_modules/zustand": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", @@ -1817,56 +1330,6 @@ } } }, - "apps/test1": { - "version": "1.0.1", - "extraneous": true, - "license": "MIT", - "dependencies": { - "postcss-url": "^10.1.3" - }, - "devDependencies": { - "@babel/core": "^7.20.2", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@svgr/core": "^7.0.0", - "@svgr/plugin-jsx": "^7.0.0", - "@tanstack/react-query": "4.28.0", - "@testing-library/dom": "^8.19.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "assert": "^2.0.0", - "autoprefixer": "^10.4.2", - "babel-jest": "^29.3.1", - "babel-plugin-transform-import-meta": "^2.2.0", - "jest": "^29.3.1", - "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "postcss": "^8.4.21", - "postcss-url": "^10.1.3", - "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-test-renderer": "^18.2.0", - "sass": "^1.60.0", - "shadow-dom-testing-library": "^1.7.1", - "tailwindcss": "^3.3.1", - "url-state-provider": "*", - "util": "^0.12.4", - "zustand": "4.3.7" - }, - "peerDependencies": { - "@tanstack/react-query": "4.28.0", - "juno-ui-components": "*", - "luxon": "^2.3.0", - "prop-types": "^15.8.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "url-state-provider": "*", - "zustand": "4.3.7" - } - }, "apps/user-activity": { "version": "1.0.2", "license": "Apache-2.0", @@ -1884,7 +1347,7 @@ "autoprefixer": "^10.4.2", "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", "postcss": "^8.4.21", @@ -1905,6 +1368,12 @@ "react-dom": "^18.2.0" } }, + "apps/user-activity/node_modules/communicator": { + "version": "2.2.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", + "integrity": "sha512-DGjFFgbAr8gXGJlnfvxEYegqtY+ZflKj+4i3uLqDv6J58gIAavOQenfps+Db1m3vM570+h9ZG8do/SNMPIbnWQ==", + "dev": true + }, "apps/volta": { "version": "1.0.4", "license": "Apache-2.0", @@ -1924,14 +1393,14 @@ "autoprefixer": "^10.4.2", "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "lodash.uniqueid": "^4.0.1", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "postcss": "^8.4.21", "postcss-url": "^10.1.3", "prop-types": "^15.8.1", @@ -1942,26 +1411,43 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.7.1", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4", - "utils": "*", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" }, "peerDependencies": { "@tanstack/react-query": "4.28.0", "custom-event-polyfill": "^1.0.7", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", - "messages-provider": "*", - "oauth": "*", + "messages-provider": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "oauth": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*", - "utils": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "utils": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", "zustand": "4.3.7" } }, + "apps/volta/node_modules/communicator": { + "version": "2.2.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", + "integrity": "sha512-DGjFFgbAr8gXGJlnfvxEYegqtY+ZflKj+4i3uLqDv6J58gIAavOQenfps+Db1m3vM570+h9ZG8do/SNMPIbnWQ==", + "dev": true + }, + "apps/volta/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, "apps/volta/node_modules/luxon": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz", @@ -1971,6 +1457,41 @@ "node": ">=12" } }, + "apps/volta/node_modules/messages-provider": { + "version": "0.1.12", + "resolved": "https://assets.juno.global.cloud.sap/libs/messages-provider@0.1.12/package.tgz", + "integrity": "sha512-8XVowXV8wxBJ10FBEAO2AGMpdaf6C6UKQCAjJELJnJHYwPDBYOnMa4nOwHCwzurH6rh0QTmbAeFz4HpD8Yglrw==", + "dev": true, + "peerDependencies": { + "juno-ui-components": "*", + "react": "18.2.0", + "zustand": "4.5.2" + } + }, + "apps/volta/node_modules/oauth": { + "version": "1.2.1", + "resolved": "https://assets.juno.global.cloud.sap/libs/oauth@1.2.1/package.tgz", + "integrity": "sha512-UekxB4dCX+TdKA+/EUAVRfWyQWSY2MVbKZri/tQSfmhHHFJHFUKoBDKCv+1mIMNhtQKYFxKqDSIQ0m8sYS/Flw==", + "dev": true + }, + "apps/volta/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", + "dev": true, + "dependencies": { + "juri": "^1.0.3" + } + }, + "apps/volta/node_modules/utils": { + "version": "1.1.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/utils@1.1.6/package.tgz", + "integrity": "sha512-EJY8lT7jRzwlOmHA4YLKdSA8caLVoR4m8giN0+A6wQ2tUFYkBKRbF+pC6On5557tZyi4M2/IHanY0gpwydH3MA==", + "dev": true, + "peerDependencies": { + "react": "^18.2.0" + } + }, "apps/whois": { "version": "3.0.5", "license": "Apache-2.0", @@ -1989,11 +1510,11 @@ "babel-jest": "^29.3.1", "babel-plugin-transform-import-meta": "^2.2.0", "cidr-regex": "^3.1.1", - "communicator": "*", + "communicator": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", "ip-regex": "^5.0.0", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "postcss": "^8.4.21", "postcss-url": "^10.1.3", @@ -2004,16 +1525,33 @@ "sass": "^1.60.0", "shadow-dom-testing-library": "^1.8.0", "tailwindcss": "^3.3.1", - "url-state-provider": "*", + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", "util": "^0.12.4" }, "peerDependencies": { - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "luxon": "^2.3.0", "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*" + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz" + } + }, + "apps/whois/node_modules/communicator": { + "version": "2.2.6", + "resolved": "https://assets.juno.global.cloud.sap/libs/communicator@2.2.6/package.tgz", + "integrity": "sha512-DGjFFgbAr8gXGJlnfvxEYegqtY+ZflKj+4i3uLqDv6J58gIAavOQenfps+Db1m3vM570+h9ZG8do/SNMPIbnWQ==", + "dev": true + }, + "apps/whois/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" } }, "apps/whois/node_modules/luxon": { @@ -2025,6 +1563,15 @@ "node": ">=12" } }, + "apps/whois/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", + "dev": true, + "dependencies": { + "juri": "^1.0.3" + } + }, "apps/widget-loader": { "version": "1.3.3", "license": "Apache-2.0", @@ -2186,7 +1733,7 @@ "@testing-library/user-event": "^14.4.3", "jest": "^29.3.1", "jest-environment-jsdom": "^29.3.1", - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "prop-types": "15.8.1", "react": "18.2.0", "react-dom": "18.2.0", @@ -2197,11 +1744,22 @@ "zustand": "4.5.2" }, "peerDependencies": { - "juno-ui-components": "*", + "juno-ui-components": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", "react": "18.2.0", "zustand": "4.5.2" } }, + "libs/messages-provider/node_modules/juno-ui-components": { + "version": "2.13.8", + "resolved": "https://assets.juno.global.cloud.sap/libs/juno-ui-components@2.13.8/package.tgz", + "integrity": "sha512-ygnXfEt77rshIkpbgvZORO2TCA2ZY//xmkY9NQtA0iazr6M0RbRnKpXycn1RQ6YVjXFfH0xhRcOImKeomrfIjw==", + "dev": true, + "peerDependencies": { + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0" + } + }, "libs/messages-provider/node_modules/rollup": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.4.0.tgz", @@ -2328,13 +1886,13 @@ "rollup": "^3.4.0", "rollup-plugin-analyzer": "^4.0.0", "rollup-plugin-delete": "^2.0.0", - "url-state-provider": "*" + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz" }, "peerDependencies": { "prop-types": "^15.8.1", "react": "18.2.0", "react-dom": "^18.2.0", - "url-state-provider": "*" + "url-state-provider": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz" } }, "libs/url-state-router/node_modules/@rollup/plugin-node-resolve": { @@ -2493,6 +2051,15 @@ "node": ">=8" } }, + "libs/url-state-router/node_modules/url-state-provider": { + "version": "1.3.2", + "resolved": "https://assets.juno.global.cloud.sap/libs/url-state-provider@1.3.2/package.tgz", + "integrity": "sha512-+6ID9hl4YIFiRd4EWy7oZlvFmevBNsIXa8KTZ0+HCj/f48s4NNZKXWooSakXeCmeFCzkcnP/Wv4jYD3RyWFAjg==", + "dev": true, + "dependencies": { + "juri": "^1.0.3" + } + }, "libs/utils": { "version": "1.1.6", "license": "Apache-2.0", @@ -5066,15 +4633,6 @@ "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==", "dev": true }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "dev": true, - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@headlessui-float/react": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/@headlessui-float/react/-/react-0.11.3.tgz", @@ -13460,18 +13018,6 @@ "integrity": "sha512-8QvcEur44BgwBJiCilZ5oVHwFts4ysA29GXCxGRLmCduofeZXWEVIsMjzWzAYXiB35jpsBdKm0NtQnG4YQ+kmA==", "dev": true }, - "node_modules/@tailwindui/react": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@tailwindui/react/-/react-0.1.1.tgz", - "integrity": "sha512-2VoUokHT/EYHaWQjH49gwKqYtwii7snb1SIp93ewfuI9fSy4x6DYocs4v4WNfyiE971ixc/PT+cs6tCi+zabSA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "react": ">=16" - } - }, "node_modules/@tanstack/query-core": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.27.0.tgz", @@ -16534,15 +16080,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", - "dev": true, - "dependencies": { - "node-fetch": "^2.6.12" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -17670,19 +17207,6 @@ "esbuild": ">=0.12 <1" } }, - "node_modules/esbuild-sass-plugin": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-2.8.0.tgz", - "integrity": "sha512-PYDw/r+0lAOBjP4CDjkRRrYDtSta4kO0+p2Ofm4n15hbyhAsxa7hQpY8fY6Ja6VAzzC//VA9F1ki5L99apjLCA==", - "dev": true, - "dependencies": { - "resolve": "^1.22.1", - "sass": "^1.59.3" - }, - "peerDependencies": { - "esbuild": "^0.17.12" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -18819,36 +18343,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphql": { - "version": "16.8.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", - "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/graphql-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-6.1.0.tgz", - "integrity": "sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==", - "dev": true, - "dependencies": { - "@graphql-typed-document-node/core": "^3.2.0", - "cross-fetch": "^3.1.5" - }, - "peerDependencies": { - "graphql": "14 - 16" - } - }, - "node_modules/greenhouse": { - "resolved": "apps/greenhouse", - "link": true - }, - "node_modules/greenhouse-management": { - "resolved": "apps/greenhouse-management", - "link": true - }, "node_modules/gunzip-maybe": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz", @@ -19318,10 +18812,6 @@ "he": "bin/he" } }, - "node_modules/heureka": { - "resolved": "apps/heureka", - "link": true - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -19583,16 +19073,6 @@ "node": ">= 4" } }, - "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, "node_modules/immutable": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", @@ -31362,16 +30842,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/sapcc-k8sclient": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sapcc-k8sclient/-/sapcc-k8sclient-1.0.2.tgz", - "integrity": "sha512-IFmZL0YxT9mP4IEI0p8Z/AUnG0D/NQRne1yo5BdgGrqOYniiUjXVcgxkx19hhJkWKIz2bVB3WJuM+3vPmPxKFA==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.7", - "text-encoding": "^0.7.0" - } - }, "node_modules/sass": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.60.0.tgz", @@ -32083,10 +31553,6 @@ "node": "*" } }, - "node_modules/supernova": { - "resolved": "apps/supernova", - "link": true - }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -32620,13 +32086,6 @@ "node": "*" } }, - "node_modules/text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", - "deprecated": "no longer maintained", - "dev": true - }, "node_modules/theming": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/theming/-/theming-3.3.0.tgz",