mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into electron-11.4.3
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
commit
e38addef78
@ -35,6 +35,15 @@ 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
|
||||
rm -rf .lens-ide-overlay/.git
|
||||
cp -r .lens-ide-overlay/* ./
|
||||
cp build/package.json.patch . && patch package.json package.json.patch
|
||||
displayName: Customize config
|
||||
env:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
- script: make node_modules
|
||||
displayName: Install dependencies
|
||||
- script: make build-npm
|
||||
@ -51,7 +60,6 @@ jobs:
|
||||
env:
|
||||
WIN_CSC_LINK: $(WIN_CSC_LINK)
|
||||
WIN_CSC_KEY_PASSWORD: $(WIN_CSC_KEY_PASSWORD)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||
- job: macOS
|
||||
@ -76,6 +84,15 @@ 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
|
||||
rm -rf .lens-ide-overlay/.git
|
||||
cp -r .lens-ide-overlay/* ./
|
||||
cp build/package.json.patch . && patch package.json package.json.patch
|
||||
displayName: Customize config
|
||||
env:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
- script: make node_modules
|
||||
displayName: Install dependencies
|
||||
- script: make build-npm
|
||||
@ -94,7 +111,6 @@ jobs:
|
||||
APPLEIDPASS: $(APPLEIDPASS)
|
||||
CSC_LINK: $(CSC_LINK)
|
||||
CSC_KEY_PASSWORD: $(CSC_KEY_PASSWORD)
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||
- job: Linux
|
||||
@ -119,6 +135,15 @@ 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
|
||||
rm -rf .lens-ide-overlay/.git
|
||||
cp -r .lens-ide-overlay/* ./
|
||||
cp build/package.json.patch . && patch package.json package.json.patch
|
||||
displayName: Customize config
|
||||
env:
|
||||
GH_TOKEN: $(LENS_IDE_GH_TOKEN)
|
||||
- script: make node_modules
|
||||
displayName: Install dependencies
|
||||
- script: make build-npm
|
||||
@ -143,7 +168,6 @@ jobs:
|
||||
displayName: Build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
env:
|
||||
GH_TOKEN: $(GH_TOKEN)
|
||||
AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)
|
||||
AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)
|
||||
- script: make publish-npm
|
||||
|
||||
8
.bundled-extensions.json
Normal file
8
.bundled-extensions.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extensions": [
|
||||
"pod-menu",
|
||||
"node-menu",
|
||||
"metrics-cluster-feature",
|
||||
"kube-object-event-status"
|
||||
]
|
||||
}
|
||||
@ -79,6 +79,8 @@ module.exports = {
|
||||
sourceType: "module",
|
||||
},
|
||||
rules: {
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
@ -137,6 +139,8 @@ module.exports = {
|
||||
jsx: true,
|
||||
},
|
||||
rules: {
|
||||
"no-invalid-this": "off",
|
||||
"@typescript-eslint/no-invalid-this": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
|
||||
18
.github/workflows/test.yml
vendored
18
.github/workflows/test.yml
vendored
@ -77,27 +77,15 @@ jobs:
|
||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||
name: Install integration test dependencies
|
||||
if: runner.os == 'Linux'
|
||||
- run: |
|
||||
set -e
|
||||
rm -rf extensions/telemetry
|
||||
xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
||||
git checkout extensions/telemetry
|
||||
- run: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux
|
||||
name: Run Linux integration tests
|
||||
if: runner.os == 'Linux'
|
||||
|
||||
- run: |
|
||||
set -e
|
||||
rm -rf extensions/telemetry
|
||||
make integration-win
|
||||
git checkout extensions/telemetry
|
||||
- run: make integration-win
|
||||
name: Run Windows integration tests
|
||||
shell: bash
|
||||
if: runner.os == 'Windows'
|
||||
|
||||
- run: |
|
||||
set -e
|
||||
rm -rf extensions/telemetry
|
||||
make integration-mac
|
||||
git checkout extensions/telemetry
|
||||
- run: make integration-mac
|
||||
name: Run macOS integration tests
|
||||
if: runner.os == 'macOS'
|
||||
|
||||
4
LICENSE
4
LICENSE
@ -1,11 +1,9 @@
|
||||
Copyright (c) 2021 Mirantis, Inc.
|
||||
Copyright (c) 2021 OpenLens Authors.
|
||||
|
||||
Portions of this software are licensed as follows:
|
||||
|
||||
* All content residing under the "docs/" directory of this repository, if that
|
||||
directory exists, is licensed under "Creative Commons: CC BY-SA 4.0 license".
|
||||
* All third party components incorporated into the Lens Software are licensed
|
||||
under the original license provided by the owner of the applicable component.
|
||||
* Content outside of the above mentioned directories or restrictions above is
|
||||
available under the "MIT" license as defined below.
|
||||
|
||||
|
||||
5
Makefile
5
Makefile
@ -65,10 +65,11 @@ integration-win: binaries/client build-extension-types build-extensions
|
||||
|
||||
.PHONY: build
|
||||
build: node_modules binaries/client build-extensions
|
||||
yarn run compile
|
||||
ifeq "$(DETECTED_OS)" "Windows"
|
||||
yarn dist:win
|
||||
./node_modules/.bin/electron-builder --publish onTag --x64 --ia32
|
||||
else
|
||||
yarn dist
|
||||
./node_modules/.bin/electron-builder --publish onTag
|
||||
endif
|
||||
|
||||
$(extension_node_modules):
|
||||
|
||||
32
README.md
32
README.md
@ -1,32 +1,22 @@
|
||||
# Lens | The Kubernetes IDE
|
||||
# Lens Open Source Project (OpenLens)
|
||||
|
||||
[](https://github.com/lensapp/lens/actions/workflows/test.yml)
|
||||
[](https://github.com/lensapp/lens/releases?label=Downloads)
|
||||
[](https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI)
|
||||
|
||||
Lens provides the full situational awareness for everything that runs in Kubernetes. It's lowering the barrier of entry for people just getting started and radically improving productivity for people with more experience.
|
||||
## The Repository
|
||||
|
||||
The Lens open source project is backed by a number of Kubernetes and cloud native ecosystem pioneers. It's a standalone application for MacOS, Windows and Linux operating systems. Lens is 100% open source and free of charge for any purpose.
|
||||
This repository ("OpenLens") is where Team Lens develops the [Lens IDE](https://k8slens.dev) product together with the community. It is backed by a number of Kubernetes and cloud native ecosystem pioneers. This source code is available to everyone under the [MIT license](./LICENSE).
|
||||
|
||||
## Lens - The Kubernetes IDE
|
||||
|
||||
Lens - The Kubernetes IDE ("Lens IDE") is a distribution of the OpenLens repository with Team Lens specific customizations released under a traditional [EULA](https://k8slens.dev/licenses/eula).
|
||||
|
||||
Lens IDE provides the full situational awareness for everything that runs in Kubernetes. It's lowering the barrier of entry for people just getting started and radically improving productivity for people with more experience.
|
||||
|
||||
Lens IDE a standalone application for MacOS, Windows and Linux operating systems. You can download it free of charge for Windows, MacOS, and Linux from [Lens IDE website](https://k8slens.dev).
|
||||
|
||||
[](https://www.youtube.com/watch?v=eeDwdVXattc)
|
||||
|
||||
## What makes Lens special?
|
||||
|
||||
* Amazing usability and end-user experience
|
||||
* Unified, secure, multi-cluster management on any platform: support for hundreds of clusters
|
||||
* Standalone application: no need to install anything in-cluster
|
||||
* Lens installs anywhere, elimanting the need to wrangle credentials
|
||||
* Real-time cluster state visualization
|
||||
* Resource utilization charts and trends with history powered by built-in Prometheus
|
||||
* Smart terminal access to nodes and containers
|
||||
* Clusters can be local (e.g. minikube) or external (e.g. EKS, GKE, AKS)
|
||||
* Performance optimized to handle massive clusters (tested with a cluster running 25k pods)
|
||||
* RBAC security is preserved, as Lens uses the standard Kubernetes API
|
||||
* Lens Extensions are used to add custom visualizations and functionality to accelerate development workflows for all the technologies and services that integrate with Kubernetes
|
||||
* Port forwarding
|
||||
* Helm package deployment: Browse and deploy Helm charts with one click-Install
|
||||
* Extensions via Lens Extensions API
|
||||
|
||||
## Installation
|
||||
|
||||
See [Getting Started](https://docs.k8slens.dev/latest/getting-started/) page.
|
||||
|
||||
@ -30,7 +30,7 @@ Each guide or code sample includes the following:
|
||||
| Sample | APIs |
|
||||
| ----- | ----- |
|
||||
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore |
|
||||
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.ClusterStore <br> Store.workspaceStore |
|
||||
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
|
||||
|
||||
@ -45,8 +45,6 @@ It accesses some Lens state data, and it periodically logs the name of the clust
|
||||
```typescript
|
||||
import { LensMainExtension, Store } from "@k8slens/extensions";
|
||||
|
||||
const clusterStore = Store.clusterStore
|
||||
|
||||
export default class ActiveClusterExtensionMain extends LensMainExtension {
|
||||
|
||||
timer: NodeJS.Timeout
|
||||
@ -54,11 +52,11 @@ export default class ActiveClusterExtensionMain extends LensMainExtension {
|
||||
onActivate() {
|
||||
console.log("Cluster logger activated");
|
||||
this.timer = setInterval(() => {
|
||||
if (!clusterStore.active) {
|
||||
if (!Store.ClusterStore.getInstance().active) {
|
||||
console.log("No active cluster");
|
||||
return;
|
||||
}
|
||||
console.log("active cluster is", clusterStore.active.contextName)
|
||||
console.log("active cluster is", Store.ClusterStore.getInstance().active.contextName)
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
|
||||
@ -57,7 +57,7 @@ The example above logs messages when the extension is enabled and disabled.
|
||||
Cluster pages appear in the cluster dashboard.
|
||||
Use cluster pages to display information about or add functionality to the active cluster.
|
||||
It is also possible to include custom details from other clusters.
|
||||
Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore).
|
||||
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
|
||||
|
||||
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:
|
||||
|
||||
|
||||
@ -15,6 +15,12 @@ This guide shows how to create a store for the [`appPreferences`](../renderer-ex
|
||||
The preference is a simple boolean that indicates whether or not something is enabled.
|
||||
However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted.
|
||||
|
||||
`Store.ExtensionStore`'s child class will need to be created before being used.
|
||||
It is recommended to call the inherited static method `getInstanceOrCreate()` only in one place, generally within you extension's `onActivate()` method.
|
||||
It is also recommenced to delete the instance, using the inherited static method `resetInstance()`, in your extension's `onDeactivate()` method.
|
||||
Everywhere else in your code you should use the `getInstance()` static method.
|
||||
This is so that your data is kept up to date and not persisted longer than expected.
|
||||
|
||||
The following example code creates a store for the `appPreferences` guide example:
|
||||
|
||||
``` typescript
|
||||
@ -50,8 +56,6 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
|
||||
```
|
||||
|
||||
First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type.
|
||||
@ -71,46 +75,51 @@ It is called when the store is being saved.
|
||||
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format.
|
||||
The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
|
||||
|
||||
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension.
|
||||
Note that `examplePreferencesStore` is a singleton.
|
||||
Calling this function again will not create a new store.
|
||||
Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension.
|
||||
Note that `ExamplePreferencesStore` is a singleton.
|
||||
Calling this function will create an instance if one has not been made before.
|
||||
Through normal use you should call `ExamplePreferencesStore.getInstance()` as that will throw an error if an instance does not exist.
|
||||
This provides some logical safety in that it limits where a new instance can be created.
|
||||
Thus it prevents an instance from being created when the constructor args are not present at the call site.
|
||||
|
||||
If you are doing some cleanup it is recommended to call `ExamplePreferencesStore.getInstance(false)` which returns `undefined` instead of throwing when there is no instance.
|
||||
|
||||
The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store.
|
||||
`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens.
|
||||
`ExamplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens.
|
||||
This can be done in `./main.ts`:
|
||||
|
||||
``` typescript
|
||||
import { LensMainExtension } from "@k8slens/extensions";
|
||||
import { examplePreferencesStore } from "./src/example-preference-store";
|
||||
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
||||
|
||||
export default class ExampleMainExtension extends LensMainExtension {
|
||||
async onActivate() {
|
||||
await examplePreferencesStore.loadExtension(this);
|
||||
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
|
||||
Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
|
||||
Here, `ExamplePreferencesStore` loads with `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
|
||||
Similarly, `ExamplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
|
||||
This can be done in `./renderer.ts`:
|
||||
|
||||
``` typescript
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
|
||||
import { examplePreferencesStore } from "./src/example-preference-store";
|
||||
import { ExamplePreferencesStore } from "./src/example-preference-store";
|
||||
import React from "react";
|
||||
|
||||
export default class ExampleRendererExtension extends LensRendererExtension {
|
||||
|
||||
async onActivate() {
|
||||
await examplePreferencesStore.loadExtension(this);
|
||||
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
|
||||
}
|
||||
|
||||
appPreferences = [
|
||||
{
|
||||
title: "Example Preferences",
|
||||
components: {
|
||||
Input: () => <ExamplePreferenceInput preference={examplePreferencesStore}/>,
|
||||
Input: () => <ExamplePreferenceInput />,
|
||||
Hint: () => <ExamplePreferenceHint/>
|
||||
}
|
||||
}
|
||||
@ -118,8 +127,8 @@ export default class ExampleRendererExtension extends LensRendererExtension {
|
||||
}
|
||||
```
|
||||
|
||||
Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
|
||||
There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
|
||||
Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is called to load `ExamplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
|
||||
|
||||
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
|
||||
|
||||
``` typescript
|
||||
@ -128,21 +137,15 @@ import { observer } from "mobx-react";
|
||||
import React from "react";
|
||||
import { ExamplePreferencesStore } from "./example-preference-store";
|
||||
|
||||
export class ExamplePreferenceProps {
|
||||
preference: ExamplePreferencesStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
|
||||
export class ExamplePreferenceInput extends React.Component {
|
||||
|
||||
render() {
|
||||
const { preference } = this.props;
|
||||
|
||||
return (
|
||||
<Component.Checkbox
|
||||
label="I understand appPreferences"
|
||||
value={preference.enabled}
|
||||
onChange={v => { preference.enabled = v; }}
|
||||
value={ExamplePreferencesStore.getInstace().enabled}
|
||||
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -159,4 +162,4 @@ export class ExamplePreferenceHint extends React.Component {
|
||||
|
||||
The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type.
|
||||
Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the
|
||||
`examplePreferencesStore`.
|
||||
`ExamplePreferencesStore`.
|
||||
|
||||
2
extensions/example-extension/.gitignore
vendored
2
extensions/example-extension/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
@ -1,11 +0,0 @@
|
||||
# Lens Example Extension
|
||||
|
||||
*TODO*: add more info
|
||||
|
||||
## Build
|
||||
|
||||
`npm run build`
|
||||
|
||||
## Dev
|
||||
|
||||
`npm run dev`
|
||||
@ -1,11 +0,0 @@
|
||||
import { LensMainExtension } from "@k8slens/extensions";
|
||||
|
||||
export default class ExampleExtensionMain extends LensMainExtension {
|
||||
onActivate() {
|
||||
console.log("EXAMPLE EXTENSION MAIN: ACTIVATED", this.name, this.id);
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
console.log("EXAMPLE EXTENSION MAIN: DEACTIVATED", this.name, this.id);
|
||||
}
|
||||
}
|
||||
6923
extensions/example-extension/package-lock.json
generated
6923
extensions/example-extension/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "example-extension",
|
||||
"version": "1.0.0",
|
||||
"description": "Example extension",
|
||||
"main": "dist/main.js",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
"metadata": {},
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js",
|
||||
"dev": "npm run build --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-open-doodles": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"jest": "^26.6.3",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CoffeeDoodle } from "react-open-doodles";
|
||||
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
|
||||
|
||||
export interface ExamplePageProps extends Interface.PageComponentProps<ExamplePageParams> {
|
||||
extension: LensRendererExtension; // provided in "./renderer.tsx"
|
||||
}
|
||||
|
||||
export interface ExamplePageParams {
|
||||
exampleId: string;
|
||||
selectedNamespaces: K8sApi.Namespace[];
|
||||
}
|
||||
|
||||
export const namespaceStore = K8sApi.apiManager.getStore<K8sApi.NamespaceStore>(K8sApi.namespacesApi);
|
||||
|
||||
@observer
|
||||
export class ExamplePage extends React.Component<ExamplePageProps> {
|
||||
async componentDidMount() {
|
||||
await namespaceStore.loadAll();
|
||||
}
|
||||
|
||||
deactivate = () => {
|
||||
const { extension } = this.props;
|
||||
|
||||
extension.disable();
|
||||
};
|
||||
|
||||
renderSelectedNamespaces() {
|
||||
const { selectedNamespaces } = this.props.params;
|
||||
|
||||
return (
|
||||
<div className="flex gaps inline">
|
||||
{selectedNamespaces.get().map(ns => {
|
||||
const name = ns.getName();
|
||||
|
||||
return <Component.Badge key={name} label={name} tooltip={`Created: ${ns.getAge()}`}/>;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { exampleId } = this.props.params;
|
||||
|
||||
return (
|
||||
<div className="flex column gaps align-flex-start" style={{ padding: 24 }}>
|
||||
<div style={{ width: 200 }}>
|
||||
<CoffeeDoodle accent="#3d90ce"/>
|
||||
</div>
|
||||
|
||||
<div>Hello from Example extension!</div>
|
||||
<div>Location: <i>{location.href}</i></div>
|
||||
<div>Namespaces: {this.renderSelectedNamespaces()}</div>
|
||||
|
||||
<p className="url-params-demo flex column gaps">
|
||||
<a onClick={() => exampleId.set("secret")}>Show secret button</a>
|
||||
{exampleId.get() === "secret" && (
|
||||
<Component.Button accent label="Deactivate" onClick={this.deactivate}/>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
import { Component, Interface, K8sApi, LensRendererExtension } from "@k8slens/extensions";
|
||||
import { ExamplePage, ExamplePageParams, namespaceStore } from "./page";
|
||||
import React from "react";
|
||||
import path from "path";
|
||||
|
||||
export default class ExampleExtension extends LensRendererExtension {
|
||||
clusterPages: Interface.PageRegistration[] = [
|
||||
{
|
||||
components: {
|
||||
Page: (props: Interface.PageComponentProps<ExamplePageParams>) => {
|
||||
return <ExamplePage {...props} extension={this}/>;
|
||||
},
|
||||
},
|
||||
params: {
|
||||
// setup basic param "exampleId" with default value "demo"
|
||||
exampleId: "demo",
|
||||
|
||||
// setup advanced multi-values param "selectedNamespaces" with custom parsing/stringification
|
||||
selectedNamespaces: {
|
||||
defaultValueStringified: ["default", "kube-system"],
|
||||
multiValues: true,
|
||||
parse(values: string[]) { // from URL
|
||||
return values.map(name => namespaceStore.getByName(name)).filter(Boolean);
|
||||
},
|
||||
stringify(values: K8sApi.Namespace[]) { // to URL
|
||||
return values.map(namespace => namespace.getName());
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
clusterPageMenus: Interface.ClusterPageMenuRegistration[] = [
|
||||
{
|
||||
title: "Example extension",
|
||||
components: {
|
||||
Icon: ExampleIcon,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function ExampleIcon(props: Component.IconProps) {
|
||||
return <Component.Icon {...props} material="pages" tooltip={path.basename(__filename)}/>;
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"module": "CommonJS",
|
||||
"target": "ES2017",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": false,
|
||||
"declaration": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"./*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"*.js"
|
||||
]
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
entry: "./main.ts",
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"mobx": "var global.Mobx",
|
||||
"react": "var global.React"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: "./renderer.tsx",
|
||||
context: __dirname,
|
||||
target: "electron-renderer",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "renderer.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -2685,9 +2685,9 @@
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
|
||||
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
|
||||
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"figgy-pudding": "^3.5.1"
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import { LensMainExtension, Util } from "@k8slens/extensions";
|
||||
|
||||
export default class LicenseLensMainExtension extends LensMainExtension {
|
||||
appMenus = [
|
||||
{
|
||||
parentId: "help",
|
||||
label: "License",
|
||||
async click() {
|
||||
Util.openExternal("https://k8slens.dev/licenses/eula");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
7060
extensions/license-menu-item/package-lock.json
generated
7060
extensions/license-menu-item/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "lens-license",
|
||||
"version": "0.1.0",
|
||||
"description": "License menu item",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"build": "webpack -p",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"@types/webpack": "^4.41.17",
|
||||
"jest": "^26.6.3",
|
||||
"mobx": "^5.15.5",
|
||||
"react": "^16.13.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"ts-node": "^9.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"module": "CommonJS",
|
||||
"target": "ES2017",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": false,
|
||||
"declaration": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import path from "path";
|
||||
|
||||
const outputPath = path.resolve(__dirname, "dist");
|
||||
|
||||
export default [
|
||||
{
|
||||
entry: "./main.ts",
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: {
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"mobx": "var global.Mobx",
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".tsx", ".ts", ".js"],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "main.js",
|
||||
path: outputPath,
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -5693,9 +5693,9 @@
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
|
||||
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
|
||||
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"figgy-pudding": "^3.5.1"
|
||||
|
||||
6
extensions/pod-menu/package-lock.json
generated
6
extensions/pod-menu/package-lock.json
generated
@ -6337,9 +6337,9 @@
|
||||
}
|
||||
},
|
||||
"ssri": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz",
|
||||
"integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz",
|
||||
"integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"figgy-pudding": "^3.5.1"
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { LensMainExtension } from "@k8slens/extensions";
|
||||
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||
|
||||
export default class SurveyMainExtension extends LensMainExtension {
|
||||
|
||||
async onActivate() {
|
||||
await surveyPreferencesStore.loadExtension(this);
|
||||
}
|
||||
}
|
||||
7928
extensions/survey/package-lock.json
generated
7928
extensions/survey/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
{
|
||||
"name": "lens-survey",
|
||||
"version": "0.1.0",
|
||||
"description": "Lens survey",
|
||||
"main": "dist/main.js",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
"metadata": {},
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack -p",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"got": "^11.8.1",
|
||||
"jest": "^26.6.3",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"react": "^16.13.1",
|
||||
"refiner-js": "^1.0.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { survey } from "./src/survey";
|
||||
import { SurveyPreferenceHint, SurveyPreferenceInput } from "./src/survey-preference";
|
||||
import { surveyPreferencesStore } from "./src/survey-preferences-store";
|
||||
import React from "react";
|
||||
|
||||
export default class SurveyRendererExtension extends LensRendererExtension {
|
||||
appPreferences = [
|
||||
{
|
||||
title: "In-App Surveys",
|
||||
showInPreferencesTab: "telemetry",
|
||||
components: {
|
||||
Hint: () => <SurveyPreferenceHint/>,
|
||||
Input: () => <SurveyPreferenceInput survey={surveyPreferencesStore}/>
|
||||
}
|
||||
}
|
||||
];
|
||||
async onActivate() {
|
||||
// Activate extension only on main renderer
|
||||
if (window.location.hostname === "localhost") {
|
||||
await surveyPreferencesStore.loadExtension(this);
|
||||
survey.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
3
extensions/survey/src/refiner-js.d.ts
vendored
3
extensions/survey/src/refiner-js.d.ts
vendored
@ -1,3 +0,0 @@
|
||||
declare module "refiner-js" {
|
||||
export default function Refiner(key: string, value: string|object|number|Boolean|Array): void;
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import { Component } from "@k8slens/extensions";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { SurveyPreferencesStore } from "./survey-preferences-store";
|
||||
|
||||
@observer
|
||||
export class SurveyPreferenceInput extends React.Component<{survey: SurveyPreferencesStore}, {}> {
|
||||
render() {
|
||||
const { survey } = this.props;
|
||||
|
||||
return (
|
||||
<Component.Checkbox
|
||||
label="Allow in-app surveys"
|
||||
value={survey.enabled}
|
||||
onChange={v => survey.enabled = v }
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class SurveyPreferenceHint extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<span>This will allow you to participate in surveys to improve the Lens experience.</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
import { Store } from "@k8slens/extensions";
|
||||
import { observable, toJS, when } from "mobx";
|
||||
|
||||
export type SurveyPreferencesModel = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export class SurveyPreferencesStore extends Store.ExtensionStore<SurveyPreferencesModel> {
|
||||
|
||||
@observable enabled = true;
|
||||
|
||||
whenEnabled = when(() => this.enabled);
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "preferences-store",
|
||||
defaults: {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected fromStore({ enabled }: SurveyPreferencesModel): void {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
toJSON(): SurveyPreferencesModel {
|
||||
return toJS({
|
||||
enabled: this.enabled
|
||||
}, {
|
||||
recurseEverything: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const surveyPreferencesStore = SurveyPreferencesStore.getInstance<SurveyPreferencesStore>();
|
||||
@ -1,46 +0,0 @@
|
||||
import { Util } from "@k8slens/extensions";
|
||||
import { machineId } from "node-machine-id";
|
||||
import Refiner from "refiner-js";
|
||||
import got from "got";
|
||||
import { surveyPreferencesStore } from "./survey-preferences-store";
|
||||
|
||||
type SurveyIdResponse = {
|
||||
surveyId: string;
|
||||
};
|
||||
export class Survey extends Util.Singleton {
|
||||
static readonly PROJECT_ID = "af468d00-4f8f-11eb-b01d-23b6562fef43";
|
||||
protected anonymousId: string;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async start() {
|
||||
await surveyPreferencesStore.whenEnabled;
|
||||
|
||||
const surveyId = await this.fetchSurveyId();
|
||||
|
||||
if (surveyId) {
|
||||
Refiner("setProject", Survey.PROJECT_ID);
|
||||
Refiner("identifyUser", {
|
||||
id: surveyId,
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async fetchSurveyId() {
|
||||
try {
|
||||
const surveyApi = process.env.SURVEY_API_URL || "https://survey.k8slens.dev";
|
||||
const anonymousId = await machineId();
|
||||
const { body } = await got(`${surveyApi}/api/survey-id?anonymousId=${anonymousId}`, { responseType: "json"});
|
||||
|
||||
return (body as SurveyIdResponse).surveyId;
|
||||
} catch(error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const survey = Survey.getInstance<Survey>();
|
||||
@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"module": "CommonJS",
|
||||
"target": "ES2017",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": false,
|
||||
"declaration": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"../../types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"renderer.ts",
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
entry: "./main.ts",
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: "./renderer.tsx",
|
||||
context: __dirname,
|
||||
target: "electron-renderer",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "renderer.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -1,19 +0,0 @@
|
||||
import { LensMainExtension } from "@k8slens/extensions";
|
||||
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
|
||||
import { tracker } from "./src/tracker";
|
||||
|
||||
export default class TelemetryMainExtension extends LensMainExtension {
|
||||
|
||||
async onActivate() {
|
||||
console.log("telemetry main extension activated");
|
||||
tracker.start();
|
||||
tracker.reportPeriodically();
|
||||
tracker.watchExtensions();
|
||||
await telemetryPreferencesStore.loadExtension(this);
|
||||
}
|
||||
|
||||
onDeactivate() {
|
||||
tracker.stop();
|
||||
console.log("telemetry main extension deactivated");
|
||||
}
|
||||
}
|
||||
7142
extensions/telemetry/package-lock.json
generated
7142
extensions/telemetry/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "lens-telemetry",
|
||||
"version": "0.1.0",
|
||||
"description": "Lens telemetry",
|
||||
"main": "dist/main.js",
|
||||
"renderer": "dist/renderer.js",
|
||||
"lens": {
|
||||
"metadata": {},
|
||||
"styles": []
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack -p",
|
||||
"dev": "webpack --watch",
|
||||
"test": "jest --passWithNoTests --env=jsdom src $@"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@k8slens/extensions": "file:../../src/extensions/npm/extensions",
|
||||
"@types/analytics-node": "^3.1.3",
|
||||
"analytics-node": "^3.4.0-beta.3",
|
||||
"jest": "^26.6.3",
|
||||
"mobx": "^5.15.5",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"react": "^16.13.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"typescript": "^4.0.3",
|
||||
"universal-analytics": "^0.4.23",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
import { LensRendererExtension } from "@k8slens/extensions";
|
||||
import { telemetryPreferencesStore } from "./src/telemetry-preferences-store";
|
||||
import { TelemetryPreferenceHint, TelemetryPreferenceInput } from "./src/telemetry-preference";
|
||||
import { tracker } from "./src/tracker";
|
||||
import React from "react";
|
||||
|
||||
export default class TelemetryRendererExtension extends LensRendererExtension {
|
||||
appPreferences = [
|
||||
{
|
||||
title: "Telemetry & Usage Tracking",
|
||||
showInPreferencesTab: "telemetry",
|
||||
id: "telemetry-tracking",
|
||||
components: {
|
||||
Hint: () => <TelemetryPreferenceHint/>,
|
||||
Input: () => <TelemetryPreferenceInput telemetry={telemetryPreferencesStore}/>
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
async onActivate() {
|
||||
console.log("telemetry extension activated");
|
||||
tracker.start();
|
||||
await telemetryPreferencesStore.loadExtension(this);
|
||||
}
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import { Component } from "@k8slens/extensions";
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { TelemetryPreferencesStore } from "./telemetry-preferences-store";
|
||||
|
||||
@observer
|
||||
export class TelemetryPreferenceInput extends React.Component<{telemetry: TelemetryPreferencesStore}, {}> {
|
||||
render() {
|
||||
const { telemetry } = this.props;
|
||||
|
||||
return (
|
||||
<Component.Checkbox
|
||||
label="Allow telemetry & usage tracking"
|
||||
value={telemetry.enabled}
|
||||
onChange={v => { telemetry.enabled = v; }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class TelemetryPreferenceHint extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<span>Telemetry & usage data is collected to continuously improve the Lens experience.</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
import { Store } from "@k8slens/extensions";
|
||||
import { observable, toJS } from "mobx";
|
||||
|
||||
export type TelemetryPreferencesModel = {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export class TelemetryPreferencesStore extends Store.ExtensionStore<TelemetryPreferencesModel> {
|
||||
|
||||
@observable enabled = true;
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "preferences-store",
|
||||
defaults: {
|
||||
enabled: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected fromStore({ enabled }: TelemetryPreferencesModel): void {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
toJSON(): TelemetryPreferencesModel {
|
||||
return toJS({
|
||||
enabled: this.enabled
|
||||
}, {
|
||||
recurseEverything: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const telemetryPreferencesStore = TelemetryPreferencesStore.getInstance<TelemetryPreferencesStore>();
|
||||
@ -1,185 +0,0 @@
|
||||
import { EventBus, Util, Catalog, App } from "@k8slens/extensions";
|
||||
import ua from "universal-analytics";
|
||||
import Analytics from "analytics-node";
|
||||
import { machineIdSync } from "node-machine-id";
|
||||
import { telemetryPreferencesStore } from "./telemetry-preferences-store";
|
||||
import { reaction, IReactionDisposer } from "mobx";
|
||||
import { comparer } from "mobx";
|
||||
|
||||
export class Tracker extends Util.Singleton {
|
||||
static readonly GA_ID = "UA-159377374-1";
|
||||
static readonly SEGMENT_KEY = "YENwswyhlOgz8P7EFKUtIZ2MfON7Yxqb";
|
||||
protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = [];
|
||||
protected started = false;
|
||||
protected visitor: ua.Visitor;
|
||||
protected analytics: Analytics;
|
||||
protected machineId: string = null;
|
||||
protected ip: string = null;
|
||||
protected appVersion: string;
|
||||
protected locale: string;
|
||||
protected userAgent: string;
|
||||
protected anonymousId: string;
|
||||
protected os: string;
|
||||
protected disposers: IReactionDisposer[];
|
||||
|
||||
protected reportInterval: NodeJS.Timeout;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
this.anonymousId = machineIdSync();
|
||||
this.os = this.resolveOS();
|
||||
this.userAgent = `Lens ${App.version} (${this.os})`;
|
||||
|
||||
try {
|
||||
this.visitor = ua(Tracker.GA_ID, this.anonymousId, { strictCidFormat: false });
|
||||
} catch (error) {
|
||||
this.visitor = ua(Tracker.GA_ID);
|
||||
}
|
||||
|
||||
this.analytics = new Analytics(Tracker.SEGMENT_KEY, { flushAt: 1 });
|
||||
this.visitor.set("dl", "https://telemetry.k8slens.dev");
|
||||
this.visitor.set("ua", this.userAgent);
|
||||
this.disposers = [];
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.started === true) { return; }
|
||||
|
||||
this.started = true;
|
||||
|
||||
const handler = (ev: EventBus.AppEvent) => {
|
||||
this.event(ev.name, ev.action, ev.params);
|
||||
};
|
||||
|
||||
this.eventHandlers.push(handler);
|
||||
EventBus.appEventBus.addListener(handler);
|
||||
}
|
||||
|
||||
watchExtensions() {
|
||||
let previousExtensions = App.getEnabledExtensions();
|
||||
|
||||
this.disposers.push(reaction(() => App.getEnabledExtensions(), (currentExtensions) => {
|
||||
const removedExtensions = previousExtensions.filter(x => !currentExtensions.includes(x));
|
||||
|
||||
removedExtensions.forEach(ext => {
|
||||
this.event("extension", "disable", { extension: ext });
|
||||
});
|
||||
const newExtensions = currentExtensions.filter(x => !previousExtensions.includes(x));
|
||||
|
||||
newExtensions.forEach(ext => {
|
||||
this.event("extension", "enable", { extension: ext });
|
||||
});
|
||||
previousExtensions = currentExtensions;
|
||||
}, { equals: comparer.structural }));
|
||||
}
|
||||
|
||||
reportPeriodically() {
|
||||
this.reportData();
|
||||
this.reportInterval = setInterval(() => {
|
||||
this.reportData();
|
||||
}, 60 * 60 * 1000); // report every 1h
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (!this.started) { return; }
|
||||
|
||||
this.started = false;
|
||||
|
||||
for (const handler of this.eventHandlers) {
|
||||
EventBus.appEventBus.removeListener(handler);
|
||||
}
|
||||
|
||||
if (this.reportInterval) {
|
||||
clearInterval(this.reportInterval);
|
||||
}
|
||||
this.disposers.forEach(disposer => {
|
||||
disposer();
|
||||
});
|
||||
}
|
||||
|
||||
protected async isTelemetryAllowed(): Promise<boolean> {
|
||||
return telemetryPreferencesStore.enabled;
|
||||
}
|
||||
|
||||
protected reportData() {
|
||||
const clustersList = Catalog.catalogEntities.getItemsForApiKind<Catalog.KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster");
|
||||
|
||||
this.event("generic-data", "report", {
|
||||
appVersion: App.version,
|
||||
os: this.os,
|
||||
clustersCount: clustersList.length,
|
||||
extensions: App.getEnabledExtensions()
|
||||
});
|
||||
|
||||
clustersList.forEach((cluster) => {
|
||||
if (!cluster?.metadata.lastSeen) { return; }
|
||||
this.reportClusterData(cluster);
|
||||
});
|
||||
}
|
||||
|
||||
protected reportClusterData(cluster: Catalog.KubernetesCluster) {
|
||||
this.event("cluster-data", "report", {
|
||||
id: cluster.metadata.id,
|
||||
managed: cluster.metadata.source !== "local",
|
||||
kubernetesVersion: cluster.metadata.version,
|
||||
distribution: cluster.metadata.distribution,
|
||||
nodesCount: cluster.metadata.nodes,
|
||||
lastSeen: cluster.metadata.lastSeen,
|
||||
prometheus: cluster.metadata.prometheus
|
||||
});
|
||||
}
|
||||
|
||||
protected resolveOS() {
|
||||
let os = "";
|
||||
|
||||
if (App.isMac) {
|
||||
os = "MacOS";
|
||||
} else if(App.isWindows) {
|
||||
os = "Windows";
|
||||
} else if (App.isLinux) {
|
||||
os = "Linux";
|
||||
|
||||
if (App.isSnap) {
|
||||
os += "; Snap";
|
||||
} else {
|
||||
os += "; AppImage";
|
||||
}
|
||||
} else {
|
||||
os = "Unknown";
|
||||
}
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
||||
try {
|
||||
const allowed = await this.isTelemetryAllowed();
|
||||
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
this.visitor.event({
|
||||
ec: eventCategory,
|
||||
ea: eventAction,
|
||||
...otherParams,
|
||||
}).send();
|
||||
|
||||
this.analytics.track({
|
||||
anonymousId: this.anonymousId,
|
||||
event: `${eventCategory} ${eventAction}`,
|
||||
context: {
|
||||
userAgent: this.userAgent,
|
||||
},
|
||||
properties: {
|
||||
category: eventCategory,
|
||||
...otherParams,
|
||||
},
|
||||
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Failed to track "${eventCategory}:${eventAction}"`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tracker = Tracker.getInstance<Tracker>();
|
||||
@ -1,29 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"module": "CommonJS",
|
||||
"target": "ES2017",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"moduleResolution": "Node",
|
||||
"sourceMap": false,
|
||||
"declaration": false,
|
||||
"strict": false,
|
||||
"noImplicitAny": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"*": [
|
||||
"node_modules/*",
|
||||
"../../types/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"renderer.ts",
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
const path = require("path");
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
entry: "./main.ts",
|
||||
context: __dirname,
|
||||
target: "electron-main",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "main.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
{
|
||||
entry: "./renderer.tsx",
|
||||
context: __dirname,
|
||||
target: "electron-renderer",
|
||||
mode: "production",
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
],
|
||||
},
|
||||
externals: [
|
||||
{
|
||||
"@k8slens/extensions": "var global.LensExtensions",
|
||||
"react": "var global.React",
|
||||
"mobx": "var global.Mobx",
|
||||
"mobx-react": "var global.MobxReact"
|
||||
}
|
||||
],
|
||||
resolve: {
|
||||
extensions: [ ".tsx", ".ts", ".js" ],
|
||||
},
|
||||
output: {
|
||||
libraryTarget: "commonjs2",
|
||||
globalObject: "this",
|
||||
filename: "renderer.js",
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -43,7 +43,7 @@ describe("Lens integration tests", () => {
|
||||
|
||||
describe("preferences page", () => {
|
||||
// it('shows "preferences"', async () => {
|
||||
// const appName: string = process.platform === "darwin" ? "Lens" : "File";
|
||||
// const appName: string = process.platform === "darwin" ? "OpenLens" : "File";
|
||||
|
||||
// await app.mainProcess.send("test-menu-item-click", appName, "Preferences");
|
||||
// await app.client.waitUntilTextExists("[data-testid=application-header]", "APPLICATION");
|
||||
@ -62,8 +62,8 @@ describe("Lens integration tests", () => {
|
||||
it("ensures helm repos", async () => {
|
||||
const repos = await listHelmRepositories();
|
||||
|
||||
if (!repos[0]) {
|
||||
fail("Lens failed to add Bitnami repository");
|
||||
if (repos.length === 0) {
|
||||
fail("Lens failed to add any repositories");
|
||||
}
|
||||
|
||||
await app.client.elementClick("[data-testid=kube-tab]");
|
||||
|
||||
@ -3,9 +3,9 @@ import * as util from "util";
|
||||
import { exec } from "child_process";
|
||||
|
||||
const AppPaths: Partial<Record<NodeJS.Platform, string>> = {
|
||||
"win32": "./dist/win-unpacked/Lens.exe",
|
||||
"linux": "./dist/linux-unpacked/kontena-lens",
|
||||
"darwin": "./dist/mac/Lens.app/Contents/MacOS/Lens",
|
||||
"win32": "./dist/win-unpacked/OpenLens.exe",
|
||||
"linux": "./dist/linux-unpacked/open-lens",
|
||||
"darwin": "./dist/mac/OpenLens.app/Contents/MacOS/OpenLens",
|
||||
};
|
||||
|
||||
interface DoneCallback {
|
||||
@ -113,16 +113,14 @@ type HelmRepository = {
|
||||
url: string;
|
||||
};
|
||||
|
||||
export async function listHelmRepositories(retries = 0): Promise<HelmRepository[]>{
|
||||
if (retries < 5) {
|
||||
export async function listHelmRepositories(): Promise<HelmRepository[]>{
|
||||
for (let i = 0; i < 10; i += 1) {
|
||||
try {
|
||||
const { stdout: reposJson } = await promiseExec("helm repo list -o json");
|
||||
const { stdout } = await promiseExec("helm repo list -o json");
|
||||
|
||||
return JSON.parse(reposJson);
|
||||
return JSON.parse(stdout);
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository
|
||||
|
||||
return await listHelmRepositories((retries + 1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
42
package.json
42
package.json
@ -1,14 +1,13 @@
|
||||
{
|
||||
"name": "kontena-lens",
|
||||
"productName": "Lens",
|
||||
"description": "Lens - The Kubernetes IDE",
|
||||
"version": "5.0.0-alpha.1",
|
||||
"name": "open-lens",
|
||||
"productName": "OpenLens",
|
||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||
"version": "5.0.0-alpha.2",
|
||||
"main": "static/build/main.js",
|
||||
"copyright": "© 2021, Mirantis, Inc.",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Mirantis, Inc.",
|
||||
"email": "info@k8slens.dev"
|
||||
"name": "OpenLens Authors"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "concurrently -k \"yarn run dev-run -C\" yarn:dev:*",
|
||||
@ -23,9 +22,9 @@
|
||||
"compile:renderer": "yarn run webpack --config webpack.renderer.ts",
|
||||
"compile:extension-types": "yarn run webpack --config webpack.extensions.ts",
|
||||
"npm:fix-package-version": "yarn run ts-node build/set_npm_version.ts",
|
||||
"build:linux": "yarn run compile && electron-builder --linux --dir -c.productName=Lens",
|
||||
"build:mac": "yarn run compile && electron-builder --mac --dir -c.productName=Lens",
|
||||
"build:win": "yarn run compile && electron-builder --win --dir -c.productName=Lens",
|
||||
"build:linux": "yarn run compile && electron-builder --linux --dir",
|
||||
"build:mac": "yarn run compile && electron-builder --mac --dir",
|
||||
"build:win": "yarn run compile && electron-builder --win --dir",
|
||||
"integration": "jest --runInBand integration",
|
||||
"dist": "yarn run compile && electron-builder --publish onTag",
|
||||
"dist:win": "yarn run compile && electron-builder --publish onTag --x64 --ia32",
|
||||
@ -107,7 +106,6 @@
|
||||
"target": [
|
||||
"deb",
|
||||
"rpm",
|
||||
"snap",
|
||||
"AppImage"
|
||||
],
|
||||
"extraResources": [
|
||||
@ -161,16 +159,6 @@
|
||||
"oneClick": false,
|
||||
"allowToChangeInstallationDirectory": true
|
||||
},
|
||||
"snap": {
|
||||
"confinement": "classic"
|
||||
},
|
||||
"publish": [
|
||||
{
|
||||
"provider": "s3",
|
||||
"bucket": "lens-binaries",
|
||||
"path": "/ide"
|
||||
}
|
||||
],
|
||||
"protocols": {
|
||||
"name": "Lens Protocol Handler",
|
||||
"schemes": [
|
||||
@ -179,17 +167,6 @@
|
||||
"role": "Viewer"
|
||||
}
|
||||
},
|
||||
"lens": {
|
||||
"extensions": [
|
||||
"telemetry",
|
||||
"pod-menu",
|
||||
"node-menu",
|
||||
"metrics-cluster-feature",
|
||||
"license-menu-item",
|
||||
"kube-object-event-status",
|
||||
"survey"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@hapi/call": "^8.0.0",
|
||||
"@hapi/subtext": "^7.0.3",
|
||||
@ -224,6 +201,7 @@
|
||||
"mobx-react": "^6.2.2",
|
||||
"mock-fs": "^4.12.0",
|
||||
"moment": "^2.26.0",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"node-pty": "^0.10.0",
|
||||
"npm": "^6.14.8",
|
||||
"openid-client": "^3.15.2",
|
||||
|
||||
@ -4,8 +4,9 @@ import yaml from "js-yaml";
|
||||
import { Cluster } from "../../main/cluster";
|
||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||
const kubeconfig = `
|
||||
@ -47,10 +48,8 @@ jest.mock("electron", () => {
|
||||
};
|
||||
});
|
||||
|
||||
let clusterStore: ClusterStore;
|
||||
|
||||
describe("empty config", () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
ClusterStore.resetInstance();
|
||||
const mockOpts = {
|
||||
"tmp": {
|
||||
@ -59,9 +58,8 @@ describe("empty config", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
await ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -70,7 +68,7 @@ describe("empty config", () => {
|
||||
|
||||
describe("with foo cluster added", () => {
|
||||
beforeEach(() => {
|
||||
clusterStore.addCluster(
|
||||
ClusterStore.getInstance().addCluster(
|
||||
new Cluster({
|
||||
id: "foo",
|
||||
contextName: "foo",
|
||||
@ -85,7 +83,7 @@ describe("empty config", () => {
|
||||
});
|
||||
|
||||
it("adds new cluster to store", async () => {
|
||||
const storedCluster = clusterStore.getById("foo");
|
||||
const storedCluster = ClusterStore.getInstance().getById("foo");
|
||||
|
||||
expect(storedCluster.id).toBe("foo");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
|
||||
@ -94,19 +92,19 @@ describe("empty config", () => {
|
||||
});
|
||||
|
||||
it("removes cluster from store", async () => {
|
||||
await clusterStore.removeById("foo");
|
||||
expect(clusterStore.getById("foo")).toBeNull();
|
||||
await ClusterStore.getInstance().removeById("foo");
|
||||
expect(ClusterStore.getInstance().getById("foo")).toBeNull();
|
||||
});
|
||||
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.active.id).toBe("foo");
|
||||
ClusterStore.getInstance().setActive("foo");
|
||||
expect(ClusterStore.getInstance().active.id).toBe("foo");
|
||||
});
|
||||
});
|
||||
|
||||
describe("with prod and dev clusters added", () => {
|
||||
beforeEach(() => {
|
||||
clusterStore.addClusters(
|
||||
ClusterStore.getInstance().addClusters(
|
||||
new Cluster({
|
||||
id: "prod",
|
||||
contextName: "foo",
|
||||
@ -127,8 +125,8 @@ describe("empty config", () => {
|
||||
});
|
||||
|
||||
it("check if store can contain multiple clusters", () => {
|
||||
expect(clusterStore.hasClusters()).toBeTruthy();
|
||||
expect(clusterStore.clusters.size).toBe(2);
|
||||
expect(ClusterStore.getInstance().hasClusters()).toBeTruthy();
|
||||
expect(ClusterStore.getInstance().clusters.size).toBe(2);
|
||||
});
|
||||
|
||||
it("check if cluster's kubeconfig file saved", () => {
|
||||
@ -178,9 +176,8 @@ describe("config with existing clusters", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -188,24 +185,24 @@ describe("config with existing clusters", () => {
|
||||
});
|
||||
|
||||
it("allows to retrieve a cluster", () => {
|
||||
const storedCluster = clusterStore.getById("cluster1");
|
||||
const storedCluster = ClusterStore.getInstance().getById("cluster1");
|
||||
|
||||
expect(storedCluster.id).toBe("cluster1");
|
||||
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
|
||||
});
|
||||
|
||||
it("allows to delete a cluster", () => {
|
||||
clusterStore.removeById("cluster2");
|
||||
const storedCluster = clusterStore.getById("cluster1");
|
||||
ClusterStore.getInstance().removeById("cluster2");
|
||||
const storedCluster = ClusterStore.getInstance().getById("cluster1");
|
||||
|
||||
expect(storedCluster).toBeTruthy();
|
||||
const storedCluster2 = clusterStore.getById("cluster2");
|
||||
const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
|
||||
|
||||
expect(storedCluster2).toBeNull();
|
||||
});
|
||||
|
||||
it("allows getting all of the clusters", async () => {
|
||||
const storedClusters = clusterStore.clustersList;
|
||||
const storedClusters = ClusterStore.getInstance().clustersList;
|
||||
|
||||
expect(storedClusters.length).toBe(3);
|
||||
expect(storedClusters[0].id).toBe("cluster1");
|
||||
@ -216,7 +213,7 @@ describe("config with existing clusters", () => {
|
||||
});
|
||||
|
||||
it("marks owned cluster disabled by default", () => {
|
||||
const storedClusters = clusterStore.clustersList;
|
||||
const storedClusters = ClusterStore.getInstance().clustersList;
|
||||
|
||||
expect(storedClusters[0].enabled).toBe(true);
|
||||
expect(storedClusters[2].enabled).toBe(false);
|
||||
@ -276,9 +273,8 @@ users:
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -286,7 +282,7 @@ users:
|
||||
});
|
||||
|
||||
it("does not enable clusters with invalid kubeconfig", () => {
|
||||
const storedClusters = clusterStore.clustersList;
|
||||
const storedClusters = ClusterStore.getInstance().clustersList;
|
||||
|
||||
expect(storedClusters.length).toBe(2);
|
||||
expect(storedClusters[0].enabled).toBeFalsy;
|
||||
@ -319,9 +315,8 @@ describe("pre 2.0 config with an existing cluster", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -329,7 +324,7 @@ describe("pre 2.0 config with an existing cluster", () => {
|
||||
});
|
||||
|
||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
|
||||
|
||||
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[]`);
|
||||
});
|
||||
@ -347,16 +342,51 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
||||
}
|
||||
},
|
||||
cluster1: {
|
||||
kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n"
|
||||
kubeConfig: JSON.stringify({
|
||||
apiVersion: "v1",
|
||||
clusters: [{
|
||||
cluster: {
|
||||
server: "https://10.211.55.6:8443",
|
||||
},
|
||||
name: "minikube",
|
||||
}],
|
||||
contexts: [{
|
||||
context: {
|
||||
cluster: "minikube",
|
||||
user: "minikube",
|
||||
name: "minikube",
|
||||
},
|
||||
name: "minikube",
|
||||
}],
|
||||
"current-context": "minikube",
|
||||
kind: "Config",
|
||||
preferences: {},
|
||||
users: [{
|
||||
name: "minikube",
|
||||
user: {
|
||||
"client-certificate": "/Users/foo/.minikube/client.crt",
|
||||
"client-key": "/Users/foo/.minikube/client.key",
|
||||
"auth-provider": {
|
||||
config: {
|
||||
"access-token": [
|
||||
"should be string"
|
||||
],
|
||||
expiry: [
|
||||
"should be string"
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
}]
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -364,10 +394,12 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
|
||||
});
|
||||
|
||||
it("replaces array format access token and expiry into string", async () => {
|
||||
const file = clusterStore.clustersList[0].kubeConfigPath;
|
||||
const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
|
||||
const config = fs.readFileSync(file, "utf8");
|
||||
const kc = yaml.safeLoad(config);
|
||||
|
||||
console.log(kc);
|
||||
|
||||
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
|
||||
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
|
||||
});
|
||||
@ -397,9 +429,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -407,7 +438,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
||||
});
|
||||
|
||||
it("moves the icon into preferences", async () => {
|
||||
const storedClusterData = clusterStore.clustersList[0];
|
||||
const storedClusterData = ClusterStore.getInstance().clustersList[0];
|
||||
|
||||
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
||||
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
||||
@ -437,9 +468,8 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -474,9 +504,8 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
};
|
||||
|
||||
mockFs(mockOpts);
|
||||
clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
return clusterStore.load();
|
||||
return ClusterStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -484,13 +513,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
||||
});
|
||||
|
||||
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
||||
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
|
||||
|
||||
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
|
||||
});
|
||||
|
||||
it("migrates to modern format with icon not in file", async () => {
|
||||
const { icon } = clusterStore.clustersList[0].preferences;
|
||||
const { icon } = ClusterStore.getInstance().clustersList[0].preferences;
|
||||
|
||||
expect(icon.startsWith("data:;base64,")).toBe(true);
|
||||
});
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { appEventBus, AppEvent } from "../event-bus";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
describe("event bus tests", () => {
|
||||
describe("emit", () => {
|
||||
|
||||
34
src/common/__tests__/hotbar-store.test.ts
Normal file
34
src/common/__tests__/hotbar-store.test.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import mockFs from "mock-fs";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { HotbarStore } from "../hotbar-store";
|
||||
|
||||
describe("HotbarStore", () => {
|
||||
beforeEach(() => {
|
||||
ClusterStore.resetInstance();
|
||||
ClusterStore.createInstance();
|
||||
|
||||
HotbarStore.resetInstance();
|
||||
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe("load", () => {
|
||||
it("loads one hotbar by default", () => {
|
||||
HotbarStore.createInstance().load();
|
||||
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("add", () => {
|
||||
it("adds a hotbar", () => {
|
||||
const hotbarStore = HotbarStore.createInstance();
|
||||
|
||||
hotbarStore.load();
|
||||
hotbarStore.add({ name: "hottest" });
|
||||
expect(hotbarStore.hotbars.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,8 @@
|
||||
import { SearchStore } from "../search-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let searchStore: SearchStore = null;
|
||||
const logs = [
|
||||
|
||||
@ -10,7 +10,7 @@ jest.mock("electron", () => {
|
||||
getVersion: () => "99.99.99",
|
||||
getPath: () => "tmp",
|
||||
getLocale: () => "en",
|
||||
setLoginItemSettings: jest.fn(),
|
||||
setLoginItemSettings: (): void => void 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
@ -18,12 +18,19 @@ jest.mock("electron", () => {
|
||||
import { UserStore } from "../user-store";
|
||||
import { SemVer } from "semver";
|
||||
import electron from "electron";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
describe("user store tests", () => {
|
||||
describe("for an empty config", () => {
|
||||
beforeEach(() => {
|
||||
UserStore.resetInstance();
|
||||
mockFs({ tmp: { "config.json": "{}" } });
|
||||
mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } });
|
||||
|
||||
(UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve());
|
||||
|
||||
return UserStore.getInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -31,14 +38,14 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
it("allows setting and retrieving lastSeenAppVersion", () => {
|
||||
const us = UserStore.getInstance<UserStore>();
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
us.lastSeenAppVersion = "1.2.3";
|
||||
expect(us.lastSeenAppVersion).toBe("1.2.3");
|
||||
});
|
||||
|
||||
it("allows adding and listing seen contexts", () => {
|
||||
const us = UserStore.getInstance<UserStore>();
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
us.seenContexts.add("foo");
|
||||
expect(us.seenContexts.size).toBe(1);
|
||||
@ -51,7 +58,7 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
it("allows setting and getting preferences", () => {
|
||||
const us = UserStore.getInstance<UserStore>();
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
us.preferences.httpsProxy = "abcd://defg";
|
||||
|
||||
@ -63,7 +70,7 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
it("correctly resets theme to default value", async () => {
|
||||
const us = UserStore.getInstance<UserStore>();
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
us.isLoaded = true;
|
||||
|
||||
@ -73,7 +80,7 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
it("correctly calculates if the last seen version is an old release", () => {
|
||||
const us = UserStore.getInstance<UserStore>();
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
expect(us.isNewVersion).toBe(true);
|
||||
|
||||
@ -94,6 +101,8 @@ describe("user store tests", () => {
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return UserStore.createInstance().load();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -101,7 +110,7 @@ describe("user store tests", () => {
|
||||
});
|
||||
|
||||
it("sets last seen app version to 0.0.0", () => {
|
||||
const us = UserStore.getInstance<UserStore>();
|
||||
const us = UserStore.getInstance();
|
||||
|
||||
expect(us.lastSeenAppVersion).toBe("0.0.0");
|
||||
});
|
||||
|
||||
@ -3,8 +3,9 @@ import { observable } from "mobx";
|
||||
import { catalogCategoryRegistry } from "../catalog-category-registry";
|
||||
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
|
||||
import { clusterDisconnectHandler } from "../cluster-ipc";
|
||||
import { clusterStore } from "../cluster-store";
|
||||
import { ClusterStore } from "../cluster-store";
|
||||
import { requestMain } from "../ipc";
|
||||
import { productName } from "../vars";
|
||||
|
||||
export type KubernetesClusterSpec = {
|
||||
kubeconfigPath: string;
|
||||
@ -56,9 +57,9 @@ export class KubernetesCluster implements CatalogEntity {
|
||||
icon: "delete",
|
||||
title: "Delete",
|
||||
onlyVisibleForSource: "local",
|
||||
onClick: async () => clusterStore.removeById(this.metadata.uid),
|
||||
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
|
||||
confirm: {
|
||||
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
|
||||
message: `Remove Kubernetes Cluster "${this.metadata.name} from ${productName}?`
|
||||
}
|
||||
},
|
||||
];
|
||||
@ -68,7 +69,7 @@ export class KubernetesCluster implements CatalogEntity {
|
||||
icon: "link_off",
|
||||
title: "Disconnect",
|
||||
onClick: async () => {
|
||||
clusterStore.deactivate(this.metadata.uid);
|
||||
ClusterStore.getInstance().deactivate(this.metadata.uid);
|
||||
requestMain(clusterDisconnectHandler, this.metadata.uid);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { handleRequest } from "./ipc";
|
||||
import { ClusterId, clusterStore } from "./cluster-store";
|
||||
import { ClusterId, ClusterStore } from "./cluster-store";
|
||||
import { appEventBus } from "./event-bus";
|
||||
import { ResourceApplier } from "../main/resource-applier";
|
||||
import { ipcMain, IpcMainInvokeEvent } from "electron";
|
||||
@ -11,10 +11,9 @@ export const clusterRefreshHandler = "cluster:refresh";
|
||||
export const clusterDisconnectHandler = "cluster:disconnect";
|
||||
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
|
||||
|
||||
|
||||
if (ipcMain) {
|
||||
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
return cluster.activate(force);
|
||||
@ -22,7 +21,7 @@ if (ipcMain) {
|
||||
});
|
||||
|
||||
handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
|
||||
@ -32,14 +31,14 @@ if (ipcMain) {
|
||||
});
|
||||
|
||||
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) return cluster.refresh({ refreshMetadata: true });
|
||||
});
|
||||
|
||||
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
|
||||
appEventBus.emit({name: "cluster", action: "stop"});
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
cluster.disconnect();
|
||||
@ -49,7 +48,7 @@ if (ipcMain) {
|
||||
|
||||
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
|
||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
const cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
const applier = new ResourceApplier(cluster);
|
||||
|
||||
@ -112,7 +112,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
private static stateRequestChannel = "cluster:states";
|
||||
|
||||
private constructor() {
|
||||
constructor() {
|
||||
super({
|
||||
configName: "lens-cluster-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
@ -337,8 +337,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
|
||||
|
||||
export function getClusterIdFromHost(host: string): ClusterId | undefined {
|
||||
// e.g host == "%clusterId.localhost:45345"
|
||||
const subDomains = host.split(":")[0].split(".");
|
||||
@ -355,5 +353,5 @@ export function getHostedClusterId() {
|
||||
}
|
||||
|
||||
export function getHostedCluster(): Cluster {
|
||||
return clusterStore.getById(getHostedClusterId());
|
||||
return ClusterStore.getInstance().getById(getHostedClusterId());
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { action, comparer, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/hotbar-store";
|
||||
import * as uuid from "uuid";
|
||||
|
||||
export interface HotbarItem {
|
||||
entity: {
|
||||
@ -12,18 +13,27 @@ export interface HotbarItem {
|
||||
}
|
||||
|
||||
export interface Hotbar {
|
||||
id: string;
|
||||
name: string;
|
||||
items: HotbarItem[];
|
||||
}
|
||||
|
||||
export interface HotbarCreateOptions {
|
||||
id?: string;
|
||||
name: string;
|
||||
items?: HotbarItem[];
|
||||
}
|
||||
|
||||
export interface HotbarStoreModel {
|
||||
hotbars: Hotbar[];
|
||||
activeHotbarId: string;
|
||||
}
|
||||
|
||||
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
@observable hotbars: Hotbar[] = [];
|
||||
@observable private _activeHotbarId: string;
|
||||
|
||||
private constructor() {
|
||||
constructor() {
|
||||
super({
|
||||
configName: "lens-hotbar-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
@ -34,28 +44,103 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
});
|
||||
}
|
||||
|
||||
get activeHotbarId() {
|
||||
return this._activeHotbarId;
|
||||
}
|
||||
|
||||
set activeHotbarId(id: string) {
|
||||
if (this.getById(id)) {
|
||||
this._activeHotbarId = id;
|
||||
}
|
||||
}
|
||||
|
||||
get activeHotbarIndex() {
|
||||
return this.hotbars.findIndex((hotbar) => hotbar.id === this.activeHotbarId);
|
||||
}
|
||||
|
||||
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
|
||||
this.hotbars = data.hotbars || [{
|
||||
name: "default",
|
||||
if (data.hotbars?.length === 0) {
|
||||
this.hotbars = [{
|
||||
id: uuid.v4(),
|
||||
name: "Default",
|
||||
items: []
|
||||
}];
|
||||
} else {
|
||||
this.hotbars = data.hotbars;
|
||||
}
|
||||
|
||||
if (data.activeHotbarId) {
|
||||
if (this.getById(data.activeHotbarId)) {
|
||||
this.activeHotbarId = data.activeHotbarId;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.activeHotbarId) {
|
||||
this.activeHotbarId = this.hotbars[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
getActive() {
|
||||
return this.getById(this.activeHotbarId);
|
||||
}
|
||||
|
||||
getByName(name: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.name === name);
|
||||
}
|
||||
|
||||
add(hotbar: Hotbar) {
|
||||
this.hotbars.push(hotbar);
|
||||
getById(id: string) {
|
||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||
}
|
||||
|
||||
add(data: HotbarCreateOptions) {
|
||||
const {
|
||||
id = uuid.v4(),
|
||||
items = [],
|
||||
name,
|
||||
} = data;
|
||||
|
||||
const hotbar = { id, name, items };
|
||||
|
||||
this.hotbars.push(hotbar as Hotbar);
|
||||
|
||||
return hotbar as Hotbar;
|
||||
}
|
||||
|
||||
@action
|
||||
remove(hotbar: Hotbar) {
|
||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||
|
||||
if (this.activeHotbarId === hotbar.id) {
|
||||
this.activeHotbarId = this.hotbars[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
switchToPrevious() {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
let index = hotbarStore.activeHotbarIndex - 1;
|
||||
|
||||
if (index < 0) {
|
||||
index = hotbarStore.hotbars.length - 1;
|
||||
}
|
||||
|
||||
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
|
||||
}
|
||||
|
||||
switchToNext() {
|
||||
const hotbarStore = HotbarStore.getInstance();
|
||||
let index = hotbarStore.activeHotbarIndex + 1;
|
||||
|
||||
if (index >= hotbarStore.hotbars.length) {
|
||||
index = 0;
|
||||
}
|
||||
|
||||
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
|
||||
}
|
||||
|
||||
toJSON(): HotbarStoreModel {
|
||||
const model: HotbarStoreModel = {
|
||||
hotbars: this.hotbars
|
||||
hotbars: this.hotbars,
|
||||
activeHotbarId: this.activeHotbarId
|
||||
};
|
||||
|
||||
return toJS(model, {
|
||||
@ -63,5 +148,3 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();
|
||||
|
||||
@ -5,8 +5,8 @@ import { pathToRegexp } from "path-to-regexp";
|
||||
import logger from "../../main/logger";
|
||||
import Url from "url-parse";
|
||||
import { RoutingError, RoutingErrorType } from "./error";
|
||||
import { extensionsStore } from "../../extensions/extensions-store";
|
||||
import { extensionLoader } from "../../extensions/extension-loader";
|
||||
import { ExtensionsStore } from "../../extensions/extensions-store";
|
||||
import { ExtensionLoader } from "../../extensions/extension-loader";
|
||||
import { LensExtension } from "../../extensions/lens-extension";
|
||||
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
|
||||
|
||||
@ -72,7 +72,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
||||
|
||||
/**
|
||||
* find the most specific matching handler and call it
|
||||
* @param routes the array of (path schemas, handler) paris to match against
|
||||
* @param routes the array of (path schemas, handler) pairs to match against
|
||||
* @param url the url (in its current state)
|
||||
*/
|
||||
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
|
||||
@ -124,7 +124,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
||||
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||
const name = [publisher, partialName].filter(Boolean).join("/");
|
||||
|
||||
const extension = extensionLoader.userExtensionsByName.get(name);
|
||||
const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name);
|
||||
|
||||
if (!extension) {
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
|
||||
@ -132,7 +132,7 @@ export abstract class LensProtocolRouter extends Singleton {
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!extensionsStore.isEnabled(extension.id)) {
|
||||
if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
|
||||
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||
|
||||
return name;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import request from "request";
|
||||
import requestPromise from "request-promise-native";
|
||||
import { userStore } from "./user-store";
|
||||
import { UserStore } from "./user-store";
|
||||
|
||||
// todo: get rid of "request" (deprecated)
|
||||
// https://github.com/lensapp/lens/issues/459
|
||||
|
||||
function getDefaultRequestOpts(): Partial<request.Options> {
|
||||
const { httpsProxy, allowUntrustedCAs } = userStore.preferences;
|
||||
const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance().preferences;
|
||||
|
||||
return {
|
||||
proxy: httpsProxy || undefined,
|
||||
|
||||
@ -3,6 +3,7 @@ import { app, remote } from "electron";
|
||||
import semver from "semver";
|
||||
import { readFile } from "fs-extra";
|
||||
import { action, computed, observable, reaction, toJS } from "mobx";
|
||||
import moment from "moment-timezone";
|
||||
import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/user-store";
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
@ -23,6 +24,7 @@ export interface UserPreferences {
|
||||
httpsProxy?: string;
|
||||
shell?: string;
|
||||
colorTheme?: string;
|
||||
localeTimezone?: string;
|
||||
allowUntrustedCAs?: boolean;
|
||||
allowTelemetry?: boolean;
|
||||
downloadMirror?: string | "default";
|
||||
@ -36,7 +38,7 @@ export interface UserPreferences {
|
||||
export class UserStore extends BaseStore<UserStoreModel> {
|
||||
static readonly defaultTheme: ThemeId = "lens-dark";
|
||||
|
||||
private constructor() {
|
||||
constructor() {
|
||||
super({
|
||||
configName: "lens-user-store",
|
||||
migrations,
|
||||
@ -54,6 +56,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
allowTelemetry: true,
|
||||
allowUntrustedCAs: false,
|
||||
colorTheme: UserStore.defaultTheme,
|
||||
localeTimezone: moment.tz.guess(true) || "UTC",
|
||||
downloadMirror: "default",
|
||||
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
|
||||
openAtLogin: false,
|
||||
@ -130,6 +133,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
this.lastSeenAppVersion = getAppVersion();
|
||||
}
|
||||
|
||||
@action
|
||||
setLocaleTimezone(tz: string) {
|
||||
this.preferences.localeTimezone = tz;
|
||||
}
|
||||
|
||||
protected refreshNewContexts = async () => {
|
||||
try {
|
||||
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
|
||||
@ -155,14 +163,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
this.newContexts.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Getting default directory to download kubectl binaries
|
||||
* @returns string
|
||||
*/
|
||||
getDefaultKubectlPath(): string {
|
||||
return path.join((app || remote.app).getPath("userData"), "binaries");
|
||||
}
|
||||
|
||||
@action
|
||||
protected async fromStore(data: Partial<UserStoreModel> = {}) {
|
||||
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
|
||||
@ -192,4 +192,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
}
|
||||
}
|
||||
|
||||
export const userStore = UserStore.getInstance<UserStore>();
|
||||
/**
|
||||
* Getting default directory to download kubectl binaries
|
||||
* @returns string
|
||||
*/
|
||||
export function getDefaultKubectlPath(): string {
|
||||
return path.join((app || remote.app).getPath("userData"), "binaries");
|
||||
}
|
||||
|
||||
@ -9,10 +9,6 @@ export function getBundledKubectlVersion(): string {
|
||||
return packageInfo.config.bundledKubectlVersion;
|
||||
}
|
||||
|
||||
export function getBundledExtensions(): string[] {
|
||||
return packageInfo.lens?.extensions || [];
|
||||
}
|
||||
|
||||
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
|
||||
const response = await requestPromise({
|
||||
method: "GET",
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
return (...params: any[]) => new Promise(resolve => {
|
||||
return (...params: F) => new Promise(resolve => {
|
||||
clearTimeout(timer);
|
||||
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
|
||||
timer = global.setTimeout(() => resolve(func(...params)), timeout);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,29 +1,66 @@
|
||||
/**
|
||||
* Narrowing class instances to the one.
|
||||
* Use "private" or "protected" modifier for constructor (when overriding) to disallow "new" usage.
|
||||
*
|
||||
* @example
|
||||
* const usersStore: UsersStore = UsersStore.getInstance();
|
||||
*/
|
||||
type StaticThis<T, R extends any[]> = { new(...args: R): T };
|
||||
|
||||
type Constructor<T = {}> = new (...args: any[]) => T;
|
||||
|
||||
class Singleton {
|
||||
export class Singleton {
|
||||
private static instances = new WeakMap<object, Singleton>();
|
||||
private static creating = "";
|
||||
|
||||
// todo: improve types inferring
|
||||
static getInstance<T>(...args: ConstructorParameters<Constructor<T>>): T {
|
||||
constructor() {
|
||||
if (Singleton.creating.length === 0) {
|
||||
throw new TypeError("A singleton class must be created by createInstance()");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the single instance of the child class if one was not already created.
|
||||
*
|
||||
* Multiple calls will return the same instance.
|
||||
* Essentially throwing away the arguments to the subsequent calls.
|
||||
*
|
||||
* Note: this is a racy function, if two (or more) calls are racing to call this function
|
||||
* only the first's arguments will be used.
|
||||
* @param this Implicit argument that is the child class type
|
||||
* @param args The constructor arguments for the child class
|
||||
* @returns An instance of the child class
|
||||
*/
|
||||
static createInstance<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
|
||||
if (!Singleton.instances.has(this)) {
|
||||
Singleton.instances.set(this, Reflect.construct(this, args));
|
||||
if (Singleton.creating.length > 0) {
|
||||
throw new TypeError("Cannot create a second singleton while creating a first");
|
||||
}
|
||||
|
||||
Singleton.creating = this.name;
|
||||
Singleton.instances.set(this, new this(...args));
|
||||
Singleton.creating = "";
|
||||
}
|
||||
|
||||
return Singleton.instances.get(this) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the instance of the child class that was previously created.
|
||||
* @param this Implicit argument that is the child class type
|
||||
* @param strict If false will return `undefined` instead of throwing when an instance doesn't exist.
|
||||
* Default: `true`
|
||||
* @returns An instance of the child class
|
||||
*/
|
||||
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
|
||||
if (!Singleton.instances.has(this) && strict) {
|
||||
throw new TypeError(`instance of ${this.name} is not created`);
|
||||
}
|
||||
|
||||
return Singleton.instances.get(this) as (T | undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the instance of the child class.
|
||||
*
|
||||
* Note: this doesn't prevent callers of `getInstance` from storing the result in a global.
|
||||
*
|
||||
* There is *no* way in JS or TS to prevent globals like that.
|
||||
*/
|
||||
static resetInstance() {
|
||||
Singleton.instances.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
export { Singleton };
|
||||
export default Singleton;
|
||||
|
||||
@ -11,7 +11,9 @@ export const isSnap = !!process.env.SNAP;
|
||||
export const isProduction = process.env.NODE_ENV === "production";
|
||||
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
||||
export const isDevelopment = !isTestEnv && !isProduction;
|
||||
export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
|
||||
|
||||
export const productName = packageInfo.productName;
|
||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||
export const publicPath = "/build/";
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { watch } from "chokidar";
|
||||
import { join, normalize } from "path";
|
||||
import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
jest.mock("../../common/ipc");
|
||||
jest.mock("fs-extra");
|
||||
@ -17,6 +18,12 @@ jest.mock("../extension-installer", () => ({
|
||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
beforeEach(() => {
|
||||
ExtensionDiscovery.resetInstance();
|
||||
ExtensionsStore.resetInstance();
|
||||
ExtensionsStore.createInstance();
|
||||
});
|
||||
|
||||
it("emits add for added extension", async done => {
|
||||
globalThis.__non_webpack_require__.mockImplementation(() => ({
|
||||
name: "my-extension"
|
||||
@ -36,7 +43,7 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any
|
||||
);
|
||||
const extensionDiscovery = new ExtensionDiscovery();
|
||||
const extensionDiscovery = ExtensionDiscovery.createInstance();
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
@ -76,7 +83,7 @@ describe("ExtensionDiscovery", () => {
|
||||
mockedWatch.mockImplementationOnce(() =>
|
||||
(mockWatchInstance) as any
|
||||
);
|
||||
const extensionDiscovery = new ExtensionDiscovery();
|
||||
const extensionDiscovery = ExtensionDiscovery.createInstance();
|
||||
|
||||
// Need to force isLoaded to be true so that the file watching is started
|
||||
extensionDiscovery.isLoaded = true;
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import { ExtensionLoader } from "../extension-loader";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const manifestPath = "manifest/path";
|
||||
const manifestPath2 = "manifest/path2";
|
||||
const manifestPath3 = "manifest/path3";
|
||||
|
||||
jest.mock("../extensions-store", () => ({
|
||||
extensionsStore: {
|
||||
ExtensionsStore: {
|
||||
getInstance: () => ({
|
||||
whenLoaded: Promise.resolve(true),
|
||||
mergeState: jest.fn()
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
@ -99,8 +105,12 @@ jest.mock(
|
||||
);
|
||||
|
||||
describe("ExtensionLoader", () => {
|
||||
it("renderer updates extension after ipc broadcast", async (done) => {
|
||||
const extensionLoader = new ExtensionLoader();
|
||||
beforeEach(() => {
|
||||
ExtensionLoader.resetInstance();
|
||||
});
|
||||
|
||||
it.only("renderer updates extension after ipc broadcast", async (done) => {
|
||||
const extensionLoader = ExtensionLoader.createInstance();
|
||||
|
||||
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
|
||||
|
||||
@ -140,20 +150,20 @@ describe("ExtensionLoader", () => {
|
||||
});
|
||||
|
||||
it("updates ExtensionsStore after isEnabled is changed", async () => {
|
||||
(extensionsStore.mergeState as any).mockClear();
|
||||
(ExtensionsStore.getInstance().mergeState as any).mockClear();
|
||||
|
||||
// Disable sending events in this test
|
||||
(ipcRenderer.on as any).mockImplementation();
|
||||
|
||||
const extensionLoader = new ExtensionLoader();
|
||||
const extensionLoader = ExtensionLoader.createInstance();
|
||||
|
||||
await extensionLoader.init();
|
||||
|
||||
expect(extensionsStore.mergeState).not.toHaveBeenCalled();
|
||||
expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
|
||||
|
||||
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
|
||||
|
||||
expect(extensionsStore.mergeState).toHaveBeenCalledWith({
|
||||
expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({
|
||||
"manifest/path": {
|
||||
enabled: false,
|
||||
name: "TestExtension"
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { LensExtension } from "../lens-extension";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import logger from "../main/logger";
|
||||
import { app } from "electron";
|
||||
import { requestMain } from "../common/ipc";
|
||||
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
|
||||
export interface ClusterFeatureStatus {
|
||||
/** feature's current version, as set by the implementation */
|
||||
@ -86,7 +86,7 @@ export abstract class ClusterFeature {
|
||||
protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
|
||||
let resources: string[];
|
||||
|
||||
const clusterModel = clusterStore.getById(cluster.metadata.uid);
|
||||
const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid);
|
||||
|
||||
if (!clusterModel) {
|
||||
throw new Error(`cluster not found`);
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
export const version = getAppVersion();
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
|
||||
export function getEnabledExtensions(): string[] {
|
||||
return extensionsStore.enabledExtensions;
|
||||
return ExtensionsStore.getInstance().enabledExtensions;
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
|
||||
import { computed } from "mobx";
|
||||
import { CatalogEntity } from "../../common/catalog-entity";
|
||||
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry";
|
||||
|
||||
@ -7,7 +6,7 @@ export { catalogCategoryRegistry as catalogCategories } from "../../common/catal
|
||||
export * from "../../common/catalog-entities";
|
||||
|
||||
export class CatalogEntityRegistry {
|
||||
@computed getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
|
||||
return registry.getItemsForApiKind<T>(apiVersion, kind);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,10 @@ import { observable, reaction, toJS, when } from "mobx";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { getBundledExtensions } from "../common/utils/app-version";
|
||||
import { Singleton } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||
|
||||
export interface InstalledExtension {
|
||||
@ -51,7 +51,7 @@ const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymb
|
||||
* - "add": When extension is added. The event is of type InstalledExtension
|
||||
* - "remove": When extension is removed. The event is of type LensExtensionId
|
||||
*/
|
||||
export class ExtensionDiscovery {
|
||||
export class ExtensionDiscovery extends Singleton {
|
||||
protected bundledFolderPath: string;
|
||||
|
||||
private loadStarted = false;
|
||||
@ -67,6 +67,7 @@ export class ExtensionDiscovery {
|
||||
public events: EventEmitter;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.events = new EventEmitter();
|
||||
}
|
||||
|
||||
@ -136,7 +137,7 @@ export class ExtensionDiscovery {
|
||||
depth: 1,
|
||||
ignoreInitial: true,
|
||||
// Try to wait until the file has been completely copied.
|
||||
// The OS might emit an event for added file even it's not completely written to the filesysten.
|
||||
// The OS might emit an event for added file even it's not completely written to the file-system.
|
||||
awaitWriteFinish: {
|
||||
// Wait 300ms until the file size doesn't change to consider the file written.
|
||||
// For a small file like package.json this should be plenty of time.
|
||||
@ -236,7 +237,7 @@ export class ExtensionDiscovery {
|
||||
/**
|
||||
* Uninstalls extension.
|
||||
* The application will detect the folder unlink and remove the extension from the UI automatically.
|
||||
* @param extension Extension to unistall.
|
||||
* @param extension Extension to uninstall.
|
||||
*/
|
||||
async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
|
||||
logger.info(`${logModule} Uninstalling ${manifest.name}`);
|
||||
@ -326,7 +327,7 @@ export class ExtensionDiscovery {
|
||||
manifestJson = __non_webpack_require__(manifestPath);
|
||||
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
|
||||
|
||||
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath);
|
||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
||||
|
||||
return {
|
||||
id: installedManifestPath,
|
||||
@ -348,7 +349,7 @@ export class ExtensionDiscovery {
|
||||
|
||||
await this.installBundledPackages(this.packageJsonPath, bundledExtensions);
|
||||
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath);
|
||||
const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name));
|
||||
|
||||
for (const extension of userExtensions) {
|
||||
if (await fs.pathExists(extension.manifestPath) === false) {
|
||||
@ -382,14 +383,9 @@ export class ExtensionDiscovery {
|
||||
async loadBundledExtensions() {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const folderPath = this.bundledFolderPath;
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
const paths = await fs.readdir(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
if (!bundledExtensions.includes(fileName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true });
|
||||
|
||||
@ -402,8 +398,7 @@ export class ExtensionDiscovery {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const bundledExtensions = getBundledExtensions();
|
||||
async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise<InstalledExtension[]> {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const paths = await fs.readdir(folderPath);
|
||||
|
||||
@ -462,5 +457,3 @@ export class ExtensionDiscovery {
|
||||
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionDiscovery = new ExtensionDiscovery();
|
||||
|
||||
@ -5,9 +5,10 @@ import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import path from "path";
|
||||
import { getHostedCluster } from "../common/cluster-store";
|
||||
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
|
||||
import { Singleton } from "../common/utils";
|
||||
import logger from "../main/logger";
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { extensionsStore } from "./extensions-store";
|
||||
import { ExtensionsStore } from "./extensions-store";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
|
||||
import type { LensMainExtension } from "./lens-main-extension";
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||
@ -24,7 +25,7 @@ const logModule = "[EXTENSIONS-LOADER]";
|
||||
/**
|
||||
* Loads installed extensions to the Lens application
|
||||
*/
|
||||
export class ExtensionLoader {
|
||||
export class ExtensionLoader extends Singleton {
|
||||
protected extensions = observable.map<LensExtensionId, InstalledExtension>();
|
||||
protected instances = observable.map<LensExtensionId, LensExtension>();
|
||||
|
||||
@ -95,11 +96,11 @@ export class ExtensionLoader {
|
||||
await this.initMain();
|
||||
}
|
||||
|
||||
await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]);
|
||||
await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
|
||||
|
||||
// save state on change `extension.isEnabled`
|
||||
reaction(() => this.storeState, extensionsState => {
|
||||
extensionsStore.mergeState(extensionsState);
|
||||
ExtensionsStore.getInstance().mergeState(extensionsState);
|
||||
});
|
||||
}
|
||||
|
||||
@ -329,5 +330,3 @@ export class ExtensionLoader {
|
||||
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionLoader = new ExtensionLoader();
|
||||
|
||||
@ -53,5 +53,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionsStore = new ExtensionsStore();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { InstalledExtension } from "./extension-discovery";
|
||||
import { action, observable, reaction } from "mobx";
|
||||
import { filesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||
import logger from "../main/logger";
|
||||
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
|
||||
|
||||
@ -49,7 +49,7 @@ export class LensExtension {
|
||||
* folder name.
|
||||
*/
|
||||
async getExtensionFileFolder(): Promise<string> {
|
||||
return filesystemProvisionerStore.requestDirectory(this.id);
|
||||
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
|
||||
}
|
||||
|
||||
get description() {
|
||||
|
||||
@ -10,7 +10,7 @@ export class LensMainExtension extends LensExtension {
|
||||
appMenus: MenuRegistration[] = [];
|
||||
|
||||
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
|
||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||
const windowManager = WindowManager.getInstance();
|
||||
const pageUrl = getExtensionPageUrl({
|
||||
extensionId: this.name,
|
||||
pageId,
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "@k8slens/extensions",
|
||||
"productName": "Lens extensions",
|
||||
"description": "Lens - The Kubernetes IDE: extensions",
|
||||
"productName": "OpenLens extensions",
|
||||
"description": "OpenLens - Open Source Kubernetes IDE: extensions",
|
||||
"version": "0.0.0",
|
||||
"copyright": "© 2021, Mirantis, Inc.",
|
||||
"copyright": "© 2021 OpenLens Authors",
|
||||
"license": "MIT",
|
||||
"main": "dist/src/extensions/extension-api.js",
|
||||
"types": "dist/src/extensions/extension-api.d.ts",
|
||||
@ -13,8 +13,7 @@
|
||||
"dist/**/*.js"
|
||||
],
|
||||
"author": {
|
||||
"name": "Mirantis, Inc.",
|
||||
"email": "info@k8slens.dev"
|
||||
"name": "OpenLens Authors"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
|
||||
import { LensExtension } from "../../lens-extension";
|
||||
import React from "react";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
let ext: LensExtension = null;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { themeStore } from "../../renderer/theme.store";
|
||||
import { ThemeStore } from "../../renderer/theme.store";
|
||||
|
||||
export function getActiveTheme() {
|
||||
return themeStore.activeTheme;
|
||||
return ThemeStore.getInstance().activeTheme;
|
||||
}
|
||||
|
||||
@ -23,7 +23,6 @@ jest.mock("winston", () => ({
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
jest.mock("../../common/ipc");
|
||||
jest.mock("../context-handler");
|
||||
jest.mock("request");
|
||||
|
||||
@ -36,6 +36,11 @@ import { bundledKubectlPath, Kubectl } from "../kubectl";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { waitUntilUsed } from "tcp-port-used";
|
||||
import { Readable } from "stream";
|
||||
import { UserStore } from "../../common/user-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
|
||||
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
||||
@ -44,6 +49,8 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilU
|
||||
describe("kube auth proxy tests", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
UserStore.resetInstance();
|
||||
UserStore.createInstance();
|
||||
});
|
||||
|
||||
it("calling exit multiple times shouldn't throw", async () => {
|
||||
|
||||
@ -36,10 +36,6 @@ import * as path from "path";
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
|
||||
describe("kubeconfig manager tests", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
const mockOpts = {
|
||||
"minikube-config.yml": JSON.stringify({
|
||||
@ -76,11 +72,11 @@ describe("kubeconfig manager tests", () => {
|
||||
const cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml"
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
const contextHandler = new ContextHandler(cluster);
|
||||
const port = await getFreePort();
|
||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||
const kubeConfManager = new KubeconfigManager(cluster, contextHandler, port);
|
||||
|
||||
expect(logger.error).not.toBeCalled();
|
||||
expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`);
|
||||
@ -98,17 +94,19 @@ describe("kubeconfig manager tests", () => {
|
||||
const cluster = new Cluster({
|
||||
id: "foo",
|
||||
contextName: "minikube",
|
||||
kubeConfigPath: "minikube-config.yml"
|
||||
kubeConfigPath: "minikube-config.yml",
|
||||
});
|
||||
const contextHandler = new ContextHandler(cluster);
|
||||
const port = await getFreePort();
|
||||
const kubeConfManager = await KubeconfigManager.create(cluster, contextHandler, port);
|
||||
const kubeConfManager = new KubeconfigManager(cluster, contextHandler, port);
|
||||
const configPath = await kubeConfManager.getPath();
|
||||
|
||||
expect(await fse.pathExists(configPath)).toBe(true);
|
||||
await kubeConfManager.unlink();
|
||||
expect(await fse.pathExists(configPath)).toBe(false);
|
||||
await kubeConfManager.unlink(); // doesn't throw
|
||||
expect(await kubeConfManager.getPath()).toBeUndefined();
|
||||
expect(async () => {
|
||||
await kubeConfManager.getPath();
|
||||
}).rejects.toThrow("already unlinked");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { autoUpdater, UpdateInfo } from "electron-updater";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment, isTestEnv } from "../common/vars";
|
||||
import { isDevelopment, isPublishConfigured, isTestEnv } from "../common/vars";
|
||||
import { delay } from "../common/utils";
|
||||
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
||||
import { once } from "lodash";
|
||||
@ -8,6 +8,10 @@ import { app, ipcMain } from "electron";
|
||||
|
||||
let installVersion: null | string = null;
|
||||
|
||||
export function isAutoUpdateEnabled() {
|
||||
return autoUpdater.isUpdaterActive() && isPublishConfigured;
|
||||
}
|
||||
|
||||
function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
|
||||
if (arg.doUpdate) {
|
||||
if (arg.now) {
|
||||
|
||||
@ -2,7 +2,7 @@ import "../common/cluster-ipc";
|
||||
import type http from "http";
|
||||
import { ipcMain } from "electron";
|
||||
import { action, autorun, observable, reaction, toJS } from "mobx";
|
||||
import { clusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
|
||||
import { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
@ -21,7 +21,7 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource);
|
||||
// auto-init clusters
|
||||
reaction(() => clusterStore.enabledClustersList, (clusters) => {
|
||||
reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => {
|
||||
clusters.forEach((cluster) => {
|
||||
if (!cluster.initialized && !cluster.initializing) {
|
||||
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
|
||||
@ -31,8 +31,8 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
}, { fireImmediately: true });
|
||||
|
||||
reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => {
|
||||
this.updateCatalogSource(clusterStore.enabledClustersList);
|
||||
reaction(() => toJS(ClusterStore.getInstance().enabledClustersList, { recurseEverything: true }), () => {
|
||||
this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList);
|
||||
}, { fireImmediately: true });
|
||||
|
||||
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
|
||||
@ -42,14 +42,14 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
// auto-stop removed clusters
|
||||
autorun(() => {
|
||||
const removedClusters = Array.from(clusterStore.removedClusters.values());
|
||||
const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values());
|
||||
|
||||
if (removedClusters.length > 0) {
|
||||
const meta = removedClusters.map(cluster => cluster.getMeta());
|
||||
|
||||
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
|
||||
removedClusters.forEach(cluster => cluster.disconnect());
|
||||
clusterStore.removedClusters.clear();
|
||||
ClusterStore.getInstance().removedClusters.clear();
|
||||
}
|
||||
}, {
|
||||
delay: 250
|
||||
@ -90,10 +90,10 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
@action syncClustersFromCatalog(entities: KubernetesCluster[]) {
|
||||
entities.filter((entity) => entity.metadata.source !== "local").forEach((entity: KubernetesCluster) => {
|
||||
const cluster = clusterStore.getById(entity.metadata.uid);
|
||||
const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
|
||||
|
||||
if (!cluster) {
|
||||
clusterStore.addCluster({
|
||||
ClusterStore.getInstance().addCluster({
|
||||
id: entity.metadata.uid,
|
||||
enabled: true,
|
||||
ownerRef: clusterOwnerRef,
|
||||
@ -145,7 +145,7 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
protected onNetworkOffline() {
|
||||
logger.info("[CLUSTER-MANAGER]: network is offline");
|
||||
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||
ClusterStore.getInstance().enabledClustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.online = false;
|
||||
cluster.accessible = false;
|
||||
@ -156,7 +156,7 @@ export class ClusterManager extends Singleton {
|
||||
|
||||
protected onNetworkOnline() {
|
||||
logger.info("[CLUSTER-MANAGER]: network is online");
|
||||
clusterStore.enabledClustersList.forEach((cluster) => {
|
||||
ClusterStore.getInstance().enabledClustersList.forEach((cluster) => {
|
||||
if (!cluster.disconnected) {
|
||||
cluster.refreshConnectionStatus().catch((e) => e);
|
||||
}
|
||||
@ -164,7 +164,7 @@ export class ClusterManager extends Singleton {
|
||||
}
|
||||
|
||||
stop() {
|
||||
clusterStore.clusters.forEach((cluster: Cluster) => {
|
||||
ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => {
|
||||
cluster.disconnect();
|
||||
});
|
||||
}
|
||||
@ -176,18 +176,18 @@ export class ClusterManager extends Singleton {
|
||||
if (req.headers.host.startsWith("127.0.0.1")) {
|
||||
const clusterId = req.url.split("/")[1];
|
||||
|
||||
cluster = clusterStore.getById(clusterId);
|
||||
cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
|
||||
if (cluster) {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
|
||||
}
|
||||
} else if (req.headers["x-cluster-id"]) {
|
||||
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString());
|
||||
cluster = ClusterStore.getInstance().getById(req.headers["x-cluster-id"].toString());
|
||||
} else {
|
||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||
|
||||
cluster = clusterStore.getById(clusterId);
|
||||
cluster = ClusterStore.getInstance().getById(clusterId);
|
||||
}
|
||||
|
||||
return cluster;
|
||||
|
||||
@ -294,7 +294,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
try {
|
||||
this.initializing = true;
|
||||
this.contextHandler = new ContextHandler(this);
|
||||
this.kubeconfigManager = await KubeconfigManager.create(this, this.contextHandler, port);
|
||||
this.kubeconfigManager = new KubeconfigManager(this, this.contextHandler, port);
|
||||
this.kubeProxyUrl = `http://localhost:${port}${apiKubePrefix}`;
|
||||
this.initialized = true;
|
||||
logger.info(`[CLUSTER]: "${this.contextName}" init success`, {
|
||||
|
||||
@ -4,11 +4,12 @@ import logger from "./logger";
|
||||
* Installs Electron developer tools in the development build.
|
||||
* The dependency is not bundled to the production build.
|
||||
*/
|
||||
export const installDeveloperTools = async () => {
|
||||
export const installDeveloperTools = () => {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
logger.info("🤓 Installing developer tools");
|
||||
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer");
|
||||
|
||||
return devToolsInstaller([REACT_DEVELOPER_TOOLS]);
|
||||
import("electron-devtools-installer")
|
||||
.then(({ default: devToolsInstaller, REACT_DEVELOPER_TOOLS }) => devToolsInstaller([REACT_DEVELOPER_TOOLS]))
|
||||
.then((name) => logger.info(`[DEVTOOLS-INSTALLER]: installed ${name}`))
|
||||
.catch(error => logger.error(`[DEVTOOLS-INSTALLER]: failed`, { error }));
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,10 +4,14 @@ import { appEventBus } from "../common/event-bus";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import logger from "./logger";
|
||||
|
||||
|
||||
export function exitApp() {
|
||||
const windowManager = WindowManager.getInstance<WindowManager>();
|
||||
const clusterManager = ClusterManager.getInstance<ClusterManager>();
|
||||
console.log("before windowManager");
|
||||
const windowManager = WindowManager.getInstance(false);
|
||||
|
||||
console.log("before clusterManager");
|
||||
const clusterManager = ClusterManager.getInstance(false);
|
||||
|
||||
console.log("after clusterManager");
|
||||
|
||||
appEventBus.emit({ name: "service", action: "close" });
|
||||
windowManager?.hide();
|
||||
|
||||
@ -14,7 +14,7 @@ interface FSProvisionModel {
|
||||
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
||||
@observable registeredExtensions = observable.map<LensExtensionId, string>();
|
||||
|
||||
private constructor() {
|
||||
constructor() {
|
||||
super({
|
||||
configName: "lens-filesystem-provisioner-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
@ -56,5 +56,3 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const filesystemProvisionerStore = FilesystemProvisionerStore.getInstance<FilesystemProvisionerStore>();
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
import { helmService } from "../helm-service";
|
||||
import { repoManager } from "../helm-repo-manager";
|
||||
import { HelmRepoManager } from "../helm-repo-manager";
|
||||
|
||||
jest.spyOn(repoManager, "init").mockImplementation();
|
||||
const mockHelmRepoManager = jest.spyOn(HelmRepoManager, "getInstance").mockImplementation();
|
||||
|
||||
jest.mock("../helm-chart-manager");
|
||||
|
||||
describe("Helm Service tests", () => {
|
||||
test("list charts without deprecated ones", async () => {
|
||||
jest.spyOn(repoManager, "repositories").mockImplementation(async () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("list charts without deprecated ones", async () => {
|
||||
mockHelmRepoManager.mockReturnValue({
|
||||
init: jest.fn(),
|
||||
repositories: jest.fn().mockImplementation(async () => {
|
||||
return [
|
||||
{ name: "stable", url: "stableurl" },
|
||||
{ name: "experiment", url: "experimenturl" }
|
||||
{ name: "experiment", url: "experimenturl" },
|
||||
];
|
||||
}),
|
||||
});
|
||||
|
||||
const charts = await helmService.listCharts();
|
||||
@ -55,11 +62,14 @@ describe("Helm Service tests", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("list charts sorted by version in descending order", async () => {
|
||||
jest.spyOn(repoManager, "repositories").mockImplementation(async () => {
|
||||
it("list charts sorted by version in descending order", async () => {
|
||||
mockHelmRepoManager.mockReturnValue({
|
||||
init: jest.fn(),
|
||||
repositories: jest.fn().mockImplementation(async () => {
|
||||
return [
|
||||
{ name: "bitnami", url: "bitnamiurl" }
|
||||
{ name: "bitnami", url: "bitnamiurl" },
|
||||
];
|
||||
}),
|
||||
});
|
||||
|
||||
const charts = await helmService.listCharts();
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import * as tempy from "tempy";
|
||||
import fs from "fs";
|
||||
import fse from "fs-extra";
|
||||
import * as yaml from "js-yaml";
|
||||
import { promiseExec} from "../promise-exec";
|
||||
import { helmCli } from "./helm-cli";
|
||||
import { Cluster } from "../cluster";
|
||||
import { toCamelCase } from "../../common/utils/camelCase";
|
||||
|
||||
export class HelmReleaseManager {
|
||||
|
||||
public async listReleases(pathToKubeconfig: string, namespace?: string) {
|
||||
export async function listReleases(pathToKubeconfig: string, namespace?: string) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces";
|
||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
try {
|
||||
const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`);
|
||||
const output = JSON.parse(stdout);
|
||||
|
||||
if (output.length == 0) {
|
||||
@ -22,14 +22,17 @@ export class HelmReleaseManager {
|
||||
});
|
||||
|
||||
return output;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){
|
||||
export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string){
|
||||
const helm = await helmCli.binaryPath();
|
||||
const fileName = tempy.file({name: "values.yaml"});
|
||||
|
||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||
await fse.writeFile(fileName, yaml.safeDump(values));
|
||||
|
||||
try {
|
||||
let generateName = "";
|
||||
@ -38,7 +41,7 @@ export class HelmReleaseManager {
|
||||
generateName = "--generate-name";
|
||||
name = "";
|
||||
}
|
||||
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);});
|
||||
const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`);
|
||||
const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
|
||||
|
||||
return {
|
||||
@ -48,80 +51,103 @@ export class HelmReleaseManager {
|
||||
namespace
|
||||
}
|
||||
};
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} finally {
|
||||
await fs.promises.unlink(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){
|
||||
const helm = await helmCli.binaryPath();
|
||||
const fileName = tempy.file({name: "values.yaml"});
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
await fs.promises.writeFile(fileName, yaml.safeDump(values));
|
||||
|
||||
try {
|
||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
return {
|
||||
log: stdout,
|
||||
release: this.getRelease(name, namespace, cluster)
|
||||
};
|
||||
} finally {
|
||||
await fs.promises.unlink(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
public async getRelease(name: string, namespace: string, cluster: Cluster) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
const release = JSON.parse(stdout);
|
||||
|
||||
release.resources = await this.getResources(name, namespace, cluster);
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
public async deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public async getValues(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --all --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);});
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
protected async getResources(name: string, namespace: string, cluster: Cluster) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const kubectl = await cluster.kubeCtl.getPath();
|
||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => {
|
||||
return { stdout: JSON.stringify({items: []})};
|
||||
});
|
||||
|
||||
return stdout;
|
||||
await fse.unlink(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
export const releaseManager = new HelmReleaseManager();
|
||||
export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const fileName = tempy.file({name: "values.yaml"});
|
||||
|
||||
await fse.writeFile(fileName, yaml.safeDump(values));
|
||||
|
||||
try {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
|
||||
|
||||
return {
|
||||
log: stdout,
|
||||
release: getRelease(name, namespace, cluster)
|
||||
};
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
} finally {
|
||||
await fse.unlink(fileName);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRelease(name: string, namespace: string, cluster: Cluster) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
|
||||
const release = JSON.parse(stdout);
|
||||
|
||||
release.resources = await getResources(name, namespace, cluster);
|
||||
|
||||
return release;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
|
||||
return stdout;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
|
||||
return stdout;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
|
||||
return JSON.parse(stdout);
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
}
|
||||
}
|
||||
|
||||
export async function rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||
|
||||
return stdout;
|
||||
} catch ({ stderr }) {
|
||||
throw stderr;
|
||||
}
|
||||
}
|
||||
|
||||
async function getResources(name: string, namespace: string, cluster: Cluster) {
|
||||
try {
|
||||
const helm = await helmCli.binaryPath();
|
||||
const kubectl = await cluster.kubeCtl.getPath();
|
||||
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
|
||||
|
||||
return stdout;
|
||||
} catch {
|
||||
return { stdout: JSON.stringify({ items: [] }) };
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,10 +77,6 @@ export class HelmRepoManager extends Singleton {
|
||||
}
|
||||
|
||||
public async repositories(): Promise<HelmRepo[]> {
|
||||
if (!this.initialized) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
try {
|
||||
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
|
||||
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
|
||||
@ -160,5 +156,3 @@ export class HelmRepoManager extends Singleton {
|
||||
return stdout;
|
||||
}
|
||||
}
|
||||
|
||||
export const repoManager = HelmRepoManager.getInstance<HelmRepoManager>();
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
import semver from "semver";
|
||||
import { Cluster } from "../cluster";
|
||||
import logger from "../logger";
|
||||
import { repoManager } from "./helm-repo-manager";
|
||||
import { HelmRepoManager } from "./helm-repo-manager";
|
||||
import { HelmChartManager } from "./helm-chart-manager";
|
||||
import { releaseManager } from "./helm-release-manager";
|
||||
import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api";
|
||||
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
|
||||
|
||||
class HelmService {
|
||||
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
|
||||
return installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async listCharts() {
|
||||
const charts: HelmChartList = {};
|
||||
|
||||
await repoManager.init();
|
||||
const repositories = await repoManager.repositories();
|
||||
const repositories = await HelmRepoManager.getInstance().repositories();
|
||||
|
||||
for (const repo of repositories) {
|
||||
charts[repo.name] = {};
|
||||
@ -36,7 +34,7 @@ class HelmService {
|
||||
readme: "",
|
||||
versions: {}
|
||||
};
|
||||
const repo = await repoManager.repository(repoName);
|
||||
const repo = await HelmRepoManager.getInstance().repository(repoName);
|
||||
const chartManager = new HelmChartManager(repo);
|
||||
const chart = await chartManager.chart(chartName);
|
||||
|
||||
@ -47,31 +45,30 @@ class HelmService {
|
||||
}
|
||||
|
||||
public async getChartValues(repoName: string, chartName: string, version = "") {
|
||||
const repo = await repoManager.repository(repoName);
|
||||
const repo = await HelmRepoManager.getInstance().repository(repoName);
|
||||
const chartManager = new HelmChartManager(repo);
|
||||
|
||||
return chartManager.getValues(chartName, version);
|
||||
}
|
||||
|
||||
public async listReleases(cluster: Cluster, namespace: string = null) {
|
||||
await repoManager.init();
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
return await releaseManager.listReleases(proxyKubeconfig, namespace);
|
||||
return listReleases(proxyKubeconfig, namespace);
|
||||
}
|
||||
|
||||
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
logger.debug("Fetch release");
|
||||
|
||||
return await releaseManager.getRelease(releaseName, namespace, cluster);
|
||||
return getRelease(releaseName, namespace, cluster);
|
||||
}
|
||||
|
||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("Fetch release values");
|
||||
|
||||
return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig);
|
||||
return getValues(releaseName, namespace, all, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
@ -79,7 +76,7 @@ class HelmService {
|
||||
|
||||
logger.debug("Fetch release history");
|
||||
|
||||
return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig);
|
||||
return getHistory(releaseName, namespace, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
|
||||
@ -87,20 +84,20 @@ class HelmService {
|
||||
|
||||
logger.debug("Delete release");
|
||||
|
||||
return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig);
|
||||
return deleteRelease(releaseName, namespace, proxyKubeconfig);
|
||||
}
|
||||
|
||||
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
|
||||
logger.debug("Upgrade release");
|
||||
|
||||
return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
||||
return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster);
|
||||
}
|
||||
|
||||
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
|
||||
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
|
||||
|
||||
logger.debug("Rollback release");
|
||||
const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig);
|
||||
const output = rollback(releaseName, namespace, revision, proxyKubeconfig);
|
||||
|
||||
return { message: output };
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import "../common/prometheus-providers";
|
||||
import * as Mobx from "mobx";
|
||||
import * as LensExtensions from "../extensions/core-api";
|
||||
import { app, autoUpdater, ipcMain, dialog, powerMonitor } from "electron";
|
||||
import { appName, isMac } from "../common/vars";
|
||||
import { appName, isMac, productName } from "../common/vars";
|
||||
import path from "path";
|
||||
import { LensProxy } from "./lens-proxy";
|
||||
import { WindowManager } from "./window-manager";
|
||||
@ -15,15 +15,15 @@ import { getFreePort } from "./port";
|
||||
import { mangleProxyEnv } from "./proxy-env";
|
||||
import { registerFileProtocol } from "../common/register-protocol";
|
||||
import logger from "./logger";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { ClusterStore } from "../common/cluster-store";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import { appEventBus } from "../common/event-bus";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { extensionsStore } from "../extensions/extensions-store";
|
||||
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
|
||||
import { ExtensionLoader } from "../extensions/extension-loader";
|
||||
import { ExtensionsStore } from "../extensions/extensions-store";
|
||||
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
|
||||
import type { LensExtensionId } from "../extensions/lens-extension";
|
||||
import { FilesystemProvisionerStore } from "./extension-filesystem";
|
||||
import { installDeveloperTools } from "./developer-tools";
|
||||
import { filesystemProvisionerStore } from "./extension-filesystem";
|
||||
import { LensProtocolRouterMain } from "./protocol-handler";
|
||||
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
|
||||
import { bindBroadcastHandlers } from "../common/ipc";
|
||||
@ -31,22 +31,18 @@ import { startUpdateChecking } from "./app-updater";
|
||||
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
|
||||
import { CatalogPusher } from "./catalog-pusher";
|
||||
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
|
||||
import { hotbarStore } from "../common/hotbar-store";
|
||||
import { HotbarStore } from "../common/hotbar-store";
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
let proxyPort: number;
|
||||
let proxyServer: LensProxy;
|
||||
let clusterManager: ClusterManager;
|
||||
let windowManager: WindowManager;
|
||||
|
||||
app.setName(appName);
|
||||
|
||||
logger.info("📟 Setting Lens as protocol client for lens://");
|
||||
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
|
||||
|
||||
if (app.setAsDefaultProtocolClient("lens")) {
|
||||
logger.info("📟 succeeded ✅");
|
||||
logger.info("📟 Protocol client register succeeded ✅");
|
||||
} else {
|
||||
logger.info("📟 failed ❗");
|
||||
logger.info("📟 Protocol client register failed ❗");
|
||||
}
|
||||
|
||||
if (!process.env.CICD) {
|
||||
@ -66,7 +62,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.exit();
|
||||
} else {
|
||||
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||
const lprm = LensProtocolRouterMain.createInstance();
|
||||
|
||||
for (const arg of process.argv) {
|
||||
if (arg.toLowerCase().startsWith("lens://")) {
|
||||
@ -77,7 +73,7 @@ if (!app.requestSingleInstanceLock()) {
|
||||
}
|
||||
|
||||
app.on("second-instance", (event, argv) => {
|
||||
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||
const lprm = LensProtocolRouterMain.createInstance();
|
||||
|
||||
for (const arg of argv) {
|
||||
if (arg.toLowerCase().startsWith("lens://")) {
|
||||
@ -86,11 +82,11 @@ app.on("second-instance", (event, argv) => {
|
||||
}
|
||||
}
|
||||
|
||||
windowManager?.ensureMainWindow();
|
||||
WindowManager.getInstance(false)?.ensureMainWindow();
|
||||
});
|
||||
|
||||
app.on("ready", async () => {
|
||||
logger.info(`🚀 Starting Lens from "${workingDir}"`);
|
||||
logger.info(`🚀 Starting ${productName} from "${workingDir}"`);
|
||||
logger.info("🐚 Syncing shell environment");
|
||||
await shellSync();
|
||||
|
||||
@ -102,7 +98,11 @@ app.on("ready", async () => {
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
await installDeveloperTools();
|
||||
const userStore = UserStore.createInstance();
|
||||
const clusterStore = ClusterStore.createInstance();
|
||||
const hotbarStore = HotbarStore.createInstance();
|
||||
const extensionsStore = ExtensionsStore.createInstance();
|
||||
const filesystemStore = FilesystemProvisionerStore.createInstance();
|
||||
|
||||
logger.info("💾 Loading stores");
|
||||
// preload
|
||||
@ -111,37 +111,38 @@ app.on("ready", async () => {
|
||||
clusterStore.load(),
|
||||
hotbarStore.load(),
|
||||
extensionsStore.load(),
|
||||
filesystemProvisionerStore.load(),
|
||||
filesystemStore.load(),
|
||||
]);
|
||||
|
||||
// find free port
|
||||
try {
|
||||
logger.info("🔑 Getting free port for LensProxy server");
|
||||
proxyPort = await getFreePort();
|
||||
const proxyPort = await getFreePort();
|
||||
|
||||
// create cluster manager
|
||||
ClusterManager.createInstance(proxyPort);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy");
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// create cluster manager
|
||||
clusterManager = ClusterManager.getInstance<ClusterManager>(proxyPort);
|
||||
const clusterManager = ClusterManager.getInstance();
|
||||
|
||||
// run proxy
|
||||
try {
|
||||
logger.info("🔌 Starting LensProxy");
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
proxyServer = LensProxy.create(proxyPort, clusterManager);
|
||||
LensProxy.createInstance(clusterManager.port).listen();
|
||||
} catch (error) {
|
||||
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error?.message}`);
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error?.message || "unknown error"}`);
|
||||
logger.error(`Could not start proxy (127.0.0:${clusterManager.port}): ${error?.message}`);
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${clusterManager.port}): ${error?.message || "unknown error"}`);
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// test proxy connection
|
||||
try {
|
||||
logger.info("🔎 Testing LensProxy connection ...");
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(proxyPort);
|
||||
const versionFromProxy = await getAppVersionFromProxyServer(clusterManager.port);
|
||||
|
||||
if (getAppVersion() !== versionFromProxy) {
|
||||
logger.error(`Proxy server responded with invalid response`);
|
||||
@ -151,7 +152,9 @@ app.on("ready", async () => {
|
||||
logger.error("Checking proxy server connection failed", error);
|
||||
}
|
||||
|
||||
extensionLoader.init();
|
||||
const extensionDiscovery = ExtensionDiscovery.createInstance();
|
||||
|
||||
ExtensionLoader.createInstance().init();
|
||||
extensionDiscovery.init();
|
||||
|
||||
// Start the app without showing the main window when auto starting on login
|
||||
@ -159,7 +162,9 @@ app.on("ready", async () => {
|
||||
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
|
||||
|
||||
logger.info("🖥️ Starting WindowManager");
|
||||
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
||||
const windowManager = WindowManager.createInstance(clusterManager.port);
|
||||
|
||||
installDeveloperTools();
|
||||
|
||||
if (!startHidden) {
|
||||
windowManager.initMainWindow();
|
||||
@ -169,13 +174,13 @@ app.on("ready", async () => {
|
||||
CatalogPusher.init(catalogEntityRegistry);
|
||||
startUpdateChecking();
|
||||
LensProtocolRouterMain
|
||||
.getInstance<LensProtocolRouterMain>()
|
||||
.getInstance()
|
||||
.rendererLoaded = true;
|
||||
});
|
||||
|
||||
extensionLoader.whenLoaded.then(() => {
|
||||
ExtensionLoader.getInstance().whenLoaded.then(() => {
|
||||
LensProtocolRouterMain
|
||||
.getInstance<LensProtocolRouterMain>()
|
||||
.getInstance()
|
||||
.extensionsLoaded = true;
|
||||
});
|
||||
|
||||
@ -189,14 +194,15 @@ app.on("ready", async () => {
|
||||
extensionDiscovery.watchExtensions();
|
||||
|
||||
// Subscribe to extensions that are copied or deleted to/from the extensions folder
|
||||
extensionDiscovery.events.on("add", (extension: InstalledExtension) => {
|
||||
extensionLoader.addExtension(extension);
|
||||
});
|
||||
extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||
extensionLoader.removeExtension(lensExtensionId);
|
||||
extensionDiscovery.events
|
||||
.on("add", (extension: InstalledExtension) => {
|
||||
ExtensionLoader.getInstance().addExtension(extension);
|
||||
})
|
||||
.on("remove", (lensExtensionId: LensExtensionId) => {
|
||||
ExtensionLoader.getInstance().removeExtension(lensExtensionId);
|
||||
});
|
||||
|
||||
extensionLoader.initExtensions(extensions);
|
||||
ExtensionLoader.getInstance().initExtensions(extensions);
|
||||
} catch (error) {
|
||||
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
|
||||
console.error(error);
|
||||
@ -212,7 +218,7 @@ app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info("APP:ACTIVATE", { hasVisibleWindows });
|
||||
|
||||
if (!hasVisibleWindows) {
|
||||
windowManager?.initMainWindow(false);
|
||||
WindowManager.getInstance(false)?.initMainWindow(false);
|
||||
}
|
||||
});
|
||||
|
||||
@ -227,8 +233,7 @@ app.on("will-quit", (event) => {
|
||||
// Quit app on Cmd+Q (MacOS)
|
||||
logger.info("APP:QUIT");
|
||||
appEventBus.emit({name: "app", action: "close"});
|
||||
|
||||
clusterManager?.stop(); // close cluster connections
|
||||
ClusterManager.getInstance(false)?.stop(); // close cluster connections
|
||||
|
||||
if (blockQuit) {
|
||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||
@ -242,7 +247,7 @@ app.on("open-url", (event, rawUrl) => {
|
||||
event.preventDefault();
|
||||
|
||||
LensProtocolRouterMain
|
||||
.getInstance<LensProtocolRouterMain>()
|
||||
.getInstance()
|
||||
.route(rawUrl)
|
||||
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl }));
|
||||
});
|
||||
|
||||
@ -9,16 +9,39 @@ import logger from "./logger";
|
||||
|
||||
export class KubeconfigManager {
|
||||
protected configDir = app.getPath("temp");
|
||||
protected tempFile: string;
|
||||
protected tempFile: string = null;
|
||||
|
||||
private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
|
||||
constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { }
|
||||
|
||||
static async create(cluster: Cluster, contextHandler: ContextHandler, port: number) {
|
||||
const kcm = new KubeconfigManager(cluster, contextHandler, port);
|
||||
async getPath(): Promise<string> {
|
||||
if (this.tempFile === undefined) {
|
||||
throw new Error("kubeconfig is already unlinked");
|
||||
}
|
||||
|
||||
await kcm.init();
|
||||
if (!this.tempFile) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
return kcm;
|
||||
// create proxy kubeconfig if it is removed without unlink called
|
||||
if (!(await fs.pathExists(this.tempFile))) {
|
||||
try {
|
||||
this.tempFile = await this.createProxyKubeconfig();
|
||||
} catch (err) {
|
||||
logger.error(`Failed to created temp config for auth-proxy`, { err });
|
||||
}
|
||||
}
|
||||
|
||||
return this.tempFile;
|
||||
}
|
||||
|
||||
async unlink() {
|
||||
if (!this.tempFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Deleting temporary kubeconfig: ${this.tempFile}`);
|
||||
await fs.unlink(this.tempFile);
|
||||
this.tempFile = undefined;
|
||||
}
|
||||
|
||||
protected async init() {
|
||||
@ -30,20 +53,6 @@ export class KubeconfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
async getPath() {
|
||||
// create proxy kubeconfig if it is removed
|
||||
if (this.tempFile !== undefined && !(await fs.pathExists(this.tempFile))) {
|
||||
try {
|
||||
this.tempFile = await this.createProxyKubeconfig();
|
||||
} catch (err) {
|
||||
logger.error(`Failed to created temp config for auth-proxy`, { err });
|
||||
}
|
||||
}
|
||||
|
||||
return this.tempFile;
|
||||
|
||||
}
|
||||
|
||||
protected resolveProxyUrl() {
|
||||
return `http://127.0.0.1:${this.port}/${this.cluster.id}`;
|
||||
}
|
||||
@ -87,14 +96,4 @@ export class KubeconfigManager {
|
||||
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
async unlink() {
|
||||
if (!this.tempFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Deleting temporary kubeconfig: ${this.tempFile}`);
|
||||
await fs.unlink(this.tempFile);
|
||||
this.tempFile = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import logger from "./logger";
|
||||
import { ensureDir, pathExists } from "fs-extra";
|
||||
import * as lockFile from "proper-lockfile";
|
||||
import { helmCli } from "./helm/helm-cli";
|
||||
import { userStore } from "../common/user-store";
|
||||
import { UserStore } from "../common/user-store";
|
||||
import { customRequest } from "../common/request";
|
||||
import { getBundledKubectlVersion } from "../common/utils/app-version";
|
||||
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||
@ -113,12 +113,12 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
public getPathFromPreferences() {
|
||||
return userStore.preferences?.kubectlBinariesPath || this.getBundledPath();
|
||||
return UserStore.getInstance().preferences?.kubectlBinariesPath || this.getBundledPath();
|
||||
}
|
||||
|
||||
protected getDownloadDir() {
|
||||
if (userStore.preferences?.downloadBinariesPath) {
|
||||
return path.join(userStore.preferences.downloadBinariesPath, "kubectl");
|
||||
if (UserStore.getInstance().preferences?.downloadBinariesPath) {
|
||||
return path.join(UserStore.getInstance().preferences.downloadBinariesPath, "kubectl");
|
||||
}
|
||||
|
||||
return Kubectl.kubectlDir;
|
||||
@ -129,7 +129,7 @@ export class Kubectl {
|
||||
return this.getBundledPath();
|
||||
}
|
||||
|
||||
if (userStore.preferences?.downloadKubectlBinaries === false) {
|
||||
if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) {
|
||||
return this.getPathFromPreferences();
|
||||
}
|
||||
|
||||
@ -223,7 +223,7 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
public async ensureKubectl(): Promise<boolean> {
|
||||
if (userStore.preferences?.downloadKubectlBinaries === false) {
|
||||
if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -273,7 +273,7 @@ export class Kubectl {
|
||||
|
||||
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const stream = customRequest({
|
||||
url: this.url,
|
||||
gzip: true,
|
||||
@ -303,7 +303,7 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
protected async writeInitScripts() {
|
||||
const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
|
||||
const kubectlPath = UserStore.getInstance().preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
|
||||
const helmPath = helmCli.getBinaryDir();
|
||||
const fsPromises = fs.promises;
|
||||
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
||||
@ -361,7 +361,7 @@ export class Kubectl {
|
||||
}
|
||||
|
||||
protected getDownloadMirror() {
|
||||
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
|
||||
const mirror = packageMirrors.get(UserStore.getInstance().preferences?.downloadMirror);
|
||||
|
||||
if (mirror) {
|
||||
return mirror;
|
||||
|
||||
@ -6,23 +6,22 @@ import url from "url";
|
||||
import * as WebSocket from "ws";
|
||||
import { apiPrefix, apiKubePrefix } from "../common/vars";
|
||||
import { Router } from "./router";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import { ContextHandler } from "./context-handler";
|
||||
import logger from "./logger";
|
||||
import { NodeShellSession, LocalShellSession } from "./shell-session";
|
||||
import { Singleton } from "../common/utils";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
|
||||
export class LensProxy {
|
||||
export class LensProxy extends Singleton {
|
||||
protected origin: string;
|
||||
protected proxyServer: http.Server;
|
||||
protected router: Router;
|
||||
protected closed = false;
|
||||
protected retryCounters = new Map<string, number>();
|
||||
|
||||
static create(port: number, clusterManager: ClusterManager) {
|
||||
return new LensProxy(port, clusterManager).listen();
|
||||
}
|
||||
constructor(protected port: number) {
|
||||
super();
|
||||
|
||||
private constructor(protected port: number, protected clusterManager: ClusterManager) {
|
||||
this.origin = `http://localhost:${port}`;
|
||||
this.router = new Router();
|
||||
}
|
||||
@ -66,7 +65,7 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||
@ -171,7 +170,7 @@ export class LensProxy {
|
||||
const ws = new WebSocket.Server({ noServer: true });
|
||||
|
||||
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
const nodeParam = url.parse(req.url, true).query["node"]?.toString();
|
||||
const shell = nodeParam
|
||||
? new NodeShellSession(socket, cluster, nodeParam)
|
||||
@ -197,7 +196,7 @@ export class LensProxy {
|
||||
}
|
||||
|
||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
|
||||
|
||||
if (cluster) {
|
||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain, IpcMainEvent, Menu, MenuItem, MenuItemConstructorOptions, webContents, shell } from "electron";
|
||||
import { autorun } from "mobx";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl } from "../common/vars";
|
||||
import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl, productName } from "../common/vars";
|
||||
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
|
||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||
@ -34,7 +34,7 @@ export function showAbout(browserWindow: BrowserWindow) {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
message: productName,
|
||||
detail: appInfo.join("\r\n")
|
||||
});
|
||||
}
|
||||
@ -55,7 +55,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{
|
||||
label: "About Lens",
|
||||
label: `About ${productName}`,
|
||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||
showAbout(browserWindow);
|
||||
}
|
||||
@ -220,7 +220,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
},
|
||||
...ignoreOnMac([
|
||||
{
|
||||
label: "About Lens",
|
||||
label: `About ${productName}`,
|
||||
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
|
||||
showAbout(browserWindow);
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { LensProtocolRouterMain } from "../router";
|
||||
import { noop } from "../../../common/utils";
|
||||
import { extensionsStore } from "../../../extensions/extensions-store";
|
||||
import { extensionLoader } from "../../../extensions/extension-loader";
|
||||
import { ExtensionsStore } from "../../../extensions/extensions-store";
|
||||
import { ExtensionLoader } from "../../../extensions/extension-loader";
|
||||
import * as uuid from "uuid";
|
||||
import { LensMainExtension } from "../../../extensions/core-api";
|
||||
import { broadcastMessage } from "../../../common/ipc";
|
||||
@ -16,20 +16,28 @@ function throwIfDefined(val: any): void {
|
||||
}
|
||||
|
||||
describe("protocol router tests", () => {
|
||||
let lpr: LensProtocolRouterMain;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(extensionsStore as any).state.clear();
|
||||
(extensionLoader as any).instances.clear();
|
||||
LensProtocolRouterMain.resetInstance();
|
||||
lpr = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
|
||||
ExtensionsStore.createInstance();
|
||||
ExtensionLoader.createInstance();
|
||||
|
||||
const lpr = LensProtocolRouterMain.createInstance();
|
||||
|
||||
lpr.extensionsLoaded = true;
|
||||
lpr.rendererLoaded = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
ExtensionsStore.resetInstance();
|
||||
ExtensionLoader.resetInstance();
|
||||
LensProtocolRouterMain.resetInstance();
|
||||
});
|
||||
|
||||
it("should throw on non-lens URLS", async () => {
|
||||
try {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
|
||||
expect(await lpr.route("https://google.ca")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
@ -38,6 +46,8 @@ describe("protocol router tests", () => {
|
||||
|
||||
it("should throw when host not internal or extension", async () => {
|
||||
try {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
|
||||
expect(await lpr.route("lens://foobar")).toBeUndefined();
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
@ -57,14 +67,15 @@ describe("protocol router tests", () => {
|
||||
isEnabled: true,
|
||||
absolutePath: "/foo/bar",
|
||||
});
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
|
||||
ext.protocolHandlers.push({
|
||||
pathSchema: "/",
|
||||
handler: noop,
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
|
||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
|
||||
|
||||
lpr.addInternalHandler("/", noop);
|
||||
|
||||
@ -86,6 +97,7 @@ describe("protocol router tests", () => {
|
||||
});
|
||||
|
||||
it("should call handler if matches", async () => {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
let called = false;
|
||||
|
||||
lpr.addInternalHandler("/page", () => { called = true; });
|
||||
@ -101,6 +113,7 @@ describe("protocol router tests", () => {
|
||||
});
|
||||
|
||||
it("should call most exact handler", async () => {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
let called: any = 0;
|
||||
|
||||
lpr.addInternalHandler("/page", () => { called = 1; });
|
||||
@ -119,6 +132,7 @@ describe("protocol router tests", () => {
|
||||
it("should call most exact handler for an extension", async () => {
|
||||
let called: any = 0;
|
||||
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
const extId = uuid.v4();
|
||||
const ext = new LensMainExtension({
|
||||
id: extId,
|
||||
@ -141,8 +155,8 @@ describe("protocol router tests", () => {
|
||||
handler: params => { called = params.pathname.id; },
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
|
||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
|
||||
try {
|
||||
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined();
|
||||
@ -155,6 +169,7 @@ describe("protocol router tests", () => {
|
||||
});
|
||||
|
||||
it("should work with non-org extensions", async () => {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
let called: any = 0;
|
||||
|
||||
{
|
||||
@ -177,8 +192,8 @@ describe("protocol router tests", () => {
|
||||
handler: params => { called = params.pathname.id; },
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
|
||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
|
||||
}
|
||||
|
||||
{
|
||||
@ -201,12 +216,12 @@ describe("protocol router tests", () => {
|
||||
handler: () => { called = 1; },
|
||||
});
|
||||
|
||||
(extensionLoader as any).instances.set(extId, ext);
|
||||
(extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" });
|
||||
(ExtensionLoader.getInstance() as any).instances.set(extId, ext);
|
||||
(ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "icecream" });
|
||||
}
|
||||
|
||||
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||
(extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||
(ExtensionsStore.getInstance() as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" });
|
||||
(ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" });
|
||||
|
||||
try {
|
||||
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
|
||||
@ -219,10 +234,13 @@ describe("protocol router tests", () => {
|
||||
});
|
||||
|
||||
it("should throw if urlSchema is invalid", () => {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
|
||||
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
|
||||
});
|
||||
|
||||
it("should call most exact handler with 3 found handlers", async () => {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
let called: any = 0;
|
||||
|
||||
lpr.addInternalHandler("/", () => { called = 2; });
|
||||
@ -241,6 +259,7 @@ describe("protocol router tests", () => {
|
||||
});
|
||||
|
||||
it("should call most exact handler with 2 found handlers", async () => {
|
||||
const lpr = LensProtocolRouterMain.getInstance();
|
||||
let called: any = 0;
|
||||
|
||||
lpr.addInternalHandler("/", () => { called = 2; });
|
||||
|
||||
@ -101,10 +101,10 @@ class HelmApiRoute extends LensApi {
|
||||
}
|
||||
|
||||
public async getReleaseValues(request: LensApiRequest) {
|
||||
const { cluster, params, response } = request;
|
||||
const { cluster, params, response, query } = request;
|
||||
|
||||
try {
|
||||
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
|
||||
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace, query.has("all"));
|
||||
|
||||
this.respondText(response, result);
|
||||
} catch (error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user