1
0
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:
DmitriyNoa 2022-03-02 16:59:33 +01:00
commit 7d47861ddc
684 changed files with 10066 additions and 22109 deletions

View File

@ -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
View 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

View File

@ -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 }}

View File

@ -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

View File

@ -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
```

View File

@ -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)

View 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")));
});

View File

@ -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>

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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);

View File

@ -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 () => {

View File

@ -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"
}

View File

@ -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;

View File

@ -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", () => {

View File

@ -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 });

View File

@ -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;

View File

@ -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]>();

View File

@ -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");

View File

@ -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(() => {

View File

@ -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;

View 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, 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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",

View File

@ -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";

View File

@ -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);
};
}

View File

@ -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>;
};
}

View File

@ -7,7 +7,7 @@ import { observable } from "mobx";
export interface ClusterFrameInfo {
frameId: number;
processId: number
processId: number;
}
export const clusterFrameMap = observable.map<string, ClusterFrameInfo>();

View 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;

View File

@ -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;

View File

@ -22,7 +22,7 @@ export interface ClusterStoreModel {
}
interface Dependencies {
createCluster: (model: ClusterModel) => Cluster
createCluster: (model: ClusterModel) => Cluster;
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {

View File

@ -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;

View File

@ -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;
}

View 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;

View File

@ -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",

View File

@ -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" });

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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,
},
};
};

View File

@ -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;

View File

@ -14,7 +14,7 @@ export interface HotbarItem {
};
params?: {
[key: string]: string;
}
};
}
export type Hotbar = Required<CreateHotbarData>;

View File

@ -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)) {

View File

@ -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;
}

View 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");
});
});
});

View File

@ -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;

View File

@ -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>({

View File

@ -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 => ({

View File

@ -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: {

View File

@ -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 {

View File

@ -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(", ");

View File

@ -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;
};
}

View File

@ -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[];

View File

@ -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);

View File

@ -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(() => {

View File

@ -147,7 +147,7 @@ export function loadConfigFromString(content: string): ConfigResult {
}
export interface SplitConfigEntry {
config: KubeConfig,
config: KubeConfig;
error?: string;
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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);

View File

@ -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";

View File

@ -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 {

View File

@ -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;
};

View File

@ -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;

View File

@ -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,
});
}
}

View File

@ -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);

View 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();
});
}
}

View File

@ -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));
}

View File

@ -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);
}
});
}

View File

@ -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);
}
}

View File

@ -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";

View 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;

View File

@ -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);
}

View 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;

View File

@ -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);
}

View File

@ -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>();

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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]>;

View File

@ -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", {

View File

@ -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;

View 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;

View File

@ -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;

View File

@ -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