Compare commits
371 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd64b54800 | ||
|
|
f12f60f0c7 | ||
|
|
b2cbf812f2 | ||
|
|
5783095e0d | ||
|
|
58ba15e1bd | ||
|
|
a6b8beb25c | ||
|
|
2b6964dcd5 | ||
|
|
e44556d100 | ||
|
|
cfc28cf3a0 | ||
|
|
443207191d | ||
|
|
c5ae60a779 | ||
|
|
4fb2eb099a | ||
|
|
62cf1dfc3e | ||
|
|
f7deda06f5 | ||
|
|
123f674e2f | ||
|
|
5d2a61c2ff | ||
|
|
f857f8dfdc | ||
|
|
6b07fbe242 | ||
|
|
2d1fa25e7e | ||
|
|
331925900a | ||
|
|
f91daafd4a | ||
|
|
a15e375105 | ||
|
|
2b7df9cfa3 | ||
|
|
b457be85c1 | ||
|
|
c9b8fb7809 | ||
|
|
939dd8ac0c | ||
|
|
ae598bec68 | ||
|
|
f647a3db03 | ||
|
|
ea7f8722ac | ||
|
|
4714d76784 | ||
|
|
5d0bdb40c1 | ||
|
|
e816f5881f | ||
|
|
2dfc25c038 | ||
|
|
aa2cc04084 | ||
|
|
65a250e2a4 | ||
|
|
323a60fe31 | ||
|
|
37af7dfbec | ||
|
|
05c7c0b5c4 | ||
|
|
9b3fd77105 | ||
|
|
9f07cea128 | ||
|
|
9d28119bc6 | ||
|
|
4c0821307d | ||
|
|
077582e795 | ||
|
|
651397e2d2 | ||
|
|
f660411722 | ||
|
|
f2eb91bc02 | ||
|
|
362f881b47 | ||
|
|
f10cc6d8a5 | ||
|
|
73f74d77bb | ||
|
|
7572f00f7c | ||
|
|
1129651e6c | ||
|
|
3f623458b3 | ||
|
|
f01c19f330 | ||
|
|
e50ae801a7 | ||
|
|
51df16e83e | ||
|
|
210a371d06 | ||
|
|
40161aee12 | ||
|
|
71d0a4d849 | ||
|
|
1d8151d41d | ||
|
|
b23310cb2d | ||
|
|
b5750ca40b | ||
|
|
63d55c1c25 | ||
|
|
756706dcd4 | ||
|
|
b76c4e077d | ||
|
|
5e24721801 | ||
|
|
7c8f3c29e0 | ||
|
|
219e6b7392 | ||
|
|
3ffdbba19b | ||
|
|
f749db9c4d | ||
|
|
996f637a9d | ||
|
|
2da8f23285 | ||
|
|
f22c84c288 | ||
|
|
285cc1fe1e | ||
|
|
96e103ff84 | ||
|
|
2717734406 | ||
|
|
9648f5ccce | ||
|
|
e1e176a22b | ||
|
|
930eefae5d | ||
|
|
aee1ac59ae | ||
|
|
cda3dd0c51 | ||
|
|
2439515055 | ||
|
|
6995fe957a | ||
|
|
5737e8495c | ||
|
|
6517c47754 | ||
|
|
aab9411ed2 | ||
|
|
63eb98e309 | ||
|
|
5b2f1e2818 | ||
|
|
945e68590b | ||
|
|
ee8bb96912 | ||
|
|
b0fb0e062b | ||
|
|
dc6d781374 | ||
|
|
d36bd6d09a | ||
|
|
21209945f2 | ||
|
|
dabf99ec1f | ||
|
|
13ac6385da | ||
|
|
2884712255 | ||
|
|
751746f0d2 | ||
|
|
e5e15f922c | ||
|
|
1a39abbdb5 | ||
|
|
69fe906c7d | ||
|
|
3b0b44f392 | ||
|
|
922bb1c7c2 | ||
|
|
f85343a173 | ||
|
|
14fa9b8894 | ||
|
|
0436eabb51 | ||
|
|
fb39d7e324 | ||
|
|
f1747b41d7 | ||
|
|
c4d4db9e68 | ||
|
|
79cbd8ee31 | ||
|
|
83c4f14acb | ||
|
|
4603c12d76 | ||
|
|
eec255df66 | ||
|
|
679843ede8 | ||
|
|
c618ea1404 | ||
|
|
f739192e80 | ||
|
|
7af6267d58 | ||
|
|
5397cec996 | ||
|
|
1ec3200ce0 | ||
|
|
780b8c4d06 | ||
|
|
2291b92f86 | ||
|
|
2868149f48 | ||
|
|
91969ab08c | ||
|
|
e997a92c63 | ||
|
|
f75beae31a | ||
|
|
d56ee6a42b | ||
|
|
d123bcee93 | ||
|
|
60e599be46 | ||
|
|
14d59e2900 | ||
|
|
fc0b051b91 | ||
|
|
16e6c49de0 | ||
|
|
80ad1c6c7a | ||
|
|
c8bf9eb05c | ||
|
|
aac7e7d2bc | ||
|
|
274f1cd6cc | ||
|
|
b5bed10571 | ||
|
|
3d7f907a33 | ||
|
|
947c0aa96f | ||
|
|
20128d39f3 | ||
|
|
46fef30caf | ||
|
|
f7e92f1744 | ||
|
|
6a7cadef70 | ||
|
|
383760e7a7 | ||
|
|
e954b7a37c | ||
|
|
9481c9afed | ||
|
|
4afb773205 | ||
|
|
4eb1dd91b1 | ||
|
|
03c2f321c4 | ||
|
|
37557126f0 | ||
|
|
7aeabc081c | ||
|
|
436e01fcbf | ||
|
|
d78e0c5866 | ||
|
|
4c84b795a0 | ||
|
|
de1915e1c2 | ||
|
|
f465c83c07 | ||
|
|
9ffcde1950 | ||
|
|
aa43dee3a4 | ||
|
|
1c3b69b3ec | ||
|
|
745545f05e | ||
|
|
764ad3e03a | ||
|
|
c251e6c697 | ||
|
|
88ea89a5ba | ||
|
|
d236ca99c8 | ||
|
|
e4d4baa0b1 | ||
|
|
dd7aca70ff | ||
|
|
133eef6745 | ||
|
|
9a3407dbd9 | ||
|
|
e4240ed107 | ||
|
|
227966b2f1 | ||
|
|
be3a5b8605 | ||
|
|
2843830ea1 | ||
|
|
d46d7ab1da | ||
|
|
0b354331d5 | ||
|
|
64331ad33a | ||
|
|
5b60263a41 | ||
|
|
95f14ed295 | ||
|
|
eb51f8a130 | ||
|
|
5531286da6 | ||
|
|
e8fe75e433 | ||
|
|
aa79f67bf2 | ||
|
|
4e2d3186a1 | ||
|
|
14abc28c7d | ||
|
|
b61adf133f | ||
|
|
27eb7949e5 | ||
|
|
b90198915e | ||
|
|
64975cac42 | ||
|
|
087399ad49 | ||
|
|
fc385344f4 | ||
|
|
56932f2c34 | ||
|
|
24df4a21d6 | ||
|
|
bea75cb011 | ||
|
|
a07c8f273d | ||
|
|
eb11a8f26e | ||
|
|
f0545d35f1 | ||
|
|
57f7c47dd1 | ||
|
|
0b4031bf24 | ||
|
|
e143963d46 | ||
|
|
b933e2dd9b | ||
|
|
0e15fe2001 | ||
|
|
021fe9c897 | ||
|
|
5f6104dbba | ||
|
|
8e9a464d62 | ||
|
|
3a7bb3efb6 | ||
|
|
d2259241e6 | ||
|
|
aad9992302 | ||
|
|
30eb209043 | ||
|
|
1dcb77812f | ||
|
|
245863b2f9 | ||
|
|
dd1fe05d65 | ||
|
|
450804ba24 | ||
|
|
a2ddb94c16 | ||
|
|
861de33bfe | ||
|
|
26d82dd5ab | ||
|
|
b1294cbe1a | ||
|
|
d4583a222e | ||
|
|
a0bf59edc6 | ||
|
|
79a79979e2 | ||
|
|
76e4fe51b5 | ||
|
|
95ea5e4d6d | ||
|
|
c139f3941d | ||
|
|
80022c3ef8 | ||
|
|
a07cfcdbb4 | ||
|
|
8826124f70 | ||
|
|
703b4029de | ||
|
|
a2dc1ed96b | ||
|
|
29c1682bbb | ||
|
|
c7d18a7fb7 | ||
|
|
e9ee10287b | ||
|
|
57d4d073e9 | ||
|
|
47dae4d35a | ||
|
|
0ac8eec368 | ||
|
|
aec46d43f7 | ||
|
|
37e1d44bf1 | ||
|
|
362cb09e6d | ||
|
|
209f5b5e44 | ||
|
|
a0680a4820 | ||
|
|
d95cac94d5 | ||
|
|
bbb425bfea | ||
|
|
679d31e4ab | ||
|
|
3119d17738 | ||
|
|
778e58360c | ||
|
|
a7c7ba80fe | ||
|
|
d86c46aabf | ||
|
|
c79259275a | ||
|
|
4a4760d5b8 | ||
|
|
244e35bb6b | ||
|
|
709c3c600b | ||
|
|
3060b92f8e | ||
|
|
f49f52efe4 | ||
|
|
6a4ca793c9 | ||
|
|
61b67f8bed | ||
|
|
ac690b6332 | ||
|
|
b613e4e9dc | ||
|
|
a9939d5067 | ||
|
|
7a25335028 | ||
|
|
8befc1d017 | ||
|
|
aaf6ae80c5 | ||
|
|
8faa5dabd5 | ||
|
|
0b891e7c28 | ||
|
|
2736777b8a | ||
|
|
96c789a012 | ||
|
|
e13aa2fde6 | ||
|
|
6ffcdf2b8e | ||
|
|
e6d6ff41a9 | ||
|
|
0e45e89a1e | ||
|
|
2df533ab5b | ||
|
|
7e32008bfe | ||
|
|
23cfd2d61b | ||
|
|
0e6231dfbd | ||
|
|
4ebf67095a | ||
|
|
5902bc010e | ||
|
|
36217401c8 | ||
|
|
e6381f96e9 | ||
|
|
efe394c8c4 | ||
|
|
a91287c2ff | ||
|
|
28a4b37bb5 | ||
|
|
2e1f2e481b | ||
|
|
714c0f9e02 | ||
|
|
35421ede58 | ||
|
|
26e0b1db32 | ||
|
|
50947e585d | ||
|
|
40ae829186 | ||
|
|
87ee388bfb | ||
|
|
b76fbb130a | ||
|
|
f3c66ecf03 | ||
|
|
83e4348ace | ||
|
|
af1c09ae02 | ||
|
|
6e5bce26e8 | ||
|
|
d0d2ed3ef1 | ||
|
|
450190a1aa | ||
|
|
74aab13e3e | ||
|
|
c5cd12b6b2 | ||
|
|
fd650f10b6 | ||
|
|
3e1e4be4b3 | ||
|
|
d0c9de9718 | ||
|
|
1a1b28d09f | ||
|
|
6c4ee06d27 | ||
|
|
7c5ffe9c07 | ||
|
|
196644683c | ||
|
|
20ee6e9695 | ||
|
|
e9abedbadb | ||
|
|
5d0a148fea | ||
|
|
c0cf6237e6 | ||
|
|
9876b6a12e | ||
|
|
90815f2271 | ||
|
|
e83ddbb15d | ||
|
|
78f458112b | ||
|
|
f6d3e519e2 | ||
|
|
9bac7306a4 | ||
|
|
030708e7cc | ||
|
|
0b8a258f7f | ||
|
|
2c25193adf | ||
|
|
2c1883c835 | ||
|
|
960c268224 | ||
|
|
c4e5094ce5 | ||
|
|
74f6236ba6 | ||
|
|
9b8edb6a39 | ||
|
|
56e9430155 | ||
|
|
a89ccbdab7 | ||
|
|
fa4819b353 | ||
|
|
8de7941063 | ||
|
|
34158a7a9c | ||
|
|
3384db7193 | ||
|
|
7de7c85426 | ||
|
|
8e65c555e0 | ||
|
|
2557e6b73d | ||
|
|
4f75ee06a0 | ||
|
|
717adc9e9c | ||
|
|
15adeb7cfa | ||
|
|
0b06036a39 | ||
|
|
f7d4dcbff4 | ||
|
|
8334f2b0b2 | ||
|
|
5cccb1caa0 | ||
|
|
db9cdeb1c9 | ||
|
|
3abae8e49e | ||
|
|
bedb356b02 | ||
|
|
34a7dc57b2 | ||
|
|
d0dbb42492 | ||
|
|
1393b117cf | ||
|
|
cf407c63a2 | ||
|
|
3f00e8ef6d | ||
|
|
758b03de36 | ||
|
|
76d55f8e44 | ||
|
|
b9392ab4c9 | ||
|
|
dadf2d1bde | ||
|
|
74c2a3d6e7 | ||
|
|
2454fcf47c | ||
|
|
96a7a429e1 | ||
|
|
f29800ed5b | ||
|
|
15ce9170f3 | ||
|
|
9a144c1c6f | ||
|
|
f897c0f197 | ||
|
|
671fa949df | ||
|
|
612352d69f | ||
|
|
ef31263797 | ||
|
|
f64fbd4a2e | ||
|
|
dffb8a726b | ||
|
|
14d4886e61 | ||
|
|
0012b0a797 | ||
|
|
f6b2a8c66d | ||
|
|
c0a1d31c8d | ||
|
|
329ae055ee | ||
|
|
7ab0f33201 | ||
|
|
2e8ba39b8f | ||
|
|
c5f9f71e45 | ||
|
|
9bb597f366 | ||
|
|
786bddc478 | ||
|
|
d9edcf2f48 | ||
|
|
2262445b75 | ||
|
|
0c486e76c0 | ||
|
|
7d50f4e620 | ||
|
|
b2ec371709 |
6
.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
Dockerfile
|
||||
*.md
|
||||
bin
|
||||
.idea
|
||||
.git
|
||||
frontend/node_modules
|
||||
59
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: 🐛 Bug
|
||||
|
||||
description: Report an issue to help improve the project.
|
||||
|
||||
labels: ["🛠 goal: fix"]
|
||||
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
|
||||
id: description
|
||||
|
||||
attributes:
|
||||
|
||||
label: Description
|
||||
|
||||
description: A brief description of the question or issue, also include what you tried and what didn't work
|
||||
|
||||
validations:
|
||||
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
|
||||
id: screenshots
|
||||
|
||||
attributes:
|
||||
|
||||
label: Screenshots
|
||||
|
||||
description: Please add screenshots if applicable
|
||||
|
||||
validations:
|
||||
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
|
||||
id: extrainfo
|
||||
|
||||
attributes:
|
||||
|
||||
label: Additional information
|
||||
|
||||
description: Is there anything else we should know about this bug?
|
||||
|
||||
validations:
|
||||
|
||||
required: false
|
||||
|
||||
- type: markdown
|
||||
|
||||
attributes:
|
||||
|
||||
value: |
|
||||
|
||||
You can also join our slack community [here](https://komodorkommunity.slack.com)
|
||||
|
||||
Feel free to check out other cool repositories of the [komodorio](https://github.com/komodorio)
|
||||
68
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
# This configures label matching for PR's.
|
||||
#
|
||||
# The keys are labels, and the values are lists of minimatch patterns
|
||||
# to which those labels apply.
|
||||
#
|
||||
# NOTE: This can only add labels, not remove them.
|
||||
# NOTE: Due to YAML syntax limitations, patterns or labels which start
|
||||
# with a character that is part of the standard YAML syntax must be
|
||||
# quoted.
|
||||
#
|
||||
# Please keep the labels sorted and deduplicated.
|
||||
|
||||
api:
|
||||
- pkg/dashboard/api.go
|
||||
|
||||
app:
|
||||
- main.go
|
||||
- pkg/dashboard/server.go
|
||||
- pkg/dashboard/subproc/*
|
||||
- pkg/dashboard/utils/*
|
||||
|
||||
backend:
|
||||
- pkg/dashboard/handlers/*
|
||||
- pkg/dashboard/scanners/*
|
||||
|
||||
ci:
|
||||
- .github/workflow/build.yml
|
||||
- ci/*
|
||||
- Makefile
|
||||
- scripts/*
|
||||
|
||||
docs:
|
||||
- CODE_OF_CONDUCT.md
|
||||
- CONTRIBUTING.md
|
||||
- LICENSE
|
||||
- README.md
|
||||
- screenshot*.png
|
||||
- screenshot*.svg
|
||||
|
||||
docker:
|
||||
- .dockerignore
|
||||
- Dockerfile
|
||||
|
||||
helm-charts:
|
||||
- charts/*
|
||||
|
||||
github-actions:
|
||||
- .github/ISSUE_TEMPLATE/*
|
||||
- .github/labeler.yml
|
||||
- .github/pull_request_template
|
||||
- .github/workflow/pull-request-labeler.yaml
|
||||
|
||||
release:
|
||||
- .github/workflows/publish-chart.yaml
|
||||
- .github/workflows/release.yaml
|
||||
- .goreleaser.yml
|
||||
- artifacthub-repo.yml
|
||||
- plugin.yaml
|
||||
|
||||
scanners:
|
||||
- pkg/dashboard/scanners/*
|
||||
|
||||
tests:
|
||||
- pkg/dashboard/**/*_test.go
|
||||
- pkg/dashboard/objects/testdata/*
|
||||
|
||||
frontend:
|
||||
- pkg/dashboard/static/*
|
||||
21
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
## Changes Proposed
|
||||
|
||||
<!-- Describe the proposed changes and any additional information -->
|
||||
|
||||
<!-- Add all the screenshots which illustrate your changes -->
|
||||
|
||||
## Check List
|
||||
|
||||
<!-- Mark all the applicable boxes. To mark the box as done follow the following conventions -->
|
||||
<!--
|
||||
[x] - Correct; marked as done
|
||||
[X] - Correct; marked as done
|
||||
|
||||
[ ] - Not correct; marked as **not** done
|
||||
-->
|
||||
|
||||
- [ ] The title of my pull request is a short description of the changes
|
||||
- [ ] This PR relates to some issue: <!-- use "Closes #999" to auto-close related issue -->
|
||||
- [ ] I have documented the changes made (if applicable)
|
||||
- [ ] I have covered the changes with unit tests
|
||||
|
||||
124
.github/workflows/build.yml
vendored
@@ -2,23 +2,46 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Node part
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: NPM install
|
||||
run: npm i
|
||||
working-directory: ./frontend
|
||||
- name: NPM build
|
||||
run: npm run build
|
||||
working-directory: ./frontend
|
||||
|
||||
# Golang part
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: Unit tests
|
||||
run: |
|
||||
go test -v -race ./... # Run all the tests with the race detector enabled
|
||||
go test -v -race ./... -covermode=atomic -coverprofile=coverage.out # Run all the tests with the race detector enabled
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
- name: Static analysis
|
||||
run: |
|
||||
go vet ./... # go vet is the official Go static analyzer
|
||||
@@ -29,15 +52,90 @@ jobs:
|
||||
- name: Dry Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --snapshot --rm-dist
|
||||
- name: Test Binary is Runnable
|
||||
version: "1.18.2"
|
||||
args: release --snapshot --clean
|
||||
- name: Test if the Binary is Runnable
|
||||
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: binaries
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: komodorio/helm-dashboard
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_PASS }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: komodorio/helm-dashboard:unstable
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: VER=0.0.0-dev
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
static_and_lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: make dir for frontend results # don't delete this step, it will break goreleaser
|
||||
run: mkdir pkg/frontend/dist && touch pkg/frontend/dist/.gitkeep
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3.2.0
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
# version: latest
|
||||
# skip-go-installation: true
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
||||
# args: --timeout=15m
|
||||
args: --timeout=5m
|
||||
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: ./frontend/package-lock.json
|
||||
- name: NPM install
|
||||
run: npm i
|
||||
working-directory: ./frontend
|
||||
- name: NPM lint
|
||||
run: npm run lint
|
||||
working-directory: ./frontend
|
||||
|
||||
- name: Helm Template Check For Sanity
|
||||
uses: igabaydulin/helm-check-action@0.2.1
|
||||
env:
|
||||
CHART_LOCATION: ./charts/helm-dashboard
|
||||
CHART_VALUES: ./charts/helm-dashboard/values.yaml
|
||||
- name: Test if the Helm plugin install script is runnable
|
||||
run: |
|
||||
scripts/install_plugin.sh
|
||||
|
||||
43
.github/workflows/publish-chart.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: publish helm chart
|
||||
|
||||
# for manual running in case we need to update the chart without releasing the dashboard app
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
HELM_REP: helm-charts
|
||||
GH_OWNER: komodorio
|
||||
CHART_DIR: charts/helm-dashboard
|
||||
|
||||
jobs:
|
||||
publish_chart:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Bump versions
|
||||
run: |
|
||||
git config user.email komi@komodor.io
|
||||
git config user.name komodor-bot
|
||||
git fetch --tags
|
||||
git checkout main
|
||||
sh ./ci/bump-versions.sh
|
||||
git add charts/helm-dashboard/Chart.yaml
|
||||
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
|
||||
git push -f || echo "Nothing to push!"
|
||||
env:
|
||||
APP_VERSION: ${{ needs.pre_release.outputs.release_tag }}
|
||||
- name: Push folder to helm-charts repository
|
||||
uses: crykn/copy_folder_to_another_repo_action@v1.0.6
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.KOMI_WORKFLOW_TOKEN }}
|
||||
with:
|
||||
source_folder: "charts/helm-dashboard"
|
||||
destination_repo: "komodorio/helm-charts"
|
||||
destination_folder: "charts/helm-dashboard"
|
||||
user_email: "komi@komodor.io"
|
||||
user_name: "komodor-bot"
|
||||
destination_branch: "master"
|
||||
commit_msg: "feat(helm-dashboard): update chart" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo
|
||||
14
.github/workflows/pull-request-labeler.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: "Pull Request Labeler"
|
||||
on:
|
||||
- pull_request_target
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
124
.github/workflows/release.yaml
vendored
@@ -1,30 +1,134 @@
|
||||
name: release
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
- "v*"
|
||||
|
||||
env:
|
||||
HELM_REP: helm-charts
|
||||
GH_OWNER: komodorio
|
||||
CHART_DIR: charts/helm-dashboard
|
||||
|
||||
jobs:
|
||||
release:
|
||||
pre_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
- name: Get tag name
|
||||
id: get_tag_name
|
||||
run: echo "TAG_NAME=$(echo ${{ github.ref_name }} | cut -d 'v' -f2)" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
release_tag: ${{ steps.get_tag_name.outputs.TAG_NAME }}
|
||||
|
||||
release:
|
||||
needs: pre_release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Node part
|
||||
- name: Setup Node.js environment
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
go-version: 1.18
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: NPM install
|
||||
run: npm i
|
||||
working-directory: ./frontend
|
||||
- name: NPM build
|
||||
run: npm run build
|
||||
working-directory: ./frontend
|
||||
|
||||
# Golang part
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
- name: git cleanup
|
||||
run: git clean -f
|
||||
run: git clean -f && git checkout frontend/yarn.lock
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
version: "1.18.2"
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Test Binary Versions
|
||||
run: "dist/helm-dashboard_linux_amd64_v1/helm-dashboard --help"
|
||||
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ release, pre_release ]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker meta
|
||||
uses: docker/metadata-action@v3
|
||||
id: meta
|
||||
with:
|
||||
images: komodorio/helm-dashboard
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_PASS }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
context: .
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: komodorio/helm-dashboard:${{ needs.pre_release.outputs.release_tag }},komodorio/helm-dashboard:latest
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: VER=${{ needs.pre_release.outputs.release_tag }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
publish_chart:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ image, pre_release ]
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Bump versions
|
||||
run: |
|
||||
git config user.email komi@komodor.io
|
||||
git config user.name komodor-bot
|
||||
git fetch --tags
|
||||
git checkout main
|
||||
sh ./ci/bump-versions.sh
|
||||
git add charts/helm-dashboard/Chart.yaml
|
||||
git add plugin.yaml
|
||||
git commit -m "Increment chart versions [skip ci]" || echo "Already up-to-date"
|
||||
git push -f || echo "Nothing to push!"
|
||||
env:
|
||||
APP_VERSION: ${{ needs.pre_release.outputs.release_tag }}
|
||||
- name: Push folder to helm-charts repository
|
||||
uses: crykn/copy_folder_to_another_repo_action@v1.0.6
|
||||
env:
|
||||
API_TOKEN_GITHUB: ${{ secrets.KOMI_WORKFLOW_TOKEN }}
|
||||
with:
|
||||
source_folder: "charts/helm-dashboard"
|
||||
destination_repo: "komodorio/helm-charts"
|
||||
destination_folder: "charts/helm-dashboard"
|
||||
user_email: "komi@komodor.io"
|
||||
user_name: "komodor-bot"
|
||||
destination_branch: "master"
|
||||
commit_msg: "feat(OSS helm-dashboard): ${{ github.event.head_commit.message }}" #important!! don't change this commit message unless you change the condition in pipeline.yml on helm-charts repo
|
||||
|
||||
8
.gitignore
vendored
@@ -15,6 +15,7 @@
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
*.cov
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
@@ -24,3 +25,10 @@ go.work
|
||||
|
||||
/bin
|
||||
/.idea/
|
||||
|
||||
/node_modules
|
||||
.DS_Store
|
||||
.vscode/
|
||||
/pkg/dashboard/objects/testdata/hello-world-0.1.0.tgz
|
||||
/pkg/frontend/dist/*
|
||||
/dist/
|
||||
|
||||
@@ -11,6 +11,9 @@ builds:
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
ignore:
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
archives:
|
||||
|
||||
2
.husky/pre-commit
Executable file
@@ -0,0 +1,2 @@
|
||||
cd frontend || exit 1
|
||||
npm run pre:commit
|
||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
https://komodorkommunity.slack.com/archives/C044U1B0265.
|
||||
itiel@komodor.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
|
||||
55
CONTRIBUTING.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Contributing to Helm Dashboard
|
||||
|
||||
We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's:
|
||||
|
||||
- Reporting a bug
|
||||
- Discussing the current state of the code
|
||||
- Submitting a fix
|
||||
- Proposing new features
|
||||
|
||||
## We Develop with GitHub
|
||||
|
||||
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
|
||||
|
||||
## First-time contributors
|
||||
|
||||
We've tagged some issues to make it easy to get started :smile:
|
||||
[Good first issues](https://github.com/komodorio/helm-dashboard/labels/good%20first%20issue)
|
||||
|
||||
Add a comment on the issue and wait for the issue to be assigned before you start working on it. This helps to avoid multiple people working on similar issues.
|
||||
|
||||
## All Code Changes Happen Through Pull Requests
|
||||
|
||||
Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests:
|
||||
|
||||
1. Fork the repo and create your branch from `main`.
|
||||
2. If you've added code that should be tested, add tests.
|
||||
3. Ensure the test suite passes.
|
||||
4. Make sure your code lints.
|
||||
5. Issue that pull request!
|
||||
|
||||
## Any contributions you make will be under the Apache License 2.0
|
||||
|
||||
In short, when you submit code changes, your submissions are understood to be under the same [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) that covers the project.
|
||||
|
||||
## Report bugs using GitHub's [issues](https://github.com/komodorio/helm-dashboard/issues)
|
||||
|
||||
We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/komodorio/helm-dashboard/issues/new) and labeling it with the `bug` label. It's that easy!
|
||||
|
||||
**Great Bug Reports** tend to have:
|
||||
|
||||
- A quick summary and/or background
|
||||
- Steps to reproduce
|
||||
- Be specific!
|
||||
- Give sample code if you can.
|
||||
- What you expected would happen
|
||||
- What actually happens
|
||||
- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work)
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under its Apache License 2.0.
|
||||
|
||||
## Questions?
|
||||
|
||||
Contact us on [Slack](https://komodorkommunity.slack.com).
|
||||
52
Dockerfile
Normal file
@@ -0,0 +1,52 @@
|
||||
# Stage - frontend
|
||||
FROM node:latest as frontend
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY frontend ./
|
||||
|
||||
RUN npm i && npm run build
|
||||
|
||||
# Stage - builder
|
||||
FROM --platform=${BUILDPLATFORM:-linux/amd64} golang as builder
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GOOS=${TARGETOS:-linux}
|
||||
ENV GOARCH=${TARGETARCH:-amd64}
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
ARG VER=0.0.0
|
||||
ENV VERSION=${VER}
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY go.mod ./
|
||||
COPY go.sum ./
|
||||
COPY main.go ./
|
||||
RUN go mod download
|
||||
|
||||
ADD . src
|
||||
|
||||
COPY --from=frontend /pkg/frontend/dist ./src/pkg/frontend/dist/
|
||||
|
||||
WORKDIR /build/src
|
||||
|
||||
RUN make build_go
|
||||
|
||||
# Stage - runner
|
||||
FROM --platform=${TARGETPLATFORM:-linux/amd64} alpine
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
ARG BUILDPLATFORM
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
COPY --from=builder /build/src/bin/dashboard /bin/helm-dashboard
|
||||
|
||||
ENTRYPOINT ["/bin/helm-dashboard", "--no-browser", "--bind=0.0.0.0", "--port=8080"]
|
||||
|
||||
# docker build . -t komodorio/helm-dashboard:0.0.0 && kind load docker-image komodorio/helm-dashboard:0.0.0
|
||||
209
FEATURES.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Helm Dashboard Features Overview
|
||||
|
||||
## General Layout and Navigation
|
||||
|
||||
### Shutting down the app
|
||||
|
||||
If you run the tool locally, you can shutdown the running process. This is useful when you can't find the console where
|
||||
you started it, or when it was started without console.
|
||||
|
||||
To close Helm-dashboard, click on the button in the rightmost corner of the screen. Once you click on it, your
|
||||
Helm-dashboard will be shut down.
|
||||
|
||||

|
||||
|
||||
## Releases Management Section
|
||||
|
||||
### Switching Clusters
|
||||
|
||||
When started as local binary, the tool reads the list of available cluster connections from kubectl config file. Those
|
||||
connections are displayed on the left side of the screen.
|
||||
|
||||

|
||||
|
||||
If you want to switch to a different cluster, simply click on the corresponding cluster as shown in the
|
||||
figure. You
|
||||
can [read here](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) to
|
||||
learn on how to configure access to multiple clusters.
|
||||
# Reset Cache
|
||||
The "Reset Cache" feature in Helm Dashboard clears the cached data and fetches the latest information from the backend or data source. It ensures that the dashboard displays up-to-date data and reflects any recent changes or updates.
|
||||

|
||||
|
||||
# Repository
|
||||
Essentially, a repository is a location where charts are gathered and can be shared. If you want to learn more about repositories, [click here](https://helm.sh/docs/topics/chart_repository/). You can find the repository in the home section, as depicted in the figure.
|
||||

|
||||
|
||||
You can add the repository by clicking on 'Add Repository', as shown in the figure.
|
||||

|
||||
|
||||
## Installed Releases List
|
||||
|
||||
A release is an installation of your Helm chart deployed in your Kubernetes Cluster. That means every time that you
|
||||
install or upgrade a Helm chart, it creates a new release revision that coexists with other releases. You
|
||||
can filter releases based on namespaces or search for release names
|
||||

|
||||
|
||||
The squares represent k8s resources installed by the release. Hover over each square to view a tooltip with details.
|
||||
Yellow indicates "pending," green signifies a healthy state, and red indicates an unhealthy state.
|
||||

|
||||
|
||||
It indicates the version of chart that corresponds to this release.
|
||||

|
||||
|
||||
A revision is linked to a release to track the number of updates/changes that release encounters.
|
||||

|
||||
|
||||
Namespaces are a way to organize clusters into virtual sub-clusters — they can be helpful when different teams or
|
||||
projects share a Kubernetes cluster. Any number of namespaces are supported within a cluster, each logically separated
|
||||
from others but with the ability to communicate with each other.
|
||||

|
||||
|
||||
Updated" refers to the amount of time that has passed since the last revision of the release. Whenever you install or
|
||||
upgrade the release, a new revision is created. You can think of it as the "age" of the latest revision.
|
||||

|
||||
|
||||
Indication of upgrade possible/repo suggested.
|
||||

|
||||
|
||||
## Release details
|
||||
|
||||
This indicates the status of the deployed release, and 'Age' represents the amount of time that has passed since the
|
||||
creation of the revision until now.
|
||||

|
||||
|
||||
You can use the Upgrade/Downgrade button to switch to different release versions, as shown in the figure.
|
||||

|
||||
|
||||
Confirm the upgrade settings and configuration and click on confirm button to continue
|
||||

|
||||
|
||||
Once the upgrade is done, your release will show the status
|
||||

|
||||
|
||||
It executes the test scripts or commands within the deployed application's environment and displays the results
|
||||

|
||||
|
||||
Running test hooks results
|
||||

|
||||
|
||||
The Helm Dashboard provides basic information about your Helm releases, including revision number, deployment date, release message, cluster details, and deployment status.
|
||||

|
||||
|
||||
### Resource Tab
|
||||
|
||||
In the Resources tab of the Helm Dashboard, you can view the kind (type), name, status, and any associated messages for your Kubernetes resources.
|
||||

|
||||
|
||||
### Manifest Tab
|
||||
|
||||
Text: The Manifests tab displays the textual representation of the Kubernetes manifests associated with your Helm release. It provides the YAML or JSON configuration files that define the desired state of the resources deployed in your cluster.
|
||||

|
||||
|
||||
Diff with Previous: The "Diff with Previous" feature allows you to compare the current version of the manifests with the previous version. It highlights the differences between the two versions, making it easier to identify the changes made in the deployment.
|
||||

|
||||
|
||||
Diff with Specific: The "Diff with Specific" option enables you to compare the current version of the manifests with a specific past version. It allows you to select a particular revision and view the differences between that revision and the current one.
|
||||

|
||||
|
||||
### Values Tab
|
||||
|
||||
Text: The Values tab displays the textual representation of the values file associated with your Helm chart. It shows the YAML or JSON file that contains the configuration values used during the deployment of the chart.
|
||||

|
||||
|
||||
Diff: The "Diff" feature allows you to compare the current values with the previous values used in a previous deployment. It highlights any differences between the two versions, making it easier to identify changes made to the configuration.
|
||||

|
||||
|
||||
### Text Tab
|
||||
|
||||
Text: The Notes tab displays any accompanying notes or documentation related to the Helm chart.
|
||||

|
||||
|
||||
Diff with previous: The "Diff" feature is not typically available in the Notes tab. This tab is primarily meant for viewing the static text-based notes associated with the chart and doesn't involve comparisons or tracking changes like in the Manifests or Values tabs.
|
||||

|
||||
|
||||
Diff with Specific Version: Similar to the previous point, the ability to view a diff with a specific version of the notes is not a standard feature in the Notes section of the Helm Dashboard. The Notes section usually presents the information for the specific version of the Helm chart that is currently deployed.
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Execute Helm tests
|
||||
|
||||
For all the release(s) (installed helm charts), you can execute helm tests for that release. For the tests to execute
|
||||
successfully, you need to have existing tests for that helm chart.
|
||||
|
||||
You can execute `helm test` for the specific release as below:
|
||||

|
||||
|
||||
The result of executed `helm test` for the release will be displayed as below:
|
||||

|
||||
|
||||
### Scanner Integrations
|
||||
|
||||
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy)
|
||||
and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s
|
||||
resources page, as well as install/upgrade preview page.
|
||||
|
||||
You can request scanning of the specific k8s resource in your cluster:
|
||||

|
||||
|
||||
If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems"
|
||||
button at the bottom of the dialog:
|
||||

|
||||
|
||||
## Repository Section
|
||||
|
||||
Essentially, a repository is a location where charts are gathered and can be shared over network. If you want to learn
|
||||
more about Helm chart
|
||||
repositories, [click here](https://helm.sh/docs/topics/chart_repository/).
|
||||
|
||||
You can access the repository management area of Helm Dashboard in the main navigation section, as depicted in the
|
||||
figure.
|
||||
|
||||

|
||||
|
||||
You can add the repository by clicking on 'Add Repository' button on the left, as shown in the figure.
|
||||

|
||||
|
||||
After completing that step, enter the following data: the repository name and its URL. You can also add the username and
|
||||
password, although this is optional.
|
||||

|
||||
|
||||
Updating means refreshing your repository. You can update your repository as shown in the figure.
|
||||

|
||||
|
||||
If you want to remove your repository from the Helm dashboard, click on the 'Remove' button as shown in the figure.
|
||||

|
||||
|
||||
Use the filter option to find the desired chart quicker from the list of charts.
|
||||

|
||||
|
||||
If you want to install a particular chart, simply hover the pointer over the chart name and an 'Install' button will
|
||||
appear, as shown in the figure.
|
||||

|
||||
|
||||
# Support for Local Charts
|
||||
|
||||
Local Helm chart is a directory with specially named files and a `Chart.yaml` file, which you can install via Helm,
|
||||
without the need to publish the chart into Helm repository. Chart developers might want to experiment with the chart
|
||||
locally, before uploading into public repository. Also, a proprietary application might only use non-published chart as
|
||||
an approach to deploy the software.
|
||||
|
||||
For all the above use-cases, you may use Helm Dashboard UI, specifying the location of your local chart folders via
|
||||
special `--local-chart` command-line parameter. The parameter might be specified multiple times, for example:
|
||||
|
||||
```shell
|
||||
helm-dashboard --local-chart=/opt/charts/my-private-app --local-chart=/home/dev/sources/app/chart
|
||||
```
|
||||
|
||||
When _valid_ local chart sources specified, the repository list would contain a surrogate `[local]` entry, with those
|
||||
charts listed inside. All the chart operations are normal: installing, reconfiguring and upgrading.
|
||||
|
||||

|
||||
|
||||
34
Makefile
@@ -1,9 +1,31 @@
|
||||
pull:
|
||||
git pull
|
||||
DATE ?= $(shell date +%FT%T%z)
|
||||
VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \
|
||||
cat $(CURDIR)/.version 2> /dev/null || echo "v0")
|
||||
|
||||
build:
|
||||
go build -o bin/dashboard .
|
||||
.PHONY: test
|
||||
test: ; $(info $(M) start unit testing...) @
|
||||
@go test $$(go list ./... | grep -v /mocks/) --race -v -short -coverpkg=./... -coverprofile=profile.cov
|
||||
@echo "\n*****************************"
|
||||
@echo "** TOTAL COVERAGE: $$(go tool cover -func profile.cov | grep total | grep -Eo '[0-9]+\.[0-9]+')% **"
|
||||
@echo "*****************************\n"
|
||||
|
||||
.PHONY: pull
|
||||
pull: ; $(info $(M) Pulling source...) @
|
||||
@git pull
|
||||
|
||||
debug:
|
||||
DEBUG=1 ./bin/dashboard
|
||||
.PHONY: build_go
|
||||
build_go: $(BIN) ; $(info $(M) Building GO...) @ ## Build program binary
|
||||
go build \
|
||||
-ldflags '-X main.version=$(VERSION) -X main.buildDate=$(DATE)' \
|
||||
-o bin/dashboard .
|
||||
|
||||
.PHONY: build_ui
|
||||
build_ui: $(BIN) ; $(info $(M) Building UI...) @ ## Build program binary
|
||||
cd frontend && npm i && npm run build && cd ..
|
||||
|
||||
.PHONY: build
|
||||
build: build_ui build_go ; $(info $(M) Building executable...) @ ## Build program binary
|
||||
|
||||
.PHONY: debug
|
||||
debug: ; $(info $(M) Running dashboard in debug mode...) @
|
||||
@DEBUG=1 ./bin/dashboard
|
||||
121
README.md
@@ -1,19 +1,26 @@
|
||||
# <img src="pkg/dashboard/static/logo.png" height=30 style="height: 2rem"> Helm Dashboard
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="images/logo-header-inverted.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="images/logo-header.svg#gh-light-mode-only">
|
||||
<img alt="Helm Dashboard" src="images/logo-header.svg#gh-light-mode-only">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
A simplified way of working with Helm.
|
||||
<p align="center">A simplified way of working with Helm.</p>
|
||||
|
||||
<kbd>[<img src="screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](screenshot.png)</kbd>
|
||||
 [](https://github.com/komodorio/helm-dashboard/issues)    [](https://github.com/komodorio/helm-dashboard/releases)  [](https://github.com/komodorio/helm-dashboard) [](https://codecov.io/gh/komodorio/helm-dashboard)
|
||||
|
||||
## What it Does?
|
||||
<kbd>[<img src="images/screenshot.png" style="width: 100%; border: 1px solid silver;" border="1" alt="Screenshot">](images/screenshot.png)</kbd>
|
||||
|
||||
The _Helm Dashboard_ plugin offers a UI-driven way to view the installed Helm charts, see their revision history and
|
||||
corresponding k8s resources. Also, you can perform simple actions like roll back to a revision or upgrade to newer
|
||||
version.
|
||||
## Description
|
||||
|
||||
This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision of
|
||||
helping Kubernetes users to navigate and troubleshoot their clusters.
|
||||
_Helm Dashboard_ is an **open-source project** which offers a UI-driven way to view the installed Helm charts, see their revision history and
|
||||
corresponding k8s resources. It also allows users to perform simple actions such as rolling back to a
|
||||
revision or upgrading to a newer version.
|
||||
This project is part of [Komodor's](https://komodor.com/?utm_campaign=Helm-Dash&utm_source=helm-dash-gh) vision to
|
||||
help Kubernetes users to navigate and troubleshoot their clusters. It is important to note that Helm Dashboard is **NOT** an official project by the [helm team](https://helm.sh/).
|
||||
|
||||
Some of the key capabilities of the tool:
|
||||
Key capabilities of the tool:
|
||||
|
||||
- See all installed charts and their revision history
|
||||
- See manifest diff of the past revisions
|
||||
@@ -21,10 +28,22 @@ Some of the key capabilities of the tool:
|
||||
- Easy rollback or upgrade version with a clear and easy manifest diff
|
||||
- Integration with popular problem scanners
|
||||
- Easy switch between multiple clusters
|
||||
- Can be used locally, or installed into Kubernetes cluster
|
||||
- Does not require Helm or Kubectl installed
|
||||
|
||||
## Installing
|
||||
All the features of the tool can be discovered via our [features overview page](FEATURES.md).
|
||||
|
||||
To install it, simply run Helm command:
|
||||
## Installation
|
||||
|
||||
### Standalone Binary
|
||||
|
||||
Since version 1.0, the recommended install method is to just use standalone binary. It does not require Helm or kubectl to be installed.
|
||||
|
||||
Download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it. See below section for some more CLI parameters to use.
|
||||
|
||||
### Using Helm plugin manager
|
||||
|
||||
To install dashboard as Helm plugin, simply run Helm command:
|
||||
|
||||
```shell
|
||||
helm plugin install https://github.com/komodorio/helm-dashboard.git
|
||||
@@ -42,10 +61,6 @@ To uninstall, run:
|
||||
helm plugin uninstall dashboard
|
||||
```
|
||||
|
||||
Note: In case standard Helm plugin way did not work for you, you can just download the appropriate [release package](https://github.com/komodorio/helm-dashboard/releases) for your platform, unpack it and just run `dashboard` binary from it.
|
||||
|
||||
## Running
|
||||
|
||||
To use the plugin, your machine needs to have working `helm` and also `kubectl` commands. Helm version 3.4.0+ is required.
|
||||
|
||||
After installing, start the UI by running:
|
||||
@@ -54,49 +69,75 @@ After installing, start the UI by running:
|
||||
helm dashboard
|
||||
```
|
||||
|
||||
The command above will launch the local Web server and will open the UI in new browser tab. The command will hang
|
||||
The command above will launch the local Web server and will open the UI in a new browser tab. The command will hang
|
||||
waiting for you to terminate it in command-line or web UI.
|
||||
|
||||
You can see the list of available command-line flags by running `helm dashboard --help`.
|
||||
|
||||
By default, the web server is only available locally. You can change that by specifying `HD_BIND` environment variable
|
||||
to the desired value. For example, `0.0.0.0` would bind to all IPv4 addresses or `[::0]` would be all IPv6 addresses.
|
||||
This can also be specified using flag `--bind <host>`, for example `--bind=0.0.0.0` or `--bind 0.0.0.0`.
|
||||
|
||||
If your port 8080 is busy, you can specify a different port to use via `HD_PORT` environment variable.
|
||||
> Precedence order: flag `--bind=<host>` > env `HD_BIND=<host>` > default value `localhost`
|
||||
|
||||
If you don't want browser tab to automatically open, set `HD_NOBROWSER=1` in your environment variables.
|
||||
If your port 8080 is busy, you can specify a different port to use via `--port <number>` command-line flag.
|
||||
|
||||
If you want to increase the logging verbosity and see all the debug info, set `DEBUG=1` environment variable.
|
||||
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.
|
||||
|
||||
## Scanner Integrations
|
||||
If you don't want the browser tab to automatically open, add `--no-browser` flag in your command-line.
|
||||
|
||||
Upon startup, Helm Dashboard detects the presence of [Trivy](https://github.com/aquasecurity/trivy)
|
||||
and [Checkov](https://github.com/bridgecrewio/checkov) scanners. When available, these scanners are offered on k8s
|
||||
resources page, as well as install/upgrade preview page.
|
||||
If you want to increase the logging verbosity and see all the debug info, use the `--verbose` flag.
|
||||
|
||||
You can request scanning of the specific k8s resource in your cluster:
|
||||

|
||||
> Disclaimer: For the sake of improving the project quality, there is user analytics collected by the tool. You can disable this collecting with `--no-analytics` option. The collection is done via DataDog RUM and Heap Analytics. Only the anonymous data is collected, no sensitive information is used.
|
||||
|
||||
If you want to validate the k8s manifest prior to installing/reconfiguring a Helm chart, look for "Scan for Problems"
|
||||
button at the bottom of the dialog:
|
||||

|
||||
### Deploying Helm Dashboard on Kubernetes
|
||||
|
||||
The official helm chart is [available here](https://github.com/komodorio/helm-charts/blob/master/charts/helm-dashboard)
|
||||
|
||||
## Support Channels
|
||||
|
||||
We have two main channels for supporting the Helm Dashboard
|
||||
users: [Slack community](https://komodorkommunity.slack.com/archives/C044U1B0265) for general conversations
|
||||
users: [Slack community](https://komodorkommunity.slack.com) for general conversations
|
||||
and [GitHub issues](https://github.com/komodorio/helm-dashboard/issues) for real bugs.
|
||||
|
||||
## Contributing
|
||||
|
||||
Kindly read our [Contributing Guide](CONTRIBUTING.md) to learn and understand about our development process, how to propose bug fixes and improvements, and how to build and test your changes to Helm Dashboard. <br>
|
||||
|
||||
## Contributors
|
||||
|
||||
<a href="https://github.com/komodorio/helm-dashboard/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=komodorio/helm-dashboard" />
|
||||
</a>
|
||||
|
||||
## Local Dev Testing
|
||||
|
||||
Prerequisites: `helm` and `kubectl` binaries installed and operational.
|
||||
Prerequisites, binaries installed and operational:
|
||||
|
||||
There is a need to build binary for plugin to function, run:
|
||||
- [Golang](https://go.dev/doc/install)
|
||||
- Node.js
|
||||
|
||||
There is a need to build frontend and then backend as a series of commands, run:
|
||||
|
||||
### Linux
|
||||
|
||||
```shell
|
||||
cd frontend && npm run build && cd ..
|
||||
go build -o bin/dashboard .
|
||||
```
|
||||
|
||||
You can just run the `bin/dashboard` binary directly, it will just work.
|
||||
Or just `make build` that will do everything inside.
|
||||
|
||||
Then, you can run `npm run dev` from `frontend` directory to work on frontend with Vite hot reload.
|
||||
|
||||
### Windows
|
||||
|
||||
```bat
|
||||
cd frontend && npm run build && cd ..
|
||||
go build -o bin\dashboard.exe .
|
||||
```
|
||||
|
||||
You can just run the `dashboard` or `dashboard.exe` binary directly.
|
||||
|
||||
To install, checkout the source code and run from source dir:
|
||||
|
||||
@@ -104,7 +145,7 @@ To install, checkout the source code and run from source dir:
|
||||
helm plugin install .
|
||||
```
|
||||
|
||||
Local installation of plugin just creates a symlink, so making the changes and rebuilding the binary would not require
|
||||
A local installation of the plugin just creates a symlink, so making the changes and rebuilding the binary would not require
|
||||
to
|
||||
reinstall a plugin.
|
||||
|
||||
@@ -115,3 +156,15 @@ helm dashboard
|
||||
```
|
||||
|
||||
Then, use the web UI.
|
||||
|
||||
## Development Snapshots
|
||||
|
||||
In our GitHub actions, we attach the built binaries as build artifacts, you can download and test it fully assembled.
|
||||
|
||||
Also, we upload `unstable` tag for Docker image upon every build of `main` branch, you can make our Helm chart to use that image by providing values:
|
||||
|
||||
```yaml
|
||||
image:
|
||||
pullPolicy: Always
|
||||
tag: unstable
|
||||
```
|
||||
|
||||
5
artifacthub-repo.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
# Artifact Hub repository metadata file
|
||||
repositoryID: 9ed6d12d-b3d5-4efd-836e-3ac9fa9dd3d1
|
||||
owners:
|
||||
- name: komodor-bot
|
||||
email: komi@komodor.io
|
||||
23
charts/helm-dashboard/.helmignore
Normal file
@@ -0,0 +1,23 @@
|
||||
# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
9
charts/helm-dashboard/Chart.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
|
||||
name: helm-dashboard
|
||||
description: A GUI Dashboard for Helm by Komodor
|
||||
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/refs/heads/main/images/logo.svg"
|
||||
|
||||
version: 2.0.5
|
||||
appVersion: "2.1.0"
|
||||
92
charts/helm-dashboard/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Helm Dashboard
|
||||
|
||||
## TL;DR;
|
||||
|
||||
```bash
|
||||
helm repo add komodorio https://helm-charts.komodor.io
|
||||
helm repo update
|
||||
helm upgrade --install helm-dashboard komodorio/helm-dashboard
|
||||
```
|
||||
|
||||
## Introduction
|
||||
|
||||
This chart bootstraps a Helm Dashboard deployment on a [Kubernetes](http://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager.
|
||||
|
||||
While installed inside cluster, Helm Dashboard will run some additional background actions, for example, will automatically update Helm repositories. To enable that behavior locally, set `HD_CLUSTER_MODE` env variable.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Kubernetes 1.16+
|
||||
|
||||
## Installing the Chart
|
||||
|
||||
To install the chart with the release name `helm-dashboard`:
|
||||
|
||||
```bash
|
||||
helm install helm-dashboard .
|
||||
```
|
||||
|
||||
The command deploys Helm Dashboard on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation.
|
||||
|
||||
> **Tip**: List all releases using `helm list`
|
||||
|
||||
## Uninstalling the Chart
|
||||
|
||||
To uninstall/delete the `helm-dashboard` deployment:
|
||||
|
||||
```bash
|
||||
helm uninstall helm-dashboard
|
||||
```
|
||||
|
||||
The command removes all the Kubernetes components associated with the chart and deletes the release.
|
||||
|
||||
## Adding Authentication
|
||||
|
||||
The task of authentication and user control is out of scope for Helm Dashboard. Luckily, there are third-party solutions which are dedicated to provide that functionality.
|
||||
|
||||
For instance, you can place authentication proxy in front of Helm Dashboard, like this one: https://github.com/oauth2-proxy/oauth2-proxy
|
||||
|
||||
## Parameters
|
||||
|
||||
The following table lists the configurable parameters of the chart and their default values.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|---------------------------------------|------------------------------------------------------------------------------------------------|----------------------------------|
|
||||
| `global.imageRegistry` | Registry for all images, useful for private registry | `""` |
|
||||
| `global.imagePullSecrets` | Specify Docker-registry secret names as an array | `[]` |
|
||||
| `image.repository` | Image registry/name | `komodorio/helm-dashboard` |
|
||||
| `image.tag` | Image tag | |
|
||||
| `image.imagePullSecrets` | Specify Docker-registry secret names as an array | `[]` |
|
||||
| `image.pullPolicy` | Image pull policy | `IfNotPresent` |
|
||||
| `replicaCount` | Number of dashboard Pods to run | `1` |
|
||||
| `dashboard.allowWriteActions` | Enables write actions. Allow modifying, deleting and creating charts and kubernetes resources. | `true` |
|
||||
| `resources.requests.cpu` | CPU resource requests | `200m` |
|
||||
| `resources.limits.cpu` | CPU resource limits | `1` |
|
||||
| `resources.requests.memory` | Memory resource requests | `256Mi` |
|
||||
| `resources.limits.memory` | Memory resource limits | `1Gi` |
|
||||
| `service.type ` | Kubernetes service type | `ClusterIP` |
|
||||
| `service.port ` | Kubernetes service port | `8080` |
|
||||
| `serviceAccount.create` | Creates a service account | `true` |
|
||||
| `serviceAccount.name` | Optional name for the service account | `{RELEASE_FULLNAME}` |
|
||||
| `nodeSelector` | Node labels for pod assignment | |
|
||||
| `affinity` | Affinity settings for pod assignment | |
|
||||
| `tolerations` | Tolerations for pod assignment | |
|
||||
| `dashboard.persistence.enabled` | Enable helm data persistene using PVC | `true` |
|
||||
| `dashboard.persistence.accessModes` | Persistent Volume access modes | `["ReadWriteOnce"]` |
|
||||
| `dashboard.persistence.storageClass` | Persistent Volume storage class | `""` |
|
||||
| `dashboard.persistence.size` | Persistent Volume size | `100M` |
|
||||
| `dashboard.persistence.finalizers` | Finalizers for the Persistent Volume Claim | `[kubernetes.io/pvc-protection]` |
|
||||
| `dashboard.persistence.lookupVolumeName` | Lookup volume name for the Persistent Volume Claim | `true` |
|
||||
| `updateStrategy.type` | Set up update strategy for helm-dashboard installation. | `RollingUpdate` |
|
||||
| `extraArgs` | Set the arguments to be supplied to the helm-dashboard binary | `[--no-browser, --bind=0.0.0.0]` |
|
||||
| `testImage.repository` | Test image registry/name | `busybox` |
|
||||
| `testImage.tag` | Test image tag | `lastest` |
|
||||
| `testImage.imagePullSecrets` | Specify Docker-registry secret names as an array | `[]` |
|
||||
| `testImage.pullPolicy` | Test image pull policy | `IfNotPresent` |
|
||||
Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`.
|
||||
|
||||
```bash
|
||||
helm upgrade --install helm-dashboard komodorio/helm-dashboard --set dashboard.allowWriteActions=true --set service.port=9090
|
||||
```
|
||||
|
||||
> **Tip**: You can use the default [values.yaml](values.yaml)
|
||||
16
charts/helm-dashboard/templates/NOTES.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
Thank you for installing Helm Dashboard.
|
||||
Helm Dashboard can be accessed:
|
||||
* Within your cluster, at the following DNS name at port {{ .Values.service.port }}:
|
||||
|
||||
{{ template "helm-dashboard.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local
|
||||
|
||||
* From outside the cluster, run these commands in the same shell:
|
||||
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "helm-dashboard.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
|
||||
|
||||
Visit our repo at:
|
||||
https://github.com/komodorio/helm-dashboard
|
||||
|
||||
51
charts/helm-dashboard/templates/_commons.tpl
Normal file
@@ -0,0 +1,51 @@
|
||||
{{/*
|
||||
Return the proper image name
|
||||
{{ include "common.images.image" ( dict "imageRoot" .Values.path.to.the.image "global" .Values.global ) }}
|
||||
*/}}
|
||||
{{- define "common.images.image" -}}
|
||||
{{- $registryName := .imageRoot.registry -}}
|
||||
{{- $repositoryName := .imageRoot.repository -}}
|
||||
{{- $separator := ":" -}}
|
||||
{{- $termination := .imageRoot.tag | toString -}}
|
||||
{{- if .global }}
|
||||
{{- if .global.imageRegistry }}
|
||||
{{- $registryName = .global.imageRegistry -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if .imageRoot.digest }}
|
||||
{{- $separator = "@" -}}
|
||||
{{- $termination = .imageRoot.digest | toString -}}
|
||||
{{- end -}}
|
||||
{{- if $registryName }}
|
||||
{{- printf "%s/%s%s%s" $registryName $repositoryName $separator $termination -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s%s%s" $repositoryName $separator $termination -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return the proper Docker Image Registry Secret Names (deprecated: use common.images.renderPullSecrets instead)
|
||||
{{ include "common.images.pullSecrets" ( dict "images" (list .Values.path.to.the.image1, .Values.path.to.the.image2) "global" .Values.global) }}
|
||||
*/}}
|
||||
{{- define "common.images.pullSecrets" -}}
|
||||
{{- $pullSecrets := list }}
|
||||
|
||||
{{- if .global }}
|
||||
{{- range .global.imagePullSecrets -}}
|
||||
{{- $pullSecrets = append $pullSecrets . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- range .images -}}
|
||||
{{- range .pullSecrets -}}
|
||||
{{- $pullSecrets = append $pullSecrets . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- if (not (empty $pullSecrets)) }}
|
||||
imagePullSecrets:
|
||||
{{- range $pullSecrets }}
|
||||
- name: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
76
charts/helm-dashboard/templates/_helpers.tpl
Normal file
@@ -0,0 +1,76 @@
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "helm-dashboard.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "helm-dashboard.fullname" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
||||
{{- $fullname := default (ternary .Release.Name (printf "%s-%s" .Release.Name $name) (contains $name .Release.Name)) .Values.fullnameOverride }}
|
||||
{{- $fullname | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "helm-dashboard.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Common labels
|
||||
*/}}
|
||||
{{- define "helm-dashboard.labels" -}}
|
||||
helm.sh/chart: {{ include "helm-dashboard.chart" . }}
|
||||
{{ include "helm-dashboard.selectorLabels" . }}
|
||||
{{- if .Chart.AppVersion }}
|
||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||
{{- end }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Selector labels
|
||||
*/}}
|
||||
{{- define "helm-dashboard.selectorLabels" -}}
|
||||
app.kubernetes.io/name: {{ include "helm-dashboard.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use
|
||||
*/}}
|
||||
{{- define "helm-dashboard.serviceAccountName" -}}
|
||||
{{- default (.Values.serviceAccount.create | ternary (include "helm-dashboard.fullname" .) "default") .Values.serviceAccount.name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Return the proper image Registry Secret Names
|
||||
*/}}
|
||||
{{- define "helm-dashboard.imagePullSecrets" -}}
|
||||
{{ include "common.images.pullSecrets" (dict "images" (list .Values.image) "global" .Values.global) }}
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{/*
|
||||
Return the proper image name
|
||||
*/}}
|
||||
{{- define "helm-dashboard.image" -}}
|
||||
{{- $image := .Values.image -}}
|
||||
{{- $tag := default .Chart.AppVersion $image.tag -}}
|
||||
{{- $_ := set $image "tag" $tag -}}
|
||||
{{ include "common.images.image" (dict "imageRoot" $_ "global" .Values.global) }}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Return the proper image name
|
||||
*/}}
|
||||
{{- define "test.image" -}}
|
||||
{{ include "common.images.image" (dict "imageRoot" .Values.testImage "global" .Values.global) }}
|
||||
{{- end -}}
|
||||
90
charts/helm-dashboard/templates/deployment.yaml
Normal file
@@ -0,0 +1,90 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.fullname" . }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
spec:
|
||||
{{- if not .Values.autoscaling.enabled }}
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
{{- end }}
|
||||
selector:
|
||||
matchLabels:
|
||||
{{- include "helm-dashboard.selectorLabels" . | nindent 6 }}
|
||||
strategy: {{- toYaml .Values.updateStrategy | nindent 4 }}
|
||||
template:
|
||||
metadata:
|
||||
{{- with .Values.podAnnotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.selectorLabels" . | nindent 8 }}
|
||||
spec:
|
||||
{{- include "helm-dashboard.imagePullSecrets" . | nindent 6 }}
|
||||
serviceAccountName: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
command:
|
||||
- /bin/helm-dashboard
|
||||
args:
|
||||
{{- with .Values.extraArgs }}
|
||||
{{- toYaml . | nindent 12 }}
|
||||
{{- end }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||
image: "{{ include "helm-dashboard.image" . }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: HELM_CACHE_HOME
|
||||
value: /opt/dashboard/helm/cache
|
||||
- name: HELM_CONFIG_HOME
|
||||
value: /opt/dashboard/helm/config
|
||||
- name: HELM_DATA_HOME
|
||||
value: /opt/dashboard/helm/data
|
||||
- name: DEBUG
|
||||
value: {{- ternary " '1'" "" .Values.debug }}
|
||||
{{- if .Values.dashboard.namespace }}
|
||||
- name: HELM_NAMESPACE
|
||||
value: {{ .Values.dashboard.namespace }}
|
||||
{{end}}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /status
|
||||
port: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /status
|
||||
port: 8080
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /opt/dashboard/helm
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
volumes:
|
||||
- name: data
|
||||
{{- if .Values.dashboard.persistence.enabled }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ include "helm-dashboard.fullname" . }}
|
||||
{{- else }}
|
||||
emptyDir: { }
|
||||
{{- end }}
|
||||
|
||||
61
charts/helm-dashboard/templates/ingress.yaml
Normal file
@@ -0,0 +1,61 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "helm-dashboard.fullname" . -}}
|
||||
{{- $svcPort := .Values.service.port -}}
|
||||
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
{{- else -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
{{- end }}
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||
ingressClassName: {{ .Values.ingress.className }}
|
||||
{{- end }}
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ .path }}
|
||||
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||
pathType: {{ .pathType }}
|
||||
{{- end }}
|
||||
backend:
|
||||
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||
service:
|
||||
name: {{ $fullName }}
|
||||
port:
|
||||
number: {{ $svcPort }}
|
||||
{{- else }}
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: {{ $svcPort }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
31
charts/helm-dashboard/templates/pvc.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
{{- if .Values.dashboard.persistence.enabled -}}
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.fullname" . }}
|
||||
namespace: {{ .Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
{{- with .Values.dashboard.persistence.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
{{- with .Values.dashboard.persistence.finalizers }}
|
||||
finalizers:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{- range .Values.dashboard.persistence.accessModes }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.dashboard.persistence.size | quote }}
|
||||
{{- if and (.Values.dashboard.persistence.lookupVolumeName) (lookup "v1" "PersistentVolumeClaim" .Release.Namespace (include "helm-dashboard.fullname" .)) }}
|
||||
volumeName: {{ (lookup "v1" "PersistentVolumeClaim" .Release.Namespace (include "helm-dashboard.fullname" .)).spec.volumeName }}
|
||||
{{- end }}
|
||||
{{- with .Values.dashboard.persistence.storageClassName }}
|
||||
storageClassName: {{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
18
charts/helm-dashboard/templates/service.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.fullname" . }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
{{- include "helm-dashboard.selectorLabels" . | nindent 4 }}
|
||||
{{- if .Values.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
|
||||
{{- end }}
|
||||
38
charts/helm-dashboard/templates/serviceaccount.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
{{- with .Values.serviceAccount.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
---
|
||||
kind: ClusterRole
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
rules:
|
||||
- apiGroups: ["*"]
|
||||
resources: ["*"]
|
||||
{{- if .Values.dashboard.allowWriteActions }}
|
||||
verbs: ["get", "list", "watch", "create", "delete", "patch", "update"]
|
||||
{{- else }}
|
||||
verbs: ["get", "list", "watch"]
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
namespace: {{ .Release.Namespace }}
|
||||
name: {{ include "helm-dashboard.serviceAccountName" . }}
|
||||
16
charts/helm-dashboard/templates/tests/test-connection.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "helm-dashboard.fullname" . }}-test-connection"
|
||||
labels:
|
||||
{{- include "helm-dashboard.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
"helm.sh/hook": test
|
||||
spec:
|
||||
{{- include "helm-dashboard.imagePullSecrets" . | nindent 2 }}
|
||||
containers:
|
||||
- name: wget
|
||||
image: {{ include "test.image" . }}
|
||||
command: ['wget']
|
||||
args: ['--timeout=5', '{{ include "helm-dashboard.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
149
charts/helm-dashboard/values.yaml
Normal file
@@ -0,0 +1,149 @@
|
||||
replicaCount: 1
|
||||
|
||||
# Flag for setting environment to debug mode
|
||||
debug: false
|
||||
|
||||
global:
|
||||
## @param global.imageRegistry Global Docker image registry
|
||||
imageRegistry: ""
|
||||
## Optionally specify an array of imagePullSecrets.
|
||||
## Example:
|
||||
## imagePullSecrets:
|
||||
## - myRegistryKeySecretName
|
||||
imagePullSecrets: []
|
||||
|
||||
image:
|
||||
repository: komodorio/helm-dashboard
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
tag: ""
|
||||
# Specifies the exact image digest to pull.
|
||||
digest: ""
|
||||
imagePullSecrets: []
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a service account should be created
|
||||
create: true
|
||||
# The name of the service account to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name: ""
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 200m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 1
|
||||
memory: 1Gi
|
||||
|
||||
dashboard:
|
||||
allowWriteActions: true
|
||||
|
||||
# default namespace for Helm operations
|
||||
namespace: ""
|
||||
|
||||
persistence:
|
||||
enabled: true
|
||||
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||
## GKE, AWS & OpenStack)
|
||||
##
|
||||
# storageClassName: default
|
||||
|
||||
## Helm Dashboard Persistent Volume access modes
|
||||
## Must match those of existing PV or dynamic provisioner
|
||||
## Ref: http://kubernetes.io/docs/user-guide/persistent-volumes/
|
||||
##
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
## Helm Dashboard Persistent Volume labels
|
||||
##
|
||||
labels: {}
|
||||
|
||||
## Helm Dashboard Persistent Volume annotations
|
||||
##
|
||||
annotations: {}
|
||||
|
||||
## Finalizer to ensure PVC is not deleted until the pod is terminated
|
||||
##
|
||||
finalizers:
|
||||
- kubernetes.io/pvc-protection
|
||||
|
||||
## Helm Dashboard data Persistent Volume size
|
||||
##
|
||||
size: 100M
|
||||
|
||||
## If 'lookupVolumeName' is set to true, Helm will attempt to retrieve
|
||||
## the current value of 'spec.volumeName' and incorporate it into the template.
|
||||
lookupVolumeName: true
|
||||
|
||||
## @param.updateStrategy.type Set up update strategy for helm-dashboard installation.
|
||||
## Set to Recreate if you use persistent volume that cannot be mounted by more than one pods to make sure the pods is destroyed first.
|
||||
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
|
||||
## Example:
|
||||
## updateStrategy:
|
||||
## type: RollingUpdate
|
||||
## rollingUpdate:
|
||||
## maxSurge: 25%
|
||||
## maxUnavailable: 25%
|
||||
##
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
|
||||
|
||||
podAnnotations: {}
|
||||
|
||||
podSecurityContext: {}
|
||||
|
||||
securityContext: {}
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 8080
|
||||
loadBalancerIP: null
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
annotations: {}
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths:
|
||||
- path: /
|
||||
pathType: ImplementationSpecific
|
||||
tls: []
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 100
|
||||
targetCPUUtilizationPercentage: 80
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
extraArgs:
|
||||
- --no-browser
|
||||
- --bind=0.0.0.0
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
|
||||
|
||||
testImage:
|
||||
repository: busybox
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
# Overrides the image tag whose default is the chart appVersion.
|
||||
# tag: ""
|
||||
## Optionally specify an array of imagePullSecrets.
|
||||
## Example:
|
||||
## imagePullSecrets:
|
||||
## - myRegistryKeySecretName
|
||||
imagePullSecrets: []
|
||||
10
ci/build-ui.sh
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
WORKING_DIRECTORY="$PWD"
|
||||
|
||||
cd "$WORKING_DIRECTORY/dashboard"
|
||||
|
||||
npm i
|
||||
npm run build
|
||||
|
||||
cp -a "$WORKING_DIRECTORY/dashboard/static/" "$WORKING_DIRECTORY/pkg/dashboard/static/"
|
||||
15
ci/bump-versions.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
WORKING_DIRECTORY="$PWD"
|
||||
|
||||
[ -z "$HELM_CHARTS_SOURCE" ] && HELM_CHARTS_SOURCE="$WORKING_DIRECTORY/charts/helm-dashboard"
|
||||
|
||||
[ -z "$APP_VERSION" ] && {
|
||||
APP_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'appVersion:' | awk -F'"' '{print $2}')
|
||||
}
|
||||
|
||||
sed -i -e "s/appVersion.*/appVersion: \"${APP_VERSION}\"/g" ${HELM_CHARTS_SOURCE}/Chart.yaml
|
||||
sed -i -e "s/version.*/version: \"${APP_VERSION}\"/g" plugin.yaml
|
||||
CURRENT_VERSION=$(cat ${HELM_CHARTS_SOURCE}/Chart.yaml | grep 'version:' | awk '{print $2}')
|
||||
NEW_VERSION=$(echo $CURRENT_VERSION | awk -F. '{$NF = $NF + 1;} 1' | sed 's/ /./g')
|
||||
sed -i -e "s/${CURRENT_VERSION}/${NEW_VERSION}/g" ${HELM_CHARTS_SOURCE}/Chart.yaml
|
||||
1
frontend/.env
Normal file
@@ -0,0 +1 @@
|
||||
VITE_SERVER_PORT=8080
|
||||
2
frontend/.flowbite-react/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
class-list.json
|
||||
pid
|
||||
10
frontend/.flowbite-react/config.json
Normal 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
|
||||
}
|
||||
22
frontend/.flowbite-react/init.tsx
Normal 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";
|
||||
27
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
static
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
/storybook-static
|
||||
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
legacy-peer-deps=true
|
||||
10
frontend/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
.env
|
||||
.gitignore
|
||||
.npmrc
|
||||
.prettierignore
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
.flowbite-react/*
|
||||
8
frontend/.prettierrc.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
trailingComma: "es5"
|
||||
tabWidth: 2
|
||||
semi: true
|
||||
singleQuote: false
|
||||
bracketSpacing: true
|
||||
plugins:
|
||||
- "prettier-plugin-tailwindcss" # should be last https://github.com/tailwindlabs/prettier-plugin-tailwindcss?tab=readme-ov-file#compatibility-with-other-prettier-plugins
|
||||
tailwindStylesheet: "./src/index.css"
|
||||
15
frontend/.storybook/main.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ["../src/**/*.stories.@(js|jsx|ts|tsx|mdx)"],
|
||||
|
||||
addons: ["@storybook/addon-links", "@storybook/addon-docs"],
|
||||
core: {},
|
||||
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
frontend/.storybook/preview-body.html
Normal file
@@ -0,0 +1 @@
|
||||
<div id="portal"></div>
|
||||
3
frontend/.storybook/preview-head.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<script>
|
||||
window.global = window;
|
||||
</script>
|
||||
33
frontend/.storybook/preview.tsx
Normal 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"];
|
||||
72
frontend/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Helm dashboard React
|
||||
|
||||
Welcome to the frontend of the helm dashboard.
|
||||
We care most about keeping the project:
|
||||
|
||||
1. Maintainable
|
||||
2. Extendable
|
||||
3. Contributor friendly
|
||||
|
||||
# The FE Stack
|
||||
|
||||
- Vite, as our build tool.
|
||||
- React, as our UI library.
|
||||
- TypeScript and ESLint will keep the project safe, please keep them clean.
|
||||
- Tailwind for styling.
|
||||
- React-Query for fetching data from the backend.
|
||||
- Storybook is utilized to develop a component library.
|
||||
|
||||
Please follow through the file structure to understand how things are structured and should be used.
|
||||
|
||||
# Contribution guide
|
||||
|
||||
## Setting up your development environment
|
||||
|
||||
1. First you should fork this repository.
|
||||
2. Clone your new repository using `git clone <https_or_ssh_url>`.
|
||||
|
||||
## Running helm dashboard
|
||||
|
||||
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.
|
||||
3. go to `frontend` in your local project.
|
||||
4. in order to install dependencies and start the development server
|
||||
- `npm i`
|
||||
- `npm run dev`
|
||||
5. with the default integration the dashboard should run on http://localhost:5173/
|
||||
|
||||
# Component library
|
||||
|
||||
We created a components library to have a consistent design system throughout the project. Please rely on these components.
|
||||
Additional information and examples on how to use them are available when you run Storybook, which shows them in an interactive way and in different scenarios.
|
||||
|
||||
Once you run it, you'll be able to see pre-made scenarios, documentation, and play with the component properties.
|
||||
|
||||
To run Storybook, make sure that all the dependencies are installed and run:
|
||||
|
||||
```shell
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Refer to the [official documentation](https://storybook.js.org/docs/react/get-started/install) for more information.
|
||||
|
||||
# Helpers
|
||||
|
||||
- Icons: https://react-icons.github.io/react-icons/
|
||||
- Tailwind: https://tailwindcss.com/docs
|
||||
- Typescript: https://www.typescriptlang.org/docs/handbook/intro.html
|
||||
- React-query: https://react-query.tanstack.com/overview
|
||||
|
||||
# Coding Conventions
|
||||
|
||||
- Use only functional components
|
||||
- Please prefer async/await over .then
|
||||
- wrap every function with try/catch unless you want to display the error to the user
|
||||
in such case we have a general error handler in the app.tsx file which will display the error to the user in a modal
|
||||
- Please use the component library we created, it will help us keep a consistent design system
|
||||
- Please use the react-query library to fetch data from the backend
|
||||
- Prefer use fetch API over axios, if you see axios in the code, replace it with fetch.
|
||||
- Use <Outlet> for inner routes
|
||||
- User query params in the url for filters or any other state that can be represented
|
||||
- Hooks:
|
||||
- useCustomSearchParams - for search params
|
||||
18
frontend/cypress.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
allowCypressEnv: false,
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "vite",
|
||||
},
|
||||
},
|
||||
|
||||
e2e: {
|
||||
baseUrl: "http://localhost:5173",
|
||||
// setupNodeEvents(on, config) {
|
||||
// // implement node event listeners here
|
||||
// },
|
||||
},
|
||||
});
|
||||
50
frontend/cypress/e2e/addRepository.cy.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
describe("Adding repository flow", () => {
|
||||
const addChartNameInput = "[data-cy='add-chart-name']";
|
||||
const addChartUrlInput = "[data-cy='add-chart-url']";
|
||||
const addChartRepositoryButton = "[data-cy='add-chart-repository-button']";
|
||||
|
||||
it("Adding new chart repository", () => {
|
||||
cy.intercept("GET", "/status", {
|
||||
fixture: "status.json",
|
||||
}).as("status");
|
||||
|
||||
cy.intercept("GET", "/api/helm/releases", {
|
||||
fixture: "releases.json",
|
||||
}).as("releases");
|
||||
|
||||
cy.visit("/#/minikube/installed?filteredNamespace=default");
|
||||
|
||||
cy.get("[data-cy='navigation-link']").contains("Repository").click();
|
||||
cy.get("[data-cy='install-repository-button']").click();
|
||||
|
||||
cy.get(addChartNameInput).type("Komodorio");
|
||||
cy.get(addChartUrlInput).type("https://helm-charts.komodor.io");
|
||||
|
||||
cy.intercept("GET", "/api/helm/repositories", {
|
||||
fixture: "repositories.json",
|
||||
}).as("repositories");
|
||||
|
||||
cy.get(addChartRepositoryButton).click();
|
||||
cy.wait("@repositories");
|
||||
|
||||
cy.contains("https://helm-charts.komodor.io");
|
||||
|
||||
cy.get("[data-cy='chart-viewer-install-button']")
|
||||
.eq(0)
|
||||
.click({ force: true })
|
||||
.contains("Install")
|
||||
.click();
|
||||
|
||||
cy.intercept("POST", "/api/helm/releases/default", {
|
||||
fixture: "defaultReleases.json",
|
||||
}).as("defaultReleases");
|
||||
|
||||
cy.intercept("GET", "/api/helm/releases/default/helm-dashboard/history", {
|
||||
fixture: "history.json",
|
||||
}).as("history");
|
||||
|
||||
cy.contains("Confirm").click();
|
||||
|
||||
cy.wait(["@defaultReleases", "@history"]);
|
||||
});
|
||||
});
|
||||
192
frontend/cypress/fixtures/defaultReleases.json
Normal file
5
frontend/cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
13
frontend/cypress/fixtures/history.json
Normal file
@@ -0,0 +1,13 @@
|
||||
[
|
||||
{
|
||||
"revision": 1,
|
||||
"updated": "2024-01-17T22:39:07.2371554+02:00",
|
||||
"status": "deployed",
|
||||
"chart": "helm-dashboard-0.1.10",
|
||||
"app_version": "1.3.3",
|
||||
"description": "Install complete",
|
||||
"chart_name": "helm-dashboard",
|
||||
"chart_ver": "0.1.10",
|
||||
"has_tests": true
|
||||
}
|
||||
]
|
||||
1
frontend/cypress/fixtures/releases.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
6
frontend/cypress/fixtures/repositories.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"name": "Komodorio",
|
||||
"url": "https://helm-charts.komodor.io"
|
||||
}
|
||||
]
|
||||
7
frontend/cypress/fixtures/status.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"CurVer": "0.0.0",
|
||||
"LatestVer": "v1.3.3",
|
||||
"Analytics": false,
|
||||
"CacheHitRatio": 0,
|
||||
"ClusterMode": false
|
||||
}
|
||||
37
frontend/cypress/support/commands.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
12
frontend/cypress/support/component-index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
13
frontend/cypress/support/component.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import "./commands";
|
||||
import { mount } from "cypress/react";
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
mount: typeof mount;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/no-namespace */
|
||||
Cypress.Commands.add("mount", mount);
|
||||
20
frontend/cypress/support/e2e.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/e2e.ts is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands";
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
119
frontend/eslint.config.js
Normal 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" },
|
||||
}
|
||||
);
|
||||
28
frontend/index.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Helm Dashboard</title>
|
||||
<script type="module" src="/analytics.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.13.1/styles/lightfair.min.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="portal"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
13777
frontend/package-lock.json
generated
Normal file
103
frontend/package.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"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:build": "storybook build",
|
||||
"lint": "npx eslint src/",
|
||||
"lint:fix": "npm run lint -- --fix --max-warnings=0",
|
||||
"prettier": "npx prettier src/ --check",
|
||||
"prettier:fix": "npm run prettier -- --write",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"cypress:component": "cypress run --component",
|
||||
"cypress:component:open": "cypress open --component"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dagrejs/dagre": "^2.0.4",
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@xyflow/react": "^12.10.1",
|
||||
"compare-versions": "^6.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"diff2html": "^3.4.52",
|
||||
"flowbite-react": "^0.12.17",
|
||||
"highlight.js": "^11.11.1",
|
||||
"html-react-parser": "^5.2.17",
|
||||
"luxon": "^3.7.2",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"react-error-boundary": "^6.1.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-modern-drawer": "^1.4.0",
|
||||
"react-router": "^7.13.0",
|
||||
"react-select": "^5.10.2",
|
||||
"swagger-ui-react": "^5.31.2",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.3",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@storybook/addon-docs": "^10.2.10",
|
||||
"@storybook/addon-links": "^10.2.10",
|
||||
"@storybook/mdx2-csf": "^1.1.0",
|
||||
"@storybook/react-vite": "^10.2.10",
|
||||
"@tailwindcss/vite": "^4.2.0",
|
||||
"@types/luxon": "^3.7.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.0",
|
||||
"@typescript-eslint/parser": "^8.56.0",
|
||||
"@vitejs/plugin-react": "^5.1.4",
|
||||
"autoprefixer": "^10.4.24",
|
||||
"babel-plugin-react-compiler": "^1.0.0",
|
||||
"cypress": "15.10.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import-x": "^4.16.1",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-storybook": "^10.2.10",
|
||||
"eslint-plugin-tsc": "^2.0.0",
|
||||
"flowbite": "^4.0.1",
|
||||
"globals": "^17.3.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.7",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"storybook": "^10.2.10",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.56.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-static-copy": "^3.2.0"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": "^10.2.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": "npm run lint:fix",
|
||||
"*.{js,jsx,ts,tsx,json,css,md,mdx}": "npm run prettier:fix"
|
||||
},
|
||||
"resolutions": {
|
||||
"dompurify": "^3.3.2"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
182
frontend/public/analytics.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const xhr = new XMLHttpRequest();
|
||||
const TRACK_EVENT_TYPE = "track";
|
||||
const IDENTIFY_EVENT_TYPE = "identify";
|
||||
const BASE_ANALYTIC_MSG = {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
cache: "no-cache",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"api-key": "komodor.analytics@admin.com"
|
||||
},
|
||||
redirect: "follow",
|
||||
referrerPolicy: "no-referrer"
|
||||
};
|
||||
xhr.onload = function() {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const responseTxt = xhr.responseText?.trim();
|
||||
if (!responseTxt) {
|
||||
console.warn("Analytics response is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
let status;
|
||||
try {
|
||||
status = JSON.parse(responseTxt);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse JSON: ", xhr.responseText, e);
|
||||
return;
|
||||
}
|
||||
|
||||
const version = status.CurVer;
|
||||
if (status.Analytics) {
|
||||
enableDD(version);
|
||||
enableHeap(version, status.ClusterMode);
|
||||
enableSegmentBackend(version, status.ClusterMode);
|
||||
} else {
|
||||
console.log("Analytics is disabled in this session");
|
||||
}
|
||||
};
|
||||
xhr.open("GET", "/status", true);
|
||||
xhr.send(null);
|
||||
|
||||
function enableDD(version) {
|
||||
(function(h, o, u, n, d) {
|
||||
h = h[d] = h[d] || {
|
||||
q: [],
|
||||
onReady: function(c) {
|
||||
h.q.push(c);
|
||||
}
|
||||
};
|
||||
d = o.createElement(u);
|
||||
d.async = true;
|
||||
d.src = n;
|
||||
n = o.getElementsByTagName(u)[0];
|
||||
n.parentNode.insertBefore(d, n);
|
||||
})(
|
||||
window,
|
||||
document,
|
||||
"script",
|
||||
"https://www.datadoghq-browser-agent.com/datadog-rum-v4.js",
|
||||
"DD_RUM"
|
||||
);
|
||||
DD_RUM.onReady(function() {
|
||||
DD_RUM.init({
|
||||
clientToken: "pub16d64cd1c00cf073ce85af914333bf72",
|
||||
applicationId: "e75439e5-e1b3-46ba-a9e9-a2e58579a2e2",
|
||||
site: "datadoghq.com",
|
||||
service: "helm-dashboard",
|
||||
version: version,
|
||||
trackInteractions: true,
|
||||
trackResources: true,
|
||||
trackLongTasks: true,
|
||||
defaultPrivacyLevel: "mask",
|
||||
sessionReplaySampleRate: 0
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function enableHeap(version, inCluster) {
|
||||
(window.heap = window.heap || []),
|
||||
(heap.load = function(e, t) {
|
||||
(window.heap.appid = e), (window.heap.config = t = t || {});
|
||||
let r = document.createElement("script");
|
||||
(r.type = "text/javascript"),
|
||||
(r.async = !0),
|
||||
(r.src = "https://cdn.heapanalytics.com/js/heap-" + e + ".js");
|
||||
let a = document.getElementsByTagName("script")[0];
|
||||
a.parentNode.insertBefore(r, a);
|
||||
for (
|
||||
let n = function(e) {
|
||||
return function() {
|
||||
heap.push([e].concat(Array.prototype.slice.call(arguments, 0)));
|
||||
};
|
||||
},
|
||||
p = [
|
||||
"addEventProperties",
|
||||
"addUserProperties",
|
||||
"clearEventProperties",
|
||||
"identify",
|
||||
"resetIdentity",
|
||||
"removeEventProperty",
|
||||
"setEventProperties",
|
||||
"track",
|
||||
"unsetEventProperty"
|
||||
],
|
||||
o = 0;
|
||||
o < p.length;
|
||||
o++
|
||||
)
|
||||
heap[p[o]] = n(p[o]);
|
||||
});
|
||||
heap.load("4249623943");
|
||||
window.heap.addEventProperties({
|
||||
version: version,
|
||||
installationMode: inCluster ? "cluster" : "local"
|
||||
});
|
||||
}
|
||||
|
||||
function sendStats(name, prop) {
|
||||
if (window.heap) {
|
||||
window.heap.track(name, prop);
|
||||
}
|
||||
}
|
||||
|
||||
function enableSegmentBackend(version, ClusterMode) {
|
||||
sendToSegmentThroughAPI(
|
||||
"helm dashboard loaded",
|
||||
{ version, installationMode: ClusterMode ? "cluster" : "local" },
|
||||
TRACK_EVENT_TYPE
|
||||
);
|
||||
}
|
||||
|
||||
function sendToSegmentThroughAPI(eventName, properties, segmentCallType) {
|
||||
const userId = getUserId();
|
||||
try {
|
||||
sendData(properties, segmentCallType, userId, eventName);
|
||||
} catch (e) {
|
||||
console.log("failed sending data to segment", e);
|
||||
}
|
||||
}
|
||||
|
||||
function sendData(data, eventType, userId, eventName) {
|
||||
const body = createBody(eventType, userId, data, eventName);
|
||||
return fetch(`https://api.komodor.com/analytics/segment/${eventType}`, {
|
||||
...BASE_ANALYTIC_MSG,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
}
|
||||
|
||||
function createBody(segmentCallType, userId, params, eventName) {
|
||||
const data = { userId: userId };
|
||||
if (segmentCallType === IDENTIFY_EVENT_TYPE) {
|
||||
data["traits"] = params;
|
||||
} else if (segmentCallType === TRACK_EVENT_TYPE) {
|
||||
if (!eventName) {
|
||||
throw new Error("no eventName parameter on segment track call");
|
||||
}
|
||||
data["properties"] = params;
|
||||
data["eventName"] = eventName;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const getUserId = (() => {
|
||||
let userId = null;
|
||||
return () => {
|
||||
if (!userId) {
|
||||
userId = crypto.randomUUID ? crypto.randomUUID() : uuid();
|
||||
}
|
||||
return userId;
|
||||
};
|
||||
})();
|
||||
|
||||
function uuid() {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
|
||||
let r = Math.random() * 16 | 0, v = c === "x" ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
92
frontend/public/logo.svg
Normal file
@@ -0,0 +1,92 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="179"
|
||||
height="164"
|
||||
viewBox="0 0 179 164"
|
||||
fill="none"
|
||||
version="1.1"
|
||||
id="svg20"
|
||||
sodipodi:docname="logo.svg"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs24" />
|
||||
<sodipodi:namedview
|
||||
id="namedview22"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
showgrid="false"
|
||||
inkscape:zoom="3.9268293"
|
||||
inkscape:cx="39.090062"
|
||||
inkscape:cy="109.24845"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2059"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg20" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:22.5327"
|
||||
id="rect967"
|
||||
width="113.34328"
|
||||
height="68.482964"
|
||||
x="32.284447"
|
||||
y="47.989918" />
|
||||
<path
|
||||
d="M106.129 93.4776C112.242 93.4776 117.336 86.7533 117.336 78.1951C117.336 69.637 112.242 62.9127 106.129 62.9127C100.016 62.9127 94.9216 69.637 94.9216 78.1951C94.9216 86.7533 100.016 93.4776 106.129 93.4776Z"
|
||||
fill="#1347FF"
|
||||
id="path2" />
|
||||
<path
|
||||
d="M84.1221 78.1951C84.1221 86.5495 79.0279 93.4776 72.915 93.4776C66.802 93.4776 61.7078 86.7533 61.7078 78.1951C61.7078 69.8408 66.802 62.9127 72.915 62.9127C79.0279 62.9127 84.1221 69.8408 84.1221 78.1951Z"
|
||||
fill="#1347FF"
|
||||
id="path4" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M22.5848 48.8529L34.607 37.2383H144.437L156.459 48.6492V114.873L144.437 126.488H34.607L22.5848 114.873V48.8529ZM42.3501 49.8717L37.6635 54.3546V109.575L42.3501 113.854H136.897L141.38 109.371V54.1508L136.897 49.668H42.3501V49.8717Z"
|
||||
fill="#1347FF"
|
||||
id="path6" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M175.817 28.68L167.87 36.8306L155.848 25.2159L163.794 17.0653C172.353 8.09963 184.579 19.918 175.817 28.68Z"
|
||||
fill="#1347FF"
|
||||
id="path8" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M97.7744 9.32228V20.5294H81.0656V9.32228C81.0656 -3.10743 97.9781 -3.10743 97.7744 9.32228Z"
|
||||
fill="#1347FF"
|
||||
id="path10" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M81.2693 154.2V142.993H97.9781V154.2C97.9781 166.629 81.0655 166.629 81.2693 154.2Z"
|
||||
fill="#1347FF"
|
||||
id="path12" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M15.2493 17.0653L23.1961 25.2159L11.1739 36.8306L3.22708 28.68C-5.53484 19.918 6.6911 8.09963 15.2493 17.0653Z"
|
||||
fill="#1347FF"
|
||||
id="path14" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M3.0233 134.842L10.9702 126.691L22.9923 138.306L15.0455 146.457C6.48732 155.422 -5.73862 143.604 3.0233 134.842Z"
|
||||
fill="#1347FF"
|
||||
id="path16" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M163.591 146.457L155.644 138.306L167.666 126.691L175.613 134.842C184.375 143.604 172.149 155.422 163.591 146.457Z"
|
||||
fill="#1347FF"
|
||||
id="path18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
681
frontend/public/openapi.json
Normal file
@@ -0,0 +1,681 @@
|
||||
{
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "Helm Dashboard API",
|
||||
"version": ""
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "Releases"
|
||||
},
|
||||
{
|
||||
"name": "Repositories"
|
||||
},
|
||||
{
|
||||
"name": "K8s"
|
||||
},
|
||||
{
|
||||
"name": "Miscellaneous"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/api/helm/releases": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get list of installed releases",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of installed releases"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace, use '[empty]' if you want to use k8s context default"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Install new release",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"chart": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"values": {
|
||||
"type": "string",
|
||||
"description": "Text of values.yaml to use"
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "In case preview=true, the preview diff is generated",
|
||||
"content": {
|
||||
"text/plain": {}
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Upgrade/reconfigure existing release",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"multipart/form-data": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"chart": {
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"values": {
|
||||
"type": "string",
|
||||
"description": "Text of values.yaml to use"
|
||||
},
|
||||
"preview": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "In case preview=true, the preview diff is generated",
|
||||
"content": {
|
||||
"text/plain": {}
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "In case preview=false, the actial install is performed and resulting release object is returned",
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/history": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get revision history for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "List of release revisions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/manifest": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"in": "query",
|
||||
"description": "Revision to get data from"
|
||||
},
|
||||
{
|
||||
"name": "revisionDiff",
|
||||
"in": "query",
|
||||
"description": "Revision to diff against"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get manifest for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Manifest text, or diff if revisionDiff is specified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/values": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"in": "query",
|
||||
"description": "Revision to get data from"
|
||||
},
|
||||
{
|
||||
"name": "revisionDiff",
|
||||
"in": "query",
|
||||
"description": "Revision to diff against"
|
||||
},
|
||||
{
|
||||
"name": "userDefined",
|
||||
"in": "query",
|
||||
"description": "If set, only user-defined values will be listed"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get values for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Values YAML text, or diff if revisionDiff is specified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/notes": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "revision",
|
||||
"in": "query",
|
||||
"description": "Revision to get data from"
|
||||
},
|
||||
{
|
||||
"name": "revisionDiff",
|
||||
"in": "query",
|
||||
"description": "Revision to diff against"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Get textual notes for release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Notes text, or diff if revisionDiff is specified"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/resources": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
},
|
||||
{
|
||||
"name": "health",
|
||||
"in": "query",
|
||||
"description": "Flag to query k8s health status of resources"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "List of installed k8s resources for this release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Structured list of resources",
|
||||
"content": {
|
||||
"application/json": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/rollback": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Rollback the release to a previous revision",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Rolled back successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/releases/{ns}/{name}/test": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "ns",
|
||||
"in": "path",
|
||||
"description": "Name of kubernetes namespace"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"description": "Name of Helm release"
|
||||
}
|
||||
],
|
||||
"post": {
|
||||
"tags": [
|
||||
"Releases"
|
||||
],
|
||||
"description": "Run the tests on a release",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Logs of a test run"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Get list of Helm repositories",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of Helm repositories"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Adds new repository",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/x-www-form-urlencoded": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"url"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Empty response in case repository were added"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/{repo}": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"description": "Name of Helm repository"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Get list of charts in repository",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of charts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Update repository from remote",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Empty response"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"description": "Remove repository",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Empty response"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/latestver": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of Helm chart to search for",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"description": "Find the latest available version of specified chart through all the repositories",
|
||||
"get": {
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The object with latest available version is returned"
|
||||
},
|
||||
"204": {
|
||||
"description": "In case no matching repository found, the response is empty with status 204"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/versions": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of Helm chart to search for",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"description": "Get the list of versions for specified chart across the repositories",
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list if chart versions is returned"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/helm/repositories/values": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "chart",
|
||||
"in": "query",
|
||||
"description": "Name of Helm chart to search for, in format of <repository>/<chart-name>",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "version",
|
||||
"in": "query",
|
||||
"description": "Version of Helm chart to get values from",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"description": "Get the original values.yaml file for the chart",
|
||||
"tags": [
|
||||
"Repositories"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The content of values.yaml"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/contexts": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"description": "Get list of kubectl contexts configured locally",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of contexts"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/{kind}/get": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "kind",
|
||||
"in": "path",
|
||||
"description": "Kind of kubernetes resource"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of kubernetes resource",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "query",
|
||||
"description": "Namespace of kubernetes resource",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns resources information"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/{kind}/list": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "kind",
|
||||
"in": "path",
|
||||
"description": "Kind of kubernetes resource",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"namespaces"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns list of resources"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/k8s/{kind}/describe": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "kind",
|
||||
"in": "path",
|
||||
"description": "Kind of kubernetes resource"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"in": "query",
|
||||
"description": "Name of kubernetes resource",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "namespace",
|
||||
"in": "query",
|
||||
"description": "Namespace of kubernetes resource",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"K8s"
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"text/plain": {}
|
||||
},
|
||||
"description": "Returns describe text"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Miscellaneous"
|
||||
],
|
||||
"description": "Shuts down the Helm Dashboard application",
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Shutdown command has been accepted"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/status": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Miscellaneous"
|
||||
],
|
||||
"description": "Gets application status",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Returns JSON with some options",
|
||||
"headers": {
|
||||
"X-Application-Name": {
|
||||
"description": "A string to self-identify the application"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
frontend/src/API/apiService.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { type QueryFunctionContext } from "@tanstack/react-query";
|
||||
|
||||
import type {
|
||||
Chart,
|
||||
ChartVersion,
|
||||
Release,
|
||||
ReleaseHealthStatus,
|
||||
ReleaseRevision,
|
||||
} from "../data/types";
|
||||
|
||||
interface ClustersResponse {
|
||||
AuthInfo: string;
|
||||
Cluster: string;
|
||||
IsCurrent: boolean;
|
||||
Name: string;
|
||||
Namespace: string;
|
||||
}
|
||||
class ApiService {
|
||||
currentCluster = "";
|
||||
constructor(protected readonly isMockMode: boolean = false) {}
|
||||
|
||||
setCluster = (cluster: string) => {
|
||||
this.currentCluster = cluster;
|
||||
};
|
||||
|
||||
public async fetchWithDefaults<T>(
|
||||
url: string,
|
||||
options?: RequestInit
|
||||
): Promise<T | string> {
|
||||
let response;
|
||||
|
||||
if (this.currentCluster) {
|
||||
const headers = new Headers(options?.headers);
|
||||
if (!headers.has("X-Kubecontext")) {
|
||||
headers.set("X-Kubecontext", this.currentCluster);
|
||||
}
|
||||
response = await fetch(url, { ...options, headers });
|
||||
} else {
|
||||
response = await fetch(url, options);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("Content-Type") || "";
|
||||
if (!contentType) {
|
||||
return {} as unknown as T;
|
||||
} else if (contentType.includes("text/plain")) {
|
||||
return await response.text();
|
||||
} else {
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchWithSafeDefaults<T>({
|
||||
url,
|
||||
options,
|
||||
fallback,
|
||||
}: {
|
||||
url: string;
|
||||
options?: RequestInit;
|
||||
fallback: T;
|
||||
}): Promise<T> {
|
||||
const data = await this.fetchWithDefaults<T>(url, options);
|
||||
if (!data) {
|
||||
console.error(url, " response is empty");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
if (typeof data === "string") {
|
||||
console.error(url, " response is string");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
getToolVersion = async () => {
|
||||
return await this.fetchWithDefaults("/status");
|
||||
};
|
||||
|
||||
getRepositoryLatestVersion = async (repositoryName: string) => {
|
||||
return await this.fetchWithDefaults(
|
||||
`/api/helm/repositories/latestver?name=${repositoryName}`
|
||||
);
|
||||
};
|
||||
|
||||
getInstalledReleases = async () => {
|
||||
return await this.fetchWithDefaults("/api/helm/releases");
|
||||
};
|
||||
|
||||
getClusters = async (): Promise<ClustersResponse[]> => {
|
||||
return await this.fetchWithSafeDefaults<ClustersResponse[]>({
|
||||
url: "/api/k8s/contexts",
|
||||
fallback: [],
|
||||
});
|
||||
};
|
||||
|
||||
getNamespaces = async () => {
|
||||
return await this.fetchWithDefaults("/api/k8s/namespaces/list");
|
||||
};
|
||||
|
||||
getRepositories = async () => {
|
||||
return await this.fetchWithDefaults("/api/helm/repositories");
|
||||
};
|
||||
|
||||
getRepositoryCharts = async ({
|
||||
queryKey,
|
||||
}: {
|
||||
queryKey: readonly unknown[];
|
||||
}): Promise<Chart[]> => {
|
||||
const [, repository] = queryKey;
|
||||
if (!repository || typeof repository !== "string") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const url = `/api/helm/repositories/${repository}`;
|
||||
return await this.fetchWithSafeDefaults<Chart[]>({ url, fallback: [] });
|
||||
};
|
||||
|
||||
getChartVersions = async ({
|
||||
queryKey,
|
||||
}: QueryFunctionContext<ChartVersion[], Chart>) => {
|
||||
const [, chart] = queryKey;
|
||||
|
||||
return await this.fetchWithDefaults(
|
||||
`/api/helm/repositories/versions?name=${chart.name}`
|
||||
);
|
||||
};
|
||||
|
||||
getResourceStatus = async ({
|
||||
release,
|
||||
}: {
|
||||
release: Release;
|
||||
}): Promise<ReleaseHealthStatus[]> => {
|
||||
if (!release) return [];
|
||||
|
||||
return await this.fetchWithSafeDefaults<ReleaseHealthStatus[]>({
|
||||
url: `/api/helm/releases/${release.namespace}/${release.name}/resources?health=true`,
|
||||
fallback: [],
|
||||
});
|
||||
};
|
||||
|
||||
getReleasesHistory = async ({
|
||||
queryKey,
|
||||
}: {
|
||||
queryKey: readonly [string, Record<string, string | undefined>];
|
||||
}): Promise<ReleaseRevision[]> => {
|
||||
const [, params] = queryKey;
|
||||
|
||||
if (!params.namespace || !params.chart) return [];
|
||||
|
||||
return await this.fetchWithSafeDefaults<ReleaseRevision[]>({
|
||||
url: `/api/helm/releases/${params.namespace}/${params.chart}/history`,
|
||||
fallback: [],
|
||||
});
|
||||
};
|
||||
|
||||
getValues = async ({
|
||||
queryKey,
|
||||
}: {
|
||||
queryKey: [
|
||||
string,
|
||||
{ namespace: string; chart: { name: string }; version: number },
|
||||
];
|
||||
}) => {
|
||||
const [, params] = queryKey;
|
||||
const { namespace, chart, version } = params;
|
||||
|
||||
if (!namespace || !chart || !chart.name || version === undefined)
|
||||
return Promise.reject(new Error("missing parameters"));
|
||||
|
||||
const url = `/api/helm/repositories/values?chart=${namespace}/${chart.name}&version=${version}`;
|
||||
return await this.fetchWithDefaults(url);
|
||||
};
|
||||
}
|
||||
|
||||
const apiService = new ApiService();
|
||||
|
||||
export default apiService;
|
||||
99
frontend/src/API/interfaces.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
export interface HelmRepository {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ChartVersion {
|
||||
name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface K8sContext {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface K8sResource {
|
||||
kind: string;
|
||||
name: string;
|
||||
namespace: string;
|
||||
}
|
||||
|
||||
export interface Scanner {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ScanResult {
|
||||
scannerType: string;
|
||||
result: string;
|
||||
}
|
||||
|
||||
export interface ScannersList {
|
||||
scanners: Scanner[];
|
||||
}
|
||||
|
||||
export interface ScanResults {
|
||||
[scannerType: string]: ScanResult;
|
||||
}
|
||||
|
||||
export interface ApplicationStatus {
|
||||
Analytics: boolean;
|
||||
CacheHitRatio: number;
|
||||
ClusterMode: boolean;
|
||||
CurVer: string;
|
||||
LatestVer: string;
|
||||
NoHealth: boolean;
|
||||
NoLatest: boolean;
|
||||
}
|
||||
|
||||
export interface KubectlContexts {
|
||||
contexts: string[];
|
||||
}
|
||||
|
||||
export interface K8sResourceList {
|
||||
items: K8sResource[];
|
||||
}
|
||||
|
||||
export type HelmRepositories = Repository[];
|
||||
|
||||
export interface ChartList {
|
||||
charts: Chart[];
|
||||
}
|
||||
|
||||
export interface LatestChartVersion {
|
||||
name: string;
|
||||
version: string;
|
||||
app_version: string;
|
||||
description: string;
|
||||
installed_namespace: string;
|
||||
installed_name: string;
|
||||
repository: string;
|
||||
urls: string[];
|
||||
isSuggestedRepo: boolean;
|
||||
}
|
||||
|
||||
export interface ChartVersions {
|
||||
versions: string[];
|
||||
}
|
||||
|
||||
export interface ValuesYamlText {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface Chart {
|
||||
name: string;
|
||||
repo: string;
|
||||
version: string;
|
||||
appVersion: string;
|
||||
description: string;
|
||||
created: string;
|
||||
digest: string;
|
||||
urls: string[];
|
||||
icon: string;
|
||||
}
|
||||
82
frontend/src/API/k8s.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { type UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import apiService from "./apiService";
|
||||
import type {
|
||||
K8sResource,
|
||||
K8sResourceList,
|
||||
KubectlContexts,
|
||||
} from "./interfaces";
|
||||
|
||||
// Get list of kubectl contexts configured locally
|
||||
// @ts-expect-error unused
|
||||
function useGetKubectlContexts(options?: UseQueryOptions<KubectlContexts>) {
|
||||
return useQuery<KubectlContexts>({
|
||||
queryKey: ["k8s", "contexts"],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<KubectlContexts>({
|
||||
url: "/api/k8s/contexts",
|
||||
fallback: { contexts: [] },
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Get resources information
|
||||
// @ts-expect-error unused
|
||||
function useGetK8sResource(
|
||||
kind: string,
|
||||
name: string,
|
||||
namespace: string,
|
||||
options?: UseQueryOptions<K8sResource>
|
||||
) {
|
||||
return useQuery<K8sResource>({
|
||||
queryKey: ["k8s", kind, "get", name, namespace],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<K8sResource>({
|
||||
url: `/api/k8s/${kind}/get?name=${name}&namespace=${namespace}`,
|
||||
fallback: { kind: "", name: "", namespace: "" },
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Get list of resources
|
||||
// @ts-expect-error unused
|
||||
function useGetK8sResourceList(
|
||||
kind: string,
|
||||
options?: UseQueryOptions<K8sResourceList>
|
||||
) {
|
||||
return useQuery<K8sResourceList>({
|
||||
queryKey: ["k8s", kind, "list"],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<K8sResourceList>({
|
||||
url: `/api/k8s/${kind}/list`,
|
||||
fallback: { items: [] },
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Get describe text for kubernetes resource
|
||||
// @ts-expect-error unused
|
||||
function useGetK8sResourceDescribe(
|
||||
kind: string,
|
||||
name: string,
|
||||
namespace: string,
|
||||
options?: UseQueryOptions<string>
|
||||
) {
|
||||
return useQuery<string>({
|
||||
queryKey: ["k8s", kind, "describe", name, namespace],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithDefaults<string>(
|
||||
`/api/k8s/${kind}/describe?name=${name}&namespace=${namespace}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: "text/plain",
|
||||
},
|
||||
}
|
||||
),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
38
frontend/src/API/other.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
type UseMutationOptions,
|
||||
type UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import apiService from "./apiService";
|
||||
import type { ApplicationStatus } from "./interfaces";
|
||||
|
||||
// Shuts down the Helm Dashboard application
|
||||
export function useShutdownHelmDashboard(
|
||||
options?: UseMutationOptions<string, Error>
|
||||
) {
|
||||
return useMutation<string, Error>({
|
||||
mutationFn: () =>
|
||||
apiService.fetchWithDefaults("/", {
|
||||
method: "DELETE",
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Gets application status
|
||||
export function useGetApplicationStatus(
|
||||
options?: UseQueryOptions<ApplicationStatus | null>
|
||||
) {
|
||||
return useQuery<ApplicationStatus | null>({
|
||||
queryKey: ["status"],
|
||||
queryFn: async () =>
|
||||
await apiService.fetchWithSafeDefaults<ApplicationStatus | null>({
|
||||
url: "/status",
|
||||
fallback: null,
|
||||
}),
|
||||
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
436
frontend/src/API/releases.ts
Normal file
@@ -0,0 +1,436 @@
|
||||
import {
|
||||
useMutation,
|
||||
type UseMutationOptions,
|
||||
useQuery,
|
||||
type UseQueryOptions,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import type { ChartVersion, Release } from "../data/types";
|
||||
import { isNewerVersion } from "../utils";
|
||||
|
||||
import apiService from "./apiService";
|
||||
import type { LatestChartVersion } from "./interfaces";
|
||||
import { getVersionManifestFormData } from "./shared";
|
||||
|
||||
export const HD_RESOURCE_CONDITION_TYPE = "hdHealth"; // it's our custom condition type, only one exists
|
||||
|
||||
export function useGetInstalledReleases(context: string) {
|
||||
return useQuery<Release[]>({
|
||||
queryKey: ["installedReleases", context],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<Release[]>({
|
||||
url: "/api/helm/releases",
|
||||
fallback: [],
|
||||
}),
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export interface ReleaseManifest {
|
||||
apiVersion: string;
|
||||
kind: string;
|
||||
metadata: {
|
||||
name: string;
|
||||
namespace: string;
|
||||
labels: Record<string, string>;
|
||||
};
|
||||
spec: {
|
||||
replicas: number;
|
||||
selector: Record<string, string>;
|
||||
template: {
|
||||
metadata: {
|
||||
labels: Record<string, string>;
|
||||
};
|
||||
spec: {
|
||||
containers: {
|
||||
name: string;
|
||||
image: string;
|
||||
ports: {
|
||||
containerPort: number;
|
||||
}[];
|
||||
env: {
|
||||
name: string;
|
||||
value: string;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function useGetReleaseManifest({
|
||||
namespace,
|
||||
chartName,
|
||||
options,
|
||||
}: {
|
||||
namespace: string;
|
||||
chartName: string;
|
||||
options?: UseQueryOptions<ReleaseManifest[]>;
|
||||
}) {
|
||||
return useQuery<ReleaseManifest[]>({
|
||||
queryKey: ["manifest", namespace, chartName],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<ReleaseManifest[]>({
|
||||
url: `/api/helm/releases/${namespace}/${chartName}/manifests`,
|
||||
fallback: [],
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export interface ContainerImage {
|
||||
resource: string;
|
||||
kind: string;
|
||||
container: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export function useGetImages(ns: string, name: string) {
|
||||
return useQuery<ContainerImage[]>({
|
||||
queryKey: ["images", ns, name],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<ContainerImage[]>({
|
||||
url: `/api/helm/releases/${ns}/${name}/images`,
|
||||
fallback: [],
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export interface RelationNode {
|
||||
id: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
inRelease: boolean;
|
||||
}
|
||||
|
||||
export interface RelationEdge {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface RelationGraph {
|
||||
nodes: RelationNode[];
|
||||
edges: RelationEdge[];
|
||||
}
|
||||
|
||||
export function useGetRelations(ns: string, name: string) {
|
||||
return useQuery<RelationGraph>({
|
||||
queryKey: ["relations", ns, name],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<RelationGraph>({
|
||||
url: `/api/helm/releases/${ns}/${name}/relations`,
|
||||
fallback: { nodes: [], edges: [] },
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// List of installed k8s resources for this release
|
||||
export function useGetResources(ns: string, name: string, enabled?: boolean) {
|
||||
return useQuery<StructuredResources[]>({
|
||||
queryKey: ["resources", ns, name],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<StructuredResources[]>({
|
||||
url: `/api/helm/releases/${ns}/${name}/resources?health=true`,
|
||||
fallback: [],
|
||||
}),
|
||||
select: (data) =>
|
||||
data
|
||||
?.map((resource) => ({
|
||||
...resource,
|
||||
status: {
|
||||
...resource.status,
|
||||
conditions: resource.status.conditions.filter(
|
||||
(c) => c.type === HD_RESOURCE_CONDITION_TYPE
|
||||
),
|
||||
},
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const interestingResources = [
|
||||
"STATEFULSET",
|
||||
"DEAMONSET",
|
||||
"DEPLOYMENT",
|
||||
];
|
||||
return (
|
||||
interestingResources.indexOf(b.kind.toUpperCase()) -
|
||||
interestingResources.indexOf(a.kind.toUpperCase())
|
||||
);
|
||||
}),
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetResourceDescription(
|
||||
type: string,
|
||||
ns: string,
|
||||
name: string,
|
||||
apiVersion?: string,
|
||||
options?: UseQueryOptions<string>
|
||||
) {
|
||||
const params = new URLSearchParams({ name, namespace: ns });
|
||||
if (apiVersion) {
|
||||
params.set("apiVersion", apiVersion);
|
||||
}
|
||||
return useQuery<string>({
|
||||
queryKey: ["describe", type, ns, name, apiVersion],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithDefaults<string>(
|
||||
`/api/k8s/${type}/describe?${params.toString()}`,
|
||||
{
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
}
|
||||
),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
export function useGetLatestVersion(
|
||||
chartName: string,
|
||||
options?: UseQueryOptions<ChartVersion[]>
|
||||
) {
|
||||
return useQuery<ChartVersion[]>({
|
||||
queryKey: ["latestver", chartName],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<ChartVersion[]>({
|
||||
url: `/api/helm/repositories/latestver?name=${chartName}`,
|
||||
fallback: [],
|
||||
}),
|
||||
gcTime: 0,
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
export function useGetVersions(
|
||||
chartName: string,
|
||||
options?: UseQueryOptions<LatestChartVersion[]>
|
||||
) {
|
||||
return useQuery<LatestChartVersion[]>({
|
||||
queryKey: ["versions", chartName],
|
||||
queryFn: async () => {
|
||||
const url = `/api/helm/repositories/versions?name=${chartName}`;
|
||||
return await apiService.fetchWithSafeDefaults<LatestChartVersion[]>({
|
||||
url,
|
||||
fallback: [],
|
||||
});
|
||||
},
|
||||
select: (data) =>
|
||||
data?.sort((a, b) => (isNewerVersion(a.version, b.version) ? 1 : -1)),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetReleaseInfoByType(
|
||||
params: ReleaseInfoParams,
|
||||
additionalParams = "",
|
||||
options?: UseQueryOptions<string>
|
||||
) {
|
||||
const { chart, namespace, tab, revision } = params;
|
||||
return useQuery<string>({
|
||||
queryKey: [tab, namespace, chart, revision, additionalParams],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithDefaults<string>(
|
||||
`/api/helm/releases/${namespace}/${chart}/${tab}?revision=${revision}${additionalParams}`,
|
||||
{
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
}
|
||||
),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetDiff(
|
||||
formData: FormData,
|
||||
options?: UseQueryOptions<string>
|
||||
) {
|
||||
return useQuery<string>({
|
||||
queryKey: ["diff", formData],
|
||||
queryFn: () => {
|
||||
return apiService.fetchWithDefaults<string>("/diff", {
|
||||
body: formData,
|
||||
|
||||
method: "POST",
|
||||
});
|
||||
},
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Rollback the release to a previous revision
|
||||
export function useRollbackRelease(
|
||||
options?: UseMutationOptions<
|
||||
string,
|
||||
Error,
|
||||
{ ns: string; name: string; revision: number }
|
||||
>
|
||||
) {
|
||||
return useMutation<
|
||||
string,
|
||||
Error,
|
||||
{ ns: string; name: string; revision: number }
|
||||
>({
|
||||
mutationFn: ({ ns, name, revision }) => {
|
||||
const formData = new FormData();
|
||||
formData.append("revision", revision.toString());
|
||||
|
||||
return apiService.fetchWithDefaults<string>(
|
||||
`/api/helm/releases/${ns}/${name}/rollback`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
},
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Run the tests on a release
|
||||
export function useTestRelease(
|
||||
options?: UseMutationOptions<string, Error, { ns: string; name: string }>
|
||||
) {
|
||||
return useMutation<string, Error, { ns: string; name: string }>({
|
||||
mutationFn: ({ ns, name }) => {
|
||||
return apiService.fetchWithDefaults<string>(
|
||||
`/api/helm/releases/${ns}/${name}/test`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
},
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function useChartReleaseValues({
|
||||
namespace = "default",
|
||||
release,
|
||||
userDefinedValue,
|
||||
revision,
|
||||
options,
|
||||
version,
|
||||
}: {
|
||||
namespace?: string;
|
||||
release: string;
|
||||
userDefinedValue?: string;
|
||||
revision?: number;
|
||||
version?: string;
|
||||
options?: UseQueryOptions<string>;
|
||||
}) {
|
||||
return useQuery<string>({
|
||||
queryKey: ["values", namespace, release, userDefinedValue, version],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithDefaults(
|
||||
`/api/helm/releases/${namespace}/${release}/values?${"userDefined=true"}${
|
||||
revision ? `&revision=${revision}` : ""
|
||||
}`,
|
||||
{
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
}
|
||||
),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export type VersionData = {
|
||||
version: string;
|
||||
repository?: string;
|
||||
urls: string[];
|
||||
};
|
||||
|
||||
export const useVersionData = ({
|
||||
version,
|
||||
userValues,
|
||||
chartAddress,
|
||||
releaseValues,
|
||||
namespace,
|
||||
releaseName,
|
||||
isInstallRepoChart = false,
|
||||
enabled = true,
|
||||
}: {
|
||||
version: string;
|
||||
userValues: string;
|
||||
chartAddress: string;
|
||||
releaseValues: string;
|
||||
namespace: string;
|
||||
releaseName: string;
|
||||
isInstallRepoChart?: boolean;
|
||||
enabled?: boolean;
|
||||
}) => {
|
||||
return useQuery<{ [key: string]: string }>({
|
||||
queryKey: [
|
||||
version,
|
||||
userValues,
|
||||
chartAddress,
|
||||
releaseValues,
|
||||
namespace,
|
||||
releaseName,
|
||||
isInstallRepoChart,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const formData = getVersionManifestFormData({
|
||||
version,
|
||||
userValues,
|
||||
chart: chartAddress,
|
||||
releaseValues,
|
||||
releaseName,
|
||||
});
|
||||
|
||||
const url = isInstallRepoChart
|
||||
? `/api/helm/releases/${namespace || "default"}`
|
||||
: `/api/helm/releases/${
|
||||
namespace ? namespace : "[empty]"
|
||||
}${`/${releaseName}`}`;
|
||||
|
||||
return await apiService.fetchWithSafeDefaults<{
|
||||
[key: string]: string;
|
||||
}>({
|
||||
url,
|
||||
options: {
|
||||
method: "post",
|
||||
body: formData,
|
||||
},
|
||||
fallback: {},
|
||||
});
|
||||
},
|
||||
|
||||
enabled,
|
||||
});
|
||||
};
|
||||
|
||||
// Request objects
|
||||
interface ReleaseInfoParams {
|
||||
chart?: string;
|
||||
tab: string;
|
||||
namespace?: string;
|
||||
revision?: string;
|
||||
}
|
||||
|
||||
export interface StructuredResources {
|
||||
kind: string;
|
||||
apiVersion: string;
|
||||
metadata: Metadata;
|
||||
spec: Spec;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface Metadata {
|
||||
name: string;
|
||||
namespace: string;
|
||||
creationTimestamp: Date;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface Spec {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
export interface Condition {
|
||||
type: string;
|
||||
status: string;
|
||||
lastProbeTime: Date;
|
||||
lastTransitionTime: Date;
|
||||
reason: string;
|
||||
message: string;
|
||||
}
|
||||
81
frontend/src/API/repositories.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
type UseMutationOptions,
|
||||
type UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import apiService from "./apiService";
|
||||
import type { HelmRepositories } from "./interfaces";
|
||||
|
||||
// Get list of Helm repositories
|
||||
export function useGetRepositories(
|
||||
options?: UseQueryOptions<HelmRepositories>
|
||||
) {
|
||||
return useQuery<HelmRepositories>({
|
||||
queryKey: ["helm", "repositories"],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<HelmRepositories>({
|
||||
url: "/api/helm/repositories",
|
||||
fallback: [],
|
||||
}),
|
||||
select: (data) => data?.sort((a, b) => a?.name?.localeCompare(b?.name)),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Update repository from remote
|
||||
export function useUpdateRepo(
|
||||
repo: string,
|
||||
options?: UseMutationOptions<string, Error>
|
||||
) {
|
||||
return useMutation<string, Error>({
|
||||
mutationFn: () => {
|
||||
return apiService.fetchWithDefaults<string>(
|
||||
`/api/helm/repositories/${repo}`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
},
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Remove repository
|
||||
export function useDeleteRepo(
|
||||
repo: string,
|
||||
options?: UseMutationOptions<string, Error>
|
||||
) {
|
||||
return useMutation<string, Error>({
|
||||
mutationFn: () => {
|
||||
return apiService.fetchWithDefaults<string>(
|
||||
`/api/helm/repositories/${repo}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
},
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
export function useChartRepoValues({
|
||||
version,
|
||||
chart,
|
||||
}: {
|
||||
version: string;
|
||||
chart: string;
|
||||
}) {
|
||||
return useQuery<string>({
|
||||
queryKey: ["helm", "repositories", "values", chart, version],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithDefaults<string>(
|
||||
`/api/helm/repositories/values?chart=${chart}&version=${version}`,
|
||||
{
|
||||
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
||||
}
|
||||
),
|
||||
enabled: Boolean(version) && Boolean(chart),
|
||||
});
|
||||
}
|
||||
71
frontend/src/API/scanners.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/** DO NOT DELETE THESE FUNCTIONS - we left this until we support scan ops again */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
type UseMutationOptions,
|
||||
type UseQueryOptions,
|
||||
useMutation,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
|
||||
import apiService from "./apiService";
|
||||
import {
|
||||
type ScanResult,
|
||||
type ScanResults,
|
||||
type ScannersList,
|
||||
} from "./interfaces";
|
||||
|
||||
// Get list of discovered scanners
|
||||
// @ts-expect-error unused
|
||||
function useGetDiscoveredScanners(options?: UseQueryOptions<ScannersList>) {
|
||||
return useQuery<ScannersList>({
|
||||
queryKey: ["scanners"],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<ScannersList>({
|
||||
url: "/api/scanners",
|
||||
fallback: { scanners: [] },
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Scan manifests using all applicable scanners
|
||||
// @ts-expect-error unused
|
||||
function useScanManifests(
|
||||
manifest: string,
|
||||
options?: UseMutationOptions<ScanResults, Error, string>
|
||||
) {
|
||||
const formData = new FormData();
|
||||
formData.append("manifest", manifest);
|
||||
return useMutation<ScanResults, Error, string>({
|
||||
mutationFn: () =>
|
||||
apiService.fetchWithSafeDefaults<ScanResults>({
|
||||
url: "/api/scanners/manifests",
|
||||
options: {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
},
|
||||
fallback: {},
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
|
||||
// Scan specified k8s resource in cluster
|
||||
// @ts-expect-error unused
|
||||
function useScanK8sResource(
|
||||
kind: string,
|
||||
namespace: string,
|
||||
name: string,
|
||||
options?: UseQueryOptions<ScanResults>
|
||||
) {
|
||||
return useQuery<ScanResults>({
|
||||
queryKey: ["scanners", "resource", kind, namespace, name],
|
||||
queryFn: () =>
|
||||
apiService.fetchWithSafeDefaults<ScanResults>({
|
||||
url: `/api/scanners/resource/${kind}?namespace=${namespace}&name=${name}`,
|
||||
fallback: {},
|
||||
}),
|
||||
...(options ?? {}),
|
||||
});
|
||||
}
|
||||
69
frontend/src/API/shared.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import apiService from "./apiService";
|
||||
|
||||
export const getVersionManifestFormData = ({
|
||||
version,
|
||||
userValues,
|
||||
chart,
|
||||
releaseValues,
|
||||
releaseName,
|
||||
}: {
|
||||
version: string;
|
||||
userValues?: string;
|
||||
chart: string;
|
||||
releaseValues?: string;
|
||||
releaseName?: string;
|
||||
}) => {
|
||||
const formData = new FormData();
|
||||
// preview needs to come first, for some reason it has a meaning at the backend
|
||||
formData.append("preview", "true");
|
||||
formData.append("chart", chart);
|
||||
formData.append("version", version);
|
||||
formData.append(
|
||||
"values",
|
||||
userValues ? userValues : releaseValues ? releaseValues : ""
|
||||
);
|
||||
if (releaseName) {
|
||||
formData.append("name", releaseName);
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
export const useDiffData = ({
|
||||
selectedRepo,
|
||||
versionsError,
|
||||
currentVerManifest,
|
||||
selectedVerData,
|
||||
chart,
|
||||
}: {
|
||||
selectedRepo: string;
|
||||
versionsError: string;
|
||||
currentVerManifest: string;
|
||||
selectedVerData: { [key: string]: string };
|
||||
chart: string;
|
||||
}) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
selectedRepo,
|
||||
versionsError,
|
||||
chart,
|
||||
currentVerManifest,
|
||||
selectedVerData,
|
||||
],
|
||||
queryFn: async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("a", currentVerManifest);
|
||||
formData.append("b", selectedVerData.manifest);
|
||||
|
||||
const diff = await apiService.fetchWithDefaults("/diff", {
|
||||
method: "post",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
return diff;
|
||||
},
|
||||
enabled: Boolean(selectedVerData),
|
||||
});
|
||||
};
|
||||
96
frontend/src/App.css
Normal file
@@ -0,0 +1,96 @@
|
||||
.app-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 5px;
|
||||
padding: 10px;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
flex: 0.6;
|
||||
}
|
||||
.header-items {
|
||||
display: flex;
|
||||
flex: 0.8;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0.2;
|
||||
justify-content: space-around;
|
||||
}
|
||||
.redirect {
|
||||
display: flex;
|
||||
flex: 0.8;
|
||||
}
|
||||
.redirect > img {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.signout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.signout-btn:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.signout-btn > span {
|
||||
font-weight: bolder;
|
||||
font-size: x-large;
|
||||
color: gray;
|
||||
}
|
||||
.card {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
}
|
||||
.card-left {
|
||||
flex: 0.2;
|
||||
margin-top: 5px;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
.card-left > h2,
|
||||
form {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.btn {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.card-right {
|
||||
flex: 0.8;
|
||||
margin-top: 5px;
|
||||
margin-left: 4px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
.card-right-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.card-right-header-right-btn {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.title {
|
||||
flex: 0.2;
|
||||
}
|
||||
.description {
|
||||
flex: 0.6;
|
||||
}
|
||||
.version {
|
||||
flex: 0.2;
|
||||
}
|
||||
.charts {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.charts > h3 {
|
||||
flex: 0.2;
|
||||
}
|
||||
91
frontend/src/App.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
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 Installed from "./pages/Installed";
|
||||
import RepositoryPage from "./pages/Repository";
|
||||
import Revision from "./pages/Revision";
|
||||
|
||||
const DocsPage = lazy(() => import("./pages/DocsPage"));
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const PageLayout = () => {
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
<Header />
|
||||
<div className="flex-1 bg-body-background bg-[url('./assets/body-background.svg')] bg-no-repeat">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SyncContext: FC = () => {
|
||||
const { context } = useParams();
|
||||
if (context) {
|
||||
apiService.setCluster(decodeURIComponent(context));
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [shouldShowErrorModal, setShowErrorModal] = useState<
|
||||
ErrorAlert | undefined
|
||||
>(undefined);
|
||||
const value = { shouldShowErrorModal, setShowErrorModal };
|
||||
|
||||
return (
|
||||
<AppContextProvider>
|
||||
<ErrorModalContext.Provider value={value}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<HashRouter>
|
||||
<Routes>
|
||||
<Route path="docs/*" element={<DocsPage />} />
|
||||
<Route path="*" element={<PageLayout />}>
|
||||
<Route path=":context?/*" element={<SyncContext />}>
|
||||
<Route
|
||||
path="repository/:selectedRepo?/*"
|
||||
element={<RepositoryPage />}
|
||||
/>
|
||||
<Route path="installed/?" element={<Installed />} />
|
||||
<Route
|
||||
path=":namespace/:chart/installed/revision/:revision"
|
||||
element={<Revision />}
|
||||
/>
|
||||
<Route path="*" element={<Installed />} />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</ErrorBoundary>
|
||||
<GlobalErrorModal
|
||||
isOpen={!!shouldShowErrorModal}
|
||||
onClose={() => setShowErrorModal(undefined)}
|
||||
titleText={shouldShowErrorModal?.title || ""}
|
||||
contentText={shouldShowErrorModal?.msg || ""}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</ErrorModalContext.Provider>
|
||||
</AppContextProvider>
|
||||
);
|
||||
}
|
||||
1
frontend/src/assets/arrow-down-icon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 512.02 319.26"><path d="M5.9 48.96 48.97 5.89c7.86-7.86 20.73-7.84 28.56 0l178.48 178.48L434.5 5.89c7.86-7.86 20.74-7.82 28.56 0l43.07 43.07c7.83 7.84 7.83 20.72 0 28.56l-192.41 192.4-.36.37-43.07 43.07c-7.83 7.82-20.7 7.86-28.56 0l-43.07-43.07-.36-.37L5.9 77.52c-7.87-7.86-7.87-20.7 0-28.56z"/></svg>
|
||||
|
After Width: | Height: | Size: 501 B |
41
frontend/src/assets/body-background.svg
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
frontend/src/assets/close.png
Normal file
|
After Width: | Height: | Size: 731 B |
1
frontend/src/assets/code-brackets.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
1
frontend/src/assets/colors.svg
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
1
frontend/src/assets/comments.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
1
frontend/src/assets/direction.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
1
frontend/src/assets/flow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
5
frontend/src/assets/k8s-watcher.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M47.1106 45.775C49.9376 45.775 52.2294 42.4134 52.2294 38.2667C52.2294 34.1199 49.9376 30.7583 47.1106 30.7583C44.2835 30.7583 41.9918 34.1199 41.9918 38.2667C41.9918 42.4134 44.2835 45.775 47.1106 45.775Z" fill="#1347FF"/>
|
||||
<path d="M37.0077 38.2667C37.0077 42.4134 34.7159 45.775 31.8888 45.775C29.0618 45.775 26.77 42.4134 26.77 38.2667C26.77 34.12 29.0618 30.7584 31.8888 30.7584C34.7159 30.7584 37.0077 34.12 37.0077 38.2667Z" fill="#1347FF"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 23.7087L14.4407 18H64.5597L70 23.6838V56.3162L64.5597 62H14.4403L9 56.3162V23.7087ZM17.9923 24.2134L15.8664 26.3861V53.6519L17.9923 55.8246H61.0736L63.1212 53.6483V26.3897L61.0736 24.2134H17.9923Z" fill="#1347FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 827 B |
42
frontend/src/assets/logo-header.svg
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.7 KiB |
1
frontend/src/assets/plugin.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
6
frontend/src/assets/react.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img"
|
||||
class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228">
|
||||
<path fill="#00D8FF"
|
||||
d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z">
|
||||
</path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
1
frontend/src/assets/repo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
1
frontend/src/assets/stackalt.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
40
frontend/src/components/Badge.stories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* @file Badge.stories.tsx
|
||||
* @description Badge stories, using Storybook.
|
||||
* We create a story for the component badge,
|
||||
* and we can use it to test the component in Storybook.
|
||||
* There, we can see the component in different states, and
|
||||
* play with the props to see how it behaves.
|
||||
* We'll use a generic story for the component, and we'll
|
||||
* use the args to pass the props.
|
||||
* We'll use a template to create the story.
|
||||
* Refer to Badge.tsx and the BadgeProps interface to see what props
|
||||
* the component accepts. The story works with the same props.
|
||||
*
|
||||
* @see https://storybook.js.org/docs/react/writing-stories/introduction
|
||||
*/
|
||||
|
||||
import type { Meta } from "@storybook/react-vite";
|
||||
|
||||
import Badge from "./Badge";
|
||||
|
||||
// We set the metadata for the story.
|
||||
// Refer to https://storybook.js.org/docs/react/writing-stories/introduction
|
||||
// for more information.
|
||||
const meta = {
|
||||
title: "Badge",
|
||||
component: Badge,
|
||||
args: {
|
||||
type: "success",
|
||||
children: "Success",
|
||||
},
|
||||
} satisfies Meta<typeof Badge>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default = {
|
||||
args: {
|
||||
type: "success",
|
||||
children: "Success",
|
||||
},
|
||||
};
|
||||
73
frontend/src/components/Badge.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* This is a generic badge component.
|
||||
* By passing props you can customize the badge.
|
||||
* The basic custom types are:
|
||||
* warning, success, error, info, default.
|
||||
* You can use this badge like any other html element.
|
||||
*
|
||||
* behind the scenes, it uses tailwindcss classes to imlement the badge,
|
||||
* with the correct styles.
|
||||
*
|
||||
* @example
|
||||
* <Badge type="warning">Warning</Badge>
|
||||
*
|
||||
* @param {string} type - The type of the badge.
|
||||
* @param {string} children - The content of the badge.
|
||||
* @returns {JSX.Element} - The badge component.
|
||||
*
|
||||
*
|
||||
*/
|
||||
import type { JSX, ReactNode } from "react";
|
||||
|
||||
export type BadgeCode = "success" | "warning" | "error" | "unknown";
|
||||
|
||||
export const BadgeCodes = Object.freeze({
|
||||
ERROR: "error",
|
||||
WARNING: "warning",
|
||||
SUCCESS: "success",
|
||||
UNKNOWN: "unknown",
|
||||
});
|
||||
|
||||
export interface BadgeProps {
|
||||
type: BadgeCode;
|
||||
children: ReactNode;
|
||||
additionalClassNames?: string;
|
||||
}
|
||||
export default function Badge(props: BadgeProps): JSX.Element {
|
||||
const colorVariants = {
|
||||
[BadgeCodes.SUCCESS]: "bg-text-success text-black-800",
|
||||
[BadgeCodes.WARNING]: "bg-text-warning text-white",
|
||||
[BadgeCodes.ERROR]: "bg-text-danger text-white",
|
||||
[BadgeCodes.UNKNOWN]: "bg-secondary text-danger",
|
||||
};
|
||||
|
||||
const badgeBase =
|
||||
"inline-flex items-center px-1 py-1 rounded-sm text-xs font-light";
|
||||
|
||||
const badgeElem = (
|
||||
<span
|
||||
className={`${badgeBase} ${colorVariants[props.type]} ${
|
||||
props.additionalClassNames ?? ""
|
||||
}`}
|
||||
>
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
return badgeElem;
|
||||
}
|
||||
|
||||
export const getBadgeType = (status: string): BadgeCode => {
|
||||
if (status === "Unknown") {
|
||||
return BadgeCodes.UNKNOWN;
|
||||
} else if (
|
||||
status === "Healthy" ||
|
||||
status.toLowerCase().includes("exists") ||
|
||||
status === "available"
|
||||
) {
|
||||
return BadgeCodes.SUCCESS;
|
||||
} else if (status === "Progressing") {
|
||||
return BadgeCodes.WARNING;
|
||||
} else {
|
||||
return BadgeCodes.ERROR;
|
||||
}
|
||||
};
|
||||
32
frontend/src/components/Button.cy.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { mount } from "cypress/react";
|
||||
|
||||
import { Button } from "./common/Button/Button";
|
||||
|
||||
describe("Button component tests", () => {
|
||||
const buttonText = "buttonText";
|
||||
|
||||
it("renders", () => {
|
||||
mount(<Button onClick={() => {}} label=""></Button>);
|
||||
cy.get("button").should("exist");
|
||||
});
|
||||
|
||||
it("Should have correct text", () => {
|
||||
mount(<Button label={buttonText} onClick={() => {}}></Button>);
|
||||
cy.get("button").contains(buttonText);
|
||||
});
|
||||
|
||||
it("calls onClick when clicked", () => {
|
||||
const onClickStub = cy.stub().as("onClick");
|
||||
|
||||
mount(<Button onClick={onClickStub} label={""}></Button>);
|
||||
|
||||
cy.get("button").click();
|
||||
cy.get("@onClick").should("have.been.calledOnce");
|
||||
});
|
||||
|
||||
it("should be disabled", () => {
|
||||
mount(<Button onClick={() => {}} disabled label={""}></Button>);
|
||||
|
||||
cy.get("button").should("be.disabled");
|
||||
});
|
||||
});
|
||||
28
frontend/src/components/Button.stories.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
import Button from "./Button";
|
||||
|
||||
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: "Button",
|
||||
component: Button,
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryObj<typeof Button> = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<span>↑</span>
|
||||
<span>Update</span>
|
||||
</>
|
||||
),
|
||||
},
|
||||
argTypes: {
|
||||
onClick: { action: "clicked" },
|
||||
},
|
||||
};
|
||||
37
frontend/src/components/Button.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @file Button.tsx
|
||||
* This component is a generic button component using tailwind.
|
||||
* You can include an optional icon.
|
||||
* You can pass the action to be done when the button is clicked using
|
||||
* the onClick prop.
|
||||
*
|
||||
* Props:
|
||||
*
|
||||
* @param children: children
|
||||
* @param onClick: () => void
|
||||
*
|
||||
*
|
||||
*/
|
||||
import type { HTMLAttributes, JSX, ReactNode } from "react";
|
||||
|
||||
// this is a type declaration for the action prop.
|
||||
// it is a function that takes a string as an argument and returns void.
|
||||
export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
export default function Button(props: ButtonProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={props.onClick}
|
||||
className={`${props.className} rounded-sm border border-gray-300 bg-white px-4 py-1 text-black hover:bg-gray-50`}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
90
frontend/src/components/ClustersList.cy.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { BrowserRouter } from "react-router";
|
||||
|
||||
import { AppContextProvider } from "../context/AppContext";
|
||||
import type { Release } from "../data/types";
|
||||
|
||||
import ClustersList from "./ClustersList";
|
||||
import { DeploymentStatus } from "./common/StatusLabel";
|
||||
|
||||
type ClustersListProps = {
|
||||
onClusterChange: (clusterName: string) => void;
|
||||
selectedCluster: string;
|
||||
filteredNamespaces: string[];
|
||||
installedReleases?: Release[];
|
||||
};
|
||||
|
||||
const generateTestReleaseData = (): Release => ({
|
||||
id: "id",
|
||||
name: "helm-dashboard",
|
||||
namespace: "default",
|
||||
revision: 1,
|
||||
updated: "2024-01-23T15:37:35.0992836+02:00",
|
||||
status: DeploymentStatus.DEPLOYED,
|
||||
chart: "helm-dashboard-0.1.10",
|
||||
chart_name: "helm-dashboard",
|
||||
chart_ver: "0.1.10",
|
||||
app_version: "1.3.3",
|
||||
icon: "https://raw.githubusercontent.com/komodorio/helm-dashboard/main/pkg/dashboard/static/logo.svg",
|
||||
description: "A GUI Dashboard for Helm by Komodor",
|
||||
has_tests: true,
|
||||
chartName: "helm-dashboard",
|
||||
chartVersion: "0.1.10",
|
||||
});
|
||||
|
||||
const renderClustersList = (props: ClustersListProps) => {
|
||||
const queryClient = new QueryClient();
|
||||
cy.mount(
|
||||
<AppContextProvider>
|
||||
<BrowserRouter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ClustersList {...props} />
|
||||
</QueryClientProvider>
|
||||
</BrowserRouter>
|
||||
</AppContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe("ClustersList", () => {
|
||||
it("Got one cluster information", () => {
|
||||
cy.intercept("GET", "/api/k8s/contexts", [
|
||||
{
|
||||
Name: "minikube",
|
||||
Namespace: "default",
|
||||
IsCurrent: true,
|
||||
},
|
||||
]).as("getClusters");
|
||||
|
||||
renderClustersList({
|
||||
selectedCluster: "minikube",
|
||||
filteredNamespaces: ["default"],
|
||||
onClusterChange: () => {},
|
||||
installedReleases: [generateTestReleaseData()],
|
||||
});
|
||||
|
||||
cy.wait("@getClusters");
|
||||
cy.get(".data-cy-clusterName").contains("minikube");
|
||||
cy.get(".data-cy-clusterList-namespace").contains("default");
|
||||
cy.get(".data-cy-clustersInput").should("be.checked");
|
||||
});
|
||||
|
||||
it("Dont have a cluster chekced", () => {
|
||||
cy.intercept("GET", "/api/k8s/contexts", [
|
||||
{
|
||||
Name: "minikube",
|
||||
Namespace: "default",
|
||||
IsCurrent: true,
|
||||
},
|
||||
]).as("getClusters");
|
||||
|
||||
renderClustersList({
|
||||
selectedCluster: "",
|
||||
filteredNamespaces: [""],
|
||||
onClusterChange: () => {},
|
||||
installedReleases: [generateTestReleaseData()],
|
||||
});
|
||||
|
||||
cy.wait("@getClusters");
|
||||
cy.get(".data-cy-clustersInput").should("not.be.checked");
|
||||
});
|
||||
});
|
||||
27
frontend/src/components/ClustersList.stories.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
import ClustersList from "./ClustersList";
|
||||
|
||||
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: "ClustersList",
|
||||
component: ClustersList,
|
||||
} satisfies Meta<typeof ClustersList>;
|
||||
|
||||
export default meta;
|
||||
|
||||
//👇 We create a “template” of how args map to rendering
|
||||
export const Default: StoryObj<typeof ClustersList> = {
|
||||
args: {
|
||||
filteredNamespaces: [""],
|
||||
installedReleases: [],
|
||||
selectedCluster: "",
|
||||
},
|
||||
|
||||
argTypes: {
|
||||
onClusterChange: { actions: "onClusterChange called" },
|
||||
},
|
||||
};
|
||||
167
frontend/src/components/ClustersList.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useEffectEvent, useMemo } from "react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import apiService from "../API/apiService";
|
||||
import { useAppContext } from "../context/AppContext";
|
||||
import type { Cluster, Release } from "../data/types";
|
||||
import useCustomSearchParams from "../hooks/useCustomSearchParams";
|
||||
|
||||
type ClustersListProps = {
|
||||
onClusterChange: (clusterName: string) => void;
|
||||
selectedCluster: string;
|
||||
filteredNamespaces: string[];
|
||||
installedReleases?: Release[];
|
||||
};
|
||||
|
||||
function getCleanClusterName(rawClusterName: string) {
|
||||
if (rawClusterName.indexOf("arn") === 0) {
|
||||
// AWS cluster
|
||||
const clusterSplit = rawClusterName.split(":");
|
||||
const clusterName = clusterSplit.slice(-1)[0].replace("cluster/", "");
|
||||
const region = clusterSplit.at(-3);
|
||||
return region + "/" + clusterName + " [AWS]";
|
||||
}
|
||||
|
||||
if (rawClusterName.indexOf("gke") === 0) {
|
||||
// GKE cluster
|
||||
return (
|
||||
rawClusterName.split("_").at(-2) +
|
||||
"/" +
|
||||
rawClusterName.split("_").at(-1) +
|
||||
" [GKE]"
|
||||
);
|
||||
}
|
||||
|
||||
return rawClusterName;
|
||||
}
|
||||
|
||||
function ClustersList({
|
||||
installedReleases,
|
||||
selectedCluster,
|
||||
filteredNamespaces,
|
||||
onClusterChange,
|
||||
}: ClustersListProps) {
|
||||
const { upsertSearchParams, removeSearchParam } = useCustomSearchParams();
|
||||
const { clusterMode } = useAppContext();
|
||||
|
||||
const { data: clusters = [], isSuccess } = useQuery<Cluster[]>({
|
||||
queryKey: ["clusters", selectedCluster],
|
||||
queryFn: apiService.getClusters,
|
||||
select: (data) =>
|
||||
data?.sort((a, b) =>
|
||||
getCleanClusterName(a.Name).localeCompare(getCleanClusterName(b.Name))
|
||||
),
|
||||
});
|
||||
|
||||
const onSuccess = useEffectEvent((clusters: Cluster[]) => {
|
||||
if (clusters && clusters.length && !selectedCluster) {
|
||||
onClusterChange(clusters[0].Name);
|
||||
}
|
||||
|
||||
if (selectedCluster) {
|
||||
const cluster = clusters.find(
|
||||
(cluster) => getCleanClusterName(cluster.Name) === selectedCluster
|
||||
);
|
||||
if (!filteredNamespaces && cluster?.Namespace) {
|
||||
upsertSearchParams("filteredNamespace", cluster.Namespace);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (clusters && isSuccess) {
|
||||
onSuccess(clusters);
|
||||
}
|
||||
}, [clusters, isSuccess]);
|
||||
|
||||
const namespaces = useMemo(() => {
|
||||
const mapNamespaces = new Map<string, number>();
|
||||
|
||||
installedReleases?.forEach((release) => {
|
||||
const amount = mapNamespaces.get(release.namespace)
|
||||
? Number(mapNamespaces.get(release.namespace)) + 1
|
||||
: 1;
|
||||
mapNamespaces.set(release.namespace, amount);
|
||||
});
|
||||
|
||||
return Array.from(mapNamespaces, ([key, value]) => ({
|
||||
id: uuidv4(),
|
||||
name: key,
|
||||
amount: value,
|
||||
}));
|
||||
}, [installedReleases]);
|
||||
|
||||
const onNamespaceChange = (namespace: string) => {
|
||||
const newSelectedNamespaces = filteredNamespaces?.includes(namespace)
|
||||
? filteredNamespaces?.filter((ns) => ns !== namespace)
|
||||
: [...(filteredNamespaces ?? []), namespace];
|
||||
removeSearchParam("filteredNamespace");
|
||||
if (newSelectedNamespaces.length > 0) {
|
||||
upsertSearchParams(
|
||||
"filteredNamespace",
|
||||
newSelectedNamespaces.map((ns) => ns).join("+")
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="custom- custom-shadow m-5 flex h-fit w-48 flex-col rounded-sm bg-white p-2 pb-4 text-cluster-list">
|
||||
{!clusterMode ? (
|
||||
<>
|
||||
<label className="font-bold">Clusters</label>
|
||||
{clusters?.map((cluster) => {
|
||||
return (
|
||||
<span
|
||||
key={cluster.Name + cluster.Namespace}
|
||||
className="data-cy-clusterName mt-2 flex items-center text-xs"
|
||||
>
|
||||
<input
|
||||
className="data-cy-clustersInput cursor-pointer"
|
||||
onChange={(e) => {
|
||||
onClusterChange(e.target.value);
|
||||
}}
|
||||
type="radio"
|
||||
id={cluster.Name}
|
||||
value={cluster.Name}
|
||||
checked={cluster.Name === selectedCluster}
|
||||
name="clusters"
|
||||
/>
|
||||
<label htmlFor={cluster.Name} className="ml-1">
|
||||
{getCleanClusterName(cluster.Name)}
|
||||
</label>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<label className="mt-4 font-bold">Namespaces</label>
|
||||
{namespaces
|
||||
?.sort((a, b) => a.name.localeCompare(b.name))
|
||||
?.map((namespace) => (
|
||||
<span key={namespace.name} className="mt-2 flex items-center text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={namespace.name}
|
||||
onChange={(event) => {
|
||||
onNamespaceChange(event.target.value);
|
||||
}}
|
||||
value={namespace.name}
|
||||
checked={
|
||||
filteredNamespaces
|
||||
? filteredNamespaces.includes(namespace.name)
|
||||
: false
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={namespace.name}
|
||||
className="data-cy-clusterList-namespace ml-1"
|
||||
>{`${namespace.name} [${namespace.amount}]`}</label>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClustersList;
|
||||
114
frontend/src/components/ErrorFallback/ErrorFallback.cy.tsx
Normal 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!");
|
||||
});
|
||||
});
|
||||