107 Commits
v2.0.1 ... main

Author SHA1 Message Date
dependabot[bot]
dd64b54800 Bump flatted from 3.3.3 to 3.4.2 in /frontend (#668)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-22 20:37:23 +00:00
saudademjj
f12f60f0c7 fix(frontend): use unique installed package keys (#667) 2026-03-19 10:11:27 +00:00
dependabot[bot]
b2cbf812f2 Bump google.golang.org/grpc from 1.68.1 to 1.79.3 (#665)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.68.1 to 1.79.3.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.68.1...v1.79.3)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-version: 1.79.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-19 08:34:56 +00:00
komodor-bot
5783095e0d Increment chart versions [skip ci] 2026-03-17 19:57:49 +00:00
dependabot[bot]
58ba15e1bd Bump dompurify and swagger-ui-react in /frontend (#664)
Bumps [dompurify](https://github.com/cure53/DOMPurify) to 3.3.3 and updates ancestor dependency [swagger-ui-react](https://github.com/swagger-api/swagger-ui). These dependencies need to be updated together.


Updates `dompurify` from 3.2.6 to 3.3.3
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.2.6...3.3.3)

Updates `swagger-ui-react` from 5.31.2 to 5.32.1
- [Release notes](https://github.com/swagger-api/swagger-ui/releases)
- [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.31.2...v5.32.1)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.3.3
  dependency-type: indirect
- dependency-name: swagger-ui-react
  dependency-version: 5.32.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-17 19:08:08 +00:00
Andrei Pohilko
a6b8beb25c Cleanup 2026-03-17 18:20:01 +00:00
Andrei Pohilko
2b6964dcd5 refactor: reduce cyclomatic complexity in relations extraction
Break up ExtractRelations, extractVolumes, extractEnvRefs, and
extractIngressBackends into smaller focused functions to pass CI
complexity checks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 18:19:39 +00:00
Andrei Pohilko
e44556d100 fix: resolve DOMPurify XSS vulnerabilities (GHSA-v8jm-5vwx-cfxm, GHSA-v2wj-7wpq-c8vv)
Add yarn resolution to pin dompurify>=3.3.2, fixing transitive dependency
from swagger-ui-react that was stuck at 3.2.6.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 18:03:09 +00:00
Andrei Pohilko
cfc28cf3a0 feat: add Relations tab with force-directed resource dependency graph (#96)
Add a new "Relations" tab after "Images" that visualizes resource
dependencies within a Helm release as an interactive force-directed graph.
Detects relationships via ownerReferences, *Ref fields, volumes, env refs,
service selectors, ingress backends, and RBAC bindings. External resources
appear as dashed oval ghost nodes. Color-coded by resource category.

Closes #96

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:58:00 +00:00
Andrei Pohilko
443207191d feat: display container images summary on Images tab (#83)
Add a new "Images" tab on the release details page that extracts
and displays all container images (including init containers) from
the release manifest. Images are grouped by image string and show
the associated resource and container name.

Closes #83

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:47:59 +00:00
Andrei Pohilko
c5ae60a779 feat: add --force flag support for helm upgrade (#505)
Add a "Force upgrade" checkbox in the upgrade modal footer that passes
the --force flag to helm upgrade, causing resources to be deleted and
recreated. Also fix the version selector flashing the URL input while
loading by showing a spinner.

Closes #505

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:18:00 +00:00
Andrei Pohilko
4fb2eb099a fix: use apiVersion to disambiguate CRDs with same kind (#504)
When multiple CRDs share the same kind but different API groups
(e.g. Traefik's Middleware under traefik.io and traefik.containo.us),
the dashboard failed to look up the correct resource. Thread apiVersion
through the resource fetch chain and use group-qualified kind
(e.g. Widget.new.example.com) for kubectl lookups.

Closes #504

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 15:55:48 +00:00
Andrei Pohilko
62cf1dfc3e feat(health): add status display for DaemonSet and StatefulSet (#32)
Introduce extendedCarp struct to capture numeric status fields
(desiredNumberScheduled, numberReady, replicas, readyReplicas, etc.)
that are lost during standard Carp unmarshaling. Synthesize an
"Available" condition from these fields so EnhanceStatus can
determine health correctly.

Closes #32

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:40:42 +00:00
Andrei Pohilko
f7deda06f5 fix: pass resource name instead of chart name to describe API (#657)
The DescribeResource component was passing the chart/release name
instead of the actual resource name, causing all resources of the
same kind to show the same describe output.

Closes #657

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:22:22 +00:00
Andrey Pokhilko
123f674e2f feat: install and upgrade charts from URLs (#663)
* feat: add support for installing and upgrading charts from URLs

Adds "Install from URL" button to the repositories page, allowing users
to install charts directly from OCI registries and other URLs without
adding them as repositories first. Also adds URL mode to the upgrade
modal (via pencil/X toggle) for charts not found in any configured repo.

Closes #660

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Alter

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 13:09:11 +00:00
Andrei Pohilko
5d2a61c2ff refactor(health): reduce cyclomatic complexity in applyCustomConditions
Extract repeated healthy/unhealthy logic into applyHealthFromCondition helper
and convert if/else chain to switch statement (complexity 29 → 16).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 09:48:25 +00:00
Matt Van Horn
f857f8dfdc feat(health): add health status for ExternalSecret, Job, HPA, and Namespace (#662)
Add condition-based health status calculation for additional resource kinds:
- ExternalSecret: checks "Ready" condition
- Job: checks "Complete" and "Failed" conditions
- HorizontalPodAutoscaler: checks "AbleToScale" and "ScalingActive" conditions
- Namespace: handles "Terminating" phase as Progressing

Closes #418

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 09:33:33 +00:00
dependabot[bot]
6b07fbe242 Bump immutable from 3.8.2 to 3.8.3 in /frontend (#661)
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 3.8.2 to 3.8.3.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v3.8.2...v3.8.3)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 3.8.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-08 19:35:30 +00:00
dependabot[bot]
2d1fa25e7e Bump rollup from 4.58.0 to 4.59.0 in /frontend (#659)
Bumps [rollup](https://github.com/rollup/rollup) from 4.58.0 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.58.0...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 16:01:09 +00:00
dependabot[bot]
331925900a Bump minimatch from 10.2.2 to 10.2.4 in /frontend (#658)
Bumps [minimatch](https://github.com/isaacs/minimatch) from 10.2.2 to 10.2.4.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v10.2.2...v10.2.4)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 10.2.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-28 14:37:59 +00:00
yuri-sakharov
f91daafd4a Improved EsLint performance + bump frontend packages (#656)
* Bump frontend packages to latest

* DropDown.stories.tsx fixed
2026-02-21 10:51:11 +00:00
dependabot[bot]
a15e375105 Bump systeminformation from 5.30.7 to 5.31.1 in /frontend (#655)
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.30.7 to 5.31.1.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.30.7...v5.31.1)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-version: 5.31.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 18:16:30 +00:00
yuri-sakharov
2b7df9cfa3 Bump FE packages to latest (#654)
* Bump FE packages to latest

* Bump eslint packages to the latest available without breaking change
2026-02-15 19:08:49 +00:00
dependabot[bot]
b457be85c1 Bump axios from 1.13.2 to 1.13.5 in /frontend (#651)
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-15 18:02:41 +00:00
yuri-sakharov
c9b8fb7809 Introduced tsconfig.app.json and tsconfig.base.json + Refactored eslint.config.js to the latest structure (#652)
* Introduced tsconfig.app.json and tsconfig.base.json

* yarn.lock

* Introduced tsconfig.app.json, tsconfig.base.jsonfig.

* Refactored eslint.config.js to latest structure

* Returned previous recommended rules.

* More rules

* Force import rules

* Check

* Check

* Cleanup ESLint configuration and plugins

* Cleanup heap: "writable",DD_RUM: "writable" from ESLint configuration

* "scripts" moved to the top of package.json
2026-02-15 17:41:04 +00:00
dependabot[bot]
939dd8ac0c Bump qs from 6.14.1 to 6.14.2 in /frontend (#653)
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-14 14:11:29 +00:00
dependabot[bot]
ae598bec68 Bump diff and diff2html in /frontend (#650)
Bumps [diff](https://github.com/kpdecker/jsdiff) to 8.0.3 and updates ancestor dependency [diff2html](https://github.com/rtfpessoa/diff2html). These dependencies need to be updated together.


Updates `diff` from 7.0.0 to 8.0.3
- [Changelog](https://github.com/kpdecker/jsdiff/blob/master/release-notes.md)
- [Commits](https://github.com/kpdecker/jsdiff/compare/7.0.0...v8.0.3)

Updates `diff2html` from 3.4.52 to 3.4.56
- [Release notes](https://github.com/rtfpessoa/diff2html/releases)
- [Commits](https://github.com/rtfpessoa/diff2html/compare/3.4.52...3.4.56)

---
updated-dependencies:
- dependency-name: diff
  dependency-version: 8.0.3
  dependency-type: indirect
- dependency-name: diff2html
  dependency-version: 3.4.56
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-08 19:12:41 +00:00
yuri-sakharov
f647a3db03 Added Error Boundary (#649)
* Added Error Boundary

* Test improvements

* Introduced useDevLogger

* Updated Cypress to latest and aligned the tests

* Added eslint-enable

* Set allowCypressEnv: false for security reasons.
2026-02-08 18:22:04 +00:00
dependabot[bot]
ea7f8722ac Bump lodash from 4.17.21 to 4.17.23 in /frontend (#647)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 09:16:40 +00:00
Andrei Pohilko
4714d76784 Don't resolve 2026-01-20 11:26:25 +00:00
Andrei Pohilko
5d0bdb40c1 Merge branch 'main' of github.com:komodorio/helm-dashboard 2026-01-20 11:22:48 +00:00
Andrei Pohilko
e816f5881f Problem with dep 2026-01-20 11:22:40 +00:00
S Kumar Dhananjaya
2dfc25c038 Fix: resolve rollback to same revision bug (#569) (#646)
* added more info to features.md

* added details to FEATURES.md

* .

* reset to last commit

* Update FEATURES.md

* fix: resolve rollback to same revision bug (#578)
2026-01-18 14:17:00 +00:00
S Kumar Dhananjaya
aa2cc04084 Fix: resolve incorrect upgrade recommendation (#577) (#645)
* added more info to features.md

* added details to FEATURES.md

* .

* reset to last commit

* Update FEATURES.md

* fix: resolve incorrect upgrade recommendation (#577)
2026-01-17 16:41:09 +00:00
S Kumar Dhananjaya
65a250e2a4 Feat: add flags to disable slow health and latest version checks (#493) (#644)
* added more info to features.md

* added details to FEATURES.md

* .

* reset to last commit

* Update FEATURES.md

* feat: add flags to disable slow health and latest version checks

- Introduce --no-health and --no-latest CLI flags
- Support HD_NO_HEALTH and HD_NO_LATEST environment variables
- Skip slow k8s health checks and latest version fetching when flags are set
- Optimize frontend data fetching based on these flags

* chore: fix lint errors in InstalledPackageCard.tsx

* chore: remove accidental lockfile changes
2026-01-17 12:04:55 +00:00
Olga Kruglova
323a60fe31 Fix inconsistency in README.md (#643) 2026-01-12 08:47:12 +00:00
Vedant Apraj
37af7dfbec fix: maintain cluster context after adding repo (#616) (#641)
* fix: maintain cluster context after adding repo (#616)

* chore: rollback lock file changes as requested
2026-01-09 17:47:22 +00:00
dependabot[bot]
05c7c0b5c4 Bump react-router from 7.9.6 to 7.12.0 in /frontend (#642)
Bumps [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) from 7.9.6 to 7.12.0.
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router)

---
updated-dependencies:
- dependency-name: react-router
  dependency-version: 7.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-09 17:33:28 +00:00
dependabot[bot]
9b3fd77105 Bump qs and @cypress/request in /frontend (#640)
Bumps [qs](https://github.com/ljharb/qs) and [@cypress/request](https://github.com/cypress-io/request). These dependencies needed to be updated together.

Updates `qs` from 6.14.0 to 6.14.1
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

Updates `@cypress/request` from 3.0.9 to 3.0.10
- [Release notes](https://github.com/cypress-io/request/releases)
- [Changelog](https://github.com/cypress-io/request/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/request/compare/v3.0.9...v3.0.10)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
- dependency-name: "@cypress/request"
  dependency-version: 3.0.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-07 11:36:25 +00:00
Suhas Magadum
9f07cea128 fix: resolve undefined cluster context in navigation (#639)
* fix: resolve undefined cluster context in navigation

* fix: encode cluster context and resolve linting failures
2026-01-05 19:27:22 +00:00
Fahim muntasir
9d28119bc6 fix: prevent drawer overlay from being cut off (#617) (#637) 2025-12-27 20:14:34 +00:00
dependabot[bot]
4c0821307d Bump storybook from 10.0.8 to 10.1.10 in /frontend (#636)
Bumps [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core) from 10.0.8 to 10.1.10.
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v10.1.10/code/core)

---
updated-dependencies:
- dependency-name: storybook
  dependency-version: 10.1.10
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 22:50:15 +00:00
yuri-sakharov
077582e795 Added lazy load, visualizer and optimized bundle chunks (#635)
* Added lazy load.

* Added visualizer and improved hljs import only yaml

* Optimized manualChunks
2025-12-06 15:19:35 +00:00
yuri-sakharov
651397e2d2 Removed redundant useMemo (#634) 2025-12-06 15:16:40 +00:00
yuri-sakharov
f660411722 Added React compiler + eslint.config.ts cleanup (#633)
* Added react compiler

* Removed project field from eslint.config.js

* Cleaned up eslint.config.js

* Added comment
2025-12-06 15:16:04 +00:00
yuri-sakharov
f2eb91bc02 Enabled recommended-requiring-type-checking as result type fixes provided (#632)
* Enabled recommended-requiring-type-checking

* from .cjs to .js

* check

* check

* check

* check

* A lot of types aligned and refactored

* More strict types

* Improvement

* Improvements

* Improvements

* Fixed routs

* Fixed import types
2025-12-01 10:19:44 +02:00
yuri-sakharov
362f881b47 Fixed AddRepositoryModal borders and margin (#631) 2025-11-30 22:17:41 +00:00
yuri-sakharov
f10cc6d8a5 Added prettier-plugin-tailwindcss and sorted the classes + some small fixes (#630)
* Added prettier-plugin-tailwindcss and sorted the classes

* Added tsc into staged check + some type improvements
2025-11-30 20:11:52 +00:00
yuri-sakharov
73f74d77bb Fixed tailwindcss classes (#629) 2025-11-30 19:36:16 +00:00
yuri-sakharov
7572f00f7c Huge bump of versions + husky + fixed DropDown key issue and pointer (#628)
* Bump lint-staged

* Check

* Check

* Added husky

* Check

* Check

* Check

* Check

* Check

* Check

* Check

* Check

* Fix husky

* Used * instead **/* in lint-staged

* Bump tailwindcss and related

* Added @tailwindcss/vite and removed postcss

* Added lint into staged

* Bump @babel/core and updated .prettierignore

* Removed tailwind.config.cjs

* Added ThemeInit

* Added cursor-pointer to Help dropdown

* Bump react-router

* Removed @types/uuid and react-router-dom

* Bump diff2html, prettier, @typescript-eslint/eslint-plugin, @typescript-eslint/parser

* removed vite-plugin-html-config and @babel/core

* removed "@eslint/eslintrc" and "@eslint/js"

* Removed redundant link

* Returned plugins and source to index.css

* Set dark to false in tailwindcss

* Fixed storybook

* Fixed useGetLatestVersion with correct gcTime: 0 option

* Added eslint-plugin-prettier

* Removed spaces

* ClustersList.tsx improved and type fixes for another files

* Repository.tsx improved

* Huge fix of types

* Huge fix of types missed

* Fixed type of SingleValue

* Added cursor pointer
2025-11-29 16:49:51 +00:00
yuri-sakharov
1129651e6c Added bracketSpacing into prettier (#627) 2025-11-27 13:27:16 +02:00
yuri-sakharov
3f623458b3 Fixed queries, mutations and JSON parse (#626)
* Fixed queries ond mutations

* Fixed JSON.parse in analytics
2025-11-27 11:44:50 +02:00
yuri-sakharov
f01c19f330 Bump a lot of packages (#625)
* Bump react, luxon, swagger, uuid

* Improved imports

* Fixed vulnerabilities

* Bump highlight.js and added eslint-plugin-react-hooks with fixes

* Bump compare-versions, html-react-parser, react-intersection-observer, react-modern-drawer

* Bump @tanstack/react-query, react-select

* Added tsc:check script
2025-11-26 18:44:59 +02:00
dependabot[bot]
e50ae801a7 Bump prismjs and swagger-ui-react in /frontend (#622)
Bumps [prismjs](https://github.com/PrismJS/prism) to 1.30.0 and updates ancestor dependency [swagger-ui-react](https://github.com/swagger-api/swagger-ui). These dependencies need to be updated together.


Updates `prismjs` from 1.29.0 to 1.30.0
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/v2/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

Updates `swagger-ui-react` from 5.1.1 to 5.30.2
- [Release notes](https://github.com/swagger-api/swagger-ui/releases)
- [Changelog](https://github.com/swagger-api/swagger-ui/blob/master/.releaserc)
- [Commits](https://github.com/swagger-api/swagger-ui/compare/v5.1.1...v5.30.2)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-version: 1.30.0
  dependency-type: indirect
- dependency-name: swagger-ui-react
  dependency-version: 5.30.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 09:15:27 +02:00
yuri-sakharov
51df16e83e Bump tailwindcss related packages and updated config. Fixed Storybook! (#624)
* Updated Story book to the latest one

* Updated tailwindcss related packages and config

* Fixed Storybook

* Added missed colors

* Fixed CSS for the error dialog
2025-11-26 08:55:42 +02:00
yuri-sakharov
210a371d06 Bumped vite, eslint, typescript, prettier and related plugins versions to latest (#623)
* Bumped vite, eslint, typescript, prettier and related plugins

* Fixed unused arg

* Fixed prettier warnings
2025-11-22 08:31:28 +02:00
dependabot[bot]
40161aee12 Bump github.com/containerd/containerd from 1.7.27 to 1.7.29 (#618)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.27 to 1.7.29.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.27...v1.7.29)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-version: 1.7.29
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 14:38:41 +02:00
dependabot[bot]
71d0a4d849 Bump golang.org/x/crypto from 0.40.0 to 0.45.0 (#621)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.40.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.40.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-20 09:41:29 +02:00
DONY BENNY
1d8151d41d added uid for identification (#620) 2025-11-15 17:28:12 +00:00
dependabot[bot]
b23310cb2d Bump esbuild, vite, @storybook/addon-essentials, @storybook/addon-interactions, @storybook/addon-styling, @storybook/react, @storybook/react-vite, @vitejs/plugin-react and storybook (#615)
Bumps [esbuild](https://github.com/evanw/esbuild) to 0.25.11 and updates ancestor dependencies [esbuild](https://github.com/evanw/esbuild), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite), [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/essentials), [@storybook/addon-interactions](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/interactions), [@storybook/addon-styling](https://github.com/storybookjs/addon-styling), [@storybook/react](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/react), [@storybook/react-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/react-vite), [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) and [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/core). These dependencies need to be updated together.


Updates `esbuild` from 0.17.19 to 0.25.11
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2023.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.17.19...v0.25.11)

Updates `vite` from 5.4.21 to 7.1.11
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

Updates `@storybook/addon-essentials` from 7.5.0 to 8.6.14
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/v8.6.14/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.6.14/code/addons/essentials)

Updates `@storybook/addon-interactions` from 7.0.24 to 7.6.20
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/7.6.20/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/7.6.20/code/addons/interactions)

Updates `@storybook/addon-styling` from 1.3.2 to 2.0.0
- [Release notes](https://github.com/storybookjs/addon-styling/releases)
- [Changelog](https://github.com/storybookjs/addon-styling/blob/main/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/addon-styling/commits)

Updates `@storybook/react` from 7.5.0 to 9.1.13
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/HEAD/code/renderers/react)

Updates `@storybook/react-vite` from 7.5.0 to 9.1.13
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/HEAD/code/frameworks/react-vite)

Updates `@vitejs/plugin-react` from 3.1.0 to 5.0.4
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@5.0.4/packages/plugin-react)

Updates `storybook` from 7.5.0 to 9.1.13
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/HEAD/code/core)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.25.11
  dependency-type: indirect
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: direct:development
- dependency-name: "@storybook/addon-essentials"
  dependency-version: 8.6.14
  dependency-type: direct:development
- dependency-name: "@storybook/addon-interactions"
  dependency-version: 7.6.20
  dependency-type: direct:development
- dependency-name: "@storybook/addon-styling"
  dependency-version: 2.0.0
  dependency-type: direct:development
- dependency-name: "@storybook/react"
  dependency-version: 9.1.13
  dependency-type: direct:development
- dependency-name: "@storybook/react-vite"
  dependency-version: 9.1.13
  dependency-type: direct:development
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 5.0.4
  dependency-type: direct:development
- dependency-name: storybook
  dependency-version: 9.1.13
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-21 10:28:53 +01:00
dependabot[bot]
b5750ca40b Bump vite from 4.5.5 to 5.4.21 in /frontend (#613)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.5 to 5.4.21.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-21 10:07:44 +01:00
dependabot[bot]
63d55c1c25 Bump form-data and @cypress/request in /frontend (#611)
Bumps [form-data](https://github.com/form-data/form-data) and [@cypress/request](https://github.com/cypress-io/request). These dependencies needed to be updated together.

Updates `form-data` from 3.0.1 to 3.0.4
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/v3.0.4/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v3.0.1...v3.0.4)

Updates `@cypress/request` from 3.0.1 to 3.0.9
- [Release notes](https://github.com/cypress-io/request/releases)
- [Changelog](https://github.com/cypress-io/request/blob/master/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/request/compare/v3.0.1...v3.0.9)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 3.0.4
  dependency-type: indirect
- dependency-name: "@cypress/request"
  dependency-version: 3.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-28 11:48:25 +01:00
dependabot[bot]
756706dcd4 Bump brace-expansion from 1.1.11 to 1.1.12 in /frontend (#604)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-28 09:55:05 +01:00
dependabot[bot]
b76c4e077d Bump axios from 1.8.3 to 1.12.2 in /frontend (#610)
Bumps [axios](https://github.com/axios/axios) from 1.8.3 to 1.12.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.8.3...v1.12.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-28 09:53:39 +01:00
dependabot[bot]
5e24721801 Bump tar-fs from 2.1.3 to 2.1.4 in /frontend (#609)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.3 to 2.1.4.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.3...v2.1.4)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-28 09:35:24 +01:00
dependabot[bot]
7c8f3c29e0 Bump vite-plugin-static-copy from 0.17.0 to 2.3.2 in /frontend (#602)
Bumps [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) from 0.17.0 to 2.3.2.
- [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases)
- [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/vite-plugin-static-copy@2.3.2/CHANGELOG.md)
- [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/v0.17.0...vite-plugin-static-copy@2.3.2)

---
updated-dependencies:
- dependency-name: vite-plugin-static-copy
  dependency-version: 2.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 08:39:17 +01:00
dependabot[bot]
219e6b7392 Bump sha.js from 2.4.11 to 2.4.12 in /frontend (#603)
Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12.
- [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12)

---
updated-dependencies:
- dependency-name: sha.js
  dependency-version: 2.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 08:39:04 +01:00
dependabot[bot]
3ffdbba19b Bump helm.sh/helm/v3 from 3.18.4 to 3.18.5 (#600)
Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.18.4 to 3.18.5.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.18.4...v3.18.5)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.18.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-14 19:08:48 +01:00
dependabot[bot]
f749db9c4d Bump on-headers and compression in /frontend (#598)
Bumps [on-headers](https://github.com/jshttp/on-headers) and [compression](https://github.com/expressjs/compression). These dependencies needed to be updated together.

Updates `on-headers` from 1.0.2 to 1.1.0
- [Release notes](https://github.com/jshttp/on-headers/releases)
- [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

Updates `compression` from 1.7.4 to 1.8.1
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/1.7.4...v1.8.1)

---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-18 22:19:39 +03:00
dependabot[bot]
996f637a9d Bump helm.sh/helm/v3 from 3.18.3 to 3.18.4 (#597)
Bumps [helm.sh/helm/v3](https://github.com/helm/helm) from 3.18.3 to 3.18.4.
- [Release notes](https://github.com/helm/helm/releases)
- [Commits](https://github.com/helm/helm/compare/v3.18.3...v3.18.4)

---
updated-dependencies:
- dependency-name: helm.sh/helm/v3
  dependency-version: 3.18.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-09 08:30:54 +01:00
Andrei Pohilko
2da8f23285 Lint 2025-07-07 14:04:09 +01:00
Andrei Pohilko
f22c84c288 Fix version upgrade notifier 2025-07-07 13:37:36 +01:00
Andrei Pohilko
285cc1fe1e Encode URL for repo
Fixes #595
2025-07-07 13:37:09 +01:00
komodor-bot
96e103ff84 Increment chart versions [skip ci] 2025-07-05 09:32:26 +00:00
Andrey Pokhilko
2717734406 Fix issue (#596) 2025-07-05 09:52:47 +01:00
Andrei Pohilko
9648f5ccce Update dependencies 2025-07-05 09:45:28 +01:00
dependabot[bot]
e1e176a22b Bump @babel/runtime from 7.21.0 to 7.27.0 in /frontend (#592)
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.21.0 to 7.27.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.0/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.27.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 13:11:14 +01:00
dependabot[bot]
930eefae5d Bump tar-fs from 2.1.2 to 2.1.3 in /frontend (#593)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.2 to 2.1.3.
- [Commits](https://github.com/mafintosh/tar-fs/commits)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-06 08:31:40 +01:00
Andrey Pokhilko
aee1ac59ae Bump Golang to 1.24 (#591)
* Bump

* Update some libs

* Bump up

* Bump action

* More bumps

* Tidy
2025-04-24 10:54:40 +01:00
dependabot[bot]
cda3dd0c51 Bump tar-fs from 2.1.1 to 2.1.2 in /frontend (#589)
Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 2.1.1 to 2.1.2.
- [Commits](https://github.com/mafintosh/tar-fs/compare/v2.1.1...v2.1.2)

---
updated-dependencies:
- dependency-name: tar-fs
  dependency-version: 2.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 22:20:15 +01:00
dependabot[bot]
2439515055 Bump github.com/containerd/containerd from 1.7.13 to 1.7.27 (#582)
Bumps [github.com/containerd/containerd](https://github.com/containerd/containerd) from 1.7.13 to 1.7.27.
- [Release notes](https://github.com/containerd/containerd/releases)
- [Changelog](https://github.com/containerd/containerd/blob/main/RELEASES.md)
- [Commits](https://github.com/containerd/containerd/compare/v1.7.13...v1.7.27)

---
updated-dependencies:
- dependency-name: github.com/containerd/containerd
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 21:38:05 +01:00
Saikota
6995fe957a Fixed the issue #577: Dashboard recommends updates while running the latest version (#584) 2025-03-28 13:44:16 +00:00
Yunlu Wen
5737e8495c load resource status lazily (#583)
Co-authored-by: Yunlu Wen <yunlu.wen@transwarp.io>
2025-03-27 11:47:30 +00:00
dependabot[bot]
6517c47754 Bump axios from 1.7.7 to 1.8.3 in /frontend (#581)
Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.3.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.8.3)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 13:01:14 +00:00
dependabot[bot]
aab9411ed2 Bump @babel/runtime-corejs3 from 7.22.6 to 7.26.10 in /frontend (#578)
Bumps [@babel/runtime-corejs3](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime-corejs3) from 7.22.6 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime-corejs3)

---
updated-dependencies:
- dependency-name: "@babel/runtime-corejs3"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-13 12:27:37 +00:00
Andrei Pohilko
63eb98e309 Bump up action 2025-02-02 22:00:45 +00:00
Andrei Pohilko
5b2f1e2818 Fix trailing space 2025-02-02 21:51:17 +00:00
Andrei Pohilko
945e68590b Fix test 2025-02-02 21:22:53 +00:00
Andrei Pohilko
ee8bb96912 Use v2 2025-02-02 21:19:49 +00:00
Andrey Pokhilko
b0fb0e062b Update build.yml 2025-02-02 21:11:08 +00:00
Pedro González Serrano
dc6d781374 Fix modulepath (#575)
If the module is released at major version 2 or higher, the module path must end with a major version suffix like /v2. This may or may not be part of the subdirectory name

See https://go.dev/ref/mod#module-path
2025-02-02 21:07:37 +00:00
dependabot[bot]
d36bd6d09a Bump golang.org/x/net from 0.23.0 to 0.33.0 (#574)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.23.0 to 0.33.0.
- [Commits](https://github.com/golang/net/compare/v0.23.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 08:49:04 +00:00
dependabot[bot]
21209945f2 Bump store2 from 2.14.2 to 2.14.4 in /frontend (#573)
Bumps [store2](https://github.com/nbubna/store) from 2.14.2 to 2.14.4.
- [Commits](https://github.com/nbubna/store/compare/2.14.2...2.14.4)

---
updated-dependencies:
- dependency-name: store2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-25 08:36:27 +00:00
Idan Bunimovich
dabf99ec1f added load balancerip support (#572) 2025-01-16 09:04:05 +00:00
dependabot[bot]
13ac6385da Bump path-to-regexp and express in /frontend (#568)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `path-to-regexp` from 0.1.10 to 0.1.12
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.12)

Updates `express` from 4.21.0 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.21.0...4.21.2)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 10:54:06 +00:00
dependabot[bot]
2884712255 Bump nanoid from 3.3.6 to 3.3.8 in /frontend (#567)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.6 to 3.3.8.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.6...3.3.8)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 10:53:48 +00:00
dependabot[bot]
751746f0d2 Bump golang.org/x/crypto from 0.21.0 to 0.31.0 (#566)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.21.0 to 0.31.0.
- [Commits](https://github.com/golang/crypto/compare/v0.21.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-12 09:07:24 +00:00
Aleksandr Cupacenko
e5e15f922c refactor persistent volume configuration (#565)
Co-authored-by: Aleksandr Cupacenko <alcu@danskebank.lt>
2024-12-02 11:51:00 +00:00
komodor-bot
1a39abbdb5 Increment chart versions [skip ci] 2024-12-01 21:44:44 +00:00
Aleksandr Cupacenko
69fe906c7d fix context decoding issue in Installed component (#563)
remove redundant code

correct encode decode

Co-authored-by: Aleksandr Cupacenko <alcu@danskebank.lt>
2024-11-26 16:15:32 +00:00
Aleksandr Cupacenko
3b0b44f392 refactor helm-dashboard.fullname template (#562)
fix ci

Co-authored-by: Aleksandr Cupacenko <alcu@danskebank.lt>
2024-11-25 10:46:36 +00:00
Aleksandr Cupacenko
922bb1c7c2 update service acccount name template logic (#561)
Co-authored-by: Aleksandr Cupacenko <alcu@danskebank.lt>
2024-11-22 09:29:55 +00:00
Aleksandr Cupacenko
f85343a173 add image digest field (#560)
Co-authored-by: Aleksandr Cupacenko <alcu@danskebank.lt>
2024-11-21 13:22:37 +00:00
dependabot[bot]
14fa9b8894 Bump cross-spawn from 7.0.3 to 7.0.6 in /frontend (#559)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)

---
updated-dependencies:
- dependency-name: cross-spawn
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-21 11:05:52 +00:00
Aleksandr Cupacenko
0436eabb51 simplify image tag assignment (#558)
Co-authored-by: Aleksandr Cupacenko <alcu@danskebank.lt>
2024-11-20 10:45:25 +00:00
komodor-bot
fb39d7e324 Increment chart versions [skip ci] 2024-11-13 11:38:40 +00:00
Andrei Pohilko
f1747b41d7 Clean yarn.lock before release 2024-11-13 11:21:03 +00:00
162 changed files with 19118 additions and 35652 deletions

View File

@@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# Node part
- name: Setup Node.js environment
uses: actions/setup-node@v2.5.2
uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
@@ -36,7 +36,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
go-version: "1.24"
- name: Unit tests
run: |
go test -v -race ./... -covermode=atomic -coverprofile=coverage.out # Run all the tests with the race detector enabled
@@ -56,7 +56,7 @@ jobs:
args: release --snapshot --clean
- name: Test if the Binary is Runnable
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: binaries
path: dist/
@@ -67,7 +67,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check out the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker meta
uses: docker/metadata-action@v3
@@ -101,7 +101,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -110,7 +110,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
go-version: "1.24"
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
with:
@@ -120,7 +120,7 @@ jobs:
args: --timeout=5m
- name: Setup Node.js environment
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: ./frontend/package-lock.json
@@ -132,7 +132,7 @@ jobs:
working-directory: ./frontend
- name: Helm Template Check For Sanity
uses: igabaydulin/helm-check-action@0.1.4
uses: igabaydulin/helm-check-action@0.2.1
env:
CHART_LOCATION: ./charts/helm-dashboard
CHART_VALUES: ./charts/helm-dashboard/values.yaml

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Bump versions

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get tag name
@@ -29,11 +29,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
# Node part
- name: Setup Node.js environment
uses: actions/setup-node@v2.5.2
uses: actions/setup-node@v4
with:
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
@@ -48,9 +48,9 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"
go-version: "1.24"
- name: git cleanup
run: git clean -f
run: git clean -f && git checkout frontend/yarn.lock
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:
@@ -67,7 +67,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check out the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Docker meta
uses: docker/metadata-action@v3
@@ -104,7 +104,7 @@ jobs:
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Bump versions

1
.gitignore vendored
View File

@@ -31,3 +31,4 @@ go.work
.vscode/
/pkg/dashboard/objects/testdata/hello-world-0.1.0.tgz
/pkg/frontend/dist/*
/dist/

2
.husky/pre-commit Executable file
View File

@@ -0,0 +1,2 @@
cd frontend || exit 1
npm run pre:commit

View File

@@ -84,7 +84,7 @@ If your port 8080 is busy, you can specify a different port to use via `--port <
If you need to limit the operations to a specific namespace, please use `--namespace=...` in your command-line. You can specify multiple namespaces, separated by commas.
If you don't want the browser tab to automatically open, add `--no-browser` flag in your command line.
If you don't want the browser tab to automatically open, add `--no-browser` flag in your command-line.
If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag.

View File

@@ -5,5 +5,5 @@ name: helm-dashboard
description: A GUI Dashboard for Helm by Komodor
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/refs/heads/main/images/logo.svg"
version: 2.0.1
appVersion: "1.3.3"
version: 2.0.5
appVersion: "2.1.0"

View File

@@ -75,7 +75,8 @@ The following table lists the configurable parameters of the chart and their def
| `dashboard.persistence.accessModes` | Persistent Volume access modes | `["ReadWriteOnce"]` |
| `dashboard.persistence.storageClass` | Persistent Volume storage class | `""` |
| `dashboard.persistence.size` | Persistent Volume size | `100M` |
| `dashboard.persistence.hostPath` | Set path in case you want to use local host path volumes (not recommended in production) | `""` |
| `dashboard.persistence.finalizers` | Finalizers for the Persistent Volume Claim | `[kubernetes.io/pvc-protection]` |
| `dashboard.persistence.lookupVolumeName` | Lookup volume name for the Persistent Volume Claim | `true` |
| `updateStrategy.type` | Set up update strategy for helm-dashboard installation. | `RollingUpdate` |
| `extraArgs` | Set the arguments to be supplied to the helm-dashboard binary | `[--no-browser, --bind=0.0.0.0]` |
| `testImage.repository` | Test image registry/name | `busybox` |

View File

@@ -11,16 +11,9 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this
If release name contains chart name it will be used as a full name.
*/}}
{{- define "helm-dashboard.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- $fullname := default (ternary .Release.Name (printf "%s-%s" .Release.Name $name) (contains $name .Release.Name)) .Values.fullnameOverride }}
{{- $fullname | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
@@ -54,11 +47,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
Create the name of the service account to use
*/}}
{{- define "helm-dashboard.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "helm-dashboard.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- default (.Values.serviceAccount.create | ternary (include "helm-dashboard.fullname" .) "default") .Values.serviceAccount.name }}
{{- end }}
{{/*
@@ -74,10 +63,7 @@ Return the proper image name
*/}}
{{- define "helm-dashboard.image" -}}
{{- $image := .Values.image -}}
{{- $tag := .Chart.AppVersion -}}
{{- if $image.tag -}}
{{- $tag = $image.tag -}}
{{- end -}}
{{- $tag := default .Chart.AppVersion $image.tag -}}
{{- $_ := set $image "tag" $tag -}}
{{ include "common.images.image" (dict "imageRoot" $_ "global" .Values.global) }}
{{- end -}}

View File

@@ -10,47 +10,22 @@ metadata:
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.dashboard.persistence.hostPath }}
storageClassName: ""
{{- else }}
{{- if kindIs "string" .Values.dashboard.persistence.storageClass }}
storageClassName: "{{ .Values.dashboard.persistence.storageClass }}"
{{- end }}
{{- end }}
accessModes:
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
{{- range .Values.dashboard.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
{{- end }}
resources:
requests:
storage: {{ .Values.dashboard.persistence.size | quote }}
{{- end }}
---
{{- if and .Values.dashboard.persistence.enabled .Values.dashboard.persistence.hostPath -}}
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ include "helm-dashboard.fullname" . }}
namespace: {{ .Release.Namespace | quote }}
labels:
{{- include "helm-dashboard.labels" . | nindent 4 }}
{{- with .Values.dashboard.persistence.annotations }}
annotations:
{{- with .Values.dashboard.persistence.finalizers }}
finalizers:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
accessModes:
{{- if not (empty .Values.dashboard.persistence.accessModes) }}
{{- range .Values.dashboard.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.dashboard.persistence.size | quote }}
{{- if and (.Values.dashboard.persistence.lookupVolumeName) (lookup "v1" "PersistentVolumeClaim" .Release.Namespace (include "helm-dashboard.fullname" .)) }}
volumeName: {{ (lookup "v1" "PersistentVolumeClaim" .Release.Namespace (include "helm-dashboard.fullname" .)).spec.volumeName }}
{{- end }}
capacity:
storage: {{ .Values.dashboard.persistence.size | quote }}
hostPath:
path: {{ .Values.dashboard.persistence.hostPath | quote }}
{{- end -}}
{{- with .Values.dashboard.persistence.storageClassName }}
storageClassName: {{ . }}
{{- end }}
{{- end }}

View File

@@ -13,3 +13,6 @@ spec:
name: http
selector:
{{- include "helm-dashboard.selectorLabels" . | nindent 4 }}
{{- if .Values.service.loadBalancerIP }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}

View File

@@ -17,6 +17,8 @@ image:
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# Specifies the exact image digest to pull.
digest: ""
imagePullSecrets: []
nameOverride: ""
@@ -47,12 +49,11 @@ dashboard:
enabled: true
## If defined, storageClassName: <storageClass>
## If set to "-", storageClassName: "", which disables dynamic provisioning
## If undefined (the default) or set to null, no storageClassName spec is
## set, choosing the default provisioner. (gp2 on AWS, standard on
## GKE, AWS & OpenStack)
##
storageClass: null
# storageClassName: default
## Helm Dashboard Persistent Volume access modes
## Must match those of existing PV or dynamic provisioner
@@ -69,14 +70,19 @@ dashboard:
##
annotations: {}
## Set path in case you want to use local host path volumes (not recommended in production)
## Finalizer to ensure PVC is not deleted until the pod is terminated
##
hostPath: ""
finalizers:
- kubernetes.io/pvc-protection
## Helm Dashboard data Persistent Volume size
##
size: 100M
## If 'lookupVolumeName' is set to true, Helm will attempt to retrieve
## the current value of 'spec.volumeName' and incorporate it into the template.
lookupVolumeName: true
## @param.updateStrategy.type Set up update strategy for helm-dashboard installation.
## Set to Recreate if you use persistent volume that cannot be mounted by more than one pods to make sure the pods is destroyed first.
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
@@ -100,6 +106,7 @@ securityContext: {}
service:
type: ClusterIP
port: 8080
loadBalancerIP: null
ingress:
enabled: false

View File

@@ -8,8 +8,8 @@ WORKING_DIRECTORY="$PWD"
APP_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'appVersion:' | awk -F'"' '{print $2}')
}
sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\" /g" ${HELM_CHARTS_SOURCE}/Chart.yaml
sed -i -e "s/version.*/version: \"${APP_VERSION}\" /g" plugin.yaml
sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\"/g" ${HELM_CHARTS_SOURCE}/Chart.yaml
sed -i -e "s/version.*/version: \"${APP_VERSION}\"/g" plugin.yaml
CURRENT_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'version:' | awk '{print $2}')
NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')
sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" ${HELM_CHARTS_SOURCE}/Chart.yaml

View File

@@ -1,44 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ["enpitech", "plugin:@typescript-eslint/recommended"],
globals: {
heap: "writable",
DD_RUM: "writable",
},
overrides: [
{
env: {
node: true,
},
files: [".eslintrc.{js,cjs}"],
parserOptions: {
sourceType: "script",
},
},
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: ["@typescript-eslint", "react"],
rules: {
// please don't make an error occur here we use console.error
"no-console": ["error", { allow: ["error"] }],
"no-alert": "error",
"no-debugger": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ vars: "all", args: "after-used", ignoreRestSiblings: true },
],
"react/react-in-jsx-scope": "off", // Vite does not require you to import React into each component file
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"@typescript-eslint/no-explicit-any": "warn",
},
};

2
frontend/.flowbite-react/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
class-list.json
pid

View File

@@ -0,0 +1,10 @@
{
"$schema": "https://unpkg.com/flowbite-react/schema.json",
"components": [],
"dark": false,
"path": "src/components",
"prefix": "",
"rsc": true,
"tsx": true,
"version": 4
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore-all lint: auto-generated file
// This file is auto-generated by the flowbite-react CLI.
// Do not edit this file directly.
// Instead, edit the .flowbite-react/config.json file.
import { StoreInit } from "flowbite-react/store/init";
import React from "react";
export const CONFIG = {
dark: false,
prefix: "",
version: 4,
};
export function ThemeInit() {
return <StoreInit {...CONFIG} />;
}
ThemeInit.displayName = "ThemeInit";

View File

@@ -1,3 +1,10 @@
# Ignore artifacts:
build
coverage
.env
.gitignore
.npmrc
.prettierignore
yarn.lock
package-lock.json
.flowbite-react/*

View File

@@ -2,3 +2,7 @@ trailingComma: "es5"
tabWidth: 2
semi: true
singleQuote: false
bracketSpacing: true
plugins:
- "prettier-plugin-tailwindcss" # should be last https://github.com/tailwindlabs/prettier-plugin-tailwindcss?tab=readme-ov-file#compatibility-with-other-prettier-plugins
tailwindStylesheet: "./src/index.css"

View File

@@ -1,32 +0,0 @@
module.exports = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: "@storybook/react",
core: {
builder: "@storybook/builder-vite",
},
webpackFinal: async (config) => {
config.module.rules.push({
test: /\.css$/,
use: [
{
loader: "postcss-loader",
options: {
postcssOptions: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
},
],
include: path.resolve(__dirname, "../"),
});
return config;
},
features: {
storyStoreV7: true,
},
};

View File

@@ -1,25 +1,15 @@
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-actions",
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-styling",
{
name: "@storybook/addon-styling",
},
"@storybook/addon-mdx-gfm",
],
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx|mdx)"],
addons: ["@storybook/addon-links", "@storybook/addon-docs"],
core: {},
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: true,
},
};
export default config;

View File

@@ -1,12 +0,0 @@
import "../src/index.css";
import "tailwindcss/tailwind.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

View File

@@ -1,12 +0,0 @@
import "tailwindcss/tailwind.css";
import "../src/index.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};

View File

@@ -0,0 +1,33 @@
import "../src/index.css";
import { BrowserRouter } from "react-router";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { Preview, StoryFn } from "@storybook/react";
import { AppContextProvider } from "../src/context/AppContext";
const queryClient = new QueryClient();
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const decorators: Preview["decorators"] = [
(Story: StoryFn) => (
<BrowserRouter>
<AppContextProvider>
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
</AppContextProvider>
</BrowserRouter>
),
];
export const tags = ["autodocs"];

View File

@@ -2,6 +2,7 @@
Welcome to the frontend of the helm dashboard.
We care most about keeping the project:
1. Maintainable
2. Extendable
3. Contributor friendly
@@ -28,12 +29,11 @@ Please follow through the file structure to understand how things are structured
1. Make sure you cloned the project correctly. This is explained in this [stage](https://github.com/komodorio/helm-dashboard/blob/helm-dashboard-v2/dashboard/README.md#setting-up-your-development-environment).
2. run the backend server. This is also explained in the above link.
2. go to `frontend` in your local project.
3. in order to install dependencies and start the development server
3. go to `frontend` in your local project.
4. in order to install dependencies and start the development server
- `npm i`
- `npm run dev`
4. with the default integration the dashboard should run on http://localhost:5173/
5. with the default integration the dashboard should run on http://localhost:5173/
# Component library

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "cypress";
export default defineConfig({
allowCypressEnv: false,
component: {
devServer: {
framework: "react",
@@ -9,8 +10,9 @@ export default defineConfig({
},
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
baseUrl: "http://localhost:5173",
// setupNodeEvents(on, config) {
// // implement node event listeners here
// },
},
});

View File

@@ -4,17 +4,15 @@ describe("Adding repository flow", () => {
const addChartRepositoryButton = "[data-cy='add-chart-repository-button']";
it("Adding new chart repository", () => {
cy.intercept("GET", "http://localhost:5173/status", {
cy.intercept("GET", "/status", {
fixture: "status.json",
}).as("status");
cy.intercept("GET", "http://localhost:5173/api/helm/releases", {
cy.intercept("GET", "/api/helm/releases", {
fixture: "releases.json",
}).as("releases");
cy.visit(
"http://localhost:5173/#/minikube/installed?filteredNamespace=default"
);
cy.visit("/#/minikube/installed?filteredNamespace=default");
cy.get("[data-cy='navigation-link']").contains("Repository").click();
cy.get("[data-cy='install-repository-button']").click();
@@ -22,11 +20,12 @@ describe("Adding repository flow", () => {
cy.get(addChartNameInput).type("Komodorio");
cy.get(addChartUrlInput).type("https://helm-charts.komodor.io");
cy.intercept("GET", "http://localhost:5173/api/helm/repositories", {
cy.intercept("GET", "/api/helm/repositories", {
fixture: "repositories.json",
}).as("repositories");
cy.get(addChartRepositoryButton).click();
cy.wait("@repositories");
cy.contains("https://helm-charts.komodor.io");
@@ -36,15 +35,13 @@ describe("Adding repository flow", () => {
.contains("Install")
.click();
cy.intercept("POST", "http://localhost:5173/api/helm/releases/default", {
cy.intercept("POST", "/api/helm/releases/default", {
fixture: "defaultReleases.json",
}).as("defaultReleases");
cy.intercept(
"GET",
"http://localhost:5173/api/helm/releases/default/helm-dashboard/history",
{ fixture: "history.json" }
).as("history");
cy.intercept("GET", "/api/helm/releases/default/helm-dashboard/history", {
fixture: "history.json",
}).as("history");
cy.contains("Confirm").click();

View File

@@ -74,12 +74,14 @@
},
"enabled":true,
"hostPath":"",
"labels":{
},
"size":"100M",
"storageClass":null
"finalizers":[
"kubernetes.io/pvc-protection"
],
"lookupVolumeName": true
}
},
"debug":false,

View File

@@ -1,6 +1,7 @@
import "./commands";
import { mount } from "cypress/react18";
import { mount } from "cypress/react";
/* eslint-disable @typescript-eslint/no-namespace */
declare global {
namespace Cypress {
interface Chainable {
@@ -8,5 +9,5 @@ declare global {
}
}
}
Cypress.Commands.add("mount", mount);
/* eslint-enable @typescript-eslint/no-namespace */
Cypress.Commands.add("mount", mount);

119
frontend/eslint.config.js Normal file
View File

@@ -0,0 +1,119 @@
import js from "@eslint/js";
import { defineConfig } from "eslint/config";
import globals from "globals";
import { configs as tseslintConf } from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import { flatConfigs as importXFlatConf } from "eslint-plugin-import-x";
import prettierRecommended from "eslint-plugin-prettier/recommended";
// import tscPlugin from "eslint-plugin-tsc";
export default defineConfig(
{ ignores: ["dist", "node_modules"] },
js.configs.recommended,
tseslintConf.recommendedTypeChecked,
// tsEslint.configs.strictTypeChecked, // The project is not ready yet
// tsEslint.configs.stylisticTypeChecked, // Added for better 2026 coding standards, however the project is not ready yet
importXFlatConf.recommended,
importXFlatConf.typescript,
react.configs.flat.recommended,
react.configs.flat["jsx-runtime"],
reactHooks.configs.flat.recommended,
prettierRecommended,
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parserOptions: {
projectService: {
allowDefaultProject: ["eslint.config.js"],
},
tsconfigRootDir: import.meta.dirname,
},
},
settings: {
react: { version: "detect" },
"import-x/resolver": {
node: true,
typescript: {
alwaysTryTypes: true,
project: "./tsconfig.json",
},
},
},
// plugins: {
// tsc: tscPlugin,
// },
rules: {
/* ───────── Base Overrides ───────── */
"no-console": ["error", { allow: ["error", "warn"] }],
"no-debugger": "error",
quotes: ["error", "double"],
semi: ["error", "always"],
/* ───────── Import Precision ───────── */
"import-x/no-duplicates": ["error", { "prefer-inline": true }],
/* ───────── React Precision ───────── */
"no-restricted-properties": [
"error",
{
object: "React",
message:
"Use named imports instead (e.g. import { useState } from 'react')",
},
],
"no-restricted-imports": [
"error",
{
name: "react",
importNames: ["default"],
message: "Default React imports are prohibited. Use named imports.",
},
],
/* ───────── TypeScript & Verbatim Syntax ───────── */
"@typescript-eslint/consistent-type-imports": [
"error",
{
prefer: "type-imports",
fixStyle: "inline-type-imports",
},
],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_" },
],
"@typescript-eslint/no-restricted-types": [
"error",
{
types: {
"React.FC": "Use 'import type { FC }' instead.",
"React.ReactNode": "Use 'import type { ReactNode }' instead.",
// FC: "Avoid FC (Functional Component) type; prefer explicit return types.",
},
},
],
},
},
{
files: ["**/*.{js,mjs}"],
...tseslintConf.disableTypeChecked,
languageOptions: {
globals: {
...globals.node,
},
},
},
{
files: ["eslint.config.js"],
rules: { "import-x/no-unresolved": "off" },
}
);

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/assets/logo.svg" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Helm Dashboard</title>
<script src="/assets/analytics.js"></script>
<script type="module" src="/analytics.js"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css"

34149
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +1,101 @@
{
"name": "dashboard",
"version": "1.0.0",
"version": "1.1.0",
"type": "module",
"description": "",
"main": "index.js",
"dependencies": {
"@tanstack/react-query": "^4.35.3",
"@types/luxon": "^3.3.0",
"@types/marked": "^5.0.0",
"compare-versions": "^6.0.0-rc.2",
"diff2html": "^3.4.46",
"eslint-config-enpitech": "^1.0.9",
"flowbite": "^1.6.6",
"flowbite-react": "^0.4.9",
"highlight.js": "^11.8.0",
"html-react-parser": "^4.0.0",
"luxon": "^3.3.0",
"marked": "^5.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.10",
"react-icons": "^4.8.0",
"react-modern-drawer": "^1.2.0",
"react-router-dom": "^6.9.0",
"react-select": "^5.7.4",
"swagger-ui-react": "^5.1.1",
"uuid": "^9.0.1",
"vite-plugin-static-copy": "^0.17.0"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@storybook/addon-actions": "^7.0.24",
"@storybook/addon-essentials": "^7.0.24",
"@storybook/addon-interactions": "^7.0.24",
"@storybook/addon-links": "^7.0.24",
"@storybook/addon-mdx-gfm": "7.0.24",
"@storybook/addon-styling": "^1.3.2",
"@storybook/react": "^7.0.24",
"@storybook/react-vite": "7.5.0",
"@storybook/testing-library": "^0.2.0",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/react": "^18.0.27",
"@types/react-dom": "^18.0.10",
"@types/swagger-ui-react": "^4.18.0",
"@types/uuid": "^9.0.4",
"@typescript-eslint/eslint-plugin": "^6.2.1",
"@typescript-eslint/parser": "^6.2.1",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.14",
"cypress": "^13.3.0",
"eslint": "^8.46.0",
"eslint-config-prettier": "^8.7.0",
"eslint-plugin-react": "^7.33.1",
"eslint-plugin-storybook": "^0.6.12",
"lint-staged": "^13.2.3",
"postcss": "^8.4.24",
"prettier": "2.8.4",
"react-icons": "^4.8.0",
"storybook": "7.5.0",
"tailwindcss": "^3.3.2",
"typescript": "^4.9.5",
"vite": "^4.1.0",
"vite-plugin-html-config": "^1.0.11"
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "cd .. && husky",
"pre:commit": "lint-staged",
"test": "echo \"Error: no test specified. Please use 'cypress:run' or 'cypress:open' commands\" && exit 1",
"tsc": "tsc",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"storybook:build": "storybook build",
"lint": "npx eslint src/",
"lint:fix": "npm run lint -- --fix",
"lint:fix": "npm run lint -- --fix --max-warnings=0",
"prettier": "npx prettier src/ --check",
"prettier:fix": "npm run prettier -- --write",
"cypress:open": "cypress open",
"cypress:run": "cypress run"
"cypress:run": "cypress run",
"cypress:component": "cypress run --component",
"cypress:component:open": "cypress open --component"
},
"dependencies": {
"@dagrejs/dagre": "^2.0.4",
"@tanstack/react-query": "^5.90.21",
"@types/d3-force": "^3.0.10",
"@xyflow/react": "^12.10.1",
"compare-versions": "^6.1.1",
"d3-force": "^3.0.0",
"diff2html": "^3.4.52",
"flowbite-react": "^0.12.17",
"highlight.js": "^11.11.1",
"html-react-parser": "^5.2.17",
"luxon": "^3.7.2",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-error-boundary": "^6.1.1",
"react-icons": "^5.5.0",
"react-intersection-observer": "^10.0.3",
"react-modern-drawer": "^1.4.0",
"react-router": "^7.13.0",
"react-select": "^5.10.2",
"swagger-ui-react": "^5.31.2",
"uuid": "^13.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@storybook/addon-docs": "^10.2.10",
"@storybook/addon-links": "^10.2.10",
"@storybook/mdx2-csf": "^1.1.0",
"@storybook/react-vite": "^10.2.10",
"@tailwindcss/vite": "^4.2.0",
"@types/luxon": "^3.7.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/swagger-ui-react": "^5.18.0",
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.0",
"@vitejs/plugin-react": "^5.1.4",
"autoprefixer": "^10.4.24",
"babel-plugin-react-compiler": "^1.0.0",
"cypress": "15.10.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.5.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-storybook": "^10.2.10",
"eslint-plugin-tsc": "^2.0.0",
"flowbite": "^4.0.1",
"globals": "^17.3.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"rollup-plugin-visualizer": "^7.0.0",
"storybook": "^10.2.10",
"tailwindcss": "^4.2.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
"vite-plugin-static-copy": "^3.2.0"
},
"overrides": {
"minimatch": "^10.2.2"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": "npm run lint:fix",
"*.{js,jsx,ts,tsx,json,css,md,mdx}": "npm run prettier:fix"
},
"resolutions": {
"dompurify": "^3.3.2"
},
"keywords": [],
"author": "",

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -13,8 +13,24 @@ const BASE_ANALYTIC_MSG = {
referrerPolicy: "no-referrer"
};
xhr.onload = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
const status = JSON.parse(xhr.responseText);
if (xhr.readyState !== XMLHttpRequest.DONE) {
return;
}
const responseTxt = xhr.responseText?.trim();
if (!responseTxt) {
console.warn("Analytics response is empty");
return;
}
let status;
try {
status = JSON.parse(responseTxt);
} catch (e) {
console.error("Failed to parse JSON: ", xhr.responseText, e);
return;
}
const version = status.CurVer;
if (status.Analytics) {
enableDD(version);
@@ -23,7 +39,6 @@ xhr.onload = function() {
} else {
console.log("Analytics is disabled in this session");
}
}
};
xhr.open("GET", "/status", true);
xhr.send(null);

View File

@@ -1,12 +1,13 @@
import {
import { type QueryFunctionContext } from "@tanstack/react-query";
import type {
Chart,
ChartVersion,
Release,
ReleaseHealthStatus,
ReleaseRevision,
Repository,
} from "../data/types";
import { type QueryFunctionContext } from "@tanstack/react-query";
interface ClustersResponse {
AuthInfo: string;
Cluster: string;
@@ -25,7 +26,7 @@ class ApiService {
public async fetchWithDefaults<T>(
url: string,
options?: RequestInit
): Promise<T> {
): Promise<T | string> {
let response;
if (this.currentCluster) {
@@ -43,59 +44,80 @@ class ApiService {
throw new Error(error);
}
let data;
if (!response.headers.get("Content-Type")) {
return {} as T;
} else if (response.headers.get("Content-Type")?.includes("text/plain")) {
data = await response.text();
const contentType = response.headers.get("Content-Type") || "";
if (!contentType) {
return {} as unknown as T;
} else if (contentType.includes("text/plain")) {
return await response.text();
} else {
data = await response.json();
return (await response.json()) as T;
}
}
public async fetchWithSafeDefaults<T>({
url,
options,
fallback,
}: {
url: string;
options?: RequestInit;
fallback: T;
}): Promise<T> {
const data = await this.fetchWithDefaults<T>(url, options);
if (!data) {
console.error(url, " response is empty");
return fallback;
}
if (typeof data === "string") {
console.error(url, " response is string");
return fallback;
}
return data;
}
getToolVersion = async () => {
const response = await fetch("/status");
const data = await response.json();
return data;
return await this.fetchWithDefaults("/status");
};
getRepositoryLatestVersion = async (repositoryName: string) => {
const data = await this.fetchWithDefaults(
return await this.fetchWithDefaults(
`/api/helm/repositories/latestver?name=${repositoryName}`
);
return data;
};
getInstalledReleases = async () => {
const data = await this.fetchWithDefaults("/api/helm/releases");
return data;
return await this.fetchWithDefaults("/api/helm/releases");
};
getClusters = async () => {
const response = await fetch("/api/k8s/contexts");
const data = (await response.json()) as ClustersResponse[];
return data;
getClusters = async (): Promise<ClustersResponse[]> => {
return await this.fetchWithSafeDefaults<ClustersResponse[]>({
url: "/api/k8s/contexts",
fallback: [],
});
};
getNamespaces = async () => {
const data = await this.fetchWithDefaults("/api/k8s/namespaces/list");
return data;
return await this.fetchWithDefaults("/api/k8s/namespaces/list");
};
getRepositories = async () => {
const data = await this.fetchWithDefaults("/api/helm/repositories");
return data;
return await this.fetchWithDefaults("/api/helm/repositories");
};
getRepositoryCharts = async ({
queryKey,
}: QueryFunctionContext<Chart[], Repository>) => {
}: {
queryKey: readonly unknown[];
}): Promise<Chart[]> => {
const [, repository] = queryKey;
const data = await this.fetchWithDefaults(
`/api/helm/repositories/${repository}`
);
return data;
if (!repository || typeof repository !== "string") {
return [];
}
const url = `/api/helm/repositories/${repository}`;
return await this.fetchWithSafeDefaults<Chart[]>({ url, fallback: [] });
};
getChartVersions = async ({
@@ -103,39 +125,37 @@ class ApiService {
}: QueryFunctionContext<ChartVersion[], Chart>) => {
const [, chart] = queryKey;
const data = await this.fetchWithDefaults(
return await this.fetchWithDefaults(
`/api/helm/repositories/versions?name=${chart.name}`
);
return data;
};
getResourceStatus = async ({
release,
}: {
release: Release;
}): Promise<ReleaseHealthStatus[] | null> => {
if (!release) return null;
}): Promise<ReleaseHealthStatus[]> => {
if (!release) return [];
const data = await this.fetchWithDefaults<
Promise<ReleaseHealthStatus[] | null>
>(
`/api/helm/releases/${release.namespace}/${release.name}/resources?health=true`
);
return data;
return await this.fetchWithSafeDefaults<ReleaseHealthStatus[]>({
url: `/api/helm/releases/${release.namespace}/${release.name}/resources?health=true`,
fallback: [],
});
};
getReleasesHistory = async ({
queryKey,
}: QueryFunctionContext<Release[], Release>): Promise<ReleaseRevision[]> => {
}: {
queryKey: readonly [string, Record<string, string | undefined>];
}): Promise<ReleaseRevision[]> => {
const [, params] = queryKey;
if (!params.namespace || !params.chart) return [];
const data = await this.fetchWithDefaults<ReleaseRevision[]>(
`/api/helm/releases/${params.namespace}/${params.chart}/history`
);
return data;
return await this.fetchWithSafeDefaults<ReleaseRevision[]>({
url: `/api/helm/releases/${params.namespace}/${params.chart}/history`,
fallback: [],
});
};
getValues = async ({
@@ -143,7 +163,7 @@ class ApiService {
}: {
queryKey: [
string,
{ namespace: string; chart: { name: string }; version: number }
{ namespace: string; chart: { name: string }; version: number },
];
}) => {
const [, params] = queryKey;
@@ -153,9 +173,7 @@ class ApiService {
return Promise.reject(new Error("missing parameters"));
const url = `/api/helm/repositories/values?chart=${namespace}/${chart.name}&version=${version}`;
const data = await this.fetchWithDefaults(url);
return data;
return await this.fetchWithDefaults(url);
};
}

View File

@@ -43,6 +43,8 @@ export interface ApplicationStatus {
ClusterMode: boolean;
CurVer: string;
LatestVer: string;
NoHealth: boolean;
NoLatest: boolean;
}
export interface KubectlContexts {

View File

@@ -1,57 +1,74 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
import { K8sResource, K8sResourceList, KubectlContexts } from "./interfaces";
import apiService from "./apiService";
import type {
K8sResource,
K8sResourceList,
KubectlContexts,
} from "./interfaces";
// Get list of kubectl contexts configured locally
// @ts-expect-error unused
function useGetKubectlContexts(options?: UseQueryOptions<KubectlContexts>) {
return useQuery<KubectlContexts>(
["k8s", "contexts"],
() => apiService.fetchWithDefaults<KubectlContexts>("/api/k8s/contexts"),
options
);
return useQuery<KubectlContexts>({
queryKey: ["k8s", "contexts"],
queryFn: () =>
apiService.fetchWithSafeDefaults<KubectlContexts>({
url: "/api/k8s/contexts",
fallback: { contexts: [] },
}),
...(options ?? {}),
});
}
// Get resources information
// @ts-expect-error unused
function useGetK8sResource(
kind: string,
name: string,
namespace: string,
options?: UseQueryOptions<K8sResource>
) {
return useQuery<K8sResource>(
["k8s", kind, "get", name, namespace],
() =>
apiService.fetchWithDefaults<K8sResource>(
`/api/k8s/${kind}/get?name=${name}&namespace=${namespace}`
),
options
);
return useQuery<K8sResource>({
queryKey: ["k8s", kind, "get", name, namespace],
queryFn: () =>
apiService.fetchWithSafeDefaults<K8sResource>({
url: `/api/k8s/${kind}/get?name=${name}&namespace=${namespace}`,
fallback: { kind: "", name: "", namespace: "" },
}),
...(options ?? {}),
});
}
// Get list of resources
// @ts-expect-error unused
function useGetK8sResourceList(
kind: string,
options?: UseQueryOptions<K8sResourceList>
) {
return useQuery<K8sResourceList>(
["k8s", kind, "list"],
() =>
apiService.fetchWithDefaults<K8sResourceList>(`/api/k8s/${kind}/list`),
options
);
return useQuery<K8sResourceList>({
queryKey: ["k8s", kind, "list"],
queryFn: () =>
apiService.fetchWithSafeDefaults<K8sResourceList>({
url: `/api/k8s/${kind}/list`,
fallback: { items: [] },
}),
...(options ?? {}),
});
}
// Get describe text for kubernetes resource
// @ts-expect-error unused
function useGetK8sResourceDescribe(
kind: string,
name: string,
namespace: string,
options?: UseQueryOptions<string>
) {
return useQuery<string>(
["k8s", kind, "describe", name, namespace],
() =>
return useQuery<string>({
queryKey: ["k8s", kind, "describe", name, namespace],
queryFn: () =>
apiService.fetchWithDefaults<string>(
`/api/k8s/${kind}/describe?name=${name}&namespace=${namespace}`,
{
@@ -60,6 +77,6 @@ function useGetK8sResourceDescribe(
},
}
),
options
);
...(options ?? {}),
});
}

View File

@@ -4,31 +4,35 @@ import {
useMutation,
useQuery,
} from "@tanstack/react-query";
import { ApplicationStatus } from "./interfaces";
import apiService from "./apiService";
import type { ApplicationStatus } from "./interfaces";
// Shuts down the Helm Dashboard application
export function useShutdownHelmDashboard(
options?: UseMutationOptions<void, Error>
options?: UseMutationOptions<string, Error>
) {
return useMutation<void, Error>(
() =>
return useMutation<string, Error>({
mutationFn: () =>
apiService.fetchWithDefaults("/", {
method: "DELETE",
}),
options
);
...(options ?? {}),
});
}
// Gets application status
export function useGetApplicationStatus(
options?: UseQueryOptions<ApplicationStatus>
options?: UseQueryOptions<ApplicationStatus | null>
) {
return useQuery<ApplicationStatus>(
["status"],
() => apiService.fetchWithDefaults<ApplicationStatus>("/status"),
{
...options,
}
);
return useQuery<ApplicationStatus | null>({
queryKey: ["status"],
queryFn: async () =>
await apiService.fetchWithSafeDefaults<ApplicationStatus | null>({
url: "/status",
fallback: null,
}),
...(options ?? {}),
});
}

View File

@@ -1,24 +1,29 @@
import {
useQuery,
type UseQueryOptions,
useMutation,
type UseMutationOptions,
useQuery,
type UseQueryOptions,
} from "@tanstack/react-query";
import { ChartVersion, Release } from "../data/types";
import { LatestChartVersion } from "./interfaces";
import type { ChartVersion, Release } from "../data/types";
import { isNewerVersion } from "../utils";
import apiService from "./apiService";
import type { LatestChartVersion } from "./interfaces";
import { getVersionManifestFormData } from "./shared";
export const HD_RESOURCE_CONDITION_TYPE = "hdHealth"; // it's our custom condition type, only one exists
export function useGetInstalledReleases(
context: string,
options?: UseQueryOptions<Release[]>
) {
return useQuery<Release[]>(
["installedReleases", context],
() => apiService.fetchWithDefaults<Release[]>("/api/helm/releases"),
options
);
export function useGetInstalledReleases(context: string) {
return useQuery<Release[]>({
queryKey: ["installedReleases", context],
queryFn: () =>
apiService.fetchWithSafeDefaults<Release[]>({
url: "/api/helm/releases",
fallback: [],
}),
retry: false,
});
}
export interface ReleaseManifest {
@@ -62,96 +67,154 @@ export function useGetReleaseManifest({
chartName: string;
options?: UseQueryOptions<ReleaseManifest[]>;
}) {
return useQuery<ReleaseManifest[]>(
["manifest", namespace, chartName],
() =>
apiService.fetchWithDefaults<ReleaseManifest[]>(
`/api/helm/releases/${namespace}/${chartName}/manifests`
),
options
);
return useQuery<ReleaseManifest[]>({
queryKey: ["manifest", namespace, chartName],
queryFn: () =>
apiService.fetchWithSafeDefaults<ReleaseManifest[]>({
url: `/api/helm/releases/${namespace}/${chartName}/manifests`,
fallback: [],
}),
...(options ?? {}),
});
}
export interface ContainerImage {
resource: string;
kind: string;
container: string;
image: string;
}
export function useGetImages(ns: string, name: string) {
return useQuery<ContainerImage[]>({
queryKey: ["images", ns, name],
queryFn: () =>
apiService.fetchWithSafeDefaults<ContainerImage[]>({
url: `/api/helm/releases/${ns}/${name}/images`,
fallback: [],
}),
});
}
export interface RelationNode {
id: string;
kind: string;
name: string;
inRelease: boolean;
}
export interface RelationEdge {
source: string;
target: string;
type: string;
}
export interface RelationGraph {
nodes: RelationNode[];
edges: RelationEdge[];
}
export function useGetRelations(ns: string, name: string) {
return useQuery<RelationGraph>({
queryKey: ["relations", ns, name],
queryFn: () =>
apiService.fetchWithSafeDefaults<RelationGraph>({
url: `/api/helm/releases/${ns}/${name}/relations`,
fallback: { nodes: [], edges: [] },
}),
});
}
// List of installed k8s resources for this release
export function useGetResources(
ns: string,
name: string,
options?: UseQueryOptions<StructuredResources[]>
) {
const { data, ...rest } = useQuery<StructuredResources[]>(
["resources", ns, name],
() =>
apiService.fetchWithDefaults<StructuredResources[]>(
`/api/helm/releases/${ns}/${name}/resources?health=true`
),
options
);
return {
data: data
?.map((resource) => ({
...resource,
status: {
...resource.status,
conditions: resource.status.conditions.filter(
(c) => c.type === HD_RESOURCE_CONDITION_TYPE
),
},
}))
.sort((a, b) => {
const interestingResources = ["STATEFULSET", "DEAMONSET", "DEPLOYMENT"];
return (
interestingResources.indexOf(b.kind.toUpperCase()) -
interestingResources.indexOf(a.kind.toUpperCase())
);
export function useGetResources(ns: string, name: string, enabled?: boolean) {
return useQuery<StructuredResources[]>({
queryKey: ["resources", ns, name],
queryFn: () =>
apiService.fetchWithSafeDefaults<StructuredResources[]>({
url: `/api/helm/releases/${ns}/${name}/resources?health=true`,
fallback: [],
}),
...rest,
};
select: (data) =>
data
?.map((resource) => ({
...resource,
status: {
...resource.status,
conditions: resource.status.conditions.filter(
(c) => c.type === HD_RESOURCE_CONDITION_TYPE
),
},
}))
.sort((a, b) => {
const interestingResources = [
"STATEFULSET",
"DEAMONSET",
"DEPLOYMENT",
];
return (
interestingResources.indexOf(b.kind.toUpperCase()) -
interestingResources.indexOf(a.kind.toUpperCase())
);
}),
enabled,
});
}
export function useGetResourceDescription(
type: string,
ns: string,
name: string,
apiVersion?: string,
options?: UseQueryOptions<string>
) {
return useQuery<string>(
["describe", type, ns, name],
() =>
const params = new URLSearchParams({ name, namespace: ns });
if (apiVersion) {
params.set("apiVersion", apiVersion);
}
return useQuery<string>({
queryKey: ["describe", type, ns, name, apiVersion],
queryFn: () =>
apiService.fetchWithDefaults<string>(
`/api/k8s/${type}/describe?name=${name}&namespace=${ns}`,
`/api/k8s/${type}/describe?${params.toString()}`,
{
headers: { "Content-Type": "text/plain; charset=utf-8" },
}
),
options
);
...(options ?? {}),
});
}
export function useGetLatestVersion(
chartName: string,
options?: UseQueryOptions<ChartVersion[]>
) {
return useQuery<ChartVersion[]>(
["latestver", chartName],
() =>
apiService.fetchWithDefaults<ChartVersion[]>(
`/api/helm/repositories/latestver?name=${chartName}`
),
options
);
return useQuery<ChartVersion[]>({
queryKey: ["latestver", chartName],
queryFn: () =>
apiService.fetchWithSafeDefaults<ChartVersion[]>({
url: `/api/helm/repositories/latestver?name=${chartName}`,
fallback: [],
}),
gcTime: 0,
...(options ?? {}),
});
}
export function useGetVersions(
chartName: string,
options?: UseQueryOptions<LatestChartVersion[]>
) {
return useQuery<LatestChartVersion[]>(
["versions", chartName],
() =>
apiService.fetchWithDefaults<LatestChartVersion[]>(
`/api/helm/repositories/versions?name=${chartName}`
),
options
);
return useQuery<LatestChartVersion[]>({
queryKey: ["versions", chartName],
queryFn: async () => {
const url = `/api/helm/repositories/versions?name=${chartName}`;
return await apiService.fetchWithSafeDefaults<LatestChartVersion[]>({
url,
fallback: [],
});
},
select: (data) =>
data?.sort((a, b) => (isNewerVersion(a.version, b.version) ? 1 : -1)),
...(options ?? {}),
});
}
export function useGetReleaseInfoByType(
@@ -160,77 +223,80 @@ export function useGetReleaseInfoByType(
options?: UseQueryOptions<string>
) {
const { chart, namespace, tab, revision } = params;
return useQuery<string>(
[tab, namespace, chart, revision, additionalParams],
() =>
return useQuery<string>({
queryKey: [tab, namespace, chart, revision, additionalParams],
queryFn: () =>
apiService.fetchWithDefaults<string>(
`/api/helm/releases/${namespace}/${chart}/${tab}?revision=${revision}${additionalParams}`,
{
headers: { "Content-Type": "text/plain; charset=utf-8" },
}
),
options
);
...(options ?? {}),
});
}
export function useGetDiff(
formData: FormData,
options?: UseQueryOptions<string>
) {
return useQuery<string>(
["diff", formData],
() => {
return useQuery<string>({
queryKey: ["diff", formData],
queryFn: () => {
return apiService.fetchWithDefaults<string>("/diff", {
body: formData,
method: "POST",
});
},
options
);
...(options ?? {}),
});
}
// Rollback the release to a previous revision
export function useRollbackRelease(
options?: UseMutationOptions<
void,
unknown,
string,
Error,
{ ns: string; name: string; revision: number }
>
) {
return useMutation<
void,
unknown,
string,
Error,
{ ns: string; name: string; revision: number }
>(({ ns, name, revision }) => {
const formData = new FormData();
formData.append("revision", revision.toString());
>({
mutationFn: ({ ns, name, revision }) => {
const formData = new FormData();
formData.append("revision", revision.toString());
return apiService.fetchWithDefaults<void>(
`/api/helm/releases/${ns}/${name}/rollback`,
{
method: "POST",
body: formData,
}
);
}, options);
return apiService.fetchWithDefaults<string>(
`/api/helm/releases/${ns}/${name}/rollback`,
{
method: "POST",
body: formData,
}
);
},
...(options ?? {}),
});
}
// Run the tests on a release
export function useTestRelease(
options?: UseMutationOptions<void, unknown, { ns: string; name: string }>
options?: UseMutationOptions<string, Error, { ns: string; name: string }>
) {
return useMutation<void, unknown, { ns: string; name: string }>(
({ ns, name }) => {
return apiService.fetchWithDefaults<void>(
return useMutation<string, Error, { ns: string; name: string }>({
mutationFn: ({ ns, name }) => {
return apiService.fetchWithDefaults<string>(
`/api/helm/releases/${ns}/${name}/test`,
{
method: "POST",
}
);
},
options
);
...(options ?? {}),
});
}
export function useChartReleaseValues({
@@ -246,12 +312,12 @@ export function useChartReleaseValues({
userDefinedValue?: string;
revision?: number;
version?: string;
options?: UseQueryOptions<unknown>;
options?: UseQueryOptions<string>;
}) {
return useQuery<unknown>(
["values", namespace, release, userDefinedValue, version],
() =>
apiService.fetchWithDefaults<unknown>(
return useQuery<string>({
queryKey: ["values", namespace, release, userDefinedValue, version],
queryFn: () =>
apiService.fetchWithDefaults(
`/api/helm/releases/${namespace}/${release}/values?${"userDefined=true"}${
revision ? `&revision=${revision}` : ""
}`,
@@ -259,10 +325,16 @@ export function useChartReleaseValues({
headers: { "Content-Type": "text/plain; charset=utf-8" },
}
),
options
);
...(options ?? {}),
});
}
export type VersionData = {
version: string;
repository?: string;
urls: string[];
};
export const useVersionData = ({
version,
userValues,
@@ -271,7 +343,7 @@ export const useVersionData = ({
namespace,
releaseName,
isInstallRepoChart = false,
options,
enabled = true,
}: {
version: string;
userValues: string;
@@ -280,10 +352,10 @@ export const useVersionData = ({
namespace: string;
releaseName: string;
isInstallRepoChart?: boolean;
options?: UseQueryOptions;
enabled?: boolean;
}) => {
return useQuery(
[
return useQuery<{ [key: string]: string }>({
queryKey: [
version,
userValues,
chartAddress,
@@ -292,7 +364,7 @@ export const useVersionData = ({
releaseName,
isInstallRepoChart,
],
async () => {
queryFn: async () => {
const formData = getVersionManifestFormData({
version,
userValues,
@@ -301,22 +373,26 @@ export const useVersionData = ({
releaseName,
});
const fetchUrl = isInstallRepoChart
const url = isInstallRepoChart
? `/api/helm/releases/${namespace || "default"}`
: `/api/helm/releases/${
namespace ? namespace : "[empty]"
}${`/${releaseName}`}`;
const data = await apiService.fetchWithDefaults(fetchUrl, {
method: "post",
body: formData,
return await apiService.fetchWithSafeDefaults<{
[key: string]: string;
}>({
url,
options: {
method: "post",
body: formData,
},
fallback: {},
});
return data;
},
// @ts-ignore
options
);
enabled,
});
};
// Request objects

View File

@@ -4,49 +4,60 @@ import {
useMutation,
useQuery,
} from "@tanstack/react-query";
import { HelmRepositories } from "./interfaces";
import apiService from "./apiService";
import type { HelmRepositories } from "./interfaces";
// Get list of Helm repositories
export function useGetRepositories(
options?: UseQueryOptions<HelmRepositories>
) {
return useQuery<HelmRepositories>(
["helm", "repositories"],
() =>
apiService.fetchWithDefaults<HelmRepositories>("/api/helm/repositories"),
options
);
return useQuery<HelmRepositories>({
queryKey: ["helm", "repositories"],
queryFn: () =>
apiService.fetchWithSafeDefaults<HelmRepositories>({
url: "/api/helm/repositories",
fallback: [],
}),
select: (data) => data?.sort((a, b) => a?.name?.localeCompare(b?.name)),
...(options ?? {}),
});
}
// Update repository from remote
export function useUpdateRepo(
repo: string,
options?: UseMutationOptions<void, unknown, void>
options?: UseMutationOptions<string, Error>
) {
return useMutation<void, unknown, void>(() => {
return apiService.fetchWithDefaults<void>(
`/api/helm/repositories/${repo}`,
{
method: "POST",
}
);
}, options);
return useMutation<string, Error>({
mutationFn: () => {
return apiService.fetchWithDefaults<string>(
`/api/helm/repositories/${repo}`,
{
method: "POST",
}
);
},
...(options ?? {}),
});
}
// Remove repository
export function useDeleteRepo(
repo: string,
options?: UseMutationOptions<void, unknown, void>
options?: UseMutationOptions<string, Error>
) {
return useMutation<void, unknown, void>(() => {
return apiService.fetchWithDefaults<void>(
`/api/helm/repositories/${repo}`,
{
method: "DELETE",
}
);
}, options);
return useMutation<string, Error>({
mutationFn: () => {
return apiService.fetchWithDefaults<string>(
`/api/helm/repositories/${repo}`,
{
method: "DELETE",
}
);
},
...(options ?? {}),
});
}
export function useChartRepoValues({
@@ -56,17 +67,15 @@ export function useChartRepoValues({
version: string;
chart: string;
}) {
return useQuery<string>(
["helm", "repositories", "values", chart, version],
() =>
return useQuery<string>({
queryKey: ["helm", "repositories", "values", chart, version],
queryFn: () =>
apiService.fetchWithDefaults<string>(
`/api/helm/repositories/values?chart=${chart}&version=${version}`,
{
headers: { "Content-Type": "text/plain; charset=utf-8" },
}
),
{
enabled: Boolean(version) && Boolean(chart),
}
);
enabled: Boolean(version) && Boolean(chart),
});
}

View File

@@ -7,48 +7,65 @@ import {
useMutation,
useQuery,
} from "@tanstack/react-query";
import { ScanResult, ScanResults, ScannersList } from "./interfaces";
import apiService from "./apiService";
import {
type ScanResult,
type ScanResults,
type ScannersList,
} from "./interfaces";
// Get list of discovered scanners
// @ts-expect-error unused
function useGetDiscoveredScanners(options?: UseQueryOptions<ScannersList>) {
return useQuery<ScannersList>(
["scanners"],
() => apiService.fetchWithDefaults<ScannersList>("/api/scanners"),
options
);
return useQuery<ScannersList>({
queryKey: ["scanners"],
queryFn: () =>
apiService.fetchWithSafeDefaults<ScannersList>({
url: "/api/scanners",
fallback: { scanners: [] },
}),
...(options ?? {}),
});
}
// Scan manifests using all applicable scanners
// @ts-expect-error unused
function useScanManifests(
manifest: string,
options?: UseMutationOptions<ScanResults, Error, string>
) {
const formData = new FormData();
formData.append("manifest", manifest);
return useMutation<ScanResults, Error, string>(
() =>
apiService.fetchWithDefaults<ScanResults>("/api/scanners/manifests", {
method: "POST",
body: formData,
return useMutation<ScanResults, Error, string>({
mutationFn: () =>
apiService.fetchWithSafeDefaults<ScanResults>({
url: "/api/scanners/manifests",
options: {
method: "POST",
body: formData,
},
fallback: {},
}),
options
);
...(options ?? {}),
});
}
// Scan specified k8s resource in cluster
// @ts-expect-error unused
function useScanK8sResource(
kind: string,
namespace: string,
name: string,
options?: UseQueryOptions<ScanResults>
) {
return useQuery<ScanResults>(
["scanners", "resource", kind, namespace, name],
() =>
apiService.fetchWithDefaults<ScanResults>(
`/api/scanners/resource/${kind}?namespace=${namespace}&name=${name}`
),
options
);
return useQuery<ScanResults>({
queryKey: ["scanners", "resource", kind, namespace, name],
queryFn: () =>
apiService.fetchWithSafeDefaults<ScanResults>({
url: `/api/scanners/resource/${kind}?namespace=${namespace}&name=${name}`,
fallback: {},
}),
...(options ?? {}),
});
}

View File

@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import apiService from "./apiService";
export const getVersionManifestFormData = ({
@@ -43,9 +44,15 @@ export const useDiffData = ({
selectedVerData: { [key: string]: string };
chart: string;
}) => {
return useQuery(
[selectedRepo, versionsError, chart, currentVerManifest, selectedVerData],
async () => {
return useQuery({
queryKey: [
selectedRepo,
versionsError,
chart,
currentVerManifest,
selectedVerData,
],
queryFn: async () => {
const formData = new FormData();
formData.append("a", currentVerManifest);
formData.append("b", selectedVerData.manifest);
@@ -57,8 +64,6 @@ export const useDiffData = ({
return diff;
},
{
enabled: Boolean(selectedVerData),
}
);
enabled: Boolean(selectedVerData),
});
};

View File

@@ -1,16 +1,22 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type FC, useState, lazy } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { HashRouter, Outlet, Route, Routes, useParams } from "react-router";
import apiService from "./API/apiService";
import ErrorFallback from "./components/ErrorFallback";
import GlobalErrorModal from "./components/modal/GlobalErrorModal";
import { AppContextProvider } from "./context/AppContext";
import {
ErrorModalContext,
type ErrorAlert,
} from "./context/ErrorModalContext";
import Header from "./layout/Header";
import { HashRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import "./index.css";
import Installed from "./pages/Installed";
import RepositoryPage from "./pages/Repository";
import Revision from "./pages/Revision";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
import { ErrorAlert, ErrorModalContext } from "./context/ErrorModalContext";
import GlobalErrorModal from "./components/modal/GlobalErrorModal";
import { AppContextProvider } from "./context/AppContext";
import apiService from "./API/apiService";
import DocsPage from "./pages/DocsPage";
const DocsPage = lazy(() => import("./pages/DocsPage"));
const queryClient = new QueryClient({
defaultOptions: {
@@ -23,19 +29,19 @@ const queryClient = new QueryClient({
const PageLayout = () => {
return (
<div className="flex flex-col h-screen">
<div className="flex h-screen flex-col">
<Header />
<div className="bg-body-background bg-no-repeat bg-[url('./assets/body-background.svg')] flex-1">
<div className="flex-1 bg-body-background bg-[url('./assets/body-background.svg')] bg-no-repeat">
<Outlet />
</div>
</div>
);
};
const SyncContext: React.FC = () => {
const SyncContext: FC = () => {
const { context } = useParams();
if (context) {
apiService.setCluster(context);
apiService.setCluster(decodeURIComponent(context));
}
return <Outlet />;
@@ -51,33 +57,33 @@ export default function App() {
<AppContextProvider>
<ErrorModalContext.Provider value={value}>
<QueryClientProvider client={queryClient}>
<HashRouter>
<Routes>
<Route path="docs/" element={<DocsPage />} />
<Route path="*" element={<PageLayout />}>
<Route path=":context?/*" element={<SyncContext />}>
<Route path="installed/?" element={<Installed />} />
<Route
path=":namespace/:chart/installed/revision/:revision"
element={<Revision />}
/>
<Route path="repository/" element={<RepositoryPage />} />
<Route
path="repository/:selectedRepo?"
element={<RepositoryPage />}
/>
<Route path="*" element={<Installed />} />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<HashRouter>
<Routes>
<Route path="docs/*" element={<DocsPage />} />
<Route path="*" element={<PageLayout />}>
<Route path=":context?/*" element={<SyncContext />}>
<Route
path="repository/:selectedRepo?/*"
element={<RepositoryPage />}
/>
<Route path="installed/?" element={<Installed />} />
<Route
path=":namespace/:chart/installed/revision/:revision"
element={<Revision />}
/>
<Route path="*" element={<Installed />} />
</Route>
</Route>
<Route path="*" element={<Installed />} />
</Route>
</Routes>
<GlobalErrorModal
isOpen={!!shouldShowErrorModal}
onClose={() => setShowErrorModal(undefined)}
titleText={shouldShowErrorModal?.title || ""}
contentText={shouldShowErrorModal?.msg || ""}
/>
</HashRouter>
</Routes>
</HashRouter>
</ErrorBoundary>
<GlobalErrorModal
isOpen={!!shouldShowErrorModal}
onClose={() => setShowErrorModal(undefined)}
titleText={shouldShowErrorModal?.title || ""}
contentText={shouldShowErrorModal?.msg || ""}
/>
</QueryClientProvider>
</ErrorModalContext.Provider>
</AppContextProvider>

View File

@@ -14,7 +14,8 @@
* @see https://storybook.js.org/docs/react/writing-stories/introduction
*/
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import Badge from "./Badge";
// We set the metadata for the story.

View File

@@ -17,6 +17,7 @@
*
*
*/
import type { JSX, ReactNode } from "react";
export type BadgeCode = "success" | "warning" | "error" | "unknown";
@@ -29,7 +30,7 @@ export const BadgeCodes = Object.freeze({
export interface BadgeProps {
type: BadgeCode;
children: React.ReactNode;
children: ReactNode;
additionalClassNames?: string;
}
export default function Badge(props: BadgeProps): JSX.Element {
@@ -41,7 +42,7 @@ export default function Badge(props: BadgeProps): JSX.Element {
};
const badgeBase =
"inline-flex items-center px-1 py-1 rounded text-xs font-light";
"inline-flex items-center px-1 py-1 rounded-sm text-xs font-light";
const badgeElem = (
<span

View File

@@ -1,11 +1,12 @@
import { mount } from "cypress/react18";
import { mount } from "cypress/react";
import { Button } from "./common/Button/Button";
describe("Button component tests", () => {
const buttonText = "buttonText";
it("renders", () => {
mount(<Button onClick={() => {}}></Button>);
mount(<Button onClick={() => {}} label=""></Button>);
cy.get("button").should("exist");
});
@@ -17,14 +18,14 @@ describe("Button component tests", () => {
it("calls onClick when clicked", () => {
const onClickStub = cy.stub().as("onClick");
mount(<Button onClick={onClickStub}></Button>);
mount(<Button onClick={onClickStub} label={""}></Button>);
cy.get("button").click();
cy.get("@onClick").should("have.been.calledOnce");
});
it("should be disabled", () => {
mount(<Button onClick={() => {}} disabled></Button>);
mount(<Button onClick={() => {}} disabled label={""}></Button>);
cy.get("button").should("be.disabled");
});

View File

@@ -1,4 +1,5 @@
import { Meta, StoryObj } from "@storybook/react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import Button from "./Button";
const meta = {

View File

@@ -12,11 +12,12 @@
*
*
*/
import type { HTMLAttributes, JSX, ReactNode } from "react";
// this is a type declaration for the action prop.
// it is a function that takes a string as an argument and returns void.
export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
children: ReactNode;
disabled?: boolean;
onClick: () => void;
className?: string;
@@ -26,7 +27,7 @@ export default function Button(props: ButtonProps): JSX.Element {
<>
<button
onClick={props.onClick}
className={`${props.className} bg-white border border-gray-300 hover:bg-gray-50 text-black py-1 px-4 rounded `}
className={`${props.className} rounded-sm border border-gray-300 bg-white px-4 py-1 text-black hover:bg-gray-50`}
disabled={props.disabled}
>
{props.children}

View File

@@ -1,8 +1,11 @@
import { AppContextProvider } from "../context/AppContext";
import ClustersList from "./ClustersList";
import { BrowserRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Release } from "../data/types";
import { BrowserRouter } from "react-router";
import { AppContextProvider } from "../context/AppContext";
import type { Release } from "../data/types";
import ClustersList from "./ClustersList";
import { DeploymentStatus } from "./common/StatusLabel";
type ClustersListProps = {
onClusterChange: (clusterName: string) => void;
@@ -17,7 +20,7 @@ const generateTestReleaseData = (): Release => ({
namespace: "default",
revision: 1,
updated: "2024-01-23T15:37:35.0992836+02:00",
status: "deployed",
status: DeploymentStatus.DEPLOYED,
chart: "helm-dashboard-0.1.10",
chart_name: "helm-dashboard",
chart_ver: "0.1.10",
@@ -44,6 +47,14 @@ const renderClustersList = (props: ClustersListProps) => {
describe("ClustersList", () => {
it("Got one cluster information", () => {
cy.intercept("GET", "/api/k8s/contexts", [
{
Name: "minikube",
Namespace: "default",
IsCurrent: true,
},
]).as("getClusters");
renderClustersList({
selectedCluster: "minikube",
filteredNamespaces: ["default"],
@@ -51,12 +62,21 @@ describe("ClustersList", () => {
installedReleases: [generateTestReleaseData()],
});
cy.wait("@getClusters");
cy.get(".data-cy-clusterName").contains("minikube");
cy.get(".data-cy-clusterList-namespace").contains("default");
cy.get(".data-cy-clustersInput").should("be.checked");
});
it("Dont have a cluster chekced", () => {
cy.intercept("GET", "/api/k8s/contexts", [
{
Name: "minikube",
Namespace: "default",
IsCurrent: true,
},
]).as("getClusters");
renderClustersList({
selectedCluster: "",
filteredNamespaces: [""],
@@ -64,6 +84,7 @@ describe("ClustersList", () => {
installedReleases: [generateTestReleaseData()],
});
cy.wait("@getClusters");
cy.get(".data-cy-clustersInput").should("not.be.checked");
});
});

View File

@@ -1,4 +1,5 @@
import { Meta, StoryObj } from "@storybook/react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import ClustersList from "./ClustersList";
const meta = {

View File

@@ -1,11 +1,12 @@
import { useMemo } from "react";
import { Cluster, Release } from "../data/types";
import apiService from "../API/apiService";
import { useQuery } from "@tanstack/react-query";
import useCustomSearchParams from "../hooks/useCustomSearchParams";
import { useAppContext } from "../context/AppContext";
import { useEffect, useEffectEvent, useMemo } from "react";
import { v4 as uuidv4 } from "uuid";
import apiService from "../API/apiService";
import { useAppContext } from "../context/AppContext";
import type { Cluster, Release } from "../data/types";
import useCustomSearchParams from "../hooks/useCustomSearchParams";
type ClustersListProps = {
onClusterChange: (clusterName: string) => void;
selectedCluster: string;
@@ -44,29 +45,36 @@ function ClustersList({
const { upsertSearchParams, removeSearchParam } = useCustomSearchParams();
const { clusterMode } = useAppContext();
const { data: clusters } = useQuery<Cluster[]>({
const { data: clusters = [], isSuccess } = useQuery<Cluster[]>({
queryKey: ["clusters", selectedCluster],
queryFn: apiService.getClusters,
onSuccess(data) {
const sortedData = data?.sort((a, b) =>
select: (data) =>
data?.sort((a, b) =>
getCleanClusterName(a.Name).localeCompare(getCleanClusterName(b.Name))
);
if (sortedData && sortedData.length > 0 && !selectedCluster) {
onClusterChange(sortedData[0].Name);
}
if (selectedCluster) {
const cluster = data.find(
(cluster) => getCleanClusterName(cluster.Name) === selectedCluster
);
if (!filteredNamespaces && cluster?.Namespace) {
upsertSearchParams("filteredNamespace", cluster.Namespace);
}
}
},
),
});
const onSuccess = useEffectEvent((clusters: Cluster[]) => {
if (clusters && clusters.length && !selectedCluster) {
onClusterChange(clusters[0].Name);
}
if (selectedCluster) {
const cluster = clusters.find(
(cluster) => getCleanClusterName(cluster.Name) === selectedCluster
);
if (!filteredNamespaces && cluster?.Namespace) {
upsertSearchParams("filteredNamespace", cluster.Namespace);
}
}
});
useEffect(() => {
if (clusters && isSuccess) {
onSuccess(clusters);
}
}, [clusters, isSuccess]);
const namespaces = useMemo(() => {
const mapNamespaces = new Map<string, number>();
@@ -98,47 +106,41 @@ function ClustersList({
};
return (
<div className="bg-white flex flex-col p-2 rounded custom-shadow text-cluster-list w-48 m-5 h-fit pb-4 custom-">
<div className="custom- custom-shadow m-5 flex h-fit w-48 flex-col rounded-sm bg-white p-2 pb-4 text-cluster-list">
{!clusterMode ? (
<>
<label className="font-bold">Clusters</label>
{clusters
?.sort((a, b) =>
getCleanClusterName(a.Name).localeCompare(
getCleanClusterName(b.Name)
)
)
?.map((cluster) => {
return (
<span
key={cluster.Name}
className="data-cy-clusterName flex items-center mt-2 text-xs"
>
<input
className="cursor-pointer data-cy-clustersInput"
onChange={(e) => {
onClusterChange(e.target.value);
}}
type="radio"
id={cluster.Name}
value={cluster.Name}
checked={cluster.Name === selectedCluster}
name="clusters"
/>
<label htmlFor={cluster.Name} className="ml-1 ">
{getCleanClusterName(cluster.Name)}
</label>
</span>
);
})}
{clusters?.map((cluster) => {
return (
<span
key={cluster.Name + cluster.Namespace}
className="data-cy-clusterName mt-2 flex items-center text-xs"
>
<input
className="data-cy-clustersInput cursor-pointer"
onChange={(e) => {
onClusterChange(e.target.value);
}}
type="radio"
id={cluster.Name}
value={cluster.Name}
checked={cluster.Name === selectedCluster}
name="clusters"
/>
<label htmlFor={cluster.Name} className="ml-1">
{getCleanClusterName(cluster.Name)}
</label>
</span>
);
})}
</>
) : null}
<label className="font-bold mt-4">Namespaces</label>
<label className="mt-4 font-bold">Namespaces</label>
{namespaces
?.sort((a, b) => a.name.localeCompare(b.name))
?.map((namespace) => (
<span key={namespace.name} className="flex items-center mt-2 text-xs">
<span key={namespace.name} className="mt-2 flex items-center text-xs">
<input
type="checkbox"
id={namespace.name}

View File

@@ -0,0 +1,114 @@
import { mount } from "cypress/react";
import { useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "./ErrorFallback";
/**
* Component tests for ErrorFallback
* Tests the error fallback UI and reset functionality
*/
describe("ErrorFallback", () => {
beforeEach(() => {
// Ensure portal root exists for createPortal
if (!document.getElementById("portal")) {
const portalDiv = document.createElement("div");
portalDiv.id = "portal";
document.body.appendChild(portalDiv);
}
});
it("should render error modal with error message and hint", () => {
const mockError = new Error("Test error message");
const mockReset = cy.stub().as("resetErrorBoundary");
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// Verify modal is open (checking document directly because of portal)
cy.get("#portal").should("be.visible");
cy.get("#portal").should("contain", "Application Error");
cy.get("#portal").should("contain", "Test error message");
// Verify Komodor hint is present (from GlobalErrorModal)
cy.get("#portal").should("contain", "Sign up for free.");
cy.get("#portal a")
.should("have.attr", "href")
.and("include", "komodor.com");
});
it("should call resetErrorBoundary when modal is closed", () => {
const mockError = new Error("Test error");
const mockReset = cy.stub().as("resetErrorBoundary");
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// Find and click close button (using the selector from Modal.tsx)
cy.get("[data-modal-hide='staticModal']").click();
// Verify reset was called
cy.get("@resetErrorBoundary").should("have.been.calledOnce");
});
it("should handle non-Error objects gracefully", () => {
const mockError = "String error" as unknown as Error;
const mockReset = cy.stub().as("resetErrorBoundary");
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// Should show fallback message
cy.get("#portal").should(
"contain",
"An unexpected error occurred. Please try again."
);
});
it("should log error in development mode", () => {
const mockError = new Error("Test error for logging");
const mockReset = cy.stub();
cy.window().then((win) => {
cy.spy(win.console, "error").as("consoleError");
});
mount(<ErrorFallback error={mockError} resetErrorBoundary={mockReset} />);
// In dev mode, error should be logged
cy.get("@consoleError").should("have.been.called");
});
it("should catch errors from a real component and recover after reset (Integration)", () => {
const BuggyComponent = ({ shouldCrash }: { shouldCrash: boolean }) => {
if (shouldCrash) {
throw new Error("Integrated crash");
}
return <div data-cy="recovered">Recovered successfully!</div>;
};
const TestWrapper = () => {
const [shouldCrash, setShouldCrash] = useState(true);
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => setShouldCrash(false)}
>
<BuggyComponent shouldCrash={shouldCrash} />
</ErrorBoundary>
);
};
mount(<TestWrapper />);
// Verify modal caught the real throw
cy.get("#portal").should("be.visible").and("not.be.empty");
cy.get("#portal").should("contain", "Integrated crash");
// Click close to reset
cy.get("[data-modal-hide='staticModal']").click();
// Verify modal is gone (portal should be empty) and component recovered
cy.get("#portal").should("be.empty");
cy.get("[data-cy='recovered']")
.should("be.visible")
.and("contain", "Recovered successfully!");
});
});

View File

@@ -0,0 +1,118 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import Button from "../Button";
import ErrorFallback from "./ErrorFallback";
const meta = {
title: "Components/ErrorFallback",
component: ErrorFallback,
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/react/configure/story-layout
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
resetErrorBoundary: { action: "reset" },
},
} satisfies Meta<typeof ErrorFallback>;
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Default error fallback with a standard error message
*/
export const Default: Story = {
args: {
error: new Error("Something went wrong in the application"),
resetErrorBoundary: () => {},
},
};
export const InteractiveIntegration: Story = {
args: {
error: new Error("Interactive Demo Error"),
resetErrorBoundary: () => {},
},
render: (args) => {
const BuggyComponent = () => {
const [shouldError, setShouldError] = useState(false);
if (shouldError) {
throw new Error(
"This is a real runtime error caught by the ErrorBoundary!"
);
}
return (
<div className="w-96 rounded border bg-white p-8 text-center shadow-md">
<h3 className="mb-4 text-lg font-bold">Interactive Demo</h3>
<p className="mb-6 text-sm text-gray-600">
Clicking the button below will cause this component to throw a
runtime error during render.
</p>
<Button onClick={() => setShouldError(true)}>
Trigger Runtime Error
</Button>
</div>
);
};
return (
<ErrorBoundary
FallbackComponent={(props) => (
<ErrorFallback
{...props}
resetErrorBoundary={() => {
props.resetErrorBoundary();
args.resetErrorBoundary();
}}
/>
)}
>
<BuggyComponent />
</ErrorBoundary>
);
},
};
/**
* Long error message to test text wrapping
*/
export const LongErrorMessage: Story = {
args: {
error: new Error(
"This is a very long error message that should demonstrate how the error modal handles text wrapping and displays lengthy error descriptions to the user. The error boundary should gracefully handle this scenario."
),
resetErrorBoundary: () => {},
},
};
/**
* Non-Error object to test fallback behavior
*/
export const NonErrorObject: Story = {
args: {
error: "String error message" as unknown as Error,
resetErrorBoundary: () => {},
},
};
/**
* Error with stack trace (useful for development)
*/
export const WithStackTrace: Story = {
args: {
error: (() => {
try {
throw new Error("Error with detailed stack trace");
} catch (e) {
return e as Error;
}
})(),
resetErrorBoundary: () => {},
},
};

View File

@@ -0,0 +1,34 @@
import type { FallbackProps } from "react-error-boundary";
import { useDevLogger } from "../../hooks/useDevLogger";
import GlobalErrorModal from "../modal/GlobalErrorModal";
/**
* Error fallback component for React Error Boundary
* Uses the existing GlobalErrorModal for consistent error display
* @param error - The error that was caught
* @param resetErrorBoundary - Function to reset the error boundary state
*/
const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => {
useDevLogger(error);
const handleClose = () => {
// Reset the error boundary to allow the component tree to re-render
resetErrorBoundary();
};
return (
<GlobalErrorModal
isOpen={true}
onClose={handleClose}
titleText="Application Error"
contentText={
error instanceof Error
? error.message
: "An unexpected error occurred. Please try again."
}
/>
);
};
export default ErrorFallback;

View File

@@ -0,0 +1 @@
export { default } from "./ErrorFallback";

View File

@@ -1,40 +1,44 @@
import { HD_RESOURCE_CONDITION_TYPE } from "../../API/releases";
import { Tooltip } from "flowbite-react";
import { ReleaseHealthStatus } from "../../data/types";
import { v4 as uuidv4 } from "uuid";
import { HD_RESOURCE_CONDITION_TYPE } from "../../API/releases";
import type { ReleaseHealthStatus } from "../../data/types";
interface Props {
statusData: ReleaseHealthStatus[];
}
const HealthStatus = ({ statusData }: Props) => {
const statuses = statusData.map((item) => {
for (let i = 0; i < item.status.conditions.length; i++) {
const cond = item.status.conditions[i];
const statuses = statusData.flatMap((item) => {
return item.status?.conditions
?.filter((cond) => cond.type === HD_RESOURCE_CONDITION_TYPE)
.map((cond) => {
const stableKey = item.metadata?.uid
? `${item.metadata.uid}-${item.metadata.namespace ?? "default"}`
: `${item.kind}-${item.metadata?.namespace ?? "default"}-${item.metadata?.name}`;
if (cond.type !== HD_RESOURCE_CONDITION_TYPE) {
continue;
}
return (
<Tooltip
key={uuidv4()} // this is not a good practice, we need to fetch some unique id from the backend
content={`${cond.status} ${item.kind} ${item.metadata.name}`}
>
<span
className={`inline-block ${
cond.status === "Healthy"
? "bg-success"
: cond.status === "Progressing"
? "bg-warning"
: "bg-danger"
} w-2.5 h-2.5 rounded-sm`}
></span>
</Tooltip>
);
}
return (
<Tooltip
key={stableKey}
content={`${cond.status} ${item.kind} ${item.metadata?.name}`}
>
<span
className={`inline-block ${
cond.status === "Healthy"
? "bg-success"
: cond.status === "Progressing"
? "bg-warning"
: "bg-danger"
} h-2.5 w-2.5 rounded-xs`}
></span>
</Tooltip>
);
});
});
if (statuses.length === 0) {
return <div>No health statuses available</div>;
}
return <div className="flex flex-wrap gap-1">{statuses}</div>;
};

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import InstalledPackageCard from "./InstalledPackageCard";
const meta = {

View File

@@ -1,20 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { Release } from "../../data/types";
import { BsArrowUpCircleFill, BsPlusCircleFill } from "react-icons/bs";
import { useInView } from "react-intersection-observer";
import apiService from "../../API/apiService";
import type { LatestChartVersion } from "../../API/interfaces";
import { useGetApplicationStatus } from "../../API/other";
import { useGetLatestVersion } from "../../API/releases";
import HelmGrayIcon from "../../assets/helm-gray-50.svg";
import type { Release, ReleaseHealthStatus } from "../../data/types";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import { getAge } from "../../timeUtils";
import { isNewerVersion } from "../../utils";
import StatusLabel, {
DeploymentStatus,
getStatusColor,
} from "../common/StatusLabel";
import { useQuery } from "@tanstack/react-query";
import apiService from "../../API/apiService";
import HealthStatus from "./HealthStatus";
import HelmGrayIcon from "../../assets/helm-gray-50.svg";
import Spinner from "../Spinner";
import { useGetLatestVersion } from "../../API/releases";
import { isNewerVersion } from "../../utils";
import { LatestChartVersion } from "../../API/interfaces";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import HealthStatus from "./HealthStatus";
type InstalledPackageCardProps = {
release: Release;
@@ -26,15 +30,21 @@ export default function InstalledPackageCard({
const navigate = useNavigateWithSearchParams();
const [isMouseOver, setIsMouseOver] = useState(false);
const { ref, inView } = useInView({
threshold: 0.3,
triggerOnce: true,
});
const { data: status } = useGetApplicationStatus();
const { data: latestVersionResult } = useGetLatestVersion(release.chartName, {
queryKey: ["chartName", release.chartName],
cacheTime: 0,
enabled: !status?.NoLatest,
});
const { data: statusData } = useQuery<unknown>({
const { data: statusData = [], isLoading } = useQuery<ReleaseHealthStatus[]>({
queryKey: ["resourceStatus", release],
queryFn: () => apiService.getResourceStatus({ release }),
enabled: inView && !status?.NoHealth,
});
const latestVersionData: LatestChartVersion | undefined =
@@ -57,14 +67,21 @@ export default function InstalledPackageCard({
setIsMouseOver(false);
};
const handleOnClick = () => {
const onClick = async () => {
const { name, namespace } = release;
navigate(`/${namespace}/${name}/installed/revision/${release.revision}`, {
state: release,
});
await navigate(
`/${namespace}/${name}/installed/revision/${release.revision}`,
{
state: release,
}
);
};
const statusColor = getStatusColor(release.status as DeploymentStatus);
const handleClick = () => {
void onClick();
};
const statusColor = getStatusColor(release.status);
const borderLeftColor: { [key: string]: string } = {
[DeploymentStatus.DEPLOYED]: "border-l-border-deployed",
[DeploymentStatus.FAILED]: "border-l-text-danger",
@@ -73,57 +90,58 @@ export default function InstalledPackageCard({
return (
<div
ref={ref}
className={`${
borderLeftColor[release.status]
} text-xs grid grid-cols-12 items-center bg-white rounded-md p-2 py-6 my-2 custom-shadow border-l-4 border-l-[${statusColor}] cursor-pointer ${
} custom-shadow my-2 grid grid-cols-12 items-center rounded-md border-l-4 bg-white p-2 py-6 text-xs border-l-[${statusColor}] cursor-pointer ${
isMouseOver && "custom-shadow-lg"
}`}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
onClick={handleOnClick}
onClick={handleClick}
>
<img
src={release.icon || HelmGrayIcon}
alt="helm release icon"
className="w-[45px] mx-4 col-span-1 min-w-[45px]"
className="col-span-1 mx-4 w-[45px] min-w-[45px]"
/>
<div className="col-span-11 -mb-5">
<div className="grid grid-cols-11">
<div className="col-span-3 font-bold text-xl mr-0.5 font-roboto-slab">
<div className="col-span-3 mr-0.5 font-roboto-slab text-xl font-bold">
{release.name}
</div>
<div className="col-span-3">
<StatusLabel status={release.status} />
</div>
<div className="col-span-2 font-bold">{release.chart}</div>
<div className="col-span-1 font-bold text-xs">
<div className="col-span-1 text-xs font-bold">
#{release.revision}
</div>
<div className="col-span-1 font-bold text-xs">
<div className="col-span-1 text-xs font-bold">
{release.namespace}
</div>
<div className="col-span-1 font-bold text-xs">{getAge(release)}</div>
<div className="col-span-1 text-xs font-bold">{getAge(release)}</div>
</div>
<div
className="grid grid-cols-11 text-xs mt-3"
className="mt-3 grid grid-cols-11 text-xs"
style={{ marginBottom: "12px" }}
>
<div className="col-span-3 h-12 line-clamp-3 mr-1">
<div className="col-span-3 mr-1 line-clamp-3 h-12">
{release.description}
</div>
<div className="col-span-3 mr-2">
{statusData ? (
<HealthStatus statusData={statusData} />
) : (
{isLoading ? (
<Spinner size={4} />
) : (
<HealthStatus statusData={statusData} />
)}
</div>
<div className="col-span-2 text-muted flex flex-col items">
<div className="items col-span-2 flex flex-col text-muted">
<span>CHART VERSION</span>
{(canUpgrade || installRepoSuggestion) && (
<div
className="text-upgradable flex flex-row items-center gap-1 font-bold"
className="flex flex-row items-center gap-1 font-bold text-upgradable"
title={`upgrade available: ${latestVersionData?.version} from ${latestVersionData?.repository}`}
>
{canUpgrade && !installRepoSuggestion ? (

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import InstalledPackagesHeader from "./InstalledPackagesHeader";
const meta = {

View File

@@ -1,9 +1,11 @@
import type { Dispatch, SetStateAction } from "react";
import HeaderLogo from "../../assets/packges-header.svg";
import { Release } from "../../data/types";
import type { Release } from "../../data/types";
type InstalledPackagesHeaderProps = {
filteredReleases?: Release[];
setFilterKey: React.Dispatch<React.SetStateAction<string>>;
setFilterKey: Dispatch<SetStateAction<string>>;
isLoading: boolean;
};
@@ -17,22 +19,22 @@ export default function InstalledPackagesHeader({
!isLoading && (numOfPackages === undefined || numOfPackages === 0)
);
return (
<div className="custom-shadow rounded-t-md ">
<div className="flex items-center justify-between bg-white px-2 py-0.5 font-inter rounded-t-md ">
<div className="custom-shadow rounded-t-md">
<div className="flex items-center justify-between rounded-t-md bg-white px-2 py-0.5 font-inter">
<div className="flex items-center">
<img
src={HeaderLogo}
alt="Helm-DashBoard"
className="display-inline h-12 ml-3 mr-3 w-[28px] "
className="display-inline mr-3 ml-3 h-12 w-[28px]"
/>
<h2 className="display-inline font-bold text-base ">{`Installed Charts (${
<h2 className="display-inline text-base font-bold">{`Installed Charts (${
numOfPackages || "0"
})`}</h2>
</div>
<div className="w-1/3">
<input
className="border-installed-charts-filter rounded p-1 text-sm w-11/12"
className="w-11/12 rounded-sm border border-installed-charts-filter p-1 text-sm"
placeholder="Filter..."
type="text"
onChange={(ev) => setFilterKey(ev.target.value)}
@@ -41,7 +43,7 @@ export default function InstalledPackagesHeader({
</div>
{showNoPackageAlert && (
<div className="bg-white rounded shadow display-none no-charts mt-3 text-sm p-4">
<div className="display-none no-charts mt-3 rounded-sm bg-white p-4 text-sm shadow-sm">
Looks like you don&apos;t have any charts installed.
&quot;Repository&quot; section may be a good place to start.
</div>

View File

@@ -0,0 +1,99 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react";
import { BrowserRouter } from "react-router";
import { AppContextProvider } from "../../context/AppContext";
import type { Release } from "../../data/types";
import { DeploymentStatus } from "../common/StatusLabel";
import InstalledPackagesList from "./InstalledPackagesList";
const baseRelease: Release = {
id: "release-id",
name: "shared-release",
namespace: "default",
revision: 1,
updated: "2024-01-23T15:37:35.0992836+02:00",
status: DeploymentStatus.DEPLOYED,
chart: "shared-release-0.1.10",
chart_name: "shared-release",
chart_ver: "0.1.10",
app_version: "1.3.3",
icon: "",
description: "A shared release used for namespace filtering tests.",
has_tests: true,
chartName: "shared-release",
chartVersion: "0.1.10",
};
const createRelease = (overrides: Partial<Release> = {}): Release => ({
...baseRelease,
...overrides,
});
function InstalledPackagesListTestWrapper() {
const [showOnlyTargetNamespace, setShowOnlyTargetNamespace] = useState(false);
const releases = [
createRelease({ id: "release-a", namespace: "airbyte" }),
createRelease({ id: "release-b", namespace: "cert-manager" }),
];
const filteredReleases = showOnlyTargetNamespace
? releases.filter((release) => release.namespace === "cert-manager")
: releases;
return (
<div>
<button type="button" onClick={() => setShowOnlyTargetNamespace(true)}>
Filter to cert-manager
</button>
<InstalledPackagesList filteredReleases={filteredReleases} />
</div>
);
}
const renderInstalledPackagesList = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
cy.mount(
<AppContextProvider>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<InstalledPackagesListTestWrapper />
</QueryClientProvider>
</BrowserRouter>
</AppContextProvider>
);
};
describe("InstalledPackagesList", () => {
it("updates visible cards when filtering duplicate release names by namespace", () => {
cy.intercept("GET", "/status", {
Analytics: false,
CacheHitRatio: 0,
ClusterMode: false,
CurVer: "1.0.0",
LatestVer: "1.0.0",
NoHealth: true,
NoLatest: true,
}).as("getStatus");
renderInstalledPackagesList();
cy.wait("@getStatus");
cy.get("img[alt='helm release icon']").should("have.length", 2);
cy.contains(/^airbyte$/).should("exist");
cy.contains(/^cert-manager$/).should("exist");
cy.contains("button", "Filter to cert-manager").click();
cy.get("img[alt='helm release icon']").should("have.length", 1);
cy.contains(/^cert-manager$/).should("exist");
cy.contains(/^airbyte$/).should("not.exist");
});
});

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import InstalledPackagesList from "./InstalledPackagesList";
const meta = {

View File

@@ -1,5 +1,6 @@
import type { Release } from "../../data/types";
import InstalledPackageCard from "./InstalledPackageCard";
import { Release } from "../../data/types";
type InstalledPackagesListProps = {
filteredReleases: Release[];
@@ -11,11 +12,10 @@ export default function InstalledPackagesList({
return (
<div>
{filteredReleases.map((installedPackage: Release) => {
const releaseKey = `${installedPackage.namespace}/${installedPackage.name}`;
return (
<InstalledPackageCard
key={installedPackage.name}
release={installedPackage}
/>
<InstalledPackageCard key={releaseKey} release={installedPackage} />
);
})}
</div>

View File

@@ -1,4 +1,6 @@
import { NavLink, useLocation, useParams } from "react-router-dom";
import { type ReactNode } from "react";
import { NavLink, useLocation, useParams } from "react-router";
import { useAppContext } from "../context/AppContext";
const LinkWithSearchParams = ({
@@ -9,10 +11,10 @@ const LinkWithSearchParams = ({
end?: boolean;
exclude?: string[];
className?: string;
children: React.ReactNode;
children: ReactNode;
}) => {
const { search } = useLocation();
const { context } = useParams();
const { context = "" } = useParams();
const { clusterMode } = useAppContext();
const params = new URLSearchParams(search);
@@ -23,17 +25,15 @@ const LinkWithSearchParams = ({
let prefixedUrl = to;
if (!clusterMode) {
prefixedUrl = `/${context}${to}`;
if (!clusterMode && context) {
prefixedUrl = `/${encodeURIComponent(context)}${to}`;
} else {
prefixedUrl = to;
}
return (
<NavLink
data-cy="navigation-link"
to={`${prefixedUrl}/?${params.toString()}`}
{...props}
/>
);
const url = `${prefixedUrl}/?${params.toString()}`;
return <NavLink data-cy="navigation-link" to={url} {...props} />;
};
export default LinkWithSearchParams;

View File

@@ -6,8 +6,9 @@
* The default story renders the component with the default props.
*/
import { Meta, StoryObj } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { action } from "storybook/actions";
import SelectMenu, { SelectMenuItem } from "./SelectMenu";
const meta = {

View File

@@ -24,6 +24,7 @@
*
*
*/
import type { JSX, ReactNode } from "react";
// define the SelectMenuItem type:
// This is an object with a label and id.
@@ -38,7 +39,7 @@ export interface SelectMenuItemProps {
export interface SelectMenuProps {
header: string;
children: React.ReactNode;
children: ReactNode;
selected: number;
onSelect: (id: number) => void;
}
@@ -74,7 +75,7 @@ export function SelectMenuItem({
export default function SelectMenu(props: SelectMenuProps): JSX.Element {
const { header, children } = props;
return (
<div className="card flex flex-col">
<div className="flex flex-col card">
<h2 className="text-xl font-bold">{header}</h2>
<div className="flex flex-col">{children}</div>
</div>

View File

@@ -1,4 +1,5 @@
import { StoryFn, Meta } from "@storybook/react";
import type { StoryFn, Meta } from "@storybook/react-vite";
import ShutDownButton from "./ShutDownButton";
const meta = {

View File

@@ -1,12 +1,13 @@
import { BsPower } from "react-icons/bs";
import Modal from "./modal/Modal";
import { useShutdownHelmDashboard } from "../API/other";
import Modal from "./modal/Modal";
function ShutDownButton() {
const { mutate: signOut, status } = useShutdownHelmDashboard();
const handleClick = async () => {
const handleClick = () => {
signOut();
};
@@ -22,7 +23,7 @@ function ShutDownButton() {
<button
onClick={handleClick}
title="Shut down the Helm Dashboard application"
className="flex justify-center w-full mr-5 py-3 border border-transparent hover:border hover:border-gray-500 rounded hover:rounded-lg"
className="mr-5 flex w-full justify-center rounded-sm border border-transparent py-3 hover:rounded-lg hover:border hover:border-gray-500"
>
<BsPower className="w-6" />
</button>

View File

@@ -3,7 +3,7 @@ export default function Spinner({ size = 8 }: { size?: number }) {
<div role="status">
<svg
aria-hidden="true"
className={`w-${size} h-${size} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
className={`w-${size} h-${size} mr-2 animate-spin fill-blue-600 text-gray-200 dark:text-gray-600`}
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import Tabs from "./Tabs";
const meta = {

View File

@@ -1,4 +1,5 @@
import { ReactNode } from "react";
import type { ReactNode } from "react";
import useCustomSearchParams from "../hooks/useCustomSearchParams";
export interface Tab {
@@ -18,19 +19,16 @@ export default function Tabs({ tabs, selectedTab }: TabsProps) {
const moveTab = (tab: Tab) => {
upsertSearchParams("tab", tab.value);
};
return (
<div className="flex flex-col">
<div className="flex pb-2">
{tabs.map((tab) => (
<button
key={tab.label}
className={`cursor-pointer px-4 py-2 text-sm font-normal text-tab-color focus:outline-none"
${
selectedTab.value === tab.value &&
"border-b-[3px] border-tab-color"
}
`}
className={`focus:outline-hidden" cursor-pointer px-4 py-2 text-sm font-normal text-tab-color ${
selectedTab.value === tab.value &&
"border-b-[3px] border-tab-color"
} `}
onClick={() => moveTab(tab)}
>
{tab.label}

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import TabsBar from "./TabsBar";
const meta = {
@@ -17,15 +18,15 @@ export const Default = {
tabs: [
{
name: "tab1",
component: <div className="w-250 h-250 bg-green-400">tab1</div>,
component: <div className="h-250 w-250 bg-green-400">tab1</div>,
},
{
name: "tab2",
component: <div className="w-250 h-250 bg-red-400">tab2</div>,
component: <div className="h-250 w-250 bg-red-400">tab2</div>,
},
{
name: "tab3",
component: <div className="w-250 h-250 bg-blue-400">tab3</div>,
component: <div className="h-250 w-250 bg-blue-400">tab3</div>,
},
],
activeTab: "tab1",

View File

@@ -14,6 +14,7 @@
*
*
*/
import type { JSX } from "react";
interface TabsBarProps {
tabs: Array<{ name: string; component: JSX.Element }>;

View File

@@ -4,7 +4,8 @@
* the first story simply renders the component with the default props.
*/
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import TextInput from "./TextInput";
const meta = {

View File

@@ -12,18 +12,19 @@
* @return JSX.Element
*
*/
import type { ChangeEvent, JSX } from "react";
export interface TextInputProps {
label: string;
placeholder: string;
isMandatory?: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
}
export default function TextInput(props: TextInputProps): JSX.Element {
return (
<div className="mb-6">
<label className="block ml-1 mb-1 text-sm font-medium text-gray-900dark:text-white">
<label className="text-gray-900dark:text-white mb-1 ml-1 block text-sm font-medium">
{props.label}
{/* if prop.isMandatory is true, add a whitespace and a red star to signify it*/}
{props.isMandatory ? <span className="text-red-500"> *</span> : ""}
@@ -31,7 +32,7 @@ export default function TextInput(props: TextInputProps): JSX.Element {
<input
type="text"
placeholder={props.placeholder}
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 "
className="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500"
/>
</div>
);

View File

@@ -1,4 +1,4 @@
import { type ReactElement, cloneElement } from "react";
import { cloneElement, type HTMLAttributes, type ReactElement } from "react";
export default function Tooltip({
id,
@@ -11,11 +11,16 @@ export default function Tooltip({
}) {
return (
<>
{cloneElement(element, { "data-tooltip-target": id })}
{cloneElement(
element as ReactElement<HTMLAttributes<HTMLElement>>,
{
"data-tooltip-target": id,
} as unknown as HTMLAttributes<HTMLElement>
)}
<div
id={id}
role="tooltip"
className="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
className="tooltip invisible absolute z-10 inline-block rounded-lg bg-gray-900 px-3 py-2 text-sm font-medium text-white opacity-0 shadow-xs transition-opacity duration-300 dark:bg-gray-700"
>
{title}
<div className="tooltip-arrow" data-popper-arrow></div>
@@ -24,14 +29,14 @@ export default function Tooltip({
<button
data-tooltip-target="tooltip-default"
type="button"
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
className="rounded-lg bg-blue-700 px-5 py-2.5 text-center text-sm font-medium text-white hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 focus:outline-hidden dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Default tooltip
</button>
<div
id="tooltip-default"
role="tooltip"
className="absolute z-10 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 tooltip dark:bg-gray-700"
className="tooltip invisible absolute z-10 inline-block rounded-lg bg-gray-900 px-3 py-2 text-sm font-medium text-white opacity-0 shadow-xs transition-opacity duration-300 dark:bg-gray-700"
>
Tooltip content
<div className="tooltip-arrow" data-popper-arrow></div>

View File

@@ -1,4 +1,5 @@
import { Meta, StoryFn } from "@storybook/react";
import type { Meta, StoryFn } from "@storybook/react-vite";
import { Troubleshoot } from "./Troubleshoot";
const meta = {

View File

@@ -8,7 +8,7 @@ export const Troubleshoot = () => {
target="_blank"
rel="noreferrer"
>
<button className="bg-primary text-white p-2 flex items-center rounded text-sm font-medium font-roboto">
<button className="flex items-center rounded-sm bg-primary p-2 font-roboto text-sm font-medium text-white">
Troubleshoot in Komodor
<RiExternalLinkLine className="ml-2 text-lg" />
</button>

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import { Button } from "./Button";

View File

@@ -21,6 +21,7 @@ interface ButtonProps {
* Optional click handler
*/
onClick?: () => void;
disabled?: boolean;
}
/**

View File

@@ -1,16 +1,13 @@
import { Meta } from "@storybook/react";
import { action } from "@storybook/addon-actions";
import DropDown from "./DropDown";
import type { Meta } from "@storybook/react-vite";
import { BsSlack, BsGithub } from "react-icons/bs";
import { action } from "storybook/actions";
import DropDown from "./DropDown";
const meta = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: "DropDown",
component: DropDown,
} as Meta<typeof DropDown>;
} satisfies Meta<typeof DropDown>;
export default meta;

View File

@@ -1,4 +1,5 @@
import { ReactNode, useEffect, useRef, useState } from "react";
import { type ReactNode, Fragment, useEffect, useRef, useState } from "react";
import ArrowDownIcon from "../../assets/arrow-down-icon.svg";
export type DropDownItem = {
@@ -29,6 +30,15 @@ function DropDown({ items }: DropDownProps) {
const modalRef = useRef<HTMLDivElement>(null);
const handleClickOutside = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setPopupState((prev) => ({
...prev,
isOpen: false,
}));
}
};
useEffect(() => {
if (popupState.isOpen) {
document.addEventListener("mousedown", handleClickOutside);
@@ -41,15 +51,6 @@ function DropDown({ items }: DropDownProps) {
};
}, [popupState.isOpen]);
const handleClickOutside = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setPopupState((prev) => ({
...prev,
isOpen: false,
}));
}
};
return (
<>
<div className="relative flex flex-col items-center">
@@ -62,21 +63,21 @@ function DropDown({ items }: DropDownProps) {
Y: e.pageY,
}));
}}
className="flex items-center justify-between"
className="flex cursor-pointer items-center justify-between"
>
Help
<img src={ArrowDownIcon} className="ml-2 w-[10px] h-[10px]" />
<img src={ArrowDownIcon} className="ml-2 h-[10px] w-[10px]" />
</button>
</div>
{popupState.isOpen && (
<div
ref={modalRef}
className={`z-10 flex flex-col py-1 gap-1 bg-white mt-3 absolute rounded border top-[${popupState.Y}] left-[${popupState.X}] border-gray-200`}
className={`absolute z-10 mt-3 flex flex-col gap-1 rounded-sm border bg-white py-1 top-[${popupState.Y}] left-[${popupState.X}] border-gray-200`}
>
{items.map((item) => (
<>
<Fragment key={item.id}>
{item.isSeparator ? (
<div className="bg-gray-300 h-[1px]" />
<div className="h-[1px] bg-gray-300" />
) : (
<div
onClick={() => {
@@ -86,9 +87,9 @@ function DropDown({ items }: DropDownProps) {
isOpen: false,
}));
}}
className={`cursor-pointer font-normal flex items-center gap-2 py-1 pl-3 pr-7 hover:bg-dropdown ${
className={`flex cursor-pointer items-center gap-2 py-1 pr-7 pl-3 font-normal hover:bg-dropdown ${
item.isDisabled
? "cursor-default hover:bg-transparent text-gray-400"
? "cursor-default text-gray-400 hover:bg-transparent"
: ""
}`}
>
@@ -96,7 +97,7 @@ function DropDown({ items }: DropDownProps) {
<span>{item.text}</span>
</div>
)}
</>
</Fragment>
))}
</div>
)}

View File

@@ -1,4 +1,4 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import { Header } from "./Header";

View File

@@ -1,24 +0,0 @@
import { Meta, StoryObj } from "@storybook/react";
import { within, userEvent } from "@storybook/testing-library";
import { Page } from "./Page";
const meta = {
title: "Example/Page",
component: Page,
parameters: {
// More on Story layout: https://storybook.js.org/docs/react/configure/story-layout
layout: "fullscreen",
},
} satisfies Meta<typeof Page>;
export default meta;
export const LoggedOut = {};
export const LoggedIn: StoryObj<typeof Page> = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const loginButton = await canvas.getByRole("button", { name: /Log in/i });
await userEvent.click(loginButton);
},
};

View File

@@ -1,4 +1,4 @@
import React from "react";
import { useState } from "react";
import { Header } from "../Header/Header";
import "./page.css";
@@ -7,8 +7,8 @@ type User = {
name: string;
};
export const Page: React.VFC = () => {
const [user, setUser] = React.useState<User>();
export const Page = () => {
const [user, setUser] = useState<User>();
return (
<article>

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import StatusLabel, { DeploymentStatus } from "./StatusLabel";
const meta = {

View File

@@ -1,10 +1,5 @@
import { AiOutlineReload } from "react-icons/ai";
type StatusLabelProps = {
status: string;
isRollback?: boolean;
};
export enum DeploymentStatus {
DEPLOYED = "deployed",
FAILED = "failed",
@@ -12,6 +7,11 @@ export enum DeploymentStatus {
SUPERSEDED = "superseded",
}
type StatusLabelProps = {
status: DeploymentStatus;
isRollback?: boolean;
};
export function getStatusColor(status: DeploymentStatus) {
if (status === DeploymentStatus.DEPLOYED) return "text-deployed";
if (status === DeploymentStatus.FAILED) return "text-failed";
@@ -20,7 +20,7 @@ export function getStatusColor(status: DeploymentStatus) {
}
function StatusLabel({ status, isRollback }: StatusLabelProps) {
const statusColor = getStatusColor(status as DeploymentStatus);
const statusColor = getStatusColor(status);
return (
<div
@@ -31,7 +31,7 @@ function StatusLabel({ status, isRollback }: StatusLabelProps) {
justifyContent: "space-between",
}}
>
<span className={`${statusColor} font-bold text-xs`}>
<span className={`${statusColor} text-xs font-bold`}>
{status.toUpperCase()}
</span>
{isRollback && <AiOutlineReload size={14} />}

View File

@@ -1,4 +1,5 @@
import { StoryFn, Meta } from "@storybook/react";
import type { StoryFn, Meta } from "@storybook/react-vite";
import AddRepositoryModal from "./AddRepositoryModal";
const meta = {

View File

@@ -1,12 +1,14 @@
import { useEffect, useState } from "react";
import Modal from "./Modal";
import Spinner from "../Spinner";
import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import apiService from "../../API/apiService";
import { useAppContext } from "../../context/AppContext";
import useAlertError from "../../hooks/useAlertError";
import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import { useAppContext } from "../../context/AppContext";
import { useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import apiService from "../../API/apiService";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import Spinner from "../Spinner";
import Modal from "./Modal";
interface FormKeys {
name: string;
@@ -21,21 +23,22 @@ type AddRepositoryModalProps = {
};
function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
const [formData, setFormData] = useState<FormKeys>({} as FormKeys);
const {
searchParamsObject: { repo_url, repo_name },
} = useCustomSearchParams();
const [formData, setFormData] = useState<FormKeys>({
name: repo_name ?? "",
url: repo_url ?? "",
username: "",
password: "",
});
const [isLoading, setIsLoading] = useState(false);
const alertError = useAlertError();
const { searchParamsObject } = useCustomSearchParams();
const { repo_url, repo_name } = searchParamsObject;
const { setSelectedRepo } = useAppContext();
const navigate = useNavigate();
const navigate = useNavigateWithSearchParams();
const queryClient = useQueryClient();
useEffect(() => {
if (!repo_url || !repo_name) return;
setFormData({ ...formData, name: repo_name, url: repo_url });
}, [repo_url, repo_name, formData]);
const addRepository = () => {
const addRepository = async () => {
const body = new FormData();
body.append("name", formData.name ?? "");
body.append("url", formData.url ?? "");
@@ -44,32 +47,42 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
setIsLoading(true);
apiService
.fetchWithDefaults<void>("/api/helm/repositories", {
try {
await apiService.fetchWithDefaults<void>("/api/helm/repositories", {
method: "POST",
body,
})
.then(() => {
setIsLoading(false);
onClose();
queryClient.invalidateQueries({
queryKey: ["helm", "repositories"],
});
setSelectedRepo(formData.name || "");
navigate(`/repository/${formData.name}`, {
replace: true,
});
})
.catch((error) => {
alertError.setShowErrorModal({
title: "Failed to add repo",
msg: error.message,
});
})
.finally(() => {
setIsLoading(false);
});
setIsLoading(false);
onClose();
await queryClient.invalidateQueries({
queryKey: ["helm", "repositories"],
});
setSelectedRepo(formData.name || "");
const path = `/repository/${formData.name}`;
await navigate(path, {
replace: true,
});
} catch (err) {
alertError.setShowErrorModal({
title: "Failed to add repo",
msg: err instanceof Error ? err.message : String(err),
});
} finally {
setIsLoading(false);
setFormData({
name: "",
url: "",
username: "",
password: "",
});
onClose();
}
};
const handleAddRepository = () => {
void addRepository();
};
return (
@@ -79,11 +92,11 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
isOpen={isOpen}
onClose={onClose}
bottomContent={
<div className="flex justify-end p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
<div className="flex justify-end gap-2 rounded-b border-t border-gray-200 p-6">
<button
data-cy="add-chart-repository-button"
className="flex items-center text-white font-medium px-3 py-1.5 bg-primary hover:bg-add-repo focus:ring-4 focus:outline-none focus:ring-blue-300 disabled:bg-blue-300 rounded-lg text-base text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={addRepository}
className="flex cursor-pointer items-center rounded-lg bg-primary px-3 py-1.5 text-center text-base font-medium text-white hover:bg-add-repo focus:ring-4 focus:ring-blue-300 focus:outline-hidden disabled:bg-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
onClick={handleAddRepository}
disabled={isLoading}
>
{isLoading && <Spinner size={4} />}
@@ -94,7 +107,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
>
<div className="flex gap-x-3">
<label className="flex-1" htmlFor="name">
<div className="mb-2 text-sm require">Name</div>
<div className="require mb-2 text-sm">Name</div>
<input
value={formData.name}
onChange={(e) =>
@@ -108,11 +121,11 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
data-cy="add-chart-name"
type="text"
placeholder="Komodorio"
className="rounded-lg p-2 w-full border border-gray-300 focus:outline-none focus:border-sky-500 input-box-shadow"
className="input-box-shadow w-full rounded-lg border border-gray-300 p-2 focus:border-sky-500 focus:outline-hidden"
/>
</label>
<label className="flex-1" htmlFor="url">
<div className="mb-2 text-sm require">URL</div>
<div className="require mb-2 text-sm">URL</div>
<input
value={formData.url}
onChange={(e) =>
@@ -126,12 +139,12 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
data-cy="add-chart-url"
type="text"
placeholder="https://helm-charts.komodor.io"
className="rounded-lg p-2 w-full border border-gray-300 focus:outline-none focus:border-sky-500 input-box-shadow"
className="input-box-shadow w-full rounded-lg border border-gray-300 p-2 focus:border-sky-500 focus:outline-hidden"
/>
</label>
</div>
<div className="flex gap-x-3">
<label className="flex-1 " htmlFor="username">
<div className="mt-6 flex gap-x-3">
<label className="flex-1" htmlFor="username">
<div className="mb-2 text-sm">Username</div>
<input
onChange={(e) =>
@@ -143,7 +156,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
required
id="username"
type="text"
className="rounded-lg p-2 w-full border border-gray-300 focus:outline-none focus:border-sky-500 input-box-shadow"
className="input-box-shadow w-full rounded-lg border border-gray-300 p-2 focus:border-sky-500 focus:outline-hidden"
/>
</label>
<label className="flex-1" htmlFor="password">
@@ -158,7 +171,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
required
id="password"
type="text"
className="rounded-lg p-2 w-full border border-gray-300 focus:outline-none focus:border-sky-500 input-box-shadow"
className="input-box-shadow w-full rounded-lg border border-gray-300 p-2 focus:border-sky-500 focus:outline-hidden"
/>
</label>
</div>

View File

@@ -1,5 +1,6 @@
import { action } from "@storybook/addon-actions";
import { Meta } from "@storybook/react";
import type { Meta } from "@storybook/react-vite";
import { action } from "storybook/actions";
import ErrorModal from "./ErrorModal";
const meta = {

View File

@@ -14,7 +14,7 @@ export default function ErrorModal({
contentText,
}: ErrorModalProps) {
const ErrorTitle = (
<div className="font-medium text-2xl text-error-color">
<div className="text-2xl font-medium text-error-color">
<div className="flex gap-3">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -33,13 +33,14 @@ export default function ErrorModal({
);
const bottomContent = (
<div className="flex py-6 px-4 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
<span className="text-sm text-muted fs-80 text-gray-500">
<div className="flex gap-2 rounded-b border-t border-gray-200 px-4 py-6 dark:border-gray-600">
<span className="fs-80 text-sm text-gray-500 text-muted">
Hint: Komodor has the same HELM capabilities, with enterprise features
and support.{" "}
<a
href="https://www.komodor.com/helm-dash/?utm_campaign=Helm%20Dashboard%20%7C%20CTA&utm_source=helm-dash&utm_medium=cta&utm_content=helm-dash"
target="_blank" rel="noreferrer"
target="_blank"
rel="noreferrer"
>
<span className="text-link-color underline">Sign up for free.</span>
</a>
@@ -49,15 +50,13 @@ export default function ErrorModal({
return (
<Modal
containerClassNames={
"border-2 border-error-border-color bg-error-background w-2/3"
}
containerClassNames={"error-dialog w-2/3"}
title={ErrorTitle}
isOpen={isOpen}
onClose={onClose}
bottomContent={bottomContent}
>
<p className="text-error-color border-green-400">{contentText}</p>
<p className="border-green-400 text-error-color">{contentText}</p>
</Modal>
);
}

View File

@@ -14,7 +14,7 @@ export default function GlobalErrorModal({
contentText,
}: ErrorModalProps) {
const ErrorTitle = (
<div className="font-medium text-2xl text-error-color">
<div className="text-2xl font-medium text-error-color">
<div className="flex gap-3">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -34,9 +34,7 @@ export default function GlobalErrorModal({
return (
<Modal
containerClassNames={
"border-2 border-error-border-color bg-error-background w-3/5 "
}
containerClassNames={"error-dialog w-3/5"}
title={ErrorTitle}
isOpen={isOpen}
onClose={onClose}
@@ -57,7 +55,7 @@ export default function GlobalErrorModal({
>
<p
style={{ minWidth: "500px" }}
className="text-error-color border-green-400 text-sm"
className="border-green-400 text-sm text-error-color"
>
{contentText}
</p>

View File

@@ -1,6 +1,10 @@
import hljs from "highlight.js";
import hljs from "highlight.js/lib/core";
import yaml from "highlight.js/lib/languages/yaml";
import Spinner from "../../Spinner";
hljs.registerLanguage("yaml", yaml);
export const ChartValues = ({
chartValues,
loading,
@@ -11,13 +15,13 @@ export const ChartValues = ({
return (
<div className="w-1/2">
<label
className="block tracking-wide text-gray-700 text-xl font-medium mb-2"
className="mb-2 block text-xl font-medium tracking-wide text-gray-700"
htmlFor="grid-user-defined-values"
>
Chart Value Reference:
</label>
<pre
className="text-base bg-chart-values p-2 rounded font-medium w-full max-h-[330px] block overflow-y-auto font-sf-mono"
className="block max-h-[330px] w-full overflow-y-auto rounded-sm bg-chart-values p-2 font-sf-mono text-base font-medium"
dangerouslySetInnerHTML={
chartValues && !loading
? {

View File

@@ -8,14 +8,14 @@ interface DefinedValuesProps {
loading: boolean;
}
export const DefinedValues = ({
const DefinedValues = ({
initialValue,
chartValues,
onUserValuesChange,
loading,
}: DefinedValuesProps) => {
return (
<div className="flex w-full gap-6 mt-4">
<div className="mt-4 flex w-full gap-6">
<UserDefinedValues
initialValue={initialValue}
onValuesChange={onUserValuesChange}
@@ -24,3 +24,5 @@ export const DefinedValues = ({
</div>
);
};
export default DefinedValues;

View File

@@ -1,5 +1,6 @@
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useParams } from "react-router";
import useDebounce from "../../../hooks/useDebounce";
export const GeneralDetails = ({
@@ -17,14 +18,17 @@ export const GeneralDetails = ({
onReleaseNameInput: (chartName: string) => void;
}) => {
const [namespaceInputValue, setNamespaceInputValue] = useState(namespace);
const namespaceInputValueDebounced = useDebounce<string>(namespaceInputValue, 500);
const namespaceInputValueDebounced = useDebounce<string>(
namespaceInputValue,
500
);
useEffect(() => {
onNamespaceInput(namespaceInputValueDebounced);
onNamespaceInput(namespaceInputValueDebounced);
}, [namespaceInputValueDebounced, onNamespaceInput]);
const { context } = useParams();
const inputClassName = ` text-lg py-1 px-2 border border-1 border-gray-300 ${
disabled ? "bg-gray-200" : "bg-white "
} rounded`;
} rounded-sm`;
return (
<div className="flex gap-8">
<div>

View File

@@ -1,38 +1,53 @@
import { useParams } from "react-router-dom";
import { useMemo, useState } from "react";
import { useMutation } from "@tanstack/react-query";
import {
useEffect,
useEffectEvent,
useMemo,
useState,
lazy,
Suspense,
} from "react";
import { useParams } from "react-router";
import { BsPencil, BsX } from "react-icons/bs";
import apiService from "../../../API/apiService";
import type { LatestChartVersion } from "../../../API/interfaces";
import {
type VersionData,
useChartReleaseValues,
useGetReleaseManifest,
useGetVersions,
useVersionData,
} from "../../../API/releases";
import Modal, { ModalButtonStyle } from "../Modal";
import { GeneralDetails } from "./GeneralDetails";
import { ManifestDiff } from "./ManifestDiff";
import { useMutation } from "@tanstack/react-query";
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
import { VersionToInstall } from "./VersionToInstall";
import { isNewerVersion, isNoneEmptyArray } from "../../../utils";
import useCustomSearchParams from "../../../hooks/useCustomSearchParams";
import { useChartRepoValues } from "../../../API/repositories";
import { useDiffData } from "../../../API/shared";
import { InstallChartModalProps } from "../../../data/types";
import { DefinedValues } from "./DefinedValues";
import apiService from "../../../API/apiService";
import type { InstallChartModalProps } from "../../../data/types";
import useCustomSearchParams from "../../../hooks/useCustomSearchParams";
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
import { isNoneEmptyArray } from "../../../utils";
import Spinner from "../../Spinner";
import Modal, { ModalButtonStyle } from "../Modal";
import { GeneralDetails } from "./GeneralDetails";
import { VersionToInstall } from "./VersionToInstall";
import { InstallUpgradeTitle } from "./InstallUpgradeTitle";
const DefinedValues = lazy(() => import("./DefinedValues"));
const ManifestDiff = lazy(() => import("./ManifestDiff"));
export const InstallReleaseChartModal = ({
isOpen,
onClose,
chartName,
currentlyInstalledChartVersion,
latestVersion,
isUpgrade = false,
latestRevision,
}: InstallChartModalProps) => {
const navigate = useNavigateWithSearchParams();
const [userValues, setUserValues] = useState<string>();
const [userValues, setUserValues] = useState<string>("");
const [installError, setInstallError] = useState("");
const [forceUpgrade, setForceUpgrade] = useState(false);
const {
namespace: queryNamespace,
@@ -44,40 +59,43 @@ export const InstallReleaseChartModal = ({
const [namespace, setNamespace] = useState(queryNamespace || "");
const [releaseName, setReleaseName] = useState(_releaseName || "");
const { error: versionsError, data: _versions } = useGetVersions(chartName, {
select: (data) => {
return data?.sort((a, b) =>
isNewerVersion(a.version, b.version) ? 1 : -1
);
},
onSuccess: (data) => {
const empty = { version: "", repository: "", urls: [] };
return setSelectedVersionData(data[0] ?? empty);
},
const {
error: versionsError,
data: _versions = [],
isSuccess,
isLoading: isLoadingVersions,
} = useGetVersions(chartName);
const [selectedVersionData, setSelectedVersionData] = useState<VersionData>();
const [versions, setVersions] = useState<
Array<LatestChartVersion & { isChartVersion: boolean }>
>([]);
const onSuccess = useEffectEvent(() => {
const empty = { version: "", repository: "", urls: [] };
setSelectedVersionData(_versions[0] ?? empty);
setVersions(
_versions?.map((v) => ({
...v,
isChartVersion: v.version === currentlyInstalledChartVersion,
}))
);
});
const versions = _versions?.map((v) => ({
...v,
isChartVersion: v.version === currentlyInstalledChartVersion,
}));
useEffect(() => {
if (isSuccess && _versions.length) {
onSuccess();
}
}, [isSuccess, _versions]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
latestVersion = latestVersion ?? currentlyInstalledChartVersion; // a guard for typescript, latestVersion is always defined
const [selectedVersionData, setSelectedVersionData] = useState<{
version: string;
repository?: string;
urls: string[];
}>();
const selectedVersion = selectedVersionData?.version || "";
const selectedRepo = selectedVersionData?.repository || "";
const selectedVersion = useMemo(() => {
return selectedVersionData?.version;
}, [selectedVersionData]);
const [chartURL, setChartURL] = useState("");
const [useURLMode, setUseURLMode] = useState(false);
const selectedRepo = useMemo(() => {
return selectedVersionData?.repository || "";
}, [selectedVersionData]);
const chartAddress = useMemo(() => {
const repoChartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData.repository) return "";
return selectedVersionData.urls?.[0]?.startsWith("file://")
@@ -85,14 +103,16 @@ export const InstallReleaseChartModal = ({
: `${selectedVersionData.repository}/${chartName}`;
}, [selectedVersionData, chartName]);
const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
// the original chart values
const { data: chartValues } = useChartRepoValues({
version: selectedVersion || "",
const { data: chartValues = "" } = useChartRepoValues({
version: selectedVersion,
chart: chartAddress,
});
// The user defined values (if any we're set)
const { data: releaseValues, isLoading: loadingReleaseValues } =
const { data: releaseValues = "", isLoading: loadingReleaseValues } =
useChartReleaseValues({
namespace,
release: String(releaseName),
@@ -100,16 +120,15 @@ export const InstallReleaseChartModal = ({
});
// This hold the selected version manifest, we use it for the diff
const { data: selectedVerData, error: selectedVerDataError } = useVersionData(
{
version: selectedVersion || "",
userValues: userValues || "",
const { data: selectedVerData = {}, error: selectedVerDataError } =
useVersionData({
version: selectedVersion,
userValues,
chartAddress,
releaseValues,
namespace,
releaseName,
}
);
});
const { data: currentVerManifest, error: currentVerManifestError } =
useGetReleaseManifest({
@@ -123,15 +142,15 @@ export const InstallReleaseChartModal = ({
error: diffError,
} = useDiffData({
selectedRepo,
versionsError: versionsError as string,
currentVerManifest,
versionsError: versionsError as unknown as string, // TODO fix it
currentVerManifest: currentVerManifest as unknown as string, // TODO fix it
selectedVerData,
chart: chartAddress,
});
// Confirm method (install)
const setReleaseVersionMutation = useMutation(
[
const setReleaseVersionMutation = useMutation<VersionData, Error>({
mutationKey: [
"setVersion",
namespace,
releaseName,
@@ -140,7 +159,7 @@ export const InstallReleaseChartModal = ({
selectedCluster,
chartAddress,
],
async () => {
mutationFn: async () => {
setInstallError("");
const formData = new FormData();
formData.append("preview", "false");
@@ -149,34 +168,36 @@ export const InstallReleaseChartModal = ({
}
formData.append("version", selectedVersion || "");
formData.append("values", userValues || releaseValues || ""); // if userValues is empty, we use the release values
if (forceUpgrade) {
formData.append("force", "true");
}
const url = `/api/helm/releases/${
namespace ? namespace : "default"
}/${releaseName}`;
const data = await apiService.fetchWithDefaults(
`/api/helm/releases/${
namespace ? namespace : "default"
}${`/${releaseName}`}`,
{
return await apiService.fetchWithSafeDefaults<VersionData>({
url,
options: {
method: "post",
body: formData,
}
);
return data;
},
fallback: { version: "", urls: [""] },
});
},
{
onSuccess: async (response) => {
onClose();
setSelectedVersionData({ version: "", urls: [] }); //cleanup
navigate(
`/${
namespace ? namespace : "default"
}/${releaseName}/installed/revision/${response.version}`
);
window.location.reload();
},
onError: (error) => {
setInstallError((error as Error)?.message || "Failed to update");
},
}
);
onSuccess: async (response) => {
onClose();
setSelectedVersionData({ version: "", urls: [] }); //cleanup
await navigate(
`/${
namespace ? namespace : "default"
}/${releaseName}/installed/revision/${response.version}`
);
window.location.reload();
},
onError: (error) => {
setInstallError(error?.message || "Failed to update");
},
});
return (
<Modal
@@ -189,31 +210,80 @@ export const InstallReleaseChartModal = ({
title={
<InstallUpgradeTitle
isUpgrade={isUpgrade}
releaseValues={isUpgrade || releaseValues}
releaseValues={isUpgrade || !!releaseValues}
chartName={chartName}
/>
}
containerClassNames="w-full text-2xl h-2/3"
bottomContent={
isUpgrade ? (
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={forceUpgrade}
onChange={(e) => setForceUpgrade(e.target.checked)}
/>
Force upgrade
</label>
) : undefined
}
actions={[
{
id: "1",
callback: setReleaseVersionMutation.mutate,
variant: ModalButtonStyle.info,
isLoading: setReleaseVersionMutation.isLoading,
isLoading: setReleaseVersionMutation.isPending,
disabled:
loadingReleaseValues ||
isLoadingDiff ||
setReleaseVersionMutation.isLoading,
setReleaseVersionMutation.isPending,
},
]}
>
{versions && isNoneEmptyArray(versions) && (
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion
/>
{isLoadingVersions ? (
<Spinner />
) : !useURLMode && versions && isNoneEmptyArray(versions) ? (
<div className="flex items-center gap-2">
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion
/>
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to URL"
onClick={() => setUseURLMode(true)}
>
<BsPencil className="text-lg" />
</button>
</div>
) : (
<div className="flex items-end gap-2">
<div className="flex-1">
<h4 className="text-lg">Chart URL:</h4>
<input
className="w-full rounded-sm border border-1 border-gray-300 bg-white px-2 py-1 text-lg"
value={chartURL}
onChange={(e) => setChartURL(e.target.value)}
placeholder="oci://registry-1.docker.io/example/chart"
/>
</div>
{versions && isNoneEmptyArray(versions) && (
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to repository"
onClick={() => {
setUseURLMode(false);
setChartURL("");
}}
>
<BsX className="text-2xl" />
</button>
)}
</div>
)}
<GeneralDetails
@@ -224,24 +294,28 @@ export const InstallReleaseChartModal = ({
onNamespaceInput={setNamespace}
/>
<DefinedValues
initialValue={releaseValues}
onUserValuesChange={(values: string) => setUserValues(values)}
chartValues={chartValues}
loading={loadingReleaseValues}
/>
<Suspense fallback={<Spinner />}>
<DefinedValues
initialValue={releaseValues}
onUserValuesChange={(values: string) => setUserValues(values)}
chartValues={chartValues}
loading={loadingReleaseValues}
/>
</Suspense>
<ManifestDiff
diff={diffData as string}
isLoading={isLoadingDiff}
error={
(currentVerManifestError as string) ||
(selectedVerDataError as string) ||
(diffError as string) ||
installError ||
(versionsError as string)
}
/>
<Suspense fallback={<Spinner />}>
<ManifestDiff
diff={diffData as string}
isLoading={isLoadingDiff}
error={
(currentVerManifestError as unknown as string) || // TODO fix it
(selectedVerDataError as unknown as string) ||
(diffError as unknown as string) ||
installError ||
(versionsError as unknown as string)
}
/>
</Suspense>
</Modal>
);
};

View File

@@ -1,27 +1,41 @@
import { useParams } from "react-router-dom";
import { useMemo, useState } from "react";
import { useGetVersions, useVersionData } from "../../../API/releases";
import Modal, { ModalButtonStyle } from "../Modal";
import { GeneralDetails } from "./GeneralDetails";
import { ManifestDiff } from "./ManifestDiff";
import { useMutation } from "@tanstack/react-query";
import { useChartRepoValues } from "../../../API/repositories";
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
import { VersionToInstall } from "./VersionToInstall";
import { isNewerVersion, isNoneEmptyArray } from "../../../utils";
import { useDiffData } from "../../../API/shared";
import { InstallChartModalProps } from "../../../data/types";
import { DefinedValues } from "./DefinedValues";
import {
lazy,
Suspense,
useEffect,
useEffectEvent,
useMemo,
useState,
} from "react";
import { useParams } from "react-router";
import { BsPencil, BsX } from "react-icons/bs";
import apiService from "../../../API/apiService";
import type { LatestChartVersion } from "../../../API/interfaces";
import { useGetVersions, useVersionData } from "../../../API/releases";
import { useChartRepoValues } from "../../../API/repositories";
import { useDiffData } from "../../../API/shared";
import type { InstallChartModalProps } from "../../../data/types";
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams";
import { isNoneEmptyArray } from "../../../utils";
import Spinner from "../../Spinner";
import Modal, { ModalButtonStyle } from "../Modal";
import { GeneralDetails } from "./GeneralDetails";
import { InstallUpgradeTitle } from "./InstallUpgradeTitle";
import { VersionToInstall } from "./VersionToInstall";
const DefinedValues = lazy(() => import("./DefinedValues"));
const ManifestDiff = lazy(() => import("./ManifestDiff"));
export const InstallRepoChartModal = ({
isOpen,
onClose,
chartName,
currentlyInstalledChartVersion,
latestVersion,
}: InstallChartModalProps) => {
urlMode: initialURLMode = false,
}: InstallChartModalProps & { urlMode?: boolean }) => {
const navigate = useNavigateWithSearchParams();
const [userValues, setUserValues] = useState("");
const [installError, setInstallError] = useState("");
@@ -31,44 +45,51 @@ export const InstallRepoChartModal = ({
const [namespace, setNamespace] = useState("");
const [releaseName, setReleaseName] = useState(chartName);
const { error: versionsError, data: _versions } = useGetVersions(chartName, {
select: (data) => {
return data?.sort((a, b) =>
isNewerVersion(a.version, b.version) ? 1 : -1
);
},
onSuccess: (data) => {
const empty = { version: "", repository: "", urls: [] };
const versionsToRepo = data.filter(
(v) => v.repository === currentRepoCtx
);
const {
error: versionsError,
data: _versions = [],
isSuccess,
} = useGetVersions(chartName);
return setSelectedVersionData(versionsToRepo[0] ?? empty);
},
});
const [versions, setVersions] = useState<
Array<LatestChartVersion & { isChartVersion: boolean }>
>([]);
const versions = _versions?.map((v) => ({
...v,
isChartVersion: v.version === currentlyInstalledChartVersion,
}));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
latestVersion = latestVersion ?? currentlyInstalledChartVersion; // a guard for typescript, latestVersion is always defined
const [selectedVersionData, setSelectedVersionData] = useState<{
version: string;
repository?: string;
urls: string[];
}>();
const selectedVersion = useMemo(() => {
return selectedVersionData?.version;
}, [selectedVersionData]);
const onSuccess = useEffectEvent(() => {
const empty = { version: "", repository: "", urls: [] };
const versionsToRepo = _versions.filter(
(v) => v.repository === currentRepoCtx
);
const selectedRepo = useMemo(() => {
return selectedVersionData?.repository;
}, [selectedVersionData]);
setSelectedVersionData(versionsToRepo[0] ?? empty);
setVersions(
_versions?.map((v) => ({
...v,
isChartVersion: v.version === currentlyInstalledChartVersion,
}))
);
});
const chartAddress = useMemo(() => {
useEffect(() => {
if (isSuccess && _versions.length) {
onSuccess();
}
}, [isSuccess, _versions]);
const selectedVersion = selectedVersionData?.version;
const selectedRepo = selectedVersionData?.repository;
const [chartURL, setChartURL] = useState("");
const [useURLMode, setUseURLMode] = useState(initialURLMode);
const repoChartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData?.repository) {
return "";
}
@@ -77,15 +98,17 @@ export const InstallRepoChartModal = ({
: `${selectedVersionData?.repository}/${chartName}`;
}, [selectedVersionData, chartName]);
const { data: chartValues, isLoading: loadingChartValues } =
const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
const { data: chartValues = "", isLoading: loadingChartValues } =
useChartRepoValues({
version: selectedVersion || "",
chart: chartAddress,
});
// This hold the selected version manifest, we use it for the diff
const { data: selectedVerData, error: selectedVerDataError } = useVersionData(
{
const { data: selectedVerData = {}, error: selectedVerDataError } =
useVersionData({
version: selectedVersion || "",
userValues,
chartAddress,
@@ -93,11 +116,8 @@ export const InstallRepoChartModal = ({
namespace,
releaseName,
isInstallRepoChart: true,
options: {
enabled: Boolean(chartAddress),
},
}
);
enabled: Boolean(chartAddress),
});
const {
data: diffData,
@@ -105,15 +125,18 @@ export const InstallRepoChartModal = ({
error: diffError,
} = useDiffData({
selectedRepo: selectedRepo || "",
versionsError: versionsError as string,
currentVerManifest: "", // current version manifest should always be empty since its a fresh install
versionsError: versionsError as unknown as string, // TODO fix it
currentVerManifest: "", // current version manifest should always be empty since it's a fresh install
selectedVerData,
chart: chartAddress,
});
// Confirm method (install)
const setReleaseVersionMutation = useMutation(
[
const setReleaseVersionMutation = useMutation<{
namespace: string;
name: string;
}>({
mutationKey: [
"setVersion",
namespace,
releaseName,
@@ -122,7 +145,7 @@ export const InstallRepoChartModal = ({
selectedCluster,
chartAddress,
],
async () => {
mutationFn: async () => {
setInstallError("");
const formData = new FormData();
formData.append("preview", "false");
@@ -130,27 +153,27 @@ export const InstallRepoChartModal = ({
formData.append("version", selectedVersion || "");
formData.append("values", userValues);
formData.append("name", releaseName || "");
const data = await apiService.fetchWithDefaults(
`/api/helm/releases/${namespace ? namespace : "default"}`,
{
return await apiService.fetchWithSafeDefaults({
url: `/api/helm/releases/${namespace ? namespace : "default"}`,
options: {
method: "post",
body: formData,
}
);
return data;
},
fallback: { namespace: "", name: "" },
});
},
{
onSuccess: async (response) => {
onClose();
navigate(
`/${response.namespace}/${response.name}/installed/revision/1`
);
},
onError: (error) => {
setInstallError((error as Error)?.message || "Failed to update");
},
}
);
onSuccess: async (response: { namespace: string; name: string }) => {
onClose();
await navigate(
`/${response.namespace}/${response.name}/installed/revision/1`
);
},
onError: (error) => {
setInstallError(error?.message || "Failed to update");
},
});
return (
<Modal
@@ -160,11 +183,15 @@ export const InstallRepoChartModal = ({
onClose();
}}
title={
<InstallUpgradeTitle
isUpgrade={false}
releaseValues={false}
chartName={chartName}
/>
initialURLMode ? (
<div className="font-bold">Install from URL</div>
) : (
<InstallUpgradeTitle
isUpgrade={false}
releaseValues={false}
chartName={chartName}
/>
)
}
containerClassNames="w-full text-2xl h-2/3"
actions={[
@@ -172,21 +199,56 @@ export const InstallRepoChartModal = ({
id: "1",
callback: setReleaseVersionMutation.mutate,
variant: ModalButtonStyle.info,
isLoading: setReleaseVersionMutation.isLoading,
isLoading: setReleaseVersionMutation.isPending,
disabled:
loadingChartValues ||
isLoadingDiff ||
setReleaseVersionMutation.isLoading,
setReleaseVersionMutation.isPending,
},
]}
>
{versions && isNoneEmptyArray(versions) && (
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion={false}
/>
{!useURLMode && versions && isNoneEmptyArray(versions) ? (
<div className="flex items-center gap-2">
<VersionToInstall
versions={versions}
initialVersion={selectedVersionData}
onSelectVersion={setSelectedVersionData}
showCurrentVersion={false}
/>
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to URL"
onClick={() => setUseURLMode(true)}
>
<BsPencil className="text-lg" />
</button>
</div>
) : (
<div className="flex items-end gap-2">
<div className="flex-1">
<h4 className="text-lg">Chart URL:</h4>
<input
className="w-full rounded-sm border border-1 border-gray-300 bg-white px-2 py-1 text-lg"
value={chartURL}
onChange={(e) => setChartURL(e.target.value)}
placeholder="oci://registry-1.docker.io/example/chart:1.0.0"
/>
</div>
{versions && isNoneEmptyArray(versions) && (
<button
type="button"
className="cursor-pointer p-1 text-gray-400 hover:text-gray-600"
title="Switch to repository"
onClick={() => {
setUseURLMode(false);
setChartURL("");
}}
>
<BsX className="text-2xl" />
</button>
)}
</div>
)}
<GeneralDetails
@@ -204,16 +266,18 @@ export const InstallRepoChartModal = ({
loading={loadingChartValues}
/>
<ManifestDiff
diff={diffData as string}
isLoading={isLoadingDiff}
error={
(selectedVerDataError as string) ||
(diffError as string) ||
installError ||
(versionsError as string)
}
/>
<Suspense fallback={<Spinner />}>
<ManifestDiff
diff={diffData as string}
isLoading={isLoadingDiff}
error={
(selectedVerDataError as unknown as string) || // TODO fix it
(diffError as unknown as string) ||
installError ||
(versionsError as unknown as string)
}
/>
</Suspense>
</Modal>
);
};

View File

@@ -1,4 +1,4 @@
import { FC } from "react";
import type { FC } from "react";
interface InstallUpgradeProps {
isUpgrade: boolean;
@@ -17,7 +17,7 @@ export const InstallUpgradeTitle: FC<InstallUpgradeProps> = ({
<div className="font-bold">
{`${text}`}
{(isUpgrade || releaseValues) && (
<span className="text-green-700">{chartName}</span>
<span className="ml-1 text-green-700">{chartName}</span>
)}
</div>
);

View File

@@ -1,9 +1,12 @@
import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-base";
import hljs from "highlight.js";
import hljs from "highlight.js/lib/core";
import yaml from "highlight.js/lib/languages/yaml";
import { useEffect, useRef } from "react";
import Spinner from "../../Spinner";
import { diffConfiguration } from "../../../utils";
import Spinner from "../../Spinner";
hljs.registerLanguage("yaml", yaml);
interface ManifestDiffProps {
diff?: string;
@@ -11,7 +14,7 @@ interface ManifestDiffProps {
error: string;
}
export const ManifestDiff = ({ diff, isLoading, error }: ManifestDiffProps) => {
const ManifestDiff = ({ diff, isLoading, error }: ManifestDiffProps) => {
const diffContainerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
@@ -35,7 +38,7 @@ export const ManifestDiff = ({ diff, isLoading, error }: ManifestDiffProps) => {
if (isLoading && !error) {
return (
<div className="flex text-lg items-end">
<div className="flex items-end text-lg">
<Spinner />
Calculating diff...
</div>
@@ -47,7 +50,7 @@ export const ManifestDiff = ({ diff, isLoading, error }: ManifestDiffProps) => {
<h4 className="text-xl">Manifest changes:</h4>
{error ? (
<p className="text-red-600 text-lg">
<p className="text-lg text-red-600">
Failed to get upgrade info: {error.toString()}
</p>
) : diff ? (
@@ -63,3 +66,5 @@ export const ManifestDiff = ({ diff, isLoading, error }: ManifestDiffProps) => {
</div>
);
};
export default ManifestDiff;

Some files were not shown because too many files have changed in this diff Show More