diff --git a/components/common/kube.js b/components/common/kube.js index f734858..afa6ee9 100644 --- a/components/common/kube.js +++ b/components/common/kube.js @@ -234,24 +234,34 @@ exports.DeleteService = async function(name) { } exports.GetRoutes = async function() { - let list = await customApi.listNamespacedCustomObject( - 'route.openshift.io', - 'v1', - namespace, - 'routes' - ); - return list.body.items; + try { + let list = await customApi.listNamespacedCustomObject( + 'route.openshift.io', + 'v1', + namespace, + 'routes' + ); + return list.body.items; + } catch (error) { + // Routes are not available (not running on OpenShift) + return []; + } } exports.DeleteRoute = async function(name) { - Log(`Kube - Deleting route ${name}`); - await customApi.deleteNamespacedCustomObject( - 'route.openshift.io', - 'v1', - namespace, - 'routes', - name - ); + try { + Log(`Kube - Deleting route ${name}`); + await customApi.deleteNamespacedCustomObject( + 'route.openshift.io', + 'v1', + namespace, + 'routes', + name + ); + } catch (error) { + // Routes are not available (not running on OpenShift) + Log(`Route deletion skipped - OpenShift routes not available: ${name}`); + } } exports.LoadDeployment = async function(name) { diff --git a/components/console/.eslintignore b/components/console/.eslintignore deleted file mode 100644 index f6b6885..0000000 --- a/components/console/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -public -build -cypress -**/*.js diff --git a/components/console/.eslintrc.json b/components/console/.eslintrc.json deleted file mode 100644 index ffe68e0..0000000 --- a/components/console/.eslintrc.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "settings": { - "import/parsers": { - "@typescript-eslint/parser": [".ts", ".tsx"] - }, - "import/resolver": { - "typescript": {}, - "node": { - "extensions": [".js", ".jsx", ".ts", ".tsx"] - } - }, - "import/extensions": [".ts", ".tsx"], - "react": { - "version": "detect" - } - }, - "env": { - "browser": true, - "es2021": true, - "node": true, - "jest": true - }, - "globals": { - "JSX": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:import/errors", - "plugin:import/warnings", - "prettier" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "comment": true, - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 12, - "extraFileExtensions": [".json"], - "sourceType": "module", - "project": ["tsconfig.json", "cypress.config.ts"], - "tsconfigRootDir": "./" - }, - "plugins": ["react", "react-hooks", "@typescript-eslint"], - "rules": { - "arrow-body-style": ["error", "as-needed"], - "padding-line-between-statements": "off", - "import/prefer-default-export": "off", - "import/no-cycle": [ - "error", - { - "maxDepth": 10, - "ignoreExternal": true - } - ], - "no-console": 1, - "semi": [1, "always"], - "eol-last": 2, - "consistent-return": 0, - "consistent-this": [1, "that"], - "curly": [2, "all"], - "default-case": [2], - "dot-notation": [2], - "no-multiple-empty-lines": [ - 2, - { - "max": 2, - "maxEOF": 0 - } - ], - "eqeqeq": [2, "allow-null"], - "guard-for-in": 2, - "import/no-unresolved": ["error"], - "import/no-duplicates": ["error"], - "max-nested-callbacks": [1, 4], - "newline-before-return": "error", - "no-alert": 2, - "no-caller": 2, - "no-constant-condition": 2, - "no-debugger": 2, - "no-else-return": ["error"], - "no-global-strict": 0, - "no-irregular-whitespace": ["error"], - "no-param-reassign": ["warn", { "props": true, "ignorePropertyModificationsFor": ["acc", "node"] }], - "no-shadow": "off", - "no-underscore-dangle": 0, - "no-var": 2, - "no-unused-vars": "off", - "object-shorthand": ["error", "properties"], - "prefer-const": [ - "error", - { - "destructuring": "all" - } - ], - "prefer-template": 2, - "radix": 2, - "import/newline-after-import": [ - "error", - { - "count": 1 - } - ], - "import/order": [ - "warn", - { - "newlines-between": "always", - "alphabetize": { - "caseInsensitive": true, - "order": "asc" - }, - "groups": ["builtin", "external", "internal", ["parent", "sibling"]], - "pathGroupsExcludedImportTypes": ["react"], - "pathGroups": [ - { - "pattern": "react", - "group": "external", - "position": "before" - } - ] - } - ], - "react/jsx-filename-extension": [1, { "extensions": [".ts", ".tsx"] }], - "react/jsx-fragments": "error", - "react/react-in-jsx-scope": "off", - "react/jsx-no-duplicate-props": 2, - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "react/function-component-definition": [ - "error", - { - "namedComponents": "function-expression", - "unnamedComponents": "function-expression" - } - ], - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "react/no-string-refs": 1, - "react/no-unknown-property": "error", - "react/jsx-no-useless-fragment": "error", - "react/no-unescaped-entities": 0, - "react/prop-types": 0, - "react/self-closing-comp": [ - "error", - { - "component": true, - "html": false - } - ], - "react/display-name": 0, - "require-atomic-updates": 0, - "@typescript-eslint/no-shadow": ["error", { "ignoreTypeValueShadow": true }], - "@typescript-eslint/padding-line-between-statements": [ - "error", - { - "blankLine": "always", - "next": "*", - "prev": ["interface", "type", "function"] - } - ], - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/naming-convention": [ - "error", - { - "selector": ["enum", "enumMember"], - "format": ["PascalCase"] - } - ], - "@typescript-eslint/no-explicit-any": "warn", - "@typescript-eslint/no-use-before-define": 0, - "@typescript-eslint/no-unused-vars": ["error"] - } -} diff --git a/components/console/.github/ISSUE_TEMPLATE/bug_report.md b/components/console/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 6ddef5c..0000000 --- a/components/console/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: - -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**browser (please complete the following information):** - -- Browser [e.g. firefox, chrome] - -**Additional context** -Add any other context about the problem here. diff --git a/components/console/.github/workflows/commitlint.yml b/components/console/.github/workflows/commitlint.yml deleted file mode 100644 index ca62516..0000000 --- a/components/console/.github/workflows/commitlint.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Lint Commit Messages -on: [pull_request, push] - -jobs: - commitlint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - uses: wagoid/commitlint-github-action@v5 diff --git a/components/console/.github/workflows/skupper-console.yml b/components/console/.github/workflows/skupper-console.yml deleted file mode 100644 index ed26757..0000000 --- a/components/console/.github/workflows/skupper-console.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: skupper-console -on: - push: - branches: [main] - pull_request: - branches: [main] -jobs: - build-and-deploy: - runs-on: ubuntu-latest - steps: - - name: Checkout 🛎️ - uses: actions/checkout@v3 - - - name: Set up Node.js ⚙️ - uses: actions/setup-node@v3 - with: - node-version: '18' - cache: 'yarn' - cache-dependency-path: yarn.lock - - - name: Install 📦 - run: | - yarn install --immutable --immutable-cache --check-cache --prefer-offline - - - name: Lint 🎨 - run: | - yarn lint - - - name: Build 🚧 - run: | - yarn build - - - name: Get number of CPU cores 💻 - id: cpu-cores - uses: SimenB/github-actions-cpu-cores@v1 - - - name: Unit tests 🔧 - run: | - yarn coverage --max-workers ${{ steps.cpu-cores.outputs.count }} diff --git a/components/console/.husky/commit-msg b/components/console/.husky/commit-msg deleted file mode 100755 index c160a77..0000000 --- a/components/console/.husky/commit-msg +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - -npx --no -- commitlint --edit ${1} diff --git a/components/console/LICENSE b/components/console/LICENSE deleted file mode 100644 index 5568447..0000000 --- a/components/console/LICENSE +++ /dev/null @@ -1,202 +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 2018 Red Hat, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/components/console/commitlint.config.js b/components/console/commitlint.config.js deleted file mode 100644 index 10be786..0000000 --- a/components/console/commitlint.config.js +++ /dev/null @@ -1,124 +0,0 @@ -const Configuration = { - /* - * Referenced packages must be installed - */ - extends: ['@commitlint/config-conventional'], - /* - * Functions that return true if commitlint should ignore the given message. - */ - /* - * Any rules defined here will override rules from @commitlint/config-conventional - */ - rules: { - 'subject-case': [2, 'always', 'sentence-case'], - 'type-empty': [2, 'never'] - }, - ignores: [(commit) => commit === ''], - /* - * Whether commitlint uses the default ignore rules. - */ - defaultIgnores: true, - /* - * Custom URL to show upon failure - */ - helpUrl: 'https://github.com/conventional-changelog/commitlint/#what-is-commitlint', - /* - * Custom prompt configs - */ - prompt: { - alias: {}, - messages: { - type: "Select the type of change that you're committing:", - scope: 'Denote the SCOPE of this change (optional):', - customScope: 'Denote the SCOPE of this change:', - subject: 'Write a SHORT, IMPERATIVE tense description of the change:\n', - body: 'Provide a LONGER description of the change (optional). Use "|" to break new line:\n', - breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\n', - footerPrefixesSelect: 'Select the ISSUES type of changeList by this change (optional):', - customFooterPrefix: 'Input ISSUES prefix:', - footer: 'List any ISSUES by this change. E.g.: #31, #34:\n', - generatingByAI: 'Generating your AI commit subject...', - generatedSelectByAI: 'Select suitable subject by AI generated:', - confirmCommit: 'Are you sure you want to proceed with the commit above?' - }, - types: [ - { value: 'feat', name: 'feat: ✨ A new feature', emoji: ':sparkles:' }, - { value: 'fix', name: 'fix: 🐞 A bug fix', emoji: ':lady_beetle:' }, - { value: 'docs', name: 'docs: 📚 Documentation only changes', emoji: ':books:' }, - { - value: 'style', - name: 'style: 💄 Changes that do not affect the meaning of the code', - emoji: ':lipstick:' - }, - { - value: 'refactor', - name: 'refactor: ♻️ A code change that neither fixes a bug nor adds a feature', - emoji: ':recycle:' - }, - { - value: 'perf', - name: 'perf: ⚡️ A code change that improves performance', - emoji: ':zap:' - }, - { - value: 'test', - name: 'test: ✅ Adding missing tests or correcting existing tests', - emoji: ':white_check_mark:' - }, - { - value: 'build', - name: 'build: 📦️ Changes that affect the build system or external dependencies', - emoji: ':package:' - }, - { - value: 'ci', - name: 'ci: 🎡 Changes to our CI configuration files and scripts', - emoji: ':ferris_wheel:' - }, - { - value: 'chore', - name: "chore: 🔨 Other changes that don't modify src or test files", - emoji: ':hammer:' - }, - { - value: 'revert', - name: 'revert: ⏪️ Reverts a previous commit', - emoji: ':rewind:' - } - ], - useEmoji: true, - emojiAlign: 'center', - useAI: false, - aiNumber: 1, - themeColorCode: '', - scopes: ['Site', 'Component', 'Process', 'Services', 'Topology', 'Metrics', 'Shared', 'Core', 'Core UI', 'General'], - allowCustomScopes: false, - allowEmptyScopes: true, - customScopesAlign: 'bottom', - customScopesAlias: 'custom', - emptyScopesAlias: 'Leave Empty', - upperCaseSubject: true, - markBreakingChangeMode: false, - allowBreakingChanges: ['feat', 'fix'], - breaklineNumber: 100, - breaklineChar: '|', - skipQuestions: [], - issuePrefixes: [{ value: 'closed', name: 'closed: ISSUES has been processed' }], - customIssuePrefixAlign: 'top', - emptyIssuePrefixAlias: 'skip', - customIssuePrefixAlias: 'custom', - allowCustomIssuePrefix: true, - allowEmptyIssuePrefix: true, - confirmColorize: true, - maxHeaderLength: Infinity, - maxSubjectLength: 60, - minSubjectLength: 0, - scopeOverrides: undefined, - defaultBody: '', - defaultIssues: '', - defaultScope: '', - defaultSubject: '' - } -}; - -module.exports = Configuration; diff --git a/components/console/eslint.config.js b/components/console/eslint.config.js new file mode 100644 index 0000000..ee2346b --- /dev/null +++ b/components/console/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js'; +import tsPlugin from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; + +export default [ + js.configs.recommended, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2021, + sourceType: 'module', + ecmaFeatures: { jsx: true } + }, + globals: { + window: 'readonly', + process: 'readonly', + require: 'readonly', + document: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + sessionStorage: 'readonly', + clearInterval: 'readonly' + } + }, + plugins: { '@typescript-eslint': tsPlugin }, + rules: { + 'no-undef': 'warn', + 'no-unused-vars': 'off' + } + }, + { + ignores: ['node_modules', 'public', 'build', 'cypress', '**/*.js'] + } +]; diff --git a/components/console/public/index.html b/components/console/index.html similarity index 58% rename from components/console/public/index.html rename to components/console/index.html index ee2895d..0387ca6 100644 --- a/components/console/public/index.html +++ b/components/console/index.html @@ -2,13 +2,14 @@ - + - - <%= title %> + + Skupper console
+ diff --git a/components/console/jest.config.ts b/components/console/jest.config.ts deleted file mode 100644 index b96a102..0000000 --- a/components/console/jest.config.ts +++ /dev/null @@ -1,44 +0,0 @@ -const ROOT = process.cwd(); - -const ROOT_PROJECT = `${ROOT}`; -const SRC_PATH = `${ROOT_PROJECT}/src`; -const MOCKS_PATH = `${ROOT_PROJECT}/mocks`; -const TS_CONFIG_PATH = './tsconfig.paths.json'; -const FILE_MOCK = 'jest.config.fileMock.ts'; - -const extensionsAllowed = { - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$/': `${MOCKS_PATH}/${FILE_MOCK}`, - '\\.(css|scss)$': `${MOCKS_PATH}/${FILE_MOCK}` -}; - -function makeModuleNameMapper(srcPath: string, tsconfigPath: string) { - const { paths } = require(tsconfigPath).compilerOptions; - const aliases: { [key: string]: string } = {}; - - Object.keys(paths).forEach((item) => { - const key = item.replace('/*', '/(.*)'); - const path = paths[item][0].replace('/*', '/$1'); - - aliases[key] = `${srcPath}/${path}`; - }); - - return { ...extensionsAllowed, ...aliases }; -} - -const config = { - preset: 'ts-jest/presets/js-with-ts', - testEnvironment: 'jsdom', - setupFilesAfterEnv: ['@testing-library/jest-dom'], - moduleNameMapper: makeModuleNameMapper(SRC_PATH, TS_CONFIG_PATH), - moduleDirectories: [`${ROOT_PROJECT}/node_modules`, `${ROOT_PROJECT}/src`], - roots: [SRC_PATH], - transformIgnorePatterns: [`${ROOT_PROJECT}/node_modules/(?!d3|d3-timer)`], - transform: { - '^.+\\.(ts|tsx)$': ['ts-jest', { isolatedModules: true }], - '^.+\\.svg$': `${MOCKS_PATH}/${FILE_MOCK}`, - '.+\\.(png)$': `${MOCKS_PATH}/${FILE_MOCK}` - }, - coveragePathIgnorePatterns: ['API', './src/index.tsx', 'routes.tsx', './src/config', 'core/components/Graph'] -}; - -export default config; diff --git a/components/console/mocks/jest.config.fileMock.ts b/components/console/mocks/jest.config.fileMock.ts deleted file mode 100644 index 919f7d6..0000000 --- a/components/console/mocks/jest.config.fileMock.ts +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - process() { - return { code: 'module.exports = {};' }; - }, - getCacheKey() { - return ''; - } -}; diff --git a/components/console/package.json b/components/console/package.json index ea0e73a..f529bde 100644 --- a/components/console/package.json +++ b/components/console/package.json @@ -1,110 +1,54 @@ { - "name": "skupper-x-webconsole", + "name": "skupper-x-console", "version": "1.0.0", - "description": "Skupper X prototype", "license": "Apache-2.0", - "keywords": [ - "skupper", - "skupper-x", - "console", - "monitoring", - "observability", - "connectivity", - "openshift" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/skupperproject/skupper-console.git" - }, - "bugs": { - "url": "https://github.com/skupperproject/skupper-console/issues" - }, "private": true, "scripts": { - "start": "webpack serve --config webpack.dev.js", - "build": "webpack --config webpack.prod.js", - "test": "ENABLE_MOCK_SERVER=true jest --config jest.config.ts", - "coverage": "yarn test --coverage --collectCoverageFrom='src/**/*.{tsx,ts}'", + "start": "vite", + "build": "vite build", "lint": "eslint src --ext .ts,.tsx --cache", "lint-fix": "yarn lint --fix", "format": "prettier --write 'src/**/*.{ts,tsx,json,css}'", - "bundle-report": "STATS=server yarn build", - "find-deadcode": "ts-prune", - "prepare": "husky", - "commit": "git-cz" + "prepare": "husky" }, "dependencies": { - "@antv/g6": "^4.8.24", - "@patternfly/patternfly": "^5.2.0", - "@patternfly/react-charts": "^7.2.0", - "@patternfly/react-code-editor": "^5.2.0", - "@patternfly/react-core": "^5.2.0", - "@patternfly/react-icons": "^5.2.0", - "@patternfly/react-table": "^5.2.0", - "@tanstack/react-query": "^5.18.1", - "axios": "^1.6.7", - "date-fns": "^3.3.1", - "framer-motion": "^11.0.3", - "node": "^21.7.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.12", - "react-router-dom": "^6.22.0" + "@antv/g6": "^5.0.48", + "@antv/g6-extension-react": "^0.2.3", + "@patternfly/patternfly": "^6.2.3", + "@patternfly/react-charts": "^8.2.2", + "@patternfly/react-code-editor": "^6.2.2", + "@patternfly/react-core": "^6.2.2", + "@patternfly/react-icons": "^6.2.2", + "@patternfly/react-table": "^6.2.2", + "@patternfly/react-tokens": "^6.2.2", + "@tanstack/react-query": "^5.79.0", + "axios": "^1.9.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^6.0.0", + "react-router-dom": "^7.6.1" }, "devDependencies": { - "@commitlint/cli": "^18.6.0", - "@commitlint/config-conventional": "^18.6.0", - "@testing-library/dom": "^9.3.4", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^14.2.1", - "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.12", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", - "circular-dependency-plugin": "^5.2.2", - "commitizen": "^4.3.0", - "copy-webpack-plugin": "^12.0.2", - "css-loader": "^6.10.0", - "css-minimizer-webpack-plugin": "^6.0.0", - "cz-git": "^1.8.0", - "eslint": "^8.56.0", + "@types/react": "^19.1.6", + "@types/react-dom": "^19.1.5", + "@typescript-eslint/eslint-plugin": "^8.33.1", + "@typescript-eslint/parser": "^8.33.1", + "@vitejs/plugin-react": "^4.3.3", + "eslint": "^9.28.0", "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.1", + "eslint-import-resolver-typescript": "^3.10.1", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest": "^27.6.3", "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "html-webpack-plugin": "^5.6.0", + "eslint-plugin-react-hooks": "^5.2.0", "husky": "^9.0.10", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "mini-css-extract-plugin": "^2.8.0", - "miragejs": "^0.1.48", - "prettier": "^3.2.5", - "start-server-and-test": "^2.0.3", - "style-loader": "^3.3.4", - "terser-webpack-plugin": "^5.3.10", - "ts-jest": "^29.1.2", - "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", - "ts-prune": "^0.10.3", - "tsconfig-paths-webpack-plugin": "^4.1.0", - "typescript": "^5.3.3", - "webpack": "^5.90.1", - "webpack-bundle-analyzer": "^4.10.1", - "webpack-cli": "^5.1.4", - "webpack-dev-server": "^4.15.1", - "webpack-merge": "^5.10.0" + "prettier": "^3.5.3", + "typescript": "^5.8.3", + "vite": "^6.0.7" }, "engines": { "node": ">=18.17.1", "yarn": ">=1.22.10" }, - "config": { - "commitizen": { - "path": "node_modules/cz-git" - } - }, "browserslist": [ ">10%", "last 2 versions", diff --git a/components/console/public/manifest.json b/components/console/public/manifest.json index 714c3aa..9f715f4 100644 --- a/components/console/public/manifest.json +++ b/components/console/public/manifest.json @@ -4,7 +4,7 @@ "name": "Network console", "icons": [ { - "src": "favicon.v.ico", + "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } diff --git a/components/console/src/API/REST.api.ts b/components/console/src/API/REST.api.ts index 268cfa7..cdf8dbe 100644 --- a/components/console/src/API/REST.api.ts +++ b/components/console/src/API/REST.api.ts @@ -1,31 +1,89 @@ import { axiosFetch } from './apiMiddleware'; import { - SiteResponse, + BackboneSiteResponse, RequestOptions, BackboneResponse, BackboneRequest, - SiteRequest, + BackboneSiteRequest, LinkResponse, LinkRequest, VanResponse, VanRequest, InvitationResponse, InvitationRequest, - MemberResponse + MemberSiteResponse, + AccessPointResponse, + AccessPointRequest, + TlsCertificateResponse, + TargetPlatformResponse, + LibraryBlockResponse, + LibraryBlockRequest, + LibraryBlockUpdateRequest, + LibraryBlockTypeResponse, + LibraryBlockTypeMap, + LibraryBlockHistoryResponse, + IngressRequest, + ApplicationResponse, + ApplicationBlock, + CreateApplicationRequest, + DeploymentRequest, + DeploymentResponse, + DeploymentDetailsResponse } from './REST.interfaces'; import { getBackbonesPATH, getBackbonePATH, - getInteriorSitesPATH, - getLinkPATH, + getBackboneActivatePATH, + getBackboneSitePATH, + getBackboneSitesPATH, + getBackboneLinkPATH, + getBackboneLinksForBackbonePATH, + getBackboneLinksForSitePATH, + getCreateLinkPATH, getVanPATH, - getInvitationPath, - getMemberPath, - getVansPATH + getVansPATH, + getVansForBackbonePATH, + getCreateVanPATH, + getEvictVanPATH, + getInvitationPATH, + getInvitationsPATH, + getInvitationYamlPATH, + getInvitationsForVanPATH, + getCreateInvitationPATH, + getExpireInvitationPATH, + getMemberPATH, + getMembersForVanPATH, + getEvictMemberPATH, + getAccessPointPATH, + getAccessPointsForSitePATH, + getAccessPointsForBackbonePATH, + getTlsCertificatePATH, + getTargetPlatformsPATH, + getSiteDeploymentPATH, + getIngressPATH, + getLibrariesPATH, + getLibraryPATH, + getLibraryConfigPATH, + getLibraryInterfacesPATH, + getLibraryBodyPATH, + getLibraryHistoryPATH, + getLibraryBlockTypesPATH, + // getLibraryBodyStylesPATH, // Not implemented in backend + getInterfaceRolesPATH, + getApplicationsPATH, + getApplicationPATH, + getApplicationBuildPATH, + getApplicationLogPATH, + getApplicationBlocksPATH, + getDeploymentsPATH, + getDeploymentPATH, + getDeploymentDeployPATH, + getDeploymentLogPATH } from './REST.paths'; import { mapOptionsToQueryParams } from './REST.utils'; export const RESTApi = { + // BACKBONE APIs fetchBackbones: async (options?: RequestOptions): Promise => { const data = await axiosFetch(getBackbonesPATH(), { params: options ? mapOptionsToQueryParams(options) : null @@ -43,28 +101,35 @@ export const RESTApi = { return id; }, + searchBackbone: async (bid: string): Promise => { + const data = await axiosFetch(getBackbonePATH(bid)); + + return data; + }, + deleteBackbone: async (bid: string): Promise => { - await axiosFetch(`${getBackbonePATH(bid)}`, { + await axiosFetch(getBackbonePATH(bid), { method: 'DELETE' }); }, activateBackbone: async (bid: string): Promise => { - await axiosFetch(`${getBackbonePATH(bid)}/activate`, { + await axiosFetch(getBackboneActivatePATH(bid), { method: 'PUT' }); }, - fetchSites: async (bid: string, options?: RequestOptions): Promise => { - const data = await axiosFetch(`${getBackbonePATH(bid)}/sites`, { + // BACKBONE SITE APIs + fetchSites: async (bid: string, options?: RequestOptions): Promise => { + const data = await axiosFetch(getBackboneSitesPATH(bid), { params: options ? mapOptionsToQueryParams(options) : null }); return data; }, - createSite: async (bid: string, data: SiteRequest): Promise => { - const { id } = await axiosFetch<{ id: string }>(`${getBackbonePATH(bid)}/sites`, { + createSite: async (bid: string, data: BackboneSiteRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getBackboneSitesPATH(bid), { method: 'POST', data }); @@ -72,119 +137,418 @@ export const RESTApi = { return id; }, - deleteSite: async (id: string): Promise => { - await axiosFetch(getInteriorSitesPATH(id), { + searchSite: async (sid: string): Promise => { + const data = await axiosFetch(getBackboneSitePATH(sid)); + + return data; + }, + + updateSite: async (sid: string, data: Partial): Promise => { + await axiosFetch(getBackboneSitePATH(sid), { + method: 'PUT', + data + }); + }, + + deleteSite: async (sid: string): Promise => { + await axiosFetch(getBackboneSitePATH(sid), { method: 'DELETE' }); }, - searchSite: async (id: string): Promise => axiosFetch(`${getInteriorSitesPATH(id)}`), + // ACCESS POINT APIs + fetchAccessPointsForSite: async (sid: string, options?: RequestOptions): Promise => { + const data = await axiosFetch(getAccessPointsForSitePATH(sid), { + params: options ? mapOptionsToQueryParams(options) : null + }); + + return data; + }, - // LINKS APIs - fetchLinks: async (bid: string, options?: RequestOptions): Promise => { - const data = await axiosFetch(`${getBackbonePATH(bid)}/links`, { + fetchAccessPointsForBackbone: async (bid: string, options?: RequestOptions): Promise => { + const data = await axiosFetch(getAccessPointsForBackbonePATH(bid), { params: options ? mapOptionsToQueryParams(options) : null }); return data; }, - createLink: async (bid: string, data?: LinkRequest): Promise => { - await axiosFetch(`${getBackbonePATH(bid)}/links`, { + createAccessPoint: async (sid: string, data: AccessPointRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getAccessPointsForSitePATH(sid), { method: 'POST', data }); + + return id; }, - deleteLink: async (id: string): Promise => { - await axiosFetch(getLinkPATH(id), { + searchAccessPoint: async (apid: string): Promise => { + const data = await axiosFetch(getAccessPointPATH(apid)); + + return data; + }, + + deleteAccessPoint: async (apid: string): Promise => { + await axiosFetch(getAccessPointPATH(apid), { method: 'DELETE' }); }, - fetchInitialDeployment: async (id: string): Promise => axiosFetch(`${getInteriorSitesPATH(id)}/kube`), + // BACKBONE LINK APIs + fetchLinksForBackbone: async (bid: string, options?: RequestOptions): Promise => { + const data = await axiosFetch(getBackboneLinksForBackbonePATH(bid), { + params: options ? mapOptionsToQueryParams(options) : null + }); - fetchIngress: async (sid: string, data: string): Promise => { - await axiosFetch(`${getInteriorSitesPATH(sid)}/ingress`, { - headers: { - 'Content-Type': 'application/json' - }, + return data; + }, + + fetchLinksForSite: async (sid: string, options?: RequestOptions): Promise => { + const data = await axiosFetch(getBackboneLinksForSitePATH(sid), { + params: options ? mapOptionsToQueryParams(options) : null + }); + + return data; + }, + + createLink: async (apid: string, data: LinkRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getCreateLinkPATH(apid), { method: 'POST', data }); + + return id; }, - fetchIncomingLinks: async (id: string): Promise => - axiosFetch(`${getInteriorSitesPATH(id)}/links/incoming/kube`), + updateLink: async (lid: string, data: Partial): Promise => { + await axiosFetch(getBackboneLinkPATH(lid), { + method: 'PUT', + data + }); + }, - fetchVans: async (): Promise => axiosFetch(`${getVansPATH()}`), + deleteLink: async (lid: string): Promise => { + await axiosFetch(getBackboneLinkPATH(lid), { + method: 'DELETE' + }); + }, + + // VAN (APPLICATION NETWORK) APIs + fetchVans: async (): Promise => { + const data = await axiosFetch(getVansPATH()); + + return data; + }, + + fetchVansForBackbone: async (bid: string): Promise => { + const data = await axiosFetch(getVansForBackbonePATH(bid)); + + return data; + }, - createVan: async (bid: string, data?: VanRequest): Promise => { - await axiosFetch(`${getBackbonePATH(bid)}/vans`, { + createVan: async (bid: string, data: VanRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getCreateVanPATH(bid), { method: 'POST', - data: { - name: data?.name - } + data }); + + return id; }, searchVan: async (vid: string): Promise => { - const data = await axiosFetch(`${getVanPATH(vid)}`, { - method: 'GET' - }); + const data = await axiosFetch(getVanPATH(vid)); return data; }, deleteVan: async (vid: string): Promise => { - await axiosFetch(`${getVanPATH(vid)}`, { + await axiosFetch(getVanPATH(vid), { method: 'DELETE' }); }, - fetchInvitations: async (vid: string): Promise => - axiosFetch(`${getVanPATH(vid)}/invitations`), + evictVan: async (vid: string): Promise => { + await axiosFetch(getEvictVanPATH(vid), { + method: 'PUT' + }); + }, + + // INVITATION APIs + fetchInvitations: async (vid: string): Promise => { + const data = await axiosFetch(getInvitationsForVanPATH(vid)); - createInvitation: async (vid: string, data?: InvitationRequest): Promise => { - await axiosFetch(`${getVanPATH(vid)}/invitations`, { + return data; + }, + + fetchAllInvitations: async (): Promise => { + const data = await axiosFetch(getInvitationsPATH()); + + return data; + }, + + createInvitation: async (vid: string, data: InvitationRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getCreateInvitationPATH(vid), { method: 'POST', data }); + + return id; }, searchInvitation: async (iid: string): Promise => { - const data = await axiosFetch(`${getInvitationPath(iid)}`, { - method: 'GET' + const data = await axiosFetch(getInvitationPATH(iid)); + + return data; + }, + + searchInvitationYAML: async (iid: string): Promise => { + const data = await axiosFetch(getInvitationYamlPATH(iid)); + + return data; + }, + + deleteInvitation: async (iid: string): Promise => { + await axiosFetch(getInvitationPATH(iid), { + method: 'DELETE' + }); + }, + + expireInvitation: async (iid: string): Promise => { + await axiosFetch(getExpireInvitationPATH(iid), { + method: 'PUT' + }); + }, + + // MEMBER APIs + fetchMembers: async (vid: string): Promise => { + const data = await axiosFetch(getMembersForVanPATH(vid)); + + return data; + }, + + searchMember: async (mid: string): Promise => { + const data = await axiosFetch(getMemberPATH(mid)); + + return data; + }, + + evictMember: async (mid: string): Promise => { + await axiosFetch(getEvictMemberPATH(mid), { + method: 'PUT' + }); + }, + + // TLS CERTIFICATE APIs + fetchTlsCertificate: async (cid: string): Promise => { + const data = await axiosFetch(getTlsCertificatePATH(cid)); + + return data; + }, + + // TARGET PLATFORM APIs + fetchTargetPlatforms: async (): Promise => { + const data = await axiosFetch(getTargetPlatformsPATH()); + + return data; + }, + + // DEPLOYMENT APIs + fetchSiteDeployment: async (sid: string, target: 'sk2' | 'kube'): Promise => { + const data = await axiosFetch(getSiteDeploymentPATH(sid, target)); + + return data; + }, + + // INGRESS APIs + createIngress: async (sid: string, data: IngressRequest): Promise => { + await axiosFetch(getIngressPATH(sid), { + method: 'POST', + data + }); + }, + + // LIBRARY BLOCK APIs + fetchLibraries: async (options?: RequestOptions): Promise => { + const data = await axiosFetch(getLibrariesPATH(), { + params: options ? mapOptionsToQueryParams(options) : null }); return data; }, - searchInvitationYAML: async (id: string): Promise => axiosFetch(`${getInvitationPath(id)}/kube`), + fetchLibraryBlock: async (id: string): Promise => { + const data = await axiosFetch(getLibraryPATH(id)); + + return data; + }, + + fetchLibraryConfig: async (id: string): Promise => { + const data = await axiosFetch(getLibraryConfigPATH(id)); + + return data; + }, + + fetchLibraryInterfaces: async (id: string): Promise => { + const data = await axiosFetch(getLibraryInterfacesPATH(id)); - deleteInvitation: async (vid: string): Promise => { - await axiosFetch(`${getInvitationPath(vid)}`, { + return data; + }, + + fetchLibraryBody: async (id: string): Promise => { + const data = await axiosFetch(getLibraryBodyPATH(id)); + + return data; + }, + + deleteLibrary: async (id: string): Promise => { + await axiosFetch(getLibraryPATH(id), { method: 'DELETE' }); }, - fetchMembers: async (vid: string): Promise => - axiosFetch(`${getVanPATH(vid)}/members`), + createLibraryJson: async (data: LibraryBlockRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getLibrariesPATH(), { + method: 'POST', + data + }); + + return id; + }, - searchMember: async (mid: string): Promise => axiosFetch(`${getMemberPath(mid)}`), + fetchLibraryBlockTypes: async (): Promise => { + const data = await axiosFetch(getLibraryBlockTypesPATH()); - fetchAccessClaims: async (bid: string): Promise<{ id: string; name: string }[]> => { - const data = await axiosFetch<{ id: string; name: string }[]>(`${getBackbonePATH(bid)}/access/claim`, { - method: 'GET' + // Convert btMap (object) to btArray (array) for backward compatibility + // Add the type field back from the object key + const blockTypesArray: LibraryBlockTypeResponse[] = Object.entries(data).map(([typeName, typeData]) => ({ + ...typeData, + type: typeName + })); + + return blockTypesArray; + }, + + // NOTE: Body styles are hardcoded as ['simple', 'composite'] in useLibraryMetadata + // because the backend doesn't implement /library/bodystyles endpoint + // fetchLibraryBodyStyles: async (): Promise => { + // const data = await axiosFetch(getLibraryBodyStylesPATH()); + // return data; + // }, + + fetchInterfaceRoles: async (): Promise<{ name: string; description?: string }[]> => { + const data = await axiosFetch<{ name: string; description?: string }[]>(getInterfaceRolesPATH()); + + return data; + }, + + updateLibraryConfig: async (id: string, config: LibraryBlockUpdateRequest): Promise => { + await axiosFetch(getLibraryConfigPATH(id), { + method: 'PUT', + data: config + }); + }, + + updateLibraryInterfaces: async (id: string, interfaces: LibraryBlockUpdateRequest): Promise => { + await axiosFetch(getLibraryInterfacesPATH(id), { + method: 'PUT', + data: interfaces + }); + }, + + updateLibraryBody: async (id: string, body: LibraryBlockUpdateRequest): Promise => { + await axiosFetch(getLibraryBodyPATH(id), { + method: 'PUT', + data: body }); + }, + + fetchLibraryHistory: async (id: string): Promise => { + const data = await axiosFetch(getLibraryHistoryPATH(id)); return data; }, - fetchAccessMember: async (bid: string): Promise<{ id: string; name: string }[]> => { - const data = await axiosFetch<{ id: string; name: string }[]>(`${getBackbonePATH(bid)}/access/member`, { - method: 'GET' + // APPLICATION APIs + fetchApplications: async (options?: RequestOptions): Promise => { + console.log('API fetchApplications called with options:', options); + const data = await axiosFetch(getApplicationsPATH(), { + params: options ? mapOptionsToQueryParams(options) : null }); + console.log('API fetchApplications result:', data); + return data; + }, + + createApplication: async (data: CreateApplicationRequest): Promise => { + const { id } = await axiosFetch<{ id: string }>(getApplicationsPATH(), { + method: 'POST', + data + }); + + return id; + }, + + deleteApplication: async (id: string): Promise => { + await axiosFetch(getApplicationPATH(id), { + method: 'DELETE' + }); + }, + + buildApplication: async (id: string): Promise => { + await axiosFetch(getApplicationBuildPATH(id), { + method: 'PUT' + }); + }, + + fetchApplicationLog: async (id: string): Promise => { + const data = await axiosFetch(getApplicationLogPATH(id)); + + return data; + }, + + fetchApplicationBlocks: async (id: string): Promise => { + const data = await axiosFetch(getApplicationBlocksPATH(id)); + + return data; + }, + + // DEPLOYMENT APIs + fetchDeployments: async (options?: RequestOptions): Promise => { + const data = await axiosFetch(getDeploymentsPATH(), { + params: options ? mapOptionsToQueryParams(options) : null + }); + + return data; + }, + + createDeployment: async (data: DeploymentRequest): Promise => { + const {id} = await axiosFetch<{ id: string }>(getDeploymentsPATH(), { + method: 'POST', + data + }); + + return id; + }, + + fetchDeploymentDetails: async (id: string): Promise => { + const data = await axiosFetch(getDeploymentPATH(id)); + + return data; + }, + + deleteDeployment: async (id: string): Promise => { + await axiosFetch(getDeploymentPATH(id), { + method: 'DELETE' + }); + }, + + deployDeployment: async (id: string): Promise => { + await axiosFetch(getDeploymentDeployPATH(id), { + method: 'PUT' + }); + }, + + fetchDeploymentLog: async (id: string): Promise => { + const data = await axiosFetch(getDeploymentLogPATH(id)); return data; } diff --git a/components/console/src/API/REST.enum.ts b/components/console/src/API/REST.enum.ts deleted file mode 100644 index cbc1e34..0000000 --- a/components/console/src/API/REST.enum.ts +++ /dev/null @@ -1,28 +0,0 @@ -export enum SortDirection { - ASC = 'asc', - DESC = 'desc' -} - -export enum AvailableProtocols { - Tcp = 'tcp', - Http = 'http1', - Http2 = 'http2', - AllHttp = 'http.*' -} - -export enum TcpStatus { - Active = 'active', - Terminated = 'terminated' -} - -export enum FlowDirection { - Outgoing = 'outgoing', - Incoming = 'incoming' -} - -export enum Quantiles { - Median = 0.5, - Ninety = 0.9, - NinetyFive = 0.95, - NinetyNine = 0.99 -} diff --git a/components/console/src/API/REST.interfaces.ts b/components/console/src/API/REST.interfaces.ts index 08befd3..f8c8b92 100644 --- a/components/console/src/API/REST.interfaces.ts +++ b/components/console/src/API/REST.interfaces.ts @@ -1,21 +1,77 @@ import { AxiosError, AxiosRequestConfig } from 'axios'; -import { DeploymentStates } from '@pages/Backbones/Backbones.enum'; +import { DeploymentStates } from '../pages/Backbones/Backbones.enum'; -import { FlowDirection, SortDirection } from './REST.enum'; +// Canonical lifecycle type for member sites (keep in sync with backend) +export type MemberLifeCycleStatus = + | 'partial' + | 'new' + | 'skx_cr_created' + | 'cm_cert_created' + | 'cm_issuer_created' + | 'ready' + | 'active' + | 'expired' + | 'failed'; + +// Canonical lifecycle type for backbone and van +export type NetworkLifeCycleStatus = + | 'partial' + | 'new' + | 'initializing' + | 'skx_cr_created' + | 'creating_resources' + | 'cm_cert_created' + | 'generating_certificates' + | 'cm_issuer_created' + | 'configuring_issuer' + | 'deploying' + | 'starting' + | 'ready' + | 'active' + | 'expired' + | 'failed' + | 'error' + | 'terminating' + | 'deleting'; + +// Canonical lifecycle type for invitation +export type InvitationLifeCycleStatus = + | 'partial' + | 'new' + | 'skx_cr_created' + | 'cm_cert_created' + | 'cm_issuer_created' + | 'ready' + | 'active' + | 'expired' + | 'failed'; + +// Canonical lifecycle type for management controller +export type ManagementControllerLifeCycleStatus = 'partial' | 'new' | 'ready'; + +// Canonical lifecycle type for applications (keep in sync with backend) +export type ApplicationLifeCycleStatus = + | 'created' + | 'build-complete' + | 'build-warnings' + | 'build-errors' + | 'deploy-complete' + | 'deploy-warnings' + | 'deploy-errors' + | 'deployed'; export type FetchWithOptions = AxiosRequestConfig; -export type FlowDirections = FlowDirection.Outgoing | FlowDirection.Incoming; -export interface RequestOptions extends Record { +export interface RequestOptions extends Record { filter?: string; offset?: number; limit?: number; - sortDirection?: SortDirection; + sortDirection?: 'asc' | 'desc'; sortName?: string; timeRangeStart?: number; timeRangeEnd?: number; - timeRangeOperation?: number; // 0: intersect , 1: contains, 2: within + timeRangeOperation?: number; } export interface QueryParams { @@ -34,8 +90,8 @@ export interface HTTPError extends AxiosError { } export type ResponseWrapper = { - results: T; // Type based on the Response interface - status: string; // this field is for debug scope. Empty value => OK. In case we have some internal BE error that is not a http status this field is not empty. For example a value can be `Malformed sortBy query` + results: T; + status: string; count: number; timeRangeCount: number; totalCount: number; @@ -50,57 +106,65 @@ export interface BackboneResponse { id: string; name: string; multitenant: boolean; - lifecycle: 'partial' | 'new' | 'ready'; + lifecycle: NetworkLifeCycleStatus; failure: string | null; } -export interface SiteRequest { +export interface BackboneSiteRequest { name: string; - claim?: 'true' | 'false'; - peer?: 'true' | 'false'; - member?: 'true' | 'false'; - manage?: 'true' | 'false'; + platform: string; metadata?: string; } -export interface SiteResponse { +export interface BackboneSiteResponse { id: string; name: string; + lifecycle: NetworkLifeCycleStatus; failure: string | null; - firstactivetime: string | null; - lastheartbeat: string | null; - lifecycle: string; metadata?: string; deploymentstate: DeploymentStates; + targetplatform: string; + platformlong: string; + firstactivetime: string | null; + lastheartbeat: string | null; + tlsexpiration?: string | null; + tlsrenewal?: string | null; + backboneid: string; } export interface LinkRequest { - listeningsite: string; connectingsite: string; - cost?: string; + cost?: number; } export interface LinkResponse { id: string; - listeninginteriorsite: string; + accesspoint: string; connectinginteriorsite: string; cost: number; } export interface VanRequest { - bid: string; name: string; + starttime?: string; + endtime?: string; + deletedelay?: string; } + export interface VanResponse { id: string; name: string; backbone: string; + backboneid: string; backbonename: string; - lifecycle: 'partial' | 'new' | 'ready'; + lifecycle: NetworkLifeCycleStatus; failure: string | null; starttime: string | null; endtime: string | null; - deletedelay: { minutes: number }; + deletedelay: string | null; + certificate?: string | null; + tlsexpiration?: string | null; + tlsrenewal?: string | null; } export interface InvitationRequest { @@ -110,28 +174,248 @@ export interface InvitationRequest { secondaryaccess?: string; joindeadline?: string; siteclass?: string; + prefix?: string; instancelimit?: number; - interactive?: boolean; + interactive?: 'true' | 'false'; } export interface InvitationResponse { id: string; name: string; - lifecycle: 'partial' | 'new' | 'ready'; + lifecycle: InvitationLifeCycleStatus; failure: string | null; joindeadline: string | null; - memberclass: string | null; + memberclasses: string[] | null; instancelimit: number | null; instancecount: number; + fetchcount: number; interactive: boolean; + vanname?: string; } -export interface MemberResponse { +export interface MemberSiteResponse { id: string; name: string; - lifecycle: 'partial' | 'new' | 'ready'; + lifecycle: MemberLifeCycleStatus; failure: string | null; lastheartbeat: string | null; - siteclass: string | null; firstactivetime: string | null; + memberof: string; + invitation: string; + invitationname?: string; + vanname?: string; + joindeadline?: string | null; // Added for join-deadline + interactive?: boolean; // Added for interactive +} + +export interface AccessPointRequest { + name?: string; + kind: 'claim' | 'peer' | 'member' | 'manage'; + bindhost?: string; +} + +export interface AccessPointResponse { + id: string; + name: string; + lifecycle: string; + failure: string | null; + hostname: string | null; + port: number | null; + kind: 'claim' | 'peer' | 'member' | 'manage'; + bindhost: string | null; + interiorsite: string; + sitename?: string; +} + +export interface TargetPlatformResponse { + shortname: string; + longname: string; +} + +export interface IngressRequest { + [apid: string]: { + host: string; + port: number; + }; +} + +export interface IngressResponse { + processed: number; +} + +export interface ClaimAccessPointResponse { + id: string; + name: string; +} + +export interface ApplicationRequest { + name: string; + rootblock: string; +} + +export interface ApplicationResponse { + id: string; + name: string; + rootblock: string; + rootname: string; + lifecycle: ApplicationLifeCycleStatus; + created: string; + buildlog?: string; +} + +export interface LibraryBlockResponse { + id: string; + type: string; + name: string; + provider: string; + bodystyle: 'simple' | 'composite'; + revision: number; + created: string; +} + +export interface LibraryBlockRequest { + name: string; + type: string; + bodystyle: 'simple' | 'composite'; + provider?: string; +} + +export interface LibraryBlockUpdateRequest { + [key: string]: unknown; +} + +export interface DeploymentRequest { + app: string; + van: string; +} + +export interface DeploymentResponse { + id: string; + lifecycle: string; + application: string; + van: string; + appname: string; + vanname: string; +} + +export interface DeploymentDetailsResponse { + id: string; + lifecycle: string; + application: string; + van: string; + appname: string; + vanname: string; + deploylog?: string; +} + +export interface TlsCertificateResponse { + id: string; + isca: boolean; + objectname: string; + signedby: string | null; + expiration: string | null; + renewaltime: string | null; + rotationordinal: number; + supercedes: string | null; +} + +export interface CertificateRequestResponse { + id: string; + requesttype: 'mgmtController' | 'backboneCA' | 'interiorRouter' | 'accessPoint' | 'vanCA' | 'memberClaim' | 'vanSite'; + issuer: string | null; + lifecycle: 'new' | 'cm_cert_created' | 'ready'; + failure: string | null; + hostname: string | null; + createdtime: string; + requesttime: string; + durationhours: number; + managementcontroller: string | null; + backbone: string | null; + interiorsite: string | null; + accesspoint: string | null; + applicationnetwork: string | null; + invitation: string | null; + site: string | null; +} + +export interface ManagementControllerResponse { + id: string; + name: string; + lifecycle: ManagementControllerLifeCycleStatus; + failure: string | null; + certificate: string | null; +} + +export interface ComposeBlockResponse { + id: string; + name: string; + lifecycle: string; + failure: string | null; +} + +export interface BootstrapResponse { + yamldata: string; +} + +export interface SiteDeploymentConfigResponse { + yamldata: string; +} + +export interface TlsCertificateRequest { + requesttype: 'mgmtController' | 'backboneCA' | 'interiorRouter' | 'accessPoint' | 'vanCA' | 'memberClaim' | 'vanSite'; + durationhours?: number; + hostname?: string; +} + +export interface HeartbeatRequest { + lastheartbeat: string; + firstactivetime?: string; +} + +export interface LibraryBlockTypeResponse { + type: string; + allownorth: boolean; + allowsouth: boolean; + allocatetosite: boolean; +} + +// Object map format returned by the backend API +export interface LibraryBlockTypeMap { + [blockTypeName: string]: LibraryBlockTypeResponse; +} + +export interface LibraryBlockHistoryResponse { + revision: number; + created: string; + author: string; + message: string; + changes: { + configuration?: boolean; + interfaces?: boolean; + body?: boolean; + }; + data?: { + configuration?: Record; + interfaces?: unknown[]; + body?: unknown; + }; +} + +export interface ErrorResponse { + error: string; + message: string; + httpStatus?: number; +} + +// Application Block interface +export interface ApplicationBlock { + instancename: string; + libraryblock: string; + libname: string; + revision: string; +} + +export interface CreateApplicationRequest { + name: string; + rootblock: string; } diff --git a/components/console/src/API/REST.paths.ts b/components/console/src/API/REST.paths.ts index 250cb58..c12804c 100644 --- a/components/console/src/API/REST.paths.ts +++ b/components/console/src/API/REST.paths.ts @@ -1,27 +1,96 @@ -import { COLLECTOR_URL } from '@config/config'; +import { BASE_URL_COLLECTOR, COLLECTOR_URL } from '../config/config'; -const BACKBONES_PATH = `${COLLECTOR_URL}/backbones/`; -export const getBackbonesPATH = () => BACKBONES_PATH; +// Note: COLLECTOR_URL already includes /api/v1alpha1 -const BACKBONE_PATH = `${COLLECTOR_URL}/backbone/`; -export const getBackbonePATH = (id: string) => `${BACKBONE_PATH}${id}`; +// Backbone paths +const BACKBONES_PATH = `${COLLECTOR_URL}/backbones`; +export const getBackbonesPATH = () => BACKBONES_PATH; +export const getBackbonePATH = (id: string) => `${BACKBONES_PATH}/${id}`; +export const getBackboneActivatePATH = (id: string) => `${BACKBONES_PATH}/${id}/activate`; -const INTERIOR_SITES_PATH = `${COLLECTOR_URL}/backbonesite/`; -export const getInteriorSitesPATH = (id: string) => `${INTERIOR_SITES_PATH}${id}`; +// Backbone Sites paths +const BACKBONE_SITES_PATH = `${COLLECTOR_URL}/backbonesites`; +export const getBackboneSitesPATH = (backboneId: string) => `${BACKBONES_PATH}/${backboneId}/sites`; +export const getBackboneSitePATH = (id: string) => `${BACKBONE_SITES_PATH}/${id}`; -const LINK_PATH = `${COLLECTOR_URL}/backbonelink/`; -export const getLinkPATH = (id: string) => `${LINK_PATH}${id}`; +// Access Points paths +const ACCESS_POINTS_PATH = `${COLLECTOR_URL}/accesspoints`; +export const getAccessPointPATH = (id: string) => `${ACCESS_POINTS_PATH}/${id}`; +export const getAccessPointsForSitePATH = (siteId: string) => `${BACKBONE_SITES_PATH}/${siteId}/accesspoints`; +export const getAccessPointsForBackbonePATH = (backboneId: string) => `${BACKBONES_PATH}/${backboneId}/accesspoints`; -const HOSTNAMES_PATH = `${COLLECTOR_URL}/hostnames`; -export const getHostnamesPATH = () => HOSTNAMES_PATH; +// Backbone Links paths +const BACKBONE_LINKS_PATH = `${COLLECTOR_URL}/backbonelinks`; +export const getBackboneLinkPATH = (id: string) => `${BACKBONE_LINKS_PATH}/${id}`; +export const getBackboneLinksForBackbonePATH = (backboneId: string) => `${BACKBONES_PATH}/${backboneId}/links`; +export const getBackboneLinksForSitePATH = (siteId: string) => `${BACKBONE_SITES_PATH}/${siteId}/links`; +export const getCreateLinkPATH = (accessPointId: string) => `${ACCESS_POINTS_PATH}/${accessPointId}/links`; +// VAN (Application Networks) paths const VANS_PATH = `${COLLECTOR_URL}/vans`; -const VAN_PATH = `${COLLECTOR_URL}/van/`; export const getVansPATH = () => VANS_PATH; -export const getVanPATH = (id: string) => `${VAN_PATH}${id}`; +export const getVanPATH = (id: string) => `${VANS_PATH}/${id}`; +export const getVansForBackbonePATH = (backboneId: string) => `${BACKBONES_PATH}/${backboneId}/vans`; +export const getCreateVanPATH = (backboneId: string) => `${BACKBONES_PATH}/${backboneId}/vans`; +export const getEvictVanPATH = (id: string) => `${VANS_PATH}/${id}/evict`; + +// Invitation paths +const INVITATIONS_PATH = `${COLLECTOR_URL}/invitations`; +export const getInvitationsPATH = () => INVITATIONS_PATH; +export const getInvitationPATH = (id: string) => `${INVITATIONS_PATH}/${id}`; +export const getInvitationYamlPATH = (id: string) => `${INVITATIONS_PATH}/${id}/kube`; +export const getInvitationsForVanPATH = (vanId: string) => `${VANS_PATH}/${vanId}/invitations`; +export const getCreateInvitationPATH = (vanId: string) => `${VANS_PATH}/${vanId}/invitations`; +export const getExpireInvitationPATH = (id: string) => `${INVITATIONS_PATH}/${id}/expire`; + +// Member paths +const MEMBERS_PATH = `${COLLECTOR_URL}/members`; +export const getMemberPATH = (id: string) => `${MEMBERS_PATH}/${id}`; +export const getMembersForVanPATH = (vanId: string) => `${VANS_PATH}/${vanId}/members`; +export const getEvictMemberPATH = (id: string) => `${MEMBERS_PATH}/${id}/evict`; + +// TLS Certificate paths +const TLS_CERTIFICATES_PATH = `${COLLECTOR_URL}/tls-certificates`; +export const getTlsCertificatePATH = (id: string) => `${TLS_CERTIFICATES_PATH}/${id}`; + +// Target Platform paths +const TARGET_PLATFORMS_PATH = `${COLLECTOR_URL}/targetplatforms`; +export const getTargetPlatformsPATH = () => TARGET_PLATFORMS_PATH; + +// Site Deployment paths +export const getSiteDeploymentPATH = (siteId: string, target: 'sk2' | 'kube') => + `${COLLECTOR_URL}/backbonesite/${siteId}/${target}`; + +// Access Point Deployment paths +export const getAccessPointDeploymentPATH = (siteId: string, target: 'sk2' | 'kube') => + `${COLLECTOR_URL}/backbonesite/${siteId}/accesspoints/${target}`; + +// Ingress paths +export const getIngressPATH = (siteId: string) => `${COLLECTOR_URL}/backbonesite/${siteId}/ingress`; + +// Library Block paths +const LIBRARIES_PATH = `${BASE_URL_COLLECTOR}/compose/v1alpha1/library/blocks`; +export const getLibrariesPATH = () => LIBRARIES_PATH; +export const getLibraryPATH = (id: string) => `${LIBRARIES_PATH}/${id}`; +export const getLibraryConfigPATH = (id: string) => `${LIBRARIES_PATH}/${id}/config`; +export const getLibraryInterfacesPATH = (id: string) => `${LIBRARIES_PATH}/${id}/interfaces`; +export const getLibraryBodyPATH = (id: string) => `${LIBRARIES_PATH}/${id}/body`; +export const getLibraryHistoryPATH = (id: string) => `${LIBRARIES_PATH}/${id}/history`; +export const getLibraryBlockTypesPATH = () => `${BASE_URL_COLLECTOR}/compose/v1alpha1/library/blocktypes`; +export const getLibraryBodyStylesPATH = () => `${BASE_URL_COLLECTOR}/compose/v1alpha1/library/bodystyles`; +export const getInterfaceRolesPATH = () => `${BASE_URL_COLLECTOR}/compose/v1alpha1/interfaceroles`; -const INVITATION_PATH = `${COLLECTOR_URL}/invitation/`; -export const getInvitationPath = (id: string) => `${INVITATION_PATH}${id}`; +// Application paths +const APPLICATIONS_PATH = `${BASE_URL_COLLECTOR}/compose/v1alpha1/applications`; +export const getApplicationsPATH = () => APPLICATIONS_PATH; +export const getApplicationPATH = (id: string) => `${APPLICATIONS_PATH}/${id}`; +export const getApplicationBuildPATH = (id: string) => `${APPLICATIONS_PATH}/${id}/build`; +export const getApplicationLogPATH = (id: string) => `${APPLICATIONS_PATH}/${id}/log`; +export const getApplicationBlocksPATH = (id: string) => `${APPLICATIONS_PATH}/${id}/blocks`; -const MEMBER_PATH = `${COLLECTOR_URL}/member/`; -export const getMemberPath = (id: string) => `${MEMBER_PATH}${id}`; +// Deployment paths +const DEPLOYMENTS_PATH = `${BASE_URL_COLLECTOR}/compose/v1alpha1/deployments`; +export const getDeploymentsPATH = () => DEPLOYMENTS_PATH; +export const getDeploymentPATH = (id: string) => `${DEPLOYMENTS_PATH}/${id}`; +export const getDeploymentDeployPATH = (id: string) => `${DEPLOYMENTS_PATH}/${id}/deploy`; +export const getDeploymentLogPATH = (id: string) => `${DEPLOYMENTS_PATH}/${id}/log`; diff --git a/components/console/src/API/REST.utils.ts b/components/console/src/API/REST.utils.ts index be6a04a..b66e775 100644 --- a/components/console/src/API/REST.utils.ts +++ b/components/console/src/API/REST.utils.ts @@ -1,4 +1,3 @@ -import { SortDirection } from './REST.enum'; import { QueryParams, RequestOptions } from './REST.interfaces'; /** @@ -27,7 +26,7 @@ export function mapOptionsToQueryParams({ limit, timeRangeEnd, timeRangeStart, - sortBy: sortName ? `${sortName}.${sortDirection || SortDirection.ASC}` : null, + sortBy: sortName ? `${sortName}.${sortDirection || 'asc'}` : null, ...queryParams }; } diff --git a/components/console/src/API/apiMiddleware.ts b/components/console/src/API/apiMiddleware.ts index 4132d86..a3ad92e 100644 --- a/components/console/src/API/apiMiddleware.ts +++ b/components/console/src/API/apiMiddleware.ts @@ -1,8 +1,7 @@ import axios, { AxiosError } from 'axios'; -import { MSG_TIMEOUT_ERROR } from '@config/config'; - import { FetchWithOptions, HTTPError } from './REST.interfaces'; +import { MSG_TIMEOUT_ERROR } from '../config/config'; function handleStatusError(e: AxiosError<{ message?: string }>) { const error: HTTPError = { ...e }; diff --git a/components/console/src/App.css b/components/console/src/App.css index 383d092..931bfb9 100644 --- a/components/console/src/App.css +++ b/components/console/src/App.css @@ -8,15 +8,48 @@ html { height: 100%; } -.sk-capitalize { - text-transform: capitalize; +/* Common utility classes using PatternFly v6 tokens */ +.sk-text-secondary { + font-size: var(--pf-t--global--FontSize--sm); + color: var(--pf-t--global--Color--200); +} + +.sk-text-error { + font-size: var(--pf-t--global--FontSize--xs); + color: var(--pf-t--global--danger-color--100); + margin-top: var(--pf-t--global--spacer--xs); +} + +.sk-text-small { + font-size: var(--pf-t--global--FontSize--sm); +} + +.sk-margin-bottom-md { + margin-bottom: var(--pf-t--global--spacer--md); +} + +.sk-margin-bottom-lg { + margin-bottom: var(--pf-t--global--spacer--lg); +} + +.sk-margin-top-xs { + margin-top: var(--pf-t--global--spacer--xs); +} + +.sk-margin-top-sm { + margin-top: var(--pf-t--global--spacer--sm); } .color-box { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; height: 18px; width: 18px; - border: var(--pf-v5-global--BorderWidth--sm) solid var(--pf-v5-global--BorderColor--200); - border-radius: var(--pf-v5-global--BorderRadius--lg); + border: var(--pf-tv5-global--BorderWidth--sm) solid var(--pf-t-global--BorderColor--200); + border-radius: 50%; vertical-align: middle; + background: currentColor; + /* Optionally, add a subtle box-shadow for better visibility */ + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); } diff --git a/components/console/src/App.tsx b/components/console/src/App.tsx index 831668e..49f1834 100644 --- a/components/console/src/App.tsx +++ b/components/console/src/App.tsx @@ -4,36 +4,35 @@ import { Page, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from '@patte import { QueryErrorResetBoundary } from '@tanstack/react-query'; import { ErrorBoundary } from 'react-error-boundary'; -import SkBreadcrumb from '@core/components/SkBreadcrumb'; -import SkUpdateDataButton from '@core/components/SkUpdateButton'; -import { getThemePreference, reflectThemePreference } from '@core/utils/isDarkTheme'; -import SkHeader from '@layout/Header'; -import RouteContainer from '@layout/RouteContainer'; -import ErrorConsole from '@pages/shared/Errors/Console'; -import LoadingPage from '@pages/shared/Loading'; -import { routes } from 'routes'; - import '@patternfly/react-core/dist/styles/base.css'; import './App.css'; +import SkBreadcrumb from './core/components/SkBreadcrumb'; +import SkUpdateDataButton from './core/components/SkUpdateButton'; +import SkHeader from './layout/Header'; +import RouteContainer from './layout/RouteContainer'; +import SkSidebar from './layout/SideBar'; +import ErrorConsole from './pages/shared/Errors/Console'; +import LoadingPage from './pages/shared/Loading'; +import { routes } from './routes'; const App = function () { - reflectThemePreference(getThemePreference()); - return ( } + masthead={} + sidebar={} breadcrumb={ - - + + - + } + isContentFilled isManagedSidebar isBreadcrumbGrouped additionalGroupedContent={ diff --git a/components/console/src/assets/skupper-logo.svg b/components/console/src/assets/skupper-logo.svg index bd0e38c..e850e65 100644 --- a/components/console/src/assets/skupper-logo.svg +++ b/components/console/src/assets/skupper-logo.svg @@ -1 +1,2 @@ -skupperlogo_rgb_horz_reverse \ No newline at end of file + +skupperlogo_rgb_horz_default \ No newline at end of file diff --git a/components/console/src/config/colors.ts b/components/console/src/config/colors.ts index 8484606..06544cf 100644 --- a/components/console/src/config/colors.ts +++ b/components/console/src/config/colors.ts @@ -1,47 +1,92 @@ -export enum Colors { - White = '--pf-v5-global--palette--white', - Black900 = '--pf-v5-global--palette--black-900', - Black600 = '--pf-v5-global--palette--black-600', - Black500 = '--pf-v5-global--palette--black-500', - Black400 = '--pf-v5-global--palette--black-400', - Black100 = '--pf-v5-global--palette--black-100', - Green500 = '--pf-v5-global--palette--green-500', - Blue400 = '--pf-v5-global--palette--blue-400', - Purple500 = '--pf-v5-global--palette--purple-500', - Cyan300 = '--pf-v5-global--palette--cyan-300', - Orange200 = '--pf-v5-global--palette--orange-200', - Yellow300 = '--pf-v5-global--palette--gold-300', - Red100 = '--pf-v5-global--palette--red-100' -} +import { + t_color_gray_90, + t_color_gray_50, + t_color_gray_30, + t_color_green_50, + t_color_green_60, + t_color_blue_40, + t_color_blue_50, + t_color_blue_60, + t_color_purple_10, + t_color_purple_20, + t_color_purple_30, + t_color_purple_40, + t_color_purple_50, + t_color_purple_60, + t_color_purple_70, + t_color_orange_10, + t_color_orange_30, + t_color_orange_40, + t_color_orange_70, + t_color_red_50, + t_color_red_60, + t_color_teal_50, + t_color_teal_60, + t_color_white, + t_global_font_size_body_default, + t_global_font_family_100, + t_global_border_width_100, + t_global_border_radius_large, + t_global_font_weight_200 +} from '@patternfly/react-tokens'; -export enum VarColors { - White = `var(${Colors.White})`, - Black100 = `var(${Colors.Black100})`, - Black400 = `var(${Colors.Black400})`, - Black500 = `var(${Colors.Black500})`, - Black600 = `var(${Colors.Black600})`, - Black900 = `var(${Colors.Black900})`, - Green500 = `var(${Colors.Green500})`, - Blue400 = `var(${Colors.Blue400})`, - Red100 = `var(${Colors.Red100})`, - Orange200 = `var(${Colors.Orange200})`, - Yellow300 = `var(${Colors.Yellow300})`, - Purple500 = `var(${Colors.Purple500})` -} +export const hexColors = { + White: t_color_white.value, + Black300: t_color_gray_30.value, + Black500: t_color_gray_50.value, + Black900: t_color_gray_90.value, + Blue100: t_color_blue_40.value, + Blue400: t_color_blue_40.value, + Blue500: t_color_blue_50.value, + Blue600: t_color_blue_60.value, -export enum HexColors { - White = '#FFFFFF', - Red100 = '#c9190b', - Orange200 = '#ef9234', - Purple500 = '#6753ac', - Green500 = '#3e8635', - Blue200 = '#73BCF7', - Blue400 = '#0066CC', - Black100 = '#F0F0F0', - Black400 = '#B8BBBE', - Black300 = '#D2D2d2', - Black500 = '#8A8D90', - Black600 = '#6A6E73', - Black800 = '#3C3F42', - Black900 = '#151515' -} + Purple100: t_color_purple_10.value, + Purple200: t_color_purple_20.value, + Purple300: t_color_purple_30.value, + Purple400: t_color_purple_40.value, + Purple500: t_color_purple_50.value, + Purple600: t_color_purple_60.value, + Purple700: t_color_purple_70.value, + Orange100: t_color_orange_10.value, + Orange300: t_color_orange_30.value, + Orange400: t_color_orange_40.value, + Orange700: t_color_orange_70.value, + Red500: t_color_red_50.value, + Red600: t_color_red_60.value, + Teal500: t_color_teal_50.value, + Teal600: t_color_teal_60.value, + Green500: t_color_green_50.value, + Green600: t_color_green_60.value, + Cyan500: t_color_teal_50.value, + Yellow500: t_color_orange_10.value, + Pink500: t_color_purple_50.value, + Indigo500: t_color_blue_40.value, + DeepOrange500: t_color_orange_70.value, + Lime500: t_color_green_50.value, + LightBlue500: t_color_blue_40.value, + DeepPurple500: t_color_purple_50.value, + Amber500: t_color_orange_10.value, + Brown500: t_color_gray_50.value, + Grey100: t_color_gray_30.value, + Grey300: t_color_gray_50.value, + Grey500: t_color_gray_50.value, + Grey900: t_color_gray_90.value, + BlueGrey500: t_color_gray_90.value +}; + +export const styles = { + default: { + fontSize: t_global_font_size_body_default, + fontLightBold: t_global_font_weight_200, + fontFamily: t_global_font_family_100.value, + borderWidth: t_global_border_width_100.value, + borderRadius: t_global_border_radius_large.value, + lightBackgroundColor: hexColors.White, + lightTextColor: hexColors.White, + darkBackgroundColor: hexColors.Black500, + darkTextColor: hexColors.Black900, + infoColor: hexColors.Blue400, + errorColor: hexColors.Red600, + warningColor: hexColors.Orange300 + } +}; diff --git a/components/console/src/config/config.ts b/components/console/src/config/config.ts index f320cb3..0016b70 100644 --- a/components/console/src/config/config.ts +++ b/components/console/src/config/config.ts @@ -1,33 +1,25 @@ -import Logo from '@assets/skupper-logo.svg'; +import Logo from '../assets/skupper-logo.svg'; /** URL config: contains configuration options and constants related to backend URLs and routing */ -const BASE_URL_COLLECTOR = process.env.COLLECTOR_URL || `${window.location.protocol}//${window.location.host}`; +export const BASE_URL_COLLECTOR = process.env.COLLECTOR_URL || `${window.location.protocol}//${window.location.host}`; const API_VERSION = '/api/v1alpha1'; const PROMETHEUS_SUFFIX = '/internal/prom'; // Base URL for the collector backend. Defaults to current host if not set in environment variables. -export const COLLECTOR_URL = `${BASE_URL_COLLECTOR}${API_VERSION}`; +// In development, webpack proxy handles API routing, so we use relative paths +const isDevelopment = typeof window !== 'undefined' && window.location.hostname === 'localhost'; +export const COLLECTOR_URL = isDevelopment ? API_VERSION : `${BASE_URL_COLLECTOR}${API_VERSION}`; export const PROMETHEUS_URL = `${COLLECTOR_URL}${PROMETHEUS_SUFFIX}`; // Default page size for tables. Set in environment variables, but can be overridden. export const DEFAULT_PAGINATION_SIZE = 10; -export const BIG_PAGINATION_SIZE = 20; -export const SMALL_PAGINATION_SIZE = 10; // Brand export const brandLogo = process.env.BRAND_APP_LOGO ? require(process.env.BRAND_APP_LOGO) : Logo; /** General config: contains various global settings and constants */ -export const UPDATE_INTERVAL = 9 * 1000; // Time in milliseconds to request updated data from the backend export const MSG_TIMEOUT_ERROR = 'The request to fetch the data has timed out.'; // Error message to display when request times out -export const TOAST_VISIBILITY_TIMEOUT = 2000; // Time in milliseconds to display toast messages export const ALERT_VISIBILITY_TIMEOUT = 5000; // Time in milliseconds to display toast messages -/** Tests */ -export const waitForElementToBeRemovedTimeout = 10000; - -export const DARK_THEME_CLASS = 'pf-v5-theme-dark'; -export const DEFAULT_FONT_VAR = 'var(--pf-v5-global--FontFamily--text)'; - -// number of nodes to start showing the aggregate nodes in the topology -export const MAX_NODE_COUNT_WITHOUT_AGGREGATION = Number(process.env.MAX_NODE_COUNT_WITHOUT_AGGREGATION) || 26; +/** Platform config: contains platform-related constants */ +export const DEFAULT_PLATFORM = 'kube'; // Default target platform for new sites diff --git a/components/console/src/config/navGroups.ts b/components/console/src/config/navGroups.ts new file mode 100644 index 0000000..ed29bd7 --- /dev/null +++ b/components/console/src/config/navGroups.ts @@ -0,0 +1,33 @@ +import { BackbonesPaths } from '../pages/Backbones/Backbones.constants'; +import { VansPaths } from '../pages/Vans/Vans.constants'; +import { LibraryPaths } from '../pages/Libraries/Libraries.constants'; +import { TopologyPaths } from '../pages/Topology/Topology.constants'; +import { ApplicationPaths } from '../pages/Applications/Applications.constants'; +import labels from '../core/config/labels.json'; +import { DeploymentPaths } from '../pages/Deployment/Deployments.constants'; + +// Navigation groupings for sidebar +export const NAV_GROUPS = [ + { + label: labels.navigation.network, + items: [ + { path: TopologyPaths.path, name: TopologyPaths.name }, + { path: BackbonesPaths.path, name: BackbonesPaths.name }, + { path: VansPaths.path, name: VansPaths.name } + ] + }, + { + label: labels.navigation.inventory, + items: [ + { path: LibraryPaths.path, name: LibraryPaths.name }, + { path: ApplicationPaths.path, name: ApplicationPaths.name } + ] + }, + { + label: labels.navigation.runtime, + items: [{ path: DeploymentPaths.path, name: DeploymentPaths.name }] + } +]; + +// Single items (not in groups) - currently empty +export const NAV_SINGLE_ITEMS = []; diff --git a/components/console/src/config/reactQuery.ts b/components/console/src/config/reactQuery.ts index a672760..ff0c292 100644 --- a/components/console/src/config/reactQuery.ts +++ b/components/console/src/config/reactQuery.ts @@ -1,7 +1,10 @@ -import { QueryObserverOptions } from '@tanstack/react-query'; +import { DefaultOptions } from '@tanstack/react-query'; interface QueryClientConfig { - defaultOptions: { queries: QueryObserverOptions }; + defaultOptions: { + queries: Partial; + suspense?: boolean; + }; } /** React query library config: contains configuration options for the React query library, used for fetching and caching data in the UI */ @@ -11,9 +14,13 @@ export const queryClientConfig: QueryClientConfig = { retry: 3, retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000), refetchOnWindowFocus: false, - refetchIntervalInBackground: true, - suspense: false, - throwOnError: true - } + refetchOnReconnect: false, + refetchIntervalInBackground: false, + throwOnError: true, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000 // 10 minutes - prevents memory buildup + }, + // If you need suspense, add it at the root level + suspense: false } }; diff --git a/components/console/src/config/routes.ts b/components/console/src/config/routes.ts index 14bc318..2d54510 100644 --- a/components/console/src/config/routes.ts +++ b/components/console/src/config/routes.ts @@ -1,5 +1,7 @@ -import { BackbonesPaths } from '@pages/Backbones/Backbones.constants'; +import { BackbonesPaths } from '../pages/Backbones/Backbones.constants'; +import { VansPaths } from '../pages/Vans/Vans.constants'; +import { TopologyPaths } from '../pages/Topology/Topology.constants'; // Navigation config -export const ROUTES = [BackbonesPaths]; +export const ROUTES = [TopologyPaths, BackbonesPaths, VansPaths]; export const DEFAULT_ROUTE = ROUTES[0].path; diff --git a/components/console/src/config/testIds.ts b/components/console/src/config/testIds.ts deleted file mode 100644 index aab10c0..0000000 --- a/components/console/src/config/testIds.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const getTestsIds = { - loadingView: () => 'sk-loading-view', - header: () => 'sk-header', - sitesView: () => 'sk-sites-view', - componentsView: () => 'sk-components-view', - processesView: () => 'sk-processes-view', - processView: (id: string) => `sk-process-view-${id}`, - processPairsView: (id: string) => `sk-process-pairs-view-${id}`, - flowPairsView: (id: string) => `sk-flow-pairs-view-${id}`, - servicesView: () => 'sk-services-view', - serviceView: (id: string) => `sk-service-view-${id}`, - topologyView: () => 'sk-topology-view', - notFoundView: () => `sk-not-found-view`, - siteView: (id: string) => `sk-site-view-${id}`, - componentView: (id: string) => `sk-component-view-${id}`, - navbarComponent: () => 'sk-nav-bar-component', - breadcrumbComponent: () => 'sk-breadcrumb' -}; diff --git a/components/console/src/core/components/ActionButtons/ActionButtons.enum.ts b/components/console/src/core/components/ActionButtons/ActionButtons.enum.ts new file mode 100644 index 0000000..e69de29 diff --git a/components/console/src/core/components/ActionButtons/CreateButton.tsx b/components/console/src/core/components/ActionButtons/CreateButton.tsx new file mode 100644 index 0000000..f1a3367 --- /dev/null +++ b/components/console/src/core/components/ActionButtons/CreateButton.tsx @@ -0,0 +1,40 @@ +import { FC, ReactNode } from 'react'; + +import { Button } from '@patternfly/react-core'; +import { PlusIcon } from '@patternfly/react-icons'; + +export interface CreateButtonProps { + onClick: () => void; + disabled?: boolean; + isLoading?: boolean; + children?: ReactNode; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'link' | 'plain' | 'control'; + showIcon?: boolean; + icon?: ReactNode; +} + +/** + * Reusable Create button component + * Provides consistent create action across the application + */ +export const CreateButton: FC = ({ + onClick, + disabled = false, + isLoading = false, + children = 'Create', + variant = 'primary', + showIcon = true, + icon +}) => ( + +); + +export default CreateButton; diff --git a/components/console/src/core/components/ActionButtons/DeleteButton.tsx b/components/console/src/core/components/ActionButtons/DeleteButton.tsx new file mode 100644 index 0000000..62daf4e --- /dev/null +++ b/components/console/src/core/components/ActionButtons/DeleteButton.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react'; + +import { Button } from '@patternfly/react-core'; +import { TrashIcon } from '@patternfly/react-icons'; +import labels from '../../config/labels'; + +export interface DeleteButtonProps { + onClick: () => void; + disabled?: boolean; + isLoading?: boolean; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'link' | 'plain' | 'control'; +} + +/** + * Reusable Delete button component + * Provides consistent delete action across the application + */ +export const DeleteButton: FC = ({ + onClick, + disabled = false, + isLoading = false, + variant = 'link' +}) => ( + +); + +export default DeleteButton; diff --git a/components/console/src/core/components/ActionButtons/EditButton.tsx b/components/console/src/core/components/ActionButtons/EditButton.tsx new file mode 100644 index 0000000..fb47908 --- /dev/null +++ b/components/console/src/core/components/ActionButtons/EditButton.tsx @@ -0,0 +1,24 @@ +import { FC } from 'react'; + +import { Button } from '@patternfly/react-core'; +import { EditIcon } from '@patternfly/react-icons'; +import labels from '../../config/labels'; + +export interface EditButtonProps { + onClick: () => void; + disabled?: boolean; + isLoading?: boolean; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'link' | 'plain' | 'control'; +} + +/** + * Reusable Edit button component + * Provides consistent edit action across the application + */ +export const EditButton: FC = ({ onClick, disabled = false, isLoading = false, variant = 'link' }) => ( + +); + +export default EditButton; diff --git a/components/console/src/core/components/ActionButtons/index.tsx b/components/console/src/core/components/ActionButtons/index.tsx new file mode 100644 index 0000000..849eb0f --- /dev/null +++ b/components/console/src/core/components/ActionButtons/index.tsx @@ -0,0 +1,98 @@ +import { FC, ReactNode } from 'react'; + +import { Button, OverflowMenu, OverflowMenuContent, OverflowMenuGroup, OverflowMenuItem } from '@patternfly/react-core'; +import { EditIcon, TrashIcon } from '@patternfly/react-icons'; +import labels from '../../../core/config/labels'; + +// Re-export individual button components +export { EditButton } from './EditButton'; +export { DeleteButton } from './DeleteButton'; +export { CreateButton } from './CreateButton'; + +export interface ActionButtonsProps { + /** The data item for this row */ + data: T; + /** Callback when edit is clicked */ + onEdit?: (item: T) => void; + /** Callback when delete is clicked */ + onDelete?: (id: string) => void; + /** Additional custom actions */ + customActions?: { + label: string; + icon?: ReactNode; + onClick: (item: T) => void; + variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'link' | 'plain' | 'control'; + isDanger?: boolean; + isDisabled?: boolean; + }[]; + /** Whether to show edit action */ + showEdit?: boolean; + /** Whether to show delete action */ + showDelete?: boolean; + /** Whether to use compact layout (for smaller spaces) */ + isCompact?: boolean; +} + +/** + * Reusable action buttons component for table rows and cards + * Provides consistent edit/delete/custom actions across the application + */ +export const ActionButtons: FC = function ({ + data, + onEdit, + onDelete, + customActions = [], + showEdit = true, + showDelete = true, + isCompact = false +}) { + const hasActions = (showEdit && onEdit) || (showDelete && onDelete) || customActions.length > 0; + + if (!hasActions) { + return null; + } + + const editAction = showEdit && onEdit && ( + + + + ); + + const deleteAction = showDelete && onDelete && ( + + + + ); + + const customActionItems = customActions.map((action, index) => ( + + + + )); + + return ( + + + + {editAction} + {customActionItems} + {deleteAction} + + + + ); +}; + +export default ActionButtons; diff --git a/components/console/src/core/components/DurationCell/DurationCell.ts b/components/console/src/core/components/DurationCell/DurationCell.ts deleted file mode 100644 index d7edad4..0000000 --- a/components/console/src/core/components/DurationCell/DurationCell.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ReactNode } from 'react'; - -export interface DurationCellProps { - data: T; - value: ReactNode; - startTime: number; - endTime: number; -} diff --git a/components/console/src/core/components/DurationCell/index.tsx b/components/console/src/core/components/DurationCell/index.tsx deleted file mode 100644 index 5ea6a20..0000000 --- a/components/console/src/core/components/DurationCell/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Tooltip } from '@patternfly/react-core'; -import { TableText } from '@patternfly/react-table'; - -import { formatTimeInterval } from '@core/utils/formatTimeInterval'; - -import { DurationCellProps } from './DurationCell'; - -/** - * startTime and endTime are expected to be in microseconds - */ -const DurationCell = function ({ startTime, endTime }: DurationCellProps) { - const duration = formatTimeInterval(endTime, startTime); - - return ( - - {duration} - - ); -}; - -export default DurationCell; diff --git a/components/console/src/core/components/EmptyData/EmptyData.enum.ts b/components/console/src/core/components/EmptyData/EmptyData.enum.ts index 8af44f4..e69de29 100644 --- a/components/console/src/core/components/EmptyData/EmptyData.enum.ts +++ b/components/console/src/core/components/EmptyData/EmptyData.enum.ts @@ -1,3 +0,0 @@ -export enum EmptyDataLabels { - Default = 'no data found' -} diff --git a/components/console/src/core/components/EmptyData/EmptyData.spec.tsx b/components/console/src/core/components/EmptyData/EmptyData.spec.tsx deleted file mode 100644 index b94a31d..0000000 --- a/components/console/src/core/components/EmptyData/EmptyData.spec.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { render, screen } from '@testing-library/react'; - -import { EmptyDataLabels } from './EmptyData.enum'; - -import EmptyData from './index'; - -describe('EmptyData component', () => { - it('should render with default message', () => { - render(); - expect(screen.getByText(EmptyDataLabels.Default)).toBeInTheDocument(); - }); - - it('should render with custom message', () => { - render(); - expect(screen.getByText('Custom Message')).toBeInTheDocument(); - }); - - it('should render with description', () => { - render(); - expect(screen.getByText('This is a description')).toBeInTheDocument(); - }); - - it('should render with custom icon', () => { - const CustomIcon = function () { - return
Custom Icon
; - }; - render(); - expect(screen.getByText('Custom Icon')).toBeInTheDocument(); - }); -}); diff --git a/components/console/src/core/components/EmptyData/index.tsx b/components/console/src/core/components/EmptyData/index.tsx index 2d982ca..ffec486 100644 --- a/components/console/src/core/components/EmptyData/index.tsx +++ b/components/console/src/core/components/EmptyData/index.tsx @@ -1,15 +1,7 @@ import { ComponentType, FC } from 'react'; -import { - Bullseye, - EmptyState, - EmptyStateBody, - EmptyStateHeader, - EmptyStateIcon, - EmptyStateVariant -} from '@patternfly/react-core'; - -import { EmptyDataLabels } from './EmptyData.enum'; +import { Bullseye, EmptyState, EmptyStateBody, EmptyStateVariant } from '@patternfly/react-core'; +import { CubesIcon } from '@patternfly/react-icons'; interface EmptyDataProps { message?: string; @@ -17,11 +9,10 @@ interface EmptyDataProps { icon?: ComponentType; } -const EmptyData: FC = function ({ message = EmptyDataLabels.Default, description, icon }) { +const EmptyData: FC = function ({ message, description, icon }) { return ( - - } /> + {description && {description}} diff --git a/components/console/src/core/components/FormWrapper/FormWrapper.enum.ts b/components/console/src/core/components/FormWrapper/FormWrapper.enum.ts new file mode 100644 index 0000000..e69de29 diff --git a/components/console/src/core/components/FormWrapper/index.tsx b/components/console/src/core/components/FormWrapper/index.tsx new file mode 100644 index 0000000..3c80699 --- /dev/null +++ b/components/console/src/core/components/FormWrapper/index.tsx @@ -0,0 +1,182 @@ +import { FC, ReactNode, FormEvent, ComponentProps } from 'react'; + +import { + Form, + FormGroup, + TextInput, + ActionGroup, + Button, + FormAlert, + Alert, + FormSelect, + FormSelectOption, + Checkbox, + ValidatedOptions +} from '@patternfly/react-core'; + +import { FormActionLabels } from './FormWrapper.enum'; + +export interface FormFieldProps { + id: string; + label: string; + type: 'text' | 'number' | 'email' | 'password' | 'datetime-local' | 'select' | 'checkbox'; + value?: string | boolean; + onChange: (value: string | boolean) => void; + isRequired?: boolean; + placeholder?: string; + options?: { value: string; label: string }[]; // For select fields + description?: string; // For checkbox descriptions + isDisabled?: boolean; + validated?: ValidatedOptions; + helperText?: string; + min?: string | number; // For number inputs + max?: string | number; // For number inputs +} + +export interface FormWrapperProps { + /** Form title for accessibility */ + title?: string; + /** Form fields configuration */ + fields: FormFieldProps[]; + /** Current validation error message */ + validationError?: string; + /** Submit button text */ + submitText?: string; + /** Cancel button text */ + cancelText?: string; + /** Submit handler */ + onSubmit: () => void; + /** Cancel handler */ + onCancel: () => void; + /** Whether form is in loading state */ + isLoading?: boolean; + /** Whether to use horizontal layout */ + isHorizontal?: boolean; + /** Custom form content to render after standard fields */ + customContent?: ReactNode; + /** Additional form props */ + formProps?: ComponentProps; +} + +/** + * Reusable form wrapper component that handles common form patterns + * Provides consistent form layout, validation, and submission handling + */ +export const FormWrapper: FC = function ({ + title, + fields, + validationError, + submitText = FormActionLabels.Submit, + cancelText = FormActionLabels.Cancel, + onSubmit, + onCancel, + isLoading = false, + isHorizontal = true, + customContent, + formProps = {} +}) { + const handleSubmit = (event?: FormEvent) => { + event?.preventDefault(); + onSubmit(); + }; + + const renderField = (field: FormFieldProps) => { + const baseProps = { + id: field.id, + name: field.id, + isDisabled: field.isDisabled || isLoading, + validated: field.validated + }; + + switch (field.type) { + case 'text': + case 'number': + case 'email': + case 'password': + case 'datetime-local': + return ( + field.onChange(value)} + placeholder={field.placeholder} + isRequired={field.isRequired} + min={field.min} + max={field.max} + /> + ); + + case 'select': + return ( + field.onChange(value)} + aria-label={field.label} + > + {field.options?.map((option) => ( + + ))} + + ); + + case 'checkbox': + return ( + field.onChange(checked)} + description={field.description} + /> + ); + + default: + return null; + } + }; + + return ( +
+ {title && } + + {validationError && ( + + + + )} + + {fields.map((field) => { + if (field.type === 'checkbox') { + return ( + + {renderField(field)} + {field.helperText &&
{field.helperText}
} +
+ ); + } + + return ( + + {renderField(field)} + {field.helperText &&
{field.helperText}
} +
+ ); + })} + + {customContent} + + + + + +
+ ); +}; + +export default FormWrapper; diff --git a/components/console/src/core/components/Graph/Graph.constants.ts b/components/console/src/core/components/Graph/Graph.constants.ts deleted file mode 100644 index 4884808..0000000 --- a/components/console/src/core/components/Graph/Graph.constants.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { ILabelConfig, LayoutConfig, ModelStyle, Modes, GraphOptions } from '@antv/g6-core'; - -import { HexColors } from '@config/colors'; - -export const GRAPH_BG_COLOR = HexColors.White; -const NODE_COLOR_DEFAULT = HexColors.White; -const NODE_BORDER_COLOR_DEFAULT = HexColors.Black300; -const NODE_COLOR_DEFAULT_LABEL = HexColors.Black900; -//const NODE_COLOR_DEFAULT_LABEL_BG = HexColors.White; -export const EDGE_COLOR_DEFAULT = HexColors.Black600; -export const EDGE_COLOR_ENDPOINT_SITE_CONNECTION_DEFAULT = HexColors.Blue400; -export const EDGE_COLOR_HOVER_DEFAULT = HexColors.Blue400; -export const EDGE_COLOR_DEFAULT_TEXT = HexColors.Black900; -const COMBO__COLOR_DEFAULT = HexColors.Black100; -const COMBO_BORDER_COLOR_DEFAULT = HexColors.White; -const COMBO_BORDER_COLOR_HOVER = HexColors.Black900; -const COMBO_COLOR_DEFAULT_LABEL = HexColors.White; -const COMBO_COLOR_DEFAULT_LABEL_BG = HexColors.Black900; - -export enum TopologyModeNames { - Default = 'default', - Performance = 'performance' -} - -export const NODE_SIZE = 36; -const ICON_SIZE = 15; -const LABEL_FONT_SIZE = 8; -// number of nodes to start showing less topology details for events like zooming in/out and dragging -export const NODE_COUNT_PERFORMANCE_THRESHOLD = 150; - -export const CUSTOM_ITEMS_NAMES = { - animatedDashEdge: 'line-dash', - siteEdge: 'site-edge', - loopEdge: 'loop', - nodeWithBadges: 'nCircle', - comboWithCustomLabel: 'cRect', - defaultNode: 'cRect' -}; - -export const BADGE_STYLE = { - containerBg: HexColors.Black500, - containerBorderColor: HexColors.White, - textColor: HexColors.White, - textFontSize: LABEL_FONT_SIZE - 1 -}; - -const INACTIVE_OPACITY_VALUE = 0.3; - -const DEFAULT_MODE: Modes = { - [TopologyModeNames.Default]: [ - { type: 'drag-node', onlyChangeComboSize: true }, - { - type: 'drag-combo', - enableDelegate: true, - activeState: 'actived', - onlyChangeComboSize: true, - shouldUpdate: () => true - }, - { type: 'drag-canvas' }, - { type: 'zoom-canvas' } - ], - [TopologyModeNames.Performance]: [ - { type: 'drag-node', onlyChangeComboSize: true, enableOptimize: true }, - { - type: 'drag-combo', - enableDelegate: true, - activeState: 'actived', - onlyChangeComboSize: true, - shouldUpdate: () => true, - enableOptimize: true - }, - { type: 'drag-canvas', enableOptimize: true }, - { type: 'zoom-canvas', enableOptimize: true, optimizeZoom: 0.1 } - ] -}; - -export const LAYOUT_TOPOLOGY_DEFAULT: LayoutConfig = { - type: 'force2', - nodeSize: NODE_SIZE, - nodeSpacing: NODE_SIZE, - preventOverlap: true, - clustering: true, - nodeClusterBy: 'cluster', - distanceThresholdMode: 'max', - clusterNodeStrength: 500000, - nodeStrength: 5000, - leafCluster: true, - gravity: 10000, - maxSpeed: 1000, - animate: false, - factor: 800 -}; - -export const LAYOUT_TOPOLOGY_SINGLE_NODE = { - type: 'dagre', - rankdir: 'BT' -}; - -export const DEFAULT_NODE_ICON = { - show: true, - width: ICON_SIZE, - height: ICON_SIZE -}; - -export const DEFAULT_NODE_CONFIG: Partial<{ - type: string; - size: number | number[]; - labelCfg: ILabelConfig; - color: string; -}> & - ModelStyle = { - type: CUSTOM_ITEMS_NAMES.nodeWithBadges, - size: [NODE_SIZE], - - icon: DEFAULT_NODE_ICON, - - style: { - fill: NODE_COLOR_DEFAULT, - stroke: NODE_BORDER_COLOR_DEFAULT, - lineWidth: 0.8 - }, - - labelCfg: { - position: 'bottom', - offset: 8, - style: { - fill: NODE_COLOR_DEFAULT_LABEL, - fontSize: LABEL_FONT_SIZE, - background: { - fill: 'transparent', - stroke: NODE_BORDER_COLOR_DEFAULT, - lineWidth: 0, - padding: [3, 4], - radius: 2 - } - } - } -}; - -export const DEFAULT_REMOTE_NODE_CONFIG: Partial<{ - type: string; - size: number | number[]; - color: string; -}> & - ModelStyle = { - ...DEFAULT_NODE_CONFIG, - type: 'circle', - size: [NODE_SIZE / 2], - - icon: { - show: false - }, - - style: { - fill: NODE_BORDER_COLOR_DEFAULT, - stroke: NODE_COLOR_DEFAULT - } -}; - -export const DEFAULT_EDGE_CONFIG: Partial<{ - type: string; - size: number | number[]; - color: string; - labelCfg: ILabelConfig; -}> & - ModelStyle = { - type: CUSTOM_ITEMS_NAMES.animatedDashEdge, - labelCfg: { - autoRotate: true, - style: { - fill: EDGE_COLOR_DEFAULT_TEXT, - stroke: GRAPH_BG_COLOR, - lineWidth: 5, - fontSize: LABEL_FONT_SIZE - } - }, - style: { - cursor: 'pointer', - opacity: 1, - lineWidth: 0.5, - lineDash: [0, 0, 0, 0], - lineAppendWidth: 20, - stroke: EDGE_COLOR_DEFAULT, - endArrow: { - path: 'M 0,0 L 8,4 L 8,-4 Z', - fill: EDGE_COLOR_DEFAULT - } - } -}; - -const DEFAULT_COMBO_CONFIG: ModelStyle & { - labelBgCfg: { - fill: string; - radius: number; - padding: number[]; - }; -} = { - type: CUSTOM_ITEMS_NAMES.comboWithCustomLabel, - padding: [15, 15, 30, 15], - style: { - cursor: 'pointer', - lineWidth: 4, - fill: COMBO__COLOR_DEFAULT, - stroke: COMBO_BORDER_COLOR_DEFAULT, - radius: 10 - }, - labelCfg: { - refY: 12, - position: 'bottom', - style: { - fill: COMBO_COLOR_DEFAULT_LABEL, - fontSize: LABEL_FONT_SIZE + 2 - } - }, - labelBgCfg: { - fill: COMBO_COLOR_DEFAULT_LABEL_BG, - radius: 2, - padding: [12, 10] - } -}; - -const DEFAULT_NODE_STATE_CONFIG = { - hover: { - shadowOffsetX: 0, - shadowOffsetY: 6, - shadowColor: HexColors.Black600, - shadowBlur: 14, - - [`${CUSTOM_ITEMS_NAMES.nodeWithBadges}-icon`]: { - cursor: 'default' - }, - 'diamond-icon': { - cursor: 'default' - } - }, - - 'selected-default': { - stroke: HexColors.Blue400, - strokeWidth: 1, - - 'text-shape': { - fill: HexColors.White - }, - - 'text-bg-shape': { - fill: HexColors.Blue400, - stroke: HexColors.Blue400 - } - }, - - hidden: { - opacity: INACTIVE_OPACITY_VALUE, - - 'text-shape': { - fillOpacity: INACTIVE_OPACITY_VALUE - }, - - 'text-bg-shape': { - opacity: INACTIVE_OPACITY_VALUE - }, - - 'circle-icon': { - opacity: INACTIVE_OPACITY_VALUE - }, - - [`${CUSTOM_ITEMS_NAMES.nodeWithBadges}-icon`]: { - opacity: INACTIVE_OPACITY_VALUE - }, - - [`${CUSTOM_ITEMS_NAMES.nodeWithBadges}-notification-container`]: { - opacity: INACTIVE_OPACITY_VALUE - }, - - 'diamond-icon': { - opacity: INACTIVE_OPACITY_VALUE - } - } -}; - -const DEFAULT_COMBO_STATE_CONFIG = { - hover: { - stroke: COMBO_BORDER_COLOR_HOVER - } -}; - -export const DEFAULT_GRAPH_CONFIG: Partial = { - modes: DEFAULT_MODE, - defaultNode: DEFAULT_NODE_CONFIG, - defaultCombo: DEFAULT_COMBO_CONFIG, - defaultEdge: DEFAULT_EDGE_CONFIG, - nodeStateStyles: DEFAULT_NODE_STATE_CONFIG, - comboStateStyles: DEFAULT_COMBO_STATE_CONFIG, - fitView: true, - fitViewPadding: 20 -}; diff --git a/components/console/src/core/components/Graph/Graph.interfaces.ts b/components/console/src/core/components/Graph/Graph.interfaces.ts deleted file mode 100644 index c87b7b1..0000000 --- a/components/console/src/core/components/Graph/Graph.interfaces.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ComponentType, MutableRefObject } from 'react'; - -import { GraphData, LayoutConfig, ModelConfig, ModelStyle, ShapeStyle } from '@antv/g6'; - -import { HexColors } from '@config/colors'; - -export interface GraphNode { - id: string; - label: string; - comboId?: string; - comboName?: string; - groupId?: string; - groupName?: string; - groupCount?: number; - type?: string; - enableBadge1?: boolean; - notificationValue?: number; - icon?: { - show?: boolean; - img?: string; - width?: number; - height?: number; - }; - style?: Record; - x?: number | undefined; - y?: number | undefined; - fx?: number | undefined; - fy?: number | undefined; - persistPositionKey?: string; - data: Record; -} - -export interface GraphCombo { - id: string; - label: string; - style?: ModelStyle; -} - -export interface GraphEdge { - id: string; - source: string; - target: string; - sourceName?: string; - targetName?: string; - type?: string; - label?: string; - labelCfg?: Record; - style?: ShapeStyle; - metrics?: { - protocol: string | undefined; - bytes: number | undefined; - byteRate: number | undefined; - latency: number | undefined; - bytesReverse: number | undefined; - byteRateReverse: number | undefined; - latencyReverse: number | undefined; - }; -} - -export interface GraphReactAdaptorExposedMethods { - saveNodePositions?: Function; - closeContextMenu?: Function; -} - -export interface GraphReactAdaptorProps { - nodes: GraphNode[]; - edges: GraphEdge[]; - combos?: GraphCombo[]; - itemSelected?: string; - onClickCombo?: Function; - onClickNode?: Function; - onClickEdge?: Function; - onClickCanvas?: Function; - legendData?: GraphData; - ref?: MutableRefObject; - layout?: LayoutConfig; - moveToSelectedNode?: boolean; - ContextMenuComponent?: ComponentType<{ item: GraphNode; target: 'edge' | 'node' | undefined }>; -} -export interface LocalStorageDataSavedPayload { - x: number; - y: number; -} - -export interface LocalStorageDataSaved { - [key: string]: LocalStorageDataSavedPayload; -} - -export interface LocalStorageData extends LocalStorageDataSavedPayload { - id: string; -} - -export interface NodeWithBadgesProps extends ModelConfig { - notificationValue?: number; - notificationBgColor?: HexColors; - notificationColor?: HexColors; - notificationFontSize?: number; -} - -export interface ComboWithCustomLabel extends ModelConfig { - labelBgCfg?: { - fill?: string; - padding?: number[]; - }; -} diff --git a/components/console/src/core/components/Graph/Legend/Shapes/Circle.tsx b/components/console/src/core/components/Graph/Legend/Shapes/Circle.tsx deleted file mode 100644 index 6b4cd35..0000000 --- a/components/console/src/core/components/Graph/Legend/Shapes/Circle.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { LEGEND_DEFAULT_STROKE_COLOR } from './Shapes.constants'; - -const SvgCircle = function ({ dimension = 12, fillOpacity = 0 }: { dimension?: number; fillOpacity?: number }) { - const circleDimension = dimension; - const circleStroke = LEGEND_DEFAULT_STROKE_COLOR; - - return ( - - - - ); -}; - -export default SvgCircle; diff --git a/components/console/src/core/components/Graph/Legend/Shapes/Diamond.tsx b/components/console/src/core/components/Graph/Legend/Shapes/Diamond.tsx deleted file mode 100644 index 8a947a8..0000000 --- a/components/console/src/core/components/Graph/Legend/Shapes/Diamond.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { LEGEND_DEFAULT_BG_COLOR, LEGEND_DEFAULT_STROKE_COLOR } from './Shapes.constants'; - -const SvgDiamond = function ({ dimension = 12 }: { dimension?: number }) { - const diamondDimension = dimension; - const diamondColor = LEGEND_DEFAULT_BG_COLOR; - const diamondStroke = LEGEND_DEFAULT_STROKE_COLOR; - - return ( - - - - ); -}; - -export default SvgDiamond; diff --git a/components/console/src/core/components/Graph/Legend/Shapes/HorizontalLine.tsx b/components/console/src/core/components/Graph/Legend/Shapes/HorizontalLine.tsx deleted file mode 100644 index d885155..0000000 --- a/components/console/src/core/components/Graph/Legend/Shapes/HorizontalLine.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { LINE_COLOR } from './Shapes.constants'; - -const SvgHorizontalLine = function ({ - color = LINE_COLOR, - dashed = false, - withConnector = false -}: { - color?: string; - dashed?: boolean; - withConnector?: boolean; -}) { - const lineWidth = 30; - const lineHeight = 12; - const circleRadius = 3; - - const dashArray = dashed ? `${lineHeight * 0.4} ${lineHeight * 0.4}` : undefined; - - return ( - - - - {withConnector && ( - - )} - - ); -}; - -export default SvgHorizontalLine; diff --git a/components/console/src/core/components/Graph/Legend/Shapes/Shapes.constants.ts b/components/console/src/core/components/Graph/Legend/Shapes/Shapes.constants.ts deleted file mode 100644 index 2ce3f77..0000000 --- a/components/console/src/core/components/Graph/Legend/Shapes/Shapes.constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { HexColors } from '@config/colors'; - -export const LEGEND_DEFAULT_BG_COLOR = HexColors.White; -export const LEGEND_DEFAULT_STROKE_COLOR = HexColors.Black500; -export const LINE_COLOR = HexColors.Black500; diff --git a/components/console/src/core/components/Graph/Legend/Shapes/Square.tsx b/components/console/src/core/components/Graph/Legend/Shapes/Square.tsx deleted file mode 100644 index 06d22fb..0000000 --- a/components/console/src/core/components/Graph/Legend/Shapes/Square.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { LEGEND_DEFAULT_STROKE_COLOR } from './Shapes.constants'; - -const SvgSquare = function () { - const squareSize = 12; - const squareStroke = LEGEND_DEFAULT_STROKE_COLOR; - - return ( - - - - ); -}; - -export default SvgSquare; diff --git a/components/console/src/core/components/Graph/Legend/index.tsx b/components/console/src/core/components/Graph/Legend/index.tsx deleted file mode 100644 index 760494c..0000000 --- a/components/console/src/core/components/Graph/Legend/index.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Divider, Flex, FlexItem, Title } from '@patternfly/react-core'; - -import SvgCircle from './Shapes/Circle'; -import SvgDiamond from './Shapes/Diamond'; -import SvgHorizontalLine from './Shapes/HorizontalLine'; -import SvgSquare from './Shapes/Square'; - -enum Labels { - EntitiesTitle = ' Entities', - LinksTitle = ' Links', - Exposed = 'Process, component or site exposed', - NoExposed = 'Process/Component', - SiteGroup = 'Related site grouping', - Remote = 'Remote process/component', - DataLink = 'Data flow link', - ActiveDataLink = 'Active data link', - SiteLink = 'Site link connected' -} - -const ProcessLegend = function () { - return ( - <> - - {Labels.EntitiesTitle} - - - - - - - - - - - - - - - - - - - - - - - {Labels.Exposed} - {Labels.NoExposed} - {Labels.SiteGroup} - {Labels.Remote} - - - - - - {Labels.LinksTitle} - - - - - - - - - - - - - - - {Labels.DataLink} - {Labels.SiteLink} - - - - ); -}; - -export default ProcessLegend; diff --git a/components/console/src/core/components/Graph/MenuControl.tsx b/components/console/src/core/components/Graph/MenuControl.tsx deleted file mode 100644 index 4792f48..0000000 --- a/components/console/src/core/components/Graph/MenuControl.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { Graph } from '@antv/g6-pc'; -import { Button, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, Tooltip } from '@patternfly/react-core'; -import { ExpandArrowsAltIcon, ExpandIcon, UndoIcon, SearchMinusIcon, SearchPlusIcon } from '@patternfly/react-icons'; - -import { GraphController } from './services'; - -type ZoomControlsProps = { - graphInstance: Graph; -}; - -const ZOOM_RATIO_OUT = 1.2; -const ZOOM_RATIO_IN = 0.8; - -const ZOOM_CONFIG = { - duration: 200, - easing: 'easeCubic' -}; - -const MenuControl = function ({ graphInstance }: ZoomControlsProps) { - const handleIncreaseZoom = () => { - handleZoom(ZOOM_RATIO_OUT); - }; - - const handleDecreaseZoom = () => { - handleZoom(ZOOM_RATIO_IN); - }; - - const handleZoom = (zoom: number) => { - const nodeCount = graphInstance.getNodes().length; - const centerPoint = graphInstance.getGraphCenterPoint(); - - graphInstance.zoom(zoom, centerPoint, !GraphController.isPerformanceThresholdExceeded(nodeCount), ZOOM_CONFIG); - }; - - const handleFitView = () => { - const nodeCount = graphInstance.getNodes().length; - - graphInstance.fitView(20, undefined, !GraphController.isPerformanceThresholdExceeded(nodeCount), ZOOM_CONFIG); - }; - - const handleCenter = () => { - graphInstance.fitCenter(false); - }; - - const handleCleanAllGraphConfigurations = () => { - GraphController.cleanAllLocalNodePositions(graphInstance.getNodes(), true); - GraphController.removeAllNodePositionsFromLocalStorage(); - - graphInstance.layout(); - setTimeout(handleFitView, 250); - }; - - return ( - - - - - - + )} + {onCancel && ( + + )} + + ); +}; + +/** + * Reusable modal wrapper component that provides consistent modal patterns + * Handles common modal structure and behavior across the application + */ +export const ModalWrapper: FC = function ({ + title, + isOpen, + onClose, + variant = ModalVariant.medium, + children, + hasNoBodyWrapper = false, + showFooter = false, + 'aria-label': ariaLabel, + 'aria-describedby': ariaDescribedby +}) { + if (hasNoBodyWrapper) { + return ( + + + + {children} + {showFooter && } + + + ); + } + + return ( + + + + {children} + {showFooter && } + + + ); +}; + +export default ModalWrapper; diff --git a/components/console/src/core/components/NavBar/index.tsx b/components/console/src/core/components/NavBar/index.tsx index d6c6b74..be184a7 100644 --- a/components/console/src/core/components/NavBar/index.tsx +++ b/components/console/src/core/components/NavBar/index.tsx @@ -1,15 +1,27 @@ -import { Nav, NavItem, NavList } from '@patternfly/react-core'; +import { Nav, NavItem, NavList, NavGroup } from '@patternfly/react-core'; import { Link, useLocation } from 'react-router-dom'; -import { ROUTES } from '@config/routes'; +import { NAV_GROUPS, NAV_SINGLE_ITEMS } from '../../../config/navGroups'; const NavBar = function () { const { pathname } = useLocation(); return (