mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into feat/turn-on-tailwind-jut
This commit is contained in:
commit
f696999189
@ -24,12 +24,12 @@ jobs:
|
||||
Write-Output ("##vso[task.setvariable variable=CI_BUILD_TAG;]$CI_BUILD_TAG")
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
displayName: Set the tag name as an environment variable
|
||||
|
||||
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: $(node_version)
|
||||
displayName: Install Node.js
|
||||
|
||||
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'yarn | "$(Agent.OS)"" | yarn.lock'
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
yarn | "$(Agent.OS)"
|
||||
path: $(YARN_CACHE_FOLDER)
|
||||
displayName: Cache Yarn packages
|
||||
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||
@ -47,13 +47,13 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
displayName: Customize config
|
||||
|
||||
|
||||
- script: make node_modules
|
||||
displayName: Install dependencies
|
||||
|
||||
|
||||
- script: make build-npm
|
||||
displayName: Generate npm package
|
||||
|
||||
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
@ -75,12 +75,12 @@ jobs:
|
||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
displayName: Set the tag name as an environment variable
|
||||
|
||||
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: $(node_version)
|
||||
displayName: Install Node.js
|
||||
|
||||
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
||||
@ -88,7 +88,7 @@ jobs:
|
||||
yarn | "$(Agent.OS)"
|
||||
path: $(YARN_CACHE_FOLDER)
|
||||
displayName: Cache Yarn packages
|
||||
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||
@ -98,13 +98,13 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
displayName: Customize config
|
||||
|
||||
|
||||
- script: make node_modules
|
||||
displayName: Install dependencies
|
||||
|
||||
|
||||
- script: make build-npm
|
||||
displayName: Generate npm package
|
||||
|
||||
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
@ -128,12 +128,12 @@ jobs:
|
||||
- script: CI_BUILD_TAG=`git describe --tags` && echo "##vso[task.setvariable variable=CI_BUILD_TAG]$CI_BUILD_TAG"
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
displayName: Set the tag name as an environment variable
|
||||
|
||||
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: $(node_version)
|
||||
displayName: Install Node.js
|
||||
|
||||
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'yarn | "$(Agent.OS)" | yarn.lock'
|
||||
@ -141,7 +141,7 @@ jobs:
|
||||
yarn | "$(Agent.OS)"
|
||||
path: $(YARN_CACHE_FOLDER)
|
||||
displayName: Cache Yarn packages
|
||||
|
||||
|
||||
- bash: |
|
||||
set -e
|
||||
git clone "https://${GH_TOKEN}@github.com/lensapp/lens-ide.git" .lens-ide-overlay
|
||||
@ -151,13 +151,13 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
displayName: Customize config
|
||||
|
||||
|
||||
- script: make node_modules
|
||||
displayName: Install dependencies
|
||||
|
||||
|
||||
- script: make build-npm
|
||||
displayName: Generate npm package
|
||||
|
||||
|
||||
- bash: |
|
||||
sudo chown root:root /
|
||||
sudo apt-get update && sudo apt-get install -y snapd
|
||||
@ -168,7 +168,7 @@ jobs:
|
||||
env:
|
||||
SNAP_LOGIN: $(SNAP_LOGIN)
|
||||
displayName: Setup snapcraft
|
||||
|
||||
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
|
||||
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
71
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ master ]
|
||||
schedule:
|
||||
- cron: '41 3 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
2
Makefile
2
Makefile
@ -60,6 +60,8 @@ build: node_modules binaries/client
|
||||
$(MAKE) build-extensions -B
|
||||
yarn run compile
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
# https://github.com/ukoloff/win-ca#clear-pem-folder-on-publish
|
||||
rm -rf node_modules/win-ca/pem
|
||||
yarn run electron-builder --publish onTag --x64 --ia32
|
||||
else
|
||||
yarn run electron-builder --publish onTag
|
||||
|
||||
@ -6,7 +6,7 @@ In this section you will learn how this extension works under the hood.
|
||||
The Hello World sample extension does three things:
|
||||
|
||||
- Implements `onActivate()` and outputs a message to the console.
|
||||
- Implements `onDectivate()` and outputs a message to the console.
|
||||
- Implements `onDeactivate()` and outputs a message to the console.
|
||||
- Registers `ClusterPage` so that the page is visible in the left-side menu of the cluster dashboard.
|
||||
|
||||
Let's take a closer look at our Hello World sample's source code and see how these three things are achieved.
|
||||
|
||||
@ -72,4 +72,4 @@ To dive deeper, consider looking at [Common Capabilities](../capabilities/common
|
||||
If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues).
|
||||
You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing).
|
||||
|
||||
The Generator source code is hosted at [Github](https://github.com/lensapp/generator-lens-ext).
|
||||
The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext).
|
||||
|
||||
@ -213,7 +213,7 @@ export class CertificatePage extends React.Component<{ extension: LensRendererEx
|
||||
return (
|
||||
<TabLayout>
|
||||
<KubeObjectListLayout
|
||||
className="Certicates" store={certificatesStore}
|
||||
className="Certificates" store={certificatesStore}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: (certificate: Certificate) => certificate.getName(),
|
||||
[sortBy.namespace]: (certificate: Certificate) => certificate.metadata.namespace,
|
||||
|
||||
@ -94,7 +94,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
||||
```
|
||||
|
||||
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
||||
This page would be defined in your extension's `Renderer.LensExtension` implmentation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||
|
||||
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ This is the cluster that the resource stack will be applied to, and the construc
|
||||
Similarly, `ExampleClusterFeature` implements an `uninstall()` method which simply invokes the `kubectlDeleteFolder()` method of the `Renderer.K8sApi.ResourceStack` class.
|
||||
`kubectlDeleteFolder()` tries to delete from the cluster all kubernetes resources found in the folder passed to it, again in this case `../resources`.
|
||||
|
||||
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utiliize the kubernetes api to inspect the resource stack status.
|
||||
`ExampleClusterFeature` also implements an `isInstalled()` method, which demonstrates how you can utilize the kubernetes api to inspect the resource stack status.
|
||||
`isInstalled()` simply tries to find a pod named `example-pod`, as a way to determine if the pod is already installed.
|
||||
This method can be useful in creating a context-sensitive UI for installing/uninstalling the feature, as demonstrated in the next sample code.
|
||||
|
||||
|
||||
@ -148,8 +148,8 @@ export class ExamplePreferenceInput extends React.Component {
|
||||
return (
|
||||
<Checkbox
|
||||
label="I understand appPreferences"
|
||||
value={ExamplePreferencesStore.getInstace().enabled}
|
||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
||||
value={ExamplePreferencesStore.getInstance().enabled}
|
||||
onChange={v => { ExamplePreferencesStore.getInstance().enabled = v; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -43,4 +43,4 @@ Say you have your project folder at `~/my-extension/` and you want to create an
|
||||
npm pack
|
||||
```
|
||||
|
||||
This will create a NPM tarball that can be hosted on Github Releases or any other publicly available file hosting service.
|
||||
This will create a NPM tarball that can be hosted on GitHub Releases or any other publicly available file hosting service.
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"name": "kube-object-event-status",
|
||||
"version": "0.1.0",
|
||||
"description": "Adds kube object status from events",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
"metadata": {},
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack && npm pack",
|
||||
"dev": "webpack --watch",
|
||||
"test": "echo NO TESTS"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
40
package.json
40
package.json
@ -3,7 +3,7 @@
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"homepage": "https://github.com/lensapp/lens",
|
||||
"version": "5.0.2",
|
||||
"version": "5.1.2",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
@ -49,7 +49,8 @@
|
||||
},
|
||||
"config": {
|
||||
"bundledKubectlVersion": "1.18.15",
|
||||
"bundledHelmVersion": "3.5.4"
|
||||
"bundledHelmVersion": "3.5.4",
|
||||
"sentryDsn": ""
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12 <13"
|
||||
@ -62,8 +63,7 @@
|
||||
},
|
||||
"moduleNameMapper": {
|
||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
||||
"^@lingui/macro$": "<rootDir>/__mocks__/@linguiMacro.ts"
|
||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||
},
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/dist",
|
||||
@ -183,6 +183,8 @@
|
||||
"@hapi/call": "^8.0.1",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
"@kubernetes/client-node": "^0.14.3",
|
||||
"@sentry/electron": "^2.5.0",
|
||||
"@sentry/integrations": "^6.8.0",
|
||||
"abort-controller": "^3.0.0",
|
||||
"array-move": "^3.0.1",
|
||||
"auto-bind": "^4.0.0",
|
||||
@ -198,7 +200,6 @@
|
||||
"electron-updater": "^4.3.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"filehound": "^1.17.4",
|
||||
"filenamify": "^4.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"handlebars": "^4.7.7",
|
||||
@ -215,7 +216,7 @@
|
||||
"mobx": "^6.3.0",
|
||||
"mobx-observable-history": "^2.0.1",
|
||||
"mobx-react": "^7.1.0",
|
||||
"mock-fs": "^4.12.0",
|
||||
"mock-fs": "^4.14.0",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"node-pty": "^0.9.0",
|
||||
@ -251,6 +252,8 @@
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||
"@sentry/react": "^6.8.0",
|
||||
"@sentry/types": "^6.8.0",
|
||||
"@testing-library/jest-dom": "^5.13.0",
|
||||
"@testing-library/react": "^11.2.6",
|
||||
"@types/byline": "^4.2.32",
|
||||
@ -274,11 +277,11 @@
|
||||
"@types/marked": "^2.0.3",
|
||||
"@types/md5-file": "^4.0.2",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/mock-fs": "^4.10.0",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/module-alias": "^2.0.0",
|
||||
"@types/node": "12.20",
|
||||
"@types/npm": "^2.0.31",
|
||||
"@types/progress-bar-webpack-plugin": "^2.1.0",
|
||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||
"@types/proper-lockfile": "^4.1.1",
|
||||
"@types/randomcolor": "^0.5.5",
|
||||
"@types/react": "^17.0.0",
|
||||
@ -294,9 +297,8 @@
|
||||
"@types/request-promise-native": "^1.0.17",
|
||||
"@types/semver": "^7.2.0",
|
||||
"@types/sharp": "^0.28.3",
|
||||
"@types/shelljs": "^0.8.8",
|
||||
"@types/spdy": "^3.4.4",
|
||||
"@types/tar": "^4.0.4",
|
||||
"@types/tar": "^4.0.5",
|
||||
"@types/tcp-port-used": "^1.0.0",
|
||||
"@types/tempy": "^0.3.0",
|
||||
"@types/url-parse": "^1.4.3",
|
||||
@ -329,15 +331,15 @@
|
||||
"eslint-plugin-unused-imports": "^1.0.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"flex.box": "^3.4.4",
|
||||
"fork-ts-checker-webpack-plugin": "^5.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^5.2.1",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"include-media": "^1.4.9",
|
||||
"jest": "^26.0.1",
|
||||
"jest": "26.6.3",
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"jest-mock-extended": "^1.0.16",
|
||||
"make-plural": "^6.2.2",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"node-loader": "^1.0.3",
|
||||
@ -350,7 +352,7 @@
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"randomcolor": "^0.6.2",
|
||||
"raw-loader": "^4.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react-beautiful-dnd": "^13.1.0",
|
||||
"react-refresh": "^0.9.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
@ -363,7 +365,7 @@
|
||||
"spectron": "11.0.0",
|
||||
"style-loader": "^2.0.0",
|
||||
"tailwindcss": "^2.2.4",
|
||||
"ts-jest": "26.3.0",
|
||||
"ts-jest": "26.5.6",
|
||||
"ts-loader": "^7.0.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"type-fest": "^1.0.2",
|
||||
@ -372,14 +374,14 @@
|
||||
"typedoc-plugin-markdown": "^3.9.0",
|
||||
"typeface-roboto": "^1.1.13",
|
||||
"typescript": "^4.3.2",
|
||||
"typescript-plugin-css-modules": "^3.2.0",
|
||||
"typescript-plugin-css-modules": "^3.4.0",
|
||||
"url-loader": "^4.1.0",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"what-input": "^5.2.10",
|
||||
"xterm": "^4.12.0",
|
||||
"xterm-addon-fit": "^0.4.0"
|
||||
"xterm-addon-fit": "^0.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ class TestCatalogCategory2 extends CatalogCategory {
|
||||
}
|
||||
|
||||
describe("CatalogCategoryRegistry", () => {
|
||||
it("should remove only the category registered when running the disopser", () => {
|
||||
it("should remove only the category registered when running the disposer", () => {
|
||||
const registry = new TestCatalogCategoryRegistry();
|
||||
|
||||
expect(registry.items.length).toBe(0);
|
||||
|
||||
102
src/common/__tests__/event-emitter.test.ts
Normal file
102
src/common/__tests__/event-emitter.test.ts
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { EventEmitter } from "../event-emitter";
|
||||
|
||||
describe("EventEmitter", () => {
|
||||
it("should stop early if a listener returns false", () => {
|
||||
let called = false;
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => false, {});
|
||||
e.addListener(() => { called = true; }, {});
|
||||
e.emit();
|
||||
|
||||
expect(called).toBe(false);
|
||||
});
|
||||
|
||||
it("shouldn't stop early if a listener returns 0", () => {
|
||||
let called = false;
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => 0 as any, {});
|
||||
e.addListener(() => { called = true; }, {});
|
||||
e.emit();
|
||||
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
|
||||
it("prepended listeners should be called before others", () => {
|
||||
const callOrder: number[] = [];
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => { callOrder.push(1); }, {});
|
||||
e.addListener(() => { callOrder.push(2); }, {});
|
||||
e.addListener(() => { callOrder.push(3); }, { prepend: true });
|
||||
e.emit();
|
||||
|
||||
expect(callOrder).toStrictEqual([3, 1, 2]);
|
||||
});
|
||||
|
||||
it("once listeners should be called only once", () => {
|
||||
const callOrder: number[] = [];
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => { callOrder.push(1); }, {});
|
||||
e.addListener(() => { callOrder.push(2); }, {});
|
||||
e.addListener(() => { callOrder.push(3); }, { once: true });
|
||||
e.emit();
|
||||
e.emit();
|
||||
|
||||
expect(callOrder).toStrictEqual([1, 2, 3, 1, 2]);
|
||||
});
|
||||
|
||||
it("removeListener should stop the listener from being called", () => {
|
||||
const callOrder: number[] = [];
|
||||
const e = new EventEmitter<[]>();
|
||||
const r = () => { callOrder.push(3); };
|
||||
|
||||
e.addListener(() => { callOrder.push(1); }, {});
|
||||
e.addListener(() => { callOrder.push(2); }, {});
|
||||
e.addListener(r);
|
||||
|
||||
e.emit();
|
||||
e.removeListener(r);
|
||||
e.emit();
|
||||
|
||||
expect(callOrder).toStrictEqual([1, 2, 3, 1, 2]);
|
||||
});
|
||||
|
||||
it("removeAllListeners should stop the all listeners from being called", () => {
|
||||
const callOrder: number[] = [];
|
||||
const e = new EventEmitter<[]>();
|
||||
|
||||
e.addListener(() => { callOrder.push(1); });
|
||||
e.addListener(() => { callOrder.push(2); });
|
||||
e.addListener(() => { callOrder.push(3); });
|
||||
|
||||
e.emit();
|
||||
e.removeAllListeners();
|
||||
e.emit();
|
||||
|
||||
expect(callOrder).toStrictEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
@ -19,10 +19,6 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { Console } from "console";
|
||||
|
||||
console = new Console(process.stdout, process.stderr);
|
||||
|
||||
import mockFs from "mock-fs";
|
||||
|
||||
jest.mock("electron", () => {
|
||||
@ -37,27 +33,27 @@ jest.mock("electron", () => {
|
||||
});
|
||||
|
||||
import { UserStore } from "../user-store";
|
||||
import { Console } from "console";
|
||||
import { SemVer } from "semver";
|
||||
import electron from "electron";
|
||||
import { stdout, stderr } from "process";
|
||||
import { beforeEachWrapped } from "../../../integration/helpers/utils";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
import type { ClusterStoreModel } from "../cluster-store";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
describe("user store tests", () => {
|
||||
describe("for an empty config", () => {
|
||||
beforeEachWrapped(() => {
|
||||
UserStore.resetInstance();
|
||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||
|
||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||
|
||||
UserStore.getInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
UserStore.resetInstance();
|
||||
});
|
||||
|
||||
it("allows setting and retrieving lastSeenAppVersion", () => {
|
||||
@ -99,14 +95,31 @@ describe("user store tests", () => {
|
||||
|
||||
describe("migrations", () => {
|
||||
beforeEachWrapped(() => {
|
||||
UserStore.resetInstance();
|
||||
mockFs({
|
||||
"tmp": {
|
||||
"config.json": JSON.stringify({
|
||||
user: { username: "foobar" },
|
||||
preferences: { colorTheme: "light" },
|
||||
lastSeenAppVersion: "1.2.3"
|
||||
})
|
||||
}),
|
||||
"lens-cluster-store.json": JSON.stringify({
|
||||
clusters: [
|
||||
{
|
||||
id: "foobar",
|
||||
kubeConfigPath: "tmp/extension_data/foo/bar",
|
||||
},
|
||||
{
|
||||
id: "barfoo",
|
||||
kubeConfigPath: "some/other/path",
|
||||
},
|
||||
]
|
||||
} as ClusterStoreModel),
|
||||
"extension_data": {},
|
||||
},
|
||||
"some": {
|
||||
"other": {
|
||||
"path": "is file",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -114,6 +127,7 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
UserStore.resetInstance();
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
@ -122,5 +136,12 @@ describe("user store tests", () => {
|
||||
|
||||
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
||||
});
|
||||
|
||||
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
expect(us.syncKubeconfigEntries.has("tmp/extension_data/foo/bar")).toBe(false);
|
||||
expect(us.syncKubeconfigEntries.has("some/other/path")).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -55,15 +55,23 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
||||
};
|
||||
}
|
||||
|
||||
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||
distro?: string;
|
||||
kubeVersion?: string;
|
||||
}
|
||||
|
||||
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
||||
|
||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||
phase: KubernetesClusterStatusPhase;
|
||||
}
|
||||
|
||||
export class KubernetesCluster extends CatalogEntity<CatalogEntityMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||
public readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public readonly kind = "KubernetesCluster";
|
||||
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||
public static readonly apiVersion = "entity.k8slens.dev/v1alpha1";
|
||||
public static readonly kind = "KubernetesCluster";
|
||||
|
||||
public readonly apiVersion = KubernetesCluster.apiVersion;
|
||||
public readonly kind = KubernetesCluster.kind;
|
||||
|
||||
async connect(): Promise<void> {
|
||||
if (app) {
|
||||
|
||||
@ -101,6 +101,8 @@ export interface ClusterPreferences extends ClusterPrometheusPreferences {
|
||||
icon?: string;
|
||||
httpsProxy?: string;
|
||||
hiddenMetrics?: string[];
|
||||
nodeShellImage?: string;
|
||||
imagePullSecret?: string;
|
||||
}
|
||||
|
||||
export interface ClusterPrometheusPreferences {
|
||||
@ -117,6 +119,8 @@ export interface ClusterPrometheusPreferences {
|
||||
|
||||
const initialStates = "cluster:states";
|
||||
|
||||
export const initialNodeShellImage = "docker.io/alpine:3.13";
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
private static StateChannel = "cluster:state";
|
||||
|
||||
|
||||
@ -29,35 +29,31 @@ interface Options {
|
||||
type Callback<D extends [...any[]]> = (...data: D) => void | boolean;
|
||||
|
||||
export class EventEmitter<D extends [...any[]]> {
|
||||
protected listeners = new Map<Callback<D>, Options>();
|
||||
protected listeners: [Callback<D>, Options][] = [];
|
||||
|
||||
addListener(callback: Callback<D>, options: Options = {}) {
|
||||
if (options.prepend) {
|
||||
const listeners = [...this.listeners];
|
||||
const fn = options.prepend ? "unshift" : "push";
|
||||
|
||||
listeners.unshift([callback, options]);
|
||||
this.listeners = new Map(listeners);
|
||||
}
|
||||
else {
|
||||
this.listeners.set(callback, options);
|
||||
}
|
||||
this.listeners[fn]([callback, options]);
|
||||
}
|
||||
|
||||
removeListener(callback: Callback<D>) {
|
||||
this.listeners.delete(callback);
|
||||
this.listeners = this.listeners.filter(([cb]) => cb !== callback);
|
||||
}
|
||||
|
||||
removeAllListeners() {
|
||||
this.listeners.clear();
|
||||
this.listeners.length = 0;
|
||||
}
|
||||
|
||||
emit(...data: D) {
|
||||
[...this.listeners].every(([callback, options]) => {
|
||||
if (options.once) {
|
||||
for (const [callback, { once }] of this.listeners) {
|
||||
if (once) {
|
||||
this.removeListener(callback);
|
||||
}
|
||||
|
||||
return callback(...data) !== false;
|
||||
});
|
||||
if (callback(...data) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ export interface HotbarStoreModel {
|
||||
activeHotbarId: string;
|
||||
}
|
||||
|
||||
export const defaultHotbarCells = 12; // Number is choosen to easy hit any item with keyboard
|
||||
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@ -203,7 +203,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remvove all hotbar items that reference the `uid`.
|
||||
* Remove all hotbar items that reference the `uid`.
|
||||
* @param uid The `EntityId` that each hotbar item refers to
|
||||
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
||||
*/
|
||||
|
||||
@ -40,14 +40,14 @@ export interface KubeApiResourceData {
|
||||
export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
||||
"clusterroles": { kind: "ClusterRole", group: "rbac.authorization.k8s.io" },
|
||||
"clusterrolebindings": { kind: "ClusterRoleBinding", group: "rbac.authorization.k8s.io" },
|
||||
"configmaps": { kind: "ConfigMap" },
|
||||
"configmaps": { kind: "ConfigMap" }, //empty group means "core"
|
||||
"cronjobs": { kind: "CronJob", group: "batch" },
|
||||
"customresourcedefinitions": { kind: "CustomResourceDefinition", group: "apiextensions.k8s.io" },
|
||||
"daemonsets": { kind: "DaemonSet", group: "apps" },
|
||||
"deployments": { kind: "Deployment", group: "apps" },
|
||||
"endpoints": { kind: "Endpoint" },
|
||||
"events": { kind: "Event" },
|
||||
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler" },
|
||||
"horizontalpodautoscalers": { kind: "HorizontalPodAutoscaler", group: "autoscaling" },
|
||||
"ingresses": { kind: "Ingress", group: "networking.k8s.io" },
|
||||
"jobs": { kind: "Job", group: "batch" },
|
||||
"namespaces": { kind: "Namespace" },
|
||||
@ -58,13 +58,13 @@ export const apiResourceRecord: Record<KubeResource, KubeApiResourceData> = {
|
||||
"persistentvolumeclaims": { kind: "PersistentVolumeClaim" },
|
||||
"pods": { kind: "Pod" },
|
||||
"poddisruptionbudgets": { kind: "PodDisruptionBudget", group: "policy" },
|
||||
"podsecuritypolicies": { kind: "PodSecurityPolicy" },
|
||||
"podsecuritypolicies": { kind: "PodSecurityPolicy", group: "policy" },
|
||||
"resourcequotas": { kind: "ResourceQuota" },
|
||||
"replicasets": { kind: "ReplicaSet", group: "apps" },
|
||||
"roles": { kind: "Role", group: "rbac.authorization.k8s.io" },
|
||||
"rolebindings": { kind: "RoleBinding", group: "rbac.authorization.k8s.io" },
|
||||
"secrets": { kind: "Secret" },
|
||||
"serviceaccounts": { kind: "ServiceAccount", group: "core" },
|
||||
"serviceaccounts": { kind: "ServiceAccount" },
|
||||
"services": { kind: "Service" },
|
||||
"statefulsets": { kind: "StatefulSet", group: "apps" },
|
||||
"storageclasses": { kind: "StorageClass", group: "storage.k8s.io" },
|
||||
|
||||
77
src/common/sentry.ts
Normal file
77
src/common/sentry.ts
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { CaptureConsole, Dedupe, Offline } from "@sentry/integrations";
|
||||
import * as Sentry from "@sentry/electron";
|
||||
import { sentryDsn, isProduction } from "./vars";
|
||||
import { UserStore } from "./user-store";
|
||||
import logger from "../main/logger";
|
||||
|
||||
/**
|
||||
* "Translate" 'browser' to 'main' as Lens developer more familiar with the term 'main'
|
||||
*/
|
||||
function mapProcessName(processType: string) {
|
||||
if (processType === "browser") {
|
||||
return "main";
|
||||
}
|
||||
|
||||
return processType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Sentry for the current process so to send errors for debugging.
|
||||
*/
|
||||
export function SentryInit() {
|
||||
const processName = mapProcessName(process.type);
|
||||
|
||||
Sentry.init({
|
||||
beforeSend: (event) => {
|
||||
// default to false, in case instance of UserStore is not created (yet)
|
||||
const allowErrorReporting = UserStore.getInstance(false)?.allowErrorReporting ?? false;
|
||||
|
||||
if (allowErrorReporting) {
|
||||
return event;
|
||||
}
|
||||
|
||||
logger.info(`🔒 [SENTRY-BEFORE-SEND-HOOK]: allowErrorReporting: ${allowErrorReporting}. Sentry event is caught but not sent to server.`);
|
||||
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
|
||||
logger.info(event);
|
||||
logger.info("🔒 [SENTRY-BEFORE-SEND-HOOK]: === END OF SENTRY EVENT ===");
|
||||
|
||||
// if return null, the event won't be sent
|
||||
// ref https://github.com/getsentry/sentry-javascript/issues/2039
|
||||
return null;
|
||||
},
|
||||
dsn: sentryDsn,
|
||||
integrations: [
|
||||
new CaptureConsole({ levels: ["error"] }),
|
||||
new Dedupe(),
|
||||
new Offline()
|
||||
],
|
||||
initialScope: {
|
||||
tags: {
|
||||
|
||||
"process": processName
|
||||
}
|
||||
},
|
||||
environment: isProduction ? "production" : "development",
|
||||
});
|
||||
}
|
||||
@ -33,5 +33,9 @@ if (isMac) {
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||
try {
|
||||
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
|
||||
} catch (error) {
|
||||
logger.error(`[CA]: failed to force load: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,4 +20,4 @@
|
||||
*/
|
||||
|
||||
export * from "./user-store";
|
||||
export type { KubeconfigSyncEntry, KubeconfigSyncValue } from "./preferences-helpers";
|
||||
export type { KubeconfigSyncEntry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
||||
|
||||
@ -106,6 +106,19 @@ const allowTelemetry: PreferenceDescription<boolean> = {
|
||||
},
|
||||
};
|
||||
|
||||
const allowErrorReporting: PreferenceDescription<boolean> = {
|
||||
fromStore(val) {
|
||||
return val ?? true;
|
||||
},
|
||||
toStore(val) {
|
||||
if (val === true) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return val;
|
||||
},
|
||||
};
|
||||
|
||||
const downloadMirror: PreferenceDescription<string> = {
|
||||
fromStore(val) {
|
||||
return val ?? "default";
|
||||
@ -180,9 +193,9 @@ const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string
|
||||
toStore(val) {
|
||||
const res: [string, string[]][] = [];
|
||||
|
||||
for (const [table, columnes] of val) {
|
||||
if (columnes.size) {
|
||||
res.push([table, Array.from(columnes)]);
|
||||
for (const [table, columns] of val) {
|
||||
if (columns.size) {
|
||||
res.push([table, Array.from(columns)]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -227,6 +240,7 @@ export const DESCRIPTORS = {
|
||||
localeTimezone,
|
||||
allowUntrustedCAs,
|
||||
allowTelemetry,
|
||||
allowErrorReporting,
|
||||
downloadMirror,
|
||||
downloadKubectlBinaries,
|
||||
downloadBinariesPath,
|
||||
|
||||
@ -31,6 +31,7 @@ import path from "path";
|
||||
import { fileNameMigration } from "../../migrations/user-store";
|
||||
import { ObservableToggleSet, toJS } from "../../renderer/utils";
|
||||
import { DESCRIPTORS, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export interface UserStoreModel {
|
||||
lastSeenAppVersion: string;
|
||||
@ -58,6 +59,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
@observable seenContexts = observable.set<string>();
|
||||
@observable newContexts = observable.set<string>();
|
||||
@observable allowTelemetry: boolean;
|
||||
@observable allowErrorReporting: boolean;
|
||||
@observable allowUntrustedCAs: boolean;
|
||||
@observable colorTheme: string;
|
||||
@observable localeTimezone: string;
|
||||
@ -118,13 +120,13 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
*/
|
||||
isTableColumnHidden(tableId: string, ...columnIds: string[]): boolean {
|
||||
if (columnIds.length === 0) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const config = this.hiddenTableColumns.get(tableId);
|
||||
|
||||
if (!config) {
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return columnIds.some(columnId => config.has(columnId));
|
||||
@ -155,8 +157,8 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
}
|
||||
|
||||
@action
|
||||
protected fromStore(data: Partial<UserStoreModel> = {}) {
|
||||
const { lastSeenAppVersion, preferences } = data;
|
||||
protected fromStore({ lastSeenAppVersion, preferences }: Partial<UserStoreModel> = {}) {
|
||||
logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences });
|
||||
|
||||
if (lastSeenAppVersion) {
|
||||
this.lastSeenAppVersion = lastSeenAppVersion;
|
||||
@ -168,6 +170,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
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);
|
||||
@ -187,6 +190,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
||||
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),
|
||||
|
||||
130
src/common/utils/__tests__/paths.test.ts
Normal file
130
src/common/utils/__tests__/paths.test.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { describeIf } from "../../../../integration/helpers/utils";
|
||||
import { isWindows } from "../../vars";
|
||||
import { isLogicalChildPath } from "../paths";
|
||||
|
||||
describe("isLogicalChildPath", () => {
|
||||
describeIf(isWindows)("windows tests", () => {
|
||||
it.each([
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:\\Foo\\Bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:\\Bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:/Bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "C:/Foo/Bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "C:\\Foo",
|
||||
testPath: "D:\\Foo\\Bar",
|
||||
expected: false,
|
||||
},
|
||||
])("test %#", (testData) => {
|
||||
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||
});
|
||||
});
|
||||
|
||||
describeIf(!isWindows)("posix tests", () => {
|
||||
it.each([
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foobar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/../bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/./bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/.bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/..bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/...bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/..\\.bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/bar/../foo",
|
||||
testPath: "/foo/bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "/foo/\\bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
parentPath: "/foo",
|
||||
testPath: "./bar",
|
||||
expected: false,
|
||||
},
|
||||
])("test %#", (testData) => {
|
||||
expect(isLogicalChildPath(testData.parentPath, testData.testPath)).toBe(testData.expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -27,7 +27,7 @@ export class ExtendedMap<K, V> extends Map<K, V> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value behind `key`. If it was not pressent, first insert the value returned by `getVal`
|
||||
* 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
|
||||
|
||||
@ -45,6 +45,7 @@ export * from "./openExternal";
|
||||
export * from "./paths";
|
||||
export * from "./reject-promise";
|
||||
export * from "./singleton";
|
||||
export * from "./sort-compare";
|
||||
export * from "./splitArray";
|
||||
export * from "./tar";
|
||||
export * from "./toggle-set";
|
||||
|
||||
@ -33,3 +33,35 @@ function resolveTilde(filePath: string) {
|
||||
export function resolvePath(filePath: string): string {
|
||||
return path.resolve(resolveTilde(filePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if `testPath` represents a potential filesystem entry that would be
|
||||
* logically "within" the `parentPath` directory.
|
||||
*
|
||||
* This function will return `true` in the above case, and `false` otherwise.
|
||||
* It will return `false` if the two paths are the same (after resolving them).
|
||||
*
|
||||
* The function makes no FS calls and is platform dependant. Meaning that the
|
||||
* results are only guaranteed to be correct for the platform you are running
|
||||
* on.
|
||||
* @param parentPath The known path of a directory
|
||||
* @param testPath The path that is to be tested
|
||||
*/
|
||||
export function isLogicalChildPath(parentPath: string, testPath: string): boolean {
|
||||
parentPath = path.resolve(parentPath);
|
||||
testPath = path.resolve(testPath);
|
||||
|
||||
if (parentPath === testPath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
while (testPath.length >= parentPath.length) {
|
||||
if (testPath === parentPath) {
|
||||
return true;
|
||||
}
|
||||
|
||||
testPath = path.dirname(testPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
55
src/common/utils/sort-compare.ts
Normal file
55
src/common/utils/sort-compare.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import semver, { SemVer } from "semver";
|
||||
|
||||
export function sortCompare<T>(left: T, right: T): -1 | 0 | 1 {
|
||||
if (left < right) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (left === right) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
interface ChartVersion {
|
||||
version: string;
|
||||
__version?: SemVer;
|
||||
}
|
||||
|
||||
export function sortCompareChartVersions(left: ChartVersion, right: ChartVersion): -1 | 0 | 1 {
|
||||
if (left.__version && right.__version) {
|
||||
return semver.compare(right.__version, left.__version);
|
||||
}
|
||||
|
||||
if (!left.__version && right.__version) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (left.__version && !right.__version) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return sortCompare(left.version, right.version);
|
||||
}
|
||||
@ -69,4 +69,6 @@ export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5
|
||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||
|
||||
export const appSemVer = new SemVer(packageInfo.version);
|
||||
export const docsUrl = `https://docs.k8slens.dev/main/` as string;
|
||||
export const docsUrl = "https://docs.k8slens.dev/main/" as string;
|
||||
|
||||
export const sentryDsn = packageInfo.config?.sentryDsn ?? "";
|
||||
|
||||
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
138
src/extensions/__tests__/extension-compatibility.test.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { isCompatibleExtension } from "../extension-compatibility";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import type { LensExtensionManifest } from "../lens-extension";
|
||||
import { appSemVer } from "../../common/vars";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
describe("extension compatibility", () => {
|
||||
describe("appSemVer with no prerelease tag", () => {
|
||||
beforeAll(() => {
|
||||
appSemVer.major = 5;
|
||||
appSemVer.minor = 0;
|
||||
appSemVer.patch = 3;
|
||||
appSemVer.prerelease = [];
|
||||
});
|
||||
|
||||
it("has no extension comparator", () => {
|
||||
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||
|
||||
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
comparator: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "bad comparator",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^4.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^5.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
comparator: "^6.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^4.0.0-alpha.1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^5.0.0-alpha.1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
comparator: "^6.0.0-alpha.1",
|
||||
expected: false,
|
||||
},
|
||||
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||
|
||||
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe("appSemVer with prerelease tag", () => {
|
||||
beforeAll(() => {
|
||||
appSemVer.major = 5;
|
||||
appSemVer.minor = 0;
|
||||
appSemVer.patch = 3;
|
||||
appSemVer.prerelease = ["beta", 3];
|
||||
});
|
||||
|
||||
it("has no extension comparator", () => {
|
||||
const manifest = { name: "extensionName", version: "0.0.1"};
|
||||
|
||||
expect(isCompatibleExtension(manifest,)).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
comparator: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "bad comparator",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^4.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^5.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
comparator: "^6.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^4.0.0-alpha.1",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
comparator: "^5.0.0-alpha.1",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
comparator: "^6.0.0-alpha.1",
|
||||
expected: false,
|
||||
},
|
||||
])("extension comparator test: %p", ({ comparator, expected }) => {
|
||||
const manifest: LensExtensionManifest = { name: "extensionName", version: "0.0.1", engines: { lens: comparator}};
|
||||
|
||||
expect(isCompatibleExtension(manifest,)).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -84,7 +84,7 @@ jest.mock(
|
||||
(channel: string, listener: (event: any, ...args: any[]) => void) => {
|
||||
if (channel === "extensions:main") {
|
||||
// First initialize with extensions 1 and 2
|
||||
// and then broadcast event to remove extensioin 2 and add extension number 3
|
||||
// and then broadcast event to remove extension 2 and add extension number 3
|
||||
setTimeout(() => {
|
||||
listener({}, [
|
||||
[
|
||||
|
||||
37
src/extensions/extension-compatibility.ts
Normal file
37
src/extensions/extension-compatibility.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import semver from "semver";
|
||||
import { appSemVer, isProduction } from "../common/vars";
|
||||
import type { LensExtensionManifest } from "./lens-extension";
|
||||
|
||||
export function isCompatibleExtension(manifest: LensExtensionManifest): boolean {
|
||||
if (manifest.engines?.lens) {
|
||||
/* include Lens's prerelease tag in the matching so the extension's compatibility is not limited by it */
|
||||
return semver.satisfies(appSemVer, manifest.engines.lens, { includePrerelease: true });
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isCompatibleBundledExtension(manifest: LensExtensionManifest): boolean {
|
||||
return !isProduction || manifest.version === appSemVer.raw;
|
||||
}
|
||||
@ -34,9 +34,8 @@ import { extensionInstaller } from "./extension-installer";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import { ExtensionLoader } from "./extension-loader";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||
import semver from "semver";
|
||||
import { appSemVer } from "../common/vars";
|
||||
import { isProduction } from "../common/vars";
|
||||
import { isCompatibleBundledExtension, isCompatibleExtension } from "./extension-compatibility";
|
||||
|
||||
export interface InstalledExtension {
|
||||
id: LensExtensionId;
|
||||
@ -362,17 +361,7 @@ export class ExtensionDiscovery extends Singleton {
|
||||
const extensionDir = path.dirname(manifestPath);
|
||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||
let isCompatible = isBundled;
|
||||
|
||||
if (manifest.engines?.lens) {
|
||||
const appSemVerLatestImplied = appSemVer;
|
||||
|
||||
if (appSemVerLatestImplied.prerelease?.[0] === "latest") {
|
||||
/* remove the "latest" prerelease tag so as not to require the extension to specify it */
|
||||
appSemVerLatestImplied.prerelease = [];
|
||||
}
|
||||
isCompatible = semver.satisfies(appSemVerLatestImplied, manifest.engines.lens);
|
||||
}
|
||||
const isCompatible = (isBundled && isCompatibleBundledExtension(manifest)) || isCompatibleExtension(manifest);
|
||||
|
||||
return {
|
||||
id,
|
||||
|
||||
@ -36,7 +36,7 @@ export abstract class IpcMain extends IpcRegistrar {
|
||||
* Listen for broadcasts within your extension
|
||||
* @param channel The channel to listen for broadcasts on
|
||||
* @param listener The function that will be called with the arguments of the broadcast
|
||||
* @returns An optional disopser, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
||||
* @returns An optional disposer, Lens will cleanup when the extension is disabled or uninstalled even if this is not called
|
||||
*/
|
||||
listen(channel: string, listener: (event: Electron.IpcRendererEvent, ...args: any[]) => any): Disposer {
|
||||
const prefixedChannel = `extensions@${this[IpcPrefix]}:${channel}`;
|
||||
|
||||
@ -74,7 +74,7 @@ export class LensExtension {
|
||||
* getExtensionFileFolder returns the path to an already created folder. This
|
||||
* folder is for the sole use of this extension.
|
||||
*
|
||||
* Note: there is no security done on this folder, only obfiscation of the
|
||||
* Note: there is no security done on this folder, only obfuscation of the
|
||||
* folder name.
|
||||
*/
|
||||
async getExtensionFileFolder(): Promise<string> {
|
||||
|
||||
@ -23,6 +23,7 @@ import type * as registries from "./registries";
|
||||
import type { Cluster } from "../main/cluster";
|
||||
import { LensExtension } from "./lens-extension";
|
||||
import { getExtensionPageUrl } from "./registries/page-registry";
|
||||
import type { CatalogEntity } from "../common/catalog";
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
globalPages: registries.PageRegistration[] = [];
|
||||
@ -37,7 +38,7 @@ export class LensRendererExtension extends LensExtension {
|
||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||
commands: registries.CommandRegistration[] = [];
|
||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration[] = [];
|
||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||
topBarItems: registries.TopBarRegistration[] = [];
|
||||
|
||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||
|
||||
@ -22,14 +22,24 @@
|
||||
import { ClusterPageRegistry, getExtensionPageUrl, GlobalPageRegistry, PageParams } from "../page-registry";
|
||||
import { LensExtension } from "../../lens-extension";
|
||||
import React from "react";
|
||||
import fse from "fs-extra";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import { ThemeStore } from "../../../renderer/theme.store";
|
||||
import { TerminalStore } from "../../renderer-api/components";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
app: {
|
||||
getPath: () => "tmp",
|
||||
},
|
||||
}));
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
describe("getPageUrl", () => {
|
||||
describe("page registry tests", () => {
|
||||
beforeEach(async () => {
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
@ -43,6 +53,9 @@ describe("getPageUrl", () => {
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
UserStore.createInstance();
|
||||
ThemeStore.createInstance();
|
||||
TerminalStore.createInstance();
|
||||
ClusterPageRegistry.createInstance();
|
||||
GlobalPageRegistry.createInstance().add({
|
||||
id: "page-with-params",
|
||||
@ -54,70 +67,6 @@ describe("getPageUrl", () => {
|
||||
test2: "" // no default value, just declaration
|
||||
},
|
||||
}, ext);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
GlobalPageRegistry.resetInstance();
|
||||
ClusterPageRegistry.resetInstance();
|
||||
});
|
||||
|
||||
it("returns a page url for extension", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
||||
});
|
||||
|
||||
it("allows to pass base url as parameter", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("removes @ and replace `/` to `--`", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
||||
});
|
||||
|
||||
it("adds / prefix", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("normalize possible multi-slashes in page.id", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("gets page url with custom params", () => {
|
||||
const params: PageParams = { test1: "one", test2: "2" };
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: ext.name,
|
||||
pageId: "page-with-params",
|
||||
params,
|
||||
});
|
||||
|
||||
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
||||
});
|
||||
|
||||
it("gets page url with default custom params", () => {
|
||||
const defaultPageUrl = getExtensionPageUrl({
|
||||
extensionId: ext.name,
|
||||
pageId: "page-with-params",
|
||||
});
|
||||
|
||||
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("globalPageRegistry", () => {
|
||||
beforeEach(async () => {
|
||||
ext = new LensExtension({
|
||||
manifest: {
|
||||
name: "@acme/foo-bar",
|
||||
version: "0.1.1"
|
||||
},
|
||||
id: "/this/is/fake/package.json",
|
||||
absolutePath: "/absolute/fake/",
|
||||
manifestPath: "/this/is/fake/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
ClusterPageRegistry.createInstance();
|
||||
GlobalPageRegistry.createInstance().add([
|
||||
{
|
||||
id: "test-page",
|
||||
@ -142,33 +91,82 @@ describe("globalPageRegistry", () => {
|
||||
afterEach(() => {
|
||||
GlobalPageRegistry.resetInstance();
|
||||
ClusterPageRegistry.resetInstance();
|
||||
TerminalStore.resetInstance();
|
||||
ThemeStore.resetInstance();
|
||||
UserStore.resetInstance();
|
||||
fse.remove("tmp");
|
||||
});
|
||||
|
||||
describe("getByPageTarget", () => {
|
||||
it("matching to first registered page without id", () => {
|
||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
||||
|
||||
expect(page.id).toEqual(undefined);
|
||||
expect(page.extensionId).toEqual(ext.name);
|
||||
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||
describe("getPageUrl", () => {
|
||||
it("returns a page url for extension", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name })).toBe("/extension/foo-bar");
|
||||
});
|
||||
|
||||
it("returns matching page", () => {
|
||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||
pageId: "test-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
|
||||
expect(page.id).toEqual("test-page");
|
||||
it("allows to pass base url as parameter", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "/test" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("returns null if target not found", () => {
|
||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||
pageId: "wrong-page",
|
||||
extensionId: ext.name
|
||||
it("removes @ and replace `/` to `--`", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: "@foo/bar" })).toBe("/extension/foo--bar");
|
||||
});
|
||||
|
||||
it("adds / prefix", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "test" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("normalize possible multi-slashes in page.id", () => {
|
||||
expect(getExtensionPageUrl({ extensionId: ext.name, pageId: "//test/" })).toBe("/extension/foo-bar/test");
|
||||
});
|
||||
|
||||
it("gets page url with custom params", () => {
|
||||
const params: PageParams = { test1: "one", test2: "2" };
|
||||
const searchParams = new URLSearchParams(params);
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: ext.name,
|
||||
pageId: "page-with-params",
|
||||
params,
|
||||
});
|
||||
|
||||
expect(page).toBeNull();
|
||||
expect(pageUrl).toBe(`/extension/foo-bar/page-with-params?${searchParams}`);
|
||||
});
|
||||
|
||||
it("gets page url with default custom params", () => {
|
||||
const defaultPageUrl = getExtensionPageUrl({
|
||||
extensionId: ext.name,
|
||||
pageId: "page-with-params",
|
||||
});
|
||||
|
||||
expect(defaultPageUrl).toBe(`/extension/foo-bar/page-with-params?test1=test1-default`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("globalPageRegistry", () => {
|
||||
describe("getByPageTarget", () => {
|
||||
it("matching to first registered page without id", () => {
|
||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({ extensionId: ext.name });
|
||||
|
||||
expect(page.id).toEqual(undefined);
|
||||
expect(page.extensionId).toEqual(ext.name);
|
||||
expect(page.url).toEqual(getExtensionPageUrl({ extensionId: ext.name }));
|
||||
});
|
||||
|
||||
it("returns matching page", () => {
|
||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||
pageId: "test-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
|
||||
expect(page.id).toEqual("test-page");
|
||||
});
|
||||
|
||||
it("returns null if target not found", () => {
|
||||
const page = GlobalPageRegistry.getInstance().getByPageTarget({
|
||||
pageId: "wrong-page",
|
||||
extensionId: ext.name
|
||||
});
|
||||
|
||||
expect(page).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -20,20 +20,25 @@
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import type { CatalogEntity } from "../common-api/catalog";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface CatalogEntityDetailComponents {
|
||||
Details: React.ComponentType<any>;
|
||||
export interface CatalogEntityDetailsProps<T extends CatalogEntity> {
|
||||
entity: T;
|
||||
}
|
||||
|
||||
export interface CatalogEntityDetailRegistration {
|
||||
export interface CatalogEntityDetailComponents<T extends CatalogEntity> {
|
||||
Details: React.ComponentType<CatalogEntityDetailsProps<T>>;
|
||||
}
|
||||
|
||||
export interface CatalogEntityDetailRegistration<T extends CatalogEntity> {
|
||||
kind: string;
|
||||
apiVersions: string[];
|
||||
components: CatalogEntityDetailComponents;
|
||||
components: CatalogEntityDetailComponents<T>;
|
||||
priority?: number;
|
||||
}
|
||||
|
||||
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration> {
|
||||
export class CatalogEntityDetailRegistry extends BaseRegistry<CatalogEntityDetailRegistration<CatalogEntity>> {
|
||||
getItemsForKind(kind: string, apiVersion: string) {
|
||||
const items = this.getItems().filter((item) => {
|
||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||
|
||||
@ -67,5 +67,5 @@ export * from "../../renderer/components/+events/kube-event-details";
|
||||
|
||||
// specific exports
|
||||
export * from "../../renderer/components/status-brick";
|
||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
||||
export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store";
|
||||
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
|
||||
|
||||
@ -207,7 +207,7 @@ describe("ContextHandler", () => {
|
||||
expect(service.id === "id_0");
|
||||
});
|
||||
|
||||
it("shouldn't pick the second provider of 2 succcess(es) after 1 failure(s)", async () => {
|
||||
it("shouldn't pick the second provider of 2 success(es) after 1 failure(s)", async () => {
|
||||
const reg = PrometheusProviderRegistry.getInstance();
|
||||
let count = 0;
|
||||
|
||||
|
||||
@ -264,6 +264,12 @@ async function watchFileChanges(filePath: string): Promise<[IComputedValue<Catal
|
||||
followSymlinks: true,
|
||||
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
|
||||
disableGlobbing: true,
|
||||
ignorePermissionErrors: true,
|
||||
usePolling: false,
|
||||
awaitWriteFinish: {
|
||||
pollInterval: 100,
|
||||
stabilityThreshold: 1000,
|
||||
},
|
||||
});
|
||||
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>();
|
||||
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx";
|
||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity } from "../../common/catalog";
|
||||
import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityKindData } from "../../common/catalog";
|
||||
import { iter } from "../../common/utils";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
@ -62,6 +62,10 @@ export class CatalogEntityRegistry {
|
||||
|
||||
return items as T[];
|
||||
}
|
||||
|
||||
getItemsByEntityClass<T extends CatalogEntity>({ apiVersion, kind }: CatalogEntityKindData): T[] {
|
||||
return this.getItemsForApiKind(apiVersion, kind);
|
||||
}
|
||||
}
|
||||
|
||||
export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
|
||||
|
||||
@ -23,7 +23,7 @@ import "../common/cluster-ipc";
|
||||
import type http from "http";
|
||||
import { action, autorun, makeObservable, observable, observe, reaction, toJS } from "mobx";
|
||||
import { ClusterId, ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||
import type { Cluster } from "./cluster";
|
||||
import { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { Singleton } from "../common/utils";
|
||||
@ -32,6 +32,8 @@ import { KubernetesCluster, KubernetesClusterPrometheusMetrics, KubernetesCluste
|
||||
import { ipcMainOn } from "../common/ipc";
|
||||
import { once } from "lodash";
|
||||
|
||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||
|
||||
export class ClusterManager extends Singleton {
|
||||
private store = ClusterStore.getInstance();
|
||||
deleting = observable.set<ClusterId>();
|
||||
@ -73,7 +75,7 @@ export class ClusterManager extends Singleton {
|
||||
if (removedClusters.length > 0) {
|
||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||
|
||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||
logger.info(`${logPrefix} removing clusters`, meta);
|
||||
removedClusters.forEach(cluster => cluster.disconnect());
|
||||
this.store.removedClusters.clear();
|
||||
}
|
||||
@ -103,13 +105,13 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
this.updateEntityStatus(entity, cluster);
|
||||
|
||||
entity.metadata.labels = Object.assign({}, cluster.labels, entity.metadata.labels);
|
||||
entity.metadata.labels.distro = cluster.distribution;
|
||||
|
||||
if (cluster.preferences?.clusterName) {
|
||||
entity.metadata.name = cluster.preferences.clusterName;
|
||||
}
|
||||
|
||||
entity.metadata.labels = {
|
||||
...entity.metadata.labels,
|
||||
...cluster.labels,
|
||||
};
|
||||
entity.metadata.distro = cluster.distribution;
|
||||
entity.metadata.kubeVersion = cluster.version;
|
||||
entity.metadata.name = cluster.name;
|
||||
entity.spec.metrics ||= { source: "local" };
|
||||
|
||||
if (entity.spec.metrics.source === "local") {
|
||||
@ -161,14 +163,22 @@ export class ClusterManager extends Singleton {
|
||||
const cluster = this.store.getById(entity.metadata.uid);
|
||||
|
||||
if (!cluster) {
|
||||
this.store.addCluster({
|
||||
id: entity.metadata.uid,
|
||||
preferences: {
|
||||
clusterName: entity.metadata.name
|
||||
},
|
||||
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||
contextName: entity.spec.kubeconfigContext
|
||||
});
|
||||
try {
|
||||
this.store.addCluster({
|
||||
id: entity.metadata.uid,
|
||||
preferences: {
|
||||
clusterName: entity.metadata.name
|
||||
},
|
||||
kubeConfigPath: entity.spec.kubeconfigPath,
|
||||
contextName: entity.spec.kubeconfigContext
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
|
||||
logger.warn(`${logPrefix} kubeconfig file disappeared`, { path: entity.spec.kubeconfigPath });
|
||||
} else {
|
||||
logger.error(`${logPrefix} failed to add cluster: ${error}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cluster.kubeConfigPath = entity.spec.kubeconfigPath;
|
||||
cluster.contextName = entity.spec.kubeconfigContext;
|
||||
@ -179,7 +189,7 @@ export class ClusterManager extends Singleton {
|
||||
}
|
||||
|
||||
protected onNetworkOffline = () => {
|
||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
||||
logger.info(`${logPrefix} network is offline`);
|
||||
this.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
@ -190,7 +200,7 @@ export class ClusterManager extends Singleton {
|
||||
};
|
||||
|
||||
protected onNetworkOnline = () => {
|
||||
logger.info("[CLUSTER-MANAGER]: network is online");
|
||||
logger.info(`${logPrefix} network is online`);
|
||||
this.store.clustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
@ -236,8 +246,10 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
||||
name: cluster.name,
|
||||
source: "local",
|
||||
labels: {
|
||||
distro: cluster.distribution,
|
||||
}
|
||||
...cluster.labels,
|
||||
},
|
||||
distro: cluster.distribution,
|
||||
kubeVersion: cluster.version,
|
||||
},
|
||||
spec: {
|
||||
kubeconfigPath: cluster.kubeConfigPath,
|
||||
|
||||
@ -34,6 +34,7 @@ import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
import { toJS } from "../common/utils";
|
||||
import { initialNodeShellImage } from "../common/cluster-store";
|
||||
|
||||
export enum ClusterStatus {
|
||||
AccessGranted = 2,
|
||||
@ -234,8 +235,18 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return this.preferences.clusterName || this.contextName;
|
||||
}
|
||||
|
||||
/**
|
||||
* The detected kubernetes distribution
|
||||
*/
|
||||
@computed get distribution(): string {
|
||||
return this.metadata.distribution?.toString() || "unknown";
|
||||
return this.metadata[ClusterMetadataKey.DISTRIBUTION]?.toString() || "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* The detected kubernetes version
|
||||
*/
|
||||
@computed get version(): string {
|
||||
return this.metadata[ClusterMetadataKey.VERSION]?.toString() || "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
@ -250,13 +261,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return toJS({ prometheus, prometheusProvider });
|
||||
}
|
||||
|
||||
/**
|
||||
* Kubernetes version
|
||||
*/
|
||||
get version(): string {
|
||||
return String(this.metadata?.version ?? "");
|
||||
}
|
||||
|
||||
constructor(model: ClusterModel) {
|
||||
makeObservable(this);
|
||||
this.id = model.id;
|
||||
@ -745,4 +749,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
isMetricHidden(resource: ClusterMetricsResourceType): boolean {
|
||||
return Boolean(this.preferences.hiddenMetrics?.includes(resource));
|
||||
}
|
||||
|
||||
get nodeShellImage(): string {
|
||||
return this.preferences.nodeShellImage || initialNodeShellImage;
|
||||
}
|
||||
|
||||
get imagePullSecret(): string {
|
||||
return this.preferences.imagePullSecret || "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,36 @@ export class HelmChartManager {
|
||||
switch (this.repo.name) {
|
||||
case "stable":
|
||||
return Promise.resolve({
|
||||
"invalid-semver": [
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "I am not semver",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "v4.3.0",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "I am not semver but more",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "v4.4.0",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
],
|
||||
"apm-server": [
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
|
||||
@ -62,6 +62,36 @@ describe("Helm Service tests", () => {
|
||||
digest: "test"
|
||||
}
|
||||
],
|
||||
"invalid-semver": [
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "v4.4.0",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "v4.3.0",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "I am not semver",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
name: "weird-versioning",
|
||||
version: "I am not semver but more",
|
||||
repo: "stable",
|
||||
digest: "test"
|
||||
},
|
||||
],
|
||||
"redis": [
|
||||
{
|
||||
apiVersion: "3.0.0",
|
||||
|
||||
@ -146,7 +146,7 @@ export class HelmRepoManager extends Singleton {
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public static async addСustomRepo(repoAttributes : HelmRepo) {
|
||||
public static async addCustomRepo(repoAttributes : HelmRepo) {
|
||||
logger.info(`[HELM]: adding repo "${repoAttributes.name}" from ${repoAttributes.url}`);
|
||||
const helm = await helmCli.binaryPath();
|
||||
|
||||
|
||||
@ -19,13 +19,14 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import semver from "semver";
|
||||
import semver, { SemVer } from "semver";
|
||||
import type { Cluster } from "../cluster";
|
||||
import logger from "../logger";
|
||||
import { HelmRepoManager } from "./helm-repo-manager";
|
||||
import { HelmChartManager } from "./helm-chart-manager";
|
||||
import type { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||
import type { HelmChart, HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||
import { iter, sortCompareChartVersions } from "../../common/utils";
|
||||
|
||||
interface GetReleaseValuesArgs {
|
||||
cluster: Cluster;
|
||||
@ -132,28 +133,55 @@ class HelmService {
|
||||
}
|
||||
|
||||
private excludeDeprecatedChartGroups(chartGroups: RepoHelmChartList) {
|
||||
const groups = new Map(Object.entries(chartGroups));
|
||||
return Object.fromEntries(
|
||||
iter.filterMap(
|
||||
Object.entries(chartGroups),
|
||||
([name, charts]) => {
|
||||
for (const chart of charts) {
|
||||
if (chart.deprecated) {
|
||||
// ignore chart group if any chart is deprecated
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [chartName, charts] of groups) {
|
||||
if (charts[0].deprecated) {
|
||||
groups.delete(chartName);
|
||||
}
|
||||
return [name, charts];
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private sortCharts(charts: HelmChart[]) {
|
||||
interface ExtendedHelmChart extends HelmChart {
|
||||
__version: SemVer;
|
||||
}
|
||||
|
||||
return Object.fromEntries(groups);
|
||||
const chartsWithVersion = Array.from(
|
||||
iter.map(
|
||||
charts,
|
||||
(chart => {
|
||||
const __version = semver.coerce(chart.version, { includePrerelease: true, loose: true });
|
||||
|
||||
if (!__version) {
|
||||
logger.error(`[HELM-SERVICE]: Version from helm chart is not loosely coercable to semver.`, { name: chart.name, version: chart.version, repo: chart.repo });
|
||||
}
|
||||
|
||||
(chart as ExtendedHelmChart).__version = __version;
|
||||
|
||||
return chart as ExtendedHelmChart;
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
return chartsWithVersion
|
||||
.sort(sortCompareChartVersions)
|
||||
.map(chart => (delete chart.__version, chart as HelmChart));
|
||||
}
|
||||
|
||||
private sortChartsByVersion(chartGroups: RepoHelmChartList) {
|
||||
for (const key in chartGroups) {
|
||||
chartGroups[key] = chartGroups[key].sort((first, second) => {
|
||||
const firstVersion = semver.coerce(first.version || 0);
|
||||
const secondVersion = semver.coerce(second.version || 0);
|
||||
|
||||
return semver.compare(secondVersion, firstVersion);
|
||||
});
|
||||
}
|
||||
|
||||
return chartGroups;
|
||||
return Object.fromEntries(
|
||||
Object.entries(chartGroups)
|
||||
.map(([name, charts]) => [name, this.sortCharts(charts)])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,6 +59,11 @@ import { UserStore } from "../common/user-store";
|
||||
import { WeblinkStore } from "../common/weblink-store";
|
||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||
import { SentryInit } from "../common/sentry";
|
||||
|
||||
// This has to be called before start using winton-based logger
|
||||
// For example, before any logger.log
|
||||
SentryInit();
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
const cleanup = disposer();
|
||||
@ -132,15 +137,20 @@ app.on("ready", async () => {
|
||||
/**
|
||||
* The following sync MUST be done before HotbarStore creation, because that
|
||||
* store has migrations that will remove items that previous migrations add
|
||||
* if this is not presant
|
||||
* if this is not present
|
||||
*/
|
||||
syncGeneralEntities();
|
||||
|
||||
logger.info("💾 Loading stores");
|
||||
|
||||
UserStore.createInstance().startMainReactions();
|
||||
|
||||
// ClusterStore depends on: UserStore
|
||||
ClusterStore.createInstance().provideInitialFromMain();
|
||||
|
||||
// HotbarStore depends on: ClusterStore
|
||||
HotbarStore.createInstance();
|
||||
|
||||
ExtensionsStore.createInstance();
|
||||
FilesystemProvisionerStore.createInstance();
|
||||
WeblinkStore.createInstance();
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
*/
|
||||
|
||||
import type { IpcMainInvokeEvent } from "electron";
|
||||
import type { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { KubernetesCluster } from "../../common/catalog-entities";
|
||||
import { clusterFrameMap } from "../../common/cluster-frames";
|
||||
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler } from "../../common/cluster-ipc";
|
||||
import { ClusterId, ClusterStore } from "../../common/cluster-store";
|
||||
@ -50,9 +50,9 @@ export function initIpcMainHandlers() {
|
||||
});
|
||||
|
||||
ipcMainHandle(clusterVisibilityHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId, visible: boolean) => {
|
||||
const entity = catalogEntityRegistry.getById<KubernetesCluster>(clusterId);
|
||||
const entity = catalogEntityRegistry.getById(clusterId);
|
||||
|
||||
for (const kubeEntity of catalogEntityRegistry.getItemsForApiKind(entity.apiVersion, entity.kind)) {
|
||||
for (const kubeEntity of catalogEntityRegistry.getItemsByEntityClass(KubernetesCluster)) {
|
||||
kubeEntity.status.active = false;
|
||||
}
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ export class PrometheusHelm extends PrometheusLens {
|
||||
readonly id: string = "helm";
|
||||
readonly name: string = "Helm";
|
||||
readonly rateAccuracy: string = "5m";
|
||||
readonly isConfigurable: boolean = false;
|
||||
readonly isConfigurable: boolean = true;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||
return this.getFirstNamespacedServer(client, "app=prometheus,component=server,heritage=Helm");
|
||||
|
||||
@ -28,7 +28,7 @@ export class PrometheusLens extends PrometheusProvider {
|
||||
readonly id: string = "lens";
|
||||
readonly name: string = "Lens";
|
||||
readonly rateAccuracy: string = "1m";
|
||||
readonly isConfigurable: boolean = true;
|
||||
readonly isConfigurable: boolean = false;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||
try {
|
||||
|
||||
@ -27,7 +27,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
||||
readonly rateAccuracy: string = "1m";
|
||||
readonly id: string = "operator";
|
||||
readonly name: string = "Prometheus Operator";
|
||||
readonly isConfigurable: boolean = false;
|
||||
readonly isConfigurable: boolean = true;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||
return this.getFirstNamespacedServer(client, "operated-prometheus=true", "self-monitor=true");
|
||||
|
||||
@ -28,7 +28,7 @@ export class PrometheusStacklight extends PrometheusProvider {
|
||||
readonly id: string = "stacklight";
|
||||
readonly name: string = "Stacklight";
|
||||
readonly rateAccuracy: string = "1m";
|
||||
readonly isConfigurable: boolean = false;
|
||||
readonly isConfigurable: boolean = true;
|
||||
|
||||
public async getPrometheusService(client: CoreV1Api): Promise<PrometheusService | undefined> {
|
||||
try {
|
||||
|
||||
@ -61,7 +61,8 @@ export class LensProxy extends Singleton {
|
||||
if (req.url.startsWith(`${apiPrefix}?`)) {
|
||||
handleWsUpgrade(req, socket, head);
|
||||
} else {
|
||||
this.handleProxyUpgrade(proxy, req, socket, head);
|
||||
this.handleProxyUpgrade(proxy, req, socket, head)
|
||||
.catch(error => logger.error(`[LENS-PROXY]: failed to handle proxy upgrade: ${error}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -168,7 +169,10 @@ export class LensProxy extends Singleton {
|
||||
if (!res.headersSent && req.url) {
|
||||
const url = new URL(req.url, "http://localhost");
|
||||
|
||||
if (url.searchParams.has("watch")) res.flushHeaders();
|
||||
if (url.searchParams.has("watch")) {
|
||||
res.statusCode = proxyRes.statusCode;
|
||||
res.flushHeaders();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -177,6 +181,8 @@ export class LensProxy extends Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.error(`[LENS-PROXY]: http proxy errored for cluster: ${error}`, { url: req.url });
|
||||
|
||||
if (target) {
|
||||
logger.debug(`Failed proxy to target: ${JSON.stringify(target, null, 2)}`);
|
||||
|
||||
@ -189,14 +195,15 @@ export class LensProxy extends Singleton {
|
||||
logger.debug(`Retrying proxy request to url: ${reqId}`);
|
||||
setTimeout(() => {
|
||||
this.retryCounters.set(reqId, retryCount + 1);
|
||||
this.handleRequest(proxy, req, res);
|
||||
this.handleRequest(proxy, req, res)
|
||||
.catch(error => logger.error(`[LENS-PROXY]: failed to handle request on proxy error: ${error}`));
|
||||
}, timeoutMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
res.writeHead(500).end("Oops, something went wrong.");
|
||||
res.writeHead(500).end(`Oops, something went wrong.\n${error}`);
|
||||
} catch (e) {
|
||||
logger.error(`[LENS-PROXY]: Failed to write headers: `, e);
|
||||
}
|
||||
|
||||
@ -134,11 +134,22 @@ export class PortForwardRoute {
|
||||
name: resourceName,
|
||||
port,
|
||||
});
|
||||
const started = await portForward.start();
|
||||
|
||||
if (!started) {
|
||||
try {
|
||||
const started = await portForward.start();
|
||||
|
||||
if (!started) {
|
||||
logger.warn("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName });
|
||||
|
||||
return respondJson(response, {
|
||||
message: "Failed to open port-forward"
|
||||
}, 400);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName });
|
||||
|
||||
return respondJson(response, {
|
||||
message: "Failed to open port-forward"
|
||||
message: error?.toString() || "Failed to open port-forward",
|
||||
}, 400);
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,8 +31,11 @@ export class LocalShellSession extends ShellSession {
|
||||
return [helmCli.getBinaryDir()];
|
||||
}
|
||||
|
||||
public async open() {
|
||||
protected get cwd(): string | undefined {
|
||||
return this.cluster.preferences?.terminalCWD;
|
||||
}
|
||||
|
||||
public async open() {
|
||||
const env = await this.getCachedShellEnv();
|
||||
const shell = env.PTYSHELL;
|
||||
const args = await this.getShellArgs(shell);
|
||||
@ -51,7 +54,7 @@ export class LocalShellSession extends ShellSession {
|
||||
case "bash":
|
||||
return ["--init-file", path.join(await this.kubectlBinDirP, ".bash_set_path")];
|
||||
case "fish":
|
||||
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${this.kubeconfigPathP}"`];
|
||||
return ["--login", "--init-command", `export PATH="${helmpath}:${kubectlPathDir}:$PATH"; export KUBECONFIG="${await this.kubeconfigPathP}"`];
|
||||
case "zsh":
|
||||
return ["--login"];
|
||||
default:
|
||||
|
||||
@ -32,6 +32,10 @@ export class NodeShellSession extends ShellSession {
|
||||
protected podId = `node-shell-${uuid()}`;
|
||||
protected kc: KubeConfig;
|
||||
|
||||
protected get cwd(): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) {
|
||||
super(socket, cluster);
|
||||
}
|
||||
@ -77,13 +81,16 @@ export class NodeShellSession extends ShellSession {
|
||||
}],
|
||||
containers: [{
|
||||
name: "shell",
|
||||
image: "docker.io/alpine:3.13",
|
||||
image: this.cluster.nodeShellImage,
|
||||
securityContext: {
|
||||
privileged: true,
|
||||
},
|
||||
command: ["nsenter"],
|
||||
args: ["-t", "1", "-m", "-u", "-i", "-n", "sleep", "14000"]
|
||||
}],
|
||||
imagePullSecrets: [{
|
||||
name: this.cluster.imagePullSecret,
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import fse from "fs-extra";
|
||||
import type { Cluster } from "../cluster";
|
||||
import { Kubectl } from "../kubectl";
|
||||
import type * as WebSocket from "ws";
|
||||
@ -50,9 +51,7 @@ export abstract class ShellSession {
|
||||
protected kubectlBinDirP: Promise<string>;
|
||||
protected kubeconfigPathP: Promise<string>;
|
||||
|
||||
protected get cwd(): string | undefined {
|
||||
return this.cluster.preferences?.terminalCWD;
|
||||
}
|
||||
protected abstract get cwd(): string | undefined;
|
||||
|
||||
constructor(protected websocket: WebSocket, protected cluster: Cluster) {
|
||||
this.kubectl = new Kubectl(cluster.version);
|
||||
@ -60,10 +59,14 @@ export abstract class ShellSession {
|
||||
this.kubectlBinDirP = this.kubectl.binDir();
|
||||
}
|
||||
|
||||
open(shell: string, args: string[], env: Record<string, any>): void {
|
||||
protected async open(shell: string, args: string[], env: Record<string, any>) {
|
||||
const cwd = (this.cwd && await fse.pathExists(this.cwd))
|
||||
? this.cwd
|
||||
: env.HOME;
|
||||
|
||||
this.shellProcess = pty.spawn(shell, args, {
|
||||
cols: 80,
|
||||
cwd: this.cwd || env.HOME,
|
||||
cwd,
|
||||
env,
|
||||
name: "xterm-256color",
|
||||
rows: 30,
|
||||
|
||||
@ -93,7 +93,7 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
click() {
|
||||
windowManager
|
||||
.navigate(preferencesURL())
|
||||
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to nativate to Preferences`, { error }));
|
||||
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to navigate to Preferences`, { error }));
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
import type { Readable } from "stream";
|
||||
import URLParse from "url-parse";
|
||||
import logger from "../logger";
|
||||
|
||||
interface GetPortArgs {
|
||||
/**
|
||||
@ -47,11 +48,15 @@ interface GetPortArgs {
|
||||
* @returns A Promise for port number
|
||||
*/
|
||||
export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number> {
|
||||
const logLines: string[] = [];
|
||||
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const handler = (data: any) => {
|
||||
const logItem: string = data.toString();
|
||||
const match = logItem.match(args.lineRegex);
|
||||
|
||||
logLines.push(logItem);
|
||||
|
||||
if (match) {
|
||||
// use unknown protocol so that there is no default port
|
||||
const addr = new URLParse(`s://${match.groups.address.trim()}`);
|
||||
@ -64,6 +69,7 @@ export function getPortFrom(stream: Readable, args: GetPortArgs): Promise<number
|
||||
};
|
||||
const timeoutID = setTimeout(() => {
|
||||
stream.off("data", handler);
|
||||
logger.warn(`[getPortFrom]: failed to retrieve port via ${args.lineRegex.toString()}: ${logLines}`);
|
||||
reject(new Error("failed to retrieve port from stream"));
|
||||
}, args.timeout ?? 5000);
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ export default {
|
||||
workspaces.set(id, name);
|
||||
}
|
||||
|
||||
const clusters: ClusterModel[] = store.get("clusters");
|
||||
const clusters: ClusterModel[] = store.get("clusters") ?? [];
|
||||
|
||||
for (const cluster of clusters) {
|
||||
if (cluster.workspace && workspaces.has(cluster.workspace)) {
|
||||
|
||||
@ -108,7 +108,7 @@ export default {
|
||||
run(store) {
|
||||
const folder = path.resolve(app.getPath("userData"), "lens-local-storage");
|
||||
|
||||
const oldClusters: ClusterModel[] = store.get("clusters");
|
||||
const oldClusters: ClusterModel[] = store.get("clusters") ?? [];
|
||||
const clusters = new Map<string, ClusterModel>();
|
||||
|
||||
for (const { id: oldId, ...cluster } of oldClusters) {
|
||||
|
||||
@ -148,8 +148,8 @@ export default {
|
||||
|
||||
store.set("hotbars", hotbars);
|
||||
} catch (error) {
|
||||
if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) {
|
||||
// ignore lens-workspace-store.json being missing
|
||||
// ignore files being missing
|
||||
if (error.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
80
src/migrations/user-store/5.0.3-beta.1.ts
Normal file
80
src/migrations/user-store/5.0.3-beta.1.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2021 OpenLens Authors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { app } from "electron";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { ClusterStore, ClusterStoreModel } from "../../common/cluster-store";
|
||||
import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store";
|
||||
import { MigrationDeclaration, migrationLog } from "../helpers";
|
||||
import { isLogicalChildPath } from "../../common/utils";
|
||||
|
||||
export default {
|
||||
version: "5.0.3-beta.1",
|
||||
run(store) {
|
||||
try {
|
||||
const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {};
|
||||
const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(app.getPath("userData"), "lens-cluster-store.json"), "utf-8")) ?? {};
|
||||
const extensionDataDir = path.resolve(app.getPath("userData"), "extension_data");
|
||||
const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath));
|
||||
|
||||
syncPaths.add(path.join(os.homedir(), ".kube"));
|
||||
|
||||
for (const cluster of clusters) {
|
||||
const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath);
|
||||
|
||||
if (dirOfKubeconfig === ClusterStore.storedKubeConfigFolder) {
|
||||
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under ClusterStore.storedKubeConfigFolder`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (syncPaths.has(cluster.kubeConfigPath) || syncPaths.has(dirOfKubeconfig)) {
|
||||
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is already being synced`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isLogicalChildPath(extensionDataDir, cluster.kubeConfigPath)) {
|
||||
migrationLog(`Skipping ${cluster.id} because kubeConfigPath is placed under an extension_data folder`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existsSync(cluster.kubeConfigPath)) {
|
||||
migrationLog(`Skipping ${cluster.id} because kubeConfigPath no longer exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
migrationLog(`Adding ${cluster.kubeConfigPath} from ${cluster.id} to sync paths`);
|
||||
syncPaths.add(cluster.kubeConfigPath);
|
||||
}
|
||||
|
||||
const updatedSyncEntries: KubeconfigSyncEntry[] = [...syncPaths].map(filePath => ({ filePath }));
|
||||
|
||||
migrationLog("Final list of synced paths", updatedSyncEntries);
|
||||
store.set("preferences", { ...preferences, syncKubeconfigEntries: updatedSyncEntries });
|
||||
} catch (error) {
|
||||
if (error.code !== "ENOENT") {
|
||||
// ignore files being missing
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
},
|
||||
} as MigrationDeclaration;
|
||||
@ -25,6 +25,7 @@ import { joinMigrations } from "../helpers";
|
||||
|
||||
import version210Beta4 from "./2.1.0-beta.4";
|
||||
import version500Alpha3 from "./5.0.0-alpha.3";
|
||||
import version503Beta1 from "./5.0.3-beta.1";
|
||||
import { fileNameMigration } from "./file-name-migration";
|
||||
|
||||
export {
|
||||
@ -34,4 +35,5 @@ export {
|
||||
export default joinMigrations(
|
||||
version210Beta4,
|
||||
version500Alpha3,
|
||||
version503Beta1,
|
||||
);
|
||||
|
||||
@ -129,8 +129,18 @@ const tests: KubeApiParseTestData[] = [
|
||||
}],
|
||||
];
|
||||
|
||||
const throwtests = [
|
||||
undefined,
|
||||
"",
|
||||
"ajklsmh"
|
||||
];
|
||||
|
||||
describe("parseApi unit tests", () => {
|
||||
it.each(tests)("testing %s", (url, expected) => {
|
||||
it.each(tests)("testing %j", (url, expected) => {
|
||||
expect(parseKubeApi(url)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it.each(throwtests)("testing %j should throw", (url) => {
|
||||
expect(() => parseKubeApi(url)).toThrowError("invalid apiPath");
|
||||
});
|
||||
});
|
||||
|
||||
@ -19,11 +19,20 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
.AppInit {
|
||||
height: 100%;
|
||||
import { Pod } from "../endpoints";
|
||||
|
||||
.waiting-services {
|
||||
font-size: small;
|
||||
opacity: .75;
|
||||
}
|
||||
}
|
||||
describe("Pod tests", () => {
|
||||
it("getAllContainers() should never throw", () => {
|
||||
const pod = new Pod({
|
||||
apiVersion: "foo",
|
||||
kind: "Pod",
|
||||
metadata: {
|
||||
name: "foobar",
|
||||
resourceVersion: "foobar",
|
||||
uid: "foobar",
|
||||
},
|
||||
});
|
||||
|
||||
expect(pod.getAllContainers()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@ -58,8 +58,14 @@ export class ApiManager {
|
||||
}
|
||||
}
|
||||
|
||||
protected resolveApi(api: string | KubeApi): KubeApi {
|
||||
if (typeof api === "string") return this.getApi(api);
|
||||
protected resolveApi(api?: string | KubeApi): KubeApi | undefined {
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (typeof api === "string") {
|
||||
return this.getApi(api);
|
||||
}
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
@ -156,9 +156,13 @@ export class Node extends KubeObject {
|
||||
}
|
||||
|
||||
getRoleLabels() {
|
||||
const roleLabels = Object.keys(this.metadata.labels).filter(key =>
|
||||
key.includes("node-role.kubernetes.io")
|
||||
).map(key => key.match(/([^/]+$)/)[0]); // all after last slash
|
||||
if (!this.metadata?.labels || typeof this.metadata.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
|
||||
|
||||
if (this.metadata.labels["kubernetes.io/role"] != undefined) {
|
||||
roleLabels.push(this.metadata.labels["kubernetes.io/role"]);
|
||||
|
||||
@ -213,7 +213,7 @@ export class Pod extends WorkloadKubeObject {
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
declare spec: {
|
||||
declare spec?: {
|
||||
volumes?: {
|
||||
name: string;
|
||||
persistentVolumeClaim: {
|
||||
@ -291,11 +291,11 @@ export class Pod extends WorkloadKubeObject {
|
||||
};
|
||||
|
||||
getInitContainers() {
|
||||
return this.spec.initContainers || [];
|
||||
return this.spec?.initContainers || [];
|
||||
}
|
||||
|
||||
getContainers() {
|
||||
return this.spec.containers || [];
|
||||
return this.spec?.containers || [];
|
||||
}
|
||||
|
||||
getAllContainers() {
|
||||
|
||||
@ -24,6 +24,9 @@
|
||||
import type { KubeObject } from "./kube-object";
|
||||
import { splitArray } from "../../common/utils";
|
||||
import { apiManager } from "./api-manager";
|
||||
import { isDebugging } from "../../common/vars";
|
||||
import logger from "../../main/logger";
|
||||
import { inspect } from "util";
|
||||
|
||||
export interface IKubeObjectRef {
|
||||
kind: string;
|
||||
@ -47,8 +50,26 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
|
||||
}
|
||||
|
||||
export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
path = new URL(path, location.origin).pathname;
|
||||
const [, prefix, ...parts] = path.split("/");
|
||||
if (!isDebugging) {
|
||||
return _parseKubeApi(path);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = _parseKubeApi(path);
|
||||
|
||||
logger.debug(`parseKubeApi(${inspect(path, false, null, false)}) -> ${inspect(res, false, null, false)}`);
|
||||
|
||||
return res;
|
||||
} catch (error) {
|
||||
logger.debug(`parseKubeApi(${inspect(path, false, null, false)}) threw: ${error}`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function _parseKubeApi(path: string): IKubeApiParsed {
|
||||
const apiPath = new URL(path, location.origin).pathname;
|
||||
const [, prefix, ...parts] = apiPath.split("/");
|
||||
const apiPrefix = `/${prefix}`;
|
||||
const [left, right, namespaced] = splitArray(parts, "namespaces");
|
||||
let apiGroup, apiVersion, namespace, resource, name;
|
||||
@ -70,12 +91,14 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
apiGroup = left.join("/");
|
||||
} else {
|
||||
switch (left.length) {
|
||||
case 0:
|
||||
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||
case 4:
|
||||
[apiGroup, apiVersion, resource, name] = left;
|
||||
break;
|
||||
case 2:
|
||||
resource = left.pop();
|
||||
// fallthrough
|
||||
// fallthrough
|
||||
case 1:
|
||||
apiVersion = left.pop();
|
||||
apiGroup = "";
|
||||
@ -91,7 +114,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
* There is no well defined selection from an array of items that were
|
||||
* separated by '/'
|
||||
*
|
||||
* Solution is to create a huristic. Namely:
|
||||
* Solution is to create a heuristic. Namely:
|
||||
* 1. if '.' in left[0] then apiGroup <- left[0]
|
||||
* 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1]
|
||||
* 3. otherwise assume apiVersion <- left[0]
|
||||
@ -113,7 +136,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
|
||||
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
|
||||
|
||||
if (!apiBase) {
|
||||
throw new Error(`invalid apiPath: ${path}`);
|
||||
throw new Error(`invalid apiPath: ${apiPath}`);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -254,12 +254,10 @@ export class KubeObject<Metadata extends IKubeObjectMetadata = IKubeObjectMetada
|
||||
}
|
||||
|
||||
getOwnerRefs() {
|
||||
const refs = this.metadata.ownerReferences || [];
|
||||
const refs = this.metadata?.ownerReferences || [];
|
||||
const namespace = this.getNs();
|
||||
|
||||
return refs.map(ownerRef => ({
|
||||
...ownerRef,
|
||||
namespace: this.getNs(),
|
||||
}));
|
||||
return refs.map(ownerRef => ({ ...ownerRef, namespace }));
|
||||
}
|
||||
|
||||
getSearchFields() {
|
||||
|
||||
@ -19,11 +19,13 @@
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
import { stringify } from "querystring";
|
||||
import { boundMethod, base64, EventEmitter } from "../utils";
|
||||
import { WebSocketApi } from "./websocket-api";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { isDevelopment } from "../../common/vars";
|
||||
import url from "url";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
import type { ParsedUrlQueryInput } from "querystring";
|
||||
|
||||
export enum TerminalChannels {
|
||||
STDIN = 0,
|
||||
@ -55,7 +57,9 @@ export class TerminalApi extends WebSocketApi {
|
||||
protected size: { Width: number; Height: number };
|
||||
|
||||
public onReady = new EventEmitter<[]>();
|
||||
public isReady = false;
|
||||
@observable public isReady = false;
|
||||
@observable public shellRunCommandsFinished = false;
|
||||
public readonly url: string;
|
||||
|
||||
constructor(protected options: TerminalApiQuery) {
|
||||
super({
|
||||
@ -63,34 +67,33 @@ export class TerminalApi extends WebSocketApi {
|
||||
flushOnOpen: false,
|
||||
pingIntervalSeconds: 30,
|
||||
});
|
||||
}
|
||||
makeObservable(this);
|
||||
|
||||
async getUrl() {
|
||||
let { port } = location;
|
||||
const { hostname, protocol } = location;
|
||||
const { id, node } = this.options;
|
||||
const wss = `ws${protocol === "https:" ? "s" : ""}://`;
|
||||
const query: TerminalApiQuery = { id };
|
||||
const { hostname, protocol, port } = location;
|
||||
const query: ParsedUrlQueryInput = {
|
||||
id: options.id,
|
||||
};
|
||||
|
||||
if (port) {
|
||||
port = `:${port}`;
|
||||
if (options.node) {
|
||||
query.node = options.node;
|
||||
query.type = options.type || "node";
|
||||
}
|
||||
|
||||
if (node) {
|
||||
query.node = node;
|
||||
query.type = "node";
|
||||
}
|
||||
|
||||
return `${wss}${hostname}${port}/api?${stringify(query)}`;
|
||||
this.url = url.format({
|
||||
protocol: protocol.includes("https") ? "wss" : "ws",
|
||||
hostname,
|
||||
port,
|
||||
pathname: "/api",
|
||||
query,
|
||||
slashes: true,
|
||||
});
|
||||
}
|
||||
|
||||
async connect() {
|
||||
const apiUrl = await this.getUrl();
|
||||
|
||||
connect() {
|
||||
this.emitStatus("Connecting ...");
|
||||
this.onData.addListener(this._onReady, { prepend: true });
|
||||
|
||||
return super.connect(apiUrl);
|
||||
this.onData.addListener(this._onShellRunCommandsFinished);
|
||||
super.connect(this.url);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
@ -106,6 +109,24 @@ export class TerminalApi extends WebSocketApi {
|
||||
this.onReady.removeAllListeners();
|
||||
}
|
||||
|
||||
_onShellRunCommandsFinished = (data: string) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a heuistic for ditermining when a shell has finished executing
|
||||
* its own rc file (or RunCommands file) such as `.bashrc` or `.zshrc`.
|
||||
*
|
||||
* This heuistic assumes that the prompt line of a terminal is a single line
|
||||
* and ends with a whitespace character.
|
||||
*/
|
||||
if (data.match(/\r?\n/) === null && data.match(/\s$/)) {
|
||||
this.shellRunCommandsFinished = true;
|
||||
this.onData.removeListener(this._onShellRunCommandsFinished);
|
||||
}
|
||||
};
|
||||
|
||||
@boundMethod
|
||||
protected _onReady(data: string) {
|
||||
if (!data) return true;
|
||||
|
||||
@ -26,6 +26,7 @@ export interface IToleration {
|
||||
key?: string;
|
||||
operator?: string;
|
||||
effect?: string;
|
||||
value?: string;
|
||||
tolerationSeconds?: number;
|
||||
}
|
||||
|
||||
|
||||
@ -47,11 +47,13 @@ import { WeblinkStore } from "../common/weblink-store";
|
||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { ThemeStore } from "./theme.store";
|
||||
import { SentryInit } from "../common/sentry";
|
||||
import { TerminalStore } from "./components/dock/terminal.store";
|
||||
|
||||
configurePackages();
|
||||
|
||||
/**
|
||||
* If this is a development buid, wait a second to attach
|
||||
* If this is a development build, wait a second to attach
|
||||
* Chrome Debugger to renderer process
|
||||
* https://stackoverflow.com/questions/52844870/debugging-electron-renderer-process-with-vscode
|
||||
*/
|
||||
@ -78,6 +80,7 @@ export async function bootstrap(App: AppComponent) {
|
||||
initializers.intiKubeObjectDetailRegistry();
|
||||
initializers.initWelcomeMenuRegistry();
|
||||
initializers.initWorkloadsOverviewDetailRegistry();
|
||||
initializers.initCatalogEntityDetailRegistry();
|
||||
initializers.initCatalog();
|
||||
initializers.initIpcRendererListeners();
|
||||
|
||||
@ -85,18 +88,31 @@ export async function bootstrap(App: AppComponent) {
|
||||
ExtensionDiscovery.createInstance().init();
|
||||
|
||||
UserStore.createInstance();
|
||||
await ClusterStore.createInstance().loadInitialOnRenderer();
|
||||
|
||||
SentryInit();
|
||||
|
||||
// ClusterStore depends on: UserStore
|
||||
const cs = ClusterStore.createInstance();
|
||||
|
||||
await cs.loadInitialOnRenderer();
|
||||
|
||||
// HotbarStore depends on: ClusterStore
|
||||
HotbarStore.createInstance();
|
||||
ExtensionsStore.createInstance();
|
||||
FilesystemProvisionerStore.createInstance();
|
||||
|
||||
// ThemeStore depends on: UserStore
|
||||
ThemeStore.createInstance();
|
||||
|
||||
// TerminalStore depends on: ThemeStore
|
||||
TerminalStore.createInstance();
|
||||
WeblinkStore.createInstance();
|
||||
|
||||
ExtensionInstallationStateStore.bindIpcListeners();
|
||||
HelmRepoManager.createInstance(); // initialize the manager
|
||||
|
||||
// Register additional store listeners
|
||||
ClusterStore.getInstance().registerIpcListener();
|
||||
cs.registerIpcListener();
|
||||
|
||||
// init app's dependencies if any
|
||||
if (App.init) {
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
import semver from "semver";
|
||||
import { observable, makeObservable } from "mobx";
|
||||
import { autoBind } from "../../utils";
|
||||
import { autoBind, sortCompareChartVersions } from "../../utils";
|
||||
import { getChartDetails, HelmChart, listCharts } from "../../api/endpoints/helm-charts.api";
|
||||
import { ItemStore } from "../../item.store";
|
||||
import flatten from "lodash/flatten";
|
||||
@ -60,12 +60,10 @@ export class HelmChartStore extends ItemStore<HelmChart> {
|
||||
}
|
||||
|
||||
protected sortVersions = (versions: IChartVersion[]) => {
|
||||
return versions.sort((first, second) => {
|
||||
const firstVersion = semver.coerce(first.version || 0);
|
||||
const secondVersion = semver.coerce(second.version || 0);
|
||||
|
||||
return semver.compare(secondVersion, firstVersion);
|
||||
});
|
||||
return versions
|
||||
.map(chartVersion => ({ ...chartVersion, __version: semver.coerce(chartVersion.version, { includePrerelease: true, loose: true }), }))
|
||||
.sort(sortCompareChartVersions)
|
||||
.map(({ __version, ...chartVersion }) => chartVersion);
|
||||
};
|
||||
|
||||
async getVersions(chartName: string, force?: boolean): Promise<IChartVersion[]> {
|
||||
|
||||
@ -27,13 +27,7 @@
|
||||
}
|
||||
|
||||
.status {
|
||||
.Badge {
|
||||
@include release-status-bgs;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@include release-status-bgs;
|
||||
}
|
||||
|
||||
.chart {
|
||||
|
||||
@ -272,7 +272,7 @@ export class ReleaseDetails extends Component<Props> {
|
||||
<DrawerItem name="Status" className="status" labelsOnly>
|
||||
<Badge
|
||||
label={release.getStatus()}
|
||||
className={cssNames("status", kebabCase(release.getStatus()))}
|
||||
className={kebabCase(release.getStatus())}
|
||||
/>
|
||||
</DrawerItem>
|
||||
{this.renderValues()}
|
||||
|
||||
@ -55,12 +55,16 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
this.loadFromContextNamespaces();
|
||||
}
|
||||
this.releaseSecrets.replace(newSecrets);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
}
|
||||
|
||||
watchSelecteNamespaces(): (() => void) {
|
||||
watchSelectedNamespaces(): (() => void) {
|
||||
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
|
||||
this.loadAll(namespaces);
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -91,7 +95,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
|
||||
this.failedLoading = false;
|
||||
} catch (error) {
|
||||
this.failedLoading = true;
|
||||
console.error("Loading Helm Chart releases has failed", error);
|
||||
console.warn("Loading Helm Chart releases has failed", error);
|
||||
|
||||
if (error.error) {
|
||||
Notifications.error(error.error);
|
||||
|
||||
@ -36,6 +36,7 @@ import { secretsStore } from "../+config-secrets/secrets.store";
|
||||
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
|
||||
import type { ReleaseRouteParams } from "../../../common/routes";
|
||||
import { releaseURL } from "../../../common/routes";
|
||||
import { namespaceStore } from "../+namespaces/namespace.store";
|
||||
|
||||
enum columnId {
|
||||
name = "name",
|
||||
@ -54,9 +55,15 @@ interface Props extends RouteComponentProps<ReleaseRouteParams> {
|
||||
@observer
|
||||
export class HelmReleases extends Component<Props> {
|
||||
componentDidMount() {
|
||||
const { match: { params: { namespace } } } = this.props;
|
||||
|
||||
if (namespace) {
|
||||
namespaceStore.selectNamespaces(namespace);
|
||||
}
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
releaseStore.watchAssociatedSecrets(),
|
||||
releaseStore.watchSelecteNamespaces(),
|
||||
releaseStore.watchSelectedNamespaces(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -88,6 +88,7 @@ export class CatalogEntityItem<T extends CatalogEntity> implements ItemObject {
|
||||
onClick?.(event);
|
||||
event.stopPropagation();
|
||||
}}
|
||||
expandable={false}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
import "./cluster-issues.scss";
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { computed, makeObservable } from "mobx";
|
||||
import { Icon } from "../icon";
|
||||
import { SubHeader } from "../layout/sub-header";
|
||||
@ -35,6 +35,7 @@ import { Spinner } from "../spinner";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import { lookupApiLink } from "../../api/kube-api";
|
||||
import { kubeSelectedUrlParam, showDetails } from "../kube-object";
|
||||
import { kubeWatchApi } from "../../api/kube-watch-api";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -65,6 +66,10 @@ export class ClusterIssues extends React.Component<Props> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
|
||||
disposeOnUnmount(this, [
|
||||
kubeWatchApi.subscribeStores([eventStore, nodesStore])
|
||||
]);
|
||||
}
|
||||
|
||||
@computed get warnings() {
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
@import "autoscaler.mixins";
|
||||
|
||||
.HpaDetails {
|
||||
.Badge {
|
||||
.status {
|
||||
@include hpa-status-bgc;
|
||||
}
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ export class HpaDetails extends React.Component<Props> {
|
||||
{hpa.getReplicas()}
|
||||
</DrawerItem>
|
||||
|
||||
<DrawerItem name="Status" labelsOnly>
|
||||
<DrawerItem name="Status" className="status" labelsOnly>
|
||||
{hpa.getConditions().map(({ type, tooltip, isReady }) => {
|
||||
if (!isReady) return null;
|
||||
|
||||
|
||||
@ -38,10 +38,7 @@
|
||||
&.status {
|
||||
flex: 1.5;
|
||||
@include table-cell-labels-offsets;
|
||||
|
||||
.Badge {
|
||||
@include hpa-status-bgc;
|
||||
}
|
||||
@include hpa-status-bgc;
|
||||
}
|
||||
|
||||
&.age {
|
||||
|
||||
@ -104,6 +104,7 @@ export class HorizontalPodAutoscalers extends React.Component<Props> {
|
||||
label={type}
|
||||
tooltip={tooltip}
|
||||
className={cssNames(type.toLowerCase())}
|
||||
expandable={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@ -175,6 +175,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
<Input
|
||||
required autoFocus
|
||||
placeholder="ResourceQuota name"
|
||||
trim
|
||||
validators={systemName}
|
||||
value={this.quotaName} onChange={v => this.quotaName = v.toLowerCase()}
|
||||
className="box grow"
|
||||
@ -218,7 +219,7 @@ export class AddQuotaDialog extends React.Component<Props> {
|
||||
<div className="quota-entries">
|
||||
{this.quotaEntries.map(([quota, value]) => {
|
||||
return (
|
||||
<div key={quota} className="quota flex gaps inline align-center">
|
||||
<div key={quota} className="quota gaps inline align-center">
|
||||
<div className="name">{quota}</div>
|
||||
<div className="value">{value}</div>
|
||||
<Icon material="clear" onClick={() => this.quotas[quota] = ""} />
|
||||
|
||||
@ -223,6 +223,7 @@ export class AddSecretDialog extends React.Component<Props> {
|
||||
<Input
|
||||
autoFocus required
|
||||
placeholder="Name"
|
||||
trim
|
||||
validators={systemName}
|
||||
value={name} onChange={v => this.name = v}
|
||||
/>
|
||||
|
||||
@ -79,7 +79,7 @@ export class Secrets extends React.Component<Props> {
|
||||
secret.getName(),
|
||||
<KubeObjectStatusIcon key="icon" object={secret} />,
|
||||
secret.getNs(),
|
||||
secret.getLabels().map(label => <Badge key={label} label={label}/>),
|
||||
secret.getLabels().map(label => <Badge key={label} label={label} expandable={false}/>),
|
||||
secret.getKeys().join(", "),
|
||||
secret.type,
|
||||
secret.getAge(),
|
||||
|
||||
@ -23,11 +23,9 @@
|
||||
|
||||
.CRDDetails {
|
||||
.conditions {
|
||||
.Badge {
|
||||
@include crd-condition-bgc;
|
||||
}
|
||||
@include crd-condition-bgc;
|
||||
}
|
||||
|
||||
|
||||
.Table {
|
||||
margin-left: -$margin * 2;
|
||||
margin-right: -$margin * 2;
|
||||
@ -41,21 +39,8 @@
|
||||
flex: 0.5;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 1.5;
|
||||
|
||||
.Badge {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.json-path {
|
||||
flex: 3;
|
||||
|
||||
.Badge {
|
||||
white-space: pre-line;
|
||||
text-overflow: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@ import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { observer } from "mobx-react";
|
||||
import type { CustomResourceDefinition } from "../../api/endpoints/crd.api";
|
||||
import { cssNames } from "../../utils";
|
||||
import { AceEditor } from "../ace-editor";
|
||||
import { Badge } from "../badge";
|
||||
import { DrawerItem, DrawerTitle } from "../drawer";
|
||||
@ -86,7 +85,8 @@ export class CRDDetails extends React.Component<Props> {
|
||||
<Badge
|
||||
key={type}
|
||||
label={type}
|
||||
className={cssNames({ disabled: status === "False" }, type)}
|
||||
disabled={status === "False"}
|
||||
className={type}
|
||||
tooltip={(
|
||||
<>
|
||||
<p>{message}</p>
|
||||
|
||||
@ -139,7 +139,7 @@ export class CrdList extends React.Component {
|
||||
]}
|
||||
renderTableContents={(crd: CustomResourceDefinition) => [
|
||||
<Link key="link" to={crd.getResourceUrl()} onClick={stopPropagation}>
|
||||
{crd.getResourceTitle()}
|
||||
{crd.getResourceKind()}
|
||||
</Link>,
|
||||
crd.getGroup(),
|
||||
crd.getVersion(),
|
||||
|
||||
@ -21,13 +21,9 @@
|
||||
|
||||
.CrdResourceDetails {
|
||||
.status {
|
||||
.value {
|
||||
.Badge {
|
||||
&.ready {
|
||||
color: white;
|
||||
background-color: $colorOk;
|
||||
}
|
||||
}
|
||||
.ready {
|
||||
color: white;
|
||||
background-color: $colorOk;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,7 +90,8 @@ export class CrdResourceDetails extends React.Component<Props> {
|
||||
.map(({ kind, message, status }, index) => (
|
||||
<Badge
|
||||
key={kind + index} label={kind}
|
||||
className={cssNames({ disabled: status === "False" }, kind.toLowerCase())}
|
||||
disabled={status === "False"}
|
||||
className={kind.toLowerCase()}
|
||||
tooltip={message}
|
||||
/>
|
||||
));
|
||||
|
||||
@ -30,7 +30,7 @@ $crd-condition-colors: (
|
||||
|
||||
@mixin crd-condition-bgc {
|
||||
@each $status, $color in $crd-condition-colors {
|
||||
&.#{$status} {
|
||||
.#{$status} {
|
||||
background: $color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -59,14 +59,7 @@ describe("Extensions", () => {
|
||||
});
|
||||
|
||||
ExtensionInstallationStateStore.reset();
|
||||
UserStore.resetInstance();
|
||||
|
||||
UserStore.createInstance();
|
||||
|
||||
ExtensionDiscovery.resetInstance();
|
||||
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
|
||||
ExtensionLoader.resetInstance();
|
||||
ExtensionLoader.createInstance().addExtension({
|
||||
id: "extensionId",
|
||||
manifest: {
|
||||
@ -79,10 +72,15 @@ describe("Extensions", () => {
|
||||
isEnabled: true,
|
||||
isCompatible: true
|
||||
});
|
||||
ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve());
|
||||
UserStore.createInstance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
UserStore.resetInstance();
|
||||
ExtensionDiscovery.resetInstance();
|
||||
ExtensionLoader.resetInstance();
|
||||
});
|
||||
|
||||
it("disables uninstall and disable buttons while uninstalling", async () => {
|
||||
|
||||
@ -225,7 +225,7 @@ export class ExtensionInstallationStateStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is at least one extension currently ininstalling
|
||||
* If there is at least one extension currently uninstalling
|
||||
*/
|
||||
static get anyUninstalling(): boolean {
|
||||
return ExtensionInstallationStateStore.uninstalling > 0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user