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
You need to enable JavaScript to run this app
+
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
+}) => (
+ : undefined}
+ isDisabled={disabled}
+ isLoading={isLoading}
+ >
+ {children}
+
+);
+
+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'
+}) => (
+ } isDanger isDisabled={disabled} isLoading={isLoading}>
+ {isLoading ? labels.buttons.deleting : labels.buttons.delete}
+
+);
+
+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' }) => (
+ } isDisabled={disabled} isLoading={isLoading}>
+ {labels.buttons.edit}
+
+);
+
+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 && (
+
+ onEdit(data)} icon={ }>
+ {labels.buttons.edit}
+
+
+ );
+
+ const deleteAction = showDelete && onDelete && (
+
+ onDelete(data.id)} icon={ } isDanger>
+ {labels.buttons.delete}
+
+
+ );
+
+ const customActionItems = customActions.map((action, index) => (
+
+ action.onClick(data)}
+ icon={action.icon}
+ isDanger={action.isDanger}
+ isDisabled={action.isDisabled}
+ >
+ {action.label}
+
+
+ ));
+
+ 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 (
+
+ );
+};
+
+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 (
-
-
-
-
-
- }
- className="sk-topology-control-bar__button"
- />
-
-
-
-
-
- }
- className="sk-topology-control-bar__button"
- />
-
-
-
-
-
- }
- className="sk-topology-control-bar__button"
- />
-
-
-
-
-
- }
- className="sk-topology-control-bar__button"
- />
-
-
-
-
-
- }
- className="sk-topology-control-bar__button"
- />
-
-
-
-
-
- );
-};
-
-export default MenuControl;
diff --git a/components/console/src/core/components/Graph/ReactAdaptor.tsx b/components/console/src/core/components/Graph/ReactAdaptor.tsx
deleted file mode 100644
index 13aec72..0000000
--- a/components/console/src/core/components/Graph/ReactAdaptor.tsx
+++ /dev/null
@@ -1,641 +0,0 @@
-import {
- FC,
- forwardRef,
- memo,
- useCallback,
- useEffect,
- useImperativeHandle,
- useInsertionEffect,
- useLayoutEffect,
- useRef,
- useState
-} from 'react';
-
-import G6, { EdgeConfig, G6GraphEvent, Graph, GraphOptions, IEdge, INode, Item, NodeConfig } from '@antv/g6';
-import { debounce } from '@patternfly/react-core';
-
-import {
- GraphEdge,
- GraphCombo,
- GraphNode,
- GraphReactAdaptorProps,
- LocalStorageData
-} from '@core/components/Graph/Graph.interfaces';
-import LoadingPage from '@pages/shared/Loading';
-
-import { DEFAULT_GRAPH_CONFIG, LAYOUT_TOPOLOGY_DEFAULT, GRAPH_BG_COLOR } from './Graph.constants';
-import MenuControl from './MenuControl';
-import { GraphController } from './services';
-import {
- registerDataEdge as registerDefaultEdgeWithHover,
- registerNodeWithBadges,
- registerComboWithCustomLabel,
- registerSiteLinkEdge
-} from './services/customItems';
-
-import './SkGraph.css';
-
-const DEFAULT_CONTEXT_MENU = {
- target: undefined,
- item: undefined,
- x: undefined,
- y: undefined
-};
-
-const GraphReactAdaptor: FC = memo(
- forwardRef(
- (
- {
- nodes: nodesWithoutPosition,
- edges,
- combos,
- onClickEdge,
- onClickNode,
- onClickCombo,
- onClickCanvas,
- itemSelected,
- layout = LAYOUT_TOPOLOGY_DEFAULT,
- moveToSelectedNode = false,
- ContextMenuComponent
- },
- ref
- ) => {
- const [isGraphLoaded, setIsGraphLoaded] = useState(false);
- const [menuContext, setMenuContext] = useState<{
- x?: number;
- y?: number;
- item?: NodeConfig | EdgeConfig;
- target: 'node' | 'edge' | undefined;
- }>(DEFAULT_CONTEXT_MENU);
-
- const itemSelectedRef = useRef();
- const isItemHighlightedRef = useRef(false);
-
- const prevNodesRef = useRef(nodesWithoutPosition);
- const prevEdgesRef = useRef(edges);
- const prevCombosRef = useRef(combos);
- const topologyGraphRef = useRef();
-
- //exported methods
- useImperativeHandle(ref, () => ({
- // Tsave the nodes positions in the local storage
- saveNodePositions() {
- const graphInstance = topologyGraphRef.current;
-
- if (!graphInstance?.getNodes()) {
- return;
- }
-
- const updatedNodes = GraphController.fromNodesToLocalStorageData(
- graphInstance.getNodes(),
- ({ id, x, y }: LocalStorageData) => ({ id, x, y })
- );
-
- GraphController.saveAllNodePositions(updatedNodes);
- },
- closeContextMenu() {
- setMenuContext(DEFAULT_CONTEXT_MENU);
- }
- }));
-
- // UTILS
- const activateNodeRelations = useCallback(
- ({ currentTarget, item, state }: { currentTarget: Graph; item: Item; state: string }) => {
- isItemHighlightedRef.current = true;
-
- const node = item as INode;
- const neighbors = node.getNeighbors();
- const neighborsIds = neighbors.map((neighbor) => neighbor.getID());
-
- currentTarget.getNodes().forEach((n: INode) => {
- currentTarget.clearItemStates(n, state);
-
- if (node.getID() !== n.getID() && !neighborsIds.includes(n.getID())) {
- currentTarget.setItemState(n, 'hidden', true);
- n.toBack();
- } else {
- currentTarget.clearItemStates(n, 'hidden');
- n.toFront();
- }
- });
-
- currentTarget.getEdges().forEach((edge: IEdge) => {
- if (node.getID() !== edge.getSource().getID() && node.getID() !== edge.getTarget().getID()) {
- edge.hide();
- } else {
- edge.show();
- }
- });
- },
- []
- );
-
- const activateEdgeRelations = useCallback(({ currentTarget, item }: { currentTarget: Graph; item: Item }) => {
- const edge = item as IEdge;
- const source = edge.getSource();
- const target = edge.getTarget();
-
- if (!itemSelectedRef.current) {
- currentTarget.getNodes().forEach((node) => {
- if (node.getID() !== source.getID() && node.getID() !== target.getID()) {
- currentTarget.setItemState(node, 'hidden', true);
- } else {
- currentTarget.clearItemStates(node, 'hidden');
- }
- });
-
- currentTarget.getEdges().forEach((topologyEdge) => {
- if (edge.getID() !== topologyEdge.getID()) {
- topologyEdge.hide();
- } else {
- topologyEdge.show();
- }
- });
- }
- }, []);
-
- const cleanAllRelations = useCallback(({ currentTarget }: { currentTarget: Graph }) => {
- // when we back from an other view and we leave a node we must erase links status
- currentTarget.getEdges().forEach((edge) => {
- edge.show();
- currentTarget.clearItemStates(edge, 'hover');
- });
-
- currentTarget.findAllByState('node', 'selected-default').forEach((node) => {
- currentTarget.clearItemStates(node, 'selected-default');
- });
-
- currentTarget.findAllByState('node', 'hover').forEach((node) => {
- currentTarget.clearItemStates(node, 'hover');
- });
-
- currentTarget.findAllByState('node', 'hidden').forEach((node) => {
- currentTarget.clearItemStates(node, 'hidden');
- });
- }, []);
-
- // SELECT EVENTS
- const handleNodeSelected = useCallback(
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) => {
- isItemHighlightedRef.current = true;
- activateNodeRelations({ currentTarget, item, state: 'selected-default' });
-
- currentTarget.setItemState(item, 'selected-default', true);
- },
- [activateNodeRelations]
- );
-
- const handleEdgeSelected = useCallback(
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) => {
- isItemHighlightedRef.current = true;
- activateEdgeRelations({ currentTarget, item });
-
- currentTarget.setItemState(item, 'hover', true);
- },
- [activateEdgeRelations]
- );
-
- /** Simulate a Select event, regardless of whether a node or edge is preselected */
- const handleItemSelected = useCallback(
- (id?: string) => {
- isItemHighlightedRef.current = true;
-
- const graphInstance = topologyGraphRef.current;
- if (graphInstance && id) {
- const item = graphInstance.findById(id);
-
- if (item) {
- if (item.get('type') === 'node') {
- handleNodeSelected({ currentTarget: graphInstance, item });
- }
-
- if (item.get('type') === 'edge') {
- handleEdgeSelected({ currentTarget: graphInstance, item });
- }
- }
- }
-
- // handleNodeMouseEnter and handleEdgeMouseEnter set hoverState to true and block any update when we changeData in the useState
- isItemHighlightedRef.current = false;
- },
- [handleEdgeSelected, handleNodeSelected]
- );
-
- // MOUSE EVENTS
- const handleNodeMouseEnter = useCallback(
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) => {
- isItemHighlightedRef.current = true;
- activateNodeRelations({ currentTarget, item, state: 'hover' });
-
- currentTarget.setItemState(item, 'hover', true);
- item.toFront();
-
- // keep the selected state when we hover a node
- const nodeSelected = currentTarget.findAllByState('node', 'selected-default')[0] as INode;
- if (nodeSelected) {
- currentTarget.setItemState(nodeSelected, 'selected-default', true);
- }
- },
- [activateNodeRelations]
- );
-
- const handleEdgeMouseEnter = useCallback(
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) => {
- isItemHighlightedRef.current = true;
- activateEdgeRelations({ currentTarget, item });
-
- currentTarget.setItemState(item, 'hover', true);
- },
- [activateEdgeRelations]
- );
-
- const handleComboMouseEnter = useCallback(({ currentTarget, item }: { currentTarget: Graph; item?: Item }) => {
- if (item) {
- currentTarget.setItemState(item, 'hover', true);
- }
- }, []);
-
- const handleNodeMouseLeave = useCallback(
- ({ currentTarget }: { currentTarget: Graph; item?: Item }) => {
- // when we back from an other view and we leave a node we must erase links status
- cleanAllRelations({ currentTarget });
-
- isItemHighlightedRef.current = false;
- },
- [cleanAllRelations]
- );
-
- const handleEdgeMouseLeave = useCallback(
- ({ currentTarget }: { currentTarget: Graph; item?: Item }) => {
- cleanAllRelations({ currentTarget });
- isItemHighlightedRef.current = false;
- },
- [cleanAllRelations]
- );
-
- const handleComboMouseLeave = useCallback(({ currentTarget, item }: { currentTarget: Graph; item?: Item }) => {
- if (item) {
- currentTarget.clearItemStates(item, 'hover');
- isItemHighlightedRef.current = false;
- }
- }, []);
-
- // CLiCK EVENTS
- const handleNodeClick = useCallback(
- ({ item, target, canvasX, canvasY }: G6GraphEvent) => {
- if (target.cfg.name === 'node-cx-shape') {
- setMenuContext({ x: canvasX, y: canvasY, item: item.getModel() as NodeConfig, target: 'node' });
- } else if (onClickNode) {
- onClickNode(item.getModel());
- }
- },
- [onClickNode]
- );
-
- const handleNodeRightClick = useCallback((ev: G6GraphEvent) => {
- ev.preventDefault();
- }, []);
-
- const handleEdgeClick = useCallback(
- ({ item, target, canvasX, canvasY }: G6GraphEvent) => {
- if (target.cfg.name === 'path-shape') {
- setMenuContext({ x: canvasX, y: canvasY, item: item.getModel() as NodeConfig, target: 'edge' });
- } else if (onClickEdge) {
- onClickEdge(item.getModel());
- }
- },
- [onClickEdge]
- );
-
- const handleComboClick = useCallback(
- ({ item }: G6GraphEvent) => {
- if (onClickCombo) {
- onClickCombo(item.getModel());
- }
- },
- [onClickCombo]
- );
-
- // DRAG EVENTS
- const handleNodeDragStart = useCallback(({ item }: G6GraphEvent) => {
- setMenuContext(DEFAULT_CONTEXT_MENU);
-
- isItemHighlightedRef.current = true;
- item.toFront();
- }, []);
-
- const handleNodeDragEnd = useCallback(() => {
- isItemHighlightedRef.current = false;
- }, []);
-
- const handleCombDragStart = useCallback(({ item }: G6GraphEvent) => {
- setMenuContext(DEFAULT_CONTEXT_MENU);
-
- item.toFront();
- isItemHighlightedRef.current = true;
- }, []);
-
- const handleComboDragEnd = useCallback(() => {
- isItemHighlightedRef.current = false;
- }, []);
-
- // CANVAS EVENTS
- const handleCanvasDblClick = useCallback(
- ({ x, y }: G6GraphEvent) => {
- setMenuContext(DEFAULT_CONTEXT_MENU);
-
- onClickCanvas?.({ x, y });
- },
- [onClickCanvas]
- );
-
- const handleCanvasClick = useCallback(() => {
- setMenuContext(DEFAULT_CONTEXT_MENU);
- }, [setMenuContext]);
-
- const handleCanvasDragStart = useCallback(() => {
- setMenuContext(DEFAULT_CONTEXT_MENU);
-
- isItemHighlightedRef.current = true;
- }, []);
-
- const handleCanvasDragEnd = useCallback(() => {
- isItemHighlightedRef.current = false;
- }, []);
-
- // TIMING EVENTS
- const handleAfterChangeData = useCallback(() => {
- if (itemSelectedRef.current) {
- // style are reset when we changeData, so we need to reapply the hover style
- handleItemSelected(itemSelectedRef.current);
- }
- setMenuContext(DEFAULT_CONTEXT_MENU);
- }, [handleItemSelected]);
-
- const handleAfterRender = useCallback(() => {
- if (itemSelectedRef.current) {
- handleItemSelected(itemSelectedRef.current);
- }
-
- setIsGraphLoaded(true);
- }, [handleItemSelected]);
-
- const bindEvents = useCallback(() => {
- /** EVENTS */
- topologyGraphRef.current?.on('node:click', handleNodeClick);
- topologyGraphRef.current?.on('node:contextmenu', handleNodeRightClick);
- topologyGraphRef.current?.on('node:dragstart', handleNodeDragStart);
- topologyGraphRef.current?.on('node:dragend', handleNodeDragEnd);
- topologyGraphRef.current?.on(
- 'node:mouseenter',
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) =>
- !itemSelectedRef.current ? handleNodeMouseEnter({ currentTarget, item }) : undefined
- );
- topologyGraphRef.current?.on(
- 'node:mouseleave',
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) =>
- !itemSelectedRef.current ? handleNodeMouseLeave({ currentTarget, item }) : undefined
- );
-
- topologyGraphRef.current?.on('edge:click', handleEdgeClick);
- topologyGraphRef.current?.on(
- 'edge:mouseenter',
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) =>
- !itemSelectedRef.current ? handleEdgeMouseEnter({ currentTarget, item }) : undefined
- );
- topologyGraphRef.current?.on(
- 'edge:mouseleave',
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) =>
- !itemSelectedRef.current ? handleEdgeMouseLeave({ currentTarget, item }) : undefined
- );
-
- topologyGraphRef.current?.on('combo:click', handleComboClick);
- topologyGraphRef.current?.on('combo:dragstart', handleCombDragStart);
- topologyGraphRef.current?.on('combo:dragend', handleComboDragEnd);
- topologyGraphRef.current?.on(
- 'combo:mouseenter',
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) =>
- !itemSelectedRef.current ? handleComboMouseEnter({ currentTarget, item }) : undefined
- );
- topologyGraphRef.current?.on(
- 'combo:mouseleave',
- ({ currentTarget, item }: { currentTarget: Graph; item: Item }) =>
- !itemSelectedRef.current ? handleComboMouseLeave({ currentTarget, item }) : undefined
- );
-
- topologyGraphRef.current?.on('canvas:dblclick', handleCanvasDblClick);
- topologyGraphRef.current?.on('canvas:click', handleCanvasClick);
- topologyGraphRef.current?.on('canvas:dragstart', handleCanvasDragStart);
- topologyGraphRef.current?.on('canvas:dragend', handleCanvasDragEnd);
-
- topologyGraphRef.current?.on('afterchangedata', handleAfterChangeData);
-
- // Be carefull: afterender is supposd to be calleed every re-render. However, in our case this event is called just one time because we update the topology usng changeData.
- // If this behaviour changes we must use a flag to check only the first render
- topologyGraphRef.current?.on('afterrender', handleAfterRender);
-
- topologyGraphRef.current?.on('viewportchange', () => {
- setMenuContext(DEFAULT_CONTEXT_MENU);
- });
- }, [
- handleNodeClick,
- handleNodeRightClick,
- handleNodeDragStart,
- handleNodeDragEnd,
- handleEdgeClick,
- handleComboClick,
- handleCombDragStart,
- handleComboDragEnd,
- handleCanvasDblClick,
- handleCanvasClick,
- handleCanvasDragStart,
- handleCanvasDragEnd,
- handleAfterChangeData,
- handleAfterRender,
- handleNodeMouseEnter,
- handleNodeMouseLeave,
- handleEdgeMouseEnter,
- handleEdgeMouseLeave,
- handleComboMouseEnter,
- handleComboMouseLeave
- ]);
-
- /** Creates network topology instance */
- const graphRef = useCallback(($node: HTMLDivElement) => {
- if (nodesWithoutPosition && !topologyGraphRef.current) {
- registerNodeWithBadges();
- registerDefaultEdgeWithHover();
- registerSiteLinkEdge();
- registerComboWithCustomLabel();
-
- const nodes = GraphController.addPositionsToNodes(nodesWithoutPosition);
- const data = GraphController.getG6Model({ edges, nodes, combos });
- const [width, height] = [$node.clientWidth, $node.clientHeight];
-
- const options: GraphOptions = {
- container: $node,
- width,
- height,
- layout: {
- ...layout,
- center: [width / 2, height / 2],
- maxIteration: GraphController.calculateMaxIteration(nodes.length)
- },
- ...DEFAULT_GRAPH_CONFIG
- };
-
- topologyGraphRef.current = new G6.Graph(options);
- topologyGraphRef.current.setMode(
- GraphController.getModeBasedOnPerformanceThreshold(nodesWithoutPosition.length)
- );
-
- topologyGraphRef.current.read(data);
- bindEvents();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- // This effect updates the topology when there are changes to the nodes, edges.
- useEffect(() => {
- const graphInstance = topologyGraphRef.current;
-
- if (!graphInstance || isItemHighlightedRef.current || !isGraphLoaded) {
- return;
- }
-
- if (
- JSON.stringify(prevNodesRef.current) === JSON.stringify(nodesWithoutPosition) &&
- JSON.stringify(prevEdgesRef.current) === JSON.stringify(edges) &&
- JSON.stringify(prevCombosRef.current) === JSON.stringify(combos)
- ) {
- return;
- }
-
- // if the layout has changed, then we don't want to use the previous positions from the current graph
- // but we want to use the positions saved from the local storage
- // This avoids the nodes to be repositioned in the graph based on a different layout (ie from dagree to force)
- const nodes = GraphController.addPositionsToNodes(
- nodesWithoutPosition,
- graphInstance.get('layout').type !== layout.type ? undefined : graphInstance.getNodes()
- );
-
- // the performance mode contains optimizations for large graphs
- graphInstance.setMode(GraphController.getModeBasedOnPerformanceThreshold(nodes.length));
- graphInstance.changeData(GraphController.getG6Model({ edges, nodes, combos }));
-
- // if the topology changes nodes, then reposition them
- const newNodesIds = nodes
- .map((node) => node.id)
- .sort()
- .join(',');
- const prevNodeIds = prevNodesRef.current
- .map((node) => node.id)
- .sort()
- .join(',');
-
- if (JSON.stringify(newNodesIds) !== JSON.stringify(prevNodeIds)) {
- // graphInstance.render();
- }
-
- // updated the prev values with the new ones
- prevNodesRef.current = nodesWithoutPosition;
- prevEdgesRef.current = edges;
- prevCombosRef.current = combos;
- }, [nodesWithoutPosition, edges, combos, isGraphLoaded, layout]);
-
- // This effect updates the layout when the layout configuration changes or when the number of nodes changes.
- // In the second case, the number of cycles for the new simulation is recalculated based on the number of nodes. (Performance optimization)
- useEffect(() => {
- const graphInstance = topologyGraphRef.current;
- const { type, ...layoutConf } = layout;
- const isLayoutChanged = graphInstance?.get('layout').type !== type;
- const newLayout = isLayoutChanged ? layout : layoutConf;
-
- graphInstance?.updateLayout({
- ...newLayout,
- maxIteration: GraphController.calculateMaxIteration(graphInstance.getNodes().length)
- });
-
- if (isLayoutChanged) {
- graphInstance?.render();
- }
- }, [layout]);
-
- // This effect updates center the node selected
- useEffect(() => {
- const graphInstance = topologyGraphRef.current;
- if (!graphInstance || !isGraphLoaded || !itemSelected || !moveToSelectedNode) {
- return;
- }
-
- const node = graphInstance.findById(itemSelected);
- if (node) {
- setTimeout(() => graphInstance.focusItem(node), 0);
- }
- }, [itemSelected, moveToSelectedNode, isGraphLoaded]);
-
- useInsertionEffect(() => {
- const graphInstance = topologyGraphRef.current;
- itemSelectedRef.current = itemSelected;
-
- if (!graphInstance || !isGraphLoaded) {
- return;
- }
-
- if (!itemSelected) {
- cleanAllRelations({ currentTarget: graphInstance });
-
- return;
- }
-
- handleItemSelected(itemSelected);
- }, [handleItemSelected, cleanAllRelations, itemSelected, isGraphLoaded]);
- // This effect handle the resize of the topology when the browser window changes size.
- useLayoutEffect(() => {
- const graphInstance = topologyGraphRef.current;
-
- if (!graphInstance) {
- return;
- }
-
- const container = graphInstance.getContainer();
- const handleResize = () => {
- try {
- graphInstance.changeSize(container.clientWidth, container.clientHeight);
- } catch {
- return;
- }
- };
-
- const debouncedHandleResize = debounce(handleResize, 200);
- window.addEventListener('resize', debouncedHandleResize);
-
- return () => {
- window.removeEventListener('resize', debouncedHandleResize);
- };
- }, []);
-
- return (
-
- {topologyGraphRef.current &&
}
- {!isGraphLoaded &&
}
-
-
- {ContextMenuComponent && menuContext.item && (
-
- )}
-
-
- );
- }
- )
-);
-
-export default GraphReactAdaptor;
diff --git a/components/console/src/core/components/Graph/SkGraph.css b/components/console/src/core/components/Graph/SkGraph.css
deleted file mode 100644
index d784547..0000000
--- a/components/console/src/core/components/Graph/SkGraph.css
+++ /dev/null
@@ -1,24 +0,0 @@
-.sk-topology-controls {
- position: absolute !important;
- bottom: 0;
- left: 0;
- background-color: transparent !important;
-}
-
-.sk-topology-control-bar__button {
- margin-right: var(--pf-v5-global--spacer--xs);
- margin-top: var(--pf-v5-global--spacer--xs);
- border: none;
- border-radius: var(--pf-v5-global--BorderRadius--sm);
- box-shadow: var(--pf-v5-global--BoxShadow--sm);
-}
-.sk-topology-control-bar__button:not(.pf-v5-m-disabled) {
- background-color: var(--pf-v5-global--BackgroundColor--100);
-}
-.sk-topology-control-bar__button:after {
- display: none;
-}
-.sk-topology-control-bar__button:hover {
- border: none;
- box-shadow: var(--pf-v5-global--BoxShadow--md);
-}
diff --git a/components/console/src/core/components/Graph/services/customItems.ts b/components/console/src/core/components/Graph/services/customItems.ts
deleted file mode 100644
index 81d78e0..0000000
--- a/components/console/src/core/components/Graph/services/customItems.ts
+++ /dev/null
@@ -1,298 +0,0 @@
-import { registerEdge, registerCombo, IGroup, registerNode } from '@antv/g6';
-
-import {
- BADGE_STYLE,
- CUSTOM_ITEMS_NAMES,
- EDGE_COLOR_DEFAULT,
- EDGE_COLOR_DEFAULT_TEXT,
- NODE_SIZE,
- EDGE_COLOR_HOVER_DEFAULT,
- EDGE_COLOR_ENDPOINT_SITE_CONNECTION_DEFAULT,
- DEFAULT_EDGE_CONFIG
-} from '../Graph.constants';
-import { ComboWithCustomLabel, NodeWithBadgesProps } from '../Graph.interfaces';
-
-export function registerDataEdge() {
- registerEdge(
- CUSTOM_ITEMS_NAMES.animatedDashEdge,
- {
- afterDraw(_, group) {
- if (group) {
- const shape = group.get('children')[0];
- const { x, y } = shape.getPoint(0);
- const circle = group.addShape('circle', {
- attrs: {
- x,
- y,
- fill: EDGE_COLOR_HOVER_DEFAULT,
- r: 4
- },
- name: 'circle-shape'
- });
-
- circle.hide();
- }
- },
-
- setState(name, value, item) {
- if (item) {
- const group = item.getContainer();
- const shape = group.get('children')[0];
- const circle = group.find((element) => element.get('name') === 'circle-shape');
-
- if ((name === 'hover' || name === 'active') && value) {
- circle.show();
-
- circle.animate(
- (ratio: number) => {
- const tmpPoint = shape.getPoint(ratio);
-
- return { x: tmpPoint.x, y: tmpPoint.y };
- },
- {
- repeat: true,
- duration: 3000
- }
- );
- } else {
- circle.stopAnimate(true);
- circle.hide();
- }
- }
- }
- },
- 'quadratic'
- );
-}
-
-export function registerSiteLinkEdge() {
- // Draw a blue line dash when hover an edge
- const lineDash = [4, 4];
-
- registerEdge(CUSTOM_ITEMS_NAMES.siteEdge, {
- draw(cfg, group) {
- const { startPoint, endPoint } = cfg;
-
- group.addShape('path', {
- attrs: {
- path: [
- ['M', startPoint?.x, startPoint?.y],
- ['L', endPoint?.x, endPoint?.y]
- ],
- stroke: 'transparent',
- lineWidth: 15,
- cursor: 'pointer'
- },
- name: 'path-shape'
- });
-
- const keyShape = group.addShape('path', {
- attrs: {
- path: [
- ['M', startPoint?.x, startPoint?.y],
- ['L', endPoint?.x, endPoint?.y]
- ],
- stroke: EDGE_COLOR_DEFAULT,
- lineWidth: 0.5,
- lineDash,
- cursor: 'pointer'
- },
- name: 'path-dash-shape'
- });
-
- if (cfg.label) {
- group.addShape('text', {
- attrs: {
- text: cfg.label,
- fill: EDGE_COLOR_DEFAULT_TEXT,
- textAlign: 'center',
- textBaseline: 'middle',
- x: (startPoint?.x || 2) / 2 + (endPoint?.x || 2) / 2,
- y: (startPoint?.y || 2) / 2 + (endPoint?.y || 2) / 2,
- ...DEFAULT_EDGE_CONFIG.labelCfg?.style,
- ...cfg.labelCfg?.style
- },
- name: 'center-text-shape'
- });
- }
-
- return keyShape;
- },
- afterDraw(_, group) {
- if (group) {
- const shape = group.get('children')[0];
- const quatile = shape.getPoint(0.97);
-
- group.addShape('circle', {
- attrs: {
- r: 4,
- fill: EDGE_COLOR_ENDPOINT_SITE_CONNECTION_DEFAULT,
- x: quatile.x,
- y: quatile.y
- }
- });
- }
- }
- });
-}
-
-export function registerComboWithCustomLabel() {
- const textContainerKey = 'combo-label-shape';
-
- registerCombo(
- CUSTOM_ITEMS_NAMES.comboWithCustomLabel,
- {
- afterDraw(cfg: ComboWithCustomLabel | undefined, group) {
- if (group?.get('children')[1]) {
- const { width, height } = group.get('children')[1].getBBox(); // combo label
-
- // creates a background container for the combo label
- const textContainer = group.addShape('rect', {
- attrs: cfg?.labelBgCfg || {},
- name: textContainerKey
- });
-
- const padding = cfg?.labelBgCfg?.padding || [0, 0];
- textContainer.attr({
- width: width + padding[0],
- height: height + padding[1]
- });
-
- textContainer.toBack();
- }
- },
- afterUpdate(cfg: ComboWithCustomLabel | undefined, combo) {
- if (combo?.get('group')?.get('children')[2]) {
- const group = combo.get('group') as IGroup;
- const { x, y } = group.get('children')[2].getBBox(); // combo label
- const textContainer = group.find((element) => element.get('name') === textContainerKey);
-
- const padding = cfg?.labelBgCfg?.padding || [0, 0];
- textContainer.attr({
- x: x - padding[0] / 2,
- y: y - padding[1] / 2
- });
- }
- }
- },
- 'rect'
- );
-}
-
-export function registerNodeWithBadges() {
- const { containerBg, containerBorderColor, textColor, textFontSize } = BADGE_STYLE;
- const r = NODE_SIZE / 5;
- const angleBadge1 = 180;
- const x = r * Math.cos(angleBadge1) - r;
- const y = r * Math.sin(angleBadge1) - r;
-
- const textContainerKey = 'node-label-shape';
- const contextMenuKey = 'node-cx-shape';
-
- registerNode(
- CUSTOM_ITEMS_NAMES.nodeWithBadges,
- {
- afterDraw(cfg: NodeWithBadgesProps | undefined, group) {
- if (group?.get('children')[3]) {
- const { width, height, x: xl, y: yl } = group.get('children')[3].getBBox(); // combo label
-
- // creates a background container for the combo label
- const bgStyle = cfg?.labelCfg?.style?.background || {};
- const padding = (bgStyle?.padding as number[]) || [0, 0];
-
- const textContainer = group.addShape('rect', {
- attrs: {
- ...bgStyle,
- width: width + padding[0] + 18,
- height: height + padding[1] + 2,
- x: xl - padding[0] * 2,
- y: yl - padding[1] / 2 - 1,
- lineWidth: 0.5,
- fill: 'white'
- },
- name: textContainerKey
- });
-
- group.addShape('line', {
- attrs: {
- x1: xl + width + padding[0],
- y1: yl - padding[1] / 2 - 1,
- x2: xl + width + padding[0],
- y2: yl + height + padding[1] / 2 + 1,
- stroke: bgStyle.stroke,
- lineWidth: 0.5
- }
- });
-
- group.addShape('circle', {
- attrs: {
- x: xl + width + 9,
- y: yl + height / 4 - 1,
- r: 1,
- fill: '#000'
- }
- });
-
- group.addShape('circle', {
- attrs: {
- x: xl + width + 9,
- y: yl + height / 2,
- r: 1,
- fill: '#000'
- }
- });
-
- group.addShape('circle', {
- attrs: {
- x: xl + width + 9,
- y: yl + (3 * height) / 4 + 1,
- r: 1,
- fill: '#000'
- }
- });
-
- group.addShape('rect', {
- attrs: {
- ...bgStyle,
- cursor: 'pointer',
- width: 14,
- height: height + padding[1],
- x: xl + width + padding[0] - 2,
- y: yl - padding[1] / 2 - 1
- },
- name: contextMenuKey
- });
-
- textContainer.toBack();
- }
-
- if (cfg?.enableBadge1 && group) {
- group.addShape('circle', {
- attrs: {
- x,
- y,
- r,
- stroke: containerBorderColor,
- fill: cfg.notificationBgColor || containerBg
- },
- name: `${CUSTOM_ITEMS_NAMES.nodeWithBadges}-notification-container`
- });
-
- group.addShape('text', {
- attrs: {
- x,
- y,
- textAlign: 'center',
- textBaseline: 'middle',
- text: cfg.notificationValue,
- fill: cfg.notificationColor || textColor,
-
- fontSize: cfg.notificationFontSize || textFontSize
- }
- });
- }
- }
- },
- 'circle'
- );
-}
diff --git a/components/console/src/core/components/Graph/services/index.ts b/components/console/src/core/components/Graph/services/index.ts
deleted file mode 100644
index f537a36..0000000
--- a/components/console/src/core/components/Graph/services/index.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import { INode, NodeConfig } from '@antv/g6';
-
-import { NODE_COUNT_PERFORMANCE_THRESHOLD, TopologyModeNames } from '../Graph.constants';
-import {
- GraphCombo,
- GraphEdge,
- GraphNode,
- LocalStorageData,
- LocalStorageDataSaved,
- LocalStorageDataSavedPayload
-} from '../Graph.interfaces';
-
-const prefixLocalStorageItem = 'skupper';
-
-export const GraphController = {
- saveAllNodePositions: (nodes: LocalStorageData[]) => {
- const savedNodePositionsMap = JSON.parse(
- localStorage.getItem(prefixLocalStorageItem) || '{}'
- ) as LocalStorageDataSaved;
-
- const nodePositionsMap = nodes.reduce((acc, { id, x, y }) => {
- acc[id] = { x, y };
-
- return acc;
- }, {} as LocalStorageDataSaved);
-
- const result = { ...savedNodePositionsMap };
-
- for (const key in nodePositionsMap) {
- if (key in result) {
- delete result[key];
- }
- }
-
- Promise.resolve(localStorage.setItem(prefixLocalStorageItem, JSON.stringify({ ...result, ...nodePositionsMap })));
- },
-
- removeAllNodePositionsFromLocalStorage() {
- localStorage.removeItem(prefixLocalStorageItem);
- },
-
- cleanControlsFromLocalStorage(suffix: string) {
- Object.keys(localStorage)
- .filter((x) => x.endsWith(suffix))
- .forEach((x) => localStorage.removeItem(x));
- },
-
- // TODO: remove this function when Backend sanitize the old process pairs
- sanitizeEdges: (nodes: GraphNode[], edges: GraphEdge[]) => {
- const availableNodesMap = nodes.reduce(
- (acc, node) => {
- acc[node.id] = node.id;
-
- return acc;
- },
- {} as Record
- );
-
- return edges.filter(({ source, target }) => availableNodesMap[source] && availableNodesMap[target]);
- },
-
- fromNodesToLocalStorageData(nodes: INode[], setLocalStorageData: Function) {
- return nodes
- .map((node) => {
- const { id, x, y, persistPositionKey } = node.getModel() as NodeConfig;
-
- if (id && x !== undefined && y !== undefined) {
- return setLocalStorageData({ id: persistPositionKey || id, x, y });
- }
-
- return undefined;
- })
- .filter(Boolean) as LocalStorageData[];
- },
-
- addPositionsToNodes(nodesWithoutPosition: GraphNode[], nodesWithPositions: INode[] = []) {
- const cache = JSON.parse(localStorage.getItem(prefixLocalStorageItem) || '{}');
-
- const nodesWithCachedPosition = nodesWithoutPosition.map((node) => {
- const positions = cache[node.persistPositionKey || node.id] as LocalStorageDataSavedPayload | undefined;
-
- const x = positions ? positions.x : node.x;
- const y = positions ? positions.y : node.y;
-
- return { ...node, x, y };
- });
-
- //Map of the most updated nodes from the graph
- const positionMap = nodesWithPositions?.reduce(
- (acc, node) => {
- const id = (node.getModel().persistPositionKey || node.getID()) as string;
- acc[id] = { x: node.getModel().x, y: node.getModel().y };
-
- return acc;
- },
- {} as Record
- );
-
- // check updated nodes from the graph otherwise rollback to the localstorage position
- const nodes = nodesWithCachedPosition.map((node) => ({
- ...node,
- x: positionMap[node.persistPositionKey || node.id]?.x || node.x,
- y: positionMap[node.persistPositionKey || node.id]?.y || node.y
- }));
-
- return nodes;
- },
-
- isPerformanceThresholdExceeded: (nodesCount: number) => nodesCount >= NODE_COUNT_PERFORMANCE_THRESHOLD,
-
- getModeBasedOnPerformanceThreshold(nodeCount: number) {
- return GraphController.isPerformanceThresholdExceeded(nodeCount)
- ? TopologyModeNames.Performance
- : TopologyModeNames.Default;
- },
-
- cleanAllLocalNodePositions(nodes: INode[], shouldRemoveFixedPosition: boolean = false) {
- nodes.forEach((node) => {
- const nodeModel = node.getModel();
- nodeModel.x = undefined;
- nodeModel.y = undefined;
-
- if (shouldRemoveFixedPosition) {
- nodeModel.fx = undefined;
- nodeModel.fy = undefined;
- }
- });
- },
-
- getG6Model: ({ nodes, edges, combos }: { nodes: GraphNode[]; edges: GraphEdge[]; combos?: GraphCombo[] }) => ({
- nodes: JSON.parse(
- JSON.stringify(
- nodes.map((node) => ({
- ...node,
- cluster: node.comboId, // activate the cluster mode for processes inside a site
- fx: node.x, // fix position saved in the local storage
- fy: node.y
- }))
- )
- ),
- edges: JSON.parse(JSON.stringify(GraphController.sanitizeEdges(nodes, edges))),
- combos: combos ? JSON.parse(JSON.stringify(combos)) : undefined
- }),
-
- calculateMaxIteration: (nodeCount: number) => {
- if (nodeCount >= 400) {
- return 100;
- }
-
- if (nodeCount > 100) {
- return 500;
- }
-
- return 1000;
- }
-};
diff --git a/components/console/src/core/components/HighlightValueCell/HighightValueCell.interfaces.ts b/components/console/src/core/components/HighlightValueCell/HighightValueCell.interfaces.ts
deleted file mode 100644
index 9fdba7c..0000000
--- a/components/console/src/core/components/HighlightValueCell/HighightValueCell.interfaces.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface HighlightValueCellProps {
- data: T;
- value: number;
- format: Function;
-}
diff --git a/components/console/src/core/components/HighlightValueCell/__tests__/HighLightValueCell.spec.tsx b/components/console/src/core/components/HighlightValueCell/__tests__/HighLightValueCell.spec.tsx
deleted file mode 100644
index eea0579..0000000
--- a/components/console/src/core/components/HighlightValueCell/__tests__/HighLightValueCell.spec.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-
-import { render } from '@testing-library/react';
-
-import { VarColors } from '@config/colors';
-
-import HighlightValueCell from './../index';
-
-describe('HighlightValueCell', () => {
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- it('should render without highlighting when value is not updated', () => {
- const data = { id: 1, name: 'Test' };
- const value = 42;
- const format = jest.fn((val) => val);
-
- const useRefSpy = jest.spyOn(React, 'useRef');
- useRefSpy.mockReturnValue({ current: value });
-
- const { getByText, queryByTestId } = render( );
-
- expect(getByText('42')).toBeInTheDocument();
- expect(format).toHaveBeenCalledWith(value);
- expect(queryByTestId('highlighted-value')).not.toBeInTheDocument();
- });
-
- it('should render with highlighting when value is updated', () => {
- const data = { id: 1, name: 'Test' };
- const value = 42;
- const newValue = 50;
- const format = jest.fn((val) => val);
-
- const useRefSpy = jest.spyOn(React, 'useRef');
- useRefSpy.mockReturnValue({ current: value });
-
- const { getByText, getByTestId } = render( );
-
- expect(getByText('50')).toBeInTheDocument();
- expect(format).toHaveBeenCalledWith(newValue);
- expect(getByTestId('highlighted-value')).toBeInTheDocument();
- expect(getByTestId('highlighted-value')).toHaveStyle(`color: ${VarColors.Green500}; font-weight: 900;`);
- });
-});
diff --git a/components/console/src/core/components/HighlightValueCell/index.tsx b/components/console/src/core/components/HighlightValueCell/index.tsx
deleted file mode 100644
index 7bea390..0000000
--- a/components/console/src/core/components/HighlightValueCell/index.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { useMemo, useRef } from 'react';
-
-import { VarColors } from '@config/colors';
-
-import { HighlightValueCellProps } from './HighightValueCell.interfaces';
-
-const HighlightValueCell = function ({ value, format }: HighlightValueCellProps) {
- const prevValueRef = useRef();
-
- const isValueUpdated = useMemo(() => {
- if (!prevValueRef.current) {
- prevValueRef.current = value;
-
- return false;
- }
-
- if (format(value) !== format(prevValueRef.current)) {
- prevValueRef.current = value;
-
- return true;
- }
-
- return false;
- }, [format, value]);
-
- return isValueUpdated ? (
-
- {format(value)}
-
- ) : (
- format(value)
- );
-};
-
-export default HighlightValueCell;
diff --git a/components/console/src/core/components/LifecycleCell/LifecycleCell.interfaces.ts b/components/console/src/core/components/LifecycleCell/LifecycleCell.interfaces.ts
new file mode 100644
index 0000000..7a5411c
--- /dev/null
+++ b/components/console/src/core/components/LifecycleCell/LifecycleCell.interfaces.ts
@@ -0,0 +1,3 @@
+export interface LifecycleCellProps {
+ lifecycle: string;
+}
diff --git a/components/console/src/core/components/LifecycleCell/LifecycleCell.tsx b/components/console/src/core/components/LifecycleCell/LifecycleCell.tsx
new file mode 100644
index 0000000..847dcc0
--- /dev/null
+++ b/components/console/src/core/components/LifecycleCell/LifecycleCell.tsx
@@ -0,0 +1,93 @@
+import React from 'react';
+import { Icon } from '@patternfly/react-core';
+import { InProgressIcon, CheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import { hexColors } from '../../../config/colors';
+import labels from '../../../core/config/labels';
+import { LifecycleCellProps } from './LifecycleCell.interfaces';
+
+const LifecycleCell: React.FC = ({ lifecycle }) => {
+ let icon = null;
+ let iconColor = '';
+ let displayName = '';
+
+ switch (lifecycle) {
+ case 'ready':
+ icon = ;
+ iconColor = hexColors.Green500;
+ displayName = labels.status.active;
+ break;
+ case 'active':
+ icon = ;
+ iconColor = hexColors.Green500;
+ displayName = labels.status.active;
+ break;
+ case 'partial':
+ icon = ;
+ iconColor = hexColors.Orange400;
+ displayName = labels.status.pending;
+ break;
+ case 'new':
+ case 'initializing':
+ icon = ;
+ iconColor = hexColors.Blue400;
+ displayName = labels.status.initializing;
+ break;
+ case 'skx_cr_created':
+ case 'creating_resources':
+ icon = ;
+ iconColor = hexColors.Cyan500;
+ displayName = labels.status.creatingResources;
+ break;
+ case 'cm_cert_created':
+ case 'generating_certificates':
+ icon = ;
+ iconColor = hexColors.Purple400;
+ displayName = labels.status.generatingCertificates;
+ break;
+ case 'cm_issuer_created':
+ case 'configuring_issuer':
+ icon = ;
+ iconColor = hexColors.Indigo500;
+ displayName = labels.status.configuringIssuer;
+ break;
+ case 'deploying':
+ case 'starting':
+ icon = ;
+ iconColor = hexColors.Teal500;
+ displayName = labels.status.deploying;
+ break;
+ case 'expired':
+ icon = ;
+ iconColor = hexColors.Orange700;
+ displayName = labels.status.expired;
+ break;
+ case 'failed':
+ case 'error':
+ icon = ;
+ iconColor = hexColors.Red600;
+ displayName = labels.status.failed;
+ break;
+ case 'terminating':
+ case 'deleting':
+ icon = ;
+ iconColor = hexColors.Grey500;
+ displayName = labels.status.terminating;
+ break;
+ default:
+ icon = ;
+ iconColor = hexColors.Black300;
+ displayName = labels.status.unknown;
+ }
+
+ return (
+
+
+ {React.cloneElement(icon, { style: { color: iconColor } })}
+ {' '}
+ {displayName}
+
+ );
+};
+
+export default LifecycleCell;
diff --git a/components/console/src/core/components/LifecycleCell/index.tsx b/components/console/src/core/components/LifecycleCell/index.tsx
new file mode 100644
index 0000000..2f9bc74
--- /dev/null
+++ b/components/console/src/core/components/LifecycleCell/index.tsx
@@ -0,0 +1,149 @@
+import React from 'react';
+import { Icon } from '@patternfly/react-core';
+import { InProgressIcon, CheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import { hexColors } from '../../../config/colors';
+import {
+ NetworkLifeCycleStatus,
+ MemberLifeCycleStatus,
+ InvitationLifeCycleStatus,
+ ManagementControllerLifeCycleStatus,
+ ApplicationLifeCycleStatus
+} from '../../../API/REST.interfaces';
+import labels from '../../../core/config/labels';
+
+export type AnyLifecycleStatus =
+ | NetworkLifeCycleStatus
+ | MemberLifeCycleStatus
+ | InvitationLifeCycleStatus
+ | ManagementControllerLifeCycleStatus
+ | ApplicationLifeCycleStatus;
+
+export interface LifecycleCellProps {
+ lifecycle: AnyLifecycleStatus;
+ className?: string;
+}
+
+const LifecycleCell: React.FC = ({ lifecycle }) => {
+ let icon = CheckCircleIcon;
+ let iconColor = '';
+ let displayName = '';
+
+ switch (lifecycle) {
+ case 'ready':
+ case 'active':
+ icon = CheckCircleIcon;
+ iconColor = hexColors.Green500;
+ displayName = labels.status.active;
+ break;
+ case 'partial':
+ icon = InProgressIcon;
+ iconColor = hexColors.Orange400;
+ displayName = labels.status.pending;
+ break;
+ case 'new':
+ case 'initializing':
+ icon = InProgressIcon;
+ iconColor = hexColors.Blue400;
+ displayName = labels.status.initializing;
+ break;
+ case 'skx_cr_created':
+ case 'creating_resources':
+ icon = InProgressIcon;
+ iconColor = hexColors.Cyan500;
+ displayName = labels.status.creatingResources;
+ break;
+ case 'cm_cert_created':
+ case 'generating_certificates':
+ icon = InProgressIcon;
+ iconColor = hexColors.Purple400;
+ displayName = labels.status.generatingCertificates;
+ break;
+ case 'cm_issuer_created':
+ case 'configuring_issuer':
+ icon = InProgressIcon;
+ iconColor = hexColors.Indigo500;
+ displayName = labels.status.configuringIssuer;
+ break;
+ case 'deploying':
+ case 'starting':
+ icon = InProgressIcon;
+ iconColor = hexColors.Teal500;
+ displayName = labels.status.deploying;
+ break;
+ case 'deployed':
+ icon = CheckCircleIcon;
+ iconColor = hexColors.Green500;
+ displayName = 'Deployed';
+ break;
+ case 'expired':
+ icon = InProgressIcon;
+ iconColor = hexColors.Orange700;
+ displayName = labels.status.expired;
+ break;
+ case 'failed':
+ case 'error':
+ icon = ExclamationCircleIcon;
+ iconColor = hexColors.Red600;
+ displayName = labels.status.failed;
+ break;
+ case 'terminating':
+ case 'deleting':
+ icon = InProgressIcon;
+ iconColor = hexColors.Grey500;
+ displayName = labels.status.terminating;
+ break;
+ // Application lifecycle statuses
+ case 'created':
+ icon = CheckCircleIcon;
+ iconColor = hexColors.Blue400;
+ displayName = 'Created';
+ break;
+ case 'build-complete':
+ icon = CheckCircleIcon;
+ iconColor = hexColors.Green500;
+ displayName = 'Build Complete';
+ break;
+ case 'build-warnings':
+ icon = ExclamationCircleIcon;
+ iconColor = hexColors.Orange400;
+ displayName = 'Build Warnings';
+ break;
+ case 'build-errors':
+ icon = ExclamationCircleIcon;
+ iconColor = hexColors.Red600;
+ displayName = 'Build Errors';
+ break;
+ case 'deploy-complete':
+ icon = CheckCircleIcon;
+ iconColor = hexColors.Green600;
+ displayName = 'Deploy Complete';
+ break;
+ case 'deploy-warnings':
+ icon = ExclamationCircleIcon;
+ iconColor = hexColors.Orange400;
+ displayName = 'Deploy Warnings';
+ break;
+ case 'deploy-errors':
+ icon = ExclamationCircleIcon;
+ iconColor = hexColors.Red600;
+ displayName = 'Deploy Errors';
+ break;
+ default:
+ icon = InProgressIcon;
+ iconColor = hexColors.Black300;
+ displayName = labels.status.unknown;
+ }
+
+ return (
+
+
+ {React.createElement(icon, { style: { color: iconColor } })}
+ {' '}
+ {displayName}
+
+ );
+};
+
+export { LifecycleCell };
+export default LifecycleCell;
diff --git a/components/console/src/core/components/LinkCell/LinkCell.interfaces.ts b/components/console/src/core/components/LinkCell/LinkCell.interfaces.ts
deleted file mode 100644
index 0890ac5..0000000
--- a/components/console/src/core/components/LinkCell/LinkCell.interfaces.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface LinkCellProps {
- data: T;
- value: string | undefined;
- link: string;
- isDisabled?: boolean;
- type?: 'backbone' | 'site' | 'link' | 'van' | 'invitation';
- fitContent?: boolean;
-}
diff --git a/components/console/src/core/components/LinkCell/__tests__/LinkCell.spec.tsx b/components/console/src/core/components/LinkCell/__tests__/LinkCell.spec.tsx
deleted file mode 100644
index 09eece5..0000000
--- a/components/console/src/core/components/LinkCell/__tests__/LinkCell.spec.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { render, screen } from '@testing-library/react';
-
-import { Wrapper } from '@core/components/Wrapper';
-
-import LinkCell from '../index';
-
-describe('LinkCell', () => {
- const data = { id: 1, name: 'Test' };
-
- it('should render a disabled cell with Truncate for non-empty value', () => {
- render( );
- const truncateElement = screen.getByText('Long text');
- expect(truncateElement).toBeInTheDocument();
- });
-
- it('should render a non-disabled cell with Link for non-empty value', () => {
- render(
-
-
-
- );
- const linkElement = screen.getByRole('link');
- expect(linkElement).toHaveAttribute('href', '#/some-link');
- });
-
- it('should render an empty cell', () => {
- render( );
- const emptyElement = screen.getByText("''");
- expect(emptyElement).toBeInTheDocument();
- });
-
- it('should render a non-disabled cell with fitContent', () => {
- render(
-
-
-
- );
- const textElement = screen.getByText('Long text');
- expect(textElement).toBeInTheDocument();
- });
-
- it('should handle non-string values', () => {
- render( );
- const emptyElement = screen.getByText("''");
- expect(emptyElement).toBeInTheDocument();
- });
-});
diff --git a/components/console/src/core/components/LinkCell/index.tsx b/components/console/src/core/components/LinkCell/index.tsx
index 450ede0..846af3e 100644
--- a/components/console/src/core/components/LinkCell/index.tsx
+++ b/components/console/src/core/components/LinkCell/index.tsx
@@ -1,9 +1,16 @@
-import { Truncate } from '@patternfly/react-core';
+import { Truncate, Flex, FlexItem } from '@patternfly/react-core';
import { Link } from 'react-router-dom';
-import ResourceIcon from '@core/components/ResourceIcon';
+import ResourceIcon from '../ResourceIcon';
-import { LinkCellProps } from './LinkCell.interfaces';
+export interface LinkCellProps {
+ data: T;
+ value: string | undefined;
+ link: string;
+ isDisabled?: boolean;
+ type?: 'backbone' | 'site' | 'link' | 'van' | 'invitation' | 'library' | 'application' | 'deployment';
+ fitContent?: boolean;
+}
// Renders the value of the cell, either truncated or not depending on the fitContent prop
function renderValue(value: string, isDisabled: boolean, fitContent: boolean) {
@@ -34,17 +41,23 @@ const LinkCell = function ({ value, link, type, isDisabled = false, fitConten
const stringValue = value.toString();
return (
-
+
{/* If a type is provided, display a corresponding icon */}
- {type && }
- {/* If the cell is disabled, render the value without a link */}
- {isDisabled ? (
- renderValue(stringValue, isDisabled, fitContent)
- ) : (
- // Otherwise, render the value as a link
- {renderValue(stringValue, isDisabled, fitContent)}
+ {type && (
+
+
+
)}
-
+ {/* If the cell is disabled, render the value without a link */}
+
+ {isDisabled ? (
+ renderValue(stringValue, isDisabled, fitContent)
+ ) : (
+ // Otherwise, render the value as a link
+ {renderValue(stringValue, isDisabled, fitContent)}
+ )}
+
+
);
};
diff --git a/components/console/src/core/components/LocaleDateTimeCell/index.tsx b/components/console/src/core/components/LocaleDateTimeCell/index.tsx
new file mode 100644
index 0000000..53b77d2
--- /dev/null
+++ b/components/console/src/core/components/LocaleDateTimeCell/index.tsx
@@ -0,0 +1,147 @@
+import React from 'react';
+
+import { Flex, FlexItem, Icon, Tooltip } from '@patternfly/react-core';
+import { CalendarAltIcon, ClockIcon, HistoryIcon } from '@patternfly/react-icons';
+import { TableText } from '@patternfly/react-table';
+
+import { formatDateWithLocale, formatRelativeTime, getDateTooltip, parseDate } from '../../utils/dateFormatUtils';
+
+export interface LocaleDateTimeCellProps {
+ /** The date value to format - can be a string, number (timestamp), or Date object */
+ value?: string | number | Date | null | undefined;
+
+ /** Whether to show the date part (default: true) */
+ showDate?: boolean;
+
+ /** Whether to show the time part (default: true) */
+ showTime?: boolean;
+
+ /** Whether to show relative time (e.g., "2 hours ago") instead of absolute time */
+ showRelative?: boolean;
+
+ /** Whether to show an icon next to the formatted date */
+ showIcon?: boolean;
+
+ /** Custom icon to display (defaults to clock icon for timestamps) */
+ icon?: React.ReactNode;
+
+ /** Whether to use a compact format for space-constrained areas */
+ compact?: boolean;
+
+ /** Custom tooltip text (defaults to full timestamp) */
+ tooltip?: string;
+
+ /** Whether to show tooltip on hover (default: true) */
+ showTooltip?: boolean;
+
+ /** Custom CSS class name */
+ className?: string;
+
+ /** Placeholder text when value is null/undefined (default: "-") */
+ placeholder?: string;
+
+ /** Whether to use table cell styling optimized for data tables */
+ isTableCell?: boolean;
+
+ /** Whether to wrap text or truncate (default: true for table cells) */
+ truncate?: boolean;
+}
+
+/**
+ * A flexible date/time formatting component with browser locale support and icons
+ * Suitable for both table cells and general UI elements
+ */
+const LocaleDateTimeCell: React.FC = ({
+ value,
+ showDate = true,
+ showTime = true,
+ showRelative = false,
+ showIcon = true,
+ icon,
+ compact = false,
+ tooltip,
+ showTooltip = true,
+ className = '',
+ placeholder = '-',
+ isTableCell = false,
+ truncate = true
+}) => {
+ // Parse the input value to a Date object
+ const parsedDate = parseDate(value);
+
+ // If no valid date, show placeholder
+ if (!parsedDate) {
+ const content = {placeholder} ;
+
+ if (isTableCell) {
+ return {content} ;
+ }
+
+ return content;
+ }
+
+ // Format the date based on options
+ const formattedDate = showRelative
+ ? formatRelativeTime(parsedDate)
+ : formatDateWithLocale(parsedDate, {
+ showDate,
+ showTime,
+ compact
+ });
+
+ // Generate tooltip content
+ const tooltipContent = tooltip || getDateTooltip(parsedDate);
+
+ // Determine which icon to show
+ const getIcon = () => {
+ if (icon) {
+ return icon;
+ }
+
+ if (showRelative) {
+ return ;
+ }
+
+ if (showDate && showTime) {
+ return ;
+ }
+
+ if (showDate) {
+ return ;
+ }
+
+ return ;
+ };
+
+ // Create the content element
+ const content = (
+
+ {showIcon && (
+
+
+ {getIcon()}
+
+
+ )}
+
+ {formattedDate}
+
+
+ );
+
+ // Wrap with tooltip if enabled
+ const wrappedContent = showTooltip ? {content} : content;
+
+ // Return table-specific formatting if needed
+ if (isTableCell) {
+ return {wrappedContent} ;
+ }
+
+ return wrappedContent;
+};
+
+export default LocaleDateTimeCell;
diff --git a/components/console/src/core/components/ModalWrapper/index.tsx b/components/console/src/core/components/ModalWrapper/index.tsx
new file mode 100644
index 0000000..82aaa9b
--- /dev/null
+++ b/components/console/src/core/components/ModalWrapper/index.tsx
@@ -0,0 +1,107 @@
+import { FC, ReactNode } from 'react';
+
+import { Modal, ModalVariant, ModalBody, ModalHeader, ModalFooter, Button } from '@patternfly/react-core';
+
+import { ModalContextProvider, useModalContext } from '../../contexts/ModalContext';
+
+export interface ModalWrapperProps {
+ /** Modal title */
+ title: string;
+ /** Whether modal is open */
+ isOpen: boolean;
+ /** Close handler */
+ onClose: () => void;
+ /** Modal size variant */
+ variant?: ModalVariant;
+ /** Modal content */
+ children: ReactNode;
+ /** Whether modal has a body wrapper */
+ hasNoBodyWrapper?: boolean;
+ /** Whether to show footer with context actions */
+ showFooter?: boolean;
+ /** Optional aria-label for accessibility */
+ 'aria-label'?: string;
+ /** Optional aria-describedby for accessibility */
+ 'aria-describedby'?: string;
+}
+
+const ModalFooterButtons = function () {
+ const { actions } = useModalContext();
+ const { onSubmit, onCancel, isSubmitting, submitLabel, cancelLabel, isSubmitDisabled } = actions;
+
+ if (!onSubmit && !onCancel) {
+ return null;
+ }
+
+ return (
+
+ {onSubmit && (
+
+ {submitLabel || 'Submit'}
+
+ )}
+ {onCancel && (
+
+ {cancelLabel || 'Cancel'}
+
+ )}
+
+ );
+};
+
+/**
+ * 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 (
+ {NAV_GROUPS.map((group) => (
+
+
+ {group.items.map(({ name, path }) => (
+
+ {name}
+
+ ))}
+
+
+ ))}
+ {/* Render single items as top-level NavItems */}
- {ROUTES.map(({ name, path }) => (
+ {NAV_SINGLE_ITEMS.map(({ name, path }) => (
{name}
diff --git a/components/console/src/core/components/NavigationViewLink/index.tsx b/components/console/src/core/components/NavigationViewLink/index.tsx
index 3c65a53..ad9cf49 100644
--- a/components/console/src/core/components/NavigationViewLink/index.tsx
+++ b/components/console/src/core/components/NavigationViewLink/index.tsx
@@ -1,6 +1,6 @@
import { FC, ReactElement } from 'react';
-import { Icon, Text, TextContent, TextVariants } from '@patternfly/react-core';
+import { Icon, Content, ContentVariants } from '@patternfly/react-core';
import { ListIcon, TopologyIcon } from '@patternfly/react-icons';
import { Link } from 'react-router-dom';
@@ -15,13 +15,13 @@ const NavigationViewLink: FC<{ link: string; linkLabel: string; iconName?: 'topo
iconName = 'topologyIcon'
}) {
return (
-
-
-
+
+
+
{icons[iconName]} {linkLabel}
-
+
-
+
);
};
diff --git a/components/console/src/core/components/ResourceIcon/ResourceIcon.css b/components/console/src/core/components/ResourceIcon/ResourceIcon.css
index dd31331..e3f08dc 100644
--- a/components/console/src/core/components/ResourceIcon/ResourceIcon.css
+++ b/components/console/src/core/components/ResourceIcon/ResourceIcon.css
@@ -1,46 +1,18 @@
/* Resource icon */
.sk-resource-icon {
- background-color: var(--pf-v5-global--palette--black-500);
- border-radius: 20px;
- color: var(--pf-v5-global--palette--white);
- display: inline-block;
- flex-shrink: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ width: 22px;
+ height: 22px;
font-size: 12px;
- font-weight: 400;
- line-height: 20px;
- margin-right: 4px;
- min-width: 20px;
- padding: 0 4px;
- text-align: center;
- white-space: nowrap;
-}
-
-.sk-resource-site {
- background-color: var(--pf-v5-global--palette--green-600);
-}
-
-.sk-resource-process-group {
- background-color: var(--pf-v5-global--palette--light-green-500);
-}
-
-.sk-resource-service {
- background-color: var(--pf-v5-global--palette--purple-500);
-}
-
-.sk-resource-process {
- background-color: var(--pf-v5-global--palette--cyan-300);
-}
-
-.sk-resource-van {
- background-color: var(--pf-v5-global--palette--blue-300);
-}
-
-.sk-resource-invitation {
- background-color: var(--pf-v5-global--palette--gold-300);
-}
-
-.sk-resource-skupper {
- margin-left: -5px;
- min-width: 32px;
- background-color: transparent;
+ font-weight: 500;
+ font-family: inherit;
+ line-height: 1;
+ margin-right: 6px;
+ background: var(--pf-v6-global--palette--black-500); /* fallback, overridden inline */
+ color: var(--pf-t-global--palette--white);
+ -webkit-font-smoothing: antialiased;
+ user-select: none;
}
diff --git a/components/console/src/core/components/ResourceIcon/index.tsx b/components/console/src/core/components/ResourceIcon/index.tsx
index c6ef531..3a97c20 100644
--- a/components/console/src/core/components/ResourceIcon/index.tsx
+++ b/components/console/src/core/components/ResourceIcon/index.tsx
@@ -2,26 +2,67 @@ import { FC } from 'react';
import { Tooltip } from '@patternfly/react-core';
-import skupperProcessSVG from '@assets/skupper.svg';
+import skupperProcessSVG from '../../../assets/skupper.svg';
+import { hexColors } from '../../../config/colors';
import './ResourceIcon.css';
interface ResourceIconProps {
- type: 'site' | 'backbone' | 'link' | 'van' | 'invitation';
+ type: 'site' | 'backbone' | 'link' | 'van' | 'invitation' | 'accessPoint' | 'library' | 'application' | 'deployment';
}
const RESOURCE_MAP = {
- site: { class: 'sk-resource-site', symbol: 'S' },
- backbone: { class: 'sk-resource-process-group', symbol: 'B' },
- link: { class: 'sk-resource-process', symbol: 'L' },
- van: { class: 'sk-resource-van', symbol: 'V' },
- invitation: { class: 'sk-resource-invitation', symbol: 'I' }
+ backbone: {
+ class: 'sk-resource-backbone',
+ symbol: 'B',
+ style: { background: hexColors.Blue400, color: hexColors.White }
+ },
+ van: {
+ class: 'sk-resource-van',
+ symbol: 'V',
+ style: { background: hexColors.Orange700, color: hexColors.White }
+ },
+ site: {
+ class: 'sk-resource-site',
+ symbol: 'S',
+ style: { background: hexColors.Teal500, color: hexColors.White }
+ },
+ accessPoint: {
+ class: 'sk-resource-ap',
+ symbol: 'AP',
+ style: { background: hexColors.Red600, color: hexColors.White }
+ },
+ link: {
+ class: 'sk-resource-link',
+ symbol: 'L',
+ style: { background: hexColors.Purple300, color: hexColors.Black900 }
+ },
+ invitation: {
+ class: 'sk-resource-invitation',
+ symbol: 'I',
+ style: { background: hexColors.Green500, color: hexColors.White }
+ },
+ library: {
+ class: 'sk-resource-library',
+ symbol: 'LIB',
+ style: { background: hexColors.Purple700, color: hexColors.White }
+ },
+ application: {
+ class: 'sk-resource-applcation',
+ symbol: 'A',
+ style: { background: hexColors.Blue100, color: hexColors.Black900 }
+ },
+ deployment: {
+ class: 'sk-resource-deployment',
+ symbol: 'D',
+ style: { background: hexColors.Cyan500, color: hexColors.White }
+ }
};
const ResourceIcon: FC = function ({ type }) {
return (
-
+
{RESOURCE_MAP[type].symbol || }
diff --git a/components/console/src/core/components/SkBreadcrumb/index.tsx b/components/console/src/core/components/SkBreadcrumb/index.tsx
index e22ff76..003bb93 100644
--- a/components/console/src/core/components/SkBreadcrumb/index.tsx
+++ b/components/console/src/core/components/SkBreadcrumb/index.tsx
@@ -1,8 +1,7 @@
import { Breadcrumb, BreadcrumbHeading, BreadcrumbItem } from '@patternfly/react-core';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
-import { getTestsIds } from '@config/testIds';
-import { getIdAndNameFromUrlParams } from '@core/utils/getIdAndNameFromUrlParams';
+import { getIdAndNameFromUrlParams } from '../../utils/getIdAndNameFromUrlParams';
const SkBreadcrumb = function () {
const { pathname } = useLocation();
@@ -18,15 +17,26 @@ const SkBreadcrumb = function () {
const queryParams = searchParams.size > 0 ? `?${searchParams.toString()}` : '';
+ // Dynamically capitalize and format segment names from URL
+ // Automatically handles both section names and detail pages with nome@id format
+ const formatDisplayName = (segment: string) =>
+ // Capitalize first letter and replace common abbreviations
+ segment
+ .split(/[-_]/) // Split on hyphens and underscores
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+
return (
-
- {pathsNormalized.map((path, index) => (
-
- {path.name !== 'sites' && path.name !== 'invitations' && (
- {path.name}
- )}
-
- ))}
+
+ {pathsNormalized.map((path, index) => {
+ const displayName = formatDisplayName(path.name);
+
+ return (
+
+ {displayName}
+
+ );
+ })}
{lastPath?.name}
diff --git a/components/console/src/core/components/SkIsLoading/index.tsx b/components/console/src/core/components/SkIsLoading/index.tsx
index cee5f9c..da54748 100644
--- a/components/console/src/core/components/SkIsLoading/index.tsx
+++ b/components/console/src/core/components/SkIsLoading/index.tsx
@@ -1,23 +1,15 @@
import { Bullseye, Spinner } from '@patternfly/react-core';
-import { getTestsIds } from '@config/testIds';
-
const SkIsLoading = function ({ customSize = '150px' }) {
return (
-
+
diff --git a/components/console/src/core/components/SkSearchFilter/SkSearchFilter.css b/components/console/src/core/components/SkSearchFilter/SkSearchFilter.css
index 2cb101f..b8cdf06 100644
--- a/components/console/src/core/components/SkSearchFilter/SkSearchFilter.css
+++ b/components/console/src/core/components/SkSearchFilter/SkSearchFilter.css
@@ -2,6 +2,6 @@
padding-left: 8px;
}
-.sk-search-filter .pf-v5-c-text-input-group__icon {
+.sk-search-filter .pf-t-c-text-input-group__icon {
display: none;
}
diff --git a/components/console/src/core/components/SkSearchFilter/__tests__/SkFilter.spec.tsx b/components/console/src/core/components/SkSearchFilter/__tests__/SkFilter.spec.tsx
deleted file mode 100644
index 6a79b0e..0000000
--- a/components/console/src/core/components/SkSearchFilter/__tests__/SkFilter.spec.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { render, fireEvent } from '@testing-library/react';
-
-import SkSearchFilter from '..';
-
-describe('SkSearchFilter', () => {
- const onSearchMock = jest.fn();
- const selectOptions = [
- { id: 'option1', name: 'Option 1' },
- { id: 'option2', name: 'Option 2' }
- ];
-
- it('should handle input change', () => {
- const { getByPlaceholderText } = render( );
-
- const searchInput = getByPlaceholderText('Search by option 1') as HTMLInputElement;
- fireEvent.change(searchInput, { target: { value: 'test' } });
-
- expect(searchInput.value).toBe('test');
- });
-
- it('should delete filter when input value is empty', () => {
- const { getByPlaceholderText } = render( );
- const searchInput = getByPlaceholderText('Search by option 1') as HTMLInputElement;
-
- fireEvent.change(searchInput, { target: { value: null } });
- expect(searchInput.value).toBe('');
- });
-
- it('should clear input on clear button click', () => {
- const { getByPlaceholderText, getByText } = render(
-
- );
-
- const searchInput = getByPlaceholderText('Search by option 1') as HTMLInputElement;
- fireEvent.change(searchInput, { target: { value: 'test' } });
-
- const clearButton = getByText('Clear all filters');
- fireEvent.click(clearButton);
-
- expect(searchInput.value).toBe('');
- });
-});
diff --git a/components/console/src/core/components/SkSearchFilter/index.tsx b/components/console/src/core/components/SkSearchFilter/index.tsx
index 4f4d9fc..34df78e 100644
--- a/components/console/src/core/components/SkSearchFilter/index.tsx
+++ b/components/console/src/core/components/SkSearchFilter/index.tsx
@@ -1,6 +1,8 @@
import { useState, MouseEvent as ReactMouseEvent, Ref, FC, FormEvent, useEffect, memo } from 'react';
import {
+ Label,
+ LabelGroup,
Toolbar,
ToolbarItem,
ToolbarContent,
@@ -12,16 +14,13 @@ import {
Select,
SelectList,
SelectOption,
- ChipGroup,
- Chip,
ToolbarFilter,
Button
} from '@patternfly/react-core';
import { FilterIcon } from '@patternfly/react-icons';
-import useDebounce from 'hooks/useDebounce';
-
import './SkSearchFilter.css';
+import useDebounce from '../../utils/useDebounce';
interface FilterValues {
[key: string]: string | undefined;
@@ -115,7 +114,7 @@ const SkSearchFilter: FC<{ onSearch?: Function; selectOptions: { id: string; nam
{statusMenuItems}
-
+
+
{selectOptions.map(({ id, name }) => {
const value = filterValues[id as keyof FilterValues];
if (value) {
return (
-
- handleDeleteFilter(id as string)}>
+
+ handleDeleteFilter(id as string)}>
{value}
-
-
+
+
);
}
diff --git a/components/console/src/core/components/SkTable/SkTable.interfaces.ts b/components/console/src/core/components/SkTable/SkTable.interfaces.ts
index da5e6b0..4537b49 100644
--- a/components/console/src/core/components/SkTable/SkTable.interfaces.ts
+++ b/components/console/src/core/components/SkTable/SkTable.interfaces.ts
@@ -1,3 +1,4 @@
+import { ComponentType } from 'react';
import { TdProps } from '@patternfly/react-table';
export type NonNullableValue = T extends null | undefined ? never : T;
@@ -6,7 +7,7 @@ export interface SKTableProps {
columns: SKColumn>[];
rows?: NonNullableValue[];
title?: string;
- customCells?: Record;
+ customCells?: Record>;
borders?: boolean;
isStriped?: boolean;
isPlain?: boolean;
@@ -17,6 +18,8 @@ export interface SKTableProps {
paginationPageSize?: number;
paginationTotalRows?: number;
onGetFilters?: Function;
+ emptyStateMessage?: string;
+ emptyStateDescription?: string;
}
export interface SKColumn extends TdProps {
diff --git a/components/console/src/core/components/SkTable/index.tsx b/components/console/src/core/components/SkTable/index.tsx
index c41529d..d74db1a 100644
--- a/components/console/src/core/components/SkTable/index.tsx
+++ b/components/console/src/core/components/SkTable/index.tsx
@@ -1,6 +1,6 @@
import { KeyboardEvent, MouseEvent as ReactMouseEvent, useCallback, useState, useMemo } from 'react';
-import { Card, CardBody, CardHeader, Flex, Text, TextContent, Title } from '@patternfly/react-core';
+import { Card, CardBody, CardHeader, Flex, Content, Title } from '@patternfly/react-core';
import { SearchIcon } from '@patternfly/react-icons';
import {
InnerScrollContainer,
@@ -15,10 +15,9 @@ import {
Tr
} from '@patternfly/react-table';
-import { getValueFromNestedProperty } from '@core/utils/getValueFromNestedProperty';
-
import SkPagination from './SkPagination';
import { NonNullableValue, SKTableProps } from './SkTable.interfaces';
+import { getValueFromNestedProperty } from '../../utils/getValueFromNestedProperty';
import EmptyData from '../EmptyData';
const FIRST_PAGE_NUMBER = 1;
@@ -34,6 +33,8 @@ const SkTable = function ({
alwaysShowPagination = true,
paginationPageSize = PAGINATION_PAGE_SIZE,
paginationTotalRows = rows.length,
+ emptyStateMessage,
+ emptyStateDescription,
...props
}: SKTableProps) {
const [activeSortIndex, setActiveSortIndex] = useState();
@@ -146,14 +147,14 @@ const SkTable = function ({
{title && (
-
+
{title}
{!isPaginationEnabled && (
- {`${paginationTotalRows || rows.length} ${rows.length === 1 ? 'item' : 'items'}`}
+ {`${paginationTotalRows || rows.length} ${rows.length === 1 ? 'item' : 'items'}`}
)}
-
+
)}
@@ -165,7 +166,7 @@ const SkTable = function ({
{skColumns.map(({ name, prop, columnDescription, isStickyColumn, modifier }, index) => (
({
}
: undefined
}
+ screenReaderText={!name ? 'Actions' : undefined}
>
{name}
@@ -190,7 +192,7 @@ const SkTable = function ({
{skRows.length === 0 && (
-
+
)}
@@ -215,7 +217,7 @@ const SkTable = function ({
)}
{!Component && (
- {(format && format(value)) || value}
+ {String((format && format(value)) || value || '-')}
)}
diff --git a/components/console/src/core/components/SkUpdateButton/index.tsx b/components/console/src/core/components/SkUpdateButton/index.tsx
index dc7786e..2af5721 100644
--- a/components/console/src/core/components/SkUpdateButton/index.tsx
+++ b/components/console/src/core/components/SkUpdateButton/index.tsx
@@ -1,4 +1,4 @@
-import { FC, useMemo, MouseEvent as ReactMouseEvent, useState, useCallback, Ref, useRef } from 'react';
+import { FC, useMemo, MouseEvent as ReactMouseEvent, useState, useCallback, Ref, useRef, useEffect } from 'react';
import { Button, MenuToggle, MenuToggleElement, Select, SelectList, SelectOption } from '@patternfly/react-core';
import { SyncIcon } from '@patternfly/react-icons';
@@ -17,6 +17,10 @@ export const refreshDataIntervalMap = [
key: 'Refresh off',
value: 0
},
+ {
+ key: '5s',
+ value: 5 * 1000
+ },
{
key: '15s',
value: 15 * 1000
@@ -48,7 +52,7 @@ const SkUpdateDataButton: FC = function ({
findRefreshDataIntervalLabelFromValue(refreshIntervalDefault)
);
- const refreshIntervalId = useRef();
+ const refreshIntervalId = useRef(undefined);
const refreshIntervalOptions = useMemo(
() =>
@@ -93,6 +97,12 @@ const SkUpdateDataButton: FC = function ({
[onRefreshIntervalSelected, revalidateLiveQueries]
);
+ useEffect(() => {
+ return () => {
+ clearInterval(refreshIntervalId.current);
+ };
+ }, []);
+
return (
<>
= function ({
}
key="split-action-primary"
data-testid="update-data-click"
onClick={() => revalidateLiveQueries()}
style={{ borderTopLeftRadius: 0, borderBottomLeftRadius: 0 }}
- >
-
-
+ />
>
);
};
diff --git a/components/console/src/core/components/TitleSection/index.tsx b/components/console/src/core/components/TitleSection/index.tsx
new file mode 100644
index 0000000..bcf1f27
--- /dev/null
+++ b/components/console/src/core/components/TitleSection/index.tsx
@@ -0,0 +1,33 @@
+import { FC } from 'react';
+
+import { Flex, FlexItem, Title } from '@patternfly/react-core';
+
+import ResourceIcon from '../ResourceIcon';
+
+export interface TitleSectionProps {
+ /** The name/title to display */
+ title: string;
+ /** The type of resource icon to display (optional) */
+ resourceType?: 'link' | 'site' | 'backbone' | 'van' | 'invitation' | 'accessPoint' | 'library' | 'deployment';
+ /** The heading level for semantic HTML */
+ headingLevel?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
+}
+
+const TitleSection: FC = ({ title, resourceType, headingLevel = 'h1' }) => (
+
+ {resourceType && (
+
+
+
+ )}
+
+ {title}
+
+
+);
+
+export default TitleSection;
diff --git a/components/console/src/core/components/ToolbarActions/index.tsx b/components/console/src/core/components/ToolbarActions/index.tsx
new file mode 100644
index 0000000..8dec0f9
--- /dev/null
+++ b/components/console/src/core/components/ToolbarActions/index.tsx
@@ -0,0 +1,126 @@
+import { FC, ReactNode } from 'react';
+
+import { Toolbar, ToolbarContent, ToolbarItem, ToolbarGroup, Button, Title } from '@patternfly/react-core';
+import { PlusIcon } from '@patternfly/react-icons';
+
+export interface ToolbarAction {
+ label: string;
+ onClick: () => void;
+ icon?: ReactNode;
+ variant?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'link' | 'plain' | 'control';
+ isDisabled?: boolean;
+}
+
+export interface ToolbarActionsProps {
+ /** Page/section title */
+ title?: string;
+ /** Page/section description */
+ description?: string;
+ /** Primary action (usually create/add) */
+ primaryAction?: ToolbarAction;
+ /** Secondary actions */
+ secondaryActions?: ToolbarAction[];
+ /** Custom content on the left side */
+ leftContent?: ReactNode;
+ /** Custom content on the right side */
+ rightContent?: ReactNode;
+ /** Whether to align secondary actions to the right */
+ alignSecondaryRight?: boolean;
+}
+
+/**
+ * Reusable toolbar component that provides consistent action patterns
+ * Handles common toolbar layouts with title, description, and actions
+ */
+export const ToolbarActions: FC = function ({
+ title,
+ description,
+ primaryAction,
+ secondaryActions = [],
+ leftContent,
+ rightContent,
+ alignSecondaryRight = true
+}) {
+ const renderAction = (action: ToolbarAction, key: string) => (
+
+ {action.label}
+
+ );
+
+ const primaryButton = primaryAction && renderAction(primaryAction, 'primary');
+ const secondaryButtons = secondaryActions.map((action, index) => renderAction(action, `secondary-${index}`));
+
+ return (
+
+
+ {/* Left side content */}
+ {leftContent && {leftContent} }
+
+ {/* Title */}
+ {title && (
+
+
+
{title}
+ {description &&
{description}
}
+
+
+ )}
+
+ {/* Secondary actions (left-aligned if not alignSecondaryRight) */}
+ {!alignSecondaryRight && secondaryButtons.length > 0 && (
+
+ {secondaryButtons.map((button) => (
+ {button}
+ ))}
+
+ )}
+
+ {/* Right-aligned actions */}
+
+ {/* Secondary actions (right-aligned) */}
+ {alignSecondaryRight &&
+ secondaryButtons.map((button) => {button} )}
+
+ {/* Primary action */}
+ {primaryButton && {primaryButton} }
+
+ {/* Custom right content */}
+ {rightContent && {rightContent} }
+
+
+
+ );
+};
+
+/**
+ * Quick helper for common "Create" button toolbar
+ */
+export const CreateToolbar: FC<{
+ title: string;
+ description?: string;
+ createLabel?: string;
+ onCreate: () => void;
+ customActions?: ToolbarAction[];
+}> = function ({ title, description, createLabel = 'Create', onCreate, customActions = [] }) {
+ return (
+ ,
+ variant: 'primary'
+ }}
+ secondaryActions={customActions}
+ />
+ );
+};
+
+export default ToolbarActions;
diff --git a/components/console/src/core/components/TransitionPages/Fade.tsx b/components/console/src/core/components/TransitionPages/Fade.tsx
deleted file mode 100644
index 96e90d2..0000000
--- a/components/console/src/core/components/TransitionPages/Fade.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { CSSProperties, FC, ReactElement } from 'react';
-
-import { LazyMotion, domAnimation, m } from 'framer-motion';
-
-const TransitionPage: FC<{ children: ReactElement; delay?: number; style?: CSSProperties }> = function ({
- children,
- delay = 0,
- ...props
-}) {
- const transition = {
- in: {
- opacity: 1
- },
- out: {
- opacity: 0
- }
- };
-
- return (
-
-
- {children}
-
-
- );
-};
-
-export default TransitionPage;
diff --git a/components/console/src/core/components/ViewDetailsCell/ViewDetailCell.interface.ts b/components/console/src/core/components/ViewDetailsCell/ViewDetailCell.interface.ts
deleted file mode 100644
index deef82a..0000000
--- a/components/console/src/core/components/ViewDetailsCell/ViewDetailCell.interface.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface ViewDetailCellProps {
- link?: string;
- value?: T;
- onClick?: Function;
-}
diff --git a/components/console/src/core/components/ViewDetailsCell/__tests__/ViewDetailCell.spec.tsx b/components/console/src/core/components/ViewDetailsCell/__tests__/ViewDetailCell.spec.tsx
deleted file mode 100644
index 134fd65..0000000
--- a/components/console/src/core/components/ViewDetailsCell/__tests__/ViewDetailCell.spec.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { render, fireEvent } from '@testing-library/react';
-
-import ViewDetailCell from '../index';
-
-describe('ViewDetailCell', () => {
- it('should render a button with a search icon', () => {
- const { getByLabelText } = render( );
- const button = getByLabelText('Action');
- expect(button).toBeInTheDocument();
- });
-
- it('should call onClick when button is clicked', () => {
- const onClick = jest.fn();
- const { getByLabelText } = render( );
- const button = getByLabelText('Action');
-
- fireEvent.click(button);
- expect(onClick).toHaveBeenCalledWith('test');
- });
-});
diff --git a/components/console/src/core/components/ViewDetailsCell/index.tsx b/components/console/src/core/components/ViewDetailsCell/index.tsx
deleted file mode 100644
index d07c981..0000000
--- a/components/console/src/core/components/ViewDetailsCell/index.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useCallback } from 'react';
-
-import { Button } from '@patternfly/react-core';
-import { SearchIcon } from '@patternfly/react-icons';
-
-import { ViewDetailCellProps } from './ViewDetailCell.interface';
-
-const ViewDetailCell = function ({ value, onClick }: ViewDetailCellProps) {
- const handleOnClick = useCallback(() => {
- if (onClick) {
- onClick(value);
- }
- }, [value, onClick]);
-
- return (
-
-
-
- );
-};
-
-export default ViewDetailCell;
diff --git a/components/console/src/core/components/Wrapper.tsx b/components/console/src/core/components/Wrapper.tsx
index 40fe325..4d6d1dd 100644
--- a/components/console/src/core/components/Wrapper.tsx
+++ b/components/console/src/core/components/Wrapper.tsx
@@ -3,7 +3,7 @@ import { ReactElement } from 'react';
import { QueryClient, QueryClientConfig, QueryClientProvider } from '@tanstack/react-query';
import { HashRouter } from 'react-router-dom';
-import { queryClientConfig } from '@config/reactQuery';
+import { queryClientConfig } from '../../config/reactQuery';
const QueryClientContext = function ({
config = {},
diff --git a/components/console/src/core/config/labels.json b/components/console/src/core/config/labels.json
new file mode 100644
index 0000000..edb47ee
--- /dev/null
+++ b/components/console/src/core/config/labels.json
@@ -0,0 +1,350 @@
+{
+ "buttons": {
+ "submit": "Submit",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "activate": "Activate",
+ "done": "Done",
+ "add": "Add",
+ "edit": "Edit",
+ "save": "Save",
+ "saveChanges": "Save Changes",
+ "reset": "Reset",
+ "newAttribute": "New attribute",
+ "editAttribute": "Edit attribute",
+ "tryAgain": "Try again",
+ "createBackboneTitle": "Create Backbone",
+ "createVansTitle": "Create Vans",
+ "createLibraryTitle": "Create Library",
+ "createSiteTitle": "Create Site",
+ "editSiteTitle": "Edit Site",
+ "createAccessPoint": "Create Access Point",
+ "addLinkTitle": "Add Link",
+ "addApTitle": "Add AP",
+ "addInvitation": "Add Invitation",
+ "addMember": "Add Member",
+ "deleting": "Deleting...",
+ "evict": "Evict",
+ "addInterface": "Add Interface",
+ "saveInterface": "Save Interface",
+ "addTemplate": "Add Template",
+ "saveTemplate": "Save Template",
+ "editTemplate": "Edit Template",
+ "createApplicationTitle": "Create Application",
+ "createDeploymentTitle": "Create Deployment",
+ "build": "Build",
+ "showLog": "Show Log",
+ "clear": "Clear",
+ "close": "Close",
+ "maximize": "Maximize",
+ "minimize": "Minimize",
+ "logs": "Logs",
+ "view": "View",
+ "fitView": "Fit View",
+ "center": "Center",
+ "resetLayout": "Reset Layout",
+ "saveTopology": "Save Layout",
+ "expire": "Expire"
+ },
+ "validation": {
+ "required": "is required",
+ "nameRequired": "Name is required",
+ "typeRequired": "Type is required",
+ "bodyStyleRequired": "Body type is required",
+ "backboneRequired": "Backbone is required",
+ "bothAccessPointsRequired": "Both claim and member access points are required",
+ "interfaceNameRequired": "Interface name is required",
+ "interfaceRoleRequired": "Interface role is required",
+ "interfaceNameExists": "Interface name already exists",
+ "failedToUpdateInterfaces": "Failed to update interfaces",
+ "descriptionRequired": "Description is required",
+ "templateContentRequired": "Template content is required",
+ "targetPlatformsRequired": "At least one target platform must be selected",
+ "validationError": "Validation Error",
+ "deploymentNameRequired": "Deployment name is required",
+ "applicationNameRequired": "Application name is required",
+ "vanNameRequired": "VAN name is required",
+ "failedToCreateDeployment": "Failed to create deployment"
+ },
+ "forms": {
+ "name": "Name",
+ "description": "Description",
+ "type": "Type",
+ "bodyStyle": "Body Type",
+ "provider": "Provider",
+ "revision": "Revision",
+ "claim": "Claim",
+ "peer": "Peer",
+ "member": "Member",
+ "manage": "Manage",
+ "startImmediately": "Start Immediately",
+ "endTime": "End Time",
+ "startTime": "Created At",
+ "noEndTime": "No End Time",
+ "claimAccessPoint": "Claim AP",
+ "selectClaimAccessPoint": "Select claim access point",
+ "deploymentName": "Deployment Name",
+ "applicationName": "Application Name",
+ "vanName": "VAN Name",
+ "lifecycle": "Lifecycle",
+ "enterDeploymentName": "Enter deployment name",
+ "enterApplicationName": "Enter application name",
+ "enterVanName": "Enter VAN name",
+ "active": "Active",
+ "inactive": "Inactive",
+ "pending": "Pending",
+ "failed": "Failed",
+ "noClaimAccessPoints": "No claim access points available",
+ "memberAccessPoint": "Member AP",
+ "selectMemberAccessPoint": "Select member access point",
+ "noMemberAccessPoints": "No member access points available",
+ "joinDeadline": "Join Deadline",
+ "oneHour": "1 hour",
+ "oneDay": "1 day",
+ "oneWeek": "1 week",
+ "oneYear": "1 year",
+ "memberClass": "Member Class",
+ "siteClass": "Site Class",
+ "memberNamePrefix": "Member Name Prefix (optional)",
+ "instanceLimit": "Instance Limit",
+ "unlimited": "Unlimited",
+ "interactive": "Interactive",
+ "interactiveDescription": "Allow interactive claiming of this invitation",
+ "attribute": "Attribute",
+ "default": "Default",
+ "defaultValue": "Default Value",
+ "actions": "Actions",
+ "addNewAttribute": "Add New Attribute",
+ "interfaceName": "Interface Name",
+ "enterInterfaceName": "Enter interface name",
+ "role": "Role",
+ "polarity": "Polarity",
+ "maxBindings": "Max Bindings",
+ "data": "Data",
+ "additionalData": "Additional Data",
+ "interfaceDetails": "Interface Details",
+ "editInterface": "Edit Interface",
+ "deleteInterface": "Delete",
+ "addNewInterface": "Add New Interface",
+ "libraryInterfacesTable": "Library interfaces table",
+ "noInterfacesDefined": "No interfaces defined. Add an interface to get started.",
+ "expand": "Expand",
+ "collapse": "Collapse",
+ "addNewTemplate": "Add New Template",
+ "editTemplate": "Edit Template",
+ "templateDetails": "Template Details",
+ "templateContent": "Template Content",
+ "noTemplatesDefined": "No templates defined. Add a template to get started.",
+ "compositeBlockEditorTitle": "Composite Block Editor",
+ "compositeBlockDefinition": "Composite Block Definition",
+ "compositeBlockEditorInfo": "The composite block editor will be available in a future release. This will provide:",
+ "compositeBlockEditorFeatures": [
+ "Visual block composition interface",
+ "Drag-and-drop block placement",
+ "Interface connection management",
+ "Real-time validation"
+ ],
+ "targetPlatforms": "Target Platforms",
+ "affinity": "Affinity",
+ "sourceCode": "Source Code",
+ "rootBlock": "Root Block",
+ "status": "Status"
+ },
+ "columns": {
+ "name": "Name",
+ "backbones": "Backbones",
+ "status": "TLS Status",
+ "deploymentStatus": "Deployment Status",
+ "startTime": "Created At",
+ "endTime": "End Time",
+ "provider": "Provider",
+ "type": "Type",
+ "revision": "Revision",
+ "bodyStyle": "Body Type",
+ "created": "Created",
+ "backboneId": "Backbone ID",
+ "multitenant": "Multitenant",
+ "targetPlatform": "Target Platform",
+ "tlsCertificateExpiration": "TLS Certificate Expiration",
+ "tlsCertificateRenewal": "TLS Certificate Renewal",
+ "failure": "Failure",
+ "cost": "Cost",
+ "classes": "Classes",
+ "instances": "Instances",
+ "fetches": "Fetches",
+ "joinDeadline": "Join deadline",
+ "interactive": "Interactive",
+ "targetPlatforms": "Target Platforms",
+ "affinity": "Affinity",
+ "description": "Description",
+ "date": "Date",
+ "author": "Author",
+ "message": "Message",
+ "changes": "Changes",
+ "actions": "Actions"
+ },
+ "navigation": {
+ "network": "Network",
+ "inventory": "Inventory",
+ "runtime": "Runtime",
+ "backbones": "Backbones",
+ "vans": "VANs",
+ "topology": "Topology",
+ "sites": "Sites",
+ "siteDetails": "Site Details",
+ "links": "Links",
+ "libraries": "Libraries",
+ "members": "Members",
+ "invitations": "Invitations",
+ "accessPoints": "Access Points",
+ "outgoingInterRouterLinks": "Outgoing Inter-Router Links",
+ "applications": "Applications",
+ "deployments": "Deployments",
+ "networkTopology": "Network Topology",
+ "topologyOverview": "Topology Overview",
+ "topologyDetails": "Details"
+ },
+ "emptyStates": {
+ "noBackbonesFound": "No Backbones found",
+ "noBackbonesDescription": "No backbones are currently available. Create a new backbone to get started.",
+ "noVansFound": "No VANs found for backbone",
+ "noSitesFound": "No sites found for backbone",
+ "noAccessPointsFound": "No access points found",
+ "noAccessPointsConfigured": "No access points are currently configured.",
+ "noLinksFound": "No links found",
+ "noLinksConfigured": "No links are currently configured.",
+ "noLibrariesFound": "No Libraries found",
+ "noInvitationsFound": "No invitations found for this VAN",
+ "noInvitationsDescription": "No invitations have been created for this VAN.",
+ "noApplicationsFound": "No Applications found",
+ "noHistoryAvailable": "No History Available",
+ "noHistoryDescription": "No revision history is available for this library block.",
+ "noTopologyData": "No topology to display",
+ "noTopologyDataDescription": "Create at least one backbone with a site to view the network topology",
+ "loadingTopology": "Loading topology data...",
+ "deploymentConfigurationNotAvailable": "Deployment Configuration Not Available",
+ "failedToLoadDeploymentLog": "Failed to Load Deployment Log",
+ "noDeploymentLogAvailable": "No Deployment Log Available"
+ },
+ "errors": {
+ "generic": "An error occurred",
+ "requiredField": "Fill out all required fields before continuing",
+ "deleteConstraint": "Cannot delete: It is still being used by other components.",
+ "noLibrariesFound": "No libraries found",
+ "noLibrariesDescription": "No libraries are currently available.",
+ "noDeploymentsFound": "No deployments found",
+ "noDeploymentsDescription": "No deployments are currently available.",
+ "noMembersFound": "No members found for this VAN",
+ "noMembersDescription": "No members are currently part of this VAN.",
+ "configurationError": "Configuration Error",
+ "failedToUpdateConfiguration": "Failed to update configuration",
+ "pageFoundTitle": "Page Not Found",
+ "httpErrorTitle": "An error occurred",
+ "httpErrorHelp": "To help us resolve the issue quickly, we recommend following these steps using the DevTool browser",
+ "httpErrorStepOpenDevTools": "Open the DevTool browser (F12)",
+ "httpErrorStepNetworkConsole": "Navigate to the \"Network\" and \"Console\" tab. Look for any error messages or red-highlighted lines. These will provide essential clues about what went wrong",
+ "httpErrorStepScreenshot": "Capture screenshots of the error and any relevant details displayed in the console. This will help our development team better understand the problem",
+ "consoleErrorTitle": "An unexpected error occurred",
+ "copy": "Copy",
+ "copied": "Copied",
+ "accessPointError": "Access Point Error",
+ "errorLoadingAccessPoints": "Error loading access points",
+ "unableToFetchAccessPoints": "Unable to fetch access points",
+ "linkError": "Link Error",
+ "linkDeletionConstraint": "Cannot delete: Link is still referenced.",
+ "linkDeletionGeneric": "Failed to delete link.",
+ "errorLoadingLinks": "Error loading links",
+ "unableToFetchLinks": "Unable to fetch links",
+ "linkCreationTooltip": "You need at least one peer access point to create a link.",
+ "linkCreationRequiresPeerAccessPoints": "Peer Access Point and destination Site Required",
+ "linkCreationDescription": "At least one peer access point is required to create a link.",
+ "evictNotImplemented": "Evict functionality is not yet available. This feature is currently under development.",
+ "errorLoadingTopology": "Error loading topology data",
+ "noPeerAccessPointsTitle": "No peer access points available for link creation",
+ "noPeerAccessPointsDescription": "To create links, you need peer access points in other sites within this backbone.",
+ "noPeerAccessPointsTooltip": "No peer access points available in other sites for creating links."
+ },
+ "descriptions": {
+ "backbones": "A backbone is the central core of a network, that connects sites",
+ "sites": "Sites are locations in the backbone network where a backbone router is deployed.",
+ "libraries": "Manage your application libraries and compositions",
+ "applications": "Manage and deploy your applications using library blocks",
+ "deployments": "Deploy and manage application instances across your network infrastructure",
+ "vans": "Virtual Application Networks (VANs) using this backbone for secure connectivity.",
+ "invitations": "Invitations allow you to add new members to this VAN. Each invitation can be used to join the network according to its configuration.",
+ "members": "Members are sites that have joined this VAN using an invitation. They participate in the application network.",
+ "configuration": "Manage configuration template attributes",
+ "interfaces": "Configure block interfaces and connections",
+ "bodySimple": "Simple block specification",
+ "bodyComposite": "Composite block definition",
+ "test": "Test composite block configuration",
+ "history": "View block revision history",
+ "revisionHistory": "Revision History",
+ "revisionDetails": "Revision Details",
+ "loading": "Loading log...",
+ "failedToLoad": "Failed to load log:",
+ "selectApplication": "Select an application to view its build log",
+ "noBuildLogAvailable": "No build log available for this application",
+ "loadingHistory": "Loading revision history..."
+ },
+ "generic": {
+ "id": "ID",
+ "name": "Name",
+ "status": "TLS Status",
+ "backbone": "Backbone",
+ "startTime": "Created At",
+ "endTime": "End Time",
+ "tlsExpiration": "CA Expiration",
+ "tlsRenewal": "CA Renewal Time",
+ "unknown": "Unknown",
+ "unnamed": "Unnamed",
+ "notAssigned": "Not assigned",
+ "notStarted": "Not started",
+ "notEnded": "Not ended",
+ "viaInvitation": "via invitation",
+ "lastHeartbeat": "Last heartbeat",
+ "never": "Never",
+ "firstActive": "First active",
+ "notActive": "Not active",
+ "evicting": "Evicting...",
+ "evict": "Evict",
+ "yes": "Yes",
+ "no": "No",
+ "enabled": "Enabled",
+ "disabled": "Disabled",
+ "notAvailable": "Not available",
+ "noHostBound": "No host bound",
+ "noAddress": "No address",
+ "configuration": "Configuration",
+ "interfaces": "Interfaces",
+ "body": "Body",
+ "test": "Test",
+ "history": "Revision History"
+ },
+
+ "placeholders": {
+ "attributeName": "Attribute name",
+ "defaultValue": "Default value",
+ "attributeDescription": "Attribute description",
+ "sourceCodePlaceholder": "Enter source code or configuration...",
+ "rootBlockPlaceholder": "Enter root block name..."
+ },
+ "status": {
+ "active": "Active",
+ "pending": "Pending",
+ "new": "New",
+ "expired": "Expired",
+ "failed": "Failed",
+ "initializing": "Initializing",
+ "creatingResources": "Creating resources",
+ "generatingCertificates": "Generating certificates",
+ "configuringIssuer": "Configuring issuer",
+ "deploying": "Deploying",
+ "status": "Status",
+ "terminating": "Terminating",
+ "unknown": "Unknown"
+ },
+ "config": {
+ "bodyStyles": ["simple", "composite"]
+ }
+}
diff --git a/components/console/src/core/config/labels.ts b/components/console/src/core/config/labels.ts
new file mode 100644
index 0000000..2351237
--- /dev/null
+++ b/components/console/src/core/config/labels.ts
@@ -0,0 +1,2 @@
+import labels from './labels.json';
+export default labels;
diff --git a/components/console/src/core/constants/ValidationMessages.enum.ts b/components/console/src/core/constants/ValidationMessages.enum.ts
new file mode 100644
index 0000000..e69de29
diff --git a/components/console/src/core/contexts/ModalContext.tsx b/components/console/src/core/contexts/ModalContext.tsx
new file mode 100644
index 0000000..799b40a
--- /dev/null
+++ b/components/console/src/core/contexts/ModalContext.tsx
@@ -0,0 +1,41 @@
+import { createContext, useContext, ReactNode, useState, useCallback } from 'react';
+
+interface ModalActionsType {
+ onSubmit?: () => void;
+ onCancel?: () => void;
+ isSubmitting?: boolean;
+ submitLabel?: string;
+ cancelLabel?: string;
+ isSubmitDisabled?: boolean;
+}
+
+interface ModalContextType {
+ actions: ModalActionsType;
+ setActions: (actions: ModalActionsType) => void;
+ updateActions: (updates: Partial) => void;
+}
+
+const ModalContext = createContext(null);
+
+export const useModalContext = () => {
+ const context = useContext(ModalContext);
+ if (!context) {
+ throw new Error('useModalContext must be used within a ModalContextProvider');
+ }
+
+ return context;
+};
+
+export const ModalContextProvider = function ({ children }: { children: ReactNode }) {
+ const [actions, setActionsState] = useState({});
+
+ const setActions = useCallback((newActions: ModalActionsType) => {
+ setActionsState(newActions);
+ }, []);
+
+ const updateActions = useCallback((updates: Partial) => {
+ setActionsState((prev) => ({ ...prev, ...updates }));
+ }, []);
+
+ return {children} ;
+};
diff --git a/components/console/src/core/utils/formatLatency.ts b/components/console/src/core/hooks/formatLatency.ts
similarity index 100%
rename from components/console/src/core/utils/formatLatency.ts
rename to components/console/src/core/hooks/formatLatency.ts
diff --git a/components/console/src/core/hooks/useModal.ts b/components/console/src/core/hooks/useModal.ts
new file mode 100644
index 0000000..3daa13c
--- /dev/null
+++ b/components/console/src/core/hooks/useModal.ts
@@ -0,0 +1,37 @@
+import { useCallback, useState } from 'react';
+
+/**
+ * Generic modal state management hook
+ * Provides consistent modal state management across the application
+ */
+export const useModal = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const [editingItem, setEditingItem] = useState();
+
+ const openModal = useCallback(() => {
+ setIsOpen(true);
+ }, []);
+
+ const openEditModal = useCallback((item: T) => {
+ setEditingItem(item);
+ setIsOpen(true);
+ }, []);
+
+ const closeModal = useCallback(() => {
+ setIsOpen(false);
+ setEditingItem(undefined);
+ }, []);
+
+ const toggleModal = useCallback(() => {
+ setIsOpen((prev) => !prev);
+ }, []);
+
+ return {
+ isOpen,
+ editingItem,
+ openModal,
+ openEditModal,
+ closeModal,
+ toggleModal
+ };
+};
diff --git a/components/console/src/core/hooks/useModalActions.ts b/components/console/src/core/hooks/useModalActions.ts
new file mode 100644
index 0000000..74e74dd
--- /dev/null
+++ b/components/console/src/core/hooks/useModalActions.ts
@@ -0,0 +1,42 @@
+import { useEffect } from 'react';
+
+import { useModalContext } from '../contexts/ModalContext';
+
+interface UseModalActionsProps {
+ onSubmit?: () => void;
+ onCancel?: () => void;
+ isSubmitting?: boolean;
+ submitLabel?: string;
+ cancelLabel?: string;
+ isSubmitDisabled?: boolean;
+}
+
+export const useModalActions = ({
+ onSubmit,
+ onCancel,
+ isSubmitting = false,
+ submitLabel,
+ cancelLabel,
+ isSubmitDisabled = false
+}: UseModalActionsProps) => {
+ const { setActions } = useModalContext();
+
+ useEffect(() => {
+ setActions({
+ onSubmit,
+ onCancel,
+ isSubmitting,
+ submitLabel,
+ cancelLabel,
+ isSubmitDisabled
+ });
+ }, [onSubmit, onCancel, isSubmitting, submitLabel, cancelLabel, isSubmitDisabled, setActions]);
+
+ // Cleanup quando il componente viene smontato
+ useEffect(
+ () => () => {
+ setActions({});
+ },
+ [setActions]
+ );
+};
diff --git a/components/console/src/core/hooks/useMutationWithCacheInvalidation.ts b/components/console/src/core/hooks/useMutationWithCacheInvalidation.ts
new file mode 100644
index 0000000..311cb97
--- /dev/null
+++ b/components/console/src/core/hooks/useMutationWithCacheInvalidation.ts
@@ -0,0 +1,220 @@
+import { useMutation, useQueryClient, MutationFunction, UseMutationOptions } from '@tanstack/react-query';
+
+import { QueriesBackbones } from '../../pages/Backbones/Backbones.enum';
+import { QueriesTopology } from '../../pages/Topology/Topology.enum';
+import { QueriesVans } from '../../pages/Vans/Vans.enum';
+import { QueriesApplications } from '../../pages/Applications/Applications.enum';
+import { QueriesLibraries } from '../../pages/Libraries/Libraries.enum';
+
+/**
+ * Configuration for cache invalidation patterns
+ */
+export interface CacheInvalidationConfig {
+ /** Specific query keys to invalidate */
+ queryKeys?: string[][];
+ /** Invalidate all backbone-related queries */
+ invalidateBackbones?: boolean;
+ /** Invalidate topology data */
+ invalidateTopology?: boolean;
+ /** Invalidate VANs data */
+ invalidateVans?: boolean;
+ /** Invalidate applications data */
+ invalidateApplications?: boolean;
+ /** Invalidate libraries data */
+ invalidateLibraries?: boolean;
+ /** Invalidate specific backbone sites by backbone ID */
+ invalidateBackboneSites?: string[];
+ /** Custom invalidation function */
+ customInvalidation?: (queryClient: ReturnType) => void;
+}
+
+/**
+ * Hook that wraps useMutation with automatic cache invalidation
+ *
+ * @param mutationFn - The mutation function
+ * @param invalidationConfig - Configuration for which caches to invalidate
+ * @param options - Additional mutation options
+ * @returns Enhanced mutation with automatic cache invalidation
+ */
+export const useMutationWithCacheInvalidation = (
+ mutationFn: MutationFunction,
+ invalidationConfig: CacheInvalidationConfig,
+ options?: UseMutationOptions
+) => {
+ const queryClient = useQueryClient();
+
+ const invalidateQueries = async () => {
+ const promises: Promise[] = [];
+
+ // Invalidate specific query keys
+ if (invalidationConfig.queryKeys) {
+ for (const queryKey of invalidationConfig.queryKeys) {
+ promises.push(queryClient.invalidateQueries({ queryKey }));
+ }
+ }
+
+ // Invalidate backbone-related queries
+ if (invalidationConfig.invalidateBackbones) {
+ promises.push(
+ queryClient.invalidateQueries({ queryKey: [QueriesBackbones.GetBackbones] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesBackbones.GetLinks] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesBackbones.GetIncomingLinks] })
+ );
+ }
+
+ // Invalidate specific backbone sites
+ if (invalidationConfig.invalidateBackboneSites) {
+ for (const backboneId of invalidationConfig.invalidateBackboneSites) {
+ promises.push(
+ queryClient.invalidateQueries({ queryKey: [QueriesBackbones.GetSites, backboneId] })
+ );
+ }
+ }
+
+ // Invalidate topology data
+ if (invalidationConfig.invalidateTopology) {
+ promises.push(
+ queryClient.invalidateQueries({ queryKey: [QueriesTopology.GetTopologyData] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesTopology.GetBackbones] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesTopology.GetSites] })
+ );
+ }
+
+ // Invalidate VANs data
+ if (invalidationConfig.invalidateVans) {
+ promises.push(
+ queryClient.invalidateQueries({ queryKey: [QueriesVans.GetVans] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesVans.GetVanMembers] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesVans.GetVanInvitations] })
+ );
+ }
+
+ // Invalidate applications data
+ if (invalidationConfig.invalidateApplications) {
+ promises.push(
+ queryClient.invalidateQueries({ queryKey: [QueriesApplications.GetApplications] })
+ );
+ }
+
+ // Invalidate libraries data
+ if (invalidationConfig.invalidateLibraries) {
+ promises.push(
+ queryClient.invalidateQueries({ queryKey: [QueriesLibraries.GetLibraries] }),
+ queryClient.invalidateQueries({ queryKey: [QueriesLibraries.GetLibraryBlocks] })
+ );
+ }
+
+ // Custom invalidation
+ if (invalidationConfig.customInvalidation) {
+ invalidationConfig.customInvalidation(queryClient);
+ }
+
+ await Promise.all(promises);
+ };
+
+ return useMutation({
+ ...options,
+ mutationFn,
+ onSuccess: async (data, variables, context) => {
+ // Execute cache invalidation
+ await invalidateQueries();
+
+ // Call original onSuccess if provided
+ if (options?.onSuccess) {
+ await options.onSuccess(data, variables, context);
+ }
+ },
+ });
+};
+
+/**
+ * Predefined invalidation configurations for common operations
+ */
+export const CacheInvalidationPresets = {
+ // Backbone operations
+ createBackbone: {
+ invalidateBackbones: true,
+ invalidateTopology: true,
+ } as CacheInvalidationConfig,
+
+ deleteBackbone: {
+ invalidateBackbones: true,
+ invalidateTopology: true,
+ } as CacheInvalidationConfig,
+
+ activateBackbone: {
+ invalidateBackbones: true,
+ invalidateTopology: true,
+ } as CacheInvalidationConfig,
+
+ // Site operations
+ createSite: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ }) as CacheInvalidationConfig,
+
+ deleteSite: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ }) as CacheInvalidationConfig,
+
+ updateSite: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ }) as CacheInvalidationConfig,
+
+ // Link operations
+ createLink: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ queryKeys: [[QueriesBackbones.GetLinks]],
+ }) as CacheInvalidationConfig,
+
+ deleteLink: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ queryKeys: [[QueriesBackbones.GetLinks]],
+ }) as CacheInvalidationConfig,
+
+ // Access Point operations
+ createAccessPoint: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ }) as CacheInvalidationConfig,
+
+ deleteAccessPoint: (backboneId: string) => ({
+ invalidateBackboneSites: [backboneId],
+ invalidateTopology: true,
+ }) as CacheInvalidationConfig,
+
+ // VAN operations
+ createVan: {
+ invalidateVans: true,
+ } as CacheInvalidationConfig,
+
+ deleteVan: {
+ invalidateVans: true,
+ } as CacheInvalidationConfig,
+
+ // Application operations
+ createApplication: {
+ invalidateApplications: true,
+ } as CacheInvalidationConfig,
+
+ deleteApplication: {
+ invalidateApplications: true,
+ } as CacheInvalidationConfig,
+
+ buildApplication: {
+ invalidateApplications: true,
+ } as CacheInvalidationConfig,
+
+ // Library operations
+ createLibrary: {
+ invalidateLibraries: true,
+ } as CacheInvalidationConfig,
+
+ deleteLibrary: {
+ invalidateLibraries: true,
+ } as CacheInvalidationConfig,
+};
diff --git a/components/console/src/core/utils/convertToPercentage.spec.ts b/components/console/src/core/utils/convertToPercentage.spec.ts
deleted file mode 100644
index f7d9b74..0000000
--- a/components/console/src/core/utils/convertToPercentage.spec.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { convertToPercentage } from './convertToPercentage';
-
-describe('convertToPercentage', () => {
- it('should return a percentage string removing the cents', () => {
- expect(convertToPercentage(3, 10)).toBe('30%');
- expect(convertToPercentage(2, 7)).toBe('29%');
- expect(convertToPercentage(0, 100)).toBe('0%');
- expect(convertToPercentage(10, 0)).toBe('Infinity%');
- expect(convertToPercentage(NaN, 100)).toBeNull();
- });
-
- it('should return null when the input is invalid', () => {
- expect(convertToPercentage(3, NaN)).toBeNull();
- expect(convertToPercentage(NaN, NaN)).toBeNull();
- });
-
- it('should handle values that are too large for Number.MAX_SAFE_INTEGER', () => {
- expect(convertToPercentage(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)).toBe('100%');
- });
-
- it('should handle negative values', () => {
- expect(convertToPercentage(-5, 20)).toBe('-25%');
- expect(convertToPercentage(5, -20)).toBe('-25%');
- expect(convertToPercentage(-5, -20)).toBe('25%');
- });
-
- it('should handle decimal values', () => {
- expect(convertToPercentage(3.14, 10)).toBe('31%');
- expect(convertToPercentage(2, 7.5)).toBe('27%');
- expect(convertToPercentage(1, 3.33)).toBe('30%');
- expect(convertToPercentage(0.1, 10)).toBe('1%');
- expect(convertToPercentage(0.001, 100)).toBe('0%');
- });
-});
diff --git a/components/console/src/core/utils/dateFormatUtils.ts b/components/console/src/core/utils/dateFormatUtils.ts
new file mode 100644
index 0000000..b76b0a9
--- /dev/null
+++ b/components/console/src/core/utils/dateFormatUtils.ts
@@ -0,0 +1,156 @@
+/**
+ * Utility functions for date formatting with browser locale support
+ * Uses only native JavaScript APIs for simple, dependency-free date handling
+ */
+
+// Type declarations for global objects that might not be available in all environments
+declare const navigator: { language: string } | undefined;
+declare const console: { warn: (message?: any, ...optionalParams: any[]) => void } | undefined;
+
+/**
+ * Gets the browser locale or falls back to 'en-US'
+ */
+function getBrowserLocale(): string {
+ if (typeof navigator !== 'undefined' && navigator?.language) {
+ return navigator.language;
+ }
+ return 'en-US';
+}
+
+/**
+ * Formats a date using the browser's locale settings
+ */
+export function formatDateWithLocale(
+ date: Date,
+ options: {
+ showDate?: boolean;
+ showTime?: boolean;
+ dateStyle?: Intl.DateTimeFormatOptions['dateStyle'];
+ timeStyle?: Intl.DateTimeFormatOptions['timeStyle'];
+ locale?: string;
+ compact?: boolean;
+ } = {}
+): string {
+ const {
+ showDate = true,
+ showTime = true,
+ dateStyle = 'medium',
+ timeStyle = 'medium',
+ locale,
+ compact = false
+ } = options;
+
+ // Use browser locale if none specified
+ const formatLocale = locale || getBrowserLocale();
+
+ // For compact format, use shorter styles
+ const actualDateStyle = compact ? 'short' : dateStyle;
+ const actualTimeStyle = compact ? 'short' : timeStyle;
+
+ try {
+ if (showDate && showTime) {
+ return new Intl.DateTimeFormat(formatLocale, {
+ dateStyle: actualDateStyle,
+ timeStyle: actualTimeStyle
+ }).format(date);
+ } else if (showDate) {
+ return new Intl.DateTimeFormat(formatLocale, {
+ dateStyle: actualDateStyle
+ }).format(date);
+ } else if (showTime) {
+ return new Intl.DateTimeFormat(formatLocale, {
+ timeStyle: actualTimeStyle
+ }).format(date);
+ }
+ } catch (error) {
+ // Fallback to standard formatting if locale is not supported
+ if (typeof console !== 'undefined' && console?.warn) {
+ console.warn('Locale formatting failed, using fallback:', error);
+ }
+ return date.toLocaleString();
+ }
+
+ return date.toLocaleString();
+}
+
+/**
+ * Formats relative time (e.g., "2 hours ago", "in 3 days") using native JavaScript
+ */
+export function formatRelativeTime(date: Date): string {
+ const now = new Date();
+ const diffInMs = now.getTime() - date.getTime();
+ const diffInSeconds = Math.floor(diffInMs / 1000);
+ const diffInMinutes = Math.floor(diffInSeconds / 60);
+ const diffInHours = Math.floor(diffInMinutes / 60);
+ const diffInDays = Math.floor(diffInHours / 24);
+ const diffInWeeks = Math.floor(diffInDays / 7);
+ const diffInMonths = Math.floor(diffInDays / 30);
+ const diffInYears = Math.floor(diffInDays / 365);
+
+ const formatUnit = (value: number, unit: string) => {
+ const absValue = Math.abs(value);
+ const plural = absValue !== 1 ? 's' : '';
+ const direction = value < 0 ? 'in ' : '';
+ const suffix = value >= 0 ? ' ago' : '';
+ return `${direction}${absValue} ${unit}${plural}${suffix}`;
+ };
+
+ if (Math.abs(diffInYears) >= 1) {
+ return formatUnit(diffInYears, 'year');
+ } else if (Math.abs(diffInMonths) >= 1) {
+ return formatUnit(diffInMonths, 'month');
+ } else if (Math.abs(diffInWeeks) >= 1) {
+ return formatUnit(diffInWeeks, 'week');
+ } else if (Math.abs(diffInDays) >= 1) {
+ return formatUnit(diffInDays, 'day');
+ } else if (Math.abs(diffInHours) >= 1) {
+ return formatUnit(diffInHours, 'hour');
+ } else if (Math.abs(diffInMinutes) >= 1) {
+ return formatUnit(diffInMinutes, 'minute');
+ } else {
+ return formatUnit(diffInSeconds, 'second');
+ }
+}
+
+/**
+ * Converts various date inputs to a Date object
+ */
+export function parseDate(value: string | number | Date | null | undefined): Date | null {
+ if (!value) {
+ return null;
+ }
+
+ if (value instanceof Date) {
+ return isNaN(value.getTime()) ? null : value;
+ }
+
+ if (typeof value === 'number') {
+ // Handle both milliseconds and seconds timestamps
+ const timestamp = value < 1e12 ? value * 1000 : value;
+ const date = new Date(timestamp);
+ return isNaN(date.getTime()) ? null : date;
+ }
+
+ if (typeof value === 'string') {
+ const date = new Date(value);
+ return isNaN(date.getTime()) ? null : date;
+ }
+
+ return null;
+}
+
+/**
+ * Gets a comprehensive tooltip with full date/time information
+ */
+export function getDateTooltip(date: Date, locale?: string): string {
+ const formatLocale = locale || getBrowserLocale();
+
+ try {
+ return new Intl.DateTimeFormat(formatLocale, {
+ dateStyle: 'full',
+ timeStyle: 'long'
+ }).format(date);
+ } catch (error) {
+ return date.toString();
+ }
+}
diff --git a/components/console/src/core/utils/deepMergeWithJSONObjects.spec.ts b/components/console/src/core/utils/deepMergeWithJSONObjects.spec.ts
deleted file mode 100644
index 6084486..0000000
--- a/components/console/src/core/utils/deepMergeWithJSONObjects.spec.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import { deepMergeJSONObjects } from './deepMergeWithJSONObjects';
-
-describe('deepMergeJSONObjects', () => {
- it('should merge two objects with shallow properties', () => {
- const obj1 = { a: 1, b: 2 };
- const obj2 = { b: 3, c: 4 };
-
- const result = deepMergeJSONObjects(obj1, obj2);
-
- expect(result).toEqual({ a: 1, b: 3, c: 4 });
- });
-
- it('should merge two objects with nested properties', () => {
- const obj1 = {
- a: 1,
- b: { c: 2, d: 6 }
- };
- const obj2 = {
- a: 0,
- b: { c: 1 }
- };
-
- const result = deepMergeJSONObjects<{ a: number; b: { c: number; d?: number } }>(obj1, obj2);
-
- expect(result).toEqual({
- a: 0,
- b: { c: 1, d: 6 }
- });
- });
-
- it('should handle empty objects', () => {
- const obj1 = {};
- const obj2 = { a: 1, b: { c: 2 } };
-
- const result = deepMergeJSONObjects(obj1, obj2);
-
- expect(result).toEqual({ a: 1, b: { c: 2 } });
- });
-});
diff --git a/components/console/src/core/utils/formatBytes.spec.ts b/components/console/src/core/utils/formatBytes.spec.ts
deleted file mode 100644
index e8294c7..0000000
--- a/components/console/src/core/utils/formatBytes.spec.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { formatByteRate, formatBytes } from './formatBytes';
-
-describe('formatBytes', () => {
- it('should return an empty string if bytesSized is NaN', () => {
- const bytes = -1024;
- const result = formatBytes(bytes);
-
- expect(result).toBe('');
- });
-
- it('should return "1 B" for 1 byte', () => {
- const result = formatBytes(1);
- expect(result).toEqual('1 B');
- });
-
- it('should return "1 KB" for 1024 bytes', () => {
- const result = formatBytes(1024);
- expect(result).toEqual('1 KB');
- });
-
- it('should return "0 B" for 0 byte', () => {
- const result = formatBytes(0);
- expect(result).toEqual('0 B');
- });
-
- it('should return empty string for negative bytes', () => {
- const result = formatBytes(-1024);
- expect(result).toEqual('');
- });
-
- it('should return "1.12 MB" for 1171868 bytes with 2 decimal points', () => {
- const result = formatBytes(1171868, 2);
- expect(result).toEqual('1.12 MB');
- });
-
- it('should return "1023 B" for 1023 bytes', () => {
- const result = formatBytes(1023);
- expect(result).toEqual('1023 B');
- });
-
- it('should return "1023 B" for 1023 bytes with 2 decimal points', () => {
- const result = formatBytes(1023, 2);
- expect(result).toEqual('1023 B');
- });
-});
-
-describe('formatByteRate', () => {
- it('should format the byte rate correctly with default decimals', () => {
- const byteRate = 1024 * 5; // 5 KB/s
- const result = formatByteRate(byteRate);
- const expectedFormatted = '5 KB/s';
-
- expect(result).toBe(expectedFormatted);
- });
-
- it('should format the byte rate correctly removing 0 padding', () => {
- const byteRate = 1024 * 5;
- const decimals = 4;
- const result = formatByteRate(byteRate, decimals);
- const expectedFormatted = '5 KB/s';
-
- expect(result).toBe(expectedFormatted);
- });
-
- it('should format the byte rate correctly with custom decimals', () => {
- const byteRate = 1024 * 2.54321;
- const decimals = 3;
- const result = formatByteRate(byteRate, decimals);
- const expectedFormatted = '2.543 KB/s';
-
- expect(result).toBe(expectedFormatted);
- });
-
- it('should return an empty string for NaN input', () => {
- const result = formatByteRate(NaN);
- expect(result).toBe('');
- });
-
- it('should return an empty string for negative input', () => {
- const result = formatByteRate(-100);
- expect(result).toBe('');
- });
-
- it('should return "0 B/s" for input 0', () => {
- const result = formatByteRate(0);
- expect(result).toBe('0 B/s');
- });
-});
diff --git a/components/console/src/core/utils/formatChartDate.spec.ts b/components/console/src/core/utils/formatChartDate.spec.ts
deleted file mode 100644
index 4134f30..0000000
--- a/components/console/src/core/utils/formatChartDate.spec.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { formatChartDate, getDayFromTimestamp, getMonthAndDay, getTimeFromTimestamp } from './formatChartDate';
-
-const fixedDate = new Date('2023-08-02T23:00:00');
-jest.spyOn(Date, 'now').mockImplementation(() => fixedDate.getTime());
-
-describe('formatChartDate', () => {
- const now = Date.now(); // Current time in milliseconds
- const oneMinuteAgo = now - 60 * 1000;
- const twoDaysAgo = now - 2 * 24 * 3600 * 1000;
- const twoWeeksAgo = now - 14 * 24 * 3600 * 1000;
-
- it('should format date with time (minutes and seconds) if start is more than one minute ago', () => {
- const timestamp = now / 1000; // Current timestamp in seconds
- const start = now / 1000;
- const result = formatChartDate(timestamp, start);
- const expectedFormatted = getTimeFromTimestamp(timestamp);
-
- expect(result).toBe(expectedFormatted);
- });
-
- it('should format date with time (minutes and seconds) if start is more than one minute ago', () => {
- const timestamp = now / 1000; // Current timestamp in seconds
- const start = oneMinuteAgo / 1000;
- const result = formatChartDate(timestamp, start);
- const expectedFormatted = getTimeFromTimestamp(timestamp, true);
-
- expect(result).toBe(expectedFormatted);
- });
-
- it('should format date with day of the week and time if start is more than two days ago', () => {
- const timestamp = twoDaysAgo / 1000;
- const start = twoDaysAgo / 1000;
- const result = formatChartDate(timestamp, start);
- const expectedFormatted = getDayFromTimestamp(timestamp);
-
- expect(result).toBe(expectedFormatted);
- });
-
- it('should format date with month abbreviation and day if start is more than two weeks ago', () => {
- const timestamp = twoWeeksAgo / 1000;
- const start = twoWeeksAgo / 1000;
- const result = formatChartDate(timestamp, start);
- const expectedFormatted = getMonthAndDay(timestamp);
-
- expect(result).toBe(expectedFormatted);
- });
-});
diff --git a/components/console/src/core/utils/formatLatency.spec.ts b/components/console/src/core/utils/formatLatency.spec.ts
deleted file mode 100644
index 6f79a23..0000000
--- a/components/console/src/core/utils/formatLatency.spec.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { formatLatency } from './formatLatency';
-
-describe('formatLatency', () => {
- it('should format latency correctly with default options', () => {
- expect(formatLatency(100)).toEqual('100 µs');
- expect(formatLatency(5000)).toEqual('5 ms');
- expect(formatLatency(1500000)).toEqual('1.5 sec');
- });
-
- it('should format latency with custom start size', () => {
- expect(formatLatency(100, { startSize: 'ms' })).toEqual('100 ms');
- });
-
- it('should format latency with custom decimal places', () => {
- expect(formatLatency(1234567, { decimals: 4 })).toEqual('1.2346 sec');
- expect(formatLatency(1234567, { decimals: 0 })).toEqual('1 sec');
- });
-
- it('should handle time of 0', () => {
- expect(formatLatency(0)).toEqual('0 µs');
- });
-
- it('should handle NaN values', () => {
- expect(formatLatency(NaN)).toEqual('');
- expect(formatLatency(123, { decimals: NaN })).toEqual('123 µs');
- });
-});
diff --git a/components/console/src/core/utils/formatNumber.spec.ts b/components/console/src/core/utils/formatNumber.spec.ts
deleted file mode 100644
index 60f28f6..0000000
--- a/components/console/src/core/utils/formatNumber.spec.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { formatNumber } from './formatNumber';
-
-describe('formatNumber', () => {
- it('should format numbers less than 1000 correctly', () => {
- expect(formatNumber(123)).toBe('123');
- expect(formatNumber(456.78)).toBe('456.78');
- expect(formatNumber(999.999)).toBe('999.999');
- });
-
- it('should format numbers in thousands', () => {
- expect(formatNumber(1000)).toBe('1 K');
- expect(formatNumber(2450)).toBe('2.45 K');
- expect(formatNumber(999950)).toBe('999.95 K');
- });
-
- it('should format numbers in millions', () => {
- expect(formatNumber(1000000)).toBe('1 Mil.');
- expect(formatNumber(24500000)).toBe('24.5 Mil.');
- expect(formatNumber(999999999)).toBe('1000 Mil.');
- });
-
- it('should format numbers in billions', () => {
- expect(formatNumber(1000000000)).toBe('1 Bil.');
- expect(formatNumber(245000000000)).toBe('245 Bil.');
- expect(formatNumber(999999999999)).toBe('1000 Bil.');
- });
-
- it('should format numbers in trillions', () => {
- expect(formatNumber(1000000000000)).toBe('1 Tril.');
- expect(formatNumber(245000000000000)).toBe('245 Tril.');
- expect(formatNumber(999999999999999)).toBe('1000 Tril.');
- });
-
- it('should format numbers with custom decimal precision', () => {
- expect(formatNumber(1234567.89, 0)).toBe('1 Mil.');
- expect(formatNumber(1234567.89, 1)).toBe('1.2 Mil.');
- expect(formatNumber(1234567.89, 3)).toBe('1.235 Mil.');
- expect(formatNumber(1234567.89, 6)).toBe('1.234568 Mil.');
- });
-});
diff --git a/components/console/src/core/utils/formatTimeInterval.spec.ts b/components/console/src/core/utils/formatTimeInterval.spec.ts
deleted file mode 100644
index 0535b7e..0000000
--- a/components/console/src/core/utils/formatTimeInterval.spec.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-// formatTimeInterval.test.ts
-
-import { formatTimeInterval } from './formatTimeInterval';
-
-describe('formatTimeInterval', () => {
- it('should format time interval correctly', () => {
- const startTime = 1627801200000000;
- const endTime = 1627804800000000;
- const expectedOutput = '1 hour';
-
- const result = formatTimeInterval(endTime, startTime);
- expect(result).toEqual(expectedOutput);
- });
-
- it('should handle a time interval with zero duration', () => {
- const startTime = 1627804800000000;
- const endTime = 1627804800000000;
- const expectedOutput = '';
-
- const result = formatTimeInterval(endTime, startTime);
- expect(result).toEqual(expectedOutput);
- });
-
- it('should handle a time interval with less than a second duration', () => {
- const startTime = 1627804800000000;
- const endTime = 1627804800000100;
- const expectedOutput = '';
-
- const result = formatTimeInterval(endTime, startTime);
- expect(result).toEqual(expectedOutput);
- });
-
- it('should handle a time interval with one-second duration', () => {
- const startTime = 1627804800000000;
- const endTime = 1627804801000000;
- const expectedOutput = '1 second';
-
- const result = formatTimeInterval(endTime, startTime);
- expect(result).toEqual(expectedOutput);
- });
-
- it('should handle a time interval with one-minute duration', () => {
- const startTime = 1627804800000000;
- const endTime = 1627804860000000;
- const expectedOutput = '1 minute';
-
- const result = formatTimeInterval(endTime, startTime);
- expect(result).toEqual(expectedOutput);
- });
-
- it('should handle a time interval with one-hour duration', () => {
- const startTime = 1627804800000000;
- const endTime = 1627808400000000;
- const expectedOutput = '1 hour';
-
- const result = formatTimeInterval(endTime, startTime);
- expect(result).toEqual(expectedOutput);
- });
-});
diff --git a/components/console/src/core/utils/formatTimeInterval.ts b/components/console/src/core/utils/formatTimeInterval.ts
deleted file mode 100644
index 69de091..0000000
--- a/components/console/src/core/utils/formatTimeInterval.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { formatDuration, intervalToDuration } from 'date-fns';
-
-/**
- * endTime and startTime are microseconds (because the Apis give us microseconds)
- */
-export function formatTimeInterval(endTime: number, startTime: number) {
- const interval = intervalToDuration({
- start: new Date((startTime as number) / 1000),
- end: new Date((endTime as number) / 1000)
- });
-
- return formatDuration(interval);
-}
diff --git a/components/console/src/core/utils/formatTrace.spec.ts b/components/console/src/core/utils/formatTrace.spec.ts
deleted file mode 100644
index 44e6383..0000000
--- a/components/console/src/core/utils/formatTrace.spec.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { formatTraceBySites } from './formatTrace';
-
-describe('formatTraceBySites', () => {
- it('should return an empty string for empty input', () => {
- const result = formatTraceBySites('');
- expect(result).toBe('');
- });
-
- it('should return the site name for a single site trace', () => {
- const trace = 'private1-skupper-router-bcd8f5469-6z7k6@private1';
- const result = formatTraceBySites(trace);
- expect(result).toBe('private1');
- });
-
- it('should return the concatenated site names for a multi-site trace', () => {
- const trace = 'private1-skupper-router-bcd8f5469-6z7k6@private1|public1-skupper-router-7b45b6b8f4-52tjs@public1';
- const result = formatTraceBySites(trace);
- expect(result).toBe('private1 -> public1');
- });
-
- it('should return the site name for a single site trace with extra pipe', () => {
- const trace = 'private1-skupper-router-bcd8f5469-6z7k6@private1|';
- const result = formatTraceBySites(trace);
- expect(result).toBe('private1');
- });
-
- it('should return an empty string for a trace without site information', () => {
- const trace = 'private1-skupper-router-bcd8f5469-6z7k6';
- const result = formatTraceBySites(trace);
- expect(result).toBe('');
- });
-});
diff --git a/components/console/src/core/utils/getCurrentAndPastTimestamps.spec.ts b/components/console/src/core/utils/getCurrentAndPastTimestamps.spec.ts
deleted file mode 100644
index 0217b2b..0000000
--- a/components/console/src/core/utils/getCurrentAndPastTimestamps.spec.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { getCurrentAndPastTimestamps } from './getCurrentAndPastTimestamps';
-
-describe('getCurrentAndPastTimestamps', () => {
- it('should returns an object with current and past Unix timestamps', () => {
- const intervalInSeconds = 3600; // 1 hour
- const result = getCurrentAndPastTimestamps(intervalInSeconds);
-
- expect(result.end - result.start).toBeCloseTo(intervalInSeconds);
- });
-
- it('should handles small intervals', () => {
- const intervalInSeconds = 0.001; // 1 millisecond
- const result = getCurrentAndPastTimestamps(intervalInSeconds);
-
- expect(result.end - result.start).toBeLessThan(1); // difference should be very small
- });
-
- it('should handles large intervals', () => {
- const intervalInSeconds = 86400 * 30; // 30 days
- const maxNumber = Number.MAX_SAFE_INTEGER; // maximum value of the Number type
- const result = getCurrentAndPastTimestamps(intervalInSeconds);
-
- expect(result.end - result.start).toBe(intervalInSeconds % maxNumber); // difference should be equal to the interval modulo the maximum value of the Number type
- });
-
- it('should handles negative intervals', () => {
- const intervalInSeconds = -3600; // negative 1 hour
- const result = getCurrentAndPastTimestamps(intervalInSeconds);
-
- expect(result.end - result.start).toBe(intervalInSeconds); // difference should be negative (because interval is negative) and equal to the interval
- });
-});
diff --git a/components/console/src/core/utils/getValueFromNestedProperty.spec.ts b/components/console/src/core/utils/getValueFromNestedProperty.spec.ts
deleted file mode 100644
index 1a6e696..0000000
--- a/components/console/src/core/utils/getValueFromNestedProperty.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { expect } from '@jest/globals';
-
-import { getValueFromNestedProperty } from './getValueFromNestedProperty';
-
-type TestObject = {
- a: {
- b: {
- c: number;
- };
- };
- d?: {
- e: string;
- };
- f: string[];
-};
-
-describe('getNestedProperty', () => {
- const testObj = {
- a: {
- b: {
- c: 42
- }
- },
- d: {
- e: 'hello'
- },
- f: ['foo', 'bar', 'baz']
- };
-
- it('should return the nested property if it exists', () => {
- expect(getValueFromNestedProperty(testObj, ['a', 'b', 'c'] as (keyof TestObject)[])).toEqual(42);
- expect(getValueFromNestedProperty(testObj, ['d', 'e'] as (keyof TestObject)[])).toEqual('hello');
- expect(getValueFromNestedProperty(testObj, ['f', '0'] as (keyof TestObject)[])).toEqual('foo');
- });
-
- it('should return undefined if the nested property does not exist', () => {
- expect(getValueFromNestedProperty(testObj, ['a', 'b', 'd'] as (keyof TestObject)[])).toBeUndefined();
- expect(getValueFromNestedProperty(testObj, ['d', 'f'] as (keyof TestObject)[])).toBeUndefined();
- expect(getValueFromNestedProperty(testObj, ['f', '3'] as (keyof TestObject)[])).toBeUndefined();
- });
-
- it('should handle empty keys array', () => {
- expect(getValueFromNestedProperty(testObj, [])).toBeUndefined();
- });
-
- it('should handle non-existent keys', () => {
- const keys = ['g'];
- expect(getValueFromNestedProperty(testObj, keys as (keyof TestObject)[])).toBeUndefined();
- });
-});
diff --git a/components/console/src/core/utils/isDarkTheme.spec.ts b/components/console/src/core/utils/isDarkTheme.spec.ts
deleted file mode 100644
index c16e87f..0000000
--- a/components/console/src/core/utils/isDarkTheme.spec.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { DARK_THEME_CLASS } from '@config/config';
-
-import {
- THEME_PREFERENCE_CACHE_KEY,
- ThemePreference,
- getThemePreference,
- reflectThemePreference,
- removeThemePreference,
- setThemePreference
-} from './isDarkTheme';
-
-const localStorageMock = {
- getItem: jest.fn(),
- setItem: jest.fn(),
- removeItem: jest.fn()
-};
-
-Object.defineProperty(window, 'localStorage', { value: localStorageMock });
-
-Object.defineProperty(document, 'documentElement', {
- writable: true,
- value: {
- classList: {
- toggle: jest.fn(),
- remove: jest.fn()
- },
- removeAttribute: jest.fn()
- }
-});
-
-describe('isDarkTheme', () => {
- afterEach(() => {
- (localStorageMock.getItem as jest.Mock).mockClear();
- (localStorageMock.setItem as jest.Mock).mockClear();
- (localStorageMock.removeItem as jest.Mock).mockClear();
- (document.documentElement.classList.toggle as jest.Mock).mockClear();
- (document.documentElement.removeAttribute as jest.Mock).mockClear();
- });
-
- describe('setThemePreference', () => {
- it('should set the theme preference to dark', () => {
- setThemePreference(ThemePreference.Dark);
- expect(localStorageMock.setItem).toHaveBeenCalledWith(THEME_PREFERENCE_CACHE_KEY, ThemePreference.Dark);
- expect(document.documentElement.classList.toggle).toHaveBeenCalledWith(DARK_THEME_CLASS);
- });
- });
-
- describe('removeThemePreference', () => {
- it('should remove the theme preference', () => {
- removeThemePreference();
- expect(localStorageMock.removeItem).toHaveBeenCalledWith(THEME_PREFERENCE_CACHE_KEY);
- });
- });
-
- describe('getThemePreference', () => {
- it('should get the theme preference', () => {
- localStorageMock.getItem.mockReturnValue(ThemePreference.Dark);
- const themePreference = getThemePreference();
- expect(localStorageMock.getItem).toHaveBeenCalledWith(THEME_PREFERENCE_CACHE_KEY);
- expect(themePreference).toBe(ThemePreference.Dark);
- });
-
- it('should get a null theme preference', () => {
- localStorageMock.getItem.mockReturnValue(null);
- const themePreference = getThemePreference();
- expect(localStorageMock.getItem).toHaveBeenCalledWith(THEME_PREFERENCE_CACHE_KEY);
- expect(themePreference).toBe(null);
- });
- });
-
- describe('reflectThemePreference', () => {
- it('should reflect the dark theme preference', () => {
- reflectThemePreference(ThemePreference.Dark);
- expect(document.documentElement.classList.toggle).toHaveBeenCalledWith(DARK_THEME_CLASS);
- });
-
- it('should reflect no theme preference', () => {
- reflectThemePreference(null);
- expect(document.documentElement.removeAttribute).toHaveBeenCalledWith('class');
- });
- });
-});
diff --git a/components/console/src/core/utils/isDarkTheme.ts b/components/console/src/core/utils/isDarkTheme.ts
deleted file mode 100644
index df53671..0000000
--- a/components/console/src/core/utils/isDarkTheme.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { DARK_THEME_CLASS } from '@config/config';
-
-export enum ThemePreference {
- Dark = DARK_THEME_CLASS
-}
-export const THEME_PREFERENCE_CACHE_KEY = 'theme-preference';
-
-export function setThemePreference(theme: string) {
- localStorage.setItem(THEME_PREFERENCE_CACHE_KEY, theme);
- reflectThemePreference(theme);
-}
-
-export function removeThemePreference() {
- localStorage.removeItem(THEME_PREFERENCE_CACHE_KEY);
- reflectThemePreference(null);
-}
-
-export function getThemePreference() {
- return localStorage.getItem(THEME_PREFERENCE_CACHE_KEY) as ThemePreference.Dark | null;
-}
-
-export function reflectThemePreference(themeClassName: string | null) {
- const htmlElement = document.documentElement;
- themeClassName ? htmlElement.classList.toggle(themeClassName) : htmlElement.removeAttribute('class');
-}
diff --git a/components/console/src/core/utils/persistData.spec.ts b/components/console/src/core/utils/persistData.spec.ts
deleted file mode 100644
index 68816d1..0000000
--- a/components/console/src/core/utils/persistData.spec.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-// sessionStorageUtils.test.ts
-
-import { getDataFromSession, storeDataToSession } from './persistData';
-
-describe('sessionStorageUtils', () => {
- beforeEach(() => {
- sessionStorage.clear();
- });
-
- it('should store data to sessionStorage and retrieve it correctly', () => {
- const key = 'testData';
- const testData = { name: 'John', age: 30 };
- storeDataToSession(key, testData);
-
- const retrievedData = getDataFromSession