diff --git a/.github/actions/rebase/action.yml b/.github/actions/rebase/action.yml
new file mode 100644
index 0000000000..4e6d0cec0b
--- /dev/null
+++ b/.github/actions/rebase/action.yml
@@ -0,0 +1,36 @@
+name: 'Rebase'
+description: 'Action for rebasing to the main branch'
+
+runs:
+ using: 'composite'
+ steps:
+ # Setup identity to avoid errors when git is trying to rebase without identity set
+ - name: Setup git identity
+ run: |
+ git config --global user.email "rebase@action.com"
+ git config --global user.name "Rebase Action"
+ shell: bash
+
+ # Update origin/main branch locally to cover case when repo is not fully cloned
+ # for example when using `actions/checkout@v4` action with default `fetch-depth` value (1)
+ # read more: https://github.com/actions/checkout?tab=readme-ov-file#usage
+ - name: Update origin/main
+ run: |
+ git fetch origin main
+ git checkout origin/main
+ git checkout -B main
+ git pull --unshallow origin main
+
+ # go back to the HEAD commit
+ git switch --detach ${{ github.sha }}
+ shell: bash
+
+ - name: Rebase to main
+ run: |
+ git rebase main
+ shell: bash
+
+ - name: Print branch log
+ run: |
+ git log origin/main~1..
+ shell: bash
diff --git a/.github/workflows/accessibility-tests.yml b/.github/workflows/accessibility-tests.yml
index b0ecc60536..78b66b255f 100644
--- a/.github/workflows/accessibility-tests.yml
+++ b/.github/workflows/accessibility-tests.yml
@@ -3,6 +3,12 @@ name: Accessibility Tests
on:
schedule:
- cron: "0 6 * * 1"
+ pull_request_target:
+ types: [opened, edited, synchronize, reopened, ready_for_review]
+ paths:
+ - ".github/workflows/accessibility-tests.yml"
+ - "tests/integration/tests/accessibility/**"
+ - "tests/integration/support/**"
jobs:
run-accessibility-tests:
diff --git a/.github/workflows/busola-backend-build.yml b/.github/workflows/busola-backend-build.yml
index 35186b5bae..f7f0619bba 100644
--- a/.github/workflows/busola-backend-build.yml
+++ b/.github/workflows/busola-backend-build.yml
@@ -22,6 +22,7 @@ permissions:
jobs:
build-backend-image:
uses: kyma-project/test-infra/.github/workflows/image-builder.yml@main # Usage: kyma-project/test-infra/.github/workflows/image-builder.yml@main
+ if: github.event.pull_request.draft == false
with:
name: busola-backend
dockerfile: Dockerfile
diff --git a/.github/workflows/busola-build.yml b/.github/workflows/busola-build.yml
index 4e490b0b11..9400d3f090 100644
--- a/.github/workflows/busola-build.yml
+++ b/.github/workflows/busola-build.yml
@@ -34,6 +34,7 @@ permissions:
jobs:
build-busola-image:
uses: kyma-project/test-infra/.github/workflows/image-builder.yml@main # Usage: kyma-project/test-infra/.github/workflows/image-builder.yml@main
+ if: github.event.pull_request.draft == false
with:
name: busola
dockerfile: Dockerfile
diff --git a/.github/workflows/busola-web-build.yml b/.github/workflows/busola-web-build.yml
index c3f60c28f3..50bf2ca2c6 100644
--- a/.github/workflows/busola-web-build.yml
+++ b/.github/workflows/busola-web-build.yml
@@ -33,6 +33,7 @@ permissions:
jobs:
build-web-image:
uses: kyma-project/test-infra/.github/workflows/image-builder.yml@main # Usage: kyma-project/test-infra/.github/workflows/image-builder.yml@main
+ if: github.event.pull_request.draft == false
with:
name: busola-web
dockerfile: Dockerfile.web
diff --git a/.github/workflows/lint-check-pr.yml b/.github/workflows/lint-check-pr.yml
index 47e5f6da83..6bec9a1185 100644
--- a/.github/workflows/lint-check-pr.yml
+++ b/.github/workflows/lint-check-pr.yml
@@ -7,6 +7,7 @@ on:
jobs:
run-lint-check:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: actions/checkout@v4
with:
diff --git a/.github/workflows/pull-integration-cluster-k3d.yml b/.github/workflows/pull-integration-cluster-k3d.yml
index 54535e08c4..5b0180e2fa 100644
--- a/.github/workflows/pull-integration-cluster-k3d.yml
+++ b/.github/workflows/pull-integration-cluster-k3d.yml
@@ -9,17 +9,18 @@ on:
- "tests/**"
- "nginx/**"
- "src/**"
+ - "backend/**"
jobs:
run-cluster-test:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- name: Create Single Cluster
uses: AbsaOSS/k3d-action@4e8b3239042be1dc0aed6c5eb80c13b18200fc79 #v2.4.0
with:
diff --git a/.github/workflows/pull-integration-namespace-k3d.yml b/.github/workflows/pull-integration-namespace-k3d.yml
index 7d944a5fbe..1359c8c519 100644
--- a/.github/workflows/pull-integration-namespace-k3d.yml
+++ b/.github/workflows/pull-integration-namespace-k3d.yml
@@ -9,17 +9,18 @@ on:
- "tests/**"
- "nginx/**"
- "src/**"
+ - "backend/**"
jobs:
run-namespace-test:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- name: Create Single Cluster
uses: AbsaOSS/k3d-action@4e8b3239042be1dc0aed6c5eb80c13b18200fc79 #v2.4.0
with:
diff --git a/.github/workflows/pull-kyma-integration-tests.yml b/.github/workflows/pull-kyma-integration-tests.yml
index 71704fc859..60fc82f6bc 100644
--- a/.github/workflows/pull-kyma-integration-tests.yml
+++ b/.github/workflows/pull-kyma-integration-tests.yml
@@ -9,18 +9,19 @@ on:
- "tests/integration/**"
- "nginx/**"
- "src/**"
+ - "backend/**"
- "kyma/**"
- "Dockerfile*"
jobs:
run-integration-test:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- name: Install k3d
env:
K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh
diff --git a/.github/workflows/pull-lighthouse.yml b/.github/workflows/pull-lighthouse.yml
index a68193dc12..d1d71ccf07 100644
--- a/.github/workflows/pull-lighthouse.yml
+++ b/.github/workflows/pull-lighthouse.yml
@@ -13,13 +13,13 @@ on:
jobs:
run-lighthouse-test:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- name: Create Single Cluster
uses: AbsaOSS/k3d-action@4e8b3239042be1dc0aed6c5eb80c13b18200fc79 #v2.4.0
with:
diff --git a/.github/workflows/pull-smoke-test-prod.yml b/.github/workflows/pull-smoke-test-prod.yml
index 64020107a7..9ba14d5f26 100644
--- a/.github/workflows/pull-smoke-test-prod.yml
+++ b/.github/workflows/pull-smoke-test-prod.yml
@@ -15,13 +15,13 @@ on:
jobs:
run-smoke-test-prod:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- name: Install k3d
env:
K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh
diff --git a/.github/workflows/pull-smoke-test-stage.yml b/.github/workflows/pull-smoke-test-stage.yml
index 5198cdebbe..0d1b6d4015 100644
--- a/.github/workflows/pull-smoke-test-stage.yml
+++ b/.github/workflows/pull-smoke-test-stage.yml
@@ -15,13 +15,13 @@ on:
jobs:
run-smoke-test-stage:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- name: Install k3d
env:
K3D_URL: https://raw.githubusercontent.com/k3d-io/k3d/main/install.sh
diff --git a/.github/workflows/pull-unit-tests.yml b/.github/workflows/pull-unit-tests.yml
index 4038d1ae7d..c233864f01 100644
--- a/.github/workflows/pull-unit-tests.yml
+++ b/.github/workflows/pull-unit-tests.yml
@@ -12,13 +12,13 @@ on:
jobs:
run-unit-test:
runs-on: ubuntu-latest
+ if: github.event.pull_request.draft == false
steps:
- uses: gardenlinux/workflow-telemetry-action@v2
with:
comment_on_pr: false
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
+ - uses: ./.github/actions/rebase
- uses: actions/setup-node@v4
with:
node-version: 20
diff --git a/backend/common.js b/backend/common.js
index 4a562926e1..0129f0d01b 100644
--- a/backend/common.js
+++ b/backend/common.js
@@ -142,10 +142,12 @@ function extractHeadersData(req) {
const clientCAHeader = 'x-client-certificate-data';
const clientKeyDataHeader = 'x-client-key-data';
const authorizationHeader = 'x-k8s-authorization';
-
- const targetApiServer = handleDockerDesktopSubsitution(
- new URL(req.headers[urlHeader]),
- );
+ let targetApiServer;
+ if (req.headers[urlHeader]) {
+ targetApiServer = handleDockerDesktopSubsitution(
+ new URL(req.headers[urlHeader]),
+ );
+ }
const ca = decodeHeaderToBuffer(req.headers[caHeader]) || certs;
const cert = decodeHeaderToBuffer(req.headers[clientCAHeader]);
const key = decodeHeaderToBuffer(req.headers[clientKeyDataHeader]);
diff --git a/backend/index.js b/backend/index.js
index d98c47589a..8c2b4e3e2c 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -1,4 +1,5 @@
import { makeHandleRequest, serveStaticApp, serveMonaco } from './common';
+import { proxyHandler } from './proxy.js';
import { handleTracking } from './tracking.js';
import jsyaml from 'js-yaml';
//import { requestLogger } from './utils/other'; //uncomment this to log the outgoing traffic
@@ -53,6 +54,8 @@ if (process.env.NODE_ENV === 'development') {
app.use(cors({ origin: '*' }));
}
+app.use('/proxy', proxyHandler);
+
let server = null;
if (
@@ -81,13 +84,15 @@ const isDocker = process.env.IS_DOCKER === 'true';
const handleRequest = makeHandleRequest();
if (isDocker) {
+ // Running in dev mode
// yup, order matters here
serveMonaco(app);
app.use('/backend', handleRequest);
serveStaticApp(app, '/', '/core-ui');
} else {
+ // Running in prod mode
handleTracking(app);
- app.use(handleRequest);
+ app.use('/backend', handleRequest);
}
process.on('SIGINT', function() {
diff --git a/backend/package-lock.json b/backend/package-lock.json
index a0477a86ad..d8cfcc0359 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -12,7 +12,7 @@
"@babel/runtime": "^7.13.10",
"compression": "^1.7.4",
"cors": "^2.8.5",
- "express": "^4.21.0",
+ "express": "^4.21.2",
"https": "^1.0.0",
"jose": "^5.2.4",
"js-yaml": "^4.1.0",
@@ -4481,9 +4481,9 @@
}
},
"node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
"engines": {
"node": ">= 0.6"
}
@@ -5174,16 +5174,16 @@
}
},
"node_modules/express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -5197,7 +5197,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -5212,6 +5212,10 @@
},
"engines": {
"node": ">= 0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/express/node_modules/safe-buffer": {
@@ -9233,9 +9237,9 @@
"dev": true
},
"node_modules/path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"node_modules/picocolors": {
"version": "1.1.0",
@@ -14511,9 +14515,9 @@
}
},
"cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="
},
"cookie-signature": {
"version": "1.0.6",
@@ -15019,16 +15023,16 @@
}
},
"express": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
- "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "version": "4.21.2",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.3",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
- "cookie": "0.6.0",
+ "cookie": "0.7.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
@@ -15042,7 +15046,7 @@
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
- "path-to-regexp": "0.1.10",
+ "path-to-regexp": "0.1.12",
"proxy-addr": "~2.0.7",
"qs": "6.13.0",
"range-parser": "~1.2.1",
@@ -18048,9 +18052,9 @@
"dev": true
},
"path-to-regexp": {
- "version": "0.1.10",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
- "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
},
"picocolors": {
"version": "1.1.0",
diff --git a/backend/package.json b/backend/package.json
index 42fb1cc89c..a56ca31360 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -16,7 +16,7 @@
"@babel/runtime": "^7.13.10",
"compression": "^1.7.4",
"cors": "^2.8.5",
- "express": "^4.21.0",
+ "express": "^4.21.2",
"https": "^1.0.0",
"jose": "^5.2.4",
"js-yaml": "^4.1.0",
diff --git a/backend/proxy.js b/backend/proxy.js
new file mode 100644
index 0000000000..ad4a35c94b
--- /dev/null
+++ b/backend/proxy.js
@@ -0,0 +1,45 @@
+import { request as httpsRequest } from 'https';
+import { request as httpRequest } from 'http';
+import { URL } from 'url';
+
+async function proxyHandler(req, res) {
+ const targetUrl = req.query.url;
+ if (!targetUrl) {
+ return res.status(400).send('Target URL is required as a query parameter');
+ }
+
+ try {
+ const parsedUrl = new URL(targetUrl);
+ const isHttps = parsedUrl.protocol === 'https:';
+ const libRequest = isHttps ? httpsRequest : httpRequest;
+
+ const options = {
+ hostname: parsedUrl.hostname,
+ port: parsedUrl.port || (isHttps ? 443 : 80),
+ path: parsedUrl.pathname + parsedUrl.search,
+ method: req.method,
+ headers: { ...req.headers, host: parsedUrl.host },
+ };
+
+ const proxyReq = libRequest(options, proxyRes => {
+ // Forward status and headers from the target response
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
+ // Pipe the response data from the target back to the client
+ proxyRes.pipe(res);
+ });
+
+ proxyReq.on('error', () => {
+ res.status(500).send('An error occurred while making the proxy request.');
+ });
+
+ if (Buffer.isBuffer(req.body)) {
+ proxyReq.end(req.body); // If the body is already buffered, use it directly.
+ } else {
+ req.pipe(proxyReq); // Otherwise, pipe the request for streamed or chunked data.
+ }
+ } catch (error) {
+ res.status(500).send('An error occurred while processing the request.');
+ }
+}
+
+export { proxyHandler };
diff --git a/docs/contributor/testing-strategy.md b/docs/contributor/testing-strategy.md
index 6ee09aa3d3..113ef87f1b 100644
--- a/docs/contributor/testing-strategy.md
+++ b/docs/contributor/testing-strategy.md
@@ -2,19 +2,27 @@
Each pull request (PR) to the repository triggers CI/CD jobs that verify the Busola configuration, build and run integration tests.
-- `pre-busola-web-deployment-check` - Checks if the Busola web image in deployment is bumped correctly.
-- `pre-busola-backend-deployment-check` - Checks if the Busola backend image in deployment is bumped correctly. The check runs only when PR changes affect the backend.
-- `pull-busola-web-build` - Unit tests of Busola checking the ESLint code quality, and building the web Docker image.
-- `pull-busola-local-build` - Unit tests of Busola checking the ESLint code quality and building the web and backend Docker image.
-- `pull-busola-backend-build` - Builds the backend Docker image. The job runs only when changes affect the backend.
-- `pull-busola-integration-cluster-k3d` - Performs integration testing for Busola related to cluster-level functionalities using a k3d cluster.
-- `pull-busola-integration-namespace-k3d` - Performs integration testing for Busola related to namespace-level functionalities using a k3d cluster.
-- `pull-busola-lighthouse` - Performs performance testing for Busola - threshold for accessibility: 80, best-practices: 100.
+- `Busola Web Build / build-web-image / Build image` - Checks the ESLint code quality and builds the web Docker image.
+- `Busola Build / build-busola-image / Build image` - Checks the ESLint code quality and builds the web and backend Docker image.
+- `Busola Backend Build / build-backend-image / Build image` - Builds the backend Docker image. The job runs only when changes affect the backend.
+- `PR Integration Cluster Tests / run-cluster-test` - Performs integration testing for Busola related to cluster-level functionalities using a k3d cluster.
+- `PR Integration Namespace Tests / run-namespace-test` - Performs integration testing for Busola related to namespace-level functionalities using a k3d cluster.
+- `PR Kyma Dashboard Integration Tests Dev / run-integration-test` - Performs integration testing for Busola with DEV environement and configuration related to the Kyma functionalities using a k3d cluster with installed Kyma.
+- `PR Kyma Dashboard Smoke Tests Stage / run-smoke-test-stage` - Performs smoke testing for Busola with STAGE environement and configuration related to the Kyma functionalities using a k3d cluster with installed Kyma.
+- `PR Kyma Dashboard Smoke Tests Prod / run-smoke-test-prod` - Performs smoke testing for Busola with PROD environement and configuration related to the Kyma functionalities using a k3d cluster with installed Kyma.
+- `PR Lighthouse Test / run-lighthouse-test` - Performs performance testing for Busola - threshold for accessibility: 80, best-practices: 100.
+- `PR Lint Check / run-lint-check` - Performing ESlint and Prettier code quality.
+- `PR Unit Tests / run-unit-test` - Performs unit tests of the Busola.
- `Lint Markdown Links PR / markdown-link-check` - Checks links in documentation.
- `CodeQL / Analyze (javascript)` - Code quality static code check.
After the pull request is merged, the following CI/CD jobs are executed:
-- `post-busola-web-build` - Performs Busola unit tests and builds the web Docker image.
-- `post-busola-local-build` - Performs Busola unit tests and builds the web and backend Docker image.
-- `post-busola-backend-build` - Builds the backend Docker image.
+- `Busola Web Build / build-web-image / Build image` - Performs Busola unit tests and builds the web Docker image.
+- `Busola Build / build-busola-image / Build image` - Performs Busola unit tests and builds the web and backend Docker image.
+- `Busola Backend Build / build-backend-image / Build image` - Builds the backend Docker image.
+- `CodeQL / Analyze (javascript)` - Code quality static code check.
+
+Following CI/CD jobs are executed once a week:
+
+- `Accessibility Tests - run-accessibility-tests` - Performs accessibility tests of the Busola using k3d cluster with Kyma installed.
diff --git a/docs/extensibility/README.md b/docs/extensibility/README.md
index c3dcc60050..011cea37a2 100644
--- a/docs/extensibility/README.md
+++ b/docs/extensibility/README.md
@@ -4,6 +4,8 @@
With Busola's extensibility feature, you can create a dedicated user interface (UI) page for your CustomResourceDefinition (CRD). It enables you to add navigation nodes, on cluster or namespace level, and to configure your [UI display](./30-details-summary.md), for example, a resource list page, and details pages. You can also [create and edit forms](./40-form-fields.md). To create a UI component, you need a ConfigMap.
+You can also leverage Busola's [custom extension feature](./custom-extensions.md) to design entirely custom user interfaces tailored to your specific needs.
+
## Create a ConfigMap for Your UI
To create a ConfigMap with your CRD's UI configuration, you can either use the Extensions feature or do it manually.
diff --git a/docs/extensibility/custom-extensions.md b/docs/extensibility/custom-extensions.md
new file mode 100644
index 0000000000..ef645b8a57
--- /dev/null
+++ b/docs/extensibility/custom-extensions.md
@@ -0,0 +1,24 @@
+# Custom Extensions
+
+Busola's custom extension feature allows you to design fully custom user interfaces beyond the built-in extensibility functionality. This feature is ideal for creating unique and specialized displays not covered by the built-in components.
+
+## Getting Started
+
+To enable the custom extension feature, you must set the corresponding feature flag in your Busola config, which is disabled by default.
+
+```yaml
+EXTENSIBILITY_CUSTOM_COMPONENTS:
+ isEnabled: true
+```
+
+## Creating Custom Extensions
+
+Creating a custom extension is as straightforward as setting up a ConfigMap with the following sections:
+
+- `data.general`: Contains configuration details
+- `data.customHtml`: Defines static HTML content
+- `data.customScript`: Adds dynamic behavior to your extension.
+
+Once your ConfigMap is ready, add it to your cluster, and Busola will load and display your custom UI.
+
+See this [example](./../../examples/custom-extension/README.md), to learn more.
diff --git a/docs/features.md b/docs/features.md
index 91283979ae..6878f63f0b 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -56,6 +56,15 @@ EXTENSIBILITY:
isEnabled: true
```
+- **EXTENSIBILITY_CUSTOM_COMPONENTS** - is used to indicate whether entirely custom extensions can be added to Busola. See [this example](../examples/custom-extension/README.md).
+
+Default settings:
+
+```yaml
+EXTENSIBILITY_CUSTOM_COMPONENTS:
+ isEnabled: false
+```
+
- **EXTERNAL_NODES** - a list of links to external websites. `category`: a category name, `icon`: an optional icon, `scope`: either `namespace` or `cluster` (defaults to `cluster`), `children`: a list of pairs (label and link).
Default settings:
diff --git a/examples/custom-extension/README.md b/examples/custom-extension/README.md
new file mode 100644
index 0000000000..9f3e7c7c35
--- /dev/null
+++ b/examples/custom-extension/README.md
@@ -0,0 +1,44 @@
+# Set Up Your Custom Busola Extension
+
+This example contains a basic custom extension that queries all deployments of a selected namespace of your cluster. Additionally, it retrieves the current weather data for Munich, Germany, from an external weather API.
+
+To set up and deploy your own custom Busola extension, follow these steps.
+
+1. Adjust the static HTML content.
+
+Edit the `ui.html` file to define the static HTML content for your custom extension.
+
+2. Configure dynamic components.
+
+Set up dynamic or behavioral components by modifying the custom element defined in the `script.js` file.
+
+- **Accessing Kubernetes resources**: Use the `fetchWrapper` function to interact with cluster resources through the Kubernetes API.
+
+- **Making external API requests**: Use the `proxyFetch` function to handle requests to external APIs that are subject to CORS regulations.
+
+3. Define extension metadata
+
+Update the `general.yaml` file to define metadata for your custom extension.
+
+> [! WARNING]
+> Ensure that the `general.customElement` property matches the name of the custom element defined in `script.js`. The script is loaded only once, and this property is used to determine whether the custom element is already defined.
+
+4. Deploy your extension
+
+Before running the deployment command, ensure that your `kubeconfig` is correctly exported and points to the desired cluster. You can check the current context by running:
+
+```bash
+kubectl config current-context
+```
+
+Run `./deploy-custom-extension.sh` to create a ConfigMap and deploy it to your cluster
+
+Alternatively, you can use the following command:
+
+```bash
+kubectl kustomize . | kubectl apply -n kyma-system -f -
+```
+
+### 5. Test your changes locally
+
+Run `npm start` to start the development server.
diff --git a/examples/custom-extension/deploy-custom-extension.sh b/examples/custom-extension/deploy-custom-extension.sh
new file mode 100755
index 0000000000..8861b96b68
--- /dev/null
+++ b/examples/custom-extension/deploy-custom-extension.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+kubectl kustomize . > ./custom-ui.yaml
+kubectl apply -f ./custom-ui.yaml -n kyma-system
diff --git a/examples/custom-extension/general.yaml b/examples/custom-extension/general.yaml
new file mode 100644
index 0000000000..fce5e5daeb
--- /dev/null
+++ b/examples/custom-extension/general.yaml
@@ -0,0 +1,10 @@
+resource:
+ kind: Secret
+ version: v1
+urlPath: custom-busola-extension-example
+category: Kyma
+name: Custom busola extension example
+scope: cluster
+customElement: my-custom-element
+description: >-
+ Custom busola extension example
diff --git a/examples/custom-extension/kustomization.yaml b/examples/custom-extension/kustomization.yaml
new file mode 100644
index 0000000000..331b64b818
--- /dev/null
+++ b/examples/custom-extension/kustomization.yaml
@@ -0,0 +1,11 @@
+configMapGenerator:
+ - name: custom-ui
+ files:
+ - customHtml=ui.html
+ - customScript=script.js
+ - general=general.yaml
+ options:
+ disableNameSuffixHash: true
+ labels:
+ busola.io/extension: 'resource'
+ busola.io/extension-version: '0.5'
diff --git a/examples/custom-extension/script.js b/examples/custom-extension/script.js
new file mode 100644
index 0000000000..bc3c6bd601
--- /dev/null
+++ b/examples/custom-extension/script.js
@@ -0,0 +1,220 @@
+function fetchWrapper(url, options = {}) {
+ if (window.extensionProps?.kymaFetchFn) {
+ return window.extensionProps.kymaFetchFn(url, options);
+ }
+ return fetch(url, options);
+}
+
+function proxyFetch(url, options = {}) {
+ const baseUrl = window.location.hostname.startsWith('localhost')
+ ? 'http://localhost:3001/proxy'
+ : '/proxy';
+ const encodedUrl = encodeURIComponent(url);
+ const proxyUrl = `${baseUrl}?url=${encodedUrl}`;
+ return fetch(proxyUrl, options);
+}
+
+class MyCustomElement extends HTMLElement {
+ connectedCallback() {
+ const shadow = this.attachShadow({ mode: 'open' });
+
+ // Add basic styling
+ const style = document.createElement('style');
+ style.textContent = `
+ .container {
+ padding: 1rem;lu
+ }
+ .deployments-list {
+ margin-top: 1rem;
+ }
+ .deployment-item {
+ padding: 0.5rem;
+ margin: 0.5rem 0;
+ background: #f5f5f5;
+ border-radius: 4px;
+ }
+ .weather-container {
+ margin-top: 2rem;
+ padding: 1rem;
+ background: #e0f7fa;
+ border-radius: 8px;
+ }
+ .weather-item {
+ padding: 0.5rem 0;
+ margin: 0.5rem 0;
+ font-size: 1rem;
+ }
+ `;
+ shadow.appendChild(style);
+
+ // Create container
+ const container = document.createElement('div');
+ container.className = 'container';
+
+ // Create namespace dropdown
+ const namespaceSelect = document.createElement('ui5-select');
+ namespaceSelect.id = 'namespaceSelect';
+ container.appendChild(namespaceSelect);
+
+ // Create deployments container
+ const deploymentsList = document.createElement('div');
+ deploymentsList.className = 'deployments-list';
+ container.appendChild(deploymentsList);
+
+ // Create weather container
+ const weatherContainer = document.createElement('div');
+ weatherContainer.className = 'weather-container';
+ weatherContainer.id = 'weatherContainer';
+ container.appendChild(weatherContainer);
+
+ shadow.appendChild(container);
+
+ // Load initial data
+ this.loadData(namespaceSelect, deploymentsList);
+
+ // Add change listener
+ namespaceSelect.addEventListener('change', () => {
+ this.updateDeploymentsList(namespaceSelect.value, deploymentsList);
+ });
+
+ // Fetch and update weather data
+ fetchMunichWeatherData().then(weatherData => {
+ this.updateWeatherUI(weatherData, weatherContainer);
+ });
+ }
+
+ async loadData(namespaceSelect, deploymentsList) {
+ try {
+ // Get namespaces
+ const namespaces = await getNamespaces();
+
+ // Populate namespace dropdown
+ namespaces.forEach(namespace => {
+ const option = document.createElement('ui5-option');
+ option.value = namespace.metadata.name;
+ option.innerHTML = namespace.metadata.name;
+ namespaceSelect.appendChild(option);
+ });
+
+ // Load deployments for first namespace
+ if (namespaces.length > 0) {
+ this.updateDeploymentsList(
+ namespaces[0].metadata.name,
+ deploymentsList,
+ );
+ }
+ } catch (error) {
+ console.error('Failed to load data:', error);
+ }
+ }
+
+ async updateDeploymentsList(namespace, deploymentsList) {
+ try {
+ const deployments = await getDeployments(namespace);
+
+ // Clear current list
+ deploymentsList.innerHTML = '';
+
+ // Add deployment to list
+ deployments.forEach(deployment => {
+ const deploymentItem = document.createElement('div');
+ deploymentItem.className = 'deployment-item';
+ deploymentItem.innerHTML = `
+
Name: ${deployment.metadata.name}
+ `;
+ deploymentsList.appendChild(deploymentItem);
+ });
+
+ // Show message if no deployments
+ if (deployments.length === 0) {
+ const messageStrip = document.createElement('ui5-message-strip');
+ messageStrip.innerHTML = 'No deployments found in this namespace';
+
+ deploymentsList.innerHTML = messageStrip.outerHTML;
+ }
+ } catch (error) {
+ console.error('Failed to update deployments:', error);
+ deploymentsList.innerHTML = 'Error loading deployments
';
+ }
+ }
+
+ async updateWeatherUI(weatherData, weatherContainer) {
+ const { temperature, condition } = weatherData;
+ weatherContainer.innerHTML = `
+ Current weather in Munich:
+ Temperature: ${temperature}°C
+ Condition: ${condition}
+ `;
+ }
+}
+
+async function getNamespaces() {
+ const resp = await fetchWrapper('/api/v1/namespaces');
+ const data = await resp.json();
+ return data.items;
+}
+
+async function getDeployments(namespace) {
+ const resp = await fetchWrapper(
+ `/apis/apps/v1/namespaces/${namespace}/deployments`,
+ );
+ const data = await resp.json();
+ return data.items;
+}
+
+async function fetchMunichWeatherData() {
+ const latitude = 48.1351;
+ const longitude = 11.582;
+ const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}¤t_weather=true`;
+
+ const response = await proxyFetch(url);
+ if (!response.ok) {
+ console.error(`Error fetching weather: ${response.status}`);
+ return;
+ }
+ const data = await response.json();
+
+ const currentWeather = data.current_weather;
+ const temperature = currentWeather.temperature;
+ const weatherCode = currentWeather.weathercode;
+
+ const weatherConditions = {
+ 0: 'Clear sky',
+ 1: 'Mainly clear',
+ 2: 'Partly cloudy',
+ 3: 'Overcast',
+ 45: 'Fog',
+ 48: 'Depositing rime fog',
+ 51: 'Light drizzle',
+ 53: 'Moderate drizzle',
+ 55: 'Dense drizzle',
+ 56: 'Light freezing drizzle',
+ 57: 'Dense freezing drizzle',
+ 61: 'Slight rain',
+ 63: 'Moderate rain',
+ 65: 'Heavy rain',
+ 66: 'Light freezing rain',
+ 67: 'Heavy freezing rain',
+ 71: 'Slight snow fall',
+ 73: 'Moderate snow fall',
+ 75: 'Heavy snow fall',
+ 77: 'Snow grains',
+ 80: 'Slight rain showers',
+ 81: 'Moderate rain showers',
+ 82: 'Violent rain showers',
+ 85: 'Slight snow showers',
+ 86: 'Heavy snow showers',
+ 95: 'Thunderstorm',
+ 96: 'Thunderstorm with slight hail',
+ 99: 'Thunderstorm with heavy hail',
+ };
+
+ const condition =
+ weatherConditions[weatherCode] || 'Unknown weather condition';
+
+ return { temperature, condition };
+}
+
+if (!customElements.get('my-custom-element')) {
+ customElements.define('my-custom-element', MyCustomElement);
+}
diff --git a/examples/custom-extension/ui.html b/examples/custom-extension/ui.html
new file mode 100644
index 0000000000..7f2d43e838
--- /dev/null
+++ b/examples/custom-extension/ui.html
@@ -0,0 +1,6 @@
+
+
+ Deployments in Namespace
+
+
+
diff --git a/index.html b/index.html
index fcd8962a7f..12afa0bd60 100644
--- a/index.html
+++ b/index.html
@@ -19,6 +19,10 @@
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
+
Busola
diff --git a/kyma/environments/dev/config.yaml b/kyma/environments/dev/config.yaml
index a5651671db..a29e6a3976 100644
--- a/kyma/environments/dev/config.yaml
+++ b/kyma/environments/dev/config.yaml
@@ -66,6 +66,8 @@ config:
isEnabled: true
EXTENSIBILITY_INJECTIONS:
isEnabled: true
+ EXTENSIBILITY_CUSTOM_COMPONENTS:
+ isEnabled: false
EXTENSIBILITY_WIZARD:
isEnabled: true
TRACKING:
diff --git a/kyma/environments/prod/config.yaml b/kyma/environments/prod/config.yaml
index 6c570ccd12..2b239ac776 100644
--- a/kyma/environments/prod/config.yaml
+++ b/kyma/environments/prod/config.yaml
@@ -68,6 +68,8 @@ config:
isEnabled: true
EXTENSIBILITY_INJECTIONS:
isEnabled: true
+ EXTENSIBILITY_CUSTOM_COMPONENTS:
+ isEnabled: false
EXTENSIBILITY_WIZARD:
isEnabled: true
EVENTING:
diff --git a/kyma/environments/stage/config.yaml b/kyma/environments/stage/config.yaml
index 8de69d54aa..a1bf90088b 100644
--- a/kyma/environments/stage/config.yaml
+++ b/kyma/environments/stage/config.yaml
@@ -66,6 +66,8 @@ config:
isEnabled: true
EXTENSIBILITY_INJECTIONS:
isEnabled: true
+ EXTENSIBILITY_CUSTOM_COMPONENTS:
+ isEnabled: false
EXTENSIBILITY_WIZARD:
isEnabled: true
EVENTING:
diff --git a/kyma/extensions/kyma/kyma.yaml b/kyma/extensions/kyma/kyma.yaml
index 93294856ea..51ae50792f 100644
--- a/kyma/extensions/kyma/kyma.yaml
+++ b/kyma/extensions/kyma/kyma.yaml
@@ -77,7 +77,20 @@ data:
- name: Version
source: version
widget: Text
- - name: State
+ - name: Module State
+ widget: Badge
+ source: "$getModuleState($$)"
+ description: "$getModuleDescription($$)"
+ highlights:
+ positive:
+ - 'Ready'
+ critical:
+ - 'Error'
+ none:
+ - 'Processing'
+ - 'Deleting'
+ - 'Unknown'
+ - name: Installation State
widget: Badge
source: 'state ? state : "UNNKOWN"'
description: 'message ? message : ""'
diff --git a/public/defaultConfig.yaml b/public/defaultConfig.yaml
index 15c3504ba3..a59bdaad55 100644
--- a/public/defaultConfig.yaml
+++ b/public/defaultConfig.yaml
@@ -49,6 +49,8 @@ config:
isEnabled: true
EXTENSIBILITY_INJECTIONS:
isEnabled: true
+ EXTENSIBILITY_CUSTOM_COMPONENTS:
+ isEnabled: false
EXTENSIBILITY_WIZARD:
isEnabled: true
TRACKING:
diff --git a/sec-scanners-config.yaml b/sec-scanners-config.yaml
index 7534b973de..97f1684e2e 100644
--- a/sec-scanners-config.yaml
+++ b/sec-scanners-config.yaml
@@ -5,6 +5,8 @@ protecode:
whitesource:
language: javascript
exclude:
+ - 'package-lock.json'
+ - '**/backend/package-lock.json'
- '**/backend/config/config.yaml'
- '**/tests/**'
- '**/kyma/enviroments/**'
diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx
index 35a42b300d..cbabe87e08 100644
--- a/src/components/App/App.tsx
+++ b/src/components/App/App.tsx
@@ -39,6 +39,7 @@ import { useAfterInitHook } from 'state/useAfterInitHook';
import useSidebarCondensed from 'sidebar/useSidebarCondensed';
import { useGetValidationEnabledSchemas } from 'state/validationEnabledSchemasAtom';
import { useGetKymaResources } from 'state/kymaResourcesAtom';
+import { Spinner } from 'shared/components/Spinner/Spinner';
export default function App() {
const language = useRecoilValue(languageAtom);
@@ -59,7 +60,7 @@ export default function App() {
useResourceSchemas();
useSidebarCondensed();
- useAuthHandler();
+ const { isLoading } = useAuthHandler();
useGetConfiguration();
useGetExtensions();
useGetExtensibilitySchemas();
@@ -75,6 +76,10 @@ export default function App() {
useAfterInitHook(kubeconfigIdState);
useGetKymaResources();
+ if (isLoading) {
+ return ;
+ }
+
return (
diff --git a/src/components/Extensibility/ExtensibilityList.js b/src/components/Extensibility/ExtensibilityList.js
index 3c1bb5024b..90864aab30 100644
--- a/src/components/Extensibility/ExtensibilityList.js
+++ b/src/components/Extensibility/ExtensibilityList.js
@@ -1,5 +1,6 @@
import pluralize from 'pluralize';
import { useTranslation } from 'react-i18next';
+import { useEffect } from 'react';
import { ResourcesList } from 'shared/components/ResourcesList/ResourcesList';
import { usePrepareListProps } from 'resources/helpers';
@@ -22,6 +23,9 @@ import { sortBy } from './helpers/sortBy';
import { Widget } from './components/Widget';
import { DataSourcesContextProvider } from './contexts/DataSources';
import { useJsonata } from './hooks/useJsonata';
+import { useFeature } from 'hooks/useFeature';
+import { createPortal } from 'react-dom';
+import YamlUploadDialog from 'resources/Namespaces/YamlUpload/YamlUploadDialog';
export const ExtensibilityListCore = ({
resMetaData,
@@ -140,6 +144,43 @@ const ExtensibilityList = ({ overrideResMetadata, ...props }) => {
const defaultResMetadata = useGetCRbyPath();
const resMetaData = overrideResMetadata || defaultResMetadata;
const { urlPath, defaultPlaceholder } = resMetaData?.general ?? {};
+ const { isEnabled: isExtensibilityCustomComponentsEnabled } = useFeature(
+ 'EXTENSIBILITY_CUSTOM_COMPONENTS',
+ );
+
+ useEffect(() => {
+ const customElement = resMetaData?.general?.customElement;
+ const customScript = resMetaData?.customScript;
+
+ if (
+ isExtensibilityCustomComponentsEnabled &&
+ customElement &&
+ customScript &&
+ !customElements.get(customElement)
+ ) {
+ const script = document.createElement('script');
+ script.type = 'module';
+ const scriptBlob = new Blob([customScript], {
+ type: 'application/javascript',
+ });
+ const blobURL = URL.createObjectURL(scriptBlob);
+ script.src = blobURL;
+
+ // Clean up the Blob URL after the script loads
+ script.onload = () => {
+ URL.revokeObjectURL(blobURL);
+ };
+
+ script.onerror = e => {
+ console.error('Script loading or execution error:', e);
+ };
+ document.head.appendChild(script);
+
+ return () => {
+ document.head.removeChild(script);
+ };
+ }
+ }, [resMetaData, isExtensibilityCustomComponentsEnabled]);
return (
{
>
-
+ {isExtensibilityCustomComponentsEnabled && resMetaData.customHtml ? (
+ <>
+
+ {createPortal(, document.body)}
+ >
+ ) : (
+
+ )}
diff --git a/src/components/Extensibility/components-form/GenericList.js b/src/components/Extensibility/components-form/GenericList.js
index 18341edf0d..d8afcb3342 100644
--- a/src/components/Extensibility/components-form/GenericList.js
+++ b/src/components/Extensibility/components-form/GenericList.js
@@ -1,4 +1,4 @@
-import React, { useState } from 'react';
+import { useState } from 'react';
import { PluginStack, useUIStore } from '@ui-schema/ui-schema';
import { Button } from '@ui5/webcomponents-react';
import { useTranslation } from 'react-i18next';
diff --git a/src/components/Extensibility/components/Badge.js b/src/components/Extensibility/components/Badge.js
index ad07f66233..a9b839f6fd 100644
--- a/src/components/Extensibility/components/Badge.js
+++ b/src/components/Extensibility/components/Badge.js
@@ -67,6 +67,7 @@ export function Badge({
else if (type === 'informative') type = 'Information';
else if (type === 'positive') type = 'Positive';
else if (type === 'critical') type = 'Negative';
+ else if (type === 'none') type = 'Neutral';
type = TYPE_FALLBACK.get(type) || type;
diff --git a/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss b/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss
index fca4f1243b..2e1e9020c2 100644
--- a/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss
+++ b/src/components/Extensibility/components/FeaturedCard/FeaturedCard.scss
@@ -3,6 +3,11 @@
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
+
+.banner-carousel {
+ height: fit-content;
+}
+
.feature-card {
position: relative;
display: flex;
diff --git a/src/components/Extensibility/helpers/jsonataWrapper.ts b/src/components/Extensibility/helpers/jsonataWrapper.ts
index fdf12715cc..b9cefac11e 100644
--- a/src/components/Extensibility/helpers/jsonataWrapper.ts
+++ b/src/components/Extensibility/helpers/jsonataWrapper.ts
@@ -6,6 +6,7 @@ import { doesUserHavePermission } from 'state/navigation/filters/permissions';
import { permissionSetsSelector } from 'state/permissionSetsSelector';
import { jwtDecode } from 'jwt-decode';
import { AuthDataState, authDataState } from 'state/authDataAtom';
+import { useModuleStatus } from '../../KymaModules/support';
/*
Turns jsonata expressions like
@@ -64,6 +65,16 @@ export function jsonataWrapper(expression: string) {
},
);
+ exp.registerFunction('getModuleState', resource => {
+ const { data: status } = useModuleStatus(resource);
+ return status?.state || 'Unknown';
+ });
+
+ exp.registerFunction('getModuleDescription', resource => {
+ const { data: status } = useModuleStatus(resource);
+ return status?.description;
+ });
+
exp.registerFunction('compareStrings', (first, second) => {
return first?.localeCompare(second) ?? 1;
});
diff --git a/src/components/HelmReleases/HelmReleasesList.js b/src/components/HelmReleases/HelmReleasesList.js
index ababeb4705..8ace16c496 100644
--- a/src/components/HelmReleases/HelmReleasesList.js
+++ b/src/components/HelmReleases/HelmReleasesList.js
@@ -72,36 +72,34 @@ function HelmReleasesList() {
];
return (
- <>
-
a.releaseName.localeCompare(b.releaseName),
- }}
- searchSettings={{
- textSearchProperties: [
- 'recentRelease.chart.metadata.name',
- 'releaseName',
- ],
- }}
- emptyListProps={{
- subtitleText: ResourceDescription,
- url: docsURL,
- showButton: false,
- }}
- readOnly
- description={ResourceDescription}
- />
- >
+ a.releaseName.localeCompare(b.releaseName),
+ }}
+ searchSettings={{
+ textSearchProperties: [
+ 'recentRelease.chart.metadata.name',
+ 'releaseName',
+ ],
+ }}
+ emptyListProps={{
+ subtitleText: ResourceDescription,
+ url: docsURL,
+ showButton: false,
+ }}
+ readOnly
+ description={ResourceDescription}
+ />
);
}
diff --git a/src/components/KymaModules/KymaModulesAddModule.js b/src/components/KymaModules/KymaModulesAddModule.js
index e0ca44c083..6c4f9df6c4 100644
--- a/src/components/KymaModules/KymaModulesAddModule.js
+++ b/src/components/KymaModules/KymaModulesAddModule.js
@@ -25,11 +25,18 @@ export default function KymaModulesAddModule({
const modulesResourceUrl = `/apis/operator.kyma-project.io/v1beta2/moduletemplates`;
+ const modulesReleaseMetaResourceUrl = `/apis/operator.kyma-project.io/v1beta2/modulereleasemetas`;
+
const { data: modules } = useGet(modulesResourceUrl, {
pollingInterval: 3000,
skip: !resourceName,
});
+ const { data: moduleReleaseMetas } = useGet(modulesReleaseMetaResourceUrl, {
+ pollingInterval: 3000,
+ skip: !resourceName,
+ });
+
const [columnsCount, setColumnsCount] = useState(2);
const [cardsContainerRef, setCardsContainerRef] = useState(null);
@@ -77,29 +84,65 @@ export default function KymaModulesAddModule({
const isAlreadyInstalled = initialUnchangedResource?.spec?.modules?.find(
installedModule => installedModule.name === name,
);
+ const moduleMetaRelase = moduleReleaseMetas?.items.find(
+ item => item.spec.moduleName === name,
+ );
- if (!existingModule && !isAlreadyInstalled) {
- acc.push({
- name: name,
- channels: [
- {
- channel: module.spec.channel,
- version: module.spec.descriptor.component.version,
- isBeta:
- module.metadata.labels['operator.kyma-project.io/beta'] ===
- 'true',
- },
- ],
- docsUrl:
- module.metadata.annotations['operator.kyma-project.io/doc-url'],
- });
- } else if (existingModule) {
- existingModule.channels?.push({
- channel: module.spec.channel,
- version: module.spec.descriptor.component.version,
- isBeta:
- module.metadata.labels['operator.kyma-project.io/beta'] === 'true',
- });
+ if (module.spec.channel) {
+ if (!existingModule && !isAlreadyInstalled) {
+ acc.push({
+ name: name,
+ channels: [
+ {
+ channel: module.spec.channel,
+ version: module.spec.descriptor.component.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] ===
+ 'true',
+ },
+ ],
+ docsUrl:
+ module.metadata.annotations['operator.kyma-project.io/doc-url'],
+ });
+ } else if (existingModule) {
+ existingModule.channels?.push({
+ channel: module.spec.channel,
+ version: module.spec.descriptor.component.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] === 'true',
+ });
+ }
+ } else {
+ if (!existingModule && !isAlreadyInstalled) {
+ moduleMetaRelase?.spec.channels.forEach(channel => {
+ if (!acc.find(item => item.name === name)) {
+ acc.push({
+ name: name,
+ channels: [
+ {
+ channel: channel.channel,
+ version: channel.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] ===
+ 'true',
+ },
+ ],
+ docsUrl:
+ module.metadata.annotations['operator.kyma-project.io/doc-url'],
+ });
+ } else {
+ acc
+ .find(item => item.name === name)
+ .channels.push({
+ channel: channel.channel,
+ version: channel.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] ===
+ 'true',
+ });
+ }
+ });
+ }
}
return acc ?? [];
diff --git a/src/components/KymaModules/KymaModulesCreate.js b/src/components/KymaModules/KymaModulesCreate.js
index 8f98f8c70a..f926979c88 100644
--- a/src/components/KymaModules/KymaModulesCreate.js
+++ b/src/components/KymaModules/KymaModulesCreate.js
@@ -44,7 +44,17 @@ export default function KymaModulesCreate({ resource, ...props }) {
const resourceName = kymaResource?.metadata.name;
const modulesResourceUrl = `/apis/operator.kyma-project.io/v1beta2/moduletemplates`;
- const { data: modules, loading } = useGet(modulesResourceUrl, {
+ const modulesReleaseMetaResourceUrl = `/apis/operator.kyma-project.io/v1beta2/modulereleasemetas`;
+
+ const { data: modules, loading: lodingModules } = useGet(modulesResourceUrl, {
+ pollingInterval: 3000,
+ skip: !resourceName,
+ });
+
+ const {
+ data: moduleReleaseMetas,
+ loading: loadingModulesReleaseMetas,
+ } = useGet(modulesReleaseMetaResourceUrl, {
pollingInterval: 3000,
skip: !resourceName,
});
@@ -68,7 +78,7 @@ export default function KymaModulesCreate({ resource, ...props }) {
onSave: false,
});
- if (loading) {
+ if (lodingModules || loadingModulesReleaseMetas) {
return (
@@ -131,29 +141,65 @@ export default function KymaModulesCreate({ resource, ...props }) {
const name =
module.metadata?.labels['operator.kyma-project.io/module-name'];
const existingModule = acc.find(item => item.name === name);
+ const moduleMetaRelase = moduleReleaseMetas?.items.find(
+ item => item.spec.moduleName === name,
+ );
- if (!existingModule) {
- acc.push({
- name: name,
- channels: [
- {
- channel: module.spec.channel,
- version: module.spec.descriptor.component.version,
- isBeta:
- module.metadata.labels['operator.kyma-project.io/beta'] ===
- 'true',
- },
- ],
- docsUrl:
- module.metadata.annotations['operator.kyma-project.io/doc-url'],
- });
+ if (module.spec.channel) {
+ if (!existingModule) {
+ acc.push({
+ name: name,
+ channels: [
+ {
+ channel: module.spec.channel,
+ version: module.spec.descriptor.component.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] ===
+ 'true',
+ },
+ ],
+ docsUrl:
+ module.metadata.annotations['operator.kyma-project.io/doc-url'],
+ });
+ } else if (existingModule) {
+ existingModule.channels?.push({
+ channel: module.spec.channel,
+ version: module.spec.descriptor.component.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] === 'true',
+ });
+ }
} else {
- existingModule.channels?.push({
- channel: module.spec.channel,
- version: module.spec.descriptor.component.version,
- isBeta:
- module.metadata.labels['operator.kyma-project.io/beta'] === 'true',
- });
+ if (!existingModule) {
+ moduleMetaRelase?.spec.channels.forEach(channel => {
+ if (!acc.find(item => item.name === name)) {
+ acc.push({
+ name: name,
+ channels: [
+ {
+ channel: channel.channel,
+ version: channel.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] ===
+ 'true',
+ },
+ ],
+ docsUrl:
+ module.metadata.annotations['operator.kyma-project.io/doc-url'],
+ });
+ } else {
+ acc
+ .find(item => item.name === name)
+ .channels.push({
+ channel: channel.channel,
+ version: channel.version,
+ isBeta:
+ module.metadata.labels['operator.kyma-project.io/beta'] ===
+ 'true',
+ });
+ }
+ });
+ }
}
return acc;
}, []);
@@ -236,9 +282,11 @@ export default function KymaModulesCreate({ resource, ...props }) {
value={channel.channel}
additionalText={channel?.isBeta ? 'Beta' : ''}
>
- {`${channel.channel[0].toUpperCase()}${channel.channel.slice(
- 1,
- )} (v${channel.version})`}{' '}
+ {`${(
+ channel?.channel[0] || ''
+ ).toUpperCase()}${channel.channel.slice(1)} (v${
+ channel.version
+ })`}{' '}
))}
diff --git a/src/components/KymaModules/KymaModulesList.js b/src/components/KymaModules/KymaModulesList.js
index 52ed92a242..5952056e45 100644
--- a/src/components/KymaModules/KymaModulesList.js
+++ b/src/components/KymaModules/KymaModulesList.js
@@ -201,7 +201,7 @@ export default function KymaModulesList({
<>
{moduleStatus?.channel
? moduleStatus?.channel
- : EMPTY_TEXT_PLACEHOLDER}
+ : kymaResource?.spec?.modules?.[moduleIndex]?.channel}
{isChannelOverriden ? (
+
diff --git a/src/components/KymaModules/ModulesCard.js b/src/components/KymaModules/ModulesCard.js
index 747ba14add..84ef68a469 100644
--- a/src/components/KymaModules/ModulesCard.js
+++ b/src/components/KymaModules/ModulesCard.js
@@ -109,9 +109,11 @@ export default function ModulesCard({
value={channel.channel}
additionalText={channel?.isBeta ? 'Beta' : ''}
>
- {`${channel.channel[0].toUpperCase()}${channel.channel.slice(
- 1,
- )} (v${channel.version})`}{' '}
+ {`${(
+ channel?.channel[0] || ''
+ ).toUpperCase()}${channel.channel.slice(1)} (v${
+ channel.version
+ })`}{' '}
))}
diff --git a/src/components/Nodes/NodeResources/NodeResources.js b/src/components/Nodes/NodeResources/NodeResources.js
index 9a1e44cfc0..28996dbf83 100644
--- a/src/components/Nodes/NodeResources/NodeResources.js
+++ b/src/components/Nodes/NodeResources/NodeResources.js
@@ -39,7 +39,7 @@ export function NodeResources({ metrics, resources }) {
max={memory.capacity}
additionalInfo={`${roundTwoDecimals(
memory.usage,
- )}GiB / ${roundTwoDecimals(memory.capacity)}GiB`}
+ )}Gi / ${roundTwoDecimals(memory.capacity)}Gi`}
/>
>
diff --git a/src/components/Nodes/nodeQueries.js b/src/components/Nodes/nodeQueries.js
index 335e35bf15..55585c57d1 100644
--- a/src/components/Nodes/nodeQueries.js
+++ b/src/components/Nodes/nodeQueries.js
@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { useGet } from 'shared/hooks/BackendAPI/useGet';
import {
getBytes,
@@ -22,27 +22,30 @@ const formatMemory = memoryStr =>
const createUsageMetrics = (node, metricsForNode) => {
const cpuUsage = formatCpu(metricsForNode?.usage.cpu);
const memoryUsage = formatMemory(metricsForNode?.usage.memory);
- const cpuCapacity = parseInt(node.status.capacity?.cpu || '0') * 1000;
- const memoryCapacity = formatMemory(node.status.capacity?.memory);
+ const cpuCapacity = parseInt(node.status.allocatable?.cpu || '0');
+ const memoryCapacity = formatMemory(node.status.allocatable?.memory);
+
+ const cpuPercentage = getPercentageFromUsage(cpuUsage, cpuCapacity);
+ const memoryPercentage = getPercentageFromUsage(memoryUsage, memoryCapacity);
return {
cpu: {
usage: cpuUsage,
capacity: cpuCapacity,
- percentage: getPercentageFromUsage(cpuUsage, cpuCapacity) + '%',
- percentageValue: getPercentageFromUsage(cpuUsage, cpuCapacity),
+ percentage: cpuPercentage + '%',
+ percentageValue: cpuPercentage,
},
memory: {
usage: memoryUsage,
capacity: memoryCapacity,
- percentage: getPercentageFromUsage(memoryUsage, memoryCapacity) + '%',
- percentageValue: getPercentageFromUsage(memoryUsage, memoryCapacity),
+ percentage: memoryPercentage + '%',
+ percentageValue: memoryPercentage,
},
};
};
export function useNodesQuery(skip = false) {
- const [data, setData] = React.useState(null);
+ const [data, setData] = useState(null);
const { data: nodeMetrics, loading: metricsLoading } = useGet(
'/apis/metrics.k8s.io/v1beta1/nodes',
{
@@ -57,7 +60,7 @@ export function useNodesQuery(skip = false) {
loading: nodesLoading,
} = useGet('/api/v1/nodes', { pollingInterval: 5500, skip });
- React.useEffect(() => {
+ useEffect(() => {
if (nodes) {
const getNodeMetrics = node => {
const metricsForNode = nodeMetrics.items.find(
@@ -83,7 +86,7 @@ export function useNodesQuery(skip = false) {
}
export function useNodeQuery(nodeName) {
- const [data, setData] = React.useState(null);
+ const [data, setData] = useState(null);
const {
data: nodeMetrics,
error: metricsError,
@@ -98,7 +101,7 @@ export function useNodeQuery(nodeName) {
loading: nodeLoading,
} = useGet(`/api/v1/nodes/${nodeName}`, { pollingInterval: 3000 });
- React.useEffect(() => {
+ useEffect(() => {
if (node) {
setData({
node,
@@ -152,7 +155,9 @@ function addResources(a, b) {
function sumContainersResources(containers) {
return containers?.reduce((containerAccu, container) => {
- return addResources(containerAccu, container.resources);
+ const containerResources = container.resources;
+ const updatedResources = addResources(containerAccu, containerResources);
+ return updatedResources;
}, structuredClone(emptyResources));
}
@@ -181,14 +186,14 @@ export function calcNodeResources(pods) {
}
export function useResourceByNode(nodeName) {
- const [data, setData] = React.useState(null);
+ const [data, setData] = useState(null);
const { data: pods, error, loading } = useGet(
`/api/v1/pods?fieldSelector=spec.nodeName=${nodeName},status.phase!=Failed,status.phase!=Succeeded&limit=500`,
);
const nodeResources = useMemo(() => calcNodeResources(pods), [pods]);
- React.useEffect(() => {
+ useEffect(() => {
if (nodeResources) {
setData(nodeResources);
}
diff --git a/src/header/Header.tsx b/src/header/Header.tsx
index 781150edf0..52ac4b7e08 100644
--- a/src/header/Header.tsx
+++ b/src/header/Header.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react';
+import { useState } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import {
Avatar,
@@ -8,7 +8,6 @@ import {
MenuDomRef,
ShellBar,
ShellBarItem,
- ShellBarDomRef,
ListItemStandard,
} from '@ui5/webcomponents-react';
import { MenuItemClickEventDetail } from '@ui5/webcomponents/dist/Menu.js';
@@ -55,21 +54,11 @@ export function Header() {
);
const [isFormOpen, setIsFormOpen] = useRecoilState(isFormOpenState);
- const shellbarRef = useRef(null);
- useEffect(() => {
- if (shellbarRef?.current) {
- shellbarRef.current.accessibilityTexts = {
- ...shellbarRef.current.accessibilityTexts,
- logoTitle: 'SAP Kyma logo',
- };
- }
- }, [shellbarRef]);
-
const inactiveClusterNames = Object.keys(clusters || {}).filter(
name => name !== cluster?.name,
);
- const nonBreakableSpaces = (number: int): string => {
+ const nonBreakableSpaces = (number: number): string => {
let spaces = '';
for (let i = 0; i < number; i++) {
spaces += '\u00a0';
@@ -120,10 +109,14 @@ export function Header() {
<>
}
- ref={shellbarRef}
onLogoClick={() => {
handleActionIfFormOpen(
isResourceEdited,
@@ -225,7 +218,6 @@ export function Header() {
text={t('common.labels.version')}
additionalText={busolaVersion}
icon="inspect"
- startsSection
/>
diff --git a/src/resources/Deployments/DeploymentDetails.js b/src/resources/Deployments/DeploymentDetails.js
index 5740639484..c6448828a9 100644
--- a/src/resources/Deployments/DeploymentDetails.js
+++ b/src/resources/Deployments/DeploymentDetails.js
@@ -64,7 +64,12 @@ export function DeploymentDetails(props) {
const statusConditions = deployment => {
return deployment?.status?.conditions?.map(condition => {
return {
- header: { titleText: condition.type, status: condition.status },
+ header: {
+ titleText: condition.type,
+ status: condition.status,
+ overrideStatusType:
+ condition.type === 'ReplicaFailure' ? 'False' : condition.status,
+ },
message: condition.message,
};
});
diff --git a/src/resources/Deployments/DeploymentStatus.js b/src/resources/Deployments/DeploymentStatus.js
index 5bf2456caf..0fb4fe10d3 100644
--- a/src/resources/Deployments/DeploymentStatus.js
+++ b/src/resources/Deployments/DeploymentStatus.js
@@ -4,7 +4,7 @@ import { RunningPodsStatus } from 'shared/components/RunningPodsStatus';
export function DeploymentStatus({ deployment }) {
const running = deployment.status.readyReplicas || 0;
- const expected = deployment.status.replicas || 0;
+ const expected = deployment.status.replicas || deployment.spec.replicas || 0;
return ;
}
diff --git a/src/resources/Namespaces/AllNamespacesDetails.js b/src/resources/Namespaces/AllNamespacesDetails.js
index 0fb112b522..8302f233d6 100644
--- a/src/resources/Namespaces/AllNamespacesDetails.js
+++ b/src/resources/Namespaces/AllNamespacesDetails.js
@@ -44,7 +44,7 @@ export function AllNamespacesDetails() {
const Events = ;
const headerActions = (
- <>
+
{createPortal(, document.body)}
- >
+
);
return (
diff --git a/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js b/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js
index 2e72432885..d935525d1f 100644
--- a/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js
+++ b/src/resources/Namespaces/NamespaceWorkloads/NamespaceWorkloadsHelpers.js
@@ -1,8 +1,12 @@
import { calculatePodState } from 'resources/Pods/PodStatus';
export function getHealthyReplicasCount(resource) {
- return resource?.filter(r => r.status.replicas === r.status.readyReplicas)
- ?.length;
+ return resource?.filter(r => {
+ const running = r.status.readyReplicas || 0;
+ const expected = r.status.replicas || r.spec.replicas || 0;
+
+ return running === expected;
+ })?.length;
}
export const PodStatusCounterKey = {
diff --git a/src/resources/Namespaces/ResourcesUsage.js b/src/resources/Namespaces/ResourcesUsage.js
index 718403e83f..3bc6eb5426 100644
--- a/src/resources/Namespaces/ResourcesUsage.js
+++ b/src/resources/Namespaces/ResourcesUsage.js
@@ -9,6 +9,7 @@ import { Card, CardHeader } from '@ui5/webcomponents-react';
const MEMORY_SUFFIX_POWER = {
// must be sorted from the smallest to the largest; it is case sensitive; more info: https://medium.com/swlh/understanding-kubernetes-resource-cpu-and-memory-units-30284b3cc866
m: 1e-3,
+ k: 1e3,
K: 1e3,
Ki: 2 ** 10,
M: 1e6,
@@ -22,24 +23,13 @@ const CPU_SUFFIX_POWER = {
m: 1e-3,
};
-export function getBytes(memoryString) {
- if (!memoryString || memoryString === '0') {
- return 0;
- }
- const suffixMatch = String(memoryString).match(/\D+$/);
-
- if (!suffixMatch?.length) {
- return memoryString;
- }
- const suffix = suffixMatch[0];
- const number = String(memoryString).replace(suffix, '');
+export function getBytes(memoryStr) {
+ if (!memoryStr) return 0;
- const suffixPower = MEMORY_SUFFIX_POWER[suffix];
- if (!suffixPower) {
- return number;
- }
-
- return number * suffixPower;
+ const unit = String(memoryStr).match(/[a-zA-Z]+/g)?.[0];
+ const value = parseFloat(memoryStr);
+ const bytes = value * (MEMORY_SUFFIX_POWER[unit] || 1);
+ return bytes;
}
export function getCpus(cpuString) {
diff --git a/src/resources/ServiceAccounts/TokenRequestModal/TokenRequestModal.tsx b/src/resources/ServiceAccounts/TokenRequestModal/TokenRequestModal.tsx
index bb21cef04a..f32aebef2d 100644
--- a/src/resources/ServiceAccounts/TokenRequestModal/TokenRequestModal.tsx
+++ b/src/resources/ServiceAccounts/TokenRequestModal/TokenRequestModal.tsx
@@ -8,6 +8,7 @@ import { ResourceForm } from 'shared/ResourceForm';
import { ComboboxInput } from 'shared/ResourceForm/inputs';
import { CopiableText } from 'shared/components/CopiableText/CopiableText';
import { Editor } from 'shared/components/MonacoEditorESM/Editor';
+import { useRef } from 'react';
const expirationSecondsOptions = [
{
@@ -71,6 +72,7 @@ export function TokenRequestModal({
}: TokenRequestModalProps) {
const { t } = useTranslation();
const downloadKubeconfig = useDownloadKubeconfigWithToken();
+ const modalRef = useRef(null);
const {
kubeconfigYaml,
@@ -78,7 +80,12 @@ export function TokenRequestModal({
generateTokenRequest,
tokenRequest,
setTokenRequest,
- } = useGenerateTokenRequest(isModalOpen, namespace, serviceAccountName);
+ } = useGenerateTokenRequest(
+ isModalOpen,
+ namespace,
+ serviceAccountName,
+ modalRef,
+ );
const isExpirationSecondsValueANumber = () =>
!Number(tokenRequest.spec.expirationSeconds);
@@ -100,6 +107,7 @@ export function TokenRequestModal({
open={isModalOpen}
onClose={handleCloseModal}
headerText={t('service-accounts.token-request.generate')}
+ ref={modalRef}
footer={
,
) => {
const { t } = useTranslation();
const post = usePost();
@@ -44,6 +45,7 @@ export const useGenerateTokenRequest = (
notifyToast(
{
content: t('service-accounts.token-request.notification.success'),
+ parentContainer: generate ? modalRef?.current : undefined,
},
3000,
);
diff --git a/src/resources/createResourceRoutes.js b/src/resources/createResourceRoutes.js
index 845145da95..9c1052751c 100644
--- a/src/resources/createResourceRoutes.js
+++ b/src/resources/createResourceRoutes.js
@@ -84,17 +84,31 @@ const ColumnWrapper = ({
});
const elementListProps = usePrepareListProps({
- ...props,
+ resourceCustomType: props.resourceCustomType,
+ resourceType: props.resourceType,
+ resourceI18Key: props.resourceI18Key,
+ apiGroup: props.apiGroup,
+ apiVersion: props.apiVersion,
+ hasDetailsView: props.hasDetailsView,
});
const elementDetailsProps = usePrepareDetailsProps({
- ...props,
+ resourceCustomType: props.resourceCustomType,
+ resourceType: props.resourceType,
+ resourceI18Key: props.resourceI18Key,
+ apiGroup: props.apiGroup,
+ apiVersion: props.apiVersion,
resourceName: layoutState?.midColumn?.resourceName ?? resourceName,
namespaceId: layoutState?.midColumn?.namespaceId ?? namespaceId,
+ showYamlTab: props.showYamlTab,
});
const elementCreateProps = usePrepareCreateProps({
- ...props,
+ resourceCustomType: props.resourceCustomType,
+ resourceType: props.resourceType,
+ resourceTypeForTitle: props.resourceType,
+ apiGroup: props.apiGroup,
+ apiVersion: props.apiVersion,
});
const listComponent = React.cloneElement(list, {
@@ -122,16 +136,13 @@ const ColumnWrapper = ({
detailsMidColumn = detailsComponent;
}
- const { schema, loading } = useGetSchema({
+ const { schema } = useGetSchema({
resource: {
group: props?.apiGroup,
version: props.apiVersion,
kind: props?.resourceType.slice(0, -1),
},
});
- if (loading) {
- return null;
- }
const createMidColumn = (
+
{
let startColumnComponent = null;
const headerActions = (
- <>
+
@@ -139,7 +139,7 @@ const ColumnWraper = (defaultColumn = 'list') => {
/>,
document.body,
)}
- >
+
);
if (!layout && defaultColumn === 'details') {
diff --git a/src/shared/components/ConditionList/ConditionList.tsx b/src/shared/components/ConditionList/ConditionList.tsx
index 397c7390b5..dae0b0dab3 100644
--- a/src/shared/components/ConditionList/ConditionList.tsx
+++ b/src/shared/components/ConditionList/ConditionList.tsx
@@ -18,6 +18,7 @@ type ConditionItem = {
type ConditionHeader = {
titleText: string | ReactNode;
status?: string;
+ overrideStatusType?: string;
};
export const ConditionList = ({
@@ -34,6 +35,7 @@ export const ConditionList = ({
key={index}
header={cond.header?.titleText}
status={cond.header?.status}
+ overrideStatusType={cond.header?.overrideStatusType}
content={cond.message}
customContent={cond.customContent}
/>
diff --git a/src/shared/components/DynamicPageComponent/DynamicPageComponent.js b/src/shared/components/DynamicPageComponent/DynamicPageComponent.js
index 3328464404..ed25639c62 100644
--- a/src/shared/components/DynamicPageComponent/DynamicPageComponent.js
+++ b/src/shared/components/DynamicPageComponent/DynamicPageComponent.js
@@ -11,7 +11,6 @@ import {
} from '@ui5/webcomponents-react';
import { Toolbar } from '@ui5/webcomponents-react-compat/dist/components/Toolbar/index.js';
import { ToolbarSpacer } from '@ui5/webcomponents-react-compat/dist/components/ToolbarSpacer/index.js';
-import { ToolbarSeparator } from '@ui5/webcomponents-react-compat/dist/components/ToolbarSeparator/index.js';
import './DynamicPageComponent.scss';
import { useEffect, useState, useRef } from 'react';
@@ -164,15 +163,15 @@ export const DynamicPageComponent = ({
>
{actions && (
-
+ <>
{actions}
{(window.location.search.includes('layout') ||
(!window.location.search.includes('layout') &&
layoutColumn?.showCreate?.resourceType)) &&
layoutNumber !== 'StartColumn' ? (
-
+
) : null}
-
+ >
)}
{window.location.search.includes('layout') ||
(!window.location.search.includes('layout') &&
diff --git a/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss b/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss
index 1d512b03ed..a1122a9d0b 100644
--- a/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss
+++ b/src/shared/components/DynamicPageComponent/DynamicPageComponent.scss
@@ -4,6 +4,7 @@
ui5-dynamic-page-title {
justify-content: center;
min-height: 3rem;
+ padding-left: 2rem;
}
.bold-title {
@@ -27,10 +28,6 @@
display: none;
}
- [data-component-name='ObjectPageTitleMiddleSection'] > div {
- flex-basis: 100%;
- }
-
&__actions {
display: flex;
align-items: center;
@@ -57,6 +54,17 @@
position: static;
padding: unset;
}
+
+ .tab-container {
+ position: sticky;
+ z-index: 1;
+ }
+}
+
+ui5-dynamic-page {
+ .no-shadow {
+ box-shadow: none;
+ }
}
.header-wrapper {
@@ -99,10 +107,6 @@
}
}
-ui5-button[data-component-name='ObjectPageAnchorBarExpandBtn'] {
- display: none;
-}
-
@media (max-width: 768px) {
.column-wrapper {
display: flex;
@@ -110,8 +114,3 @@ ui5-button[data-component-name='ObjectPageAnchorBarExpandBtn'] {
gap: 12px;
}
}
-
-[data-component-name='DynamicPageContent'],
-[data-component-name='ObjectPageContent'] {
- padding: 0 !important;
-}
diff --git a/src/shared/components/ExpandableListItem/ExpandableListItem.tsx b/src/shared/components/ExpandableListItem/ExpandableListItem.tsx
index 0f8ad6751a..0af3a9bad2 100644
--- a/src/shared/components/ExpandableListItem/ExpandableListItem.tsx
+++ b/src/shared/components/ExpandableListItem/ExpandableListItem.tsx
@@ -8,6 +8,7 @@ import './ExpandableListItem.scss';
type ExpandableListItemProps = {
header: string | ReactNode;
status?: string;
+ overrideStatusType?: string;
content?: string;
customContent?: CustomContent[];
};
@@ -21,12 +22,18 @@ export type CustomContent = {
export const ExpandableListItem = ({
header,
status,
+ overrideStatusType,
content,
customContent,
}: ExpandableListItemProps) => {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
+ let statusType = status === 'True' ? 'Success' : 'Error';
+ if (overrideStatusType !== undefined) {
+ statusType = overrideStatusType === 'True' ? 'Success' : 'Error';
+ }
+
return (
<>
+
{status}
)}
diff --git a/src/shared/components/GenericList/GenericList.js b/src/shared/components/GenericList/GenericList.js
index 2c310262fb..e8cf5dab4a 100644
--- a/src/shared/components/GenericList/GenericList.js
+++ b/src/shared/components/GenericList/GenericList.js
@@ -32,6 +32,7 @@ import pluralize from 'pluralize';
import { isResourceEditedState } from 'state/resourceEditedAtom';
import { isFormOpenState } from 'state/formOpenAtom';
import { handleActionIfFormOpen } from '../UnsavedMessageBox/helpers';
+import './GenericList.scss';
const defaultSort = {
name: nameLocaleSort,
@@ -113,9 +114,7 @@ export const GenericList = ({
}, [pageSize, pagination]);
const { i18n, t } = useTranslation();
- const [currentPage, setCurrentPage] = React.useState(
- pagination?.initialPage || 1,
- );
+ const [currentPage, setCurrentPage] = useState(pagination?.initialPage || 1);
const [filteredEntries, setFilteredEntries] = useState(() =>
sorting(sort, entries),
@@ -197,7 +196,7 @@ export const GenericList = ({
const renderTableBody = () => {
if (serverDataError) {
return (
-
+
{getErrorMessage(serverDataError)}
);
@@ -205,7 +204,7 @@ export const GenericList = ({
if (serverDataLoading) {
return (
-
+
);
diff --git a/src/shared/components/GenericList/components.js b/src/shared/components/GenericList/components.js
index 200d53dbc3..4bad7d571d 100644
--- a/src/shared/components/GenericList/components.js
+++ b/src/shared/components/GenericList/components.js
@@ -137,7 +137,7 @@ const DefaultRowRenderer = ({
);
return (
-
+
{cells}
{!!actions.length && actionsCell}
{displayArrow && (
diff --git a/src/shared/components/ListActions/ListActions.js b/src/shared/components/ListActions/ListActions.js
index ba46752312..f635ceef42 100644
--- a/src/shared/components/ListActions/ListActions.js
+++ b/src/shared/components/ListActions/ListActions.js
@@ -22,7 +22,10 @@ const StandaloneAction = ({ action, entry }) => {
return (