mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into improve-dock-tabs-ux
# Conflicts: # src/renderer/components/dock/dock-tabs.tsx
This commit is contained in:
commit
7d47861ddc
33
.eslintrc.js
33
.eslintrc.js
@ -129,6 +129,16 @@ module.exports = {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/member-delimiter-style": ["error", {
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true,
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false,
|
||||
},
|
||||
}],
|
||||
"react/display-name": "off",
|
||||
"space-before-function-paren": "off",
|
||||
"@typescript-eslint/space-before-function-paren": ["error", {
|
||||
@ -136,6 +146,29 @@ module.exports = {
|
||||
"named": "never",
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"@typescript-eslint/naming-convention": ["error",
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^Props$",
|
||||
"match": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"selector": "typeAlias",
|
||||
"format": ["PascalCase"],
|
||||
"leadingUnderscore": "forbid",
|
||||
"trailingUnderscore": "forbid",
|
||||
"custom": {
|
||||
"regex": "^(Props|State)$",
|
||||
"match": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/consistent-type-definitions": ["error", "interface"],
|
||||
"unused-imports/no-unused-imports-ts": process.env.PROD === "true" ? "error" : "warn",
|
||||
"unused-imports/no-unused-vars-ts": [
|
||||
"warn", {
|
||||
|
||||
28
.github/workflows/electronegativity.yml
vendored
Normal file
28
.github/workflows/electronegativity.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Electronegativity
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_job:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "14"
|
||||
|
||||
- uses: doyensec/electronegativity-action@v1.1
|
||||
with:
|
||||
input: src/
|
||||
electron-version: "14.2.4"
|
||||
severity: medium
|
||||
|
||||
- name: Upload sarif
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
sarif_file: ../results
|
||||
25
.github/workflows/main.yml
vendored
25
.github/workflows/main.yml
vendored
@ -6,6 +6,9 @@ on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
concurrency:
|
||||
group: publish-docs
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
verify-docs:
|
||||
name: Verify docs
|
||||
@ -17,7 +20,7 @@ jobs:
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Checkout Release from lens
|
||||
uses: actions/checkout@v2
|
||||
@ -31,8 +34,8 @@ jobs:
|
||||
|
||||
- name: Generate Extensions API Reference using typedocs
|
||||
run: |
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
- name: Verify that the markdown is valid
|
||||
run: |
|
||||
@ -49,7 +52,7 @@ jobs:
|
||||
- name: Set up Python 3.7
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
python-version: "3.x"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@ -64,8 +67,8 @@ jobs:
|
||||
|
||||
- name: git config
|
||||
run: |
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
git config --local user.email "action@github.com"
|
||||
git config --local user.name "GitHub Action"
|
||||
|
||||
- name: Using Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
@ -74,13 +77,13 @@ jobs:
|
||||
|
||||
- name: Generate Extensions API Reference using typedocs
|
||||
run: |
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
yarn install
|
||||
yarn typedocs-extensions-api
|
||||
|
||||
- name: mkdocs deploy master
|
||||
if: contains(github.ref, 'refs/heads/master')
|
||||
run: |
|
||||
mike deploy --push master
|
||||
mike deploy --push master
|
||||
|
||||
- name: Get the release version
|
||||
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
@ -90,5 +93,5 @@ jobs:
|
||||
- name: mkdocs deploy new release
|
||||
if: contains(github.ref, 'refs/tags/v') && !github.event.release.prerelease
|
||||
run: |
|
||||
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
||||
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
||||
mike deploy --push --update-aliases ${{ steps.get_version.outputs.VERSION }} latest
|
||||
mike set-default --push ${{ steps.get_version.outputs.VERSION }}
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -42,7 +42,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- uses: nick-invision/retry@v2
|
||||
- uses: nick-fields/retry@v2
|
||||
name: Install dependencies
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
- run: make build-npm
|
||||
name: Generate npm package
|
||||
|
||||
- uses: nick-invision/retry@v2
|
||||
- uses: nick-fields/retry@v2
|
||||
name: Build bundled extensions
|
||||
with:
|
||||
timeout_minutes: 15
|
||||
|
||||
@ -6,7 +6,7 @@ Lens releases are built by CICD automatically on git tags. The typical release p
|
||||
|
||||
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.
|
||||
|
||||
This can be helped (if you have the `gh` CLI installed) by running:
|
||||
This can be helped (if you have the `gh` CLI and `jq` installed) by running:
|
||||
```
|
||||
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100" | jq -r '[.[] | select(.milestone.title == "<VERSION>") | select((.merged_at | type) == "string")] | sort_by(.merged_at) | map(.merge_commit_sha) | join(" ")'
|
||||
```
|
||||
@ -36,11 +36,12 @@ But you will probably need to verify that all the PRs have correct milestones.
|
||||
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100" | jq -r '[.[] | select(.milestone.title == "<VERSION>") | select((.merged_at | type) == "string")] | sort_by(.merged_at) | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' | pbcopy
|
||||
```
|
||||
|
||||
And if you want to specify just bug fixes then you can add the following to the end of the `[ ... ]` section in the above command (before the `sort_by`) to just have bug fixes. Switch to `== "enhancement"` for enhancements and `all()` with `. != "bug && . != "enhancement"` for maintanence sections.
|
||||
And if you want to specify just bug fixes then you can add the following to the end of the `[ ... ]` section in the above command (before the `sort_by`) to just have bug fixes. Switch to `== "enhancement"` for enhancements and `all()` instead of `any()` with `. != "bug and . != "enhancement"` for maintanence sections.
|
||||
|
||||
```
|
||||
| select(any(.labels | map(.name)[]; . == "bug"))
|
||||
```
|
||||
See notes below for ideas for building changelogs.
|
||||
|
||||
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:
|
||||
@ -64,3 +65,31 @@ Other tasks
|
||||
- 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)
|
||||
- update issues on github (bump those that did not make it into the release to a subsequent release) (for major/minor/patch releases)
|
||||
|
||||
|
||||
### Rough Notes
|
||||
|
||||
To get all of the PRs since a certain date you can use something like:
|
||||
```
|
||||
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100&q=committer-date:>YYYY-MM-DD" > prs.json
|
||||
```
|
||||
|
||||
If there are more than 100 PRs you'll need to rerun for multiple pages:
|
||||
```
|
||||
gh api -XGET "/repos/lensapp/lens/pulls?state=closed&per_page=100&page=2&q=committer-date:>YYYY-MM-DD" >> prs.json
|
||||
```
|
||||
|
||||
To get all the bug PRs:
|
||||
```
|
||||
jq -r '[.[] | select((.merged_at | type) == "string") | select(any(.labels | map(.name)[]; . == "bug"))] | sort_by(.merged_at) | reverse | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' prs.json
|
||||
```
|
||||
|
||||
To get all the enhancement PRs:
|
||||
```
|
||||
jq -r '[.[] | select((.merged_at | type) == "string") | select(any(.labels | map(.name)[]; . == "enhancement"))] | sort_by(.merged_at) | reverse | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' prs.json
|
||||
```
|
||||
|
||||
To get all of the maintenance PRs:
|
||||
```
|
||||
jq -r '[.[] | select((.merged_at | type) == "string") | select(all(.labels | map(.name)[]; . != "bug" and . != "enhancement" and . != "skip-changelog"))] | sort_by(.merged_at) | reverse | map([.title, " (**#", .number, "**) ", .user.html_url] | join ("")) | join("\n")' prs.json
|
||||
```
|
||||
@ -3,4 +3,4 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export default {};
|
||||
export default ""; // mostly path to bundled file or data-url (webpack)
|
||||
98
build/download_k8s_proxy.ts
Normal file
98
build/download_k8s_proxy.ts
Normal file
@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import packageInfo from "../package.json";
|
||||
import fs from "fs";
|
||||
import request from "request";
|
||||
import { ensureDir, pathExists } from "fs-extra";
|
||||
import path from "path";
|
||||
import { noop } from "lodash";
|
||||
import { isLinux, isMac } from "../src/common/vars";
|
||||
|
||||
class K8sProxyDownloader {
|
||||
public version: string;
|
||||
protected url: string;
|
||||
protected path: string;
|
||||
protected dirname: string;
|
||||
|
||||
constructor(version: string, platform: string, arch: string, target: string) {
|
||||
this.version = version;
|
||||
this.url = `https://github.com/lensapp/lens-k8s-proxy/releases/download/v${this.version}/lens-k8s-proxy-${platform}-${arch}`;
|
||||
this.dirname = path.dirname(target);
|
||||
this.path = target;
|
||||
}
|
||||
|
||||
public async checkBinary() {
|
||||
const exists = await pathExists(this.path);
|
||||
|
||||
if (exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async download() {
|
||||
if (await this.checkBinary()) {
|
||||
return console.log("Already exists");
|
||||
}
|
||||
|
||||
await ensureDir(path.dirname(this.path), 0o755);
|
||||
|
||||
const file = fs.createWriteStream(this.path);
|
||||
|
||||
console.log(`Downloading lens-k8s-proxy ${this.version} from ${this.url} to ${this.path}`);
|
||||
const requestOpts: request.UriOptions & request.CoreOptions = {
|
||||
uri: this.url,
|
||||
gzip: true,
|
||||
followAllRedirects: true,
|
||||
};
|
||||
const stream = request(requestOpts);
|
||||
|
||||
stream.on("complete", () => {
|
||||
console.log("lens-k8s-proxy binary download finished");
|
||||
file.end(noop);
|
||||
});
|
||||
|
||||
stream.on("error", (error) => {
|
||||
console.log(error);
|
||||
fs.unlink(this.path, noop);
|
||||
throw error;
|
||||
});
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
file.on("close", () => {
|
||||
console.log("lens-k8s-proxy binary download closed");
|
||||
fs.chmod(this.path, 0o755, (err) => {
|
||||
if (err) reject(err);
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
stream.pipe(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
const downloadVersion = packageInfo.config.k8sProxyVersion;
|
||||
const baseDir = path.join(__dirname, "..", "binaries", "client");
|
||||
|
||||
const downloads = [];
|
||||
|
||||
if (isMac) {
|
||||
downloads.push({ platform: "darwin", arch: "amd64", target: path.join(baseDir, "darwin", "x64", "lens-k8s-proxy") });
|
||||
downloads.push({ platform: "darwin", arch: "arm64", target: path.join(baseDir, "darwin", "arm64", "lens-k8s-proxy") });
|
||||
} else if (isLinux) {
|
||||
downloads.push({ platform: "linux", arch: "amd64", target: path.join(baseDir, "linux", "x64", "lens-k8s-proxy") });
|
||||
downloads.push({ platform: "linux", arch: "arm64", target: path.join(baseDir, "linux", "arm64", "lens-k8s-proxy") });
|
||||
} else {
|
||||
downloads.push({ platform: "windows", arch: "amd64", target: path.join(baseDir, "windows", "x64", "lens-k8s-proxy.exe") });
|
||||
downloads.push({ platform: "windows", arch: "386", target: path.join(baseDir, "windows", "ia32", "lens-k8s-proxy.exe") });
|
||||
}
|
||||
|
||||
downloads.forEach((dlOpts) => {
|
||||
console.log(dlOpts);
|
||||
const downloader = new K8sProxyDownloader(downloadVersion, dlOpts.platform, dlOpts.arch, dlOpts.target);
|
||||
|
||||
console.log(`Downloading: ${JSON.stringify(dlOpts)}`);
|
||||
downloader.download().then(() => downloader.checkBinary().then(() => console.log("Download complete")));
|
||||
});
|
||||
@ -724,11 +724,11 @@ const {
|
||||
|
||||
type Pod = Renderer.K8sApi.Pod;
|
||||
|
||||
interface Props {
|
||||
interface PodsDetailsListProps {
|
||||
pods?: Pod[];
|
||||
}
|
||||
|
||||
export class PodsDetailsList extends React.Component<Props> {
|
||||
export class PodsDetailsList extends React.Component<PodsDetailsListProps> {
|
||||
getTableRow = (pod: Pod) => {
|
||||
return (
|
||||
<TableRow key={index} nowrap>
|
||||
|
||||
@ -109,13 +109,13 @@ To allow the end-user to control the life cycle of this cluster feature the foll
|
||||
}
|
||||
} = Renderer;
|
||||
|
||||
interface Props {
|
||||
interface ExampleClusterFeatureSettingsProps {
|
||||
cluster: Common.Catalog.KubernetesCluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ExampleClusterFeatureSettings extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
export class ExampleClusterFeatureSettings extends React.Component<ExampleClusterFeatureSettingsProps> {
|
||||
constructor(props: ExampleClusterFeatureSettingsProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
3404
extensions/kube-object-event-status/package-lock.json
generated
3404
extensions/kube-object-event-status/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,8 +18,8 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.46.0"
|
||||
"ts-loader": "latest",
|
||||
"typescript": "latest",
|
||||
"webpack": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
5612
extensions/metrics-cluster-feature/package-lock.json
generated
5612
extensions/metrics-cluster-feature/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -19,10 +19,10 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"semver": "^7.3.2",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.44.2"
|
||||
"jest": "latest",
|
||||
"ts-loader": "latest",
|
||||
"typescript": "latest",
|
||||
"webpack": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,13 +17,13 @@ const {
|
||||
},
|
||||
} = Renderer;
|
||||
|
||||
interface Props {
|
||||
export interface MetricsSettingsProps {
|
||||
cluster: Common.Catalog.KubernetesCluster;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class MetricsSettings extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
export class MetricsSettings extends React.Component<MetricsSettingsProps> {
|
||||
constructor(props: MetricsSettingsProps) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
5931
extensions/node-menu/package-lock.json
generated
5931
extensions/node-menu/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,9 +18,9 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.46.0"
|
||||
"jest": "latest",
|
||||
"ts-loader": "latest",
|
||||
"typescript": "latest",
|
||||
"webpack": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
5931
extensions/pod-menu/package-lock.json
generated
5931
extensions/pod-menu/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,9 +18,9 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^4.46.0"
|
||||
"jest": "latest",
|
||||
"ts-loader": "latest",
|
||||
"typescript": "latest",
|
||||
"webpack": "latest"
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,9 +60,7 @@ describe("preferences page tests", () => {
|
||||
// Skipping, but will turn it on again in the follow up PR
|
||||
it.skip("ensures helm repos", async () => {
|
||||
await window.click("[data-testid=kubernetes-tab]");
|
||||
await window.waitForSelector("[data-testid=repository-name]", {
|
||||
timeout: 140_000,
|
||||
});
|
||||
await window.waitForSelector("[data-testid=repository-name]");
|
||||
await window.click("#HelmRepoSelect");
|
||||
await window.waitForSelector("div.Select__option");
|
||||
}, 10*60*1000);
|
||||
|
||||
@ -233,16 +233,16 @@ const commonPageTests: CommonPageTest[] = [{
|
||||
},
|
||||
},
|
||||
{
|
||||
drawerId: "apps",
|
||||
drawerId: "helm",
|
||||
pages: [
|
||||
{
|
||||
name: "Charts",
|
||||
href: "/apps/charts",
|
||||
href: "/helm/charts",
|
||||
expectedSelector: "div.HelmCharts input",
|
||||
},
|
||||
{
|
||||
name: "Releases",
|
||||
href: "/apps/releases",
|
||||
href: "/helm/releases",
|
||||
expectedSelector: "h5.title",
|
||||
expectedText: "Releases",
|
||||
},
|
||||
@ -320,7 +320,7 @@ utils.describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => {
|
||||
await frame.waitForSelector(`.Menu >> text="Add to Hotbar"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Settings"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Disconnect"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Delete"`);
|
||||
await frame.waitForSelector(`.Menu >> text="Remove"`);
|
||||
});
|
||||
|
||||
it("should navigate around common cluster pages", async () => {
|
||||
|
||||
88
package.json
88
package.json
@ -3,7 +3,7 @@
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"version": "5.3.0",
|
||||
"version": "5.4.0",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
@ -16,9 +16,9 @@
|
||||
"dev-build": "concurrently yarn:compile:*",
|
||||
"debug-build": "concurrently yarn:compile:main yarn:compile:extension-types",
|
||||
"dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"",
|
||||
"dev:main": "yarn run compile:main --watch",
|
||||
"dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts",
|
||||
"dev:extension-types": "yarn run compile:extension-types --watch",
|
||||
"dev:main": "yarn run compile:main --watch --progress",
|
||||
"dev:renderer": "yarn run compile:renderer --watch --progress",
|
||||
"dev:extension-types": "yarn run compile:extension-types --watch --progress",
|
||||
"compile": "env NODE_ENV=production concurrently yarn:compile:*",
|
||||
"compile:main": "yarn run webpack --config webpack.main.ts",
|
||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||
@ -34,6 +34,7 @@
|
||||
"download-bins": "concurrently yarn:download:*",
|
||||
"download:kubectl": "yarn run ts-node build/download_kubectl.ts",
|
||||
"download:helm": "yarn run ts-node build/download_helm.ts",
|
||||
"download:k8s-proxy": "yarn run ts-node build/download_k8s_proxy.ts",
|
||||
"build:tray-icons": "yarn run ts-node build/build_tray_icon.ts",
|
||||
"build:theme-vars": "yarn run ts-node build/build_theme_vars.ts",
|
||||
"lint": "PROD=true yarn run eslint --ext js,ts,tsx --max-warnings=0 .",
|
||||
@ -47,8 +48,9 @@
|
||||
"postversion": "git push --set-upstream ${GIT_REMOTE:-origin} release/v$npm_package_version"
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.21.2",
|
||||
"bundledHelmVersion": "3.6.3",
|
||||
"k8sProxyVersion": "0.1.2",
|
||||
"bundledKubectlVersion": "1.23.3",
|
||||
"bundledHelmVersion": "3.7.2",
|
||||
"sentryDsn": ""
|
||||
},
|
||||
"engines": {
|
||||
@ -62,7 +64,7 @@
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||
"\\.(svg|png|jpg|eot|woff2?|ttf)$": "<rootDir>/__mocks__/assetMock.ts"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/dist",
|
||||
@ -129,6 +131,10 @@
|
||||
"from": "binaries/client/linux/${arch}/kubectl",
|
||||
"to": "./${arch}/kubectl"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/linux/${arch}/lens-k8s-proxy",
|
||||
"to": "./${arch}/lens-k8s-proxy"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/${arch}/helm3/helm3",
|
||||
"to": "./helm3/helm3"
|
||||
@ -150,6 +156,10 @@
|
||||
"from": "binaries/client/darwin/${arch}/kubectl",
|
||||
"to": "./${arch}/kubectl"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/darwin/${arch}/lens-k8s-proxy",
|
||||
"to": "./${arch}/lens-k8s-proxy"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/${arch}/helm3/helm3",
|
||||
"to": "./helm3/helm3"
|
||||
@ -169,6 +179,14 @@
|
||||
"from": "binaries/client/windows/ia32/kubectl.exe",
|
||||
"to": "./ia32/kubectl.exe"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/windows/x64/lens-k8s-proxy",
|
||||
"to": "./x64/lens-k8s-proxy.exe"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/windows/ia32/lens-k8s-proxy",
|
||||
"to": "./ia32/lens-k8s-proxy.exe"
|
||||
},
|
||||
{
|
||||
"from": "binaries/client/x64/helm3/helm3.exe",
|
||||
"to": "./helm3/helm3.exe"
|
||||
@ -194,8 +212,8 @@
|
||||
"@hapi/call": "^8.0.1",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.16.1",
|
||||
"@ogre-tools/injectable": "3.2.0",
|
||||
"@ogre-tools/injectable-react": "3.2.0",
|
||||
"@ogre-tools/injectable": "5.0.1",
|
||||
"@ogre-tools/injectable-react": "5.0.1",
|
||||
"@sentry/electron": "^2.5.4",
|
||||
"@sentry/integrations": "^6.15.0",
|
||||
"@types/circular-dependency-plugin": "5.0.4",
|
||||
@ -268,7 +286,7 @@
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@sentry/types": "^6.14.1",
|
||||
"@testing-library/jest-dom": "^5.16.1",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
@ -291,12 +309,11 @@
|
||||
"@types/lodash": "^4.14.177",
|
||||
"@types/marked": "^4.0.1",
|
||||
"@types/md5-file": "^4.0.2",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mini-css-extract-plugin": "^2.4.0",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "14.17.33",
|
||||
"@types/node-fetch": "^2.5.12",
|
||||
"@types/npm": "^2.0.32",
|
||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||
"@types/proper-lockfile": "^4.1.2",
|
||||
"@types/randomcolor": "^0.5.6",
|
||||
"@types/react": "^17.0.34",
|
||||
@ -304,7 +321,7 @@
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-router-dom": "^5.3.2",
|
||||
"@types/react-select": "3.1.2",
|
||||
"@types/react-table": "^7.7.8",
|
||||
"@types/react-table": "^7.7.9",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/readable-stream": "^2.3.12",
|
||||
@ -319,36 +336,35 @@
|
||||
"@types/triple-beam": "^1.3.2",
|
||||
"@types/url-parse": "^1.4.5",
|
||||
"@types/uuid": "^8.3.3",
|
||||
"@types/webpack": "^4.41.32",
|
||||
"@types/webpack-dev-server": "^3.11.6",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/webpack-dev-server": "^4.7.2",
|
||||
"@types/webpack-env": "^1.16.3",
|
||||
"@types/webpack-node-externals": "^1.7.1",
|
||||
"@types/webpack-node-externals": "^2.5.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.10.1",
|
||||
"@typescript-eslint/parser": "^5.10.1",
|
||||
"ansi_up": "^5.1.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"color": "^3.2.1",
|
||||
"concurrently": "^5.3.0",
|
||||
"css-loader": "^5.2.7",
|
||||
"concurrently": "^7.0.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"deepdash": "^5.3.9",
|
||||
"dompurify": "^2.3.4",
|
||||
"electron": "^14.2.4",
|
||||
"electron-builder": "^22.14.5",
|
||||
"electron-notarize": "^0.3.0",
|
||||
"esbuild": "^0.13.15",
|
||||
"esbuild-loader": "^2.16.0",
|
||||
"esbuild-loader": "^2.18.0",
|
||||
"eslint": "^8.7.0",
|
||||
"eslint-plugin-header": "^3.1.1",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
"flex.box": "^3.4.4",
|
||||
"fork-ts-checker-webpack-plugin": "^5.2.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.5.0",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"include-media": "^1.4.9",
|
||||
"jest": "26.6.3",
|
||||
@ -356,43 +372,41 @@
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-mock-extended": "^1.0.18",
|
||||
"make-plural": "^6.2.2",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"mini-css-extract-plugin": "^2.5.2",
|
||||
"node-gyp": "7.1.2",
|
||||
"node-loader": "^1.0.3",
|
||||
"node-loader": "^2.0.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"playwright": "^1.17.1",
|
||||
"postcss": "^8.4.5",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"postcss-loader": "^6.2.1",
|
||||
"randomcolor": "^0.6.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-refresh": "^0.9.0",
|
||||
"react-refresh": "^0.11.0",
|
||||
"react-refresh-typescript": "^2.0.3",
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-select": "3.2.0",
|
||||
"react-select-event": "^5.1.0",
|
||||
"react-table": "^7.7.0",
|
||||
"react-window": "^1.8.6",
|
||||
"sass": "^1.45.1",
|
||||
"sass-loader": "^10.2.0",
|
||||
"sass-loader": "^12.4.0",
|
||||
"sharp": "^0.29.3",
|
||||
"style-loader": "^2.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"tailwindcss": "^3.0.7",
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-loader": "^9.2.6",
|
||||
"ts-node": "^10.4.0",
|
||||
"type-fest": "^1.0.2",
|
||||
"typed-emitter": "^1.4.0",
|
||||
"typedoc": "0.22.10",
|
||||
"typedoc-plugin-markdown": "^3.11.3",
|
||||
"typedoc-plugin-markdown": "^3.11.12",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.5.2",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-dev-server": "^3.11.3",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"webpack": "^5.69.0",
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4",
|
||||
"webpack-node-externals": "^3.0.0",
|
||||
"xterm": "^4.15.0",
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type {
|
||||
DependencyInjectionContainer,
|
||||
DiContainer,
|
||||
} from "@ogre-tools/injectable";
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ jest.mock("electron", () => ({
|
||||
}));
|
||||
|
||||
describe("cluster-store", () => {
|
||||
let mainDi: DependencyInjectionContainer;
|
||||
let mainDi: DiContainer;
|
||||
let clusterStore: ClusterStore;
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
|
||||
|
||||
@ -43,30 +43,30 @@ users:
|
||||
command: foo
|
||||
`;
|
||||
|
||||
interface kubeconfig {
|
||||
apiVersion: string,
|
||||
interface Kubeconfig {
|
||||
apiVersion: string;
|
||||
clusters: [{
|
||||
name: string,
|
||||
name: string;
|
||||
cluster: {
|
||||
server: string
|
||||
}
|
||||
}],
|
||||
server: string;
|
||||
};
|
||||
}];
|
||||
contexts: [{
|
||||
context: {
|
||||
cluster: string,
|
||||
user: string,
|
||||
},
|
||||
name: string
|
||||
}],
|
||||
cluster: string;
|
||||
user: string;
|
||||
};
|
||||
name: string;
|
||||
}];
|
||||
users: [{
|
||||
name: string
|
||||
}],
|
||||
kind: string,
|
||||
"current-context": string,
|
||||
preferences: {}
|
||||
name: string;
|
||||
}];
|
||||
kind: string;
|
||||
"current-context": string;
|
||||
preferences: {};
|
||||
}
|
||||
|
||||
let mockKubeConfig: kubeconfig;
|
||||
let mockKubeConfig: Kubeconfig;
|
||||
|
||||
describe("kube helpers", () => {
|
||||
describe("validateKubeconfig", () => {
|
||||
|
||||
@ -28,7 +28,7 @@ import electron from "electron";
|
||||
import { stdout, stderr } from "process";
|
||||
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
|
||||
import userStoreInjectable from "../user-store/user-store.injectable";
|
||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import type { ClusterStoreModel } from "../cluster-store/cluster-store";
|
||||
import { defaultTheme } from "../vars";
|
||||
@ -37,7 +37,7 @@ console = new Console(stdout, stderr);
|
||||
|
||||
describe("user store tests", () => {
|
||||
let userStore: UserStore;
|
||||
let mainDi: DependencyInjectionContainer;
|
||||
let mainDi: DiContainer;
|
||||
|
||||
beforeEach(async () => {
|
||||
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appEventBus } from "./event-bus";
|
||||
|
||||
const appEventBusInjectable = getInjectable({
|
||||
id: "app-event-bus",
|
||||
instantiate: () => appEventBus,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default appEventBusInjectable;
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
|
||||
import { EventEmitter } from "../event-emitter";
|
||||
|
||||
export type AppEvent = {
|
||||
export interface AppEvent {
|
||||
name: string;
|
||||
action: string;
|
||||
params?: object;
|
||||
};
|
||||
}
|
||||
|
||||
export const appEventBus = new EventEmitter<[AppEvent]>();
|
||||
|
||||
@ -8,7 +8,7 @@ import { createChannel } from "../ipc-channel/create-channel/create-channel";
|
||||
|
||||
export type AppPaths = Record<PathName, string>;
|
||||
|
||||
export const appPathsInjectionToken = getInjectionToken<AppPaths>();
|
||||
export const appPathsInjectionToken = getInjectionToken<AppPaths>({ id: "app-paths-token" });
|
||||
|
||||
export const appPathsIpcChannel = createChannel<AppPaths>("app-paths");
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import { AppPaths, appPathsInjectionToken } from "./app-path-injection-token";
|
||||
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
|
||||
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
|
||||
@ -13,8 +13,8 @@ import directoryForIntegrationTestingInjectable from "../../main/app-paths/direc
|
||||
import path from "path";
|
||||
|
||||
describe("app-paths", () => {
|
||||
let mainDi: DependencyInjectionContainer;
|
||||
let rendererDi: DependencyInjectionContainer;
|
||||
let mainDi: DiContainer;
|
||||
let rendererDi: DiContainer;
|
||||
let runSetups: () => Promise<void[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@ -2,15 +2,15 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
const directoryForBinariesInjectable = getInjectable({
|
||||
id: "directory-for-binaries",
|
||||
|
||||
instantiate: (di) =>
|
||||
path.join(di.inject(directoryForUserDataInjectable), "binaries"),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForBinariesInjectable;
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import isDevelopmentInjectable from "../../vars/is-development.injectable";
|
||||
import contextDirInjectable from "../../vars/context-dir.injectable";
|
||||
|
||||
const directoryForBundledBinariesInjectable = getInjectable({
|
||||
id: "directory-for-bundled-binaries",
|
||||
instantiate: (di) => {
|
||||
if (di.inject(isDevelopmentInjectable)) {
|
||||
return path.join(di.inject(contextDirInjectable), "binaries");
|
||||
}
|
||||
|
||||
return process.resourcesPath;
|
||||
},
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForBundledBinariesInjectable;
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
|
||||
const directoryForDownloadsInjectable = getInjectable({
|
||||
id: "directory-for-downloads",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).downloads,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForDownloadsInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
|
||||
const directoryForExesInjectable = getInjectable({
|
||||
id: "directory-for-exes",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).exe,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForExesInjectable;
|
||||
|
||||
@ -2,15 +2,15 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
|
||||
import path from "path";
|
||||
|
||||
const directoryForKubeConfigsInjectable = getInjectable({
|
||||
id: "directory-for-kube-configs",
|
||||
|
||||
instantiate: (di) =>
|
||||
path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForKubeConfigsInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
|
||||
const directoryForTempInjectable = getInjectable({
|
||||
id: "directory-for-temp",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).temp,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForTempInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { appPathsInjectionToken } from "../app-path-injection-token";
|
||||
|
||||
const directoryForUserDataInjectable = getInjectable({
|
||||
id: "directory-for-user-data",
|
||||
instantiate: (di) => di.inject(appPathsInjectionToken).userData,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForUserDataInjectable;
|
||||
|
||||
@ -2,21 +2,21 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||
|
||||
const getCustomKubeConfigDirectoryInjectable = getInjectable({
|
||||
id: "get-custom-kube-config-directory",
|
||||
|
||||
instantiate: (di) => (directoryName: string) => {
|
||||
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
|
||||
|
||||
|
||||
return path.resolve(
|
||||
directoryForKubeConfigs,
|
||||
directoryName,
|
||||
);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default getCustomKubeConfigDirectoryInjectable;
|
||||
|
||||
@ -11,6 +11,7 @@ import { app } from "electron";
|
||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
|
||||
import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
|
||||
|
||||
export interface KubernetesClusterPrometheusMetrics {
|
||||
address?: {
|
||||
@ -58,7 +59,11 @@ export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconn
|
||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
}
|
||||
|
||||
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||
export class KubernetesCluster<
|
||||
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
|
||||
Status extends KubernetesClusterStatus = KubernetesClusterStatus,
|
||||
Spec extends KubernetesClusterSpec = KubernetesClusterSpec,
|
||||
> extends CatalogEntity<Metadata, Status, Spec> {
|
||||
public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1";
|
||||
public static readonly kind: string = "KubernetesCluster";
|
||||
|
||||
@ -134,7 +139,7 @@ class KubernetesClusterCategory extends CatalogCategory {
|
||||
public readonly kind = "CatalogCategory";
|
||||
public metadata = {
|
||||
name: "Clusters",
|
||||
icon: require(`!!raw-loader!./icons/kubernetes.svg`).default, // eslint-disable-line
|
||||
icon: KubeClusterCategoryIcon,
|
||||
};
|
||||
public spec: CatalogCategorySpec = {
|
||||
group: "entity.k8slens.dev",
|
||||
|
||||
@ -14,9 +14,9 @@ export interface WebLinkStatus extends CatalogEntityStatus {
|
||||
phase: WebLinkStatusPhase;
|
||||
}
|
||||
|
||||
export type WebLinkSpec = {
|
||||
export interface WebLinkSpec {
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus, WebLinkSpec> {
|
||||
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
|
||||
@ -4,15 +4,16 @@
|
||||
*/
|
||||
|
||||
import { action, computed, observable, makeObservable } from "mobx";
|
||||
import { Disposer, ExtendedMap, iter } from "../utils";
|
||||
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
import { once } from "lodash";
|
||||
import { iter, getOrInsertMap, strictSet } from "../utils";
|
||||
import type { Disposer } from "../utils";
|
||||
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "./catalog-entity";
|
||||
|
||||
export type CategoryFilter = (category: CatalogCategory) => any;
|
||||
|
||||
export class CatalogCategoryRegistry {
|
||||
protected categories = observable.set<CatalogCategory>();
|
||||
protected groupKinds = new ExtendedMap<string, ExtendedMap<string, CatalogCategory>>();
|
||||
protected groupKinds = new Map<string, Map<string, CatalogCategory>>();
|
||||
protected filters = observable.set<CategoryFilter>([], {
|
||||
deep: false,
|
||||
});
|
||||
@ -22,14 +23,14 @@ export class CatalogCategoryRegistry {
|
||||
}
|
||||
|
||||
@action add(category: CatalogCategory): Disposer {
|
||||
const byGroup = getOrInsertMap(this.groupKinds, category.spec.group);
|
||||
|
||||
this.categories.add(category);
|
||||
this.groupKinds
|
||||
.getOrInsert(category.spec.group, ExtendedMap.new)
|
||||
.strictSet(category.spec.names.kind, category);
|
||||
strictSet(byGroup, category.spec.names.kind, category);
|
||||
|
||||
return () => {
|
||||
this.categories.delete(category);
|
||||
this.groupKinds.get(category.spec.group).delete(category.spec.names.kind);
|
||||
byGroup.delete(category.spec.names.kind);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -160,7 +160,7 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
|
||||
* @param id The id of a category is parse
|
||||
* @returns The group and kind parts of the ID
|
||||
*/
|
||||
public static parseId(id: string): { group?: string, kind?: string } {
|
||||
public static parseId(id: string): { group?: string; kind?: string } {
|
||||
const [group, kind] = id.split("/") ?? [];
|
||||
|
||||
return { group, kind };
|
||||
@ -245,7 +245,7 @@ export interface CatalogEntityContextMenu {
|
||||
*/
|
||||
confirm?: {
|
||||
message: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface CatalogEntityAddMenu extends CatalogEntityContextMenu {
|
||||
@ -257,7 +257,7 @@ export interface CatalogEntitySettingsMenu {
|
||||
group?: string;
|
||||
title: string;
|
||||
components: {
|
||||
View: React.ComponentType<any>
|
||||
View: React.ComponentType<any>;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import { observable } from "mobx";
|
||||
|
||||
export interface ClusterFrameInfo {
|
||||
frameId: number;
|
||||
processId: number
|
||||
processId: number;
|
||||
}
|
||||
|
||||
export const clusterFrameMap = observable.map<string, ClusterFrameInfo>();
|
||||
|
||||
22
src/common/cluster-store/allowed-resources.injectable.ts
Normal file
22
src/common/cluster-store/allowed-resources.injectable.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { comparer, computed } from "mobx";
|
||||
import hostedClusterInjectable from "./hosted-cluster.injectable";
|
||||
|
||||
const allowedResourcesInjectable = getInjectable({
|
||||
id: "allowed-resources",
|
||||
|
||||
instantiate: (di) => {
|
||||
const cluster = di.inject(hostedClusterInjectable);
|
||||
|
||||
return computed(() => new Set(cluster.allowedResources), {
|
||||
// This needs to be here so that during refresh changes are only propogated when necessary
|
||||
equals: (cur, prev) => comparer.structural(cur, prev),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default allowedResourcesInjectable;
|
||||
@ -2,17 +2,17 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { ClusterStore } from "./cluster-store";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
|
||||
const clusterStoreInjectable = getInjectable({
|
||||
id: "cluster-store",
|
||||
|
||||
instantiate: (di) =>
|
||||
ClusterStore.createInstance({
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
}),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default clusterStoreInjectable;
|
||||
|
||||
@ -22,7 +22,7 @@ export interface ClusterStoreModel {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
createCluster: (model: ClusterModel) => Cluster
|
||||
createCluster: (model: ClusterModel) => Cluster;
|
||||
}
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
@ -2,18 +2,18 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getHostedClusterId } from "../../utils";
|
||||
import clusterStoreInjectable from "../cluster-store.injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { getHostedClusterId } from "../utils";
|
||||
import clusterStoreInjectable from "./cluster-store.injectable";
|
||||
|
||||
const hostedClusterInjectable = getInjectable({
|
||||
id: "hosted-cluster",
|
||||
|
||||
instantiate: (di) => {
|
||||
const hostedClusterId = getHostedClusterId();
|
||||
|
||||
return di.inject(clusterStoreInjectable).getById(hostedClusterId);
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default hostedClusterInjectable;
|
||||
@ -157,7 +157,7 @@ export const initialNodeShellImage = "docker.io/alpine:3.13";
|
||||
* The arguments for requesting to refresh a cluster's metadata
|
||||
*/
|
||||
export interface ClusterRefreshOptions {
|
||||
refreshMetadata?: boolean
|
||||
refreshMetadata?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -170,7 +170,7 @@ export interface ClusterState {
|
||||
accessible: boolean;
|
||||
ready: boolean;
|
||||
isAdmin: boolean;
|
||||
allowedNamespaces: string[]
|
||||
allowedResources: string[]
|
||||
allowedNamespaces: string[];
|
||||
allowedResources: string[];
|
||||
isGlobalWatchEnabled: boolean;
|
||||
}
|
||||
|
||||
45
src/common/cluster/authorization-review.injectable.ts
Normal file
45
src/common/cluster/authorization-review.injectable.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { AuthorizationV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import logger from "../logger";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
|
||||
|
||||
/**
|
||||
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
|
||||
*/
|
||||
export function authorizationReview(proxyConfig: KubeConfig): CanI {
|
||||
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
|
||||
|
||||
/**
|
||||
* Requests the permissions for actions on the kube cluster
|
||||
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
|
||||
* @returns `true` if the actions described are allowed
|
||||
*/
|
||||
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
|
||||
try {
|
||||
const { body } = await api.createSelfSubjectAccessReview({
|
||||
apiVersion: "authorization.k8s.io/v1",
|
||||
kind: "SelfSubjectAccessReview",
|
||||
spec: { resourceAttributes },
|
||||
});
|
||||
|
||||
return body.status?.allowed ?? false;
|
||||
} catch (error) {
|
||||
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const authorizationReviewInjectable = getInjectable({
|
||||
id: "authorization-review",
|
||||
instantiate: () => authorizationReview,
|
||||
});
|
||||
|
||||
export default authorizationReviewInjectable;
|
||||
@ -7,7 +7,7 @@ import { ipcMain } from "electron";
|
||||
import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { broadcastMessage } from "../ipc";
|
||||
import type { ContextHandler } from "../../main/context-handler/context-handler";
|
||||
import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { HttpError, KubeConfig } from "@kubernetes/client-node";
|
||||
import type { Kubectl } from "../../main/kubectl/kubectl";
|
||||
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
|
||||
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
|
||||
@ -21,12 +21,16 @@ import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../clu
|
||||
import { disposer, toJS } from "../utils";
|
||||
import type { Response } from "request";
|
||||
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
|
||||
import type { CanI } from "./authorization-review.injectable";
|
||||
import type { ListNamespaces } from "./list-namespaces.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
directoryForKubeConfigs: string,
|
||||
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager,
|
||||
createContextHandler: (cluster: Cluster) => ContextHandler,
|
||||
createKubectl: (clusterVersion: string) => Kubectl
|
||||
export interface ClusterDependencies {
|
||||
readonly directoryForKubeConfigs: string;
|
||||
createKubeconfigManager: (cluster: Cluster) => KubeconfigManager;
|
||||
createContextHandler: (cluster: Cluster) => ContextHandler;
|
||||
createKubectl: (clusterVersion: string) => Kubectl;
|
||||
createAuthorizationReview: (config: KubeConfig) => CanI;
|
||||
createListNamespaces: (config: KubeConfig) => ListNamespaces;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -213,7 +217,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return this.preferences.defaultNamespace;
|
||||
}
|
||||
|
||||
constructor(private dependencies: Dependencies, model: ClusterModel) {
|
||||
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel) {
|
||||
makeObservable(this);
|
||||
this.id = model.id;
|
||||
this.updateModel(model);
|
||||
@ -375,7 +379,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@action disconnect() {
|
||||
@action disconnect(): void {
|
||||
if (this.disconnected) {
|
||||
return void logger.debug("[CLUSTER]: already disconnected", { id: this.id });
|
||||
}
|
||||
|
||||
logger.info(`[CLUSTER]: disconnecting`, { id: this.id });
|
||||
this.eventsDisposer();
|
||||
this.contextHandler?.stopServer();
|
||||
@ -425,10 +433,20 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
* @internal
|
||||
*/
|
||||
private async refreshAccessibility(): Promise<void> {
|
||||
this.isAdmin = await this.isClusterAdmin();
|
||||
this.isGlobalWatchEnabled = await this.canUseWatchApi({ resource: "*" });
|
||||
this.allowedNamespaces = await this.getAllowedNamespaces();
|
||||
this.allowedResources = await this.getAllowedResources();
|
||||
const proxyConfig = await this.getProxyKubeconfig();
|
||||
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
|
||||
|
||||
this.isAdmin = await canI({
|
||||
namespace: "kube-system",
|
||||
resource: "*",
|
||||
verb: "create",
|
||||
});
|
||||
this.isGlobalWatchEnabled = await canI({
|
||||
verb: "watch",
|
||||
resource: "*",
|
||||
});
|
||||
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
|
||||
this.allowedResources = await this.getAllowedResources(canI);
|
||||
this.ready = true;
|
||||
}
|
||||
|
||||
@ -507,50 +525,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @param resourceAttributes resource attributes
|
||||
*/
|
||||
async canI(resourceAttributes: V1ResourceAttributes): Promise<boolean> {
|
||||
const authApi = (await this.getProxyKubeconfig()).makeApiClient(AuthorizationV1Api);
|
||||
|
||||
try {
|
||||
const accessReview = await authApi.createSelfSubjectAccessReview({
|
||||
apiVersion: "authorization.k8s.io/v1",
|
||||
kind: "SelfSubjectAccessReview",
|
||||
spec: { resourceAttributes },
|
||||
});
|
||||
|
||||
return accessReview.body.status.allowed;
|
||||
} catch (error) {
|
||||
logger.error(`failed to request selfSubjectAccessReview: ${error}`);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async isClusterAdmin(): Promise<boolean> {
|
||||
return this.canI({
|
||||
namespace: "kube-system",
|
||||
resource: "*",
|
||||
verb: "create",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
async canUseWatchApi(customizeResource: V1ResourceAttributes = {}): Promise<boolean> {
|
||||
return this.canI({
|
||||
verb: "watch",
|
||||
resource: "*",
|
||||
...customizeResource,
|
||||
});
|
||||
}
|
||||
|
||||
toJSON(): ClusterModel {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
@ -622,20 +596,17 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
broadcastMessage(`cluster:${this.id}:connection-update`, update);
|
||||
}
|
||||
|
||||
protected async getAllowedNamespaces() {
|
||||
protected async getAllowedNamespaces(proxyConfig: KubeConfig) {
|
||||
if (this.accessibleNamespaces.length) {
|
||||
return this.accessibleNamespaces;
|
||||
}
|
||||
|
||||
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
|
||||
|
||||
try {
|
||||
const { body: { items }} = await api.listNamespace();
|
||||
const namespaces = items.map(ns => ns.metadata.name);
|
||||
const listNamespaces = this.dependencies.createListNamespaces(proxyConfig);
|
||||
|
||||
return namespaces;
|
||||
return await listNamespaces();
|
||||
} catch (error) {
|
||||
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
|
||||
const ctx = proxyConfig.getContextObject(this.contextName);
|
||||
const namespaceList = [ctx.namespace].filter(Boolean);
|
||||
|
||||
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
|
||||
@ -649,7 +620,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
}
|
||||
}
|
||||
|
||||
protected async getAllowedResources() {
|
||||
protected async getAllowedResources(canI: CanI) {
|
||||
try {
|
||||
if (!this.allowedNamespaces.length) {
|
||||
return [];
|
||||
@ -662,7 +633,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
requests.push(apiLimit(async () => {
|
||||
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
|
||||
if (!this.resourceAccessStatuses.get(apiResource)) {
|
||||
const result = await this.canI({
|
||||
const result = await canI({
|
||||
resource: apiResource.apiName,
|
||||
group: apiResource.group,
|
||||
verb: "list",
|
||||
|
||||
@ -7,4 +7,4 @@ import type { ClusterModel } from "../cluster-types";
|
||||
import type { Cluster } from "./cluster";
|
||||
|
||||
export const createClusterInjectionToken =
|
||||
getInjectionToken<(model: ClusterModel) => Cluster>();
|
||||
getInjectionToken<(model: ClusterModel) => Cluster>({ id: "create-cluster-token" });
|
||||
|
||||
25
src/common/cluster/list-namespaces.injectable.ts
Normal file
25
src/common/cluster/list-namespaces.injectable.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { CoreV1Api, KubeConfig } from "@kubernetes/client-node";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
|
||||
export type ListNamespaces = () => Promise<string[]>;
|
||||
|
||||
export function listNamespaces(config: KubeConfig): ListNamespaces {
|
||||
const coreApi = config.makeApiClient(CoreV1Api);
|
||||
|
||||
return async () => {
|
||||
const { body: { items }} = await coreApi.listNamespace();
|
||||
|
||||
return items.map(ns => ns.metadata.name);
|
||||
};
|
||||
}
|
||||
|
||||
const listNamespacesInjectable = getInjectable({
|
||||
id: "list-namespaces",
|
||||
instantiate: () => listNamespaces,
|
||||
});
|
||||
|
||||
export default listNamespacesInjectable;
|
||||
@ -2,18 +2,18 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import path from "path";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
|
||||
const directoryForLensLocalStorageInjectable = getInjectable({
|
||||
id: "directory-for-lens-local-storage",
|
||||
|
||||
instantiate: (di) =>
|
||||
path.resolve(
|
||||
di.inject(directoryForUserDataInjectable),
|
||||
"lens-local-storage",
|
||||
),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default directoryForLensLocalStorageInjectable;
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fse from "fs-extra";
|
||||
|
||||
const fsInjectable = getInjectable({
|
||||
id: "fs",
|
||||
instantiate: () => fse,
|
||||
causesSideEffects: true,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default fsInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
const readDirInjectable = getInjectable({
|
||||
id: "read-dir",
|
||||
instantiate: (di) => di.inject(fsInjectable).readdir,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default readDirInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
const readFileInjectable = getInjectable({
|
||||
id: "read-file",
|
||||
instantiate: (di) => di.inject(fsInjectable).readFile,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default readFileInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import fsInjectable from "./fs.injectable";
|
||||
|
||||
const readJsonFileInjectable = getInjectable({
|
||||
id: "read-json-file",
|
||||
instantiate: (di) => di.inject(fsInjectable).readJson,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default readJsonFileInjectable;
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import type { EnsureOptions, WriteOptions } from "fs-extra";
|
||||
import path from "path";
|
||||
import type { JsonValue } from "type-fest";
|
||||
@ -23,6 +23,8 @@ const writeJsonFile = ({ writeJson, ensureDir }: Dependencies) => async (filePat
|
||||
};
|
||||
|
||||
const writeJsonFileInjectable = getInjectable({
|
||||
id: "write-json-file",
|
||||
|
||||
instantiate: (di) => {
|
||||
const { writeJson, ensureDir } = di.inject(fsInjectable);
|
||||
|
||||
@ -31,8 +33,6 @@ const writeJsonFileInjectable = getInjectable({
|
||||
ensureDir,
|
||||
});
|
||||
},
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default writeJsonFileInjectable;
|
||||
|
||||
@ -4,18 +4,17 @@
|
||||
*/
|
||||
|
||||
import esbuild from "esbuild";
|
||||
import type { Options as TSLoaderOptions } from "ts-loader";
|
||||
|
||||
/**
|
||||
* A function returning webpack ts/tsx loader
|
||||
*
|
||||
* depends on env LENS_DEV_USE_ESBUILD_LOADER to use esbuild-loader (faster) or good-old ts-loader
|
||||
*
|
||||
* @param testRegExp - the regex for webpack to conditional find the files
|
||||
* @returns ts/tsx webpack loader configuration object
|
||||
*/
|
||||
const getTSLoader = (
|
||||
testRegExp: RegExp, transpileOnly = true,
|
||||
) => {
|
||||
const getTSLoader = (options: Partial<TSLoaderOptions> = {}, testRegExp?: RegExp) => {
|
||||
testRegExp ??= /\.tsx?$/; // by default covers react/jsx-stuff
|
||||
options.transpileOnly ??= true;
|
||||
|
||||
if (process.env.LENS_DEV_USE_ESBUILD_LOADER === "true") {
|
||||
console.info(`\n🚀 using esbuild-loader for ts(x)`);
|
||||
|
||||
@ -24,7 +23,7 @@ const getTSLoader = (
|
||||
loader: "esbuild-loader",
|
||||
options: {
|
||||
loader: "tsx",
|
||||
target: "es2015",
|
||||
target: "ES2019", // supported by >= electron@14
|
||||
implementation: esbuild,
|
||||
},
|
||||
};
|
||||
@ -35,9 +34,7 @@ const getTSLoader = (
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "ts-loader",
|
||||
options: {
|
||||
transpileOnly,
|
||||
},
|
||||
options,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { HotbarStore } from "./hotbar-store";
|
||||
|
||||
const hotbarManagerInjectable = getInjectable({
|
||||
id: "hotbar-manager",
|
||||
instantiate: () => HotbarStore.getInstance(),
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default hotbarManagerInjectable;
|
||||
|
||||
@ -14,7 +14,7 @@ export interface HotbarItem {
|
||||
};
|
||||
params?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type Hotbar = Required<CreateHotbarData>;
|
||||
|
||||
@ -29,10 +29,10 @@ export function onceCorrect<
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: IPC,
|
||||
channel: string,
|
||||
listener: Listener,
|
||||
verifier: ListVerifier<Rest<Parameters<Listener>>>,
|
||||
source: IPC;
|
||||
channel: string;
|
||||
listener: Listener;
|
||||
verifier: ListVerifier<Rest<Parameters<Listener>>>;
|
||||
}): void {
|
||||
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]): void {
|
||||
if (verifier(args)) {
|
||||
@ -63,10 +63,10 @@ export function onCorrect<
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: IPC,
|
||||
channel: string,
|
||||
listener: Listener,
|
||||
verifier: ListVerifier<Rest<Parameters<Listener>>>,
|
||||
source: IPC;
|
||||
channel: string;
|
||||
listener: Listener;
|
||||
verifier: ListVerifier<Rest<Parameters<Listener>>>;
|
||||
}): Disposer {
|
||||
function wrappedListener(event: ListenerEvent<IPC>, ...args: unknown[]) {
|
||||
if (verifier(args)) {
|
||||
@ -89,9 +89,9 @@ export function handleCorrect<
|
||||
handler,
|
||||
verifier,
|
||||
}: {
|
||||
channel: string,
|
||||
handler: Handler,
|
||||
verifier: ListVerifier<Rest<Parameters<Handler>>>,
|
||||
channel: string;
|
||||
handler: Handler;
|
||||
verifier: ListVerifier<Rest<Parameters<Handler>>>;
|
||||
}): Disposer {
|
||||
function wrappedHandler(event: Electron.IpcMainInvokeEvent, ...args: unknown[]): ReturnType<Handler> {
|
||||
if (verifier(args)) {
|
||||
|
||||
@ -21,7 +21,7 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
@observable isLoading = false;
|
||||
@observable isLoaded = false;
|
||||
@observable items = observable.array<Item>([], { deep: false });
|
||||
@observable selectedItemsIds = observable.map<string, boolean>();
|
||||
@observable selectedItemsIds = observable.set<string>();
|
||||
|
||||
constructor() {
|
||||
makeObservable(this);
|
||||
@ -29,7 +29,11 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
}
|
||||
|
||||
@computed get selectedItems(): Item[] {
|
||||
return this.items.filter(item => this.selectedItemsIds.get(item.getId()));
|
||||
return this.pickOnlySelected(this.items);
|
||||
}
|
||||
|
||||
public pickOnlySelected(items: Item[]): Item[] {
|
||||
return items.filter(item => this.selectedItemsIds.has(item.getId()));
|
||||
}
|
||||
|
||||
public getItems(): Item[] {
|
||||
@ -152,12 +156,12 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
}
|
||||
|
||||
isSelected(item: Item) {
|
||||
return !!this.selectedItemsIds.get(item.getId());
|
||||
return this.selectedItemsIds.has(item.getId());
|
||||
}
|
||||
|
||||
@action
|
||||
select(item: Item) {
|
||||
this.selectedItemsIds.set(item.getId(), true);
|
||||
this.selectedItemsIds.add(item.getId());
|
||||
}
|
||||
|
||||
@action
|
||||
@ -207,6 +211,8 @@ export abstract class ItemStore<Item extends ItemObject> {
|
||||
|
||||
async removeSelectedItems?(): Promise<any>;
|
||||
|
||||
async removeItems?(items: Item[]): Promise<void>;
|
||||
|
||||
* [Symbol.iterator]() {
|
||||
yield* this.items;
|
||||
}
|
||||
|
||||
131
src/common/k8s-api/__tests__/nodes.test.ts
Normal file
131
src/common/k8s-api/__tests__/nodes.test.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { Node } from "../endpoints";
|
||||
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
describe("Nodes tests", () => {
|
||||
describe("getRoleLabels()", () => {
|
||||
it("should return empty string if labels is not present", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("");
|
||||
});
|
||||
|
||||
it("should return empty string if labels is empty object", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("");
|
||||
});
|
||||
|
||||
it("should return rest of keys with substring node-role.kubernetes.io/", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io/foobar1": "bat",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("foobar, foobar1");
|
||||
});
|
||||
|
||||
it("should return rest of keys with substring node-role.kubernetes.io/ after last /", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node-role.kubernetes.io/foobar": "bat",
|
||||
"hellonode-role.kubernetes.io//////foobar1": "bat",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("foobar, foobar1");
|
||||
});
|
||||
|
||||
it("should return value of label kubernetes.io/role if present", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"kubernetes.io/role": "master",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("master");
|
||||
});
|
||||
|
||||
it("should return value of label node.kubernetes.io/role if present", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"node.kubernetes.io/role": "master",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("master");
|
||||
});
|
||||
|
||||
it("all sources should be joined together", () => {
|
||||
const node = new Node({
|
||||
apiVersion: "foo",
|
||||
kind: "Node",
|
||||
metadata: {
|
||||
name: "bar",
|
||||
resourceVersion: "1",
|
||||
uid: "bat",
|
||||
labels: {
|
||||
"aksjhdkjahsdnode-role.kubernetes.io/foobar": "bat",
|
||||
"kubernetes.io/role": "master",
|
||||
"node.kubernetes.io/role": "master-v2-max",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(node.getRoleLabels()).toBe("foobar, master, master-v2-max");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -9,12 +9,12 @@ import { crdResourcesURL } from "../../routes";
|
||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||
import type { KubeJsonApiData } from "../kube-json-api";
|
||||
|
||||
type AdditionalPrinterColumnsCommon = {
|
||||
interface AdditionalPrinterColumnsCommon {
|
||||
name: string;
|
||||
type: "integer" | "number" | "string" | "boolean" | "date";
|
||||
priority: number;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AdditionalPrinterColumnsV1 = AdditionalPrinterColumnsCommon & {
|
||||
jsonPath: string;
|
||||
|
||||
@ -90,7 +90,7 @@ export interface RawHelmChart {
|
||||
urls?: string[];
|
||||
maintainers?: HelmChartMaintainer[];
|
||||
dependencies?: RawHelmChartDependency[];
|
||||
annotations?: Record<string, string>,
|
||||
annotations?: Record<string, string>;
|
||||
}
|
||||
|
||||
const helmChartMaintainerValidator = Joi.object<HelmChartMaintainer>({
|
||||
|
||||
@ -7,7 +7,7 @@ import yaml from "js-yaml";
|
||||
import { formatDuration } from "../../utils";
|
||||
import capitalize from "lodash/capitalize";
|
||||
import { apiBase } from "../index";
|
||||
import { helmChartStore } from "../../../renderer/components/+apps-helm-charts/helm-chart.store";
|
||||
import { helmChartStore } from "../../../renderer/components/+helm-charts/helm-chart.store";
|
||||
import type { ItemObject } from "../../item.store";
|
||||
import { KubeObject } from "../kube-object";
|
||||
import type { JsonApiData } from "../json-api";
|
||||
@ -70,8 +70,8 @@ export interface IReleaseRevision {
|
||||
|
||||
type EndpointParams = {}
|
||||
| { namespace: string }
|
||||
| { namespace: string, name: string }
|
||||
| { namespace: string, name: string, route: string };
|
||||
| { namespace: string; name: string }
|
||||
| { namespace: string; name: string; route: string };
|
||||
|
||||
interface EndpointQuery {
|
||||
all?: boolean;
|
||||
@ -163,13 +163,13 @@ interface HelmReleaseDto {
|
||||
}
|
||||
|
||||
export interface HelmRelease extends HelmReleaseDto, ItemObject {
|
||||
getNs: () => string
|
||||
getChart: (withVersion?: boolean) => string
|
||||
getRevision: () => number
|
||||
getStatus: () => string
|
||||
getVersion: () => string
|
||||
getUpdated: (humanize?: boolean, compact?: boolean) => string | number
|
||||
getRepo: () => Promise<string>
|
||||
getNs: () => string;
|
||||
getChart: (withVersion?: boolean) => string;
|
||||
getRevision: () => number;
|
||||
getStatus: () => string;
|
||||
getVersion: () => string;
|
||||
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
|
||||
getRepo: () => Promise<string>;
|
||||
}
|
||||
|
||||
const toHelmRelease = (release: HelmReleaseDto) : HelmRelease => ({
|
||||
|
||||
@ -58,7 +58,7 @@ export interface IIngressService {
|
||||
port: {
|
||||
name?: string;
|
||||
number?: number;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const getBackendServiceNamePort = (backend: IIngressBackend) => {
|
||||
@ -96,8 +96,8 @@ export interface Ingress {
|
||||
apiGroup: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
}
|
||||
}>
|
||||
};
|
||||
}>;
|
||||
};
|
||||
status: {
|
||||
loadBalancer: {
|
||||
|
||||
@ -33,7 +33,7 @@ export enum LimitPart {
|
||||
type LimitRangeParts = Partial<Record<LimitPart, Record<string, string>>>;
|
||||
|
||||
export interface LimitRangeItem extends LimitRangeParts {
|
||||
type: string
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface LimitRange {
|
||||
|
||||
@ -101,8 +101,8 @@ export interface Node {
|
||||
daemonEndpoints?: {
|
||||
kubeletEndpoint: {
|
||||
Port: number; //it must be uppercase for backwards compatibility
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
nodeInfo?: {
|
||||
machineID: string;
|
||||
systemUUID: string;
|
||||
@ -140,6 +140,12 @@ function* getTrueConditionTypes(conditions: IterableIterator<NodeCondition> | It
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This regex is used in the `getRoleLabels()` method bellow, but placed here
|
||||
* as factoring out regexes is best practice.
|
||||
*/
|
||||
const nodeRoleLabelKeyMatcher = /^.*node-role.kubernetes.io\/+(?<role>.+)$/;
|
||||
|
||||
export class Node extends KubeObject {
|
||||
static kind = "Node";
|
||||
static namespaced = false;
|
||||
@ -165,17 +171,29 @@ export class Node extends KubeObject {
|
||||
return this.spec.taints || [];
|
||||
}
|
||||
|
||||
getRoleLabels() {
|
||||
if (!this.metadata?.labels || typeof this.metadata.labels !== "object") {
|
||||
getRoleLabels(): string {
|
||||
const { labels } = this.metadata;
|
||||
|
||||
if (!labels || typeof labels !== "object") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const roleLabels = Object.keys(this.metadata.labels)
|
||||
.filter(key => key.includes("node-role.kubernetes.io"))
|
||||
.map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
||||
const roleLabels: string[] = [];
|
||||
|
||||
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
||||
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
||||
for (const labelKey of Object.keys(labels)) {
|
||||
const match = nodeRoleLabelKeyMatcher.exec(labelKey);
|
||||
|
||||
if (match) {
|
||||
roleLabels.push(match.groups.role);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof labels["kubernetes.io/role"] === "string") {
|
||||
roleLabels.push(labels["kubernetes.io/role"]);
|
||||
}
|
||||
|
||||
if (typeof labels["node.kubernetes.io/role"] === "string") {
|
||||
roleLabels.push(labels["node.kubernetes.io/role"]);
|
||||
}
|
||||
|
||||
return roleLabels.join(", ");
|
||||
|
||||
@ -16,10 +16,10 @@ export interface PodDisruptionBudget {
|
||||
selector: LabelSelector;
|
||||
};
|
||||
status: {
|
||||
currentHealthy: number
|
||||
desiredHealthy: number
|
||||
disruptionsAllowed: number
|
||||
expectedPods: number
|
||||
currentHealthy: number;
|
||||
desiredHealthy: number;
|
||||
disruptionsAllowed: number;
|
||||
expectedPods: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -43,9 +43,9 @@ export interface IPodMetrics<T = IMetrics> {
|
||||
[metric: string]: T;
|
||||
cpuUsage: T;
|
||||
memoryUsage: T;
|
||||
fsUsage: T,
|
||||
fsWrites: T,
|
||||
fsReads: T,
|
||||
fsUsage: T;
|
||||
fsWrites: T;
|
||||
fsReads: T;
|
||||
networkReceive: T;
|
||||
networkTransmit: T;
|
||||
cpuRequests?: T;
|
||||
@ -117,7 +117,7 @@ export interface IPodContainer extends Partial<Record<PodContainerProbe, IContai
|
||||
};
|
||||
secretRef?: {
|
||||
name: string;
|
||||
}
|
||||
};
|
||||
}[];
|
||||
volumeMounts?: {
|
||||
name: string;
|
||||
@ -273,7 +273,7 @@ export class Pod extends WorkloadKubeObject {
|
||||
hostIP: string;
|
||||
podIP: string;
|
||||
podIPs?: {
|
||||
ip: string
|
||||
ip: string;
|
||||
}[];
|
||||
startTime: string;
|
||||
initContainerStatuses?: IPodContainerStatus[];
|
||||
|
||||
@ -16,12 +16,9 @@ import { KubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object";
|
||||
import byline from "byline";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-event";
|
||||
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
|
||||
import { noop } from "../utils";
|
||||
import { noop, WrappedAbortController } from "../utils";
|
||||
import type { RequestInit } from "node-fetch";
|
||||
|
||||
// BUG: https://github.com/mysticatea/abort-controller/pull/22
|
||||
// eslint-disable-next-line import/no-named-as-default
|
||||
import AbortController from "abort-controller";
|
||||
import type AbortController from "abort-controller";
|
||||
import { Agent, AgentOptions } from "https";
|
||||
import type { Patch } from "rfc6902";
|
||||
|
||||
@ -91,7 +88,7 @@ export interface KubeApiListOptions {
|
||||
export interface IKubePreferredVersion {
|
||||
preferredVersion?: {
|
||||
version: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface IKubeResourceList {
|
||||
@ -108,7 +105,7 @@ export interface IKubeResourceList {
|
||||
export interface ILocalKubeApiConfig {
|
||||
metadata: {
|
||||
uid: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Background";
|
||||
@ -119,7 +116,7 @@ export type PropagationPolicy = undefined | "Orphan" | "Foreground" | "Backgroun
|
||||
export interface IKubeApiCluster extends ILocalKubeApiConfig { }
|
||||
|
||||
export type PartialKubeObject<T extends KubeObject> = Partial<Omit<T, "metadata">> & {
|
||||
metadata?: Partial<T["metadata"]>,
|
||||
metadata?: Partial<T["metadata"]>;
|
||||
};
|
||||
|
||||
export interface IRemoteKubeApiConfig {
|
||||
@ -127,12 +124,12 @@ export interface IRemoteKubeApiConfig {
|
||||
server: string;
|
||||
caData?: string;
|
||||
skipTLSVerify?: boolean;
|
||||
}
|
||||
};
|
||||
user: {
|
||||
token?: string | (() => Promise<string>);
|
||||
clientCertificateData?: string;
|
||||
clientKeyData?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function forCluster<T extends KubeObject, Y extends KubeApi<T> = KubeApi<T>>(cluster: ILocalKubeApiConfig, kubeClass: KubeObjectConstructor<T>, apiClass: new (apiOpts: IKubeApiOptions<T>) => Y = null): KubeApi<T> {
|
||||
@ -219,16 +216,16 @@ export function ensureObjectSelfLink(api: KubeApi<KubeObject>, object: KubeJsonA
|
||||
|
||||
export type KubeApiWatchCallback = (data: IKubeWatchEvent<KubeJsonApiData>, error: any) => void;
|
||||
|
||||
export type KubeApiWatchOptions = {
|
||||
export interface KubeApiWatchOptions {
|
||||
namespace: string;
|
||||
callback?: KubeApiWatchCallback;
|
||||
abortController?: AbortController
|
||||
abortController?: AbortController;
|
||||
watchId?: string;
|
||||
retry?: boolean;
|
||||
|
||||
// timeout in seconds
|
||||
timeout?: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type KubeApiPatchType = "merge" | "json" | "strategic";
|
||||
|
||||
@ -582,14 +579,7 @@ export class KubeApi<T extends KubeObject> {
|
||||
const { watchId = `${this.kind.toLowerCase()}-${this.watchId++}` } = opts;
|
||||
|
||||
// Create AbortController for this request
|
||||
const abortController = new AbortController();
|
||||
|
||||
// If caller aborts, abort using request's abortController
|
||||
if (opts.abortController) {
|
||||
opts.abortController.signal.addEventListener("abort", () => {
|
||||
abortController.abort();
|
||||
});
|
||||
}
|
||||
const abortController = new WrappedAbortController(opts.abortController);
|
||||
|
||||
abortController.signal.addEventListener("abort", () => {
|
||||
logger.info(`[KUBE-API] watch (${watchId}) aborted ${watchUrl}`);
|
||||
@ -681,13 +671,14 @@ export class KubeApi<T extends KubeObject> {
|
||||
callback(null, error);
|
||||
});
|
||||
|
||||
return abortController.abort;
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}
|
||||
|
||||
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
|
||||
if (event.type === "ERROR") {
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
ensureObjectSelfLink(this, event.object);
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import type { ClusterContext } from "./cluster-context";
|
||||
|
||||
import { action, computed, makeObservable, observable, reaction, when } from "mobx";
|
||||
import { autoBind, noop, rejectPromiseBy } from "../utils";
|
||||
import { autoBind, Disposer, noop, rejectPromiseBy } from "../utils";
|
||||
import { KubeObject, KubeStatus } from "./kube-object";
|
||||
import type { IKubeWatchEvent } from "./kube-watch-event";
|
||||
import { ItemStore } from "../item.store";
|
||||
@ -247,7 +247,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
}
|
||||
|
||||
@action
|
||||
async reloadAll(opts: { force?: boolean, namespaces?: string[], merge?: boolean } = {}) {
|
||||
async reloadAll(opts: { force?: boolean; namespaces?: string[]; merge?: boolean } = {}) {
|
||||
const { force = false, ...loadingOptions } = opts;
|
||||
|
||||
if (this.isLoading || (this.isLoaded && !force)) {
|
||||
@ -312,14 +312,14 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return this.api.create(params, data);
|
||||
}
|
||||
|
||||
create = async (params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> => {
|
||||
async create(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {
|
||||
const newItem = await this.createItem(params, data);
|
||||
const items = this.sortItems([...this.items, newItem]);
|
||||
|
||||
this.items.replace(items);
|
||||
|
||||
return newItem;
|
||||
};
|
||||
}
|
||||
|
||||
private postUpdate(rawItem: KubeJsonApiData): T {
|
||||
const newItem = new this.api.objectConstructor(rawItem);
|
||||
@ -369,6 +369,10 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
return Promise.all(this.selectedItems.map(this.remove));
|
||||
}
|
||||
|
||||
async removeItems(items: T[]) {
|
||||
await Promise.all(items.map(this.remove));
|
||||
}
|
||||
|
||||
// collect items from watch-api events to avoid UI blowing up with huge streams of data
|
||||
protected eventsBuffer = observable.array<IKubeWatchEvent<KubeJsonApiData>>([], { deep: false });
|
||||
|
||||
@ -378,7 +382,7 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
||||
});
|
||||
}
|
||||
|
||||
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}) {
|
||||
subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}): Disposer {
|
||||
if (this.api.isNamespaced) {
|
||||
Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])])
|
||||
.then(() => {
|
||||
|
||||
@ -147,7 +147,7 @@ export function loadConfigFromString(content: string): ConfigResult {
|
||||
}
|
||||
|
||||
export interface SplitConfigEntry {
|
||||
config: KubeConfig,
|
||||
config: KubeConfig;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -63,8 +63,8 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader
|
||||
extensionsStore: ExtensionsStore
|
||||
extensionLoader: ExtensionLoader;
|
||||
extensionsStore: ExtensionsStore;
|
||||
}
|
||||
|
||||
export abstract class LensProtocolRouter {
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../utils/buildUrl";
|
||||
import { appsRoute } from "./apps";
|
||||
import { helmRoute } from "./helm";
|
||||
|
||||
export const helmChartsRoute: RouteProps = {
|
||||
path: `${appsRoute.path}/charts/:repo?/:chartName?`,
|
||||
path: `${helmRoute.path}/charts/:repo?/:chartName?`,
|
||||
};
|
||||
|
||||
export interface HelmChartsRouteParams {
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../utils/buildUrl";
|
||||
|
||||
export const appsRoute: RouteProps = {
|
||||
path: "/apps",
|
||||
export const helmRoute: RouteProps = {
|
||||
path: "/helm",
|
||||
};
|
||||
|
||||
export const appsURL = buildURL(appsRoute.path);
|
||||
export const helmURL = buildURL(helmRoute.path);
|
||||
@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
export * from "./add-cluster";
|
||||
export * from "./apps";
|
||||
export * from "./catalog";
|
||||
export * from "./cluster-view";
|
||||
export * from "./cluster";
|
||||
@ -16,6 +15,7 @@ export * from "./entity-settings";
|
||||
export * from "./events";
|
||||
export * from "./extensions";
|
||||
export * from "./helm-charts";
|
||||
export * from "./helm";
|
||||
export * from "./hpa";
|
||||
export * from "./ingresses";
|
||||
export * from "./limit-ranges";
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
|
||||
import type { RouteProps } from "react-router";
|
||||
import { buildURL } from "../utils/buildUrl";
|
||||
import { appsRoute } from "./apps";
|
||||
import { helmRoute } from "./helm";
|
||||
|
||||
export const releaseRoute: RouteProps = {
|
||||
path: `${appsRoute.path}/releases/:namespace?/:name?`,
|
||||
path: `${helmRoute.path}/releases/:namespace?/:name?`,
|
||||
};
|
||||
|
||||
export interface ReleaseRouteParams {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import moment from "moment-timezone";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { getAppVersion, ObservableToggleSet } from "../utils";
|
||||
import { getAppVersion } from "../utils";
|
||||
import type { editor } from "monaco-editor";
|
||||
import merge from "lodash/merge";
|
||||
import { SemVer } from "semver";
|
||||
@ -236,10 +236,10 @@ const terminalCopyOnSelect: PreferenceDescription<boolean> = {
|
||||
},
|
||||
};
|
||||
|
||||
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, ObservableToggleSet<string>>> = {
|
||||
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, Set<string>>> = {
|
||||
fromStore(val) {
|
||||
return new Map(
|
||||
(val ?? []).map(([tableId, columnIds]) => [tableId, new ObservableToggleSet(columnIds)]),
|
||||
(val ?? []).map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
|
||||
);
|
||||
},
|
||||
toStore(val) {
|
||||
@ -327,7 +327,7 @@ export type ExtensionRegistry = {
|
||||
location: ExtensionRegistryLocation.DEFAULT | ExtensionRegistryLocation.NPMRC;
|
||||
customUrl?: undefined;
|
||||
} | {
|
||||
location: ExtensionRegistryLocation.CUSTOM,
|
||||
location: ExtensionRegistryLocation.CUSTOM;
|
||||
customUrl: string;
|
||||
};
|
||||
|
||||
|
||||
@ -2,13 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { UserStore } from "./user-store";
|
||||
|
||||
const userStoreInjectable = getInjectable({
|
||||
id: "user-store",
|
||||
instantiate: () => UserStore.createInstance(),
|
||||
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default userStoreInjectable;
|
||||
|
||||
@ -5,14 +5,15 @@
|
||||
|
||||
import { app, ipcMain } from "electron";
|
||||
import semver, { SemVer } from "semver";
|
||||
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||
import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
|
||||
import { BaseStore } from "../base-store";
|
||||
import migrations, { fileNameMigration } from "../../migrations/user-store";
|
||||
import { getAppVersion } from "../utils/app-version";
|
||||
import { kubeConfigDefaultPath } from "../kube-helpers";
|
||||
import { appEventBus } from "../app-event-bus/event-bus";
|
||||
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
||||
import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers";
|
||||
import { getOrInsertSet, toggle, toJS, entries, fromEntries } from "../../renderer/utils";
|
||||
import { DESCRIPTORS } from "./preferences-helpers";
|
||||
import type { EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel, TerminalConfig } from "./preferences-helpers";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export interface UserStoreModel {
|
||||
@ -71,7 +72,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
* The column IDs under each configurable table ID that have been configured
|
||||
* to not be shown
|
||||
*/
|
||||
hiddenTableColumns = observable.map<string, ObservableToggleSet<string>>();
|
||||
hiddenTableColumns = observable.map<string, Set<string>>();
|
||||
|
||||
/**
|
||||
* Monaco editor configs
|
||||
@ -133,16 +134,11 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
return columnIds.some(columnId => config.has(columnId));
|
||||
}
|
||||
|
||||
@action
|
||||
/**
|
||||
* Toggles the hidden configuration of a table's column
|
||||
*/
|
||||
toggleTableColumnVisibility(tableId: string, columnId: string) {
|
||||
if (!this.hiddenTableColumns.get(tableId)) {
|
||||
this.hiddenTableColumns.set(tableId, new ObservableToggleSet());
|
||||
}
|
||||
|
||||
this.hiddenTableColumns.get(tableId).toggle(columnId);
|
||||
toggle(getOrInsertSet(this.hiddenTableColumns, tableId), columnId);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -169,55 +165,31 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||
}
|
||||
|
||||
this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy);
|
||||
this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell);
|
||||
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme);
|
||||
this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(preferences?.terminalTheme);
|
||||
this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone);
|
||||
this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs);
|
||||
this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry);
|
||||
this.allowErrorReporting = DESCRIPTORS.allowErrorReporting.fromStore(preferences?.allowErrorReporting);
|
||||
this.downloadMirror = DESCRIPTORS.downloadMirror.fromStore(preferences?.downloadMirror);
|
||||
this.downloadKubectlBinaries = DESCRIPTORS.downloadKubectlBinaries.fromStore(preferences?.downloadKubectlBinaries);
|
||||
this.downloadBinariesPath = DESCRIPTORS.downloadBinariesPath.fromStore(preferences?.downloadBinariesPath);
|
||||
this.kubectlBinariesPath = DESCRIPTORS.kubectlBinariesPath.fromStore(preferences?.kubectlBinariesPath);
|
||||
this.openAtLogin = DESCRIPTORS.openAtLogin.fromStore(preferences?.openAtLogin);
|
||||
this.hiddenTableColumns.replace(DESCRIPTORS.hiddenTableColumns.fromStore(preferences?.hiddenTableColumns));
|
||||
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
||||
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
||||
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
||||
this.terminalConfig = DESCRIPTORS.terminalConfig.fromStore(preferences?.terminalConfig);
|
||||
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
|
||||
this.extensionRegistryUrl = DESCRIPTORS.extensionRegistryUrl.fromStore(preferences?.extensionRegistryUrl);
|
||||
for (const [key, { fromStore }] of entries(DESCRIPTORS)) {
|
||||
const curVal = this[key];
|
||||
const newVal = fromStore((preferences)?.[key] as never) as never;
|
||||
|
||||
if (
|
||||
isObservableArray(curVal)
|
||||
|| isObservableSet(curVal)
|
||||
|| isObservableMap(curVal)
|
||||
) {
|
||||
curVal.replace(newVal);
|
||||
} else {
|
||||
this[key] = newVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toJSON(): UserStoreModel {
|
||||
const model: UserStoreModel = {
|
||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||
preferences: {
|
||||
httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy),
|
||||
shell: DESCRIPTORS.shell.toStore(this.shell),
|
||||
colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme),
|
||||
terminalTheme: DESCRIPTORS.terminalTheme.toStore(this.terminalTheme),
|
||||
localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone),
|
||||
allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs),
|
||||
allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry),
|
||||
allowErrorReporting: DESCRIPTORS.allowErrorReporting.toStore(this.allowErrorReporting),
|
||||
downloadMirror: DESCRIPTORS.downloadMirror.toStore(this.downloadMirror),
|
||||
downloadKubectlBinaries: DESCRIPTORS.downloadKubectlBinaries.toStore(this.downloadKubectlBinaries),
|
||||
downloadBinariesPath: DESCRIPTORS.downloadBinariesPath.toStore(this.downloadBinariesPath),
|
||||
kubectlBinariesPath: DESCRIPTORS.kubectlBinariesPath.toStore(this.kubectlBinariesPath),
|
||||
openAtLogin: DESCRIPTORS.openAtLogin.toStore(this.openAtLogin),
|
||||
hiddenTableColumns: DESCRIPTORS.hiddenTableColumns.toStore(this.hiddenTableColumns),
|
||||
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
||||
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
||||
terminalConfig: DESCRIPTORS.terminalConfig.toStore(this.terminalConfig),
|
||||
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
|
||||
extensionRegistryUrl: DESCRIPTORS.extensionRegistryUrl.toStore(this.extensionRegistryUrl),
|
||||
},
|
||||
};
|
||||
const preferences = fromEntries(
|
||||
entries(DESCRIPTORS)
|
||||
.map(([key, { toStore }]) => [key, toStore(this[key] as never)]),
|
||||
) as UserPreferencesModel;
|
||||
|
||||
return toJS(model);
|
||||
return toJS({
|
||||
lastSeenAppVersion: this.lastSeenAppVersion,
|
||||
preferences,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ describe("human format durations", () => {
|
||||
});
|
||||
|
||||
test("durations less than 8 years returns years and days", () => {
|
||||
const timeValue = Date.now() - new Date(moment().subtract(2, "years").subtract(5, "days").subtract(2, "hours").toDate()).getTime();
|
||||
const timeValue = new Date(2020, 0, 10, 12, 0, 0, 0).getTime() - new Date(2018, 0, 4, 12, 0, 0, 0).getTime();
|
||||
|
||||
const res = formatDuration(timeValue);
|
||||
|
||||
|
||||
15
src/common/utils/abort-controller.ts
Normal file
15
src/common/utils/abort-controller.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import AbortController from "abort-controller";
|
||||
|
||||
export class WrappedAbortController extends AbortController {
|
||||
constructor(parent?: AbortController) {
|
||||
super();
|
||||
|
||||
parent?.signal.addEventListener("abort", () => {
|
||||
this.abort();
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import type { KubeResource } from "../rbac";
|
||||
import { getHostedClusterId } from "./cluster-id-url-parsing";
|
||||
|
||||
export function isAllowedResource(resource: KubeResource | KubeResource[]) {
|
||||
const resources = [resource].flat();
|
||||
const cluster = ClusterStore.getInstance().getById(getHostedClusterId());
|
||||
|
||||
if (!cluster?.allowedResources) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resources.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const allowedResources = new Set(cluster.allowedResources);
|
||||
|
||||
return resources.every(resource => allowedResources.has(resource));
|
||||
}
|
||||
@ -3,6 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { runInAction } from "mobx";
|
||||
|
||||
/**
|
||||
* Get the value behind `key`. If it was not present, first insert `value`
|
||||
* @param map The map to interact with
|
||||
@ -19,20 +21,70 @@ export function getOrInsert<K, V>(map: Map<K, V>, key: K, value: V): V {
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but specifically for when `V` is `Map<any, any>` so that
|
||||
* the typings are inferred.
|
||||
* Like `getOrInsert` but specifically for when `V` is `Map<MK, MV>` so that
|
||||
* the typings are inferred correctly.
|
||||
*/
|
||||
export function getOrInsertMap<K, MK, MV>(map: Map<K, Map<MK, MV>>, key: K): Map<MK, MV> {
|
||||
return getOrInsert(map, key, new Map<MK, MV>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but with delayed creation of the item
|
||||
* Like `getOrInsert` but specifically for when `V` is `Set<any>` so that
|
||||
* the typings are inferred.
|
||||
*/
|
||||
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, value: () => V): V {
|
||||
export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
|
||||
return getOrInsert(map, key, new Set<SK>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `getOrInsert` but with delayed creation of the item. Which is useful
|
||||
* if it is very expensive to create the initial value.
|
||||
*/
|
||||
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, value());
|
||||
map.set(key, builder());
|
||||
}
|
||||
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value associated with `key` iff there was not a previous value
|
||||
* @param map The map to interact with
|
||||
* @throws if `key` already in map
|
||||
* @returns `this` so that `strictSet` can be chained
|
||||
*/
|
||||
export function strictSet<K, V>(map: Map<K, V>, key: K, val: V): typeof map {
|
||||
if (map.has(key)) {
|
||||
throw new TypeError("Duplicate key in map");
|
||||
}
|
||||
|
||||
return map.set(key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value associated with `key`
|
||||
* @param map The map to interact with
|
||||
* @throws if `key` did not a value associated with it
|
||||
*/
|
||||
export function strictGet<K, V>(map: Map<K, V>, key: K): V {
|
||||
if (!map.has(key)) {
|
||||
throw new TypeError("key not in map");
|
||||
}
|
||||
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* If `key` is in `set`, remove it otherwise add it.
|
||||
* @param set The set to manipulate
|
||||
* @param key The key to toggle the "is in"-ness of
|
||||
*/
|
||||
export function toggle<K>(set: Set<K>, key: K): void {
|
||||
runInAction(() => {
|
||||
// Returns true if value was already in Set; otherwise false.
|
||||
if (!set.delete(key)) {
|
||||
set.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { action, ObservableMap, runInAction } from "mobx";
|
||||
|
||||
export function multiSet<T, V>(map: Map<T, V>, newEntries: [T, V][]): void {
|
||||
runInAction(() => {
|
||||
for (const [key, val] of newEntries) {
|
||||
map.set(key, val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class ExtendedMap<K, V> extends Map<K, V> {
|
||||
static new<K, V>(entries?: readonly (readonly [K, V])[] | null): ExtendedMap<K, V> {
|
||||
return new ExtendedMap<K, V>(entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value behind `key`. If it was not present, first insert the value returned by `getVal`
|
||||
* @param key The key to insert into the map with
|
||||
* @param getVal A function that returns a new instance of `V`.
|
||||
* @returns The value in the map
|
||||
*/
|
||||
getOrInsert(key: K, getVal: () => V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, getVal()).get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value associated with `key` iff there was not a previous value
|
||||
* @throws if `key` already in map
|
||||
* @returns `this` so that `strictSet` can be chained
|
||||
*/
|
||||
strictSet(key: K, val: V): this {
|
||||
if (this.has(key)) {
|
||||
throw new TypeError("Duplicate key in map");
|
||||
}
|
||||
|
||||
return this.set(key, val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value associated with `key`
|
||||
* @throws if `key` did not a value associated with it
|
||||
*/
|
||||
strictGet(key: K): V {
|
||||
if (!this.has(key)) {
|
||||
throw new TypeError("key not in map");
|
||||
}
|
||||
|
||||
return this.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtendedObservableMap<K, V> extends ObservableMap<K, V> {
|
||||
@action
|
||||
getOrInsert(key: K, getVal: () => V): V {
|
||||
if (this.has(key)) {
|
||||
return this.get(key);
|
||||
}
|
||||
|
||||
return this.set(key, getVal()).get(key);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ export function noop<T extends any[]>(...args: T): void {
|
||||
return void args;
|
||||
}
|
||||
|
||||
export * from "./abort-controller";
|
||||
export * from "./app-version";
|
||||
export * from "./autobind";
|
||||
export * from "./camelCase";
|
||||
@ -24,13 +25,12 @@ export * from "./delay";
|
||||
export * from "./disposer";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./extended-map";
|
||||
export * from "./formatDuration";
|
||||
export * from "./getRandId";
|
||||
export * from "./hash-set";
|
||||
export * from "./n-fircate";
|
||||
export * from "./objects";
|
||||
export * from "./openExternal";
|
||||
export * from "./openBrowser";
|
||||
export * from "./paths";
|
||||
export * from "./promise-exec";
|
||||
export * from "./reject-promise";
|
||||
@ -38,12 +38,12 @@ export * from "./singleton";
|
||||
export * from "./sort-compare";
|
||||
export * from "./splitArray";
|
||||
export * from "./tar";
|
||||
export * from "./toggle-set";
|
||||
export * from "./toJS";
|
||||
export * from "./type-narrowing";
|
||||
export * from "./types";
|
||||
export * from "./wait-for-path";
|
||||
|
||||
export type { Tuple } from "./tuple";
|
||||
|
||||
import * as iter from "./iter";
|
||||
import * as array from "./array";
|
||||
import * as tuple from "./tuple";
|
||||
|
||||
22
src/common/utils/is-allowed-resource.injectable.ts
Normal file
22
src/common/utils/is-allowed-resource.injectable.ts
Normal file
@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import allowedResourcesInjectable from "../cluster-store/allowed-resources.injectable";
|
||||
import type { KubeResource } from "../rbac";
|
||||
|
||||
export type IsAllowedResource = (resource: KubeResource) => boolean;
|
||||
|
||||
// TODO: This injectable obscures MobX de-referencing. Make it more apparent in usage.
|
||||
const isAllowedResourceInjectable = getInjectable({
|
||||
id: "is-allowed-resource",
|
||||
|
||||
instantiate: (di) => {
|
||||
const allowedResources = di.inject(allowedResourcesInjectable);
|
||||
|
||||
return (resource: KubeResource) => allowedResources.get().has(resource);
|
||||
},
|
||||
});
|
||||
|
||||
export default isAllowedResourceInjectable;
|
||||
@ -7,6 +7,10 @@
|
||||
* A better typed version of `Object.fromEntries` where the keys are known to
|
||||
* be a specific subset
|
||||
*/
|
||||
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): { [k in Key]: T } {
|
||||
export function fromEntries<T, Key extends string>(entries: Iterable<readonly [Key, T]>): Record<Key, T> {
|
||||
return Object.fromEntries(entries) as { [k in Key]: T };
|
||||
}
|
||||
|
||||
export function entries<T extends Record<string, any>>(obj: T): [keyof T, T[keyof T]][] {
|
||||
return Object.entries(obj);
|
||||
}
|
||||
|
||||
29
src/common/utils/openBrowser.ts
Normal file
29
src/common/utils/openBrowser.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { shell } from "electron";
|
||||
|
||||
const allowedProtocols = new Set(["http:", "https:"]);
|
||||
|
||||
/**
|
||||
* Opens a link using the program configured as the default browser
|
||||
* on the target platform. Will reject URLs with a scheme other than
|
||||
* http or https to prevent programs other than the default browser
|
||||
* running.
|
||||
*
|
||||
* @param url The URL to open
|
||||
*/
|
||||
export function openBrowser(url: string): Promise<void> {
|
||||
if (allowedProtocols.has(new URL(url).protocol)) {
|
||||
return shell.openExternal(url);
|
||||
}
|
||||
|
||||
return Promise.reject(new TypeError("not an http(s) URL"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use openBrowser
|
||||
*/
|
||||
export const openExternal = openBrowser;
|
||||
@ -1,11 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
// Opens a link in external browser
|
||||
import { shell } from "electron";
|
||||
|
||||
export function openExternal(url: string) {
|
||||
return shell.openExternal(url);
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
type StaticThis<T, R extends any[]> = { new(...args: R): T };
|
||||
interface StaticThis<T, R extends any[]> { new(...args: R): T }
|
||||
|
||||
export class Singleton {
|
||||
private static instances = new WeakMap<object, Singleton>();
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { ObservableSet } from "mobx";
|
||||
|
||||
export class ToggleSet<T> extends Set<T> {
|
||||
public toggle(value: T): void {
|
||||
if (!this.delete(value)) {
|
||||
// Set.prototype.delete returns false if `value` was not in the set
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ObservableToggleSet<T> extends ObservableSet<T> {
|
||||
public toggle(value: T): void {
|
||||
if (!this.delete(value)) {
|
||||
// Set.prototype.delete returns false if `value` was not in the set
|
||||
this.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,8 +8,14 @@ import * as array from "../utils/array";
|
||||
/**
|
||||
* A strict N-tuple of type T
|
||||
*/
|
||||
export type Tuple<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
|
||||
type _TupleOf<T, N extends number, R extends unknown[]> = R["length"] extends N ? R : _TupleOf<T, N, [T, ...R]>;
|
||||
export type Tuple<T, N extends number> = N extends N
|
||||
? number extends N
|
||||
? T[]
|
||||
: TupleOfImpl<T, N, []>
|
||||
: never;
|
||||
type TupleOfImpl<T, N extends number, R extends unknown[]> = R["length"] extends N
|
||||
? R
|
||||
: TupleOfImpl<T, N, [T, ...R]>;
|
||||
|
||||
/**
|
||||
* Iterates over `sources` yielding full tuples until one of the tuple arrays
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An N length tuple of T
|
||||
*/
|
||||
export type Tuple<T, N extends number> = N extends N ? number extends N ? T[] : _TupleOf<T, N, []> : never;
|
||||
type _TupleOf<T, N extends number, R extends unknown[]> = R["length"] extends N ? R : _TupleOf<T, N, [T, ...R]>;
|
||||
@ -38,7 +38,6 @@ export const mainDir = path.join(contextDir, "src/main");
|
||||
export const rendererDir = path.join(contextDir, "src/renderer");
|
||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
||||
export const webpackDevServerPort = 9009;
|
||||
|
||||
// Special runtime paths
|
||||
defineGlobal("__static", {
|
||||
|
||||
@ -3,11 +3,12 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { updateRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api";
|
||||
import { contextDir } from "../vars";
|
||||
|
||||
const updateReleaseInjectable = getInjectable({
|
||||
instantiate: () => updateRelease,
|
||||
const contextDirInjectable = getInjectable({
|
||||
id: "context-dir",
|
||||
instantiate: () => contextDir,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default updateReleaseInjectable;
|
||||
export default contextDirInjectable;
|
||||
14
src/common/vars/is-development.injectable.ts
Normal file
14
src/common/vars/is-development.injectable.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { isDevelopment } from "../vars";
|
||||
|
||||
const isDevelopmentInjectable = getInjectable({
|
||||
id: "is-development",
|
||||
instantiate: () => isDevelopment,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default isDevelopmentInjectable;
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { isLinux } from "../vars";
|
||||
|
||||
const isLinuxInjectable = getInjectable({
|
||||
id: "is-linux",
|
||||
instantiate: () => isLinux,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default isLinuxInjectable;
|
||||
|
||||
@ -2,12 +2,12 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { isWindows } from "../vars";
|
||||
|
||||
const isWindowsInjectable = getInjectable({
|
||||
id: "is-windows",
|
||||
instantiate: () => isWindows,
|
||||
lifecycle: lifecycleEnum.singleton,
|
||||
});
|
||||
|
||||
export default isWindowsInjectable;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user