mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into close-dock-tabs-with-cmd-w
This commit is contained in:
commit
2822f82f8d
@ -15,7 +15,7 @@ trigger: none
|
|||||||
jobs:
|
jobs:
|
||||||
- job: Linux
|
- job: Linux
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-16.04
|
vmImage: ubuntu-18.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
kube_1.16:
|
kube_1.16:
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
||||||
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
||||||
sudo minikube start --driver=none --kubernetes-version $(kubernetes_version)
|
sudo minikube start --driver=none --kubernetes-version $(kubernetes_version)
|
||||||
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
sudo mv /root/.kube /root/.minikube $HOME
|
||||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||||
displayName: Install integration test dependencies
|
displayName: Install integration test dependencies
|
||||||
- script: make node_modules
|
- script: make node_modules
|
||||||
|
|||||||
@ -109,7 +109,7 @@ jobs:
|
|||||||
|
|
||||||
- job: Linux
|
- job: Linux
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-16.04
|
vmImage: ubuntu-18.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node_14.x:
|
node_14.x:
|
||||||
|
|||||||
16
.github/workflows/test.yml
vendored
16
.github/workflows/test.yml
vendored
@ -2,7 +2,7 @@ name: Test
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- "**"
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-16.04, macos-11, windows-2019]
|
os: [ubuntu-18.04, macos-11, windows-2019]
|
||||||
node-version: [14.x]
|
node-version: [14.x]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Release from lens
|
- name: Checkout Release from lens
|
||||||
@ -67,15 +67,9 @@ jobs:
|
|||||||
- run: make test-extensions
|
- run: make test-extensions
|
||||||
name: Run In-tree Extension tests
|
name: Run In-tree Extension tests
|
||||||
|
|
||||||
- run: |
|
- name: Install integration test dependencies
|
||||||
sudo apt-get update
|
id: minikube
|
||||||
sudo apt-get install libgconf-2-4 conntrack -y
|
uses: medyagh/setup-minikube@5a9a7104d7322fa40424de8855c84685e89cefd7
|
||||||
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
|
|
||||||
sudo install minikube-linux-amd64 /usr/local/bin/minikube
|
|
||||||
sudo minikube start --driver=none
|
|
||||||
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
|
||||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
|
||||||
name: Install integration test dependencies
|
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
|
|
||||||
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration
|
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration
|
||||||
|
|||||||
2
.yarnrc
2
.yarnrc
@ -1,3 +1,3 @@
|
|||||||
disturl "https://atom.io/download/electron"
|
disturl "https://atom.io/download/electron"
|
||||||
target "12.0.15"
|
target "13.5.1"
|
||||||
runtime "electron"
|
runtime "electron"
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
Lens releases are built by CICD automatically on git tags. The typical release process flow is the following:
|
Lens releases are built by CICD automatically on git tags. The typical release process flow is the following:
|
||||||
|
|
||||||
1. From a clean and up to date `master` run `make release-version <version-type>` where `<version-type>` is one of the following:
|
1. If doing a patch release checkout the `release/vMAJOR.MINOR` branch for the appropriate `MAJOR`/`MINOR` version and manually `cherry-pick` the PRs required for the patch that were commited to master. If there are any conflicts they must be resolved manually. If necessary, get assistance from the PR authors.
|
||||||
|
|
||||||
|
1. From a clean and up to date `master` (or `release/vMAJOR.MINOR` if doing a patch release) run `make release-version <version-type>` where `<version-type>` is one of the following:
|
||||||
- `major`
|
- `major`
|
||||||
- `minor`
|
- `minor`
|
||||||
- `patch`
|
- `patch`
|
||||||
@ -17,15 +19,15 @@ Lens releases are built by CICD automatically on git tags. The typical release p
|
|||||||
- `rc`
|
- `rc`
|
||||||
|
|
||||||
This assumes origin is set to https://github.com/lensapp/lens.git. If not then set GIT_REMOTE to the remote that is set to https://github.com/lensapp/lens.git. For example run `GIT_REMOTE=upstream make release-version ...`
|
This assumes origin is set to https://github.com/lensapp/lens.git. If not then set GIT_REMOTE to the remote that is set to https://github.com/lensapp/lens.git. For example run `GIT_REMOTE=upstream make release-version ...`
|
||||||
1. Open the PR (git should have printed a link to GitHub in the console) with the contents of all the accepted PRs since the last release. The PR description needs to be filled with the draft release description. From https://github.com/lensapp/lens click on Releases, the draft release should be first in the list, click `Edit` and copy/paste the markdown to the PR description. Add the `skip-changelog` label and click `Create Pull Request`.
|
1. Open the PR (git should have printed a link to GitHub in the console) with the contents of all the accepted PRs since the last release. The PR description needs to be filled with the draft release description. From https://github.com/lensapp/lens click on Releases, the draft release should be first in the list, click `Edit` and copy/paste the markdown to the PR description. Add the `skip-changelog` label and click `Create Pull Request`. If this is a patch release be sure to set the PR base branch to `release/vMAJOR.MINOR` instead of `master`.
|
||||||
1. After the PR is accepted and passes CI (and before merging), go to the same branch and run `make tag-release` (set GIT_REMOTE if necessary). This additionally runs the azure jobs to build the binaries and put them on S3.
|
1. After the PR is accepted and passes CI (and before merging), go to the same branch and run `make tag-release` (set GIT_REMOTE if necessary). This additionally triggers the azure jobs to build the binaries and put them on S3.
|
||||||
1. If the CI fails at this stage the problem needs to be fixed. Sometimes an azure job fails due to outside service issues (e.g. Apple signing occasionally fails), in which case the specific azure job can be rerun from https://dev.azure.com/lensapp/lensapp/_build. Otherwise changes to the codebase may need to be done and committed to the release branch and pushed to https://github.com/lensapp/lens. CI will run again. As well the release tag needs to be manually set to this new commit. You can do something like:
|
1. If the CI fails at this stage the problem needs to be fixed. Sometimes an azure job fails due to outside service issues (e.g. Apple signing occasionally fails), in which case the specific azure job can be rerun from https://dev.azure.com/lensapp/lensapp/_build. Otherwise changes to the codebase may need to be done and committed to the release branch and pushed to https://github.com/lensapp/lens. CI will run again. As well the release tag needs to be manually set to this new commit. You can do something like:
|
||||||
- `git push origin :refs/tags/vX.Y.Z-beta.N` (removes the tag from https://github.com/lensapp/lens)
|
- `git push origin :refs/tags/vX.Y.Z-beta.N` (removes the tag from https://github.com/lensapp/lens)
|
||||||
- `git tag -fa vX.Y.Z-beta.N` (move the tag locally to the current commit)
|
- `git tag -fa vX.Y.Z-beta.N` (move the tag locally to the current commit)
|
||||||
- `git push origin --tags` (update the tags on https://github.com/lensapp/lens to reflect this local change)
|
- `git push origin --tags` (update the tags on https://github.com/lensapp/lens to reflect this local change)
|
||||||
|
|
||||||
1. Once CI passes again go to the releases tab on GitHub. You can use the existing draft release prepared by k8slens-bot (select the correct tag). Or you can create a new release from the tag that was created, and make sure that the change log is the same as that of the PR, and the title is the tag. Either way, click the prerelease checkbox if this is not a new major, minor, or patch version before clicking `Publish release`.
|
1. Once CI passes again go to the releases tab on GitHub. You can use the existing draft release prepared by k8slens-bot (select the correct tag). Or you can create a new release from the tag that was created, and make sure that the change log is the same as that of the PR, and the title is the tag. Either way, click the prerelease checkbox if this is not a new major, minor, or patch version before clicking `Publish release`.
|
||||||
1. Merge the release PR after the release is published. GitHub should delete the branch once it is merged.
|
1. Merge the release PR after the release is published. If it is a patch release then there is no need to squash the cherry-picked commits as part of the merge. GitHub should delete the branch once it is merged.
|
||||||
1. If you have just released a new major or minor version then create a new `release/vMAJOR.MINOR` branch from that same tag and push it to https://github.com/lensapp/lens. Given the commit of the merged release PR from the master branch you can do this like
|
1. If you have just released a new major or minor version then create a new `release/vMAJOR.MINOR` branch from that same tag and push it to https://github.com/lensapp/lens. Given the commit of the merged release PR from the master branch you can do this like
|
||||||
|
|
||||||
`git push origin <commit>:refs/heads/release/vX.Y`
|
`git push origin <commit>:refs/heads/release/vX.Y`
|
||||||
@ -37,4 +39,4 @@ Other tasks
|
|||||||
- generate a changelog from the prerelease descriptions (for major/minor releases)
|
- generate a changelog from the prerelease descriptions (for major/minor releases)
|
||||||
- announce the release on lens and lens-hq slack channels (release is announced automatically on the community slack lens channel through the above publishing process)
|
- announce the release on lens and lens-hq slack channels (release is announced automatically on the community slack lens channel through the above publishing process)
|
||||||
- announce on lens-hq that master is open for PR merges for the next release (for major/minor releases)
|
- announce on lens-hq that master is open for PR merges for the next release (for major/minor releases)
|
||||||
- update issues on github (bump those that did not make it into the release to a subsequent release) (for major/minor releases)
|
- update issues on github (bump those that did not make it into the release to a subsequent release) (for major/minor/patch releases)
|
||||||
@ -8,9 +8,9 @@
|
|||||||
"version": "file:../../src/extensions/npm/extensions",
|
"version": "file:../../src/extensions/npm/extensions",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "4.12.3",
|
||||||
"@types/node": "*",
|
"@types/node": "14.17.14",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "3.1.2",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"typed-emitter": "^1.3.1"
|
"typed-emitter": "^1.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
27
extensions/metrics-cluster-feature/package-lock.json
generated
27
extensions/metrics-cluster-feature/package-lock.json
generated
@ -662,10 +662,11 @@
|
|||||||
"version": "file:../../src/extensions/npm/extensions",
|
"version": "file:../../src/extensions/npm/extensions",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "4.12.3",
|
||||||
"@types/node": "*",
|
"@types/node": "14.17.14",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "3.1.2",
|
||||||
"conf": "^7.0.1"
|
"conf": "^7.0.1",
|
||||||
|
"typed-emitter": "^1.3.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": {
|
"@babel/runtime": {
|
||||||
@ -1329,6 +1330,12 @@
|
|||||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"typed-emitter": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/typed-emitter/-/typed-emitter-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-2h7utWyXgd2R2u2IuL8B4yu1gqMxbgUj2VS/MGVbFhEVQNJKXoQQoS5CBMh+eW31zFeSmDfEQ3qQf4xy5SlPVQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"uri-js": {
|
"uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
@ -1742,9 +1749,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
@ -6645,9 +6652,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tmpl": {
|
"tmpl": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
|
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"to-arraybuffer": {
|
"to-arraybuffer": {
|
||||||
|
|||||||
18
extensions/node-menu/package-lock.json
generated
18
extensions/node-menu/package-lock.json
generated
@ -628,9 +628,9 @@
|
|||||||
"version": "file:../../src/extensions/npm/extensions",
|
"version": "file:../../src/extensions/npm/extensions",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "4.12.3",
|
||||||
"@types/node": "*",
|
"@types/node": "14.17.14",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "3.1.2",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"typed-emitter": "^1.3.1"
|
"typed-emitter": "^1.3.1"
|
||||||
},
|
},
|
||||||
@ -1707,9 +1707,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
@ -6608,9 +6608,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tmpl": {
|
"tmpl": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
|
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"to-arraybuffer": {
|
"to-arraybuffer": {
|
||||||
|
|||||||
18
extensions/pod-menu/package-lock.json
generated
18
extensions/pod-menu/package-lock.json
generated
@ -628,9 +628,9 @@
|
|||||||
"version": "file:../../src/extensions/npm/extensions",
|
"version": "file:../../src/extensions/npm/extensions",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "4.12.3",
|
||||||
"@types/node": "*",
|
"@types/node": "14.17.14",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "3.1.2",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"typed-emitter": "^1.3.1"
|
"typed-emitter": "^1.3.1"
|
||||||
},
|
},
|
||||||
@ -1660,9 +1660,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"ansi-styles": {
|
"ansi-styles": {
|
||||||
@ -6561,9 +6561,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tmpl": {
|
"tmpl": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||||
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
|
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"to-arraybuffer": {
|
"to-arraybuffer": {
|
||||||
|
|||||||
@ -25,16 +25,24 @@
|
|||||||
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
|
||||||
cluster and vice versa.
|
cluster and vice versa.
|
||||||
*/
|
*/
|
||||||
import type { Page } from "playwright";
|
import type { ElectronApplication, Page } from "playwright";
|
||||||
import * as utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
|
|
||||||
describe("preferences page tests", () => {
|
describe("preferences page tests", () => {
|
||||||
let window: Page, cleanup: () => Promise<void>;
|
let window: Page, cleanup: () => Promise<void>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
({ window, cleanup } = await utils.start());
|
let app: ElectronApplication;
|
||||||
|
|
||||||
|
({ window, cleanup, app } = await utils.start());
|
||||||
await utils.clickWelcomeButton(window);
|
await utils.clickWelcomeButton(window);
|
||||||
await window.keyboard.press("Meta+,");
|
|
||||||
|
await app.evaluate(async ({ app }) => {
|
||||||
|
await app.applicationMenu
|
||||||
|
.getMenuItemById(process.platform === "darwin" ? "root" : "file")
|
||||||
|
.submenu.getMenuItemById("preferences")
|
||||||
|
.click();
|
||||||
|
});
|
||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
|||||||
@ -19,14 +19,14 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Page } from "playwright";
|
import type { ElectronApplication, Page } from "playwright";
|
||||||
import * as utils from "../helpers/utils";
|
import * as utils from "../helpers/utils";
|
||||||
|
|
||||||
describe("Lens command palette", () => {
|
describe("Lens command palette", () => {
|
||||||
let window: Page, cleanup: () => Promise<void>;
|
let window: Page, cleanup: () => Promise<void>, app: ElectronApplication;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
({ window, cleanup } = await utils.start());
|
({ window, cleanup, app } = await utils.start());
|
||||||
await utils.clickWelcomeButton(window);
|
await utils.clickWelcomeButton(window);
|
||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
|
|
||||||
@ -35,8 +35,13 @@ describe("Lens command palette", () => {
|
|||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
|
|
||||||
describe("menu", () => {
|
describe("menu", () => {
|
||||||
it("opens command dialog from keyboard shortcut", async () => {
|
it("opens command dialog from menu", async () => {
|
||||||
await window.keyboard.press("Meta+Shift+p");
|
await app.evaluate(async ({ app }) => {
|
||||||
|
await app.applicationMenu
|
||||||
|
.getMenuItemById("view")
|
||||||
|
.submenu.getMenuItemById("command-palette")
|
||||||
|
.click();
|
||||||
|
});
|
||||||
await window.waitForSelector(".Select__option >> text=Hotbar: Switch");
|
await window.waitForSelector(".Select__option >> text=Hotbar: Switch");
|
||||||
}, 10*60*1000);
|
}, 10*60*1000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -104,10 +104,7 @@ function minikubeEntityId() {
|
|||||||
* From the catalog, click the minikube entity and wait for it to connect, returning its frame
|
* From the catalog, click the minikube entity and wait for it to connect, returning its frame
|
||||||
*/
|
*/
|
||||||
export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Frame> {
|
export async function lauchMinikubeClusterFromCatalog(window: Page): Promise<Frame> {
|
||||||
await window.waitForSelector("div.TableCell");
|
|
||||||
await window.click("div.TableCell >> text='minikube'");
|
await window.click("div.TableCell >> text='minikube'");
|
||||||
await window.waitForSelector("div.drawer-title-text >> text='KubernetesCluster: minikube'");
|
|
||||||
await window.click("div.EntityIcon div.HotbarIcon div div.MuiAvatar-root");
|
|
||||||
|
|
||||||
const minikubeFrame = await window.waitForSelector(`#cluster-frame-${minikubeEntityId()}`);
|
const minikubeFrame = await window.waitForSelector(`#cluster-frame-${minikubeEntityId()}`);
|
||||||
|
|
||||||
|
|||||||
67
package.json
67
package.json
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.2.0",
|
"version": "5.3.0-alpha.0",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -185,7 +185,6 @@
|
|||||||
"@kubernetes/client-node": "^0.15.1",
|
"@kubernetes/client-node": "^0.15.1",
|
||||||
"@sentry/electron": "^2.5.0",
|
"@sentry/electron": "^2.5.0",
|
||||||
"@sentry/integrations": "^6.10.0",
|
"@sentry/integrations": "^6.10.0",
|
||||||
"@types/which": "^2.0.1",
|
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"array-move": "^3.0.1",
|
"array-move": "^3.0.1",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
@ -194,14 +193,16 @@
|
|||||||
"byline": "^5.0.0",
|
"byline": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"chokidar": "^3.4.3",
|
"chokidar": "^3.4.3",
|
||||||
|
"command-exists": "1.2.9",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-updater": "^4.4.3",
|
"electron-updater": "^4.6.0",
|
||||||
"electron-window-state": "^5.0.3",
|
"electron-window-state": "^5.0.3",
|
||||||
"filehound": "^1.17.4",
|
"filehound": "^1.17.4",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
|
"got": "^11.8.2",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
@ -209,7 +210,7 @@
|
|||||||
"joi": "^17.4.2",
|
"joi": "^17.4.2",
|
||||||
"js-yaml": "^3.14.0",
|
"js-yaml": "^3.14.0",
|
||||||
"jsdom": "^16.7.0",
|
"jsdom": "^16.7.0",
|
||||||
"jsonpath": "^1.0.2",
|
"jsonpath": "^1.1.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"mac-ca": "^1.0.6",
|
"mac-ca": "^1.0.6",
|
||||||
"marked": "^2.0.3",
|
"marked": "^2.0.3",
|
||||||
@ -223,10 +224,9 @@
|
|||||||
"monaco-editor": "^0.26.1",
|
"monaco-editor": "^0.26.1",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"node-pty": "^0.10.1",
|
"node-pty": "^0.10.1",
|
||||||
"npm": "^6.14.8",
|
"npm": "^6.14.15",
|
||||||
"openid-client": "^3.15.2",
|
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"path-to-regexp": "^6.1.0",
|
"path-to-regexp": "^6.2.0",
|
||||||
"proper-lockfile": "^4.1.2",
|
"proper-lockfile": "^4.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
@ -237,17 +237,17 @@
|
|||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.9",
|
"request-promise-native": "^1.0.9",
|
||||||
|
"rfc6902": "^4.0.2",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"serializr": "^2.0.5",
|
"serializr": "^2.0.5",
|
||||||
"shell-env": "^3.0.1",
|
"shell-env": "^3.0.1",
|
||||||
"spdy": "^4.0.2",
|
"spdy": "^4.0.2",
|
||||||
"tar": "^6.1.10",
|
"tar": "^6.1.10",
|
||||||
"tcp-port-used": "^1.0.1",
|
"tcp-port-used": "^1.0.2",
|
||||||
"tempy": "^0.5.0",
|
"tempy": "1.0.1",
|
||||||
"url-parse": "^1.5.1",
|
"url-parse": "^1.5.3",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"which": "^2.0.2",
|
"win-ca": "^3.4.5",
|
||||||
"win-ca": "^3.2.0",
|
|
||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"winston-console-format": "^1.0.8",
|
"winston-console-format": "^1.0.8",
|
||||||
"winston-transport-browserconsole": "^1.0.5",
|
"winston-transport-browserconsole": "^1.0.5",
|
||||||
@ -275,9 +275,9 @@
|
|||||||
"@types/glob-to-regexp": "^0.4.1",
|
"@types/glob-to-regexp": "^0.4.1",
|
||||||
"@types/hapi": "^18.0.6",
|
"@types/hapi": "^18.0.6",
|
||||||
"@types/hoist-non-react-statics": "^3.3.1",
|
"@types/hoist-non-react-statics": "^3.3.1",
|
||||||
"@types/html-webpack-plugin": "^3.2.3",
|
"@types/html-webpack-plugin": "^3.2.6",
|
||||||
"@types/http-proxy": "^1.17.7",
|
"@types/http-proxy": "^1.17.7",
|
||||||
"@types/jest": "^26.0.22",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/js-yaml": "^3.12.4",
|
"@types/js-yaml": "^3.12.4",
|
||||||
"@types/jsdom": "^16.2.4",
|
"@types/jsdom": "^16.2.4",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
@ -291,19 +291,19 @@
|
|||||||
"@types/node-fetch": "^2.5.12",
|
"@types/node-fetch": "^2.5.12",
|
||||||
"@types/npm": "^2.0.32",
|
"@types/npm": "^2.0.32",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||||
"@types/proper-lockfile": "^4.1.1",
|
"@types/proper-lockfile": "^4.1.2",
|
||||||
"@types/randomcolor": "^0.5.6",
|
"@types/randomcolor": "^0.5.6",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.0",
|
||||||
"@types/react-beautiful-dnd": "^13.1.1",
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router-dom": "^5.1.6",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
"@types/react-select": "3.1.2",
|
"@types/react-select": "3.1.2",
|
||||||
"@types/react-table": "^7.7.0",
|
"@types/react-table": "^7.7.6",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"@types/readable-stream": "^2.3.9",
|
"@types/readable-stream": "^2.3.9",
|
||||||
"@types/request": "^2.48.7",
|
"@types/request": "^2.48.7",
|
||||||
"@types/request-promise-native": "^1.0.17",
|
"@types/request-promise-native": "^1.0.18",
|
||||||
"@types/semver": "^7.2.0",
|
"@types/semver": "^7.2.0",
|
||||||
"@types/sharp": "^0.28.3",
|
"@types/sharp": "^0.28.3",
|
||||||
"@types/spdy": "^3.4.4",
|
"@types/spdy": "^3.4.4",
|
||||||
@ -314,26 +314,26 @@
|
|||||||
"@types/url-parse": "^1.4.3",
|
"@types/url-parse": "^1.4.3",
|
||||||
"@types/uuid": "^8.3.1",
|
"@types/uuid": "^8.3.1",
|
||||||
"@types/webdriverio": "^4.13.0",
|
"@types/webdriverio": "^4.13.0",
|
||||||
"@types/webpack": "^4.41.17",
|
"@types/webpack": "^4.41.31",
|
||||||
"@types/webpack-dev-server": "^3.11.1",
|
"@types/webpack-dev-server": "^3.11.6",
|
||||||
"@types/webpack-env": "^1.15.2",
|
"@types/webpack-env": "^1.16.2",
|
||||||
"@types/webpack-node-externals": "^1.7.1",
|
"@types/webpack-node-externals": "^1.7.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.29.0",
|
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||||
"@typescript-eslint/parser": "^4.29.1",
|
"@typescript-eslint/parser": "^4.29.1",
|
||||||
"ansi_up": "^5.0.1",
|
"ansi_up": "^5.0.1",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"circular-dependency-plugin": "^5.2.2",
|
"circular-dependency-plugin": "^5.2.2",
|
||||||
"color": "^3.1.2",
|
"color": "^3.1.2",
|
||||||
"concurrently": "^5.2.0",
|
"concurrently": "^5.2.0",
|
||||||
"css-loader": "^5.2.6",
|
"css-loader": "^5.2.7",
|
||||||
"deepdash": "^5.3.5",
|
"deepdash": "^5.3.9",
|
||||||
"dompurify": "^2.3.1",
|
"dompurify": "^2.3.1",
|
||||||
"electron": "^12.0.17",
|
"electron": "^13.5.1",
|
||||||
"electron-builder": "^22.11.11",
|
"electron-builder": "^22.11.11",
|
||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
"esbuild": "^0.12.24",
|
"esbuild": "^0.12.24",
|
||||||
"esbuild-loader": "^2.13.1",
|
"esbuild-loader": "^2.15.1",
|
||||||
"eslint": "^7.7.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.24.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
@ -350,7 +350,7 @@
|
|||||||
"jest-fetch-mock": "^3.0.3",
|
"jest-fetch-mock": "^3.0.3",
|
||||||
"jest-mock-extended": "^1.0.16",
|
"jest-mock-extended": "^1.0.16",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^1.6.0",
|
"mini-css-extract-plugin": "^1.6.2",
|
||||||
"node-gyp": "7.1.2",
|
"node-gyp": "7.1.2",
|
||||||
"node-loader": "^1.0.3",
|
"node-loader": "^1.0.3",
|
||||||
"nodemon": "^2.0.12",
|
"nodemon": "^2.0.12",
|
||||||
@ -358,6 +358,7 @@
|
|||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"postcss-loader": "4.3.0",
|
"postcss-loader": "4.3.0",
|
||||||
"postinstall-postinstall": "^2.1.0",
|
"postinstall-postinstall": "^2.1.0",
|
||||||
|
"prettier": "^2.4.1",
|
||||||
"progress-bar-webpack-plugin": "^2.1.0",
|
"progress-bar-webpack-plugin": "^2.1.0",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
@ -368,20 +369,20 @@
|
|||||||
"react-select-event": "^5.1.0",
|
"react-select-event": "^5.1.0",
|
||||||
"react-table": "^7.7.0",
|
"react-table": "^7.7.0",
|
||||||
"react-window": "^1.8.5",
|
"react-window": "^1.8.5",
|
||||||
"sass": "^1.39.0",
|
"sass": "^1.41.1",
|
||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.29.0",
|
"sharp": "^0.29.1",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.2.4",
|
"tailwindcss": "^2.2.4",
|
||||||
"ts-jest": "26.5.6",
|
"ts-jest": "26.5.6",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^10.1.0",
|
"ts-node": "^10.2.1",
|
||||||
"type-fest": "^1.0.2",
|
"type-fest": "^1.0.2",
|
||||||
"typed-emitter": "^1.3.1",
|
"typed-emitter": "^1.3.1",
|
||||||
"typedoc": "0.21.0-beta.2",
|
"typedoc": "0.22.5",
|
||||||
"typedoc-plugin-markdown": "^3.9.0",
|
"typedoc-plugin-markdown": "^3.9.0",
|
||||||
"typeface-roboto": "^1.1.13",
|
"typeface-roboto": "^1.1.13",
|
||||||
"typescript": "^4.3.2",
|
"typescript": "^4.4.3",
|
||||||
"typescript-plugin-css-modules": "^3.4.0",
|
"typescript-plugin-css-modules": "^3.4.0",
|
||||||
"url-loader": "^4.1.1",
|
"url-loader": "^4.1.1",
|
||||||
"webpack": "^4.46.0",
|
"webpack": "^4.46.0",
|
||||||
|
|||||||
@ -46,6 +46,10 @@ export abstract class BaseStore<T> extends Singleton {
|
|||||||
protected constructor(protected params: BaseStoreParams<T>) {
|
protected constructor(protected params: BaseStoreParams<T>) {
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
|
||||||
|
if (ipcRenderer) {
|
||||||
|
params.migrations = undefined; // don't run migrations on renderer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -57,6 +57,10 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catalogCategoryRegistry
|
||||||
|
.getCategoryForEntity<WebLinkCategory>(this)
|
||||||
|
?.emit("contextMenuOpen", this, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
src/common/catalog/catalog-run-event.ts
Normal file
44
src/common/catalog/catalog-run-event.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { CatalogEntity } from "../catalog";
|
||||||
|
|
||||||
|
export class CatalogRunEvent {
|
||||||
|
#defaultPrevented: boolean;
|
||||||
|
#target: CatalogEntity;
|
||||||
|
|
||||||
|
get defaultPrevented() {
|
||||||
|
return this.#defaultPrevented;
|
||||||
|
}
|
||||||
|
|
||||||
|
get target() {
|
||||||
|
return this.#target;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor({ target }: { target: CatalogEntity }) {
|
||||||
|
this.#defaultPrevented = false;
|
||||||
|
this.#target = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
preventDefault() {
|
||||||
|
this.#defaultPrevented = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,18 +19,15 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
export class ExecValidationNotFoundError extends Error {
|
export class ExecValidationNotFoundError extends Error {
|
||||||
constructor(execPath: string) {
|
constructor(execPath: string, isAbsolute: boolean) {
|
||||||
|
super(`User Exec command "${execPath}" not found on host.`);
|
||||||
let message = `User Exec command "${execPath}" not found on host.`;
|
let message = `User Exec command "${execPath}" not found on host.`;
|
||||||
|
|
||||||
if (!path.isAbsolute(execPath)) {
|
if (!isAbsolute) {
|
||||||
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
||||||
}
|
}
|
||||||
|
this.message = message;
|
||||||
super(message);
|
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
Error.captureStackTrace(this, this.constructor);
|
Error.captureStackTrace(this, this.constructor);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,11 +31,13 @@ export class PersistentVolumeClaimsApi extends KubeApi<PersistentVolumeClaim> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetrics> {
|
export function getMetricsForPvc(pvc: PersistentVolumeClaim): Promise<IPvcMetrics> {
|
||||||
|
const opts = { category: "pvc", pvc: pvc.getName(), namespace: pvc.getNs() };
|
||||||
|
|
||||||
return metricsApi.getMetrics({
|
return metricsApi.getMetrics({
|
||||||
diskUsage: { category: "pvc", pvc: pvc.getName() },
|
diskUsage: opts,
|
||||||
diskCapacity: { category: "pvc", pvc: pvc.getName() }
|
diskCapacity: opts
|
||||||
}, {
|
}, {
|
||||||
namespace: pvc.getNs()
|
namespace: opts.namespace
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,19 +22,27 @@
|
|||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export const resourceApplierApi = {
|
export const annotations = [
|
||||||
annotations: [
|
|
||||||
"kubectl.kubernetes.io/last-applied-configuration"
|
"kubectl.kubernetes.io/last-applied-configuration"
|
||||||
],
|
];
|
||||||
|
|
||||||
async update(resource: object | string): Promise<KubeJsonApiData | null> {
|
export async function update(resource: object | string): Promise<KubeJsonApiData> {
|
||||||
if (typeof resource === "string") {
|
if (typeof resource === "string") {
|
||||||
resource = jsYaml.safeLoad(resource);
|
resource = jsYaml.safeLoad(resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [data = null] = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
|
return apiBase.post<KubeJsonApiData>("/stack", { data: resource });
|
||||||
|
}
|
||||||
return data;
|
|
||||||
|
export async function patch(name: string, kind: string, ns: string, patch: Patch): Promise<KubeJsonApiData> {
|
||||||
|
return apiBase.patch<KubeJsonApiData>("/stack", {
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
kind,
|
||||||
|
ns,
|
||||||
|
patch,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { parseKubeApi } from "./kube-api-parse";
|
|||||||
import type { KubeJsonApiData } from "./kube-json-api";
|
import type { KubeJsonApiData } from "./kube-json-api";
|
||||||
import type { RequestInit } from "node-fetch";
|
import type { RequestInit } from "node-fetch";
|
||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
||||||
namespaces: string[];
|
namespaces: string[];
|
||||||
@ -159,7 +160,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
this.loadedNamespaces = namespaces;
|
this.loadedNamespaces = namespaces;
|
||||||
|
|
||||||
return Promise // load resources per namespace
|
return Promise // load resources per namespace
|
||||||
.all(namespaces.map(namespace => api.list({ namespace, reqInit })))
|
.all(namespaces.map(namespace => api.list({ namespace, reqInit }, this.query)))
|
||||||
.then(items => items.flat().filter(Boolean));
|
.then(items => items.flat().filter(Boolean));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -279,19 +280,29 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
return newItem;
|
return newItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(item: T, data: Partial<T>): Promise<T> {
|
private postUpdate(rawItem: KubeJsonApiData): T {
|
||||||
const rawItem = await item.update(data);
|
|
||||||
const newItem = new this.api.objectConstructor(rawItem);
|
const newItem = new this.api.objectConstructor(rawItem);
|
||||||
|
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
||||||
|
|
||||||
ensureObjectSelfLink(this.api, newItem);
|
ensureObjectSelfLink(this.api, newItem);
|
||||||
|
|
||||||
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
if (index < 0) {
|
||||||
|
this.items.push(newItem);
|
||||||
this.items.splice(index, 1, newItem);
|
} else {
|
||||||
|
this.items[index] = newItem;
|
||||||
|
}
|
||||||
|
|
||||||
return newItem;
|
return newItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async patch(item: T, patch: Patch): Promise<T> {
|
||||||
|
return this.postUpdate(await item.patch(patch));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(item: T, data: Partial<T>): Promise<T> {
|
||||||
|
return this.postUpdate(await item.update(data));
|
||||||
|
}
|
||||||
|
|
||||||
async remove(item: T) {
|
async remove(item: T) {
|
||||||
await item.delete();
|
await item.delete();
|
||||||
this.items.remove(item);
|
this.items.remove(item);
|
||||||
|
|||||||
@ -27,9 +27,9 @@ import { autoBind, formatDuration } from "../utils";
|
|||||||
import type { ItemObject } from "../item.store";
|
import type { ItemObject } from "../item.store";
|
||||||
import { apiKube } from "./index";
|
import { apiKube } from "./index";
|
||||||
import type { JsonApiParams } from "./json-api";
|
import type { JsonApiParams } from "./json-api";
|
||||||
import { resourceApplierApi } from "./endpoints/resource-applier.api";
|
import * as resourceApplierApi from "./endpoints/resource-applier.api";
|
||||||
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
|
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
|
||||||
import _ from "lodash";
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
|
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
|
||||||
kind?: string;
|
kind?: string;
|
||||||
@ -98,6 +98,12 @@ export interface KubeObjectStatus {
|
|||||||
|
|
||||||
export type KubeMetaField = keyof KubeObjectMetadata;
|
export type KubeMetaField = keyof KubeObjectMetadata;
|
||||||
|
|
||||||
|
export class KubeCreationError extends Error {
|
||||||
|
constructor(message: string, public data: any) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
|
export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata, Status = any, Spec = any> implements ItemObject {
|
||||||
static readonly kind: string;
|
static readonly kind: string;
|
||||||
static readonly namespaced: boolean;
|
static readonly namespaced: boolean;
|
||||||
@ -191,18 +197,32 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static readonly nonEditableFields = [
|
/**
|
||||||
"apiVersion",
|
* These must be RFC6902 compliant paths
|
||||||
"kind",
|
*/
|
||||||
"metadata.name",
|
private static readonly nonEditiablePathPrefixes = [
|
||||||
"metadata.selfLink",
|
"/metadata/managedFields",
|
||||||
"metadata.resourceVersion",
|
"/status",
|
||||||
"metadata.uid",
|
|
||||||
"managedFields",
|
|
||||||
"status",
|
|
||||||
];
|
];
|
||||||
|
private static readonly nonEditablePaths = new Set([
|
||||||
|
"/apiVersion",
|
||||||
|
"/kind",
|
||||||
|
"/metadata/name",
|
||||||
|
"/metadata/selfLink",
|
||||||
|
"/metadata/resourceVersion",
|
||||||
|
"/metadata/uid",
|
||||||
|
...KubeObject.nonEditiablePathPrefixes,
|
||||||
|
]);
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
constructor(data: KubeJsonApiData) {
|
||||||
|
if (typeof data !== "object") {
|
||||||
|
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.metadata || typeof data.metadata !== "object") {
|
||||||
|
throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata`, data);
|
||||||
|
}
|
||||||
|
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
}
|
}
|
||||||
@ -264,7 +284,7 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOwnerRefs() {
|
getOwnerRefs() {
|
||||||
const refs = this.metadata?.ownerReferences || [];
|
const refs = this.metadata.ownerReferences || [];
|
||||||
const namespace = this.getNs();
|
const namespace = this.getNs();
|
||||||
|
|
||||||
return refs.map(ownerRef => ({ ...ownerRef, namespace }));
|
return refs.map(ownerRef => ({ ...ownerRef, namespace }));
|
||||||
@ -286,14 +306,31 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
return JSON.parse(JSON.stringify(this));
|
return JSON.parse(JSON.stringify(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// use unified resource-applier api for updating all k8s objects
|
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
|
||||||
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
for (const op of patch) {
|
||||||
for (const field of KubeObject.nonEditableFields) {
|
if (KubeObject.nonEditablePaths.has(op.path)) {
|
||||||
if (!_.isEqual(_.get(this, field), _.get(data, field))) {
|
throw new Error(`Failed to update ${this.kind}: JSON pointer ${op.path} has been modified`);
|
||||||
throw new Error(`Failed to update Kube Object: ${field} has been modified`);
|
}
|
||||||
|
|
||||||
|
for (const pathPrefix of KubeObject.nonEditiablePathPrefixes) {
|
||||||
|
if (op.path.startsWith(`${pathPrefix}/`)) {
|
||||||
|
throw new Error(`Failed to update ${this.kind}: Child JSON pointer of ${op.path} has been modified`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resourceApplierApi.patch(this.getName(), this.kind, this.getNs(), patch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a full update (or more specifically a replace)
|
||||||
|
*
|
||||||
|
* Note: this is brittle if `data` is not actually partial (but instead whole).
|
||||||
|
* As fields such as `resourceVersion` will probably out of date. This is a
|
||||||
|
* common race condition.
|
||||||
|
*/
|
||||||
|
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
||||||
|
// use unified resource-applier api for updating all k8s objects
|
||||||
return resourceApplierApi.update({
|
return resourceApplierApi.update({
|
||||||
...this.toPlainObject(),
|
...this.toPlainObject(),
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@ -25,11 +25,11 @@ import path from "path";
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
import commandExists from "command-exists";
|
||||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||||
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
||||||
import { resolvePath } from "./utils";
|
import { resolvePath } from "./utils";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
import which from "which";
|
|
||||||
|
|
||||||
export type KubeConfigValidationOpts = {
|
export type KubeConfigValidationOpts = {
|
||||||
validateCluster?: boolean;
|
validateCluster?: boolean;
|
||||||
@ -295,17 +295,13 @@ export function validateKubeConfig(config: KubeConfig, contextName: string, vali
|
|||||||
|
|
||||||
// Validate exec command if present
|
// Validate exec command if present
|
||||||
if (validateExec && user?.exec) {
|
if (validateExec && user?.exec) {
|
||||||
try {
|
const execCommand = user.exec["command"];
|
||||||
which.sync(user.exec.command);
|
// check if the command is absolute or not
|
||||||
|
const isAbsolute = path.isAbsolute(execCommand);
|
||||||
|
|
||||||
// If this doesn't throw an error it also means that it has found the executable.
|
// validate the exec struct in the user object, start with the command field
|
||||||
} catch (error) {
|
if (!commandExists.sync(execCommand)) {
|
||||||
switch (error?.code) {
|
return new ExecValidationNotFoundError(execCommand, isAbsolute);
|
||||||
case "ENOENT":
|
|
||||||
return new ExecValidationNotFoundError(user.exec.command);
|
|
||||||
default:
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -59,9 +59,6 @@ if (ipcMain) {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
transports.push(new BrowserConsole());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTestEnv) {
|
if (!isTestEnv) {
|
||||||
transports.push(
|
transports.push(
|
||||||
@ -76,6 +73,9 @@ if (!isTestEnv) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
transports.push(new BrowserConsole());
|
||||||
|
}
|
||||||
|
|
||||||
export default winston.createLogger({
|
export default winston.createLogger({
|
||||||
format: format.simple(),
|
format: format.simple(),
|
||||||
|
|||||||
@ -30,4 +30,17 @@ export const catalogRoute: RouteProps = {
|
|||||||
path: "/catalog/:group?/:kind?"
|
path: "/catalog/:group?/:kind?"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getPreviousTabUrl = (path: string) => {
|
||||||
|
const [group, kind] = path.split("/");
|
||||||
|
|
||||||
|
return catalogURL({
|
||||||
|
params: {
|
||||||
|
group: group || browseCatalogTab,
|
||||||
|
kind
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const catalogURL = buildURL<CatalogViewRouteParam>(catalogRoute.path);
|
export const catalogURL = buildURL<CatalogViewRouteParam>(catalogRoute.path);
|
||||||
|
|
||||||
|
export const browseCatalogTab = "browse";
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export * from "./network-policies";
|
|||||||
export * from "./network";
|
export * from "./network";
|
||||||
export * from "./nodes";
|
export * from "./nodes";
|
||||||
export * from "./pod-disruption-budgets";
|
export * from "./pod-disruption-budgets";
|
||||||
|
export * from "./port-forwards";
|
||||||
export * from "./preferences";
|
export * from "./preferences";
|
||||||
export * from "./releases";
|
export * from "./releases";
|
||||||
export * from "./resource-quotas";
|
export * from "./resource-quotas";
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { endpointRoute } from "./endpoints";
|
|||||||
import { ingressRoute } from "./ingresses";
|
import { ingressRoute } from "./ingresses";
|
||||||
import { networkPoliciesRoute } from "./network-policies";
|
import { networkPoliciesRoute } from "./network-policies";
|
||||||
import { servicesRoute, servicesURL } from "./services";
|
import { servicesRoute, servicesURL } from "./services";
|
||||||
|
import { portForwardsRoute } from "./port-forwards";
|
||||||
|
|
||||||
export const networkRoute: RouteProps = {
|
export const networkRoute: RouteProps = {
|
||||||
path: [
|
path: [
|
||||||
@ -32,6 +33,7 @@ export const networkRoute: RouteProps = {
|
|||||||
endpointRoute,
|
endpointRoute,
|
||||||
ingressRoute,
|
ingressRoute,
|
||||||
networkPoliciesRoute,
|
networkPoliciesRoute,
|
||||||
|
portForwardsRoute,
|
||||||
].map(route => route.path.toString())
|
].map(route => route.path.toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -19,26 +19,14 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.EntitySettings {
|
import type { RouteProps } from "react-router";
|
||||||
$spacing: $padding * 3;
|
import { buildURL } from "../utils/buildUrl";
|
||||||
|
|
||||||
|
export const portForwardsRoute: RouteProps = {
|
||||||
|
path: "/port-forwards"
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: move sub-component styles to separate files
|
export interface PortForwardsRouteParams {
|
||||||
.admin-note {
|
|
||||||
font-size: small;
|
|
||||||
opacity: 0.5;
|
|
||||||
margin-left: $margin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-area {
|
export const portForwardsURL = buildURL<PortForwardsRouteParams>(portForwardsRoute.path);
|
||||||
margin-top: $margin * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-loader {
|
|
||||||
margin-top: $margin * 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input, .Select {
|
|
||||||
margin-top: $padding;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -200,6 +200,19 @@ const openAtLogin: PreferenceDescription<boolean> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const terminalCopyOnSelect: PreferenceDescription<boolean> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return val ?? false;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!val) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, ObservableToggleSet<string>>> = {
|
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, ObservableToggleSet<string>>> = {
|
||||||
fromStore(val) {
|
fromStore(val) {
|
||||||
return new Map(
|
return new Map(
|
||||||
@ -274,4 +287,5 @@ export const DESCRIPTORS = {
|
|||||||
hiddenTableColumns,
|
hiddenTableColumns,
|
||||||
syncKubeconfigEntries,
|
syncKubeconfigEntries,
|
||||||
editorConfiguration,
|
editorConfiguration,
|
||||||
|
terminalCopyOnSelect,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||||
import { BaseStore } from "../base-store";
|
import { BaseStore } from "../base-store";
|
||||||
@ -48,7 +48,11 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
});
|
});
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
|
||||||
|
if (ipcMain) {
|
||||||
fileNameMigration();
|
fileNameMigration();
|
||||||
|
}
|
||||||
|
|
||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,6 +74,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
@observable shell?: string;
|
@observable shell?: string;
|
||||||
@observable downloadBinariesPath?: string;
|
@observable downloadBinariesPath?: string;
|
||||||
@observable kubectlBinariesPath?: string;
|
@observable kubectlBinariesPath?: string;
|
||||||
|
@observable terminalCopyOnSelect: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download kubectl binaries matching cluster version
|
* Download kubectl binaries matching cluster version
|
||||||
@ -212,6 +217,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns));
|
this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns));
|
||||||
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
||||||
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
||||||
|
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): UserStoreModel {
|
toJSON(): UserStoreModel {
|
||||||
@ -233,6 +239,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns),
|
hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns),
|
||||||
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||||
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
||||||
|
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -33,8 +33,9 @@ export function getBundledKubectlVersion(): string {
|
|||||||
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
|
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
|
||||||
const response = await requestPromise({
|
const response = await requestPromise({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
uri: `http://localhost:${proxyPort}/version`,
|
uri: `http://127.0.0.1:${proxyPort}/version`,
|
||||||
resolveWithFullResponse: true
|
resolveWithFullResponse: true,
|
||||||
|
proxy: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return JSON.parse(response.body).version;
|
return JSON.parse(response.body).version;
|
||||||
|
|||||||
@ -69,7 +69,7 @@ export const apiKubePrefix = "/api-kube" as string; // k8s cluster apis
|
|||||||
|
|
||||||
// Links
|
// Links
|
||||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
||||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
|
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string;
|
||||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||||
|
|
||||||
export const appSemVer = new SemVer(packageInfo.version);
|
export const appSemVer = new SemVer(packageInfo.version);
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a filtering function for the catalog catogries. This will be removed if the extension is disabled.
|
* Add a filtering function for the catalog categories. This will be removed if the extension is disabled.
|
||||||
* @param fn The function which should return a truthy value for those categories which should be kept.
|
* @param fn The function which should return a truthy value for those categories which should be kept.
|
||||||
* @returns A function to clean up the filter
|
* @returns A function to clean up the filter
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -16,9 +16,9 @@
|
|||||||
"name": "OpenLens Authors"
|
"name": "OpenLens Authors"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "14.17.14",
|
||||||
"@types/react-select": "*",
|
"@types/react-select": "3.1.2",
|
||||||
"@material-ui/core": "*",
|
"@material-ui/core": "4.12.3",
|
||||||
"conf": "^7.0.1",
|
"conf": "^7.0.1",
|
||||||
"typed-emitter": "^1.3.1"
|
"typed-emitter": "^1.3.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,9 +25,9 @@ import { BaseRegistry } from "./base-registry";
|
|||||||
import type { LensExtension } from "../lens-extension";
|
import type { LensExtension } from "../lens-extension";
|
||||||
import type { CatalogEntity } from "../../common/catalog";
|
import type { CatalogEntity } from "../../common/catalog";
|
||||||
|
|
||||||
export type CommandContext = {
|
export interface CommandContext {
|
||||||
entity?: CatalogEntity;
|
entity?: CatalogEntity;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface CommandRegistration {
|
export interface CommandRegistration {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@ -22,7 +22,8 @@
|
|||||||
|
|
||||||
import type { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
import type { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||||
import { catalogEntityRegistry as registry } from "../../renderer/api/catalog-entity-registry";
|
import { catalogEntityRegistry as registry } from "../../renderer/api/catalog-entity-registry";
|
||||||
|
import type { CatalogEntityOnBeforeRun } from "../../renderer/api/catalog-entity-registry";
|
||||||
|
import type { Disposer } from "../../common/utils";
|
||||||
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
|
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog/catalog-category-registry";
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@ -48,6 +49,18 @@ export class CatalogEntityRegistry {
|
|||||||
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
|
getItemsForCategory<T extends CatalogEntity>(category: CatalogCategory): T[] {
|
||||||
return registry.getItemsForCategory(category);
|
return registry.getItemsForCategory(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a onBeforeRun hook to a catalog entities. If `onBeforeRun` was previously
|
||||||
|
* added then it will not be added again.
|
||||||
|
* @param onBeforeRun The function to be called with a `CatalogRunEvent`
|
||||||
|
* event target will be the catalog entity. onBeforeRun hook can call event.preventDefault()
|
||||||
|
* to stop run sequence
|
||||||
|
* @returns A function to remove that hook
|
||||||
|
*/
|
||||||
|
addOnBeforeRun(onBeforeRun: CatalogEntityOnBeforeRun): Disposer {
|
||||||
|
return registry.addOnBeforeRun(onBeforeRun);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const catalogEntities = new CatalogEntityRegistry();
|
export const catalogEntities = new CatalogEntityRegistry();
|
||||||
|
|||||||
@ -21,10 +21,17 @@
|
|||||||
|
|
||||||
import fetchMock from "jest-fetch-mock";
|
import fetchMock from "jest-fetch-mock";
|
||||||
import configurePackages from "./common/configure-packages";
|
import configurePackages from "./common/configure-packages";
|
||||||
|
import { configure } from "mobx";
|
||||||
|
|
||||||
// setup default configuration for external npm-packages
|
// setup default configuration for external npm-packages
|
||||||
configurePackages();
|
configurePackages();
|
||||||
|
|
||||||
|
configure({
|
||||||
|
// Needed because we want to use jest.spyOn()
|
||||||
|
// ref https://github.com/mobxjs/mobx/issues/2784
|
||||||
|
safeDescriptors: false,
|
||||||
|
});
|
||||||
|
|
||||||
// rewire global.fetch to call 'fetchMock'
|
// rewire global.fetch to call 'fetchMock'
|
||||||
fetchMock.enableMocks();
|
fetchMock.enableMocks();
|
||||||
|
|
||||||
|
|||||||
56
src/main/__test__/lens-proxy.test.ts
Normal file
56
src/main/__test__/lens-proxy.test.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { isLongRunningRequest } from "../lens-proxy";
|
||||||
|
|
||||||
|
describe("isLongRunningRequest", () => {
|
||||||
|
it("returns true on watches", () => {
|
||||||
|
["watch=true", "watch=1", "watch"].forEach((param) => {
|
||||||
|
expect(
|
||||||
|
isLongRunningRequest(`/api/v1/namespaces/default/pods?${param}`)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false on disabled watches", () => {
|
||||||
|
["watch=false", "watch=0", ""].forEach((param) => {
|
||||||
|
expect(
|
||||||
|
isLongRunningRequest(`/api/v1/namespaces/default/pods?${param}`)
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true on follows", () => {
|
||||||
|
["follow=true", "follow=1", "follow"].forEach((param) => {
|
||||||
|
expect(
|
||||||
|
isLongRunningRequest(`/api/v1/namespaces/default/pods/foo/log?${param}`)
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false on disabled follows", () => {
|
||||||
|
["follow=false", "follow=0", ""].forEach((param) => {
|
||||||
|
expect(
|
||||||
|
isLongRunningRequest(`/api/v1/namespaces/default/pods/foo/log?${param}`)
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -131,11 +131,11 @@ export class ClusterManager extends Singleton {
|
|||||||
entity.spec.metrics.prometheus = prometheus;
|
entity.spec.metrics.prometheus = prometheus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only set the icon if the preference is set. If the preference is not set
|
||||||
|
// then let the source determine if a cluster has an icon.
|
||||||
if (cluster.preferences.icon) {
|
if (cluster.preferences.icon) {
|
||||||
entity.spec.icon ??= {};
|
entity.spec.icon ??= {};
|
||||||
entity.spec.icon.src = cluster.preferences.icon;
|
entity.spec.icon.src = cluster.preferences.icon;
|
||||||
} else {
|
|
||||||
entity.spec.icon = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogEntityRegistry.items.splice(index, 1, entity);
|
catalogEntityRegistry.items.splice(index, 1, entity);
|
||||||
|
|||||||
@ -117,10 +117,10 @@ export class ContextHandler {
|
|||||||
return `http://127.0.0.1:${this.kubeAuthProxy.port}${path}`;
|
return `http://127.0.0.1:${this.kubeAuthProxy.port}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getApiTarget(isWatchRequest = false): Promise<httpProxy.ServerOptions> {
|
async getApiTarget(isLongRunningRequest = false): Promise<httpProxy.ServerOptions> {
|
||||||
const timeout = isWatchRequest ? 4 * 60 * 60 * 1000 : 30000; // 4 hours for watch request, 30 seconds for the rest
|
const timeout = isLongRunningRequest ? 4 * 60 * 60_000 : 30_000; // 4 hours for long running request, 30 seconds for the rest
|
||||||
|
|
||||||
if (isWatchRequest) {
|
if (isLongRunningRequest) {
|
||||||
return this.newApiTarget(timeout);
|
return this.newApiTarget(timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import { Singleton } from "../common/utils";
|
|||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
import type { ProxyApiRequestArgs } from "./proxy-functions";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
|
import { getBoolean } from "./utils/parse-query";
|
||||||
|
|
||||||
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
|
type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null;
|
||||||
|
|
||||||
@ -40,6 +41,15 @@ export interface LensProxyFunctions {
|
|||||||
kubeApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
kubeApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const watchParam = "watch";
|
||||||
|
const followParam = "follow";
|
||||||
|
|
||||||
|
export function isLongRunningRequest(reqUrl: string) {
|
||||||
|
const url = new URL(reqUrl, "http://localhost");
|
||||||
|
|
||||||
|
return getBoolean(url.searchParams, watchParam) || getBoolean(url.searchParams, followParam);
|
||||||
|
}
|
||||||
|
|
||||||
export class LensProxy extends Singleton {
|
export class LensProxy extends Singleton {
|
||||||
protected origin: string;
|
protected origin: string;
|
||||||
protected proxyServer: http.Server;
|
protected proxyServer: http.Server;
|
||||||
@ -174,9 +184,8 @@ export class LensProxy extends Singleton {
|
|||||||
if (req.url.startsWith(apiKubePrefix)) {
|
if (req.url.startsWith(apiKubePrefix)) {
|
||||||
delete req.headers.authorization;
|
delete req.headers.authorization;
|
||||||
req.url = req.url.replace(apiKubePrefix, "");
|
req.url = req.url.replace(apiKubePrefix, "");
|
||||||
const isWatchRequest = req.url.includes("watch=");
|
|
||||||
|
|
||||||
return contextHandler.getApiTarget(isWatchRequest);
|
return contextHandler.getApiTarget(isLongRunningRequest(req.url));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
114
src/main/menu.ts
114
src/main/menu.ts
@ -19,19 +19,23 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, BrowserWindow, dialog, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import type { WindowManager } from "./window-manager";
|
import type { WindowManager } from "./window-manager";
|
||||||
import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl, productName } from "../common/vars";
|
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../common/vars";
|
||||||
import { MenuRegistry } from "../extensions/registries/menu-registry";
|
import { MenuRegistry } from "../extensions/registries/menu-registry";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "./exit-app";
|
||||||
import { broadcastMessage, ipcMainOn } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import * as packageJson from "../../package.json";
|
import * as packageJson from "../../package.json";
|
||||||
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../common/routes";
|
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../common/routes";
|
||||||
|
|
||||||
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
|
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
|
||||||
|
|
||||||
|
interface MenuItemsOpts extends MenuItemConstructorOptions {
|
||||||
|
submenu?: MenuItemConstructorOptions[];
|
||||||
|
}
|
||||||
|
|
||||||
export function initMenu(windowManager: WindowManager) {
|
export function initMenu(windowManager: WindowManager) {
|
||||||
return autorun(() => buildMenu(windowManager), {
|
return autorun(() => buildMenu(windowManager), {
|
||||||
delay: 100
|
delay: 100
|
||||||
@ -68,11 +72,13 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
await windowManager.navigate(url);
|
await windowManager.navigate(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const macAppMenu: MenuItemConstructorOptions = {
|
const macAppMenu: MenuItemsOpts = {
|
||||||
label: app.getName(),
|
label: app.getName(),
|
||||||
|
id: "root",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: `About ${productName}`,
|
label: `About ${productName}`,
|
||||||
|
id: "about",
|
||||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||||
showAbout(browserWindow);
|
showAbout(browserWindow);
|
||||||
}
|
}
|
||||||
@ -81,6 +87,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Preferences",
|
label: "Preferences",
|
||||||
accelerator: "CmdOrCtrl+,",
|
accelerator: "CmdOrCtrl+,",
|
||||||
|
id: "preferences",
|
||||||
click() {
|
click() {
|
||||||
navigate(preferencesURL());
|
navigate(preferencesURL());
|
||||||
},
|
},
|
||||||
@ -88,6 +95,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Extensions",
|
label: "Extensions",
|
||||||
accelerator: "CmdOrCtrl+Shift+E",
|
accelerator: "CmdOrCtrl+Shift+E",
|
||||||
|
id: "extensions",
|
||||||
click() {
|
click() {
|
||||||
navigate(extensionsURL());
|
navigate(extensionsURL());
|
||||||
}
|
}
|
||||||
@ -102,18 +110,21 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Quit",
|
label: "Quit",
|
||||||
accelerator: "Cmd+Q",
|
accelerator: "Cmd+Q",
|
||||||
|
id: "quit",
|
||||||
click() {
|
click() {
|
||||||
exitApp();
|
exitApp();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const fileMenu: MenuItemConstructorOptions = {
|
const fileMenu: MenuItemsOpts = {
|
||||||
label: "File",
|
label: "File",
|
||||||
|
id: "file",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: "Add Cluster",
|
label: "Add Cluster",
|
||||||
accelerator: "CmdOrCtrl+Shift+A",
|
accelerator: "CmdOrCtrl+Shift+A",
|
||||||
|
id: "add-cluster",
|
||||||
click() {
|
click() {
|
||||||
navigate(addClusterURL());
|
navigate(addClusterURL());
|
||||||
}
|
}
|
||||||
@ -122,6 +133,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
{
|
||||||
label: "Preferences",
|
label: "Preferences",
|
||||||
|
id: "preferences",
|
||||||
accelerator: "Ctrl+,",
|
accelerator: "Ctrl+,",
|
||||||
click() {
|
click() {
|
||||||
navigate(preferencesURL());
|
navigate(preferencesURL());
|
||||||
@ -150,6 +162,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Exit",
|
label: "Exit",
|
||||||
accelerator: "Alt+F4",
|
accelerator: "Alt+F4",
|
||||||
|
id: "quit",
|
||||||
click() {
|
click() {
|
||||||
exitApp();
|
exitApp();
|
||||||
}
|
}
|
||||||
@ -157,8 +170,9 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
])
|
])
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const editMenu: MenuItemConstructorOptions = {
|
const editMenu: MenuItemsOpts = {
|
||||||
label: "Edit",
|
label: "Edit",
|
||||||
|
id: "edit",
|
||||||
submenu: [
|
submenu: [
|
||||||
{ role: "undo" },
|
{ role: "undo" },
|
||||||
{ role: "redo" },
|
{ role: "redo" },
|
||||||
@ -171,12 +185,14 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ role: "selectAll" },
|
{ role: "selectAll" },
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
const viewMenu: MenuItemConstructorOptions = {
|
const viewMenu: MenuItemsOpts = {
|
||||||
label: "View",
|
label: "View",
|
||||||
|
id: "view",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: "Catalog",
|
label: "Catalog",
|
||||||
accelerator: "Shift+CmdOrCtrl+C",
|
accelerator: "Shift+CmdOrCtrl+C",
|
||||||
|
id: "catalog",
|
||||||
click() {
|
click() {
|
||||||
navigate(catalogURL());
|
navigate(catalogURL());
|
||||||
}
|
}
|
||||||
@ -184,6 +200,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Command Palette...",
|
label: "Command Palette...",
|
||||||
accelerator: "Shift+CmdOrCtrl+P",
|
accelerator: "Shift+CmdOrCtrl+P",
|
||||||
|
id: "command-palette",
|
||||||
click() {
|
click() {
|
||||||
broadcastMessage("command-palette:open");
|
broadcastMessage("command-palette:open");
|
||||||
}
|
}
|
||||||
@ -192,6 +209,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Back",
|
label: "Back",
|
||||||
accelerator: "CmdOrCtrl+[",
|
accelerator: "CmdOrCtrl+[",
|
||||||
|
id: "go-back",
|
||||||
click() {
|
click() {
|
||||||
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goBack());
|
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goBack());
|
||||||
}
|
}
|
||||||
@ -199,6 +217,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Forward",
|
label: "Forward",
|
||||||
accelerator: "CmdOrCtrl+]",
|
accelerator: "CmdOrCtrl+]",
|
||||||
|
id: "go-forward",
|
||||||
click() {
|
click() {
|
||||||
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goForward());
|
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goForward());
|
||||||
}
|
}
|
||||||
@ -206,6 +225,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{
|
{
|
||||||
label: "Reload",
|
label: "Reload",
|
||||||
accelerator: "CmdOrCtrl+R",
|
accelerator: "CmdOrCtrl+R",
|
||||||
|
id: "reload",
|
||||||
click() {
|
click() {
|
||||||
windowManager.reload();
|
windowManager.reload();
|
||||||
}
|
}
|
||||||
@ -219,23 +239,27 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ role: "togglefullscreen" }
|
{ role: "togglefullscreen" }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
const helpMenu: MenuItemConstructorOptions = {
|
const helpMenu: MenuItemsOpts = {
|
||||||
role: "help",
|
role: "help",
|
||||||
|
id: "help",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: "Welcome",
|
label: "Welcome",
|
||||||
|
id: "welcome",
|
||||||
click() {
|
click() {
|
||||||
navigate(welcomeURL());
|
navigate(welcomeURL());
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Documentation",
|
label: "Documentation",
|
||||||
|
id: "documentation",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
shell.openExternal(docsUrl);
|
shell.openExternal(docsUrl);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Support",
|
label: "Support",
|
||||||
|
id: "support",
|
||||||
click: async () => {
|
click: async () => {
|
||||||
shell.openExternal(supportUrl);
|
shell.openExternal(supportUrl);
|
||||||
},
|
},
|
||||||
@ -243,6 +267,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
...ignoreOnMac([
|
...ignoreOnMac([
|
||||||
{
|
{
|
||||||
label: `About ${productName}`,
|
label: `About ${productName}`,
|
||||||
|
id: "about",
|
||||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||||
showAbout(browserWindow);
|
showAbout(browserWindow);
|
||||||
}
|
}
|
||||||
@ -251,69 +276,28 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
// Prepare menu items order
|
// Prepare menu items order
|
||||||
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
|
const appMenu = new Map([
|
||||||
mac: macAppMenu,
|
["mac", macAppMenu],
|
||||||
file: fileMenu,
|
["file", fileMenu],
|
||||||
edit: editMenu,
|
["edit", editMenu],
|
||||||
view: viewMenu,
|
["view", viewMenu],
|
||||||
help: helpMenu,
|
["help", helpMenu],
|
||||||
};
|
]);
|
||||||
|
|
||||||
// Modify menu from extensions-api
|
// Modify menu from extensions-api
|
||||||
MenuRegistry.getInstance().getItems().forEach(({ parentId, ...menuItem }) => {
|
for (const { parentId, ...menuItem } of MenuRegistry.getInstance().getItems()) {
|
||||||
try {
|
if (!appMenu.has(parentId)) {
|
||||||
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
|
logger.error(`[MENU]: cannot register menu item for parentId=${parentId}, parent item doesn't exist`, { menuItem });
|
||||||
|
|
||||||
topMenu.push(menuItem);
|
continue;
|
||||||
} catch (err) {
|
}
|
||||||
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem });
|
|
||||||
|
appMenu.get(parentId).submenu.push(menuItem);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (!isMac) {
|
if (!isMac) {
|
||||||
delete appMenu.mac;
|
appMenu.delete("mac");
|
||||||
}
|
}
|
||||||
|
|
||||||
const menu = Menu.buildFromTemplate(Object.values(appMenu));
|
Menu.setApplicationMenu(Menu.buildFromTemplate([...appMenu.values()]));
|
||||||
|
|
||||||
Menu.setApplicationMenu(menu);
|
|
||||||
|
|
||||||
if (isTestEnv) {
|
|
||||||
// this is a workaround for the test environment (spectron) not being able to directly access
|
|
||||||
// the application menus (https://github.com/electron-userland/spectron/issues/21)
|
|
||||||
ipcMainOn("test-menu-item-click", (event: IpcMainEvent, ...names: string[]) => {
|
|
||||||
let menu: Menu = Menu.getApplicationMenu();
|
|
||||||
const parentLabels: string[] = [];
|
|
||||||
let menuItem: MenuItem;
|
|
||||||
|
|
||||||
for (const name of names) {
|
|
||||||
parentLabels.push(name);
|
|
||||||
menuItem = menu?.items?.find(item => item.label === name);
|
|
||||||
|
|
||||||
if (!menuItem) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
menu = menuItem.submenu;
|
|
||||||
}
|
|
||||||
|
|
||||||
const menuPath: string = parentLabels.join(" -> ");
|
|
||||||
|
|
||||||
if (!menuItem) {
|
|
||||||
logger.info(`[MENU:test-menu-item-click] Cannot find menu item ${menuPath}`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { enabled, visible, click } = menuItem;
|
|
||||||
|
|
||||||
if (enabled === false || visible === false || typeof click !== "function") {
|
|
||||||
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} not clickable`);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[MENU:test-menu-item-click] Menu item ${menuPath} click!`);
|
|
||||||
menuItem.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,9 +38,9 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "cluster":
|
case "cluster":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",instance=~"${opts.nodes}"}) by (component)`;
|
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="memory"})`;
|
||||||
case "memoryLimits":
|
case "memoryLimits":
|
||||||
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
||||||
case "cpuRequests":
|
case "cpuRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||||
case "cpuLimits":
|
case "cpuLimits":
|
||||||
@ -66,49 +66,49 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "podAllocatableCapacity":
|
case "podAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "nodes":
|
case "nodes":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!="POD",container!=""}) by (node)`;
|
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
|
||||||
case "cpuCapacity":
|
case "cpuCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "cpuAllocatableCapacity":
|
case "cpuAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "pods":
|
case "pods":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(container_cpu_usage_seconds_total{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
return `sum(rate(container_cpu_usage_seconds_total{container!="", image!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||||
case "cpuRequests":
|
case "cpuRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}", resource="cpu", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}", resource="cpu", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||||
case "cpuLimits":
|
case "cpuLimits":
|
||||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}", resource="cpu", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}", resource="cpu", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!="POD",container!="",image!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
return `sum(container_memory_working_set_bytes{container!="", image!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}", resource="memory", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
return `sum(kube_pod_container_resource_requests{pod=~"${opts.pods}", resource="memory", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||||
case "memoryLimits":
|
case "memoryLimits":
|
||||||
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}", resource="memory", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
return `sum(kube_pod_container_resource_limits{pod=~"${opts.pods}", resource="memory", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(container_fs_usage_bytes{container!="POD",container!="",pod=~"${opts.pods}",namespace="${opts.namespace}"}) by (${opts.selector})`;
|
return `sum(container_fs_usage_bytes{container!="", pod=~"${opts.pods}", namespace="${opts.namespace}"}) by (${opts.selector})`;
|
||||||
case "networkReceive":
|
case "networkReceive":
|
||||||
return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
return `sum(rate(container_network_receive_bytes_total{pod=~"${opts.pods}", namespace="${opts.namespace}"}[${this.rateAccuracy}])) by (${opts.selector})`;
|
||||||
case "networkTransmit":
|
case "networkTransmit":
|
||||||
|
|||||||
@ -126,7 +126,7 @@ describe("protocol router tests", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await delay(50);
|
await delay(50);
|
||||||
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app/", "matched");
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched");
|
||||||
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
expect(broadcastMessage).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -22,55 +22,96 @@
|
|||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
import type { KubernetesObject } from "@kubernetes/client-node";
|
import type { KubernetesObject } from "@kubernetes/client-node";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import fs from "fs";
|
import fs from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as tempy from "tempy";
|
import * as tempy from "tempy";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { cloneJsonObject } from "../common/utils";
|
import { cloneJsonObject } from "../common/utils";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
import { promiseExecFile } from "./promise-exec";
|
||||||
|
|
||||||
export class ResourceApplier {
|
export class ResourceApplier {
|
||||||
constructor(protected cluster: Cluster) {
|
constructor(protected cluster: Cluster) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch a kube resource's manifest, throwing any error that occurs.
|
||||||
|
* @param name The name of the kube resource
|
||||||
|
* @param kind The kind of the kube resource
|
||||||
|
* @param patch The list of JSON operations
|
||||||
|
* @param ns The optional namespace of the kube resource
|
||||||
|
*/
|
||||||
|
async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> {
|
||||||
|
appEventBus.emit({ name: "resource", action: "patch" });
|
||||||
|
|
||||||
|
const kubectl = await this.cluster.ensureKubectl();
|
||||||
|
const kubectlPath = await kubectl.getPath();
|
||||||
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
const args = [
|
||||||
|
"--kubeconfig", proxyKubeconfigPath,
|
||||||
|
"patch",
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ns) {
|
||||||
|
args.push("--namespace", ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(
|
||||||
|
"--type", "json",
|
||||||
|
"--patch", JSON.stringify(patch),
|
||||||
|
"-o", "json"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await promiseExecFile(kubectlPath, args);
|
||||||
|
|
||||||
|
return stdout;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.stderr ?? error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||||
resource = this.sanitizeObject(resource);
|
resource = this.sanitizeObject(resource);
|
||||||
appEventBus.emit({ name: "resource", action: "apply" });
|
appEventBus.emit({ name: "resource", action: "apply" });
|
||||||
|
|
||||||
return await this.kubectlApply(yaml.safeDump(resource));
|
return this.kubectlApply(yaml.safeDump(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlApply(content: string): Promise<string> {
|
protected async kubectlApply(content: string): Promise<string> {
|
||||||
const kubectl = await this.cluster.ensureKubectl();
|
const kubectl = await this.cluster.ensureKubectl();
|
||||||
const kubectlPath = await kubectl.getPath();
|
const kubectlPath = await kubectl.getPath();
|
||||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
|
||||||
const fileName = tempy.file({ name: "resource.yaml" });
|
const fileName = tempy.file({ name: "resource.yaml" });
|
||||||
|
const args = [
|
||||||
|
"apply",
|
||||||
|
"--kubeconfig", proxyKubeconfigPath,
|
||||||
|
"-o", "json",
|
||||||
|
"-f", fileName,
|
||||||
|
];
|
||||||
|
|
||||||
fs.writeFileSync(fileName, content);
|
logger.debug(`shooting manifests with ${kubectlPath}`, { args });
|
||||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${fileName}"`;
|
|
||||||
|
|
||||||
logger.debug(`shooting manifests with: ${cmd}`);
|
const execEnv = { ...process.env };
|
||||||
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
|
|
||||||
const httpsProxy = this.cluster.preferences?.httpsProxy;
|
const httpsProxy = this.cluster.preferences?.httpsProxy;
|
||||||
|
|
||||||
if (httpsProxy) {
|
if (httpsProxy) {
|
||||||
execEnv["HTTPS_PROXY"] = httpsProxy;
|
execEnv.HTTPS_PROXY = httpsProxy;
|
||||||
}
|
}
|
||||||
exec(cmd, { env: execEnv },
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (stderr != "") {
|
|
||||||
fs.unlinkSync(fileName);
|
|
||||||
reject(stderr);
|
|
||||||
|
|
||||||
return;
|
try {
|
||||||
|
await fs.writeFile(fileName, content);
|
||||||
|
const { stdout } = await promiseExecFile(kubectlPath, args);
|
||||||
|
|
||||||
|
return stdout;
|
||||||
|
} catch (error) {
|
||||||
|
throw error?.stderr ?? error;
|
||||||
|
} finally {
|
||||||
|
await fs.unlink(fileName);
|
||||||
}
|
}
|
||||||
fs.unlinkSync(fileName);
|
|
||||||
resolve(JSON.parse(stdout));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise<string> {
|
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise<string> {
|
||||||
|
|||||||
@ -179,8 +179,11 @@ export class Router {
|
|||||||
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
|
this.router.add({ method: "post", path: `${apiPrefix}/metrics` }, MetricsRoute.routeMetrics);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders);
|
||||||
|
|
||||||
// Port-forward API
|
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/{namespace}/{resourceType}/{resourceName}/port-forward/{port}` }, PortForwardRoute.routePortForward);
|
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routePortForward);
|
||||||
|
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
||||||
|
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forwards` }, PortForwardRoute.routeAllPortForwards);
|
||||||
|
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
||||||
|
|
||||||
// Helm API
|
// Helm API
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, HelmApiRoute.listCharts);
|
this.router.add({ method: "get", path: `${apiPrefix}/v2/charts` }, HelmApiRoute.listCharts);
|
||||||
@ -198,5 +201,6 @@ export class Router {
|
|||||||
|
|
||||||
// Resource Applier API
|
// Resource Applier API
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
|
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
|
||||||
|
this.router.add({ method: "patch", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.patchResource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import type { LensApiRequest } from "../router";
|
|||||||
import { helmService } from "../helm/helm-service";
|
import { helmService } from "../helm/helm-service";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { respondJson, respondText } from "../utils/http-responses";
|
import { respondJson, respondText } from "../utils/http-responses";
|
||||||
import { getBoolean } from "./utils/parse-query";
|
import { getBoolean } from "../utils/parse-query";
|
||||||
|
|
||||||
export class HelmApiRoute {
|
export class HelmApiRoute {
|
||||||
static async listCharts(request: LensApiRequest) {
|
static async listCharts(request: LensApiRequest) {
|
||||||
|
|||||||
@ -22,7 +22,6 @@
|
|||||||
import type { LensApiRequest } from "../router";
|
import type { LensApiRequest } from "../router";
|
||||||
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
|
||||||
import { Kubectl } from "../kubectl";
|
import { Kubectl } from "../kubectl";
|
||||||
import { shell } from "electron";
|
|
||||||
import * as tcpPortUsed from "tcp-port-used";
|
import * as tcpPortUsed from "tcp-port-used";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { getPortFrom } from "../utils/get-port";
|
import { getPortFrom } from "../utils/get-port";
|
||||||
@ -33,7 +32,8 @@ interface PortForwardArgs {
|
|||||||
kind: string;
|
kind: string;
|
||||||
namespace: string;
|
namespace: string;
|
||||||
name: string;
|
name: string;
|
||||||
port: string;
|
port: number;
|
||||||
|
forwardPort: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
const internalPortRegex = /^forwarding from (?<address>.+) ->/i;
|
||||||
@ -56,8 +56,8 @@ class PortForward {
|
|||||||
public kind: string;
|
public kind: string;
|
||||||
public namespace: string;
|
public namespace: string;
|
||||||
public name: string;
|
public name: string;
|
||||||
public port: string;
|
public port: number;
|
||||||
public internalPort?: number;
|
public forwardPort: number;
|
||||||
|
|
||||||
constructor(public kubeConfig: string, args: PortForwardArgs) {
|
constructor(public kubeConfig: string, args: PortForwardArgs) {
|
||||||
this.clusterId = args.clusterId;
|
this.clusterId = args.clusterId;
|
||||||
@ -65,16 +65,17 @@ class PortForward {
|
|||||||
this.namespace = args.namespace;
|
this.namespace = args.namespace;
|
||||||
this.name = args.name;
|
this.name = args.name;
|
||||||
this.port = args.port;
|
this.port = args.port;
|
||||||
|
this.forwardPort = args.forwardPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
const kubectlBin = await Kubectl.bundled().getPath();
|
const kubectlBin = await Kubectl.bundled().getPath(true);
|
||||||
const args = [
|
const args = [
|
||||||
"--kubeconfig", this.kubeConfig,
|
"--kubeconfig", this.kubeConfig,
|
||||||
"port-forward",
|
"port-forward",
|
||||||
"-n", this.namespace,
|
"-n", this.namespace,
|
||||||
`${this.kind}/${this.name}`,
|
`${this.kind}/${this.name}`,
|
||||||
`:${this.port}`
|
`${this.forwardPort ?? ""}:${this.port}`
|
||||||
];
|
];
|
||||||
|
|
||||||
this.process = spawn(kubectlBin, args, {
|
this.process = spawn(kubectlBin, args, {
|
||||||
@ -89,12 +90,15 @@ class PortForward {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.internalPort = await getPortFrom(this.process.stdout, {
|
const internalPort = await getPortFrom(this.process.stdout, {
|
||||||
lineRegex: internalPortRegex,
|
lineRegex: internalPortRegex,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await tcpPortUsed.waitUntilUsed(this.internalPort, 500, 15000);
|
await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000);
|
||||||
|
|
||||||
|
// make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward')
|
||||||
|
this.forwardPort = internalPort;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -104,27 +108,30 @@ class PortForward {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public open() {
|
public async stop() {
|
||||||
shell.openExternal(`http://localhost:${this.internalPort}`)
|
this.process.kill();
|
||||||
.catch(error => logger.error(`[PORT-FORWARD]: failed to open external shell: ${error}`, {
|
|
||||||
clusterId: this.clusterId,
|
|
||||||
port: this.port,
|
|
||||||
kind: this.kind,
|
|
||||||
namespace: this.namespace,
|
|
||||||
name: this.name,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PortForwardRoute {
|
export class PortForwardRoute {
|
||||||
static async routePortForward(request: LensApiRequest) {
|
static async routePortForward(request: LensApiRequest) {
|
||||||
const { params, response, cluster} = request;
|
const { params, query, response, cluster } = request;
|
||||||
const { namespace, port, resourceType, resourceName } = params;
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
|
try {
|
||||||
let portForward = PortForward.getPortforward({
|
let portForward = PortForward.getPortforward({
|
||||||
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
namespace, port
|
namespace, port, forwardPort,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let thePort = 0;
|
||||||
|
|
||||||
|
if (forwardPort > 0 && forwardPort < 65536) {
|
||||||
|
thePort = forwardPort;
|
||||||
|
}
|
||||||
|
|
||||||
if (!portForward) {
|
if (!portForward) {
|
||||||
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`);
|
||||||
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
portForward = new PortForward(await cluster.getProxyKubeconfigPath(), {
|
||||||
@ -133,29 +140,81 @@ export class PortForwardRoute {
|
|||||||
namespace,
|
namespace,
|
||||||
name: resourceName,
|
name: resourceName,
|
||||||
port,
|
port,
|
||||||
|
forwardPort: thePort,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
|
||||||
const started = await portForward.start();
|
const started = await portForward.start();
|
||||||
|
|
||||||
if (!started) {
|
if (!started) {
|
||||||
logger.warn("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
return respondJson(response, {
|
return respondJson(response, {
|
||||||
message: "Failed to open port-forward"
|
message: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respondJson(response, { port: portForward.forwardPort });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
logger.error(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
||||||
|
|
||||||
return respondJson(response, {
|
return respondJson(response, {
|
||||||
message: error?.toString() || "Failed to open port-forward",
|
message: `Failed to forward port ${port}`
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
portForward.open();
|
static async routeCurrentPortForward(request: LensApiRequest) {
|
||||||
|
const { params, query, response, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
respondJson(response, {});
|
const portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
|
namespace, port, forwardPort
|
||||||
|
});
|
||||||
|
|
||||||
|
respondJson(response, { port: portForward?.forwardPort ?? null });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async routeAllPortForwards(request: LensApiRequest) {
|
||||||
|
const { response } = request;
|
||||||
|
|
||||||
|
const portForwards: PortForwardArgs[] = PortForward.portForwards.map(f => (
|
||||||
|
{
|
||||||
|
clusterId: f.clusterId,
|
||||||
|
kind: f.kind,
|
||||||
|
namespace: f.namespace,
|
||||||
|
name: f.name,
|
||||||
|
port: f.port,
|
||||||
|
forwardPort: f.forwardPort,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
respondJson(response, { portForwards });
|
||||||
|
}
|
||||||
|
|
||||||
|
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
||||||
|
const { params, query, response, cluster } = request;
|
||||||
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
const port = Number(query.get("port"));
|
||||||
|
const forwardPort = Number(query.get("forwardPort"));
|
||||||
|
|
||||||
|
const portForward = PortForward.getPortforward({
|
||||||
|
clusterId: cluster.id, kind: resourceType, name: resourceName,
|
||||||
|
namespace, port, forwardPort,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await portForward.stop();
|
||||||
|
respondJson(response, { status: true });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[PORT-FORWARD-ROUTE]: error stopping a port-forward", { namespace, port, forwardPort, resourceType, resourceName });
|
||||||
|
|
||||||
|
return respondJson(response, {
|
||||||
|
message: `error stopping a forward port ${port}`
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,19 @@ export class ResourceApplierApiRoute {
|
|||||||
try {
|
try {
|
||||||
const resource = await new ResourceApplier(cluster).apply(payload);
|
const resource = await new ResourceApplier(cluster).apply(payload);
|
||||||
|
|
||||||
respondJson(response, [resource], 200);
|
respondJson(response, resource, 200);
|
||||||
|
} catch (error) {
|
||||||
|
respondText(response, error, 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async patchResource(request: LensApiRequest) {
|
||||||
|
const { response, cluster, payload } = request;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resource = await new ResourceApplier(cluster).patch(payload.name, payload.kind, payload.patch, payload.ns);
|
||||||
|
|
||||||
|
respondJson(response, resource, 200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
respondText(response, error, 422);
|
respondText(response, error, 422);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,14 +21,37 @@
|
|||||||
|
|
||||||
import type http from "http";
|
import type http from "http";
|
||||||
|
|
||||||
export function respondJson(res: http.ServerResponse, content: any, status = 200) {
|
/**
|
||||||
respond(res, JSON.stringify(content), "application/json", status);
|
* Respond to a HTTP request with a body of JSON data
|
||||||
|
* @param res The HTTP response to write data to
|
||||||
|
* @param content The data or its JSON stringified version of it
|
||||||
|
* @param status [200] The status code to respond with
|
||||||
|
*/
|
||||||
|
export function respondJson(res: http.ServerResponse, content: Object | string, status = 200) {
|
||||||
|
const normalizedContent = typeof content === "object"
|
||||||
|
? JSON.stringify(content)
|
||||||
|
: content;
|
||||||
|
|
||||||
|
respond(res, normalizedContent, "application/json", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond to a HTTP request with a body of plain text data
|
||||||
|
* @param res The HTTP response to write data to
|
||||||
|
* @param content The string data to respond with
|
||||||
|
* @param status [200] The status code to respond with
|
||||||
|
*/
|
||||||
export function respondText(res: http.ServerResponse, content: string, status = 200) {
|
export function respondText(res: http.ServerResponse, content: string, status = 200) {
|
||||||
respond(res, content, "text/plain", status);
|
respond(res, content, "text/plain", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Respond to a HTTP request with a body of plain text data
|
||||||
|
* @param res The HTTP response to write data to
|
||||||
|
* @param content The string data to respond with
|
||||||
|
* @param contentType The HTTP Content-Type header value
|
||||||
|
* @param status [200] The status code to respond with
|
||||||
|
*/
|
||||||
export function respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
|
export function respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
|
||||||
res.setHeader("Content-Type", contentType);
|
res.setHeader("Content-Type", contentType);
|
||||||
res.statusCode = status;
|
res.statusCode = status;
|
||||||
|
|||||||
@ -31,7 +31,6 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { productName } from "../common/vars";
|
import { productName } from "../common/vars";
|
||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
import * as path from "path";
|
|
||||||
|
|
||||||
function isHideable(window: BrowserWindow | null): boolean {
|
function isHideable(window: BrowserWindow | null): boolean {
|
||||||
return Boolean(window && !window.isDestroyed());
|
return Boolean(window && !window.isDestroyed());
|
||||||
@ -85,7 +84,6 @@ export class WindowManager extends Singleton {
|
|||||||
titleBarStyle: "hiddenInset",
|
titleBarStyle: "hiddenInset",
|
||||||
backgroundColor: "#1e2124",
|
backgroundColor: "#1e2124",
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__static, "build", "preload.js"),
|
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
nodeIntegrationInSubFrames: true,
|
nodeIntegrationInSubFrames: true,
|
||||||
enableRemoteModule: true,
|
enableRemoteModule: true,
|
||||||
@ -111,10 +109,6 @@ export class WindowManager extends Singleton {
|
|||||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||||
})
|
})
|
||||||
.webContents
|
.webContents
|
||||||
.on("new-window", (event, url) => {
|
|
||||||
event.preventDefault();
|
|
||||||
shell.openExternal(url);
|
|
||||||
})
|
|
||||||
.on("dom-ready", () => {
|
.on("dom-ready", () => {
|
||||||
appEventBus.emit({ name: "app", action: "dom-ready" });
|
appEventBus.emit({ name: "app", action: "dom-ready" });
|
||||||
})
|
})
|
||||||
@ -152,6 +146,10 @@ export class WindowManager extends Singleton {
|
|||||||
|
|
||||||
// Always disable Node.js integration for all webviews
|
// Always disable Node.js integration for all webviews
|
||||||
webPreferences.nodeIntegration = false;
|
webPreferences.nodeIntegration = false;
|
||||||
|
}).setWindowOpenHandler((details) => {
|
||||||
|
shell.openExternal(details.url);
|
||||||
|
|
||||||
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -19,12 +19,12 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { app } from "electron";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getPath } from "../../common/utils/getPath";
|
|
||||||
|
|
||||||
export function fileNameMigration() {
|
export function fileNameMigration() {
|
||||||
const userDataPath = getPath("userData");
|
const userDataPath = app.getPath("userData");
|
||||||
const configJsonPath = path.join(userDataPath, "config.json");
|
const configJsonPath = path.join(userDataPath, "config.json");
|
||||||
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
|
const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json");
|
||||||
|
|
||||||
|
|||||||
@ -27,15 +27,22 @@ import type { Cluster } from "../../main/cluster";
|
|||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import { Disposer, iter } from "../utils";
|
import { Disposer, iter } from "../utils";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
|
import logger from "../../common/logger";
|
||||||
|
import { catalogEntityRunContext } from "./catalog-entity";
|
||||||
|
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
||||||
|
|
||||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||||
|
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@observable.ref activeEntity: CatalogEntity;
|
@observable protected activeEntityId: string | undefined = undefined;
|
||||||
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||||
protected filters = observable.set<EntityFilter>([], {
|
protected filters = observable.set<EntityFilter>([], {
|
||||||
deep: false,
|
deep: false,
|
||||||
});
|
});
|
||||||
|
protected onBeforeRunHooks = observable.set<CatalogEntityOnBeforeRun>([], {
|
||||||
|
deep: false,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
* Buffer for keeping entities that don't yet have CatalogCategory synced
|
||||||
@ -46,6 +53,22 @@ export class CatalogEntityRegistry {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get activeEntity(): CatalogEntity | null {
|
||||||
|
return this._entities.get(this.activeEntityId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set activeEntity(raw: CatalogEntity | string | null) {
|
||||||
|
if (raw) {
|
||||||
|
const id = typeof raw === "string"
|
||||||
|
? raw
|
||||||
|
: raw.getId();
|
||||||
|
|
||||||
|
this.activeEntityId = id;
|
||||||
|
} else {
|
||||||
|
this.activeEntityId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
ipcRendererOn("catalog:items", (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
ipcRendererOn("catalog:items", (event, items: (CatalogEntityData & CatalogEntityKindData)[]) => {
|
||||||
this.updateItems(items);
|
this.updateItems(items);
|
||||||
@ -153,6 +176,60 @@ export class CatalogEntityRegistry {
|
|||||||
|
|
||||||
return once(() => void this.filters.delete(fn));
|
return once(() => void this.filters.delete(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a onBeforeRun hook. If `onBeforeRun` was previously added then it will not be added again
|
||||||
|
* @param onBeforeRun The function that should return a boolean if the onRun of catalog entity should be triggered.
|
||||||
|
* @returns A function to remove that hook
|
||||||
|
*/
|
||||||
|
addOnBeforeRun(onBeforeRun: CatalogEntityOnBeforeRun): Disposer {
|
||||||
|
logger.debug(`[CATALOG-ENTITY-REGISTRY]: adding onBeforeRun hook`);
|
||||||
|
|
||||||
|
this.onBeforeRunHooks.add(onBeforeRun);
|
||||||
|
|
||||||
|
return once(() => void this.onBeforeRunHooks.delete(onBeforeRun));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs all the registered `onBeforeRun` hooks, short circuiting on the first event that's preventDefaulted
|
||||||
|
* @param entity The entity to run the hooks on
|
||||||
|
* @returns Whether the entities `onRun` method should be executed
|
||||||
|
*/
|
||||||
|
async onBeforeRun(entity: CatalogEntity): Promise<boolean> {
|
||||||
|
logger.debug(`[CATALOG-ENTITY-REGISTRY]: run onBeforeRun on ${entity.getId()}`);
|
||||||
|
|
||||||
|
const runEvent = new CatalogRunEvent({ target: entity });
|
||||||
|
|
||||||
|
for (const onBeforeRun of this.onBeforeRunHooks) {
|
||||||
|
try {
|
||||||
|
await onBeforeRun(runEvent);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onBeforeRun threw an error`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runEvent.defaultPrevented) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the onBeforeRun check and, if successful, then proceed to call `entity`'s onRun method
|
||||||
|
* @param entity The instance to invoke the hooks and then execute the onRun
|
||||||
|
*/
|
||||||
|
onRun(entity: CatalogEntity): void {
|
||||||
|
this.onBeforeRun(entity)
|
||||||
|
.then(doOnRun => {
|
||||||
|
if (doOnRun) {
|
||||||
|
return entity.onRun?.(catalogEntityRunContext);
|
||||||
|
} else {
|
||||||
|
logger.debug(`onBeforeRun for ${entity.getId()} returned false`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => logger.error(`[CATALOG-ENTITY-REGISTRY]: entity ${entity.getId()} onRun threw an error`, error));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import * as ReactRouterDom from "react-router-dom";
|
|||||||
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
import * as LensExtensionsCommonApi from "../extensions/common-api";
|
||||||
import * as LensExtensionsRendererApi from "../extensions/renderer-api";
|
import * as LensExtensionsRendererApi from "../extensions/renderer-api";
|
||||||
import { monaco } from "react-monaco-editor";
|
import { monaco } from "react-monaco-editor";
|
||||||
import { render, unmountComponentAtNode } from "react-dom";
|
import { render } from "react-dom";
|
||||||
import { delay } from "../common/utils";
|
import { delay } from "../common/utils";
|
||||||
import { isMac, isDevelopment } from "../common/vars";
|
import { isMac, isDevelopment } from "../common/vars";
|
||||||
import { ClusterStore } from "../common/cluster-store";
|
import { ClusterStore } from "../common/cluster-store";
|
||||||
@ -66,7 +66,7 @@ async function attachChromeDebugger() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppComponent = React.ComponentType & {
|
type AppComponent = React.ComponentType & {
|
||||||
init?(): Promise<void>;
|
init?(rootElem: HTMLElement): Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function bootstrap(App: AppComponent) {
|
export async function bootstrap(App: AppComponent) {
|
||||||
@ -125,16 +125,8 @@ export async function bootstrap(App: AppComponent) {
|
|||||||
|
|
||||||
// init app's dependencies if any
|
// init app's dependencies if any
|
||||||
if (App.init) {
|
if (App.init) {
|
||||||
await App.init();
|
await App.init(rootElem);
|
||||||
}
|
}
|
||||||
window.addEventListener("message", (ev: MessageEvent) => {
|
|
||||||
if (ev.data === "teardown") {
|
|
||||||
UserStore.getInstance(false)?.unregisterIpcListener();
|
|
||||||
ClusterStore.getInstance(false)?.unregisterIpcListener();
|
|
||||||
unmountComponentAtNode(rootElem);
|
|
||||||
window.location.href = "about:blank";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
render(<>
|
render(<>
|
||||||
{isMac && <div id="draggable-top" />}
|
{isMac && <div id="draggable-top" />}
|
||||||
{DefaultProps(App)}
|
{DefaultProps(App)}
|
||||||
|
|||||||
@ -50,11 +50,4 @@
|
|||||||
display: block;
|
display: block;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions-panel {
|
|
||||||
.Spinner {
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-left: $spacing;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import "./add-cluster.scss";
|
|||||||
import type { KubeConfig } from "@kubernetes/client-node";
|
import type { KubeConfig } from "@kubernetes/client-node";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import { debounce } from "lodash";
|
import { debounce } from "lodash";
|
||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
import { action, computed, observable, makeObservable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
@ -41,7 +41,6 @@ import { SettingLayout } from "../layout/setting-layout";
|
|||||||
import MonacoEditor from "react-monaco-editor";
|
import MonacoEditor from "react-monaco-editor";
|
||||||
import { ThemeStore } from "../../theme.store";
|
import { ThemeStore } from "../../theme.store";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { Spinner } from "../spinner";
|
|
||||||
|
|
||||||
interface Option {
|
interface Option {
|
||||||
config: KubeConfig;
|
config: KubeConfig;
|
||||||
@ -63,8 +62,7 @@ export class AddCluster extends React.Component {
|
|||||||
@observable kubeContexts = observable.map<string, Option>();
|
@observable kubeContexts = observable.map<string, Option>();
|
||||||
@observable customConfig = "";
|
@observable customConfig = "";
|
||||||
@observable isWaiting = false;
|
@observable isWaiting = false;
|
||||||
@observable isCheckingInput = false;
|
@observable errors: string[] = [];
|
||||||
@observable errorText: string;
|
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -77,39 +75,25 @@ export class AddCluster extends React.Component {
|
|||||||
|
|
||||||
@computed get allErrors(): string[] {
|
@computed get allErrors(): string[] {
|
||||||
return [
|
return [
|
||||||
this.errorText,
|
...this.errors,
|
||||||
...iter.map(this.kubeContexts.values(), ({ error }) => error)
|
...iter.map(this.kubeContexts.values(), ({ error }) => error)
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
_refreshContexts = debounce(() => {
|
@action
|
||||||
runInAction(() => {
|
refreshContexts = debounce(() => {
|
||||||
try {
|
const { config, error } = loadConfigFromString(this.customConfig.trim() || "{}");
|
||||||
const text = this.customConfig.trim();
|
|
||||||
|
|
||||||
if (!text) {
|
|
||||||
return this.kubeContexts.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { config, error } = loadConfigFromString(text);
|
|
||||||
|
|
||||||
this.kubeContexts.replace(getContexts(config));
|
this.kubeContexts.replace(getContexts(config));
|
||||||
this.errorText = error?.toString();
|
|
||||||
} catch (error) {
|
|
||||||
this.kubeContexts.clear();
|
|
||||||
this.errorText = error?.toString() || "An error occured";
|
|
||||||
} finally {
|
|
||||||
this.isCheckingInput = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
refreshContexts = () => {
|
if (error) {
|
||||||
// Clear the kubeContexts immediately
|
this.errors.push(error.toString());
|
||||||
this.isCheckingInput = true;
|
}
|
||||||
this.kubeContexts.clear();
|
|
||||||
this._refreshContexts();
|
if (config.contexts.length === 0) {
|
||||||
};
|
this.errors.push('No contexts defined, either missing the "contexts" field, or it is empty.');
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addClusters = async () => {
|
addClusters = async () => {
|
||||||
@ -147,7 +131,7 @@ export class AddCluster extends React.Component {
|
|||||||
value={this.customConfig}
|
value={this.customConfig}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
this.customConfig = value;
|
this.customConfig = value;
|
||||||
this.errorText = "";
|
this.errors.length = 0;
|
||||||
this.refreshContexts();
|
this.refreshContexts();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -168,7 +152,6 @@ export class AddCluster extends React.Component {
|
|||||||
tooltip={this.kubeContexts.size === 0 || "Paste in at least one cluster to add."}
|
tooltip={this.kubeContexts.size === 0 || "Paste in at least one cluster to add."}
|
||||||
tooltipOverrideDisabled
|
tooltipOverrideDisabled
|
||||||
/>
|
/>
|
||||||
{this.isCheckingInput && <Spinner />}
|
|
||||||
</div>
|
</div>
|
||||||
</SettingLayout>
|
</SettingLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -59,7 +59,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
@observable details: IReleaseDetails;
|
@observable details: IReleaseDetails;
|
||||||
@observable values = "";
|
@observable values = "";
|
||||||
@observable valuesLoading = false;
|
@observable valuesLoading = false;
|
||||||
@observable showOnlyUserSuppliedValues = false;
|
@observable showOnlyUserSuppliedValues = true;
|
||||||
@observable saving = false;
|
@observable saving = false;
|
||||||
@observable releaseSecret: Secret;
|
@observable releaseSecret: Secret;
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
onChange={text => this.values = text}
|
onChange={text => this.values = text}
|
||||||
theme={ThemeStore.getInstance().activeTheme.monacoTheme}
|
theme={ThemeStore.getInstance().activeTheme.monacoTheme}
|
||||||
className={cssNames("MonacoEditor", {loading: valuesLoading})}
|
className={cssNames("MonacoEditor", {loading: valuesLoading})}
|
||||||
options={{readOnly: valuesLoading || this.showOnlyUserSuppliedValues, ...UserStore.getInstance().getEditorOptions()}}
|
options={{readOnly: valuesLoading, ...UserStore.getInstance().getEditorOptions()}}
|
||||||
>
|
>
|
||||||
{valuesLoading && <Spinner center />}
|
{valuesLoading && <Spinner center />}
|
||||||
</MonacoEditor>
|
</MonacoEditor>
|
||||||
|
|||||||
@ -23,13 +23,12 @@ import "./catalog-entity-details.scss";
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Drawer, DrawerItem } from "../drawer";
|
import { Drawer, DrawerItem } from "../drawer";
|
||||||
import { catalogEntityRunContext } from "../../api/catalog-entity";
|
|
||||||
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
|
import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
|
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
|
||||||
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||||
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
import type { CatalogEntityItem } from "./catalog-entity.store";
|
import type { CatalogEntityItem } from "./catalog-entity-item";
|
||||||
import { isDevelopment } from "../../../common/vars";
|
import { isDevelopment } from "../../../common/vars";
|
||||||
|
|
||||||
interface Props<T extends CatalogEntity> {
|
interface Props<T extends CatalogEntity> {
|
||||||
@ -68,8 +67,10 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
|
|||||||
material={item.entity.spec.icon?.material}
|
material={item.entity.spec.icon?.material}
|
||||||
background={item.entity.spec.icon?.background}
|
background={item.entity.spec.icon?.background}
|
||||||
disabled={!item?.enabled}
|
disabled={!item?.enabled}
|
||||||
onClick={() => item.onRun(catalogEntityRunContext)}
|
onClick={() => item.onRun()}
|
||||||
size={128} />
|
size={128}
|
||||||
|
data-testid="detail-panel-hot-bar-icon"
|
||||||
|
/>
|
||||||
{item?.enabled && (
|
{item?.enabled && (
|
||||||
<div className="IconHint">
|
<div className="IconHint">
|
||||||
Click to open
|
Click to open
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import { MenuItem } from "../menu";
|
|||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import type { CatalogEntityItem } from "./catalog-entity.store";
|
import type { CatalogEntityItem } from "./catalog-entity-item";
|
||||||
|
|
||||||
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
|
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
|
||||||
item: CatalogEntityItem<T> | null | undefined;
|
item: CatalogEntityItem<T> | null | undefined;
|
||||||
|
|||||||
114
src/renderer/components/+catalog/catalog-entity-item.tsx
Normal file
114
src/renderer/components/+catalog/catalog-entity-item.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import styles from "./catalog.module.css";
|
||||||
|
import React from "react";
|
||||||
|
import { action, computed } from "mobx";
|
||||||
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
|
import type { ItemObject } from "../../../common/item.store";
|
||||||
|
import { Badge } from "../badge";
|
||||||
|
import { navigation } from "../../navigation";
|
||||||
|
import { searchUrlParam } from "../input";
|
||||||
|
import { makeCss } from "../../../common/utils/makeCss";
|
||||||
|
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
|
import type { CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
|
||||||
|
const css = makeCss(styles);
|
||||||
|
|
||||||
|
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
||||||
|
constructor(public entity: T, private registry: CatalogEntityRegistry) {}
|
||||||
|
|
||||||
|
get kind() {
|
||||||
|
return this.entity.kind;
|
||||||
|
}
|
||||||
|
|
||||||
|
get apiVersion() {
|
||||||
|
return this.entity.apiVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
return this.entity.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.entity.metadata.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get id() {
|
||||||
|
return this.entity.metadata.uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get phase() {
|
||||||
|
return this.entity.status.phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
get enabled() {
|
||||||
|
return this.entity.status.enabled ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
get labels() {
|
||||||
|
return KubeObject.stringifyLabels(this.entity.metadata.labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabelBadges(onClick?: React.MouseEventHandler<any>) {
|
||||||
|
return this.labels
|
||||||
|
.map(label => (
|
||||||
|
<Badge
|
||||||
|
className={css.badge}
|
||||||
|
key={label}
|
||||||
|
label={label}
|
||||||
|
title={label}
|
||||||
|
onClick={(event) => {
|
||||||
|
navigation.searchParams.set(searchUrlParam.name, label);
|
||||||
|
onClick?.(event);
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
expandable={false}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
get source() {
|
||||||
|
return this.entity.metadata.source || "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
get searchFields() {
|
||||||
|
return [
|
||||||
|
this.name,
|
||||||
|
this.id,
|
||||||
|
this.phase,
|
||||||
|
`source=${this.source}`,
|
||||||
|
...this.labels,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
onRun() {
|
||||||
|
this.registry.onRun(this.entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async onContextMenuOpen(ctx: any) {
|
||||||
|
return this.entity.onContextMenuOpen(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,106 +19,16 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import styles from "./catalog.module.css";
|
import { computed, makeObservable, observable, reaction } from "mobx";
|
||||||
|
import { catalogEntityRegistry, CatalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import React from "react";
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from "mobx";
|
import { ItemStore } from "../../../common/item.store";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
|
||||||
import type { CatalogEntity, CatalogEntityActionContext } from "../../api/catalog-entity";
|
|
||||||
import { ItemObject, ItemStore } from "../../../common/item.store";
|
|
||||||
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
|
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
|
||||||
import { autoBind } from "../../../common/utils";
|
import { autoBind, disposer } from "../../../common/utils";
|
||||||
import { Badge } from "../badge";
|
import { CatalogEntityItem } from "./catalog-entity-item";
|
||||||
import { navigation } from "../../navigation";
|
|
||||||
import { searchUrlParam } from "../input";
|
|
||||||
import { makeCss } from "../../../common/utils/makeCss";
|
|
||||||
import { KubeObject } from "../../../common/k8s-api/kube-object";
|
|
||||||
|
|
||||||
const css = makeCss(styles);
|
|
||||||
|
|
||||||
export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
|
||||||
constructor(public entity: T) {}
|
|
||||||
|
|
||||||
get kind() {
|
|
||||||
return this.entity.kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
get apiVersion() {
|
|
||||||
return this.entity.apiVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return this.entity.metadata.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
getName() {
|
|
||||||
return this.entity.metadata.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get id() {
|
|
||||||
return this.entity.metadata.uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get phase() {
|
|
||||||
return this.entity.status.phase;
|
|
||||||
}
|
|
||||||
|
|
||||||
get enabled() {
|
|
||||||
return this.entity.status.enabled ?? true;
|
|
||||||
}
|
|
||||||
|
|
||||||
get labels() {
|
|
||||||
return KubeObject.stringifyLabels(this.entity.metadata.labels);
|
|
||||||
}
|
|
||||||
|
|
||||||
getLabelBadges(onClick?: React.MouseEventHandler<any>) {
|
|
||||||
return this.labels
|
|
||||||
.map(label => (
|
|
||||||
<Badge
|
|
||||||
className={css.badge}
|
|
||||||
key={label}
|
|
||||||
label={label}
|
|
||||||
title={label}
|
|
||||||
onClick={(event) => {
|
|
||||||
navigation.searchParams.set(searchUrlParam.name, label);
|
|
||||||
onClick?.(event);
|
|
||||||
event.stopPropagation();
|
|
||||||
}}
|
|
||||||
expandable={false}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
get source() {
|
|
||||||
return this.entity.metadata.source || "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
get searchFields() {
|
|
||||||
return [
|
|
||||||
this.name,
|
|
||||||
this.id,
|
|
||||||
this.phase,
|
|
||||||
`source=${this.source}`,
|
|
||||||
...this.labels,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
onRun(ctx: CatalogEntityActionContext) {
|
|
||||||
this.entity.onRun(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
async onContextMenuOpen(ctx: any) {
|
|
||||||
return this.entity.onContextMenuOpen(ctx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntity>> {
|
export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntity>> {
|
||||||
constructor() {
|
constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) {
|
||||||
super();
|
super();
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
@ -129,10 +39,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
|
|||||||
|
|
||||||
@computed get entities() {
|
@computed get entities() {
|
||||||
if (!this.activeCategory) {
|
if (!this.activeCategory) {
|
||||||
return catalogEntityRegistry.filteredItems.map(entity => new CatalogEntityItem(entity));
|
return this.registry.filteredItems.map(entity => new CatalogEntityItem(entity, this.registry));
|
||||||
}
|
}
|
||||||
|
|
||||||
return catalogEntityRegistry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity));
|
return this.registry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity, this.registry));
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get selectedItem() {
|
@computed get selectedItem() {
|
||||||
@ -140,12 +50,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch() {
|
watch() {
|
||||||
const disposers: IReactionDisposer[] = [
|
return disposer(
|
||||||
reaction(() => this.entities, () => this.loadAll()),
|
reaction(() => this.entities, () => this.loadAll()),
|
||||||
reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100})
|
reaction(() => this.activeCategory, () => this.loadAll(), { delay: 100}),
|
||||||
];
|
);
|
||||||
|
|
||||||
return () => disposers.forEach((dispose) => dispose());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadAll() {
|
loadAll() {
|
||||||
|
|||||||
@ -41,11 +41,15 @@ function getCategories() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCategoryIcon(category: CatalogCategory) {
|
function getCategoryIcon(category: CatalogCategory) {
|
||||||
if (!category.metadata?.icon) return null;
|
const { icon } = category.metadata ?? {};
|
||||||
|
|
||||||
return category.metadata.icon.includes("<svg")
|
if (typeof icon === "string") {
|
||||||
? <Icon small svg={category.metadata.icon}/>
|
return icon.includes("<svg")
|
||||||
: <Icon small material={category.metadata.icon}/>;
|
? <Icon small svg={icon}/>
|
||||||
|
: <Icon small material={icon}/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Item(props: TreeItemProps) {
|
function Item(props: TreeItemProps) {
|
||||||
|
|||||||
366
src/renderer/components/+catalog/catalog.test.tsx
Normal file
366
src/renderer/components/+catalog/catalog.test.tsx
Normal file
@ -0,0 +1,366 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
import { Catalog } from "./catalog";
|
||||||
|
import { createMemoryHistory } from "history";
|
||||||
|
import { mockWindow } from "../../../../__mocks__/windowMock";
|
||||||
|
import { kubernetesClusterCategory } from "../../../common/catalog-entities/kubernetes-cluster";
|
||||||
|
import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../../common/catalog";
|
||||||
|
import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
|
||||||
|
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
|
||||||
|
import { CatalogEntityItem } from "./catalog-entity-item";
|
||||||
|
import { CatalogEntityStore } from "./catalog-entity.store";
|
||||||
|
|
||||||
|
mockWindow();
|
||||||
|
|
||||||
|
// avoid TypeError: Cannot read property 'getPath' of undefined
|
||||||
|
jest.mock("@electron/remote", () => {
|
||||||
|
return {
|
||||||
|
app: {
|
||||||
|
getPath: () => {
|
||||||
|
// avoid TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string. Received undefined
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("<Catalog />", () => {
|
||||||
|
const history = createMemoryHistory();
|
||||||
|
const mockLocation = {
|
||||||
|
pathname: "",
|
||||||
|
search: "",
|
||||||
|
state: "",
|
||||||
|
hash: "",
|
||||||
|
};
|
||||||
|
const mockMatch = {
|
||||||
|
params: {
|
||||||
|
// will be used to match activeCategory
|
||||||
|
// need to be the same as property values in kubernetesClusterCategory
|
||||||
|
group: "entity.k8slens.dev",
|
||||||
|
kind: "KubernetesCluster",
|
||||||
|
},
|
||||||
|
isExact: true,
|
||||||
|
path: "",
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const catalogEntityUid = "a_catalogEntity_uid";
|
||||||
|
const catalogEntity = {
|
||||||
|
enabled: true,
|
||||||
|
apiVersion: "api",
|
||||||
|
kind: "kind",
|
||||||
|
metadata: {
|
||||||
|
uid: catalogEntityUid,
|
||||||
|
name: "a catalog entity",
|
||||||
|
labels: {
|
||||||
|
test: "label",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
phase: "",
|
||||||
|
},
|
||||||
|
spec: {},
|
||||||
|
};
|
||||||
|
const catalogEntityItemMethods = {
|
||||||
|
getId: () => catalogEntity.metadata.uid,
|
||||||
|
getName: () => catalogEntity.metadata.name,
|
||||||
|
onContextMenuOpen: () => {},
|
||||||
|
onSettingsOpen: () => {},
|
||||||
|
onRun: () => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
CatalogEntityDetailRegistry.createInstance();
|
||||||
|
// mock the return of getting CatalogCategoryRegistry.filteredItems
|
||||||
|
jest
|
||||||
|
.spyOn(catalogCategoryRegistry, "filteredItems", "get")
|
||||||
|
.mockImplementation(() => {
|
||||||
|
return [kubernetesClusterCategory];
|
||||||
|
});
|
||||||
|
|
||||||
|
// we don't care what this.renderList renders in this test case.
|
||||||
|
jest.spyOn(Catalog.prototype, "renderList").mockImplementation(() => {
|
||||||
|
return <span>empty renderList</span>;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
CatalogEntityDetailRegistry.resetInstance();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", (done) => {
|
||||||
|
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||||
|
const onRun = jest.fn();
|
||||||
|
const catalogEntityItem = new CatalogEntityItem({
|
||||||
|
...catalogEntity,
|
||||||
|
...catalogEntityItemMethods,
|
||||||
|
onRun,
|
||||||
|
}, catalogEntityRegistry);
|
||||||
|
|
||||||
|
// mock as if there is a selected item > the detail panel opens
|
||||||
|
jest
|
||||||
|
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||||
|
.mockImplementation(() => catalogEntityItem);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addOnBeforeRun(
|
||||||
|
(event) => {
|
||||||
|
expect(event.target).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"apiVersion": "api",
|
||||||
|
"enabled": true,
|
||||||
|
"getId": [Function],
|
||||||
|
"getName": [Function],
|
||||||
|
"kind": "kind",
|
||||||
|
"metadata": Object {
|
||||||
|
"labels": Object {
|
||||||
|
"test": "label",
|
||||||
|
},
|
||||||
|
"name": "a catalog entity",
|
||||||
|
"uid": "a_catalogEntity_uid",
|
||||||
|
},
|
||||||
|
"onContextMenuOpen": [Function],
|
||||||
|
"onRun": [MockFunction],
|
||||||
|
"onSettingsOpen": [Function],
|
||||||
|
"spec": Object {},
|
||||||
|
"status": Object {
|
||||||
|
"phase": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(onRun).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Catalog
|
||||||
|
history={history}
|
||||||
|
location={mockLocation}
|
||||||
|
match={mockMatch}
|
||||||
|
catalogEntityStore={catalogEntityStore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onBeforeRun prevents event => onRun wont be triggered", (done) => {
|
||||||
|
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||||
|
const onRun = jest.fn();
|
||||||
|
const catalogEntityItem = new CatalogEntityItem({
|
||||||
|
...catalogEntity,
|
||||||
|
...catalogEntityItemMethods,
|
||||||
|
onRun,
|
||||||
|
}, catalogEntityRegistry);
|
||||||
|
|
||||||
|
// mock as if there is a selected item > the detail panel opens
|
||||||
|
jest
|
||||||
|
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||||
|
.mockImplementation(() => catalogEntityItem);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addOnBeforeRun(
|
||||||
|
(e) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(onRun).not.toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Catalog
|
||||||
|
history={history}
|
||||||
|
location={mockLocation}
|
||||||
|
match={mockMatch}
|
||||||
|
catalogEntityStore={catalogEntityStore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addOnBeforeRun throw an exception => onRun will be triggered", (done) => {
|
||||||
|
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||||
|
const onRun = jest.fn();
|
||||||
|
const catalogEntityItem = new CatalogEntityItem({
|
||||||
|
...catalogEntity,
|
||||||
|
...catalogEntityItemMethods,
|
||||||
|
onRun,
|
||||||
|
}, catalogEntityRegistry);
|
||||||
|
|
||||||
|
// mock as if there is a selected item > the detail panel opens
|
||||||
|
jest
|
||||||
|
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||||
|
.mockImplementation(() => catalogEntityItem);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addOnBeforeRun(
|
||||||
|
() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(onRun).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
throw new Error("error!");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Catalog
|
||||||
|
history={history}
|
||||||
|
location={mockLocation}
|
||||||
|
match={mockMatch}
|
||||||
|
catalogEntityStore={catalogEntityStore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => {
|
||||||
|
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||||
|
const onRun = jest.fn(() => done());
|
||||||
|
const catalogEntityItem = new CatalogEntityItem({
|
||||||
|
...catalogEntity,
|
||||||
|
...catalogEntityItemMethods,
|
||||||
|
onRun,
|
||||||
|
}, catalogEntityRegistry);
|
||||||
|
|
||||||
|
// mock as if there is a selected item > the detail panel opens
|
||||||
|
jest
|
||||||
|
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||||
|
.mockImplementation(() => catalogEntityItem);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addOnBeforeRun(
|
||||||
|
async () => {
|
||||||
|
// no op
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Catalog
|
||||||
|
history={history}
|
||||||
|
location={mockLocation}
|
||||||
|
match={mockMatch}
|
||||||
|
catalogEntityStore={catalogEntityStore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addOnRunHook return a promise and prevents event wont be triggered", (done) => {
|
||||||
|
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||||
|
const onRun = jest.fn();
|
||||||
|
const catalogEntityItem = new CatalogEntityItem({
|
||||||
|
...catalogEntity,
|
||||||
|
...catalogEntityItemMethods,
|
||||||
|
onRun,
|
||||||
|
}, catalogEntityRegistry);
|
||||||
|
|
||||||
|
// mock as if there is a selected item > the detail panel opens
|
||||||
|
jest
|
||||||
|
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||||
|
.mockImplementation(() => catalogEntityItem);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addOnBeforeRun(
|
||||||
|
async (e) => {
|
||||||
|
expect(onRun).not.toBeCalled();
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(onRun).not.toBeCalled();
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Catalog
|
||||||
|
history={history}
|
||||||
|
location={mockLocation}
|
||||||
|
match={mockMatch}
|
||||||
|
catalogEntityStore={catalogEntityStore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("addOnRunHook return a promise and reject => onRun will be triggered", (done) => {
|
||||||
|
const catalogCategoryRegistry = new CatalogCategoryRegistry();
|
||||||
|
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||||
|
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
|
||||||
|
const onRun = jest.fn();
|
||||||
|
const catalogEntityItem = new CatalogEntityItem({
|
||||||
|
...catalogEntity,
|
||||||
|
...catalogEntityItemMethods,
|
||||||
|
onRun,
|
||||||
|
}, catalogEntityRegistry);
|
||||||
|
|
||||||
|
// mock as if there is a selected item > the detail panel opens
|
||||||
|
jest
|
||||||
|
.spyOn(catalogEntityStore, "selectedItem", "get")
|
||||||
|
.mockImplementation(() => catalogEntityItem);
|
||||||
|
|
||||||
|
catalogEntityRegistry.addOnBeforeRun(
|
||||||
|
async () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(onRun).toHaveBeenCalled();
|
||||||
|
done();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
throw new Error("rejection!");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<Catalog
|
||||||
|
history={history}
|
||||||
|
location={mockLocation}
|
||||||
|
match={mockMatch}
|
||||||
|
catalogEntityStore={catalogEntityStore}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
userEvent.click(screen.getByTestId("detail-panel-hot-bar-icon"));
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -25,7 +25,8 @@ import React from "react";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ItemListLayout } from "../item-object-list";
|
import { ItemListLayout } from "../item-object-list";
|
||||||
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
|
||||||
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
|
import { CatalogEntityStore } from "./catalog-entity.store";
|
||||||
|
import type { CatalogEntityItem } from "./catalog-entity-item";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { MenuItem, MenuActions } from "../menu";
|
import { MenuItem, MenuActions } from "../menu";
|
||||||
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
|
||||||
@ -39,12 +40,12 @@ import { MainLayout } from "../layout/main-layout";
|
|||||||
import { createAppStorage, cssNames } from "../../utils";
|
import { createAppStorage, cssNames } from "../../utils";
|
||||||
import { makeCss } from "../../../common/utils/makeCss";
|
import { makeCss } from "../../../common/utils/makeCss";
|
||||||
import { CatalogEntityDetails } from "./catalog-entity-details";
|
import { CatalogEntityDetails } from "./catalog-entity-details";
|
||||||
import { catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes";
|
||||||
import { CatalogMenu } from "./catalog-menu";
|
import { CatalogMenu } from "./catalog-menu";
|
||||||
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
import { HotbarIcon } from "../hotbar/hotbar-icon";
|
||||||
import { RenderDelay } from "../render-delay/render-delay";
|
import { RenderDelay } from "../render-delay/render-delay";
|
||||||
|
|
||||||
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", "");
|
export const previousActiveTab = createAppStorage("catalog-previous-active-tab", browseCatalogTab);
|
||||||
|
|
||||||
enum sortBy {
|
enum sortBy {
|
||||||
name = "name",
|
name = "name",
|
||||||
@ -55,7 +56,10 @@ enum sortBy {
|
|||||||
|
|
||||||
const css = makeCss(styles);
|
const css = makeCss(styles);
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<CatalogViewRouteParam> {}
|
interface Props extends RouteComponentProps<CatalogViewRouteParam> {
|
||||||
|
catalogEntityStore?: CatalogEntityStore;
|
||||||
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Catalog extends React.Component<Props> {
|
export class Catalog extends React.Component<Props> {
|
||||||
@observable private catalogEntityStore?: CatalogEntityStore;
|
@observable private catalogEntityStore?: CatalogEntityStore;
|
||||||
@ -65,8 +69,11 @@ export class Catalog extends React.Component<Props> {
|
|||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.catalogEntityStore = new CatalogEntityStore();
|
this.catalogEntityStore = props.catalogEntityStore;
|
||||||
}
|
}
|
||||||
|
static defaultProps = {
|
||||||
|
catalogEntityStore: new CatalogEntityStore(),
|
||||||
|
};
|
||||||
|
|
||||||
get routeActiveTab(): string {
|
get routeActiveTab(): string {
|
||||||
const { group, kind } = this.props.match.params ?? {};
|
const { group, kind } = this.props.match.params ?? {};
|
||||||
@ -75,7 +82,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
return `${group}/${kind}`;
|
return `${group}/${kind}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "";
|
return browseCatalogTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
@ -89,7 +96,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
previousActiveTab.set(this.routeActiveTab);
|
previousActiveTab.set(this.routeActiveTab);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await when(() => (routeTab === "" || !!catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load
|
await when(() => (routeTab === browseCatalogTab || !!catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load
|
||||||
const item = catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab);
|
const item = catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -117,15 +124,16 @@ export class Catalog extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
addToHotbar(item: CatalogEntityItem<CatalogEntity>): void {
|
addToHotbar(item: CatalogEntityItem<CatalogEntity>): void {
|
||||||
HotbarStore.getInstance().addToHotbar(item.entity);
|
HotbarStore.getInstance().addToHotbar(item.entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
onDetails = (item: CatalogEntityItem<CatalogEntity>) => {
|
onDetails = (item: CatalogEntityItem<CatalogEntity>) => {
|
||||||
if (this.catalogEntityStore.selectedItemId === item.getId()) {
|
if (this.catalogEntityStore.selectedItemId) {
|
||||||
this.catalogEntityStore.selectedItemId = null;
|
this.catalogEntityStore.selectedItemId = null;
|
||||||
} else {
|
} else {
|
||||||
this.catalogEntityStore.selectedItemId = item.getId();
|
item.onRun();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -157,7 +165,7 @@ export class Catalog extends React.Component<Props> {
|
|||||||
if (activeCategory) {
|
if (activeCategory) {
|
||||||
navigate(catalogURL({ params: {group: activeCategory.spec.group, kind: activeCategory.spec.names.kind }}));
|
navigate(catalogURL({ params: {group: activeCategory.spec.group, kind: activeCategory.spec.names.kind }}));
|
||||||
} else {
|
} else {
|
||||||
navigate(catalogURL());
|
navigate(catalogURL({ params: { group: browseCatalogTab }}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -176,6 +184,9 @@ export class Catalog extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuActions onOpen={onOpen}>
|
<MenuActions onOpen={onOpen}>
|
||||||
|
<MenuItem key="open-details" onClick={() => this.catalogEntityStore.selectedItemId = item.getId()}>
|
||||||
|
View Details
|
||||||
|
</MenuItem>
|
||||||
{
|
{
|
||||||
this.contextMenu.menuItems.map((menuItem, index) => (
|
this.contextMenu.menuItems.map((menuItem, index) => (
|
||||||
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
|
||||||
|
|||||||
@ -72,6 +72,8 @@ export class ConfigMapDetails extends React.Component<Props> {
|
|||||||
<>ConfigMap <b>{configMap.getName()}</b> successfully updated.</>
|
<>ConfigMap <b>{configMap.getName()}</b> successfully updated.</>
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(`Failed to save config map: ${error}`);
|
||||||
} finally {
|
} finally {
|
||||||
this.isSaving = false;
|
this.isSaving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.entityName {
|
||||||
|
@apply font-bold overflow-hidden;
|
||||||
|
word-break: break-word;
|
||||||
|
color: var(--textColorAccent);
|
||||||
|
display: -webkit-box;
|
||||||
|
/* Simulate text-overflow:ellipsis styles but for multiple text lines */
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import "./entity-settings.scss";
|
import styles from "./entity-settings.module.css";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
@ -98,7 +98,9 @@ export class EntitySettings extends React.Component<Props> {
|
|||||||
source={this.entity.metadata.source}
|
source={this.entity.metadata.source}
|
||||||
src={this.entity.spec.icon?.src}
|
src={this.entity.spec.icon?.src}
|
||||||
/>
|
/>
|
||||||
<h2>{this.entity.metadata.name}</h2>
|
<div className={styles.entityName}>
|
||||||
|
{this.entity.metadata.name}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
|
||||||
{ groups.map((group, groupIndex) => (
|
{ groups.map((group, groupIndex) => (
|
||||||
@ -138,7 +140,6 @@ export class EntitySettings extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingLayout
|
<SettingLayout
|
||||||
className="CatalogEntitySettings"
|
|
||||||
navigation={this.renderNavigation()}
|
navigation={this.renderNavigation()}
|
||||||
contentGaps={false}
|
contentGaps={false}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import { observer } from "mobx-react";
|
|||||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
|
import { LocaleDate } from "../locale-date";
|
||||||
import { eventStore } from "./event.store";
|
import { eventStore } from "./event.store";
|
||||||
|
|
||||||
export interface KubeEventDetailsProps {
|
export interface KubeEventDetailsProps {
|
||||||
@ -74,7 +75,7 @@ export class KubeEventDetails extends React.Component<KubeEventDetailsProps> {
|
|||||||
{involvedObject.fieldPath}
|
{involvedObject.fieldPath}
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
<DrawerItem name="Last seen">
|
<DrawerItem name="Last seen">
|
||||||
{lastTimestamp}
|
<LocaleDate date={lastTimestamp} />
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -18,4 +18,5 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
export * from "./parse-query";
|
|
||||||
|
export * from "./port-forwards";
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { boundMethod, cssNames } from "../../utils";
|
||||||
|
import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward";
|
||||||
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
|
import { MenuItem } from "../menu";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { PortForwardDialog } from "../../port-forward";
|
||||||
|
|
||||||
|
interface Props extends MenuActionsProps {
|
||||||
|
portForward: PortForwardItem;
|
||||||
|
hideDetails?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortForwardMenu extends React.Component<Props> {
|
||||||
|
@boundMethod
|
||||||
|
remove() {
|
||||||
|
return removePortForward(this.props.portForward);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
|
if (!portForward) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MenuItem onClick={() => openPortForward(this.props.portForward)}>
|
||||||
|
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
||||||
|
<span className="title">Open</span>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
|
||||||
|
<Icon material="edit" tooltip="Change port" interactive={toolbar} />
|
||||||
|
<span className="title">Edit</span>
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { className, ...menuProps } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuActions
|
||||||
|
{...menuProps}
|
||||||
|
className={cssNames("PortForwardMenu", className)}
|
||||||
|
removeAction={this.remove}
|
||||||
|
>
|
||||||
|
{this.renderContent()}
|
||||||
|
</MenuActions>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.PortForwards {
|
||||||
|
.TableCell {
|
||||||
|
&.warning {
|
||||||
|
@include table-cell-warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
103
src/renderer/components/+network-port-forwards/port-forwards.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import "./port-forwards.scss";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
import { ItemListLayout } from "../item-object-list/item-list-layout";
|
||||||
|
import { PortForwardItem, portForwardStore } from "../../port-forward";
|
||||||
|
import { PortForwardMenu } from "./port-forward-menu";
|
||||||
|
|
||||||
|
enum columnId {
|
||||||
|
name = "name",
|
||||||
|
namespace = "namespace",
|
||||||
|
kind = "kind",
|
||||||
|
port = "port",
|
||||||
|
forwardPort = "forwardPort",
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class PortForwards extends React.Component {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
portForwardStore.watch(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRemoveDialogMessage(selectedItems: PortForwardItem[]) {
|
||||||
|
const forwardPorts = selectedItems.map(item => item.getForwardPort()).join(", ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<>Stop forwarding from <b>{forwardPorts}</b>?</>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ItemListLayout
|
||||||
|
isConfigurable
|
||||||
|
tableId="port_forwards"
|
||||||
|
className="PortForwards" store={portForwardStore}
|
||||||
|
sortingCallbacks={{
|
||||||
|
[columnId.name]: item => item.getName(),
|
||||||
|
[columnId.namespace]: item => item.getNs(),
|
||||||
|
[columnId.kind]: item => item.getKind(),
|
||||||
|
[columnId.port]: item => item.getPort(),
|
||||||
|
[columnId.forwardPort]: item => item.getForwardPort(),
|
||||||
|
}}
|
||||||
|
searchFilters={[
|
||||||
|
item => item.getSearchFields(),
|
||||||
|
]}
|
||||||
|
renderHeaderTitle="Port Forwarding"
|
||||||
|
renderTableHeader={[
|
||||||
|
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
|
||||||
|
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
|
||||||
|
{ title: "Kind", className: "kind", sortBy: columnId.kind, id: columnId.kind },
|
||||||
|
{ title: "Pod Port", className: "port", sortBy: columnId.port, id: columnId.port },
|
||||||
|
{ title: "Local Port", className: "forwardPort", sortBy: columnId.forwardPort, id: columnId.forwardPort },
|
||||||
|
]}
|
||||||
|
renderTableContents={item => [
|
||||||
|
item.getName(),
|
||||||
|
item.getNs(),
|
||||||
|
item.getKind(),
|
||||||
|
item.getPort(),
|
||||||
|
item.getForwardPort(),
|
||||||
|
]}
|
||||||
|
renderItemMenu={pf => (
|
||||||
|
<PortForwardMenu
|
||||||
|
portForward={pf}
|
||||||
|
removeConfirmationMessage={this.renderRemoveDialogMessage([pf])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
customizeRemoveDialog={selectedItems => ({
|
||||||
|
message: this.renderRemoveDialogMessage(selectedItems)
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -32,6 +32,7 @@ import { ServicePortComponent } from "./service-port-component";
|
|||||||
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
import { endpointStore } from "../+network-endpoints/endpoints.store";
|
||||||
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
import { ServiceDetailsEndpoint } from "./service-details-endpoint";
|
||||||
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api";
|
||||||
|
import { portForwardStore } from "../../port-forward";
|
||||||
|
|
||||||
interface Props extends KubeObjectDetailsProps<Service> {
|
interface Props extends KubeObjectDetailsProps<Service> {
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ export class ServiceDetails extends React.Component<Props> {
|
|||||||
preload: true,
|
preload: true,
|
||||||
namespaces: [service.getNs()],
|
namespaces: [service.getNs()],
|
||||||
}),
|
}),
|
||||||
|
portForwardStore.watch(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,11 +33,13 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $primary;
|
color: $primary;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Spinner {
|
.portInput {
|
||||||
--spinner-size: #{$unit * 2};
|
display: inline-block !important;
|
||||||
margin-left: $margin;
|
width: 70px;
|
||||||
position: absolute;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,12 +22,15 @@
|
|||||||
import "./service-port-component.scss";
|
import "./service-port-component.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
||||||
import { apiBase } from "../../api";
|
import { observable, makeObservable, reaction } from "mobx";
|
||||||
import { observable, makeObservable } from "mobx";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { addPortForward, getPortForward, openPortForward, PortForwardDialog, portForwardStore, removePortForward } from "../../port-forward";
|
||||||
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -38,19 +41,85 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class ServicePortComponent extends React.Component<Props> {
|
export class ServicePortComponent extends React.Component<Props> {
|
||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
|
@observable forwardPort = 0;
|
||||||
|
@observable isPortForwarded = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => [ portForwardStore.portForwards, this.props.service ], () => this.init()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.checkExistingPortForwarding().catch(error => {
|
||||||
|
logger.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExistingPortForwarding() {
|
||||||
|
const { service, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
const activePort = await getPortForward(portForward) ?? 0;
|
||||||
|
|
||||||
|
this.forwardPort = activePort;
|
||||||
|
this.isPortForwarded = activePort ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.waiting = true;
|
||||||
|
this.isPortForwarded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.forwardPort = await addPortForward(portForward);
|
||||||
|
|
||||||
|
if (this.forwardPort) {
|
||||||
|
portForward.forwardPort = this.forwardPort;
|
||||||
|
openPortForward(portForward);
|
||||||
|
this.isPortForwarded = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(error);
|
||||||
|
} finally {
|
||||||
|
this.waiting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopPortForward() {
|
||||||
|
const { service, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiBase.post(`/pods/${service.getNs()}/service/${service.getName()}/port-forward/${port.port}`, {});
|
await removePortForward(portForward);
|
||||||
|
this.isPortForwarded = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(error);
|
Notifications.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -59,16 +128,33 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { port } = this.props;
|
const { port, service } = this.props;
|
||||||
|
|
||||||
|
const portForwardAction = async () => {
|
||||||
|
if (this.isPortForwarded) {
|
||||||
|
await this.stopPortForward();
|
||||||
|
} else {
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "service",
|
||||||
|
name: service.getName(),
|
||||||
|
namespace: service.getNs(),
|
||||||
|
port: port.port,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
PortForwardDialog.open(portForward, { openInBrowser: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward()}>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{port.toString()}
|
{port.toString()}
|
||||||
|
</span>
|
||||||
|
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
||||||
{this.waiting && (
|
{this.waiting && (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
)}
|
)}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { Services } from "../+network-services";
|
|||||||
import { Endpoints } from "../+network-endpoints";
|
import { Endpoints } from "../+network-endpoints";
|
||||||
import { Ingresses } from "../+network-ingresses";
|
import { Ingresses } from "../+network-ingresses";
|
||||||
import { NetworkPolicies } from "../+network-policies";
|
import { NetworkPolicies } from "../+network-policies";
|
||||||
|
import { PortForwards } from "../+network-port-forwards";
|
||||||
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
import { isAllowedResource } from "../../../common/utils/allowed-resource";
|
||||||
import * as routes from "../../../common/routes";
|
import * as routes from "../../../common/routes";
|
||||||
|
|
||||||
@ -72,6 +73,13 @@ export class Network extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
title: "Port Forwarding",
|
||||||
|
component: PortForwards,
|
||||||
|
url: routes.portForwardsURL(),
|
||||||
|
routePath: routes.portForwardsRoute.path.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -236,8 +236,8 @@ export class Nodes extends React.Component<Props> {
|
|||||||
this.renderDiskUsage(node),
|
this.renderDiskUsage(node),
|
||||||
<>
|
<>
|
||||||
<span id={tooltipId}>{node.getTaints().length}</span>
|
<span id={tooltipId}>{node.getTaints().length}</span>
|
||||||
<Tooltip targetId={tooltipId} style={{ whiteSpace: "pre-line" }}>
|
<Tooltip targetId={tooltipId} tooltipOnParentHover={true} style={{ whiteSpace: "pre-line" }}>
|
||||||
{node.getTaints().map(({ key, effect }) => `${key}: ${effect}`).join("\n")}
|
{node.getTaints().map(({ key, value, effect }) => `${key}=${value}:${effect}`).join("\n")}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>,
|
</>,
|
||||||
node.getRoleLabels(),
|
node.getRoleLabels(),
|
||||||
|
|||||||
@ -72,6 +72,20 @@ export const Application = observer(() => {
|
|||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="terminalSelection">
|
||||||
|
<SubTitle title="Terminal copy & paste" />
|
||||||
|
<FormSwitch
|
||||||
|
label="Copy on select and paste on right-click"
|
||||||
|
control={
|
||||||
|
<Switcher
|
||||||
|
checked={UserStore.getInstance().terminalCopyOnSelect}
|
||||||
|
onChange={v => UserStore.getInstance().terminalCopyOnSelect = v.target.checked}
|
||||||
|
name="terminalCopyOnSelect"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<section id="other">
|
<section id="other">
|
||||||
|
|||||||
@ -75,6 +75,7 @@ export class DeploymentReplicaSets extends React.Component<Props> {
|
|||||||
sortable={this.sortingCallbacks}
|
sortable={this.sortingCallbacks}
|
||||||
sortByDefault={{ sortBy: sortBy.pods, orderBy: "desc" }}
|
sortByDefault={{ sortBy: sortBy.pods, orderBy: "desc" }}
|
||||||
sortSyncWithUrl={false}
|
sortSyncWithUrl={false}
|
||||||
|
tableId="deployment_replica_sets_view"
|
||||||
className="box grow"
|
className="box grow"
|
||||||
>
|
>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
|
|||||||
@ -112,6 +112,7 @@ const dummyDeployment: Deployment = {
|
|||||||
toPlainObject: jest.fn(),
|
toPlainObject: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<DeploymentScaleDialog />", () => {
|
describe("<DeploymentScaleDialog />", () => {
|
||||||
|
|||||||
@ -34,11 +34,13 @@
|
|||||||
color: $primary;
|
color: $primary;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-right: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Spinner {
|
.portInput {
|
||||||
--spinner-size: #{$unit * 2};
|
display: inline-block !important;
|
||||||
margin-left: $margin;
|
width: 70px;
|
||||||
position: absolute;
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,12 +22,15 @@
|
|||||||
import "./pod-container-port.scss";
|
import "./pod-container-port.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Pod } from "../../../common/k8s-api/endpoints";
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import { apiBase } from "../../api";
|
import { observable, makeObservable, reaction } from "mobx";
|
||||||
import { observable, makeObservable } from "mobx";
|
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
|
import { Button } from "../button";
|
||||||
|
import { addPortForward, getPortForward, openPortForward, PortForwardDialog, portForwardStore, removePortForward } from "../../port-forward";
|
||||||
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -42,19 +45,85 @@ interface Props {
|
|||||||
@observer
|
@observer
|
||||||
export class PodContainerPort extends React.Component<Props> {
|
export class PodContainerPort extends React.Component<Props> {
|
||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
|
@observable forwardPort = 0;
|
||||||
|
@observable isPortForwarded = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => [ portForwardStore.portForwards, this.props.pod ], () => this.init()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.checkExistingPortForwarding().catch(error => {
|
||||||
|
logger.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExistingPortForwarding() {
|
||||||
|
const { pod, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
const activePort = await getPortForward(portForward) ?? 0;
|
||||||
|
|
||||||
|
this.forwardPort = activePort;
|
||||||
|
this.isPortForwarded = activePort ? true : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.waiting = true;
|
||||||
|
this.isPortForwarded = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.forwardPort = await addPortForward(portForward);
|
||||||
|
|
||||||
|
if (this.forwardPort) {
|
||||||
|
portForward.forwardPort = this.forwardPort;
|
||||||
|
openPortForward(portForward);
|
||||||
|
this.isPortForwarded = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Notifications.error(error);
|
||||||
|
} finally {
|
||||||
|
this.waiting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopPortForward() {
|
||||||
|
const { pod, port } = this.props;
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await apiBase.post(`/pods/${pod.getNs()}/pod/${pod.getName()}/port-forward/${port.containerPort}`, {});
|
await removePortForward(portForward);
|
||||||
|
this.isPortForwarded = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(error);
|
Notifications.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@ -63,18 +132,35 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const { name, containerPort, protocol } = port;
|
const { name, containerPort, protocol } = port;
|
||||||
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
||||||
|
|
||||||
|
const portForwardAction = async () => {
|
||||||
|
if (this.isPortForwarded) {
|
||||||
|
await this.stopPortForward();
|
||||||
|
} else {
|
||||||
|
const portForward: ForwardedPort = {
|
||||||
|
kind: "pod",
|
||||||
|
name: pod.getName(),
|
||||||
|
namespace: pod.getNs(),
|
||||||
|
port: port.containerPort,
|
||||||
|
forwardPort: this.forwardPort,
|
||||||
|
};
|
||||||
|
|
||||||
|
PortForwardDialog.open(portForward, { openInBrowser: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward()}>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{text}
|
{text}
|
||||||
|
</span>
|
||||||
|
<Button onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
||||||
{this.waiting && (
|
{this.waiting && (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
)}
|
)}
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,8 @@ import { ContainerCharts } from "./container-charts";
|
|||||||
import { LocaleDate } from "../locale-date";
|
import { LocaleDate } from "../locale-date";
|
||||||
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
import { getActiveClusterEntity } from "../../api/catalog-entity-registry";
|
||||||
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
import { ClusterMetricsResourceType } from "../../../common/cluster-types";
|
||||||
|
import { portForwardStore } from "../../port-forward/port-forward.store";
|
||||||
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pod: Pod;
|
pod: Pod;
|
||||||
@ -42,8 +44,15 @@ interface Props {
|
|||||||
metrics?: { [key: string]: IMetrics };
|
metrics?: { [key: string]: IMetrics };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
export class PodDetailsContainer extends React.Component<Props> {
|
export class PodDetailsContainer extends React.Component<Props> {
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
portForwardStore.watch(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
renderStatus(state: string, status: IPodContainerStatus) {
|
renderStatus(state: string, status: IPodContainerStatus) {
|
||||||
const ready = status ? status.ready : "";
|
const ready = status ? status.ready : "";
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,7 @@ const dummyReplicaSet: ReplicaSet = {
|
|||||||
toPlainObject: jest.fn(),
|
toPlainObject: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<ReplicaSetScaleDialog />", () => {
|
describe("<ReplicaSetScaleDialog />", () => {
|
||||||
|
|||||||
@ -117,6 +117,7 @@ const dummyStatefulSet: StatefulSet = {
|
|||||||
toPlainObject: jest.fn(),
|
toPlainObject: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<StatefulSetScaleDialog />", () => {
|
describe("<StatefulSetScaleDialog />", () => {
|
||||||
|
|||||||
@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import React from "react";
|
||||||
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
|
import { CommandOverlay } from "../command-palette";
|
||||||
|
import { Select } from "../select";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ActivateEntityCommand extends React.Component {
|
||||||
|
@computed get options() {
|
||||||
|
return catalogEntityRegistry.items.map(entity => ({
|
||||||
|
label: `${entity.kind}: ${entity.getName()}`,
|
||||||
|
value: entity,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelect(entity: CatalogEntity): void {
|
||||||
|
catalogEntityRegistry.onRun(entity);
|
||||||
|
CommandOverlay.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
menuPortalTarget={null}
|
||||||
|
onChange={(v) => this.onSelect(v.value)}
|
||||||
|
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||||
|
menuIsOpen={true}
|
||||||
|
options={this.options}
|
||||||
|
autoFocus={true}
|
||||||
|
escapeClearsValue={false}
|
||||||
|
placeholder="Activate entity ..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/renderer/components/activate-entity-command/index.ts
Normal file
22
src/renderer/components/activate-entity-command/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from "./activate-entity-command";
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observable, makeObservable, reaction } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "../navigation";
|
import { history } from "../navigation";
|
||||||
@ -73,6 +73,8 @@ import { getHostedClusterId } from "../utils";
|
|||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import type { ClusterId } from "../../common/cluster-types";
|
import type { ClusterId } from "../../common/cluster-types";
|
||||||
import { watchHistoryState } from "../remote-helpers/history-updater";
|
import { watchHistoryState } from "../remote-helpers/history-updater";
|
||||||
|
import { unmountComponentAtNode } from "react-dom";
|
||||||
|
import { PortForwardDialog } from "../port-forward";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -83,7 +85,7 @@ export class App extends React.Component {
|
|||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
static async init() {
|
static async init(rootElem: HTMLElement) {
|
||||||
catalogEntityRegistry.init();
|
catalogEntityRegistry.init();
|
||||||
const frameId = webFrame.routingId;
|
const frameId = webFrame.routingId;
|
||||||
|
|
||||||
@ -97,13 +99,7 @@ export class App extends React.Component {
|
|||||||
|
|
||||||
await cluster.whenReady; // cluster.activate() is done at this point
|
await cluster.whenReady; // cluster.activate() is done at this point
|
||||||
|
|
||||||
const activeEntityDisposer = reaction(() => catalogEntityRegistry.getById(App.clusterId), (entity) => {
|
catalogEntityRegistry.activeEntity = App.clusterId;
|
||||||
if (!entity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
catalogEntityRegistry.activeEntity = entity;
|
|
||||||
activeEntityDisposer();
|
|
||||||
}, {fireImmediately: true});
|
|
||||||
|
|
||||||
ExtensionLoader.getInstance().loadOnClusterRenderer();
|
ExtensionLoader.getInstance().loadOnClusterRenderer();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -118,6 +114,20 @@ export class App extends React.Component {
|
|||||||
window.addEventListener("online", () => {
|
window.addEventListener("online", () => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("message", (ev: MessageEvent) => {
|
||||||
|
if (ev.data === "teardown") {
|
||||||
|
unmountComponentAtNode(rootElem);
|
||||||
|
window.location.href = "about:blank";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.onbeforeunload = () => {
|
||||||
|
logger.info(`[APP]: Unload dashboard, clusterId=${App.clusterId}, frameId=${frameId}`);
|
||||||
|
|
||||||
|
unmountComponentAtNode(rootElem);
|
||||||
|
};
|
||||||
|
|
||||||
whatInput.ask(); // Start to monitor user input device
|
whatInput.ask(); // Start to monitor user input device
|
||||||
|
|
||||||
// Setup hosted cluster context
|
// Setup hosted cluster context
|
||||||
@ -222,6 +232,7 @@ export class App extends React.Component {
|
|||||||
<StatefulSetScaleDialog/>
|
<StatefulSetScaleDialog/>
|
||||||
<ReplicaSetScaleDialog/>
|
<ReplicaSetScaleDialog/>
|
||||||
<CronJobTriggerDialog/>
|
<CronJobTriggerDialog/>
|
||||||
|
<PortForwardDialog/>
|
||||||
<CommandContainer clusterId={App.clusterId}/>
|
<CommandContainer clusterId={App.clusterId}/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import React from "react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { BottomBar } from "./bottom-bar";
|
import { BottomBar } from "./bottom-bar";
|
||||||
import { Catalog } from "../+catalog";
|
import { Catalog, previousActiveTab } from "../+catalog";
|
||||||
import { Preferences } from "../+preferences";
|
import { Preferences } from "../+preferences";
|
||||||
import { AddCluster } from "../+add-cluster";
|
import { AddCluster } from "../+add-cluster";
|
||||||
import { ClusterView } from "./cluster-view";
|
import { ClusterView } from "./cluster-view";
|
||||||
@ -40,6 +40,7 @@ import { reaction } from "mobx";
|
|||||||
import { navigation } from "../../navigation";
|
import { navigation } from "../../navigation";
|
||||||
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
|
||||||
import { TopBar } from "../layout/topbar";
|
import { TopBar } from "../layout/topbar";
|
||||||
|
import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClusterManager extends React.Component {
|
export class ClusterManager extends React.Component {
|
||||||
@ -56,6 +57,7 @@ export class ClusterManager extends React.Component {
|
|||||||
<main>
|
<main>
|
||||||
<div id="lens-views"/>
|
<div id="lens-views"/>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
<Redirect exact from={catalogURL()} to={getPreviousTabUrl(previousActiveTab.get())}/>
|
||||||
<Route component={Welcome} {...routes.welcomeRoute} />
|
<Route component={Welcome} {...routes.welcomeRoute} />
|
||||||
<Route component={Catalog} {...routes.catalogRoute} />
|
<Route component={Catalog} {...routes.catalogRoute} />
|
||||||
<Route component={Preferences} {...routes.preferencesRoute} />
|
<Route component={Preferences} {...routes.preferencesRoute} />
|
||||||
|
|||||||
@ -33,7 +33,6 @@ import { clusterActivateHandler } from "../../../common/cluster-ipc";
|
|||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { navigate } from "../../navigation";
|
import { navigate } from "../../navigation";
|
||||||
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
import { catalogURL, ClusterViewRouteParams } from "../../../common/routes";
|
||||||
import { previousActiveTab } from "../+catalog";
|
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<ClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -85,7 +84,7 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
const disconnected = values[1];
|
const disconnected = values[1];
|
||||||
|
|
||||||
if (hasLoadedView(this.clusterId) && disconnected) {
|
if (hasLoadedView(this.clusterId) && disconnected) {
|
||||||
navigate(`${catalogURL()}/${previousActiveTab.get()}`); // redirect to catalog when active cluster get disconnected/not available
|
navigate(catalogURL()); // redirect to catalog when active cluster get disconnected/not available
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -25,9 +25,11 @@ import { Pod } from "../../../../common/k8s-api/endpoints";
|
|||||||
import { ThemeStore } from "../../../theme.store";
|
import { ThemeStore } from "../../../theme.store";
|
||||||
import { dockStore } from "../dock.store";
|
import { dockStore } from "../dock.store";
|
||||||
import { logTabStore } from "../log-tab.store";
|
import { logTabStore } from "../log-tab.store";
|
||||||
import { TerminalStore } from "../terminal.store";
|
|
||||||
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
|
import { mockWindow } from "../../../../../__mocks__/windowMock";
|
||||||
|
|
||||||
|
mockWindow();
|
||||||
|
|
||||||
jest.mock("react-monaco-editor", () => null);
|
jest.mock("react-monaco-editor", () => null);
|
||||||
|
|
||||||
@ -45,7 +47,6 @@ describe("log tab store", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UserStore.createInstance();
|
UserStore.createInstance();
|
||||||
ThemeStore.createInstance();
|
ThemeStore.createInstance();
|
||||||
TerminalStore.createInstance();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -53,7 +54,6 @@ describe("log tab store", () => {
|
|||||||
dockStore.reset();
|
dockStore.reset();
|
||||||
UserStore.resetInstance();
|
UserStore.resetInstance();
|
||||||
ThemeStore.resetInstance();
|
ThemeStore.resetInstance();
|
||||||
TerminalStore.resetInstance();
|
|
||||||
fse.remove("tmp");
|
fse.remove("tmp");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -135,11 +135,11 @@ describe("log tab store", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// FIXME: this is failed when it's not .only == depends on something above
|
// FIXME: this is failed when it's not .only == depends on something above
|
||||||
it.only("closes tab if no pods left in store", () => {
|
it.only("closes tab if no pods left in store", async () => {
|
||||||
const selectedPod = new Pod(deploymentPod1);
|
const selectedPod = new Pod(deploymentPod1);
|
||||||
const selectedContainer = selectedPod.getInitContainers()[0];
|
const selectedContainer = selectedPod.getInitContainers()[0];
|
||||||
|
|
||||||
logTabStore.createPodTab({
|
const id = logTabStore.createPodTab({
|
||||||
selectedPod,
|
selectedPod,
|
||||||
selectedContainer
|
selectedContainer
|
||||||
});
|
});
|
||||||
@ -147,6 +147,7 @@ describe("log tab store", () => {
|
|||||||
podsStore.items.clear();
|
podsStore.items.clear();
|
||||||
|
|
||||||
expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
|
expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
|
||||||
expect(dockStore.getTabById(dockStore.selectedTabId)).toBeUndefined();
|
expect(logTabStore.getData(id)).toBeUndefined();
|
||||||
|
expect(dockStore.getTabById(id)).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,10 +33,10 @@ import { createResourceStore } from "./create-resource.store";
|
|||||||
import type { DockTab } from "./dock.store";
|
import type { DockTab } from "./dock.store";
|
||||||
import { EditorPanel } from "./editor-panel";
|
import { EditorPanel } from "./editor-panel";
|
||||||
import { InfoPanel } from "./info-panel";
|
import { InfoPanel } from "./info-panel";
|
||||||
import { resourceApplierApi } from "../../../common/k8s-api/endpoints/resource-applier.api";
|
import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api";
|
||||||
import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { monacoModelsManager } from "./monaco-model-manager";
|
import { monacoModelsManager } from "./monaco-model-manager";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -95,7 +95,7 @@ export class CreateResource extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
create = async () => {
|
create = async (): Promise<undefined> => {
|
||||||
if (this.error || !this.data.trim()) {
|
if (this.error || !this.data.trim()) {
|
||||||
// do not save when field is empty or there is an error
|
// do not save when field is empty or there is an error
|
||||||
return null;
|
return null;
|
||||||
@ -103,31 +103,31 @@ export class CreateResource extends React.Component<Props> {
|
|||||||
|
|
||||||
// skip empty documents if "---" pasted at the beginning or end
|
// skip empty documents if "---" pasted at the beginning or end
|
||||||
const resources = jsYaml.safeLoadAll(this.data).filter(Boolean);
|
const resources = jsYaml.safeLoadAll(this.data).filter(Boolean);
|
||||||
const createdResources: string[] = [];
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
await Promise.all(
|
if (resources.length === 0) {
|
||||||
resources.map(data => {
|
return void logger.info("Nothing to create");
|
||||||
return resourceApplierApi.update(data)
|
|
||||||
.then(item => createdResources.push(item.metadata.name))
|
|
||||||
.catch((err: JsonApiErrorParsed) => errors.push(err.toString()));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
errors.forEach(error => Notifications.error(error));
|
|
||||||
if (!createdResources.length) throw errors[0];
|
|
||||||
}
|
}
|
||||||
const successMessage = (
|
|
||||||
|
const createdResources: string[] = [];
|
||||||
|
|
||||||
|
for (const result of await Promise.allSettled(resources.map(resourceApplierApi.update))) {
|
||||||
|
if (result.status === "fulfilled") {
|
||||||
|
createdResources.push(result.value.metadata.name);
|
||||||
|
} else {
|
||||||
|
Notifications.error(result.reason.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdResources.length > 0) {
|
||||||
|
Notifications.ok((
|
||||||
<p>
|
<p>
|
||||||
{createdResources.length === 1 ? "Resource" : "Resources"}{" "}
|
{createdResources.length === 1 ? "Resource" : "Resources"}{" "}
|
||||||
<b>{createdResources.join(", ")}</b> successfully created
|
<b>{createdResources.join(", ")}</b> successfully created
|
||||||
</p>
|
</p>
|
||||||
);
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Notifications.ok(successMessage);
|
return undefined;
|
||||||
|
|
||||||
return successMessage;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renderControls(){
|
renderControls(){
|
||||||
|
|||||||
@ -287,7 +287,7 @@ export class DockStore implements DockStorageState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async closeTab(tabId: TabId) {
|
closeTab(tabId: TabId) {
|
||||||
const tab = this.getTabById(tabId);
|
const tab = this.getTabById(tabId);
|
||||||
|
|
||||||
if (!tab || tab.pinned) {
|
if (!tab || tab.pinned) {
|
||||||
@ -305,12 +305,6 @@ export class DockStore implements DockStorageState {
|
|||||||
if (this.tabs.length) {
|
if (this.tabs.length) {
|
||||||
const newTab = this.tabs.slice(-1)[0]; // last
|
const newTab = this.tabs.slice(-1)[0]; // last
|
||||||
|
|
||||||
if (newTab?.kind === TabKind.TERMINAL) {
|
|
||||||
// close the dock when selected sibling inactive terminal tab
|
|
||||||
const { TerminalStore } = await import("./terminal.store");
|
|
||||||
|
|
||||||
if (!TerminalStore.getInstance(false)?.isConnected(newTab.id)) this.close();
|
|
||||||
}
|
|
||||||
this.selectTab(newTab.id);
|
this.selectTab(newTab.id);
|
||||||
} else {
|
} else {
|
||||||
this.selectedTabId = null;
|
this.selectedTabId = null;
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {monacoModelsManager} from "./monaco-model-manager";
|
|||||||
export interface EditingResource {
|
export interface EditingResource {
|
||||||
resource: string; // resource path, e.g. /api/v1/namespaces/default
|
resource: string; // resource path, e.g. /api/v1/namespaces/default
|
||||||
draft?: string; // edited draft in yaml
|
draft?: string; // edited draft in yaml
|
||||||
|
firstDraft?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditResourceStore extends DockTabStore<EditingResource> {
|
export class EditResourceStore extends DockTabStore<EditingResource> {
|
||||||
@ -106,6 +107,10 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
|
|||||||
return dockStore.getTabById(tabId);
|
return dockStore.getTabById(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearInitialDraft(tabId: TabId): void {
|
||||||
|
delete this.getData(tabId)?.firstDraft;
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
super.reset();
|
super.reset();
|
||||||
Array.from(this.watchers).forEach(([tabId, dispose]) => {
|
Array.from(this.watchers).forEach(([tabId, dispose]) => {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import "./edit-resource.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { action, computed, makeObservable, observable } from "mobx";
|
import { action, computed, makeObservable, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import jsYaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import type { DockTab } from "./dock.store";
|
import type { DockTab } from "./dock.store";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { editResourceStore } from "./edit-resource.store";
|
import { editResourceStore } from "./edit-resource.store";
|
||||||
@ -33,6 +33,7 @@ import { Badge } from "../badge";
|
|||||||
import { EditorPanel } from "./editor-panel";
|
import { EditorPanel } from "./editor-panel";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
|
import { createPatch } from "rfc6902";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -71,16 +72,17 @@ export class EditResource extends React.Component<Props> {
|
|||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time
|
return yaml.safeDump(this.resource.toPlainObject()); // dump resource first time
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
saveDraft(draft: string | object) {
|
saveDraft(draft: string | object) {
|
||||||
if (typeof draft === "object") {
|
if (typeof draft === "object") {
|
||||||
draft = draft ? jsYaml.safeDump(draft) : undefined;
|
draft = draft ? yaml.safeDump(draft) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
editResourceStore.setData(this.tabId, {
|
editResourceStore.setData(this.tabId, {
|
||||||
|
firstDraft: draft, // this must be before the next line
|
||||||
...editResourceStore.getData(this.tabId),
|
...editResourceStore.getData(this.tabId),
|
||||||
draft,
|
draft,
|
||||||
});
|
});
|
||||||
@ -95,16 +97,18 @@ export class EditResource extends React.Component<Props> {
|
|||||||
if (this.error) {
|
if (this.error) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const store = editResourceStore.getStore(this.tabId);
|
|
||||||
const updatedResource: KubeObject = await store.update(this.resource, jsYaml.safeLoad(this.draft));
|
|
||||||
|
|
||||||
this.saveDraft(updatedResource.toPlainObject()); // update with new resourceVersion to avoid further errors on save
|
const store = editResourceStore.getStore(this.tabId);
|
||||||
const resourceType = updatedResource.kind;
|
const currentVersion = yaml.safeLoad(this.draft);
|
||||||
const resourceName = updatedResource.getName();
|
const firstVersion = yaml.safeLoad(editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
|
||||||
|
const patches = createPatch(firstVersion, currentVersion);
|
||||||
|
const updatedResource = await store.patch(this.resource, patches);
|
||||||
|
|
||||||
|
editResourceStore.clearInitialDraft(this.tabId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
{resourceType} <b>{resourceName}</b> updated.
|
{updatedResource.kind} <b>{updatedResource.getName()}</b> updated.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -91,10 +91,7 @@ export class EditorPanel extends React.Component<Props> {
|
|||||||
|
|
||||||
onChange = (value: string) => {
|
onChange = (value: string) => {
|
||||||
this.validate(value);
|
this.validate(value);
|
||||||
|
this.props.onChange?.(value, this.yamlError);
|
||||||
if (this.props.onChange) {
|
|
||||||
this.props.onChange(value, this.yamlError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import { podsStore } from "../+workloads-pods/pods.store";
|
|||||||
|
|
||||||
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
|
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
|
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
import { DockTabStore } from "./dock-tab.store";
|
import { DockTabStore } from "./dock-tab.store";
|
||||||
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
|
||||||
|
|
||||||
@ -51,17 +52,15 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
storageKey: "pod_logs"
|
storageKey: "pod_logs"
|
||||||
});
|
});
|
||||||
|
|
||||||
reaction(() => podsStore.items.length, () => {
|
reaction(() => podsStore.items.length, () => this.updateTabsData());
|
||||||
this.updateTabsData();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void {
|
createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {
|
||||||
const podOwner = selectedPod.getOwnerRefs()[0];
|
const podOwner = selectedPod.getOwnerRefs()[0];
|
||||||
const pods = podsStore.getPodsByOwnerId(podOwner?.uid);
|
const pods = podsStore.getPodsByOwnerId(podOwner?.uid);
|
||||||
const title = `Pod ${selectedPod.getName()}`;
|
const title = `Pod ${selectedPod.getName()}`;
|
||||||
|
|
||||||
this.createLogsTab(title, {
|
return this.createLogsTab(title, {
|
||||||
pods: pods.length ? pods : [selectedPod],
|
pods: pods.length ? pods : [selectedPod],
|
||||||
selectedPod,
|
selectedPod,
|
||||||
selectedContainer
|
selectedContainer
|
||||||
@ -97,7 +96,7 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
}, false);
|
}, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createLogsTab(title: string, data: LogTabData) {
|
private createLogsTab(title: string, data: LogTabData): string {
|
||||||
const id = uniqueId("log-tab-");
|
const id = uniqueId("log-tab-");
|
||||||
|
|
||||||
this.createDockTab({ id, title });
|
this.createDockTab({ id, title });
|
||||||
@ -106,19 +105,24 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
showTimestamps: false,
|
showTimestamps: false,
|
||||||
previous: false
|
previous: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateTabsData() {
|
private updateTabsData() {
|
||||||
const promises: Promise<void>[] = [];
|
|
||||||
|
|
||||||
for (const [tabId, tabData] of this.data) {
|
for (const [tabId, tabData] of this.data) {
|
||||||
|
try {
|
||||||
|
if (!tabData.selectedPod) {
|
||||||
|
tabData.selectedPod = tabData.pods[0];
|
||||||
|
}
|
||||||
|
|
||||||
const pod = new Pod(tabData.selectedPod);
|
const pod = new Pod(tabData.selectedPod);
|
||||||
const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
|
const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
|
||||||
const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
|
const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
|
||||||
const selectedPod = isSelectedPodInList ? pod : pods[0];
|
const selectedPod = isSelectedPodInList ? pod : pods[0];
|
||||||
const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0];
|
const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0];
|
||||||
|
|
||||||
if (pods.length) {
|
if (pods.length > 0) {
|
||||||
this.setData(tabId, {
|
this.setData(tabId, {
|
||||||
...tabData,
|
...tabData,
|
||||||
selectedPod,
|
selectedPod,
|
||||||
@ -128,16 +132,18 @@ export class LogTabStore extends DockTabStore<LogTabData> {
|
|||||||
|
|
||||||
this.renameTab(tabId);
|
this.renameTab(tabId);
|
||||||
} else {
|
} else {
|
||||||
promises.push(this.closeTab(tabId));
|
this.closeTab(tabId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[LOG-TAB-STORE]: failed to set data for tabId=${tabId} deleting`, error,);
|
||||||
|
this.data.delete(tabId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
private closeTab(tabId: string) {
|
||||||
}
|
|
||||||
|
|
||||||
private async closeTab(tabId: string) {
|
|
||||||
this.clearData(tabId);
|
this.clearData(tabId);
|
||||||
await dockStore.closeTab(tabId);
|
dockStore.closeTab(tabId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,8 @@ import { ThemeStore } from "../../theme.store";
|
|||||||
import { boundMethod } from "../../utils";
|
import { boundMethod } from "../../utils";
|
||||||
import { isMac } from "../../../common/vars";
|
import { isMac } from "../../../common/vars";
|
||||||
import { camelCase } from "lodash";
|
import { camelCase } from "lodash";
|
||||||
|
import { UserStore } from "../../../common/user-store";
|
||||||
|
import { clipboard } from "electron";
|
||||||
|
|
||||||
export class Terminal {
|
export class Terminal {
|
||||||
static spawningPool: HTMLElement;
|
static spawningPool: HTMLElement;
|
||||||
@ -115,11 +117,13 @@ export class Terminal {
|
|||||||
this.xterm.open(Terminal.spawningPool);
|
this.xterm.open(Terminal.spawningPool);
|
||||||
this.xterm.registerLinkMatcher(/https?:\/\/[^\s]+/i, this.onClickLink);
|
this.xterm.registerLinkMatcher(/https?:\/\/[^\s]+/i, this.onClickLink);
|
||||||
this.xterm.attachCustomKeyEventHandler(this.keyHandler);
|
this.xterm.attachCustomKeyEventHandler(this.keyHandler);
|
||||||
|
this.xterm.onSelectionChange(this.onSelectionChange);
|
||||||
|
|
||||||
// bind events
|
// bind events
|
||||||
const onDataHandler = this.xterm.onData(this.onData);
|
const onDataHandler = this.xterm.onData(this.onData);
|
||||||
|
|
||||||
this.viewport.addEventListener("scroll", this.onScroll);
|
this.viewport.addEventListener("scroll", this.onScroll);
|
||||||
|
this.elem.addEventListener("contextmenu", this.onContextMenu);
|
||||||
this.api.onReady.addListener(this.onClear, { once: true }); // clear status logs (connecting..)
|
this.api.onReady.addListener(this.onClear, { once: true }); // clear status logs (connecting..)
|
||||||
this.api.onData.addListener(this.onApiData);
|
this.api.onData.addListener(this.onApiData);
|
||||||
window.addEventListener("resize", this.onResize);
|
window.addEventListener("resize", this.onResize);
|
||||||
@ -133,6 +137,7 @@ export class Terminal {
|
|||||||
() => this.fitAddon.dispose(),
|
() => this.fitAddon.dispose(),
|
||||||
() => this.api.removeAllListeners(),
|
() => this.api.removeAllListeners(),
|
||||||
() => window.removeEventListener("resize", this.onResize),
|
() => window.removeEventListener("resize", this.onResize),
|
||||||
|
() => this.elem.removeEventListener("contextmenu", this.onContextMenu),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,6 +203,24 @@ export class Terminal {
|
|||||||
window.open(link, "_blank");
|
window.open(link, "_blank");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onContextMenu = () => {
|
||||||
|
const { terminalCopyOnSelect } = UserStore.getInstance();
|
||||||
|
const textFromClipboard = clipboard.readText();
|
||||||
|
|
||||||
|
if (terminalCopyOnSelect) {
|
||||||
|
this.xterm.paste(textFromClipboard);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectionChange = () => {
|
||||||
|
const { terminalCopyOnSelect } = UserStore.getInstance();
|
||||||
|
const selection = this.xterm.getSelection().trim();
|
||||||
|
|
||||||
|
if (terminalCopyOnSelect && selection !== "") {
|
||||||
|
clipboard.writeText(selection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
keyHandler = (evt: KeyboardEvent): boolean => {
|
keyHandler = (evt: KeyboardEvent): boolean => {
|
||||||
const { code, ctrlKey, metaKey } = evt;
|
const { code, ctrlKey, metaKey } = evt;
|
||||||
|
|
||||||
|
|||||||
@ -31,12 +31,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.HotbarIcon {
|
.HotbarIcon {
|
||||||
|
--iconActiveShadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
|
||||||
|
--iconHoverShadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50;
|
||||||
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: none;
|
transition: none;
|
||||||
text-shadow: 0 0 4px #0000008f;
|
text-shadow: 0 0 4px #0000008f;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 0; // allows to catch state of :active pseudo-selector
|
||||||
|
|
||||||
|
&:active .MuiAvatar-root {
|
||||||
|
box-shadow: var(--iconActiveShadow) !important;
|
||||||
|
}
|
||||||
|
|
||||||
div.MuiAvatar-colorDefault {
|
div.MuiAvatar-colorDefault {
|
||||||
font-weight:500;
|
font-weight:500;
|
||||||
@ -64,20 +72,17 @@
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.MuiAvatar-root {
|
.MuiAvatar-root {
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
|
box-shadow: var(--iconActiveShadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.interactive {
|
&.interactive:not(.active):hover {
|
||||||
&:hover {
|
box-shadow: var(--iconHoverShadow);
|
||||||
&:not(.active) {
|
|
||||||
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,20 +122,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
|
||||||
border-radius: 6px;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px var(--textColorAccent);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&:not(.active) {
|
|
||||||
box-shadow: 0 0 0px 3px var(--clusterMenuBackground), 0 0 0px 6px #ffffff50;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.materialIcon {
|
.materialIcon {
|
||||||
margin-left: 1px;
|
margin-left: 1px;
|
||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { HotbarEntityIcon } from "./hotbar-entity-icon";
|
|||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
import { HotbarStore } from "../../../common/hotbar-store";
|
||||||
import { CatalogEntity, catalogEntityRunContext } from "../../api/catalog-entity";
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, Droppable, DropResult } from "react-beautiful-dnd";
|
||||||
import { HotbarSelector } from "./hotbar-selector";
|
import { HotbarSelector } from "./hotbar-selector";
|
||||||
import { HotbarCell } from "./hotbar-cell";
|
import { HotbarCell } from "./hotbar-cell";
|
||||||
@ -124,7 +124,7 @@ export class HotbarMenu extends React.Component<Props> {
|
|||||||
key={index}
|
key={index}
|
||||||
index={index}
|
index={index}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
onClick={() => entity.onRun(catalogEntityRunContext)}
|
onClick={() => catalogEntityRegistry.onRun(entity)}
|
||||||
className={cssNames({ isDragging: snapshot.isDragging })}
|
className={cssNames({ isDragging: snapshot.isDragging })}
|
||||||
remove={this.removeItem}
|
remove={this.removeItem}
|
||||||
add={this.addItem}
|
add={this.addItem}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user