70 Commits

Author SHA1 Message Date
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
148 changed files with 18615 additions and 35202 deletions

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 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. 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 description: A GUI Dashboard for Helm by Komodor
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/refs/heads/main/images/logo.svg" icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/refs/heads/main/images/logo.svg"
version: 2.0.3 version: 2.0.4
appVersion: "2.0.3" appVersion: "2.0.4"

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: # Ignore artifacts:
build build
coverage coverage
.env
.gitignore
.npmrc
.prettierignore
yarn.lock
package-lock.json
.flowbite-react/*

View File

@@ -2,3 +2,7 @@ trailingComma: "es5"
tabWidth: 2 tabWidth: 2
semi: true semi: true
singleQuote: false 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"; import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = { const config: StorybookConfig = {
stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"], stories: ["../src/**/*.stories.@(js|jsx|ts|tsx|mdx)"],
addons: [
"@storybook/addon-actions", addons: ["@storybook/addon-links", "@storybook/addon-docs"],
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-styling",
{
name: "@storybook/addon-styling",
},
"@storybook/addon-mdx-gfm",
],
core: {}, core: {},
framework: { framework: {
name: "@storybook/react-vite", name: "@storybook/react-vite",
options: {}, options: {},
}, },
docs: {
autodocs: true,
},
}; };
export default config; 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. Welcome to the frontend of the helm dashboard.
We care most about keeping the project: We care most about keeping the project:
1. Maintainable 1. Maintainable
2. Extendable 2. Extendable
3. Contributor friendly 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). 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. run the backend server. This is also explained in the above link.
2. go to `frontend` in your local project. 3. go to `frontend` in your local project.
3. in order to install dependencies and start the development server 4. in order to install dependencies and start the development server
- `npm i` - `npm i`
- `npm run dev` - `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 # Component library

View File

@@ -1,6 +1,7 @@
import { defineConfig } from "cypress"; import { defineConfig } from "cypress";
export default defineConfig({ export default defineConfig({
allowCypressEnv: false,
component: { component: {
devServer: { devServer: {
framework: "react", framework: "react",
@@ -9,8 +10,9 @@ export default defineConfig({
}, },
e2e: { e2e: {
setupNodeEvents(on, config) { baseUrl: "http://localhost:5173",
// implement node event listeners here // 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']"; const addChartRepositoryButton = "[data-cy='add-chart-repository-button']";
it("Adding new chart repository", () => { it("Adding new chart repository", () => {
cy.intercept("GET", "http://localhost:5173/status", { cy.intercept("GET", "/status", {
fixture: "status.json", fixture: "status.json",
}).as("status"); }).as("status");
cy.intercept("GET", "http://localhost:5173/api/helm/releases", { cy.intercept("GET", "/api/helm/releases", {
fixture: "releases.json", fixture: "releases.json",
}).as("releases"); }).as("releases");
cy.visit( cy.visit("/#/minikube/installed?filteredNamespace=default");
"http://localhost:5173/#/minikube/installed?filteredNamespace=default"
);
cy.get("[data-cy='navigation-link']").contains("Repository").click(); cy.get("[data-cy='navigation-link']").contains("Repository").click();
cy.get("[data-cy='install-repository-button']").click(); cy.get("[data-cy='install-repository-button']").click();
@@ -22,11 +20,12 @@ describe("Adding repository flow", () => {
cy.get(addChartNameInput).type("Komodorio"); cy.get(addChartNameInput).type("Komodorio");
cy.get(addChartUrlInput).type("https://helm-charts.komodor.io"); 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", fixture: "repositories.json",
}).as("repositories"); }).as("repositories");
cy.get(addChartRepositoryButton).click(); cy.get(addChartRepositoryButton).click();
cy.wait("@repositories");
cy.contains("https://helm-charts.komodor.io"); cy.contains("https://helm-charts.komodor.io");
@@ -36,15 +35,13 @@ describe("Adding repository flow", () => {
.contains("Install") .contains("Install")
.click(); .click();
cy.intercept("POST", "http://localhost:5173/api/helm/releases/default", { cy.intercept("POST", "/api/helm/releases/default", {
fixture: "defaultReleases.json", fixture: "defaultReleases.json",
}).as("defaultReleases"); }).as("defaultReleases");
cy.intercept( cy.intercept("GET", "/api/helm/releases/default/helm-dashboard/history", {
"GET", fixture: "history.json",
"http://localhost:5173/api/helm/releases/default/helm-dashboard/history", }).as("history");
{ fixture: "history.json" }
).as("history");
cy.contains("Confirm").click(); cy.contains("Confirm").click();

View File

@@ -1,6 +1,7 @@
import "./commands"; import "./commands";
import { mount } from "cypress/react18"; import { mount } from "cypress/react";
/* eslint-disable @typescript-eslint/no-namespace */
declare global { declare global {
namespace Cypress { namespace Cypress {
interface Chainable { interface Chainable {
@@ -8,5 +9,5 @@ declare global {
} }
} }
} }
/* eslint-enable @typescript-eslint/no-namespace */
Cypress.Commands.add("mount", mount); 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"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Helm Dashboard</title> <title>Helm Dashboard</title>
<script src="/assets/analytics.js"></script> <script type="module" src="/analytics.js"></script>
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css"

34159
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,101 @@
{ {
"name": "dashboard", "name": "dashboard",
"version": "1.0.0", "version": "1.1.0",
"type": "module", "type": "module",
"description": "", "description": "",
"main": "index.js", "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-intersection-observer": "^9.16.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": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "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", "storybook": "storybook dev -p 6006",
"build-storybook": "storybook build", "storybook:build": "storybook build",
"lint": "npx eslint src/", "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": "npx prettier src/ --check",
"prettier:fix": "npm run prettier -- --write", "prettier:fix": "npm run prettier -- --write",
"cypress:open": "cypress open", "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": [], "keywords": [],
"author": "", "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" referrerPolicy: "no-referrer"
}; };
xhr.onload = function() { xhr.onload = function() {
if (xhr.readyState === XMLHttpRequest.DONE) { if (xhr.readyState !== XMLHttpRequest.DONE) {
const status = JSON.parse(xhr.responseText); 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; const version = status.CurVer;
if (status.Analytics) { if (status.Analytics) {
enableDD(version); enableDD(version);
@@ -23,7 +39,6 @@ xhr.onload = function() {
} else { } else {
console.log("Analytics is disabled in this session"); console.log("Analytics is disabled in this session");
} }
}
}; };
xhr.open("GET", "/status", true); xhr.open("GET", "/status", true);
xhr.send(null); xhr.send(null);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,49 +4,60 @@ import {
useMutation, useMutation,
useQuery, useQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { HelmRepositories } from "./interfaces";
import apiService from "./apiService"; import apiService from "./apiService";
import type { HelmRepositories } from "./interfaces";
// Get list of Helm repositories // Get list of Helm repositories
export function useGetRepositories( export function useGetRepositories(
options?: UseQueryOptions<HelmRepositories> options?: UseQueryOptions<HelmRepositories>
) { ) {
return useQuery<HelmRepositories>( return useQuery<HelmRepositories>({
["helm", "repositories"], queryKey: ["helm", "repositories"],
() => queryFn: () =>
apiService.fetchWithDefaults<HelmRepositories>("/api/helm/repositories"), apiService.fetchWithSafeDefaults<HelmRepositories>({
options url: "/api/helm/repositories",
); fallback: [],
}),
select: (data) => data?.sort((a, b) => a?.name?.localeCompare(b?.name)),
...(options ?? {}),
});
} }
// Update repository from remote // Update repository from remote
export function useUpdateRepo( export function useUpdateRepo(
repo: string, repo: string,
options?: UseMutationOptions<void, unknown, void> options?: UseMutationOptions<string, Error>
) { ) {
return useMutation<void, unknown, void>(() => { return useMutation<string, Error>({
return apiService.fetchWithDefaults<void>( mutationFn: () => {
`/api/helm/repositories/${repo}`, return apiService.fetchWithDefaults<string>(
{ `/api/helm/repositories/${repo}`,
method: "POST", {
} method: "POST",
); }
}, options); );
},
...(options ?? {}),
});
} }
// Remove repository // Remove repository
export function useDeleteRepo( export function useDeleteRepo(
repo: string, repo: string,
options?: UseMutationOptions<void, unknown, void> options?: UseMutationOptions<string, Error>
) { ) {
return useMutation<void, unknown, void>(() => { return useMutation<string, Error>({
return apiService.fetchWithDefaults<void>( mutationFn: () => {
`/api/helm/repositories/${repo}`, return apiService.fetchWithDefaults<string>(
{ `/api/helm/repositories/${repo}`,
method: "DELETE", {
} method: "DELETE",
); }
}, options); );
},
...(options ?? {}),
});
} }
export function useChartRepoValues({ export function useChartRepoValues({
@@ -56,17 +67,15 @@ export function useChartRepoValues({
version: string; version: string;
chart: string; chart: string;
}) { }) {
return useQuery<string>( return useQuery<string>({
["helm", "repositories", "values", chart, version], queryKey: ["helm", "repositories", "values", chart, version],
() => queryFn: () =>
apiService.fetchWithDefaults<string>( apiService.fetchWithDefaults<string>(
`/api/helm/repositories/values?chart=${chart}&version=${version}`, `/api/helm/repositories/values?chart=${chart}&version=${version}`,
{ {
headers: { "Content-Type": "text/plain; charset=utf-8" }, 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, useMutation,
useQuery, useQuery,
} from "@tanstack/react-query"; } from "@tanstack/react-query";
import { ScanResult, ScanResults, ScannersList } from "./interfaces";
import apiService from "./apiService"; import apiService from "./apiService";
import {
type ScanResult,
type ScanResults,
type ScannersList,
} from "./interfaces";
// Get list of discovered scanners // Get list of discovered scanners
// @ts-expect-error unused
function useGetDiscoveredScanners(options?: UseQueryOptions<ScannersList>) { function useGetDiscoveredScanners(options?: UseQueryOptions<ScannersList>) {
return useQuery<ScannersList>( return useQuery<ScannersList>({
["scanners"], queryKey: ["scanners"],
() => apiService.fetchWithDefaults<ScannersList>("/api/scanners"), queryFn: () =>
options apiService.fetchWithSafeDefaults<ScannersList>({
); url: "/api/scanners",
fallback: { scanners: [] },
}),
...(options ?? {}),
});
} }
// Scan manifests using all applicable scanners // Scan manifests using all applicable scanners
// @ts-expect-error unused
function useScanManifests( function useScanManifests(
manifest: string, manifest: string,
options?: UseMutationOptions<ScanResults, Error, string> options?: UseMutationOptions<ScanResults, Error, string>
) { ) {
const formData = new FormData(); const formData = new FormData();
formData.append("manifest", manifest); formData.append("manifest", manifest);
return useMutation<ScanResults, Error, string>( return useMutation<ScanResults, Error, string>({
() => mutationFn: () =>
apiService.fetchWithDefaults<ScanResults>("/api/scanners/manifests", { apiService.fetchWithSafeDefaults<ScanResults>({
method: "POST", url: "/api/scanners/manifests",
body: formData, options: {
method: "POST",
body: formData,
},
fallback: {},
}), }),
options ...(options ?? {}),
); });
} }
// Scan specified k8s resource in cluster // Scan specified k8s resource in cluster
// @ts-expect-error unused
function useScanK8sResource( function useScanK8sResource(
kind: string, kind: string,
namespace: string, namespace: string,
name: string, name: string,
options?: UseQueryOptions<ScanResults> options?: UseQueryOptions<ScanResults>
) { ) {
return useQuery<ScanResults>( return useQuery<ScanResults>({
["scanners", "resource", kind, namespace, name], queryKey: ["scanners", "resource", kind, namespace, name],
() => queryFn: () =>
apiService.fetchWithDefaults<ScanResults>( apiService.fetchWithSafeDefaults<ScanResults>({
`/api/scanners/resource/${kind}?namespace=${namespace}&name=${name}` url: `/api/scanners/resource/${kind}?namespace=${namespace}&name=${name}`,
), fallback: {},
options }),
); ...(options ?? {}),
});
} }

View File

@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import apiService from "./apiService"; import apiService from "./apiService";
export const getVersionManifestFormData = ({ export const getVersionManifestFormData = ({
@@ -43,9 +44,15 @@ export const useDiffData = ({
selectedVerData: { [key: string]: string }; selectedVerData: { [key: string]: string };
chart: string; chart: string;
}) => { }) => {
return useQuery( return useQuery({
[selectedRepo, versionsError, chart, currentVerManifest, selectedVerData], queryKey: [
async () => { selectedRepo,
versionsError,
chart,
currentVerManifest,
selectedVerData,
],
queryFn: async () => {
const formData = new FormData(); const formData = new FormData();
formData.append("a", currentVerManifest); formData.append("a", currentVerManifest);
formData.append("b", selectedVerData.manifest); formData.append("b", selectedVerData.manifest);
@@ -57,8 +64,6 @@ export const useDiffData = ({
return diff; 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 Header from "./layout/Header";
import { HashRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import "./index.css";
import Installed from "./pages/Installed"; import Installed from "./pages/Installed";
import RepositoryPage from "./pages/Repository"; import RepositoryPage from "./pages/Repository";
import Revision from "./pages/Revision"; import Revision from "./pages/Revision";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState } from "react"; const DocsPage = lazy(() => import("./pages/DocsPage"));
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 queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
@@ -23,16 +29,16 @@ const queryClient = new QueryClient({
const PageLayout = () => { const PageLayout = () => {
return ( return (
<div className="flex flex-col h-screen"> <div className="flex h-screen flex-col">
<Header /> <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 /> <Outlet />
</div> </div>
</div> </div>
); );
}; };
const SyncContext: React.FC = () => { const SyncContext: FC = () => {
const { context } = useParams(); const { context } = useParams();
if (context) { if (context) {
apiService.setCluster(decodeURIComponent(context)); apiService.setCluster(decodeURIComponent(context));
@@ -51,33 +57,33 @@ export default function App() {
<AppContextProvider> <AppContextProvider>
<ErrorModalContext.Provider value={value}> <ErrorModalContext.Provider value={value}>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<HashRouter> <ErrorBoundary FallbackComponent={ErrorFallback}>
<Routes> <HashRouter>
<Route path="docs/" element={<DocsPage />} /> <Routes>
<Route path="*" element={<PageLayout />}> <Route path="docs/*" element={<DocsPage />} />
<Route path=":context?/*" element={<SyncContext />}> <Route path="*" element={<PageLayout />}>
<Route path="installed/?" element={<Installed />} /> <Route path=":context?/*" element={<SyncContext />}>
<Route <Route
path=":namespace/:chart/installed/revision/:revision" path="repository/:selectedRepo?/*"
element={<Revision />} element={<RepositoryPage />}
/> />
<Route path="repository/" element={<RepositoryPage />} /> <Route path="installed/?" element={<Installed />} />
<Route <Route
path="repository/:selectedRepo?" path=":namespace/:chart/installed/revision/:revision"
element={<RepositoryPage />} element={<Revision />}
/> />
<Route path="*" element={<Installed />} /> <Route path="*" element={<Installed />} />
</Route>
</Route> </Route>
<Route path="*" element={<Installed />} /> </Routes>
</Route> </HashRouter>
</Routes> </ErrorBoundary>
<GlobalErrorModal <GlobalErrorModal
isOpen={!!shouldShowErrorModal} isOpen={!!shouldShowErrorModal}
onClose={() => setShowErrorModal(undefined)} onClose={() => setShowErrorModal(undefined)}
titleText={shouldShowErrorModal?.title || ""} titleText={shouldShowErrorModal?.title || ""}
contentText={shouldShowErrorModal?.msg || ""} contentText={shouldShowErrorModal?.msg || ""}
/> />
</HashRouter>
</QueryClientProvider> </QueryClientProvider>
</ErrorModalContext.Provider> </ErrorModalContext.Provider>
</AppContextProvider> </AppContextProvider>

View File

@@ -14,7 +14,8 @@
* @see https://storybook.js.org/docs/react/writing-stories/introduction * @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"; import Badge from "./Badge";
// We set the metadata for the story. // 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"; export type BadgeCode = "success" | "warning" | "error" | "unknown";
@@ -29,7 +30,7 @@ export const BadgeCodes = Object.freeze({
export interface BadgeProps { export interface BadgeProps {
type: BadgeCode; type: BadgeCode;
children: React.ReactNode; children: ReactNode;
additionalClassNames?: string; additionalClassNames?: string;
} }
export default function Badge(props: BadgeProps): JSX.Element { export default function Badge(props: BadgeProps): JSX.Element {
@@ -41,7 +42,7 @@ export default function Badge(props: BadgeProps): JSX.Element {
}; };
const badgeBase = 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 = ( const badgeElem = (
<span <span

View File

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

View File

@@ -12,11 +12,12 @@
* *
* *
*/ */
import type { HTMLAttributes, JSX, ReactNode } from "react";
// this is a type declaration for the action prop. // this is a type declaration for the action prop.
// it is a function that takes a string as an argument and returns void. // it is a function that takes a string as an argument and returns void.
export interface ButtonProps extends React.HTMLAttributes<HTMLButtonElement> { export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
children: React.ReactNode; children: ReactNode;
disabled?: boolean; disabled?: boolean;
onClick: () => void; onClick: () => void;
className?: string; className?: string;
@@ -26,7 +27,7 @@ export default function Button(props: ButtonProps): JSX.Element {
<> <>
<button <button
onClick={props.onClick} 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} disabled={props.disabled}
> >
{props.children} {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 { 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 = { type ClustersListProps = {
onClusterChange: (clusterName: string) => void; onClusterChange: (clusterName: string) => void;
@@ -17,7 +20,7 @@ const generateTestReleaseData = (): Release => ({
namespace: "default", namespace: "default",
revision: 1, revision: 1,
updated: "2024-01-23T15:37:35.0992836+02:00", updated: "2024-01-23T15:37:35.0992836+02:00",
status: "deployed", status: DeploymentStatus.DEPLOYED,
chart: "helm-dashboard-0.1.10", chart: "helm-dashboard-0.1.10",
chart_name: "helm-dashboard", chart_name: "helm-dashboard",
chart_ver: "0.1.10", chart_ver: "0.1.10",
@@ -44,6 +47,14 @@ const renderClustersList = (props: ClustersListProps) => {
describe("ClustersList", () => { describe("ClustersList", () => {
it("Got one cluster information", () => { it("Got one cluster information", () => {
cy.intercept("GET", "/api/k8s/contexts", [
{
Name: "minikube",
Namespace: "default",
IsCurrent: true,
},
]).as("getClusters");
renderClustersList({ renderClustersList({
selectedCluster: "minikube", selectedCluster: "minikube",
filteredNamespaces: ["default"], filteredNamespaces: ["default"],
@@ -51,12 +62,21 @@ describe("ClustersList", () => {
installedReleases: [generateTestReleaseData()], installedReleases: [generateTestReleaseData()],
}); });
cy.wait("@getClusters");
cy.get(".data-cy-clusterName").contains("minikube"); cy.get(".data-cy-clusterName").contains("minikube");
cy.get(".data-cy-clusterList-namespace").contains("default"); cy.get(".data-cy-clusterList-namespace").contains("default");
cy.get(".data-cy-clustersInput").should("be.checked"); cy.get(".data-cy-clustersInput").should("be.checked");
}); });
it("Dont have a cluster chekced", () => { it("Dont have a cluster chekced", () => {
cy.intercept("GET", "/api/k8s/contexts", [
{
Name: "minikube",
Namespace: "default",
IsCurrent: true,
},
]).as("getClusters");
renderClustersList({ renderClustersList({
selectedCluster: "", selectedCluster: "",
filteredNamespaces: [""], filteredNamespaces: [""],
@@ -64,6 +84,7 @@ describe("ClustersList", () => {
installedReleases: [generateTestReleaseData()], installedReleases: [generateTestReleaseData()],
}); });
cy.wait("@getClusters");
cy.get(".data-cy-clustersInput").should("not.be.checked"); 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"; import ClustersList from "./ClustersList";
const meta = { 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 { useQuery } from "@tanstack/react-query";
import useCustomSearchParams from "../hooks/useCustomSearchParams"; import { useEffect, useEffectEvent, useMemo } from "react";
import { useAppContext } from "../context/AppContext";
import { v4 as uuidv4 } from "uuid"; 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 = { type ClustersListProps = {
onClusterChange: (clusterName: string) => void; onClusterChange: (clusterName: string) => void;
selectedCluster: string; selectedCluster: string;
@@ -44,29 +45,36 @@ function ClustersList({
const { upsertSearchParams, removeSearchParam } = useCustomSearchParams(); const { upsertSearchParams, removeSearchParam } = useCustomSearchParams();
const { clusterMode } = useAppContext(); const { clusterMode } = useAppContext();
const { data: clusters } = useQuery<Cluster[]>({ const { data: clusters = [], isSuccess } = useQuery<Cluster[]>({
queryKey: ["clusters", selectedCluster], queryKey: ["clusters", selectedCluster],
queryFn: apiService.getClusters, queryFn: apiService.getClusters,
onSuccess(data) { select: (data) =>
const sortedData = data?.sort((a, b) => data?.sort((a, b) =>
getCleanClusterName(a.Name).localeCompare(getCleanClusterName(b.Name)) 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 namespaces = useMemo(() => {
const mapNamespaces = new Map<string, number>(); const mapNamespaces = new Map<string, number>();
@@ -98,47 +106,41 @@ function ClustersList({
}; };
return ( 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 ? ( {!clusterMode ? (
<> <>
<label className="font-bold">Clusters</label> <label className="font-bold">Clusters</label>
{clusters {clusters?.map((cluster) => {
?.sort((a, b) => return (
getCleanClusterName(a.Name).localeCompare( <span
getCleanClusterName(b.Name) key={cluster.Name + cluster.Namespace}
) className="data-cy-clusterName mt-2 flex items-center text-xs"
) >
?.map((cluster) => { <input
return ( className="data-cy-clustersInput cursor-pointer"
<span onChange={(e) => {
key={cluster.Name} onClusterChange(e.target.value);
className="data-cy-clusterName flex items-center mt-2 text-xs" }}
> type="radio"
<input id={cluster.Name}
className="cursor-pointer data-cy-clustersInput" value={cluster.Name}
onChange={(e) => { checked={cluster.Name === selectedCluster}
onClusterChange(e.target.value); name="clusters"
}} />
type="radio" <label htmlFor={cluster.Name} className="ml-1">
id={cluster.Name} {getCleanClusterName(cluster.Name)}
value={cluster.Name} </label>
checked={cluster.Name === selectedCluster} </span>
name="clusters" );
/> })}
<label htmlFor={cluster.Name} className="ml-1 ">
{getCleanClusterName(cluster.Name)}
</label>
</span>
);
})}
</> </>
) : null} ) : null}
<label className="font-bold mt-4">Namespaces</label> <label className="mt-4 font-bold">Namespaces</label>
{namespaces {namespaces
?.sort((a, b) => a.name.localeCompare(b.name)) ?.sort((a, b) => a.name.localeCompare(b.name))
?.map((namespace) => ( ?.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 <input
type="checkbox" type="checkbox"
id={namespace.name} 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 { 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 { interface Props {
statusData: ReleaseHealthStatus[]; statusData: ReleaseHealthStatus[];
} }
const HealthStatus = ({ statusData }: Props) => { const HealthStatus = ({ statusData }: Props) => {
const statuses = statusData.map((item) => { const statuses = statusData.flatMap((item) => {
for (let i = 0; i < item.status.conditions.length; i++) { return item.status?.conditions
const cond = item.status.conditions[i]; ?.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) { return (
continue; <Tooltip
} key={stableKey}
content={`${cond.status} ${item.kind} ${item.metadata?.name}`}
return ( >
<Tooltip <span
key={uuidv4()} // this is not a good practice, we need to fetch some unique id from the backend className={`inline-block ${
content={`${cond.status} ${item.kind} ${item.metadata.name}`} cond.status === "Healthy"
> ? "bg-success"
<span : cond.status === "Progressing"
className={`inline-block ${ ? "bg-warning"
cond.status === "Healthy" : "bg-danger"
? "bg-success" } h-2.5 w-2.5 rounded-xs`}
: cond.status === "Progressing" ></span>
? "bg-warning" </Tooltip>
: "bg-danger" );
} w-2.5 h-2.5 rounded-sm`} });
></span>
</Tooltip>
);
}
}); });
if (statuses.length === 0) {
return <div>No health statuses available</div>;
}
return <div className="flex flex-wrap gap-1">{statuses}</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"; import InstalledPackageCard from "./InstalledPackageCard";
const meta = { const meta = {

View File

@@ -1,21 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { Release } from "../../data/types";
import { BsArrowUpCircleFill, BsPlusCircleFill } from "react-icons/bs"; 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 { getAge } from "../../timeUtils";
import { isNewerVersion } from "../../utils";
import StatusLabel, { import StatusLabel, {
DeploymentStatus, DeploymentStatus,
getStatusColor, getStatusColor,
} from "../common/StatusLabel"; } 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 Spinner from "../Spinner";
import { useGetLatestVersion } from "../../API/releases";
import { isNewerVersion } from "../../utils"; import HealthStatus from "./HealthStatus";
import { LatestChartVersion } from "../../API/interfaces";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import {useInView} from "react-intersection-observer";
type InstalledPackageCardProps = { type InstalledPackageCardProps = {
release: Release; release: Release;
@@ -27,19 +30,21 @@ export default function InstalledPackageCard({
const navigate = useNavigateWithSearchParams(); const navigate = useNavigateWithSearchParams();
const [isMouseOver, setIsMouseOver] = useState(false); const [isMouseOver, setIsMouseOver] = useState(false);
const {ref, inView} = useInView({ const { ref, inView } = useInView({
threshold: 0.3, threshold: 0.3,
triggerOnce: true, triggerOnce: true,
}); });
const { data: status } = useGetApplicationStatus();
const { data: latestVersionResult } = useGetLatestVersion(release.chartName, { const { data: latestVersionResult } = useGetLatestVersion(release.chartName, {
queryKey: ["chartName", 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], queryKey: ["resourceStatus", release],
enabled: inView,
queryFn: () => apiService.getResourceStatus({ release }), queryFn: () => apiService.getResourceStatus({ release }),
enabled: inView && !status?.NoHealth,
}); });
const latestVersionData: LatestChartVersion | undefined = const latestVersionData: LatestChartVersion | undefined =
@@ -62,14 +67,21 @@ export default function InstalledPackageCard({
setIsMouseOver(false); setIsMouseOver(false);
}; };
const handleOnClick = () => { const onClick = async () => {
const { name, namespace } = release; const { name, namespace } = release;
navigate(`/${namespace}/${name}/installed/revision/${release.revision}`, { await navigate(
state: release, `/${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 } = { const borderLeftColor: { [key: string]: string } = {
[DeploymentStatus.DEPLOYED]: "border-l-border-deployed", [DeploymentStatus.DEPLOYED]: "border-l-border-deployed",
[DeploymentStatus.FAILED]: "border-l-text-danger", [DeploymentStatus.FAILED]: "border-l-text-danger",
@@ -81,55 +93,55 @@ export default function InstalledPackageCard({
ref={ref} ref={ref}
className={`${ className={`${
borderLeftColor[release.status] 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" isMouseOver && "custom-shadow-lg"
}`} }`}
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut} onMouseOut={handleMouseOut}
onClick={handleOnClick} onClick={handleClick}
> >
<img <img
src={release.icon || HelmGrayIcon} src={release.icon || HelmGrayIcon}
alt="helm release icon" 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="col-span-11 -mb-5">
<div className="grid grid-cols-11"> <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} {release.name}
</div> </div>
<div className="col-span-3"> <div className="col-span-3">
<StatusLabel status={release.status} /> <StatusLabel status={release.status} />
</div> </div>
<div className="col-span-2 font-bold">{release.chart}</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} #{release.revision}
</div> </div>
<div className="col-span-1 font-bold text-xs"> <div className="col-span-1 text-xs font-bold">
{release.namespace} {release.namespace}
</div> </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>
<div <div
className="grid grid-cols-11 text-xs mt-3" className="mt-3 grid grid-cols-11 text-xs"
style={{ marginBottom: "12px" }} 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} {release.description}
</div> </div>
<div className="col-span-3 mr-2"> <div className="col-span-3 mr-2">
{statusData ? ( {isLoading ? (
<HealthStatus statusData={statusData} />
) : (
<Spinner size={4} /> <Spinner size={4} />
) : (
<HealthStatus statusData={statusData} />
)} )}
</div> </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> <span>CHART VERSION</span>
{(canUpgrade || installRepoSuggestion) && ( {(canUpgrade || installRepoSuggestion) && (
<div <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}`} title={`upgrade available: ${latestVersionData?.version} from ${latestVersionData?.repository}`}
> >
{canUpgrade && !installRepoSuggestion ? ( {canUpgrade && !installRepoSuggestion ? (

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
import type { Release } from "../../data/types";
import InstalledPackageCard from "./InstalledPackageCard"; import InstalledPackageCard from "./InstalledPackageCard";
import { Release } from "../../data/types";
type InstalledPackagesListProps = { type InstalledPackagesListProps = {
filteredReleases: Release[]; filteredReleases: Release[];

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

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import { BsPower } from "react-icons/bs"; import { BsPower } from "react-icons/bs";
import Modal from "./modal/Modal";
import { useShutdownHelmDashboard } from "../API/other"; import { useShutdownHelmDashboard } from "../API/other";
import Modal from "./modal/Modal";
function ShutDownButton() { function ShutDownButton() {
const { mutate: signOut, status } = useShutdownHelmDashboard(); const { mutate: signOut, status } = useShutdownHelmDashboard();
const handleClick = async () => { const handleClick = () => {
signOut(); signOut();
}; };
@@ -22,7 +23,7 @@ function ShutDownButton() {
<button <button
onClick={handleClick} onClick={handleClick}
title="Shut down the Helm Dashboard application" 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" /> <BsPower className="w-6" />
</button> </button>

View File

@@ -3,7 +3,7 @@ export default function Spinner({ size = 8 }: { size?: number }) {
<div role="status"> <div role="status">
<svg <svg
aria-hidden="true" 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" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" 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"; import Tabs from "./Tabs";
const meta = { const meta = {

View File

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

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react"; import type { Meta } from "@storybook/react-vite";
import TabsBar from "./TabsBar"; import TabsBar from "./TabsBar";
const meta = { const meta = {
@@ -17,15 +18,15 @@ export const Default = {
tabs: [ tabs: [
{ {
name: "tab1", 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", 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", 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", activeTab: "tab1",

View File

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

View File

@@ -4,7 +4,8 @@
* the first story simply renders the component with the default props. * 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"; import TextInput from "./TextInput";
const meta = { const meta = {

View File

@@ -12,18 +12,19 @@
* @return JSX.Element * @return JSX.Element
* *
*/ */
import type { ChangeEvent, JSX } from "react";
export interface TextInputProps { export interface TextInputProps {
label: string; label: string;
placeholder: string; placeholder: string;
isMandatory?: boolean; isMandatory?: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
} }
export default function TextInput(props: TextInputProps): JSX.Element { export default function TextInput(props: TextInputProps): JSX.Element {
return ( return (
<div className="mb-6"> <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} {props.label}
{/* if prop.isMandatory is true, add a whitespace and a red star to signify it*/} {/* if prop.isMandatory is true, add a whitespace and a red star to signify it*/}
{props.isMandatory ? <span className="text-red-500"> *</span> : ""} {props.isMandatory ? <span className="text-red-500"> *</span> : ""}
@@ -31,7 +32,7 @@ export default function TextInput(props: TextInputProps): JSX.Element {
<input <input
type="text" type="text"
placeholder={props.placeholder} 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> </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({ export default function Tooltip({
id, id,
@@ -11,11 +11,16 @@ export default function Tooltip({
}) { }) {
return ( return (
<> <>
{cloneElement(element, { "data-tooltip-target": id })} {cloneElement(
element as ReactElement<HTMLAttributes<HTMLElement>>,
{
"data-tooltip-target": id,
} as unknown as HTMLAttributes<HTMLElement>
)}
<div <div
id={id} id={id}
role="tooltip" 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} {title}
<div className="tooltip-arrow" data-popper-arrow></div> <div className="tooltip-arrow" data-popper-arrow></div>
@@ -24,14 +29,14 @@ export default function Tooltip({
<button <button
data-tooltip-target="tooltip-default" data-tooltip-target="tooltip-default"
type="button" 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 Default tooltip
</button> </button>
<div <div
id="tooltip-default" id="tooltip-default"
role="tooltip" 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 Tooltip content
<div className="tooltip-arrow" data-popper-arrow></div> <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"; import { Troubleshoot } from "./Troubleshoot";
const meta = { const meta = {

View File

@@ -8,7 +8,7 @@ export const Troubleshoot = () => {
target="_blank" target="_blank"
rel="noreferrer" 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 Troubleshoot in Komodor
<RiExternalLinkLine className="ml-2 text-lg" /> <RiExternalLinkLine className="ml-2 text-lg" />
</button> </button>

View File

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

View File

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

View File

@@ -1,16 +1,13 @@
import { Meta } from "@storybook/react"; import type { Meta } from "@storybook/react-vite";
import { action } from "@storybook/addon-actions";
import DropDown from "./DropDown";
import { BsSlack, BsGithub } from "react-icons/bs"; import { BsSlack, BsGithub } from "react-icons/bs";
import { action } from "storybook/actions";
import DropDown from "./DropDown";
const meta = { 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", title: "DropDown",
component: DropDown, component: DropDown,
} as Meta<typeof DropDown>; } satisfies Meta<typeof DropDown>;
export default meta; 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"; import ArrowDownIcon from "../../assets/arrow-down-icon.svg";
export type DropDownItem = { export type DropDownItem = {
@@ -29,6 +30,15 @@ function DropDown({ items }: DropDownProps) {
const modalRef = useRef<HTMLDivElement>(null); 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(() => { useEffect(() => {
if (popupState.isOpen) { if (popupState.isOpen) {
document.addEventListener("mousedown", handleClickOutside); document.addEventListener("mousedown", handleClickOutside);
@@ -41,15 +51,6 @@ function DropDown({ items }: DropDownProps) {
}; };
}, [popupState.isOpen]); }, [popupState.isOpen]);
const handleClickOutside = (event: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
setPopupState((prev) => ({
...prev,
isOpen: false,
}));
}
};
return ( return (
<> <>
<div className="relative flex flex-col items-center"> <div className="relative flex flex-col items-center">
@@ -62,21 +63,21 @@ function DropDown({ items }: DropDownProps) {
Y: e.pageY, Y: e.pageY,
})); }));
}} }}
className="flex items-center justify-between" className="flex cursor-pointer items-center justify-between"
> >
Help Help
<img src={ArrowDownIcon} className="ml-2 w-[10px] h-[10px]" /> <img src={ArrowDownIcon} className="ml-2 h-[10px] w-[10px]" />
</button> </button>
</div> </div>
{popupState.isOpen && ( {popupState.isOpen && (
<div <div
ref={modalRef} 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) => ( {items.map((item) => (
<> <Fragment key={item.id}>
{item.isSeparator ? ( {item.isSeparator ? (
<div className="bg-gray-300 h-[1px]" /> <div className="h-[1px] bg-gray-300" />
) : ( ) : (
<div <div
onClick={() => { onClick={() => {
@@ -86,9 +87,9 @@ function DropDown({ items }: DropDownProps) {
isOpen: false, 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 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> <span>{item.text}</span>
</div> </div>
)} )}
</> </Fragment>
))} ))}
</div> </div>
)} )}

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,14 @@
import { useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query";
import Modal from "./Modal"; import { useState } from "react";
import Spinner from "../Spinner";
import apiService from "../../API/apiService";
import { useAppContext } from "../../context/AppContext";
import useAlertError from "../../hooks/useAlertError"; import useAlertError from "../../hooks/useAlertError";
import useCustomSearchParams from "../../hooks/useCustomSearchParams"; import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import { useAppContext } from "../../context/AppContext"; import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import { useQueryClient } from "@tanstack/react-query"; import Spinner from "../Spinner";
import { useNavigate } from "react-router-dom";
import apiService from "../../API/apiService"; import Modal from "./Modal";
interface FormKeys { interface FormKeys {
name: string; name: string;
@@ -21,21 +23,22 @@ type AddRepositoryModalProps = {
}; };
function AddRepositoryModal({ isOpen, onClose }: 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 [isLoading, setIsLoading] = useState(false);
const alertError = useAlertError(); const alertError = useAlertError();
const { searchParamsObject } = useCustomSearchParams();
const { repo_url, repo_name } = searchParamsObject;
const { setSelectedRepo } = useAppContext(); const { setSelectedRepo } = useAppContext();
const navigate = useNavigate(); const navigate = useNavigateWithSearchParams();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
useEffect(() => { const addRepository = async () => {
if (!repo_url || !repo_name) return;
setFormData({ ...formData, name: repo_name, url: repo_url });
}, [repo_url, repo_name, formData]);
const addRepository = () => {
const body = new FormData(); const body = new FormData();
body.append("name", formData.name ?? ""); body.append("name", formData.name ?? "");
body.append("url", formData.url ?? ""); body.append("url", formData.url ?? "");
@@ -44,32 +47,42 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
setIsLoading(true); setIsLoading(true);
apiService try {
.fetchWithDefaults<void>("/api/helm/repositories", { await apiService.fetchWithDefaults<void>("/api/helm/repositories", {
method: "POST", method: "POST",
body, 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 ( return (
@@ -79,11 +92,11 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
bottomContent={ 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 <button
data-cy="add-chart-repository-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" 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={addRepository} onClick={handleAddRepository}
disabled={isLoading} disabled={isLoading}
> >
{isLoading && <Spinner size={4} />} {isLoading && <Spinner size={4} />}
@@ -94,7 +107,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
> >
<div className="flex gap-x-3"> <div className="flex gap-x-3">
<label className="flex-1" htmlFor="name"> <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 <input
value={formData.name} value={formData.name}
onChange={(e) => onChange={(e) =>
@@ -108,11 +121,11 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
data-cy="add-chart-name" data-cy="add-chart-name"
type="text" type="text"
placeholder="Komodorio" 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>
<label className="flex-1" htmlFor="url"> <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 <input
value={formData.url} value={formData.url}
onChange={(e) => onChange={(e) =>
@@ -126,12 +139,12 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
data-cy="add-chart-url" data-cy="add-chart-url"
type="text" type="text"
placeholder="https://helm-charts.komodor.io" 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> </label>
</div> </div>
<div className="flex gap-x-3"> <div className="mt-6 flex gap-x-3">
<label className="flex-1 " htmlFor="username"> <label className="flex-1" htmlFor="username">
<div className="mb-2 text-sm">Username</div> <div className="mb-2 text-sm">Username</div>
<input <input
onChange={(e) => onChange={(e) =>
@@ -143,7 +156,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
required required
id="username" id="username"
type="text" 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>
<label className="flex-1" htmlFor="password"> <label className="flex-1" htmlFor="password">
@@ -158,7 +171,7 @@ function AddRepositoryModal({ isOpen, onClose }: AddRepositoryModalProps) {
required required
id="password" id="password"
type="text" 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>
</div> </div>

View File

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

View File

@@ -14,7 +14,7 @@ export default function ErrorModal({
contentText, contentText,
}: ErrorModalProps) { }: ErrorModalProps) {
const ErrorTitle = ( 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"> <div className="flex gap-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -33,13 +33,14 @@ export default function ErrorModal({
); );
const bottomContent = ( const bottomContent = (
<div className="flex py-6 px-4 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600"> <div className="flex gap-2 rounded-b border-t border-gray-200 px-4 py-6 dark:border-gray-600">
<span className="text-sm text-muted fs-80 text-gray-500"> <span className="fs-80 text-sm text-gray-500 text-muted">
Hint: Komodor has the same HELM capabilities, with enterprise features Hint: Komodor has the same HELM capabilities, with enterprise features
and support.{" "} and support.{" "}
<a <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" 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> <span className="text-link-color underline">Sign up for free.</span>
</a> </a>
@@ -49,15 +50,13 @@ export default function ErrorModal({
return ( return (
<Modal <Modal
containerClassNames={ containerClassNames={"error-dialog w-2/3"}
"border-2 border-error-border-color bg-error-background w-2/3"
}
title={ErrorTitle} title={ErrorTitle}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
bottomContent={bottomContent} bottomContent={bottomContent}
> >
<p className="text-error-color border-green-400">{contentText}</p> <p className="border-green-400 text-error-color">{contentText}</p>
</Modal> </Modal>
); );
} }

View File

@@ -14,7 +14,7 @@ export default function GlobalErrorModal({
contentText, contentText,
}: ErrorModalProps) { }: ErrorModalProps) {
const ErrorTitle = ( 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"> <div className="flex gap-3">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -34,9 +34,7 @@ export default function GlobalErrorModal({
return ( return (
<Modal <Modal
containerClassNames={ containerClassNames={"error-dialog w-3/5"}
"border-2 border-error-border-color bg-error-background w-3/5 "
}
title={ErrorTitle} title={ErrorTitle}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}
@@ -57,7 +55,7 @@ export default function GlobalErrorModal({
> >
<p <p
style={{ minWidth: "500px" }} style={{ minWidth: "500px" }}
className="text-error-color border-green-400 text-sm" className="border-green-400 text-sm text-error-color"
> >
{contentText} {contentText}
</p> </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"; import Spinner from "../../Spinner";
hljs.registerLanguage("yaml", yaml);
export const ChartValues = ({ export const ChartValues = ({
chartValues, chartValues,
loading, loading,
@@ -11,13 +15,13 @@ export const ChartValues = ({
return ( return (
<div className="w-1/2"> <div className="w-1/2">
<label <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" htmlFor="grid-user-defined-values"
> >
Chart Value Reference: Chart Value Reference:
</label> </label>
<pre <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={ dangerouslySetInnerHTML={
chartValues && !loading chartValues && !loading
? { ? {

View File

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

View File

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

View File

@@ -1,38 +1,53 @@
import { useParams } from "react-router-dom"; import { useMutation } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { 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, useChartReleaseValues,
useGetReleaseManifest, useGetReleaseManifest,
useGetVersions, useGetVersions,
useVersionData, useVersionData,
} from "../../../API/releases"; } 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 { useChartRepoValues } from "../../../API/repositories";
import { useDiffData } from "../../../API/shared"; import { useDiffData } from "../../../API/shared";
import { InstallChartModalProps } from "../../../data/types"; import type { InstallChartModalProps } from "../../../data/types";
import { DefinedValues } from "./DefinedValues"; import useCustomSearchParams from "../../../hooks/useCustomSearchParams";
import apiService from "../../../API/apiService"; 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"; import { InstallUpgradeTitle } from "./InstallUpgradeTitle";
const DefinedValues = lazy(() => import("./DefinedValues"));
const ManifestDiff = lazy(() => import("./ManifestDiff"));
export const InstallReleaseChartModal = ({ export const InstallReleaseChartModal = ({
isOpen, isOpen,
onClose, onClose,
chartName, chartName,
currentlyInstalledChartVersion, currentlyInstalledChartVersion,
latestVersion,
isUpgrade = false, isUpgrade = false,
latestRevision, latestRevision,
}: InstallChartModalProps) => { }: InstallChartModalProps) => {
const navigate = useNavigateWithSearchParams(); const navigate = useNavigateWithSearchParams();
const [userValues, setUserValues] = useState<string>(); const [userValues, setUserValues] = useState<string>("");
const [installError, setInstallError] = useState(""); const [installError, setInstallError] = useState("");
const [forceUpgrade, setForceUpgrade] = useState(false);
const { const {
namespace: queryNamespace, namespace: queryNamespace,
@@ -44,40 +59,43 @@ export const InstallReleaseChartModal = ({
const [namespace, setNamespace] = useState(queryNamespace || ""); const [namespace, setNamespace] = useState(queryNamespace || "");
const [releaseName, setReleaseName] = useState(_releaseName || ""); const [releaseName, setReleaseName] = useState(_releaseName || "");
const { error: versionsError, data: _versions } = useGetVersions(chartName, { const {
select: (data) => { error: versionsError,
return data?.sort((a, b) => data: _versions = [],
isNewerVersion(a.version, b.version) ? 1 : -1 isSuccess,
); isLoading: isLoadingVersions,
}, } = useGetVersions(chartName);
onSuccess: (data) => {
const empty = { version: "", repository: "", urls: [] }; const [selectedVersionData, setSelectedVersionData] = useState<VersionData>();
return setSelectedVersionData(data[0] ?? empty);
}, 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) => ({ useEffect(() => {
...v, if (isSuccess && _versions.length) {
isChartVersion: v.version === currentlyInstalledChartVersion, onSuccess();
})); }
}, [isSuccess, _versions]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars const selectedVersion = selectedVersionData?.version || "";
latestVersion = latestVersion ?? currentlyInstalledChartVersion; // a guard for typescript, latestVersion is always defined const selectedRepo = selectedVersionData?.repository || "";
const [selectedVersionData, setSelectedVersionData] = useState<{
version: string;
repository?: string;
urls: string[];
}>();
const selectedVersion = useMemo(() => { const [chartURL, setChartURL] = useState("");
return selectedVersionData?.version; const [useURLMode, setUseURLMode] = useState(false);
}, [selectedVersionData]);
const selectedRepo = useMemo(() => { const repoChartAddress = useMemo(() => {
return selectedVersionData?.repository || "";
}, [selectedVersionData]);
const chartAddress = useMemo(() => {
if (!selectedVersionData || !selectedVersionData.repository) return ""; if (!selectedVersionData || !selectedVersionData.repository) return "";
return selectedVersionData.urls?.[0]?.startsWith("file://") return selectedVersionData.urls?.[0]?.startsWith("file://")
@@ -85,14 +103,16 @@ export const InstallReleaseChartModal = ({
: `${selectedVersionData.repository}/${chartName}`; : `${selectedVersionData.repository}/${chartName}`;
}, [selectedVersionData, chartName]); }, [selectedVersionData, chartName]);
const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
// the original chart values // the original chart values
const { data: chartValues } = useChartRepoValues({ const { data: chartValues = "" } = useChartRepoValues({
version: selectedVersion || "", version: selectedVersion,
chart: chartAddress, chart: chartAddress,
}); });
// The user defined values (if any we're set) // The user defined values (if any we're set)
const { data: releaseValues, isLoading: loadingReleaseValues } = const { data: releaseValues = "", isLoading: loadingReleaseValues } =
useChartReleaseValues({ useChartReleaseValues({
namespace, namespace,
release: String(releaseName), release: String(releaseName),
@@ -100,16 +120,15 @@ export const InstallReleaseChartModal = ({
}); });
// This hold the selected version manifest, we use it for the diff // 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 || "", version: selectedVersion,
userValues: userValues || "", userValues,
chartAddress, chartAddress,
releaseValues, releaseValues,
namespace, namespace,
releaseName, releaseName,
} });
);
const { data: currentVerManifest, error: currentVerManifestError } = const { data: currentVerManifest, error: currentVerManifestError } =
useGetReleaseManifest({ useGetReleaseManifest({
@@ -123,15 +142,15 @@ export const InstallReleaseChartModal = ({
error: diffError, error: diffError,
} = useDiffData({ } = useDiffData({
selectedRepo, selectedRepo,
versionsError: versionsError as string, versionsError: versionsError as unknown as string, // TODO fix it
currentVerManifest, currentVerManifest: currentVerManifest as unknown as string, // TODO fix it
selectedVerData, selectedVerData,
chart: chartAddress, chart: chartAddress,
}); });
// Confirm method (install) // Confirm method (install)
const setReleaseVersionMutation = useMutation( const setReleaseVersionMutation = useMutation<VersionData, Error>({
[ mutationKey: [
"setVersion", "setVersion",
namespace, namespace,
releaseName, releaseName,
@@ -140,7 +159,7 @@ export const InstallReleaseChartModal = ({
selectedCluster, selectedCluster,
chartAddress, chartAddress,
], ],
async () => { mutationFn: async () => {
setInstallError(""); setInstallError("");
const formData = new FormData(); const formData = new FormData();
formData.append("preview", "false"); formData.append("preview", "false");
@@ -149,34 +168,36 @@ export const InstallReleaseChartModal = ({
} }
formData.append("version", selectedVersion || ""); formData.append("version", selectedVersion || "");
formData.append("values", userValues || releaseValues || ""); // if userValues is empty, we use the release values 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( return await apiService.fetchWithSafeDefaults<VersionData>({
`/api/helm/releases/${ url,
namespace ? namespace : "default" options: {
}${`/${releaseName}`}`,
{
method: "post", method: "post",
body: formData, body: formData,
} },
); fallback: { version: "", urls: [""] },
return data; });
}, },
{ onSuccess: async (response) => {
onSuccess: async (response) => { onClose();
onClose(); setSelectedVersionData({ version: "", urls: [] }); //cleanup
setSelectedVersionData({ version: "", urls: [] }); //cleanup await navigate(
navigate( `/${
`/${ namespace ? namespace : "default"
namespace ? namespace : "default" }/${releaseName}/installed/revision/${response.version}`
}/${releaseName}/installed/revision/${response.version}` );
); window.location.reload();
window.location.reload(); },
}, onError: (error) => {
onError: (error) => { setInstallError(error?.message || "Failed to update");
setInstallError((error as Error)?.message || "Failed to update"); },
}, });
}
);
return ( return (
<Modal <Modal
@@ -189,31 +210,80 @@ export const InstallReleaseChartModal = ({
title={ title={
<InstallUpgradeTitle <InstallUpgradeTitle
isUpgrade={isUpgrade} isUpgrade={isUpgrade}
releaseValues={isUpgrade || releaseValues} releaseValues={isUpgrade || !!releaseValues}
chartName={chartName} chartName={chartName}
/> />
} }
containerClassNames="w-full text-2xl h-2/3" 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={[ actions={[
{ {
id: "1", id: "1",
callback: setReleaseVersionMutation.mutate, callback: setReleaseVersionMutation.mutate,
variant: ModalButtonStyle.info, variant: ModalButtonStyle.info,
isLoading: setReleaseVersionMutation.isLoading, isLoading: setReleaseVersionMutation.isPending,
disabled: disabled:
loadingReleaseValues || loadingReleaseValues ||
isLoadingDiff || isLoadingDiff ||
setReleaseVersionMutation.isLoading, setReleaseVersionMutation.isPending,
}, },
]} ]}
> >
{versions && isNoneEmptyArray(versions) && ( {isLoadingVersions ? (
<VersionToInstall <Spinner />
versions={versions} ) : !useURLMode && versions && isNoneEmptyArray(versions) ? (
initialVersion={selectedVersionData} <div className="flex items-center gap-2">
onSelectVersion={setSelectedVersionData} <VersionToInstall
showCurrentVersion 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 <GeneralDetails
@@ -224,24 +294,28 @@ export const InstallReleaseChartModal = ({
onNamespaceInput={setNamespace} onNamespaceInput={setNamespace}
/> />
<DefinedValues <Suspense fallback={<Spinner />}>
initialValue={releaseValues} <DefinedValues
onUserValuesChange={(values: string) => setUserValues(values)} initialValue={releaseValues}
chartValues={chartValues} onUserValuesChange={(values: string) => setUserValues(values)}
loading={loadingReleaseValues} chartValues={chartValues}
/> loading={loadingReleaseValues}
/>
</Suspense>
<ManifestDiff <Suspense fallback={<Spinner />}>
diff={diffData as string} <ManifestDiff
isLoading={isLoadingDiff} diff={diffData as string}
error={ isLoading={isLoadingDiff}
(currentVerManifestError as string) || error={
(selectedVerDataError as string) || (currentVerManifestError as unknown as string) || // TODO fix it
(diffError as string) || (selectedVerDataError as unknown as string) ||
installError || (diffError as unknown as string) ||
(versionsError as string) installError ||
} (versionsError as unknown as string)
/> }
/>
</Suspense>
</Modal> </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 { useMutation } from "@tanstack/react-query";
import { useChartRepoValues } from "../../../API/repositories"; import {
import useNavigateWithSearchParams from "../../../hooks/useNavigateWithSearchParams"; lazy,
import { VersionToInstall } from "./VersionToInstall"; Suspense,
import { isNewerVersion, isNoneEmptyArray } from "../../../utils"; useEffect,
import { useDiffData } from "../../../API/shared"; useEffectEvent,
import { InstallChartModalProps } from "../../../data/types"; useMemo,
import { DefinedValues } from "./DefinedValues"; useState,
} from "react";
import { useParams } from "react-router";
import { BsPencil, BsX } from "react-icons/bs";
import apiService from "../../../API/apiService"; 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 { InstallUpgradeTitle } from "./InstallUpgradeTitle";
import { VersionToInstall } from "./VersionToInstall";
const DefinedValues = lazy(() => import("./DefinedValues"));
const ManifestDiff = lazy(() => import("./ManifestDiff"));
export const InstallRepoChartModal = ({ export const InstallRepoChartModal = ({
isOpen, isOpen,
onClose, onClose,
chartName, chartName,
currentlyInstalledChartVersion, currentlyInstalledChartVersion,
latestVersion, urlMode: initialURLMode = false,
}: InstallChartModalProps) => { }: InstallChartModalProps & { urlMode?: boolean }) => {
const navigate = useNavigateWithSearchParams(); const navigate = useNavigateWithSearchParams();
const [userValues, setUserValues] = useState(""); const [userValues, setUserValues] = useState("");
const [installError, setInstallError] = useState(""); const [installError, setInstallError] = useState("");
@@ -31,44 +45,51 @@ export const InstallRepoChartModal = ({
const [namespace, setNamespace] = useState(""); const [namespace, setNamespace] = useState("");
const [releaseName, setReleaseName] = useState(chartName); const [releaseName, setReleaseName] = useState(chartName);
const { error: versionsError, data: _versions } = useGetVersions(chartName, { const {
select: (data) => { error: versionsError,
return data?.sort((a, b) => data: _versions = [],
isNewerVersion(a.version, b.version) ? 1 : -1 isSuccess,
); } = useGetVersions(chartName);
},
onSuccess: (data) => {
const empty = { version: "", repository: "", urls: [] };
const versionsToRepo = data.filter(
(v) => v.repository === currentRepoCtx
);
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<{ const [selectedVersionData, setSelectedVersionData] = useState<{
version: string; version: string;
repository?: string; repository?: string;
urls: string[]; urls: string[];
}>(); }>();
const selectedVersion = useMemo(() => { const onSuccess = useEffectEvent(() => {
return selectedVersionData?.version; const empty = { version: "", repository: "", urls: [] };
}, [selectedVersionData]); const versionsToRepo = _versions.filter(
(v) => v.repository === currentRepoCtx
);
const selectedRepo = useMemo(() => { setSelectedVersionData(versionsToRepo[0] ?? empty);
return selectedVersionData?.repository; setVersions(
}, [selectedVersionData]); _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) { if (!selectedVersionData || !selectedVersionData?.repository) {
return ""; return "";
} }
@@ -77,15 +98,17 @@ export const InstallRepoChartModal = ({
: `${selectedVersionData?.repository}/${chartName}`; : `${selectedVersionData?.repository}/${chartName}`;
}, [selectedVersionData, chartName]); }, [selectedVersionData, chartName]);
const { data: chartValues, isLoading: loadingChartValues } = const chartAddress = useURLMode ? chartURL : repoChartAddress || chartURL;
const { data: chartValues = "", isLoading: loadingChartValues } =
useChartRepoValues({ useChartRepoValues({
version: selectedVersion || "", version: selectedVersion || "",
chart: chartAddress, chart: chartAddress,
}); });
// This hold the selected version manifest, we use it for the diff // 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 || "", version: selectedVersion || "",
userValues, userValues,
chartAddress, chartAddress,
@@ -93,11 +116,8 @@ export const InstallRepoChartModal = ({
namespace, namespace,
releaseName, releaseName,
isInstallRepoChart: true, isInstallRepoChart: true,
options: { enabled: Boolean(chartAddress),
enabled: Boolean(chartAddress), });
},
}
);
const { const {
data: diffData, data: diffData,
@@ -105,15 +125,18 @@ export const InstallRepoChartModal = ({
error: diffError, error: diffError,
} = useDiffData({ } = useDiffData({
selectedRepo: selectedRepo || "", selectedRepo: selectedRepo || "",
versionsError: versionsError as string, versionsError: versionsError as unknown as string, // TODO fix it
currentVerManifest: "", // current version manifest should always be empty since its a fresh install currentVerManifest: "", // current version manifest should always be empty since it's a fresh install
selectedVerData, selectedVerData,
chart: chartAddress, chart: chartAddress,
}); });
// Confirm method (install) // Confirm method (install)
const setReleaseVersionMutation = useMutation( const setReleaseVersionMutation = useMutation<{
[ namespace: string;
name: string;
}>({
mutationKey: [
"setVersion", "setVersion",
namespace, namespace,
releaseName, releaseName,
@@ -122,7 +145,7 @@ export const InstallRepoChartModal = ({
selectedCluster, selectedCluster,
chartAddress, chartAddress,
], ],
async () => { mutationFn: async () => {
setInstallError(""); setInstallError("");
const formData = new FormData(); const formData = new FormData();
formData.append("preview", "false"); formData.append("preview", "false");
@@ -130,27 +153,27 @@ export const InstallRepoChartModal = ({
formData.append("version", selectedVersion || ""); formData.append("version", selectedVersion || "");
formData.append("values", userValues); formData.append("values", userValues);
formData.append("name", releaseName || ""); 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", method: "post",
body: formData, body: formData,
} },
); fallback: { namespace: "", name: "" },
return data; });
}, },
{
onSuccess: async (response) => { onSuccess: async (response: { namespace: string; name: string }) => {
onClose(); onClose();
navigate( await navigate(
`/${response.namespace}/${response.name}/installed/revision/1` `/${response.namespace}/${response.name}/installed/revision/1`
); );
}, },
onError: (error) => { onError: (error) => {
setInstallError((error as Error)?.message || "Failed to update"); setInstallError(error?.message || "Failed to update");
}, },
} });
);
return ( return (
<Modal <Modal
@@ -160,11 +183,15 @@ export const InstallRepoChartModal = ({
onClose(); onClose();
}} }}
title={ title={
<InstallUpgradeTitle initialURLMode ? (
isUpgrade={false} <div className="font-bold">Install from URL</div>
releaseValues={false} ) : (
chartName={chartName} <InstallUpgradeTitle
/> isUpgrade={false}
releaseValues={false}
chartName={chartName}
/>
)
} }
containerClassNames="w-full text-2xl h-2/3" containerClassNames="w-full text-2xl h-2/3"
actions={[ actions={[
@@ -172,21 +199,56 @@ export const InstallRepoChartModal = ({
id: "1", id: "1",
callback: setReleaseVersionMutation.mutate, callback: setReleaseVersionMutation.mutate,
variant: ModalButtonStyle.info, variant: ModalButtonStyle.info,
isLoading: setReleaseVersionMutation.isLoading, isLoading: setReleaseVersionMutation.isPending,
disabled: disabled:
loadingChartValues || loadingChartValues ||
isLoadingDiff || isLoadingDiff ||
setReleaseVersionMutation.isLoading, setReleaseVersionMutation.isPending,
}, },
]} ]}
> >
{versions && isNoneEmptyArray(versions) && ( {!useURLMode && versions && isNoneEmptyArray(versions) ? (
<VersionToInstall <div className="flex items-center gap-2">
versions={versions} <VersionToInstall
initialVersion={selectedVersionData} versions={versions}
onSelectVersion={setSelectedVersionData} initialVersion={selectedVersionData}
showCurrentVersion={false} 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 <GeneralDetails
@@ -204,16 +266,18 @@ export const InstallRepoChartModal = ({
loading={loadingChartValues} loading={loadingChartValues}
/> />
<ManifestDiff <Suspense fallback={<Spinner />}>
diff={diffData as string} <ManifestDiff
isLoading={isLoadingDiff} diff={diffData as string}
error={ isLoading={isLoadingDiff}
(selectedVerDataError as string) || error={
(diffError as string) || (selectedVerDataError as unknown as string) || // TODO fix it
installError || (diffError as unknown as string) ||
(versionsError as string) installError ||
} (versionsError as unknown as string)
/> }
/>
</Suspense>
</Modal> </Modal>
); );
}; };

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import useDebounce from "../../../hooks/useDebounce"; import useDebounce from "../../../hooks/useDebounce";
export const UserDefinedValues = ({ export const UserDefinedValues = ({
@@ -20,9 +21,9 @@ export const UserDefinedValues = ({
}, [debouncedValue, onValuesChange, initialValue]); }, [debouncedValue, onValuesChange, initialValue]);
return ( return (
<div className="w-1/2 "> <div className="w-1/2">
<label <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" htmlFor="grid-user-defined-values"
> >
User-Defined Values: User-Defined Values:
@@ -32,7 +33,7 @@ export const UserDefinedValues = ({
defaultValue={initialValue} defaultValue={initialValue}
onChange={(e) => setUserDefinedValues(e.target.value)} onChange={(e) => setUserDefinedValues(e.target.value)}
rows={14} rows={14}
className="block p-2.5 w-full text-md text-gray-900 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 resize-none font-monospace" className="text-md font-monospace block w-full resize-none rounded-lg border border-gray-300 p-2.5 text-gray-900 focus:border-blue-500 focus:ring-blue-500"
></textarea> ></textarea>
</div> </div>
); );

View File

@@ -1,7 +1,13 @@
import { useMemo, useState } from "react"; import { type FC, useMemo, useState } from "react";
import Select, { components } from "react-select";
import { BsCheck2 } from "react-icons/bs"; import { BsCheck2 } from "react-icons/bs";
import { NonEmptyArray } from "../../../data/types"; import Select, {
type GroupBase,
type SingleValueProps,
type OptionProps,
components,
} from "react-select";
import type { NonEmptyArray } from "../../../data/types";
interface Version { interface Version {
repository: string; repository: string;
@@ -10,7 +16,19 @@ interface Version {
urls: string[]; urls: string[];
} }
export const VersionToInstall: React.FC<{ type VersionOptionType = {
value: Omit<Version, "isChartVersion">;
label: string;
check: boolean;
};
type SpecificSingleValueProps = SingleValueProps<
VersionOptionType,
false, // IsMulti
GroupBase<VersionOptionType>
>;
export const VersionToInstall: FC<{
versions: NonEmptyArray<Version>; versions: NonEmptyArray<Version>;
initialVersion?: { initialVersion?: {
repository?: string; repository?: string;
@@ -30,7 +48,7 @@ export const VersionToInstall: React.FC<{
const currentVersion = const currentVersion =
chartVersion && showCurrentVersion ? ( chartVersion && showCurrentVersion ? (
<p className="text-xl text-muted ml-2"> <p className="ml-2 text-xl text-muted">
{"(current version is "} {"(current version is "}
<span className="text-green-700">{`${chartVersion}`}</span> <span className="text-green-700">{`${chartVersion}`}</span>
{")"} {")"}
@@ -59,12 +77,12 @@ export const VersionToInstall: React.FC<{
[options, initialVersion] [options, initialVersion]
); );
return ( return (
<div className="flex gap-2 text-xl items-center"> <div className="flex items-center gap-2 text-xl">
{versions?.length && (selectedOption || initOpt) ? ( {versions?.length && (selectedOption || initOpt) ? (
<> <>
Version to install:{" "} Version to install:{" "}
<Select <Select
className="basic-single cursor-pointer min-w-[272px]" className="basic-single min-w-[272px] cursor-pointer"
classNamePrefix="select" classNamePrefix="select"
isClearable={false} isClearable={false}
isSearchable={false} isSearchable={false}
@@ -78,18 +96,34 @@ export const VersionToInstall: React.FC<{
}} }}
value={selectedOption ?? initOpt} value={selectedOption ?? initOpt}
components={{ components={{
SingleValue: ({ children, ...props }) => ( SingleValue: ({
<components.SingleValue {...props}> children,
<span className="text-green-700 font-bold">{children}</span> ...props
{props.data.check && showCurrentVersion && ( }: SpecificSingleValueProps) => {
<BsCheck2 className="inline-block ml-2 text-green-700 font-bold" /> const OriginalSingleValue =
)} components.SingleValue as FC<SpecificSingleValueProps>;
</components.SingleValue>
), return (
Option: ({ children, innerProps, data }) => ( <OriginalSingleValue {...props}>
<span className="font-bold text-green-700">{children}</span>
{props.data.check && showCurrentVersion && (
<BsCheck2 className="ml-2 inline-block font-bold text-green-700" />
)}
</OriginalSingleValue>
);
},
Option: ({
children,
innerProps,
data,
}: OptionProps<
VersionOptionType,
false,
GroupBase<VersionOptionType>
>) => (
<div <div
className={ className={
"flex items-center py-2 pl-4 pr-2 text-green-700 hover:bg-blue-100" "flex items-center py-2 pr-2 pl-4 text-green-700 hover:bg-blue-100"
} }
{...innerProps} {...innerProps}
> >
@@ -97,7 +131,7 @@ export const VersionToInstall: React.FC<{
{data.check && showCurrentVersion && ( {data.check && showCurrentVersion && (
<BsCheck2 <BsCheck2
fontWeight={"bold"} fontWeight={"bold"}
className="inline-block ml-2 text-green-700 font-bold" className="ml-2 inline-block font-bold text-green-700"
/> />
)} )}
</div> </div>

View File

@@ -1,6 +1,7 @@
import { action } from "@storybook/addon-actions"; import type { StoryObj, StoryFn, Meta } from "@storybook/react-vite";
import { StoryObj, StoryFn, Meta } from "@storybook/react"; import { action } from "storybook/actions";
import Modal, { ModalAction, ModalButtonStyle } from "./Modal";
import Modal, { type ModalAction, ModalButtonStyle } from "./Modal";
const meta = { const meta = {
/* 👇 The title prop is optional. /* 👇 The title prop is optional.
@@ -51,7 +52,7 @@ const customModalActions: ModalAction[] = [
id: "1", id: "1",
text: "custom button 1", text: "custom button 1",
className: className:
"text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800", "text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:outline-hidden focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800",
callback: () => { callback: () => {
action("clickCustomButton")("confirmModal: clicked custom button 1"); action("clickCustomButton")("confirmModal: clicked custom button 1");
}, },

View File

@@ -1,5 +1,6 @@
import { PropsWithChildren, ReactNode } from "react"; import type { PropsWithChildren, ReactNode } from "react";
import ReactDom from "react-dom"; import { createPortal } from "react-dom";
import Spinner from "../Spinner"; import Spinner from "../Spinner";
export enum ModalButtonStyle { export enum ModalButtonStyle {
@@ -41,23 +42,23 @@ const Modal = ({
const colorVariants = new Map<ModalButtonStyle, string>([ const colorVariants = new Map<ModalButtonStyle, string>([
[ [
ModalButtonStyle.default, ModalButtonStyle.default,
"text-base font-semibold text-gray-500 bg-white hover:bg-gray-100 disabled:bg-gray-200 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 font-medium px-5 py-1 hover:text-gray-900 focus:z-10 ", "text-base font-semibold text-gray-500 bg-white hover:bg-gray-100 disabled:bg-gray-200 focus:ring-4 focus:outline-hidden focus:ring-gray-200 rounded-lg border border-gray-200 font-medium px-5 py-1 hover:text-gray-900 focus:z-10 ",
], ],
[ [
ModalButtonStyle.info, ModalButtonStyle.info,
"font-semibold text-white bg-blue-700 hover:bg-blue-800 disabled:bg-blue-700/80 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-base px-3 py-1.5 text-center ", "font-semibold text-white bg-blue-700 hover:bg-blue-800 disabled:bg-blue-700/80 focus:ring-4 focus:outline-hidden focus:ring-blue-300 font-medium rounded-lg text-base px-3 py-1.5 text-center ",
], ],
[ [
ModalButtonStyle.success, ModalButtonStyle.success,
"font-semibold text-white bg-green-700 hover:bg-green-800 disabled:bg-green-700/80 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-base px-3 py-1.5 text-center ", "font-semibold text-white bg-green-700 hover:bg-green-800 disabled:bg-green-700/80 focus:ring-4 focus:outline-hidden focus:ring-green-300 font-medium rounded-lg text-base px-3 py-1.5 text-center ",
], ],
[ [
ModalButtonStyle.error, ModalButtonStyle.error,
"font-semibold text-white bg-red-700 hover:bg-red-800 disabled:bg-red-700/80 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-base px-3 py-1.5 text-center ", "font-semibold text-white bg-red-700 hover:bg-red-800 disabled:bg-red-700/80 focus:ring-4 focus:outline-hidden focus:ring-red-300 font-medium rounded-lg text-base px-3 py-1.5 text-center ",
], ],
[ [
ModalButtonStyle.disabled, ModalButtonStyle.disabled,
"font-semibold text-gray-500 bg-gray-200 hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-base font-medium px-3 py-1.5 hover:text-gray-900 focus:z-10 ", "font-semibold text-gray-500 bg-gray-200 hover:bg-gray-100 focus:ring-4 focus:outline-hidden focus:ring-gray-200 rounded-lg border border-gray-200 text-base font-medium px-3 py-1.5 hover:text-gray-900 focus:z-10 ",
], ],
]); ]);
@@ -71,21 +72,21 @@ const Modal = ({
const getTitle = (title: string | ReactNode) => { const getTitle = (title: string | ReactNode) => {
if (typeof title === "string") if (typeof title === "string")
return <h3 className="text-xl font-medium text-grey">{title}</h3>; return <h3 className="text-grey text-xl font-medium">{title}</h3>;
else return title; else return title;
}; };
return ReactDom.createPortal( return createPortal(
<> <>
{isOpen && ( {isOpen && (
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity "> <div className="fixed inset-0 bg-black/75 transition-opacity">
<div className="flex justify-center"> <div className="flex justify-center">
<div <div
style={{ style={{
maxHeight: "95vh", maxHeight: "95vh",
overflow: "hidden", overflow: "hidden",
}} }}
className={`relative rounded-lg shadow m-7 w-2/5 max-w-[1300px] ${ className={`relative m-7 w-2/5 max-w-[1300px] rounded-lg shadow-sm ${
!containerClassNames || !containerClassNames ||
(containerClassNames && !containerClassNames.includes("bg-")) (containerClassNames && !containerClassNames.includes("bg-"))
? "bg-white" ? "bg-white"
@@ -93,17 +94,17 @@ const Modal = ({
} ${containerClassNames ?? ""}`} } ${containerClassNames ?? ""}`}
> >
{title && ( {title && (
<div className="flex items-start justify-between p-4 border-b rounded-t "> <div className="flex items-start justify-between rounded-t border-b border-gray-200 p-4">
{getTitle(title)} {getTitle(title)}
{onClose ? ( {onClose ? (
<button <button
type="button" type="button"
className="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white" className="ml-auto inline-flex cursor-pointer items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900 dark:hover:bg-gray-600 dark:hover:text-white"
data-modal-hide="staticModal" data-modal-hide="staticModal"
onClick={() => onClose()} onClick={() => onClose()}
> >
<svg <svg
className="w-5 h-5" className="h-5 w-5"
fill="currentColor" fill="currentColor"
viewBox="0 0 20 20" viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -118,20 +119,19 @@ const Modal = ({
) : null} ) : null}
</div> </div>
)} )}
<div className="p-4 space-y-6 overflow-y-auto max-h-[calc(100vh_-_200px)]"> <div className="max-h-[calc(100vh_-_200px)] gap-6 overflow-y-auto p-4">
{children} {children}
</div> </div>
{bottomContent ? ( <div className="flex items-center justify-between rounded-b border-t border-gray-200 p-6">
<div className="p-5 text-sm">{bottomContent}</div> <div>{bottomContent}</div>
) : ( <div className="flex gap-2">
<div className="flex justify-end p-6 space-x-2 border-t border-gray-200 rounded-b ">
{actions?.map((action) => ( {actions?.map((action) => (
<button <button
key={action.id} key={action.id}
type="button" type="button"
className={ className={
action.isLoading action.isLoading
? `flex items-center font-bold justify-around space-x-1 ${getClassName( ? `flex items-center justify-around gap-1 font-bold ${getClassName(
action action
)}` )}`
: `${getClassName(action)} ` : `${getClassName(action)} `
@@ -144,7 +144,7 @@ const Modal = ({
</button> </button>
))} ))}
</div> </div>
)} </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,4 +1,5 @@
import { Meta } from "@storybook/react"; import type { Meta } from "@storybook/react-vite";
import ChartViewer from "./ChartViewer"; import ChartViewer from "./ChartViewer";
//👇 This default export determines where your story goes in the story list //👇 This default export determines where your story goes in the story list

View File

@@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Chart } from "../../data/types";
import type { Chart } from "../../data/types";
import { InstallRepoChartModal } from "../modal/InstallChartModal/InstallRepoChartModal"; import { InstallRepoChartModal } from "../modal/InstallChartModal/InstallRepoChartModal";
type ChartViewerProps = { type ChartViewerProps = {
@@ -21,11 +22,11 @@ function ChartViewer({ chart }: ChartViewerProps) {
<> <>
<div <div
data-cy="chart-viewer-install-button" data-cy="chart-viewer-install-button"
className="grid grid-cols-10 gap-3 hover:bg-body-background p-4 text-sm" className="grid grid-cols-10 gap-3 p-4 text-sm hover:bg-body-background"
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut} onMouseOut={handleMouseOut}
> >
<span className="col-span-2 font-semibold flex flex-row items-center gap-1 text-base"> <span className="col-span-2 flex flex-row items-center gap-1 text-base font-semibold">
<img src={chart.icon} className="h-4" /> <img src={chart.icon} className="h-4" />
{chart.name} {chart.name}
</span> </span>
@@ -33,7 +34,7 @@ function ChartViewer({ chart }: ChartViewerProps) {
<span className="col-span-1 text-center">{chart.version}</span> <span className="col-span-1 text-center">{chart.version}</span>
<span className="col-span-1 text-center"> <span className="col-span-1 text-center">
<button <button
className={`bg-white border border-gray-300 px-2 p-1 rounded-md font-semibold ${ className={`rounded-md border border-gray-300 bg-white p-1 px-2 font-semibold ${
showInstallButton ? "visible" : "invisible" showInstallButton ? "visible" : "invisible"
}`} }`}
onClick={() => setShowInstallModal(true)} onClick={() => setShowInstallModal(true)}

View File

@@ -1,4 +1,5 @@
import { StoryFn, Meta } from "@storybook/react"; import type { StoryFn, Meta } from "@storybook/react-vite";
import RepositoriesList from "./RepositoriesList"; import RepositoriesList from "./RepositoriesList";
const meta = { const meta = {
@@ -17,7 +18,6 @@ const Template: StoryFn<typeof RepositoriesList> = () => (
<RepositoriesList <RepositoriesList
selectedRepository={undefined} selectedRepository={undefined}
// in this case we allow Unexpected empty method // in this case we allow Unexpected empty method
//eslint-disable-next-line @typescript-eslint/no-empty-function
onRepositoryChanged={() => {}} onRepositoryChanged={() => {}}
repositories={[]} repositories={[]}
/> />

View File

@@ -1,7 +1,7 @@
import { useMemo } from "react"; import type { Repository } from "../../data/types";
import AddRepositoryModal from "../modal/AddRepositoryModal";
import { Repository } from "../../data/types";
import useCustomSearchParams from "../../hooks/useCustomSearchParams"; import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import AddRepositoryModal from "../modal/AddRepositoryModal";
import { InstallRepoChartModal } from "../modal/InstallChartModal/InstallRepoChartModal";
type RepositoriesListProps = { type RepositoriesListProps = {
selectedRepository: Repository | undefined; selectedRepository: Repository | undefined;
@@ -16,10 +16,7 @@ function RepositoriesList({
}: RepositoriesListProps) { }: RepositoriesListProps) {
const { searchParamsObject, upsertSearchParams, removeSearchParam } = const { searchParamsObject, upsertSearchParams, removeSearchParam } =
useCustomSearchParams(); useCustomSearchParams();
const showAddRepositoryModal = useMemo( const showAddRepositoryModal = searchParamsObject["add_repo"] === "true";
() => searchParamsObject["add_repo"] === "true",
[searchParamsObject]
);
const setShowAddRepositoryModal = (value: boolean) => { const setShowAddRepositoryModal = (value: boolean) => {
if (value) { if (value) {
upsertSearchParams("add_repo", "true"); upsertSearchParams("add_repo", "true");
@@ -27,10 +24,18 @@ function RepositoriesList({
removeSearchParam("add_repo"); removeSearchParam("add_repo");
} }
}; };
const showInstallURLModal = searchParamsObject["install_url"] === "true";
const setShowInstallURLModal = (value: boolean) => {
if (value) {
upsertSearchParams("install_url", "true");
} else {
removeSearchParam("install_url");
}
};
return ( return (
<> <>
<div className="h-fit bg-white w-72 flex flex-col p-3 rounded custom-shadow text-dark gap-3"> <div className="custom-shadow flex h-fit w-72 flex-col gap-3 rounded-sm bg-white p-3 text-dark">
<label className="font-bold">Repositories</label> <label className="font-bold">Repositories</label>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{repositories?.map((repository) => ( {repositories?.map((repository) => (
@@ -60,15 +65,23 @@ function RepositoriesList({
data-cy="install-repository-button" data-cy="install-repository-button"
type="button" type="button"
style={{ marginTop: "10px" }} style={{ marginTop: "10px" }}
className="h-8 w-fit flex items-center gap-2 border rounded text-muted border-gray-300 px-3 py-1 text-sm font-semibold" className="flex h-8 w-fit cursor-pointer items-center gap-2 rounded-sm border border-gray-300 px-3 py-1 text-sm font-semibold text-muted"
onClick={() => setShowAddRepositoryModal(true)} onClick={() => setShowAddRepositoryModal(true)}
> >
+ Add Repository + Add Repository
</button> </button>
<button
data-cy="install-url-button"
type="button"
className="flex h-8 w-fit cursor-pointer items-center gap-2 rounded-sm border border-gray-300 px-3 py-1 text-sm font-semibold text-muted"
onClick={() => setShowInstallURLModal(true)}
>
Install from URL
</button>
<p className="text-xs"> <p className="text-xs">
Charts developers: you can also add local directories as chart source. Charts developers: you can also add local directories as chart source.
Use{" "} Use{" "}
<span className="text-green-600 font-monospace">--local-chart</span>{" "} <span className="font-monospace text-green-600">--local-chart</span>{" "}
CLI switch to specify it. CLI switch to specify it.
</p> </p>
</div> </div>
@@ -76,6 +89,12 @@ function RepositoriesList({
isOpen={showAddRepositoryModal} isOpen={showAddRepositoryModal}
onClose={() => setShowAddRepositoryModal(false)} onClose={() => setShowAddRepositoryModal(false)}
/> />
<InstallRepoChartModal
isOpen={showInstallURLModal}
onClose={() => setShowInstallURLModal(false)}
chartName=""
urlMode
/>
</> </>
); );
} }

View File

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

View File

@@ -1,13 +1,15 @@
import { BsTrash3, BsArrowRepeat } from "react-icons/bs";
import { Chart, Repository } from "../../data/types";
import ChartViewer from "./ChartViewer";
import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useQuery, useQueryClient } from "@tanstack/react-query";
import apiService from "../../API/apiService";
import Spinner from "../Spinner";
import { useUpdateRepo } from "../../API/repositories";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom"; import { BsTrash3, BsArrowRepeat } from "react-icons/bs";
import { useNavigate } from "react-router";
import apiService from "../../API/apiService";
import { useUpdateRepo } from "../../API/repositories";
import { useAppContext } from "../../context/AppContext"; import { useAppContext } from "../../context/AppContext";
import type { Chart, Repository } from "../../data/types";
import Spinner from "../Spinner";
import ChartViewer from "./ChartViewer";
type RepositoryViewerProps = { type RepositoryViewerProps = {
repository: Repository | undefined; repository: Repository | undefined;
@@ -22,7 +24,6 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { data: charts, isLoading } = useQuery<Chart[]>({ const { data: charts, isLoading } = useQuery<Chart[]>({
//@ts-ignore
queryKey: ["charts", repository?.name || ""], queryKey: ["charts", repository?.name || ""],
queryFn: apiService.getRepositoryCharts, queryFn: apiService.getRepositoryCharts,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
@@ -41,8 +42,6 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
}); });
const removeRepository = async () => { const removeRepository = async () => {
//this is expected
//eslint-disable-next-line no-alert
if (confirm("Confirm removing repository?")) { if (confirm("Confirm removing repository?")) {
try { try {
setIsRemove(true); setIsRemove(true);
@@ -53,9 +52,9 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
method: "DELETE", method: "DELETE",
} }
); );
navigate("/repository", { replace: true }); await navigate("/repository", { replace: true });
setSelectedRepo(""); setSelectedRepo("");
queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["helm", "repositories"], queryKey: ["helm", "repositories"],
}); });
} catch (error) { } catch (error) {
@@ -76,7 +75,7 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
if (repository === undefined) { if (repository === undefined) {
return ( return (
<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 repositories installed. You can add Looks like you don&apos;t have any repositories installed. You can add
one with the &quot;Add Repository&quot; button on the left side bar. one with the &quot;Add Repository&quot; button on the left side bar.
</div> </div>
@@ -84,10 +83,10 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
} }
return ( return (
<div className="flex flex-col p-6 gap-3 bg-white custom-shadow border rounded-md"> <div className="custom-shadow flex flex-col gap-3 rounded-md border bg-white p-6">
<span className="text-muted font-bold text-xs">REPOSITORY</span> <span className="text-xs font-bold text-muted">REPOSITORY</span>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="text-dark text-3xl font-semibold"> <span className="text-3xl font-semibold text-dark">
{repository?.name} {repository?.name}
</span> </span>
@@ -98,17 +97,17 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
update.mutate(); update.mutate();
}} }}
> >
<span className="h-8 flex items-center gap-2 bg-white border border-gray-300 px-5 py-1 text-sm font-semibold rounded"> <span className="flex h-8 items-center gap-2 rounded-sm border border-gray-300 bg-white px-5 py-1 text-sm font-semibold">
{update.isLoading ? <Spinner size={4} /> : <BsArrowRepeat />} {update.isPending ? <Spinner size={4} /> : <BsArrowRepeat />}
Update Update
</span> </span>
</button> </button>
<button <button
onClick={() => { onClick={() => {
removeRepository(); void removeRepository();
}} }}
> >
<span className="h-8 flex items-center gap-2 bg-white border border-gray-300 px-5 py-1 text-sm font-semibold rounded"> <span className="flex h-8 items-center gap-2 rounded-sm border border-gray-300 bg-white px-5 py-1 text-sm font-semibold">
{isRemoveLoading ? <Spinner size={4} /> : <BsTrash3 />} {isRemoveLoading ? <Spinner size={4} /> : <BsTrash3 />}
Remove Remove
</span> </span>
@@ -119,15 +118,15 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
value={searchValue} value={searchValue}
type="text" type="text"
placeholder="Filter..." placeholder="Filter..."
className="mt-2 h-8 p-2 text-sm w-full border border-gray-300 focus:outline-none focus:border-sky-500 input-box-shadow rounded" className="input-box-shadow mt-2 h-8 w-full rounded-sm border border-gray-300 p-2 text-sm focus:border-sky-500 focus:outline-hidden"
/> />
</div> </div>
</div> </div>
<span className="text-dark text-sm bg-repository px-3 py-1 rounded-md self-start -mt-10"> <span className="-mt-10 self-start rounded-md bg-repository px-3 py-1 text-sm text-dark">
URL: <span className="font-bold">{repository?.url}</span> URL: <span className="font-bold">{repository?.url}</span>
</span> </span>
<div className="bg-secondary grid grid-cols-10 text-xs font-bold p-2 px-4 mt-4 rounded-md"> <div className="mt-4 grid grid-cols-10 rounded-md bg-secondary p-2 px-4 text-xs font-bold">
<span className="col-span-2">CHART NAME</span> <span className="col-span-2">CHART NAME</span>
<span className="col-span-6">DESCRIPTION</span> <span className="col-span-6">DESCRIPTION</span>
<span className="col-span-1 text-center">VERSION</span> <span className="col-span-1 text-center">VERSION</span>
@@ -142,7 +141,7 @@ function RepositoryViewer({ repository }: RepositoryViewerProps) {
)} )}
{showNoChartsAlert && ( {showNoChartsAlert && (
<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 repositories installed. You can add Looks like you don&apos;t have any repositories installed. You can add
one with the &quot;Add Repository&quot; button on the left side bar. one with the &quot;Add Repository&quot; button on the left side bar.
</div> </div>

View File

@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { type UseQueryResult, useMutation } from "@tanstack/react-query";
import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-slim.js"; import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-slim.js";
import { useEffect, useRef, useState } from "react";
import { import {
BsPencil, BsPencil,
BsTrash3, BsTrash3,
@@ -9,9 +9,9 @@ import {
BsArrowUp, BsArrowUp,
BsCheckCircle, BsCheckCircle,
} from "react-icons/bs"; } from "react-icons/bs";
import { Release, ReleaseRevision } from "../../data/types"; import { useNavigate, useParams, useSearchParams } from "react-router";
import StatusLabel, { DeploymentStatus } from "../common/StatusLabel";
import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import apiService from "../../API/apiService";
import { import {
useGetReleaseInfoByType, useGetReleaseInfoByType,
useGetLatestVersion, useGetLatestVersion,
@@ -19,19 +19,20 @@ import {
useRollbackRelease, useRollbackRelease,
useTestRelease, useTestRelease,
} from "../../API/releases"; } from "../../API/releases";
import type { ReleaseRevision } from "../../data/types";
import RevisionDiff from "./RevisionDiff"; import useAlertError from "../../hooks/useAlertError";
import RevisionResource from "./RevisionResource"; import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams";
import Tabs from "../Tabs"; import { diffConfiguration, isNewerVersion } from "../../utils";
import { type UseQueryResult, useMutation } from "@tanstack/react-query"; import Button from "../Button";
import StatusLabel, { DeploymentStatus } from "../common/StatusLabel";
import { InstallReleaseChartModal } from "../modal/InstallChartModal/InstallReleaseChartModal";
import Modal, { ModalButtonStyle } from "../modal/Modal"; import Modal, { ModalButtonStyle } from "../modal/Modal";
import Spinner from "../Spinner"; import Spinner from "../Spinner";
import useAlertError from "../../hooks/useAlertError"; import Tabs from "../Tabs";
import Button from "../Button"; import RevisionDiff from "./RevisionDiff";
import { InstallReleaseChartModal } from "../modal/InstallChartModal/InstallReleaseChartModal"; import RevisionImages from "./RevisionImages";
import { diffConfiguration, isNewerVersion } from "../../utils"; import RevisionRelations from "./RevisionRelations";
import useNavigateWithSearchParams from "../../hooks/useNavigateWithSearchParams"; import RevisionResource from "./RevisionResource";
import apiService from "../../API/apiService";
type RevisionTagProps = { type RevisionTagProps = {
caption: string; caption: string;
@@ -39,18 +40,18 @@ type RevisionTagProps = {
}; };
type RevisionDetailsProps = { type RevisionDetailsProps = {
release: Release; release: ReleaseRevision;
installedRevision: ReleaseRevision; installedRevision: ReleaseRevision;
isLatest: boolean; isLatest: boolean;
latestRevision: number; latestRevision: number;
}; };
export default function RevisionDetails({ const RevisionDetails = ({
release, release,
installedRevision, installedRevision,
isLatest, isLatest,
latestRevision, latestRevision,
}: RevisionDetailsProps) { }: RevisionDetailsProps) => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const revisionTabs = [ const revisionTabs = [
@@ -79,6 +80,16 @@ export default function RevisionDetails({
label: "Notes", label: "Notes",
content: <RevisionDiff latestRevision={latestRevision} />, content: <RevisionDiff latestRevision={latestRevision} />,
}, },
{
value: "images",
label: "Images",
content: <RevisionImages />,
},
{
value: "relations",
label: "Relations",
content: <RevisionRelations />,
},
]; ];
const { context, namespace, chart } = useParams(); const { context, namespace, chart } = useParams();
const tab = searchParams.get("tab"); const tab = searchParams.get("tab");
@@ -91,21 +102,21 @@ export default function RevisionDetails({
refetch: refetchLatestVersion, refetch: refetchLatestVersion,
isLoading: isLoadingLatestVersion, isLoading: isLoadingLatestVersion,
isRefetching: isRefetchingLatestVersion, isRefetching: isRefetchingLatestVersion,
} = useGetLatestVersion(release.chart_name, { cacheTime: 0 }); } = useGetLatestVersion(release.chart_name);
const [showTestsResults, setShowTestResults] = useState(false); const [showTestsResults, setShowTestResults] = useState(false);
const { setShowErrorModal } = useAlertError(); const { setShowErrorModal } = useAlertError();
const { const {
mutate: runTests, mutate: runTests,
isLoading: isRunningTests, isPending: isRunningTests,
data: testResults, data: testResults,
} = useTestRelease({ } = useTestRelease({
onError: (error) => { onError: (error) => {
setShowTestResults(false); setShowTestResults(false);
setShowErrorModal({ setShowErrorModal({
title: "Failed to run tests for chart " + chart, title: "Failed to run tests for chart " + chart,
msg: (error as Error).message, msg: error.message,
}); });
console.error("Failed to execute test for chart", error); console.error("Failed to execute test for chart", error);
}, },
@@ -135,7 +146,7 @@ export default function RevisionDetails({
}; };
const displayTestResults = () => { const displayTestResults = () => {
if (!testResults || (testResults as []).length === 0) { if (!testResults || !testResults.length) {
return ( return (
<div> <div>
Tests executed successfully Tests executed successfully
@@ -147,7 +158,7 @@ export default function RevisionDetails({
} else { } else {
return ( return (
<div> <div>
{(testResults as string).split("\n").map((line, index) => ( {testResults.split("\n").map((line, index) => (
<div key={index} className="mb-2"> <div key={index} className="mb-2">
{line} {line}
<br /> <br />
@@ -160,15 +171,22 @@ export default function RevisionDetails({
const Header = () => { const Header = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const addRepo = async () => {
await navigate(
`/repository?add_repo=true&repo_url=${latestVerData?.[0]?.urls[0]}&repo_name=${latestVerData?.[0]?.repository}`
);
};
return ( return (
<header className="flex flex-wrap justify-between"> <header className="flex flex-wrap justify-between">
<h1 className=" text-3xl font-semibold float-left mb-1 font-roboto-slab"> <h1 className="float-left mb-1 font-roboto-slab text-3xl font-semibold">
{chart} {chart}
</h1> </h1>
<div className="flex flex-row flex-wrap gap-3 float-right h-fit"> <div className="float-right flex h-fit flex-row flex-wrap gap-3">
<div className="flex flex-col"> <div className="flex flex-col">
<Button <Button
className="flex justify-center items-center gap-2 min-w-[150px] text-sm font-semibold disabled:bg-gray-200" className="flex min-w-[150px] items-center justify-center gap-2 text-sm font-semibold disabled:bg-gray-200"
onClick={() => setIsReconfigureModalOpen(true)} onClick={() => setIsReconfigureModalOpen(true)}
disabled={isLoadingLatestVersion || isRefetchingLatestVersion} disabled={isLoadingLatestVersion || isRefetchingLatestVersion}
> >
@@ -206,18 +224,16 @@ export default function RevisionDetails({
{latestVerData?.[0]?.isSuggestedRepo ? ( {latestVerData?.[0]?.isSuggestedRepo ? (
<span <span
onClick={() => { onClick={() => {
navigate( void addRepo();
`/repository?add_repo=true&repo_url=${latestVerData[0].urls[0]}&repo_name=${latestVerData[0].repository}`
);
}} }}
className="underline text-sm cursor-pointer text-blue-600" className="cursor-pointer text-sm text-blue-600 underline"
> >
Add repository for it: {latestVerData[0].repository} Add repository for it: {latestVerData[0].repository}
</span> </span>
) : ( ) : (
<span <span
onClick={() => refetchLatestVersion()} onClick={() => void refetchLatestVersion()}
className="underline cursor-pointer text-xs" className="cursor-pointer text-xs underline"
> >
Check for new version Check for new version
</span> </span>
@@ -230,7 +246,7 @@ export default function RevisionDetails({
{" "} {" "}
<Button <Button
onClick={handleRunTests} onClick={handleRunTests}
className="flex items-center gap-2 h-1/2 text-sm font-semibold" className="flex h-1/2 items-center gap-2 text-sm font-semibold"
> >
<BsCheckCircle /> <BsCheckCircle />
Run tests Run tests
@@ -242,7 +258,7 @@ export default function RevisionDetails({
onClose={() => setShowTestResults(false)} onClose={() => setShowTestResults(false)}
> >
{isRunningTests ? ( {isRunningTests ? (
<div className="flex mr-2 items-center"> <div className="mr-2 flex items-center">
<Spinner /> Waiting for completion.. <Spinner /> Waiting for completion..
</div> </div>
) : ( ) : (
@@ -263,10 +279,10 @@ export default function RevisionDetails({
: isNewerVersion(installedRevision.chart_ver, latestVerData?.[0]?.version); : isNewerVersion(installedRevision.chart_ver, latestVerData?.[0]?.version);
return ( return (
<div className="flex flex-col px-16 pt-5 gap-3"> <div className="flex flex-col gap-3 px-16 pt-5">
<StatusLabel status={release.status} /> <StatusLabel status={release.status} />
<Header /> <Header />
<div className="flex flex-row gap-6 text-sm -mt-4"> <div className="-mt-4 flex flex-row gap-6 text-sm">
<span> <span>
Revision <span className="font-semibold">#{release.revision}</span> Revision <span className="font-semibold">#{release.revision}</span>
</span> </span>
@@ -302,11 +318,11 @@ export default function RevisionDetails({
<Tabs tabs={revisionTabs} selectedTab={selectedTab} /> <Tabs tabs={revisionTabs} selectedTab={selectedTab} />
</div> </div>
); );
} };
function RevisionTag({ caption, text }: RevisionTagProps) { function RevisionTag({ caption, text }: RevisionTagProps) {
return ( return (
<span className="bg-revision p-1 rounded px-2 text-sm"> <span className="rounded-sm bg-revision p-1 px-2 text-sm">
<span>{caption}:</span> <span>{caption}:</span>
<span className="font-bold"> {text}</span> <span className="font-bold"> {text}</span>
</span> </span>
@@ -317,7 +333,7 @@ const Rollback = ({
release, release,
installedRevision, installedRevision,
}: { }: {
release: Release; release: ReleaseRevision;
installedRevision: ReleaseRevision; installedRevision: ReleaseRevision;
}) => { }) => {
const { chart, namespace, revision } = useParams(); const { chart, namespace, revision } = useParams();
@@ -326,11 +342,13 @@ const Rollback = ({
const [showRollbackDiff, setShowRollbackDiff] = useState(false); const [showRollbackDiff, setShowRollbackDiff] = useState(false);
const revisionInt = parseInt(revision || "", 10); const revisionInt = parseInt(revision || "", 10);
const { mutate: rollbackRelease, isLoading: isRollingBackRelease } = const { mutate: rollbackRelease, isPending: isRollingBackRelease } =
useRollbackRelease({ useRollbackRelease({
onSuccess: () => { onSuccess: async () => {
navigate( await navigate(
`/${namespace}/${chart}/installed/revision/${revisionInt + 1}` `/${namespace}/${chart}/installed/revision/${
installedRevision.revision + 1
}`
); );
window.location.reload(); window.location.reload();
}, },
@@ -352,7 +370,7 @@ const Rollback = ({
: revisionInt; : revisionInt;
const rollbackTitle = ( const rollbackTitle = (
<div className="font-semibold text-lg"> <div className="text-lg font-semibold">
Rollback <span className="text-red-500">{chart}</span> from revision{" "} Rollback <span className="text-red-500">{chart}</span> from revision{" "}
{installedRevision.revision} to {rollbackRevision} {installedRevision.revision} to {rollbackRevision}
</div> </div>
@@ -380,9 +398,9 @@ const Rollback = ({
id: "1", id: "1",
callback: () => { callback: () => {
rollbackRelease({ rollbackRelease({
ns: namespace as string, ns: namespace,
name: String(chart), name: String(chart),
revision: release.revision, revision: rollbackRevision,
}); });
}, },
variant: ModalButtonStyle.info, variant: ModalButtonStyle.info,
@@ -421,7 +439,7 @@ const Rollback = ({
}, [data, isLoading, fetchedDataSuccessfully]); }, [data, isLoading, fetchedDataSuccessfully]);
return ( return (
<div className="flex flex-col space-y-4"> <div className="flex flex-col gap-4">
{isLoading ? ( {isLoading ? (
<div className="flex gap-2 text-sm"> <div className="flex gap-2 text-sm">
<Spinner /> <Spinner />
@@ -441,7 +459,7 @@ const Rollback = ({
<> <>
<Button <Button
onClick={handleRollback} onClick={handleRollback}
className="flex items-center gap-2 h-1/2 text-sm font-semibold" className="flex h-1/2 items-center gap-2 text-sm font-semibold"
> >
<BsArrowRepeat /> <BsArrowRepeat />
Rollback to #{rollbackRevision} Rollback to #{rollbackRevision}
@@ -454,27 +472,23 @@ const Rollback = ({
const Uninstall = () => { const Uninstall = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { namespace = "", chart = "" } = useParams(); const { namespace = "", chart = "" } = useParams();
const { data: resources } = useGetResources(namespace, chart, { const { data: resources } = useGetResources(namespace, chart, isOpen);
enabled: isOpen,
});
const uninstallMutation = useMutation( const uninstallMutation = useMutation({
["uninstall", namespace, chart], mutationKey: ["uninstall", namespace, chart],
() => mutationFn: () =>
apiService.fetchWithDefaults( apiService.fetchWithDefaults(
"/api/helm/releases/" + namespace + "/" + chart, "/api/helm/releases/" + namespace + "/" + chart,
{ {
method: "delete", method: "delete",
} }
), ),
{ onSuccess: () => {
onSuccess: () => { window.location.href = "/";
window.location.href = "/"; },
}, });
}
);
const uninstallTitle = ( const uninstallTitle = (
<div className="font-semibold text-lg"> <div className="text-lg font-semibold">
Uninstall <span className="text-red-500">{chart}</span> from namespace{" "} Uninstall <span className="text-red-500">{chart}</span> from namespace{" "}
<span className="text-red-500">{namespace}</span> <span className="text-red-500">{namespace}</span>
</div> </div>
@@ -484,7 +498,7 @@ const Uninstall = () => {
<> <>
<Button <Button
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
className="flex items-center gap-2 hover:bg-red-200 h-1/2 text-sm font-semibold" className="flex h-1/2 items-center gap-2 text-sm font-semibold hover:bg-red-200"
> >
<BsTrash3 /> <BsTrash3 />
Uninstall Uninstall
@@ -499,7 +513,7 @@ const Uninstall = () => {
id: "1", id: "1",
callback: uninstallMutation.mutate, callback: uninstallMutation.mutate,
variant: ModalButtonStyle.info, variant: ModalButtonStyle.info,
isLoading: uninstallMutation.isLoading, isLoading: uninstallMutation.isPending,
}, },
]} ]}
containerClassNames="w-[800px]" containerClassNames="w-[800px]"
@@ -511,18 +525,18 @@ const Uninstall = () => {
key={ key={
resource.apiVersion + resource.kind + resource.metadata.name resource.apiVersion + resource.kind + resource.metadata.name
} }
className="flex justify-start gap-1 w-full mb-3" className="mb-3 flex w-full justify-start gap-1"
> >
<span <span
style={{ style={{
textAlign: "end", textAlign: "end",
paddingRight: "30px", paddingRight: "30px",
}} }}
className=" w-3/5 italic" className="w-3/5 italic"
> >
{resource.kind} {resource.kind}
</span> </span>
<span className=" w-4/5 font-semibold"> <span className="w-4/5 font-semibold">
{resource.metadata.name} {resource.metadata.name}
</span> </span>
</div> </div>
@@ -533,3 +547,5 @@ const Uninstall = () => {
</> </>
); );
}; };
export default RevisionDetails;

View File

@@ -1,14 +1,16 @@
import { ChangeEvent, useMemo, useState, useRef, useEffect } from "react";
import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-slim.js"; import { Diff2HtmlUI } from "diff2html/lib/ui/js/diff2html-ui-slim.js";
import { useGetReleaseInfoByType } from "../../API/releases"; import hljs from "highlight.js/lib/core";
import { useParams } from "react-router-dom"; import yaml from "highlight.js/lib/languages/yaml";
import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import parse from "html-react-parser"; import parse from "html-react-parser";
import { type ChangeEvent, useMemo, useState, useRef, useEffect } from "react";
import { useParams } from "react-router";
import hljs from "highlight.js"; import { useGetReleaseInfoByType } from "../../API/releases";
import Spinner from "../Spinner"; import useCustomSearchParams from "../../hooks/useCustomSearchParams";
import { diffConfiguration } from "../../utils"; import { diffConfiguration } from "../../utils";
import Spinner from "../Spinner";
hljs.registerLanguage("yaml", yaml);
type RevisionDiffProps = { type RevisionDiffProps = {
includeUserDefineOnly?: boolean; includeUserDefineOnly?: boolean;
@@ -37,7 +39,7 @@ function RevisionDiff({
"user-defined": userDefinedValue, "user-defined": userDefinedValue,
} = searchParams; } = searchParams;
//@ts-ignore //@ts-expect-error useRef need to find elegant way for it
const diffElement = useRef<HTMLElement>({}); const diffElement = useRef<HTMLElement>({});
const handleChanged = (e: ChangeEvent<HTMLInputElement>) => { const handleChanged = (e: ChangeEvent<HTMLInputElement>) => {
@@ -108,7 +110,7 @@ function RevisionDiff({
!isLoading !isLoading
) { ) {
const diff2htmlUi = new Diff2HtmlUI( const diff2htmlUi = new Diff2HtmlUI(
diffElement!.current!, diffElement.current,
data, data,
diffConfiguration diffConfiguration
); );
@@ -134,7 +136,7 @@ function RevisionDiff({
return ( return (
<div> <div>
<div className="flex mb-3 p-2 border border-revision flex-row items-center justify-between w-full bg-white rounded"> <div className="mb-3 flex w-full flex-row items-center justify-between rounded-sm border border-revision bg-white p-2">
<div className="flex items-center"> <div className="flex items-center">
<input <input
checked={viewMode === "view"} checked={viewMode === "view"}
@@ -143,7 +145,7 @@ function RevisionDiff({
type="radio" type="radio"
value="view" value="view"
name="notes-view" name="notes-view"
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300" className="h-4 w-4 border-gray-300 bg-gray-100 text-blue-600"
/> />
<label <label
htmlFor="view" htmlFor="view"
@@ -160,7 +162,7 @@ function RevisionDiff({
type="radio" type="radio"
value="diff-with-previous" value="diff-with-previous"
name="notes-view" name="notes-view"
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300" className="h-4 w-4 border-gray-300 bg-gray-100 text-blue-600"
/> />
<label <label
htmlFor="diff-with-previous" htmlFor="diff-with-previous"
@@ -177,7 +179,7 @@ function RevisionDiff({
type="radio" type="radio"
value="diff-with-specific-revision" value="diff-with-specific-revision"
name="notes-view" name="notes-view"
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300" className="h-4 w-4 border-gray-300 bg-gray-100 text-blue-600"
/> />
<label <label
htmlFor="diff-with-specific-revision" htmlFor="diff-with-specific-revision"
@@ -186,7 +188,7 @@ function RevisionDiff({
<div> <div>
Diff with specific revision: Diff with specific revision:
<input <input
className="border ml-2 border-gray-500 w-10 p-1 rounded-sm" className="ml-2 w-10 rounded-xs border border-gray-500 p-1"
type="text" type="text"
value={specificVersion} value={specificVersion}
onChange={(e) => setSpecificVersion(Number(e.target.value))} onChange={(e) => setSpecificVersion(Number(e.target.value))}
@@ -201,7 +203,7 @@ function RevisionDiff({
type="checkbox" type="checkbox"
onChange={handleUserDefinedCheckbox} onChange={handleUserDefinedCheckbox}
checked={!!userDefinedValue} checked={!!userDefinedValue}
className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" className="h-4 w-4 rounded-sm border-gray-300 bg-gray-100 text-blue-600 focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:ring-offset-gray-800 dark:focus:ring-blue-600"
/> />
<label <label
htmlFor="user-define-only-checkbox" htmlFor="user-define-only-checkbox"
@@ -214,15 +216,17 @@ function RevisionDiff({
</div> </div>
{isLoading ? <Spinner /> : ""} {isLoading ? <Spinner /> : ""}
{viewMode === VIEW_MODE_VIEW_ONLY && content ? ( {viewMode === VIEW_MODE_VIEW_ONLY && content ? (
<div className="bg-white overflow-x-auto w-full p-3 relative"> <div className="relative w-full overflow-x-auto bg-white p-3">
<pre className="bg-white rounded font-sf-mono">{parse(content)}</pre> <pre className="rounded-sm bg-white font-sf-mono">
{parse(content)}
</pre>
</div> </div>
) : ( ) : (
"" ""
)} )}
<div <div
className="bg-white w-full relative leading-5 font-sf-mono" className="relative w-full bg-white font-sf-mono leading-5"
//@ts-ignore //@ts-expect-error ref
ref={diffElement} ref={diffElement}
></div> ></div>
</div> </div>

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