mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into extension-auto-update
This commit is contained in:
commit
ee265a9db6
13
.eslintrc.js
13
.eslintrc.js
@ -54,6 +54,7 @@ module.exports = {
|
|||||||
"react-hooks",
|
"react-hooks",
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"header/header": [2, "./license-header"],
|
"header/header": [2, "./license-header"],
|
||||||
"comma-dangle": ["error", "always-multiline"],
|
"comma-dangle": ["error", "always-multiline"],
|
||||||
"comma-spacing": "error",
|
"comma-spacing": "error",
|
||||||
@ -107,7 +108,10 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
parser: "@typescript-eslint/parser",
|
parser: "@typescript-eslint/parser",
|
||||||
extends: [
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
"header",
|
"header",
|
||||||
@ -118,7 +122,7 @@ module.exports = {
|
|||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-irregular-whitespace": "error",
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"header/header": [2, "./license-header"],
|
"header/header": [2, "./license-header"],
|
||||||
"no-invalid-this": "off",
|
"no-invalid-this": "off",
|
||||||
"@typescript-eslint/no-invalid-this": ["error"],
|
"@typescript-eslint/no-invalid-this": ["error"],
|
||||||
@ -191,8 +195,11 @@ module.exports = {
|
|||||||
"unused-imports",
|
"unused-imports",
|
||||||
],
|
],
|
||||||
extends: [
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:react/recommended",
|
"plugin:react/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/typescript",
|
||||||
],
|
],
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: 2018,
|
||||||
@ -200,8 +207,9 @@ module.exports = {
|
|||||||
jsx: true,
|
jsx: true,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"no-irregular-whitespace": "error",
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"header/header": [2, "./license-header"],
|
"header/header": [2, "./license-header"],
|
||||||
|
"react/prop-types": "off",
|
||||||
"no-invalid-this": "off",
|
"no-invalid-this": "off",
|
||||||
"@typescript-eslint/no-invalid-this": ["error"],
|
"@typescript-eslint/no-invalid-this": ["error"],
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
@ -246,7 +254,6 @@ module.exports = {
|
|||||||
"objectsInObjects": false,
|
"objectsInObjects": false,
|
||||||
"arraysInObjects": true,
|
"arraysInObjects": true,
|
||||||
}],
|
}],
|
||||||
"react/prop-types": "off",
|
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error"],
|
"@typescript-eslint/semi": ["error"],
|
||||||
"linebreak-style": ["error", "unix"],
|
"linebreak-style": ["error", "unix"],
|
||||||
|
|||||||
@ -37,9 +37,9 @@ export default class ExampleMainExtension extends Main.LensExtension {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### App Menus
|
### Menus
|
||||||
|
|
||||||
This extension can register custom app menus that will be displayed on OS native menus.
|
This extension can register custom app and tray menus that will be displayed on OS native menus.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@ -56,6 +56,29 @@ export default class ExampleMainExtension extends Main.LensExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
trayMenus = [
|
||||||
|
{
|
||||||
|
label: "My links",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Lens",
|
||||||
|
click() {
|
||||||
|
Main.Navigation.navigate("https://k8slens.dev");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "separator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Lens Github",
|
||||||
|
click() {
|
||||||
|
Main.Navigation.navigate("https://github.com/lensapp/lens");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
The Main Extension API is the interface to Lens's main process.
|
The Main Extension API is the interface to Lens's main process.
|
||||||
Lens runs in both main and renderer processes.
|
Lens runs in both main and renderer processes.
|
||||||
The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process.
|
The Main Extension API allows you to access, configure, and customize Lens data, add custom application menu items and [protocol handlers](protocol-handlers.md), and run custom code in Lens's main process.
|
||||||
It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities.
|
It also provides convenient methods for navigating to built-in Lens pages and extension pages, as well as adding and removing sources of catalog entities.
|
||||||
|
|
||||||
## `Main.LensExtension` Class
|
## `Main.LensExtension` Class
|
||||||
|
|
||||||
@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores
|
|||||||
### `appMenus`
|
### `appMenus`
|
||||||
|
|
||||||
The Main Extension API allows you to customize the UI application menu.
|
The Main Extension API allows you to customize the UI application menu.
|
||||||
Note that this is the only UI feature that the Main Extension API allows you to customize.
|
|
||||||
The following example demonstrates adding an item to the **Help** menu.
|
The following example demonstrates adding an item to the **Help** menu.
|
||||||
|
|
||||||
``` typescript
|
``` typescript
|
||||||
@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
|||||||
```
|
```
|
||||||
|
|
||||||
`appMenus` is an array of objects that satisfy the `MenuRegistration` interface.
|
`appMenus` is an array of objects that satisfy the `MenuRegistration` interface.
|
||||||
`MenuRegistration` extends React's `MenuItemConstructorOptions` interface.
|
`MenuRegistration` extends Electron's `MenuItemConstructorOptions` interface.
|
||||||
The properties of the appMenus array objects are defined as follows:
|
The properties of the appMenus array objects are defined as follows:
|
||||||
|
|
||||||
* `parentId` is the name of the menu where your new menu item will be listed.
|
* `parentId` is the name of the menu where your new menu item will be listed.
|
||||||
@ -96,6 +95,35 @@ export default class SamplePageMainExtension extends Main.LensExtension {
|
|||||||
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
|
||||||
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
|
||||||
|
|
||||||
|
### `trayMenus`
|
||||||
|
|
||||||
|
`trayMenus` is an array of `TrayMenuRegistration` objects. Most importantly you can define a `label` and a `click` handler. Other properties are `submenu`, `enabled`, `toolTip`, `id` and `type`.
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
interface TrayMenuRegistration {
|
||||||
|
label?: string;
|
||||||
|
click?: (menuItem: TrayMenuRegistration) => void;
|
||||||
|
id?: string;
|
||||||
|
type?: "normal" | "separator" | "submenu"
|
||||||
|
toolTip?: string;
|
||||||
|
enabled?: boolean;
|
||||||
|
submenu?: TrayMenuRegistration[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The following example demonstrates how tray menus can be added from extension:
|
||||||
|
|
||||||
|
``` typescript
|
||||||
|
import { Main } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
export default class SampleTrayMenuMainExtension extends Main.LensExtension {
|
||||||
|
trayMenus = [{
|
||||||
|
label: "menu from the extension",
|
||||||
|
click: () => { console.log("tray menu clicked!") }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
### `addCatalogSource()` and `removeCatalogSource()` Methods
|
||||||
|
|
||||||
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
|
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).
|
||||||
|
|||||||
36
extensions/.eslintrc.js
Normal file
36
extensions/.eslintrc.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
"import/no-unresolved": ["error", {
|
||||||
|
ignore: ["@k8slens/extensions"],
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -62,8 +62,7 @@
|
|||||||
},
|
},
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
|
||||||
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
|
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
|
||||||
"src/(.*)": "<rootDir>/__mocks__/windowMock.ts"
|
|
||||||
},
|
},
|
||||||
"modulePathIgnorePatterns": [
|
"modulePathIgnorePatterns": [
|
||||||
"<rootDir>/dist",
|
"<rootDir>/dist",
|
||||||
@ -200,6 +199,7 @@
|
|||||||
"@ogre-tools/injectable-react": "2.0.0",
|
"@ogre-tools/injectable-react": "2.0.0",
|
||||||
"@sentry/electron": "^2.5.4",
|
"@sentry/electron": "^2.5.4",
|
||||||
"@sentry/integrations": "^6.15.0",
|
"@sentry/integrations": "^6.15.0",
|
||||||
|
"@types/circular-dependency-plugin": "5.0.4",
|
||||||
"abort-controller": "^3.0.0",
|
"abort-controller": "^3.0.0",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
"autobind-decorator": "^2.4.0",
|
"autobind-decorator": "^2.4.0",
|
||||||
@ -214,7 +214,7 @@
|
|||||||
"filehound": "^1.17.5",
|
"filehound": "^1.17.5",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"got": "^11.8.2",
|
"got": "^11.8.3",
|
||||||
"grapheme-splitter": "^1.0.4",
|
"grapheme-splitter": "^1.0.4",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"http-proxy": "^1.18.1",
|
"http-proxy": "^1.18.1",
|
||||||
@ -333,7 +333,7 @@
|
|||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"css-loader": "^5.2.7",
|
"css-loader": "^5.2.7",
|
||||||
"deepdash": "^5.3.9",
|
"deepdash": "^5.3.9",
|
||||||
"dompurify": "^2.3.3",
|
"dompurify": "^2.3.4",
|
||||||
"electron": "^13.6.1",
|
"electron": "^13.6.1",
|
||||||
"electron-builder": "^22.14.5",
|
"electron-builder": "^22.14.5",
|
||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
@ -341,6 +341,7 @@
|
|||||||
"esbuild-loader": "^2.16.0",
|
"esbuild-loader": "^2.16.0",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
|
"eslint-plugin-import": "^2.25.3",
|
||||||
"eslint-plugin-react": "^7.27.1",
|
"eslint-plugin-react": "^7.27.1",
|
||||||
"eslint-plugin-react-hooks": "^4.3.0",
|
"eslint-plugin-react-hooks": "^4.3.0",
|
||||||
"eslint-plugin-unused-imports": "^1.1.5",
|
"eslint-plugin-unused-imports": "^1.1.5",
|
||||||
|
|||||||
@ -472,8 +472,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
|
|||||||
it("moves the icon into preferences", async () => {
|
it("moves the icon into preferences", async () => {
|
||||||
const storedClusterData = ClusterStore.getInstance().clustersList[0];
|
const storedClusterData = ClusterStore.getInstance().clustersList[0];
|
||||||
|
|
||||||
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
|
expect(Object.prototype.hasOwnProperty.call(storedClusterData, "icon")).toBe(false);
|
||||||
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
|
expect(Object.prototype.hasOwnProperty.call(storedClusterData.preferences, "icon")).toBe(true);
|
||||||
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -251,7 +251,7 @@ describe("HotbarStore", () => {
|
|||||||
const hotbarStore = HotbarStore.getInstance();
|
const hotbarStore = HotbarStore.getInstance();
|
||||||
|
|
||||||
hotbarStore.add({ name: "hottest", id: "hottest" });
|
hotbarStore.add({ name: "hottest", id: "hottest" });
|
||||||
hotbarStore.activeHotbarId = "hottest";
|
hotbarStore.setActiveHotbar("hottest");
|
||||||
|
|
||||||
const { error } = logger;
|
const { error } = logger;
|
||||||
const mocked = jest.fn();
|
const mocked = jest.fn();
|
||||||
|
|||||||
@ -42,9 +42,9 @@ import { Console } from "console";
|
|||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import electron from "electron";
|
import electron from "electron";
|
||||||
import { stdout, stderr } from "process";
|
import { stdout, stderr } from "process";
|
||||||
import { ThemeStore } from "../../renderer/theme.store";
|
|
||||||
import type { ClusterStoreModel } from "../cluster-store";
|
import type { ClusterStoreModel } from "../cluster-store";
|
||||||
import { AppPaths } from "../app-paths";
|
import { AppPaths } from "../app-paths";
|
||||||
|
import { defaultTheme } from "../vars";
|
||||||
|
|
||||||
console = new Console(stdout, stderr);
|
console = new Console(stdout, stderr);
|
||||||
AppPaths.init();
|
AppPaths.init();
|
||||||
@ -75,7 +75,7 @@ describe("user store tests", () => {
|
|||||||
us.httpsProxy = "abcd://defg";
|
us.httpsProxy = "abcd://defg";
|
||||||
|
|
||||||
expect(us.httpsProxy).toBe("abcd://defg");
|
expect(us.httpsProxy).toBe("abcd://defg");
|
||||||
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
expect(us.colorTheme).toBe(defaultTheme);
|
||||||
|
|
||||||
us.colorTheme = "light";
|
us.colorTheme = "light";
|
||||||
expect(us.colorTheme).toBe("light");
|
expect(us.colorTheme).toBe("light");
|
||||||
@ -86,7 +86,7 @@ describe("user store tests", () => {
|
|||||||
|
|
||||||
us.colorTheme = "some other theme";
|
us.colorTheme = "some other theme";
|
||||||
us.resetTheme();
|
us.resetTheme();
|
||||||
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
|
expect(us.colorTheme).toBe(defaultTheme);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("correctly calculates if the last seen version is an old release", () => {
|
it("correctly calculates if the last seen version is an old release", () => {
|
||||||
|
|||||||
@ -23,7 +23,8 @@ import { app, ipcMain, ipcRenderer } from "electron";
|
|||||||
import { observable, when } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { fromEntries, toJS } from "./utils";
|
import { fromEntries } from "./utils/objects";
|
||||||
|
import { toJS } from "./utils/toJS";
|
||||||
import { isWindows } from "./vars";
|
import { isWindows } from "./vars";
|
||||||
|
|
||||||
export type PathName = Parameters<typeof app["getPath"]>[0];
|
export type PathName = Parameters<typeof app["getPath"]>[0];
|
||||||
|
|||||||
@ -20,11 +20,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
|
||||||
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
|
||||||
import { ClusterStore } from "../cluster-store";
|
import { ClusterStore } from "../cluster-store";
|
||||||
import { broadcastMessage, requestMain } from "../ipc";
|
import { broadcastMessage, requestMain } from "../ipc";
|
||||||
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
|
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
|
||||||
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CatalogCategory, CatalogEntity, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
import { CatalogCategory, CatalogEntity, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
|
||||||
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
|
||||||
import { productName } from "../vars";
|
import { productName } from "../vars";
|
||||||
import { WeblinkStore } from "../weblink-store";
|
import { WeblinkStore } from "../weblink-store";
|
||||||
@ -86,21 +86,6 @@ export class WebLinkCategory extends CatalogCategory {
|
|||||||
kind: "WebLink",
|
kind: "WebLink",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
public static onAdd?: () => void;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.on("catalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
|
|
||||||
ctx.menuItems.push({
|
|
||||||
icon: "public",
|
|
||||||
title: "Add web link",
|
|
||||||
onClick: () => {
|
|
||||||
WebLinkCategory.onAdd();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
catalogCategoryRegistry.add(new WebLinkCategory());
|
catalogCategoryRegistry.add(new WebLinkCategory());
|
||||||
|
|||||||
@ -18,18 +18,12 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { HotbarStore } from "./hotbar-store";
|
||||||
|
|
||||||
import React from "react";
|
const hotbarManagerInjectable = getInjectable({
|
||||||
import { TabLayout } from "../layout/tab-layout";
|
instantiate: () => HotbarStore.getInstance(),
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
export class NotFound extends React.Component {
|
export default hotbarManagerInjectable;
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<TabLayout className="NotFound" contentClass="flex">
|
|
||||||
<p className="box center">
|
|
||||||
Page not found
|
|
||||||
</p>
|
|
||||||
</TabLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { action, comparer, observable, makeObservable } from "mobx";
|
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import migrations from "../migrations/hotbar-store";
|
import migrations from "../migrations/hotbar-store";
|
||||||
import { toJS } from "./utils";
|
import { toJS } from "./utils";
|
||||||
@ -27,7 +27,7 @@ import { CatalogEntity } from "./catalog";
|
|||||||
import { catalogEntity } from "../main/catalog-sources/general";
|
import { catalogEntity } from "../main/catalog-sources/general";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
|
import { broadcastMessage, HotbarTooManyItems } from "./ipc";
|
||||||
import { defaultHotbarCells, getEmptyHotbar, Hotbar, HotbarCreateOptions } from "./hotbar-types";
|
import { defaultHotbarCells, getEmptyHotbar, Hotbar, CreateHotbarData, CreateHotbarOptions } from "./hotbar-types";
|
||||||
|
|
||||||
export interface HotbarStoreModel {
|
export interface HotbarStoreModel {
|
||||||
hotbars: Hotbar[];
|
hotbars: Hotbar[];
|
||||||
@ -52,22 +52,40 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
this.load();
|
this.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHotbarId() {
|
@computed get activeHotbarId() {
|
||||||
return this._activeHotbarId;
|
return this._activeHotbarId;
|
||||||
}
|
}
|
||||||
|
|
||||||
set activeHotbarId(id: string) {
|
/**
|
||||||
if (this.getById(id)) {
|
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
|
||||||
this._activeHotbarId = id;
|
* @param hotbar The hotbar instance, or the index, or its ID
|
||||||
|
*/
|
||||||
|
setActiveHotbar(hotbar: Hotbar | number | string) {
|
||||||
|
if (typeof hotbar === "number") {
|
||||||
|
if (hotbar >= 0 && hotbar < this.hotbars.length) {
|
||||||
|
this._activeHotbarId = this.hotbars[hotbar].id;
|
||||||
|
}
|
||||||
|
} else if (typeof hotbar === "string") {
|
||||||
|
if (this.getById(hotbar)) {
|
||||||
|
this._activeHotbarId = hotbar;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.hotbars.indexOf(hotbar) >= 0) {
|
||||||
|
this._activeHotbarId = hotbar.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hotbarIndex(id: string) {
|
private hotbarIndexById(id: string) {
|
||||||
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get activeHotbarIndex() {
|
private hotbarIndex(hotbar: Hotbar) {
|
||||||
return this.hotbarIndex(this.activeHotbarId);
|
return this.hotbars.indexOf(hotbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get activeHotbarIndex() {
|
||||||
|
return this.hotbarIndexById(this.activeHotbarId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -87,13 +105,11 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
this.hotbars.forEach(ensureExactHotbarItemLength);
|
this.hotbars.forEach(ensureExactHotbarItemLength);
|
||||||
|
|
||||||
if (data.activeHotbarId) {
|
if (data.activeHotbarId) {
|
||||||
if (this.getById(data.activeHotbarId)) {
|
this.setActiveHotbar(data.activeHotbarId);
|
||||||
this.activeHotbarId = data.activeHotbarId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.activeHotbarId) {
|
if (!this.activeHotbarId) {
|
||||||
this.activeHotbarId = this.hotbars[0].id;
|
this.setActiveHotbar(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,8 +134,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
return this.hotbars.find((hotbar) => hotbar.id === id);
|
return this.hotbars.find((hotbar) => hotbar.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => {
|
||||||
add(data: HotbarCreateOptions, { setActive = false } = {}) {
|
|
||||||
const hotbar = getEmptyHotbar(data.name, data.id);
|
const hotbar = getEmptyHotbar(data.name, data.id);
|
||||||
|
|
||||||
this.hotbars.push(hotbar);
|
this.hotbars.push(hotbar);
|
||||||
@ -127,29 +142,29 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
if (setActive) {
|
if (setActive) {
|
||||||
this._activeHotbarId = hotbar.id;
|
this._activeHotbarId = hotbar.id;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
@action
|
setHotbarName = action((id: string, name: string) => {
|
||||||
setHotbarName(id: string, name: string) {
|
|
||||||
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
||||||
|
|
||||||
if(index < 0) {
|
if (index < 0) {
|
||||||
console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
|
return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.hotbars[index].name = name;
|
this.hotbars[index].name = name;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
remove = action((hotbar: Hotbar) => {
|
||||||
|
if (this.hotbars.length <= 1) {
|
||||||
|
throw new Error("Cannot remove the last hotbar");
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
remove(hotbar: Hotbar) {
|
|
||||||
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
||||||
|
|
||||||
if (this.activeHotbarId === hotbar.id) {
|
if (this.activeHotbarId === hotbar.id) {
|
||||||
this.activeHotbarId = this.hotbars[0].id;
|
this.setActiveHotbar(0);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
||||||
@ -263,7 +278,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
index = hotbarStore.hotbars.length - 1;
|
index = hotbarStore.hotbars.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
|
hotbarStore.setActiveHotbar(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
switchToNext() {
|
switchToNext() {
|
||||||
@ -274,7 +289,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
index = 0;
|
index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
|
hotbarStore.setActiveHotbar(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -284,6 +299,20 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
isAddedToActive(entity: CatalogEntity) {
|
isAddedToActive(entity: CatalogEntity) {
|
||||||
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
|
return !!this.getActive().items.find(item => item?.entity.uid === entity.metadata.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDisplayLabel(hotbar: Hotbar): string {
|
||||||
|
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDisplayIndex(hotbar: Hotbar): string {
|
||||||
|
const index = this.hotbarIndex(hotbar);
|
||||||
|
|
||||||
|
if (index < 0) {
|
||||||
|
return "??";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${index + 1}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -292,12 +321,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|||||||
* @param hotbar The hotbar to modify
|
* @param hotbar The hotbar to modify
|
||||||
*/
|
*/
|
||||||
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
||||||
if (hotbar.items.length === defaultHotbarCells) {
|
// if there are not enough items
|
||||||
// if we already have `defaultHotbarCells` then we are good to stop
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, keep adding empty entries until full
|
|
||||||
while (hotbar.items.length < defaultHotbarCells) {
|
while (hotbar.items.length < defaultHotbarCells) {
|
||||||
hotbar.items.push(null);
|
hotbar.items.push(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,14 +33,18 @@ export interface HotbarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Hotbar = Required<HotbarCreateOptions>;
|
export type Hotbar = Required<CreateHotbarData>;
|
||||||
|
|
||||||
export interface HotbarCreateOptions {
|
export interface CreateHotbarData {
|
||||||
id?: string;
|
id?: string;
|
||||||
name: string;
|
name: string;
|
||||||
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
|
items?: Tuple<HotbarItem | null, typeof defaultHotbarCells>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CreateHotbarOptions {
|
||||||
|
setActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
export const defaultHotbarCells = 12; // Number is chosen to easy hit any item with keyboard
|
||||||
|
|
||||||
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
|
export function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
|
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
|
||||||
|
export const catalogEntityRunListener = "catalog-entity:run";
|
||||||
|
|
||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
export * from "./invalid-kubeconfig";
|
export * from "./invalid-kubeconfig";
|
||||||
|
|||||||
@ -30,7 +30,17 @@ import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
|||||||
import type { Disposer } from "../utils";
|
import type { Disposer } from "../utils";
|
||||||
import type remote from "@electron/remote";
|
import type remote from "@electron/remote";
|
||||||
|
|
||||||
const electronRemote = ipcMain ? null : require("@electron/remote");
|
const electronRemote = (() => {
|
||||||
|
if (ipcRenderer) {
|
||||||
|
try {
|
||||||
|
return require("@electron/remote");
|
||||||
|
} catch {
|
||||||
|
// ignore temp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
const subFramesChannel = "ipc:get-sub-frames";
|
const subFramesChannel = "ipc:get-sub-frames";
|
||||||
|
|
||||||
|
|||||||
@ -19,10 +19,10 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CustomResourceDefinition } from "../endpoints";
|
import { CustomResourceDefinition, CustomResourceDefinitionSpec } from "../endpoints";
|
||||||
|
|
||||||
describe("Crds", () => {
|
describe("Crds", () => {
|
||||||
describe("getVersion", () => {
|
describe("getVersion()", () => {
|
||||||
it("should throw if none of the versions are served", () => {
|
it("should throw if none of the versions are served", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "apiextensions.k8s.io/v1",
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
@ -136,7 +136,7 @@ describe("Crds", () => {
|
|||||||
expect(crd.getVersion()).toBe("123");
|
expect(crd.getVersion()).toBe("123");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the version name from the version field", () => {
|
it("should get the version name from the version field, ignoring versions on v1beta", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "apiextensions.k8s.io/v1beta1",
|
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
@ -147,7 +147,14 @@ describe("Crds", () => {
|
|||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
version: "abc",
|
version: "abc",
|
||||||
},
|
versions: [
|
||||||
|
{
|
||||||
|
name: "foobar",
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as CustomResourceDefinitionSpec,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(crd.getVersion()).toBe("abc");
|
expect(crd.getVersion()).toBe("abc");
|
||||||
|
|||||||
@ -164,14 +164,14 @@ describe("KubeObject", () => {
|
|||||||
|
|
||||||
describe("isJsonApiDataList", () => {
|
describe("isJsonApiDataList", () => {
|
||||||
function isAny(val: unknown): val is any {
|
function isAny(val: unknown): val is any {
|
||||||
return !Boolean(void val);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isNotAny(val: unknown): val is any {
|
function isNotAny(val: unknown): val is any {
|
||||||
return Boolean(void val);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBoolean(val: unknown): val is Boolean {
|
function isBoolean(val: unknown): val is boolean {
|
||||||
return typeof val === "boolean";
|
return typeof val === "boolean";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,34 +48,36 @@ export interface CRDVersion {
|
|||||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomResourceDefinition {
|
export interface CustomResourceDefinitionSpec {
|
||||||
spec: {
|
group: string;
|
||||||
group: string;
|
/**
|
||||||
/**
|
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
*/
|
||||||
*/
|
version?: string;
|
||||||
version?: string;
|
names: {
|
||||||
names: {
|
plural: string;
|
||||||
plural: string;
|
singular: string;
|
||||||
singular: string;
|
kind: string;
|
||||||
kind: string;
|
listKind: string;
|
||||||
listKind: string;
|
|
||||||
};
|
|
||||||
scope: "Namespaced" | "Cluster" | string;
|
|
||||||
/**
|
|
||||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
|
||||||
*/
|
|
||||||
validation?: object;
|
|
||||||
versions?: CRDVersion[];
|
|
||||||
conversion: {
|
|
||||||
strategy?: string;
|
|
||||||
webhook?: any;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
|
||||||
*/
|
|
||||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
|
||||||
};
|
};
|
||||||
|
scope: "Namespaced" | "Cluster";
|
||||||
|
/**
|
||||||
|
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||||
|
*/
|
||||||
|
validation?: object;
|
||||||
|
versions?: CRDVersion[];
|
||||||
|
conversion: {
|
||||||
|
strategy?: string;
|
||||||
|
webhook?: any;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
|
||||||
|
*/
|
||||||
|
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomResourceDefinition {
|
||||||
|
spec: CustomResourceDefinitionSpec;
|
||||||
status: {
|
status: {
|
||||||
conditions: {
|
conditions: {
|
||||||
lastTransitionTime: string;
|
lastTransitionTime: string;
|
||||||
@ -150,27 +152,32 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPreferedVersion(): CRDVersion {
|
getPreferedVersion(): CRDVersion {
|
||||||
// Prefer the modern `versions` over the legacy `version`
|
const { apiVersion } = this;
|
||||||
if (this.spec.versions) {
|
|
||||||
for (const version of this.spec.versions) {
|
|
||||||
if (version.storage) {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (this.spec.version) {
|
|
||||||
const { additionalPrinterColumns: apc } = this.spec;
|
|
||||||
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
|
||||||
|
|
||||||
return {
|
switch (apiVersion) {
|
||||||
name: this.spec.version,
|
case "apiextensions.k8s.io/v1":
|
||||||
served: true,
|
for (const version of this.spec.versions) {
|
||||||
storage: true,
|
if (version.storage) {
|
||||||
schema: this.spec.validation,
|
return version;
|
||||||
additionalPrinterColumns,
|
}
|
||||||
};
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "apiextensions.k8s.io/v1beta1": {
|
||||||
|
const { additionalPrinterColumns: apc } = this.spec;
|
||||||
|
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: this.spec.version,
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
schema: this.spec.validation,
|
||||||
|
additionalPrinterColumns,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
@ -197,7 +204,7 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
|
.filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidation() {
|
getValidation() {
|
||||||
|
|||||||
@ -187,7 +187,7 @@ export class Ingress extends KubeObject {
|
|||||||
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
|
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
|
||||||
|
|
||||||
if (rules && rules.length > 0) {
|
if (rules && rules.length > 0) {
|
||||||
if (rules.some(rule => rule.hasOwnProperty("http"))) {
|
if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) {
|
||||||
ports.push(httpPort);
|
ports.push(httpPort);
|
||||||
}
|
}
|
||||||
} else if (servicePort !== undefined) {
|
} else if (servicePort !== undefined) {
|
||||||
|
|||||||
@ -184,7 +184,8 @@ export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
|
|||||||
if (metric.data.result.length) {
|
if (metric.data.result.length) {
|
||||||
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
|
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch {
|
||||||
|
// ignore error
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -34,6 +34,9 @@ import type { IKubeWatchEvent } from "./kube-watch-api";
|
|||||||
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
|
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
|
||||||
import { noop } from "../utils";
|
import { noop } from "../utils";
|
||||||
import type { RequestInit } from "node-fetch";
|
import type { RequestInit } from "node-fetch";
|
||||||
|
|
||||||
|
// BUG: https://github.com/mysticatea/abort-controller/pull/22
|
||||||
|
// eslint-disable-next-line import/no-named-as-default
|
||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
import { Agent, AgentOptions } from "https";
|
import { Agent, AgentOptions } from "https";
|
||||||
import type { Patch } from "rfc6902";
|
import type { Patch } from "rfc6902";
|
||||||
@ -698,21 +701,16 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
|
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
|
||||||
|
if (event.type === "ERROR") {
|
||||||
|
return;
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case "ADDED":
|
|
||||||
case "DELETED":
|
|
||||||
|
|
||||||
case "MODIFIED": {
|
|
||||||
ensureObjectSelfLink(this, event.object);
|
|
||||||
|
|
||||||
const { namespace, resourceVersion } = event.object.metadata;
|
|
||||||
|
|
||||||
this.setResourceVersion(namespace, resourceVersion);
|
|
||||||
this.setResourceVersion("", resourceVersion);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureObjectSelfLink(this, event.object);
|
||||||
|
|
||||||
|
const { namespace, resourceVersion } = event.object.metadata;
|
||||||
|
|
||||||
|
this.setResourceVersion(namespace, resourceVersion);
|
||||||
|
this.setResourceVersion("", resourceVersion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,9 @@ import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api";
|
|||||||
import { parseKubeApi } from "./kube-api-parse";
|
import { parseKubeApi } from "./kube-api-parse";
|
||||||
import type { KubeJsonApiData } from "./kube-json-api";
|
import type { KubeJsonApiData } from "./kube-json-api";
|
||||||
import type { RequestInit } from "node-fetch";
|
import type { RequestInit } from "node-fetch";
|
||||||
|
|
||||||
|
// BUG: https://github.com/mysticatea/abort-controller/pull/22
|
||||||
|
// eslint-disable-next-line import/no-named-as-default
|
||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
import type { Patch } from "rfc6902";
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
@ -235,8 +238,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async loadAll({ namespaces = this.context.contextNamespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
async loadAll({ namespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise<void | T[]> {
|
||||||
await this.contextReady;
|
await this.contextReady;
|
||||||
|
namespaces ??= this.context.contextNamespaces;
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -469,7 +473,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ADDED":
|
case "ADDED":
|
||||||
case "MODIFIED":
|
|
||||||
|
// falls through
|
||||||
|
case "MODIFIED": {
|
||||||
const newItem = new this.api.objectConstructor(object);
|
const newItem = new this.api.objectConstructor(object);
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
@ -477,7 +483,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
} else {
|
} else {
|
||||||
items[index] = newItem;
|
items[index] = newItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "DELETED":
|
case "DELETED":
|
||||||
if (item) {
|
if (item) {
|
||||||
items.splice(index, 1);
|
items.splice(index, 1);
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export abstract class LensProtocolRouter {
|
|||||||
|
|
||||||
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
||||||
|
|
||||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||||
|
|
||||||
constructor(protected dependencies: Dependencies) {}
|
constructor(protected dependencies: Dependencies) {}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export class SearchStore {
|
|||||||
* @param value Unescaped string
|
* @param value Unescaped string
|
||||||
*/
|
*/
|
||||||
public static escapeRegex(value?: string): string {
|
public static escapeRegex(value?: string): string {
|
||||||
return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : "";
|
return value ? value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -22,11 +22,11 @@
|
|||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { ThemeStore } from "../../renderer/theme.store";
|
|
||||||
import { getAppVersion, ObservableToggleSet } from "../utils";
|
import { getAppVersion, ObservableToggleSet } from "../utils";
|
||||||
import type { editor } from "monaco-editor";
|
import type { editor } from "monaco-editor";
|
||||||
import merge from "lodash/merge";
|
import merge from "lodash/merge";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
|
import { defaultTheme } from "../vars";
|
||||||
|
|
||||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
@ -72,10 +72,10 @@ const shell: PreferenceDescription<string | undefined> = {
|
|||||||
|
|
||||||
const colorTheme: PreferenceDescription<string> = {
|
const colorTheme: PreferenceDescription<string> = {
|
||||||
fromStore(val) {
|
fromStore(val) {
|
||||||
return val || ThemeStore.defaultTheme;
|
return val || defaultTheme;
|
||||||
},
|
},
|
||||||
toStore(val) {
|
toStore(val) {
|
||||||
if (!val || val === ThemeStore.defaultTheme) {
|
if (!val || val === defaultTheme) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
|
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
|
||||||
const scope = typeof global !== "undefined" ? global : window;
|
const scope = typeof global !== "undefined" ? global : window;
|
||||||
|
|
||||||
if (scope.hasOwnProperty(propName)) {
|
if (Object.prototype.hasOwnProperty.call(scope, propName)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export type Falsey = false | 0 | "" | null | undefined;
|
|||||||
* Create a new type safe empty Iterable
|
* Create a new type safe empty Iterable
|
||||||
* @returns An `Iterable` that yields 0 items
|
* @returns An `Iterable` that yields 0 items
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line require-yield
|
||||||
export function* newEmpty<T>(): IterableIterator<T> {
|
export function* newEmpty<T>(): IterableIterator<T> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,12 +31,13 @@ export interface ReadFileFromTarOpts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise<R> {
|
export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise<R> {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const fileChunks: Buffer[] = [];
|
const fileChunks: Buffer[] = [];
|
||||||
|
|
||||||
await tar.list({
|
tar.list({
|
||||||
file: tarPath,
|
file: tarPath,
|
||||||
filter: entryPath => path.normalize(entryPath) === filePath,
|
filter: entryPath => path.normalize(entryPath) === filePath,
|
||||||
|
sync: true,
|
||||||
onentry(entry: FileStat) {
|
onentry(entry: FileStat) {
|
||||||
entry.on("data", chunk => {
|
entry.on("data", chunk => {
|
||||||
fileChunks.push(chunk);
|
fileChunks.push(chunk);
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { array } from "../utils";
|
import * as array from "../utils/array";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A strict N-tuple of type T
|
* A strict N-tuple of type T
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export const isIntegrationTesting = process.argv.includes(integrationTestingArg)
|
|||||||
export const productName = packageInfo.productName;
|
export const productName = packageInfo.productName;
|
||||||
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
|
||||||
export const publicPath = "/build/" as string;
|
export const publicPath = "/build/" as string;
|
||||||
|
export const defaultTheme = "lens-dark" as string;
|
||||||
|
|
||||||
// Webpack build paths
|
// Webpack build paths
|
||||||
export const contextDir = process.cwd();
|
export const contextDir = process.cwd();
|
||||||
|
|||||||
@ -19,7 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import mockFs from "mock-fs";
|
|
||||||
import { watch } from "chokidar";
|
import { watch } from "chokidar";
|
||||||
import { ExtensionsStore } from "../extensions-store";
|
import { ExtensionsStore } from "../extensions-store";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
@ -30,6 +29,7 @@ import { AppPaths } from "../../common/app-paths";
|
|||||||
import type { ExtensionLoader } from "../extension-loader";
|
import type { ExtensionLoader } from "../extension-loader";
|
||||||
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
|
||||||
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
jest.setTimeout(60_000);
|
jest.setTimeout(60_000);
|
||||||
|
|
||||||
@ -43,6 +43,7 @@ jest.mock("../extension-installer", () => ({
|
|||||||
installPackage: jest.fn(),
|
installPackage: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock("fs-extra");
|
||||||
jest.mock("electron", () => ({
|
jest.mock("electron", () => ({
|
||||||
app: {
|
app: {
|
||||||
getVersion: () => "99.99.99",
|
getVersion: () => "99.99.99",
|
||||||
@ -63,6 +64,7 @@ AppPaths.init();
|
|||||||
|
|
||||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
|
||||||
|
const mockedFse = fse as jest.Mocked<typeof fse>;
|
||||||
|
|
||||||
describe("ExtensionDiscovery", () => {
|
describe("ExtensionDiscovery", () => {
|
||||||
let extensionLoader: ExtensionLoader;
|
let extensionLoader: ExtensionLoader;
|
||||||
@ -77,63 +79,60 @@ describe("ExtensionDiscovery", () => {
|
|||||||
extensionLoader = di.inject(extensionLoaderInjectable);
|
extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with mockFs", () => {
|
it("emits add for added extension", async (done) => {
|
||||||
beforeEach(() => {
|
let addHandler: (filePath: string) => void;
|
||||||
mockFs({
|
|
||||||
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({
|
|
||||||
name: "my-extension",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
mockedFse.readJson.mockImplementation((p) => {
|
||||||
mockFs.restore();
|
expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json"));
|
||||||
});
|
|
||||||
|
|
||||||
it("emits add for added extension", async (done) => {
|
return {
|
||||||
let addHandler: (filePath: string) => void;
|
name: "my-extension",
|
||||||
|
version: "1.0.0",
|
||||||
const mockWatchInstance: any = {
|
|
||||||
on: jest.fn((event: string, handler: typeof addHandler) => {
|
|
||||||
if (event === "add") {
|
|
||||||
addHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mockWatchInstance;
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
mockedWatch.mockImplementationOnce(() =>
|
|
||||||
(mockWatchInstance) as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
const extensionDiscovery = ExtensionDiscovery.createInstance(
|
|
||||||
extensionLoader,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Need to force isLoaded to be true so that the file watching is started
|
|
||||||
extensionDiscovery.isLoaded = true;
|
|
||||||
|
|
||||||
await extensionDiscovery.watchExtensions();
|
|
||||||
|
|
||||||
extensionDiscovery.events.on("add", extension => {
|
|
||||||
expect(extension).toEqual({
|
|
||||||
absolutePath: expect.any(String),
|
|
||||||
id: path.normalize("node_modules/my-extension/package.json"),
|
|
||||||
isBundled: false,
|
|
||||||
isEnabled: false,
|
|
||||||
isCompatible: false,
|
|
||||||
manifest: {
|
|
||||||
name: "my-extension",
|
|
||||||
},
|
|
||||||
manifestPath: path.normalize("node_modules/my-extension/package.json"),
|
|
||||||
availableUpdate: null,
|
|
||||||
});
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mockedFse.pathExists.mockImplementation(() => true);
|
||||||
|
|
||||||
|
const mockWatchInstance: any = {
|
||||||
|
on: jest.fn((event: string, handler: typeof addHandler) => {
|
||||||
|
if (event === "add") {
|
||||||
|
addHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockWatchInstance;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockedWatch.mockImplementationOnce(() =>
|
||||||
|
(mockWatchInstance) as any,
|
||||||
|
);
|
||||||
|
const extensionDiscovery = ExtensionDiscovery.createInstance(
|
||||||
|
extensionLoader,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Need to force isLoaded to be true so that the file watching is started
|
||||||
|
extensionDiscovery.isLoaded = true;
|
||||||
|
|
||||||
|
await extensionDiscovery.watchExtensions();
|
||||||
|
|
||||||
|
extensionDiscovery.events.on("add", extension => {
|
||||||
|
expect(extension).toEqual({
|
||||||
|
absolutePath: expect.any(String),
|
||||||
|
id: path.normalize("node_modules/my-extension/package.json"),
|
||||||
|
isBundled: false,
|
||||||
|
isEnabled: false,
|
||||||
|
isCompatible: false,
|
||||||
|
manifest: {
|
||||||
|
name: "my-extension",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
manifestPath: path.normalize("node_modules/my-extension/package.json"),
|
||||||
|
availableUpdate: null,
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't emit add for added file under extension", async done => {
|
it("doesn't emit add for added file under extension", async done => {
|
||||||
@ -150,7 +149,7 @@ describe("ExtensionDiscovery", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
mockedWatch.mockImplementationOnce(() =>
|
mockedWatch.mockImplementationOnce(() =>
|
||||||
(mockWatchInstance) as any,
|
(mockWatchInstance) as any,
|
||||||
);
|
);
|
||||||
const extensionDiscovery = ExtensionDiscovery.createInstance(
|
const extensionDiscovery = ExtensionDiscovery.createInstance(
|
||||||
extensionLoader,
|
extensionLoader,
|
||||||
@ -173,3 +172,4 @@ describe("ExtensionDiscovery", () => {
|
|||||||
}, 10);
|
}, 10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { Injectable } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
|
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
|
||||||
|
|
||||||
|
type FactoryType = <
|
||||||
|
TInjectable extends Injectable<unknown, TInstance, TInstantiationParameter>,
|
||||||
|
TInstantiationParameter,
|
||||||
|
TInstance extends (...args: unknown[]) => any,
|
||||||
|
TFunction extends (...args: unknown[]) => any = Awaited<
|
||||||
|
ReturnType<TInjectable["instantiate"]>
|
||||||
|
>,
|
||||||
|
>(
|
||||||
|
injectableKey: TInjectable,
|
||||||
|
...instantiationParameter: TentativeTuple<TInstantiationParameter>
|
||||||
|
) => (...args: Parameters<TFunction>) => ReturnType<TFunction>;
|
||||||
|
|
||||||
|
export const asLegacyGlobalFunctionForExtensionApi: FactoryType =
|
||||||
|
(injectableKey, ...instantiationParameter) =>
|
||||||
|
(...args) => {
|
||||||
|
const injected = getLegacyGlobalDiForExtensionApi().inject(
|
||||||
|
injectableKey,
|
||||||
|
...instantiationParameter,
|
||||||
|
);
|
||||||
|
|
||||||
|
return injected(...args);
|
||||||
|
};
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { Injectable } from "@ogre-tools/injectable";
|
||||||
|
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
|
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
|
||||||
|
|
||||||
|
export const asLegacyGlobalObjectForExtensionApi = <
|
||||||
|
TInjectable extends Injectable<unknown, unknown, TInstantiationParameter>,
|
||||||
|
TInstantiationParameter,
|
||||||
|
>(
|
||||||
|
injectableKey: TInjectable,
|
||||||
|
...instantiationParameter: TentativeTuple<TInstantiationParameter>
|
||||||
|
) =>
|
||||||
|
new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get(target, propertyName) {
|
||||||
|
if (propertyName === "$$typeof") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance: any = getLegacyGlobalDiForExtensionApi().inject(
|
||||||
|
injectableKey,
|
||||||
|
...instantiationParameter,
|
||||||
|
);
|
||||||
|
|
||||||
|
const propertyValue = instance[propertyName];
|
||||||
|
|
||||||
|
if (typeof propertyValue === "function") {
|
||||||
|
return function (...args: any[]) {
|
||||||
|
return propertyValue.apply(instance, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return propertyValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
) as ReturnType<TInjectable["instantiate"]>;
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { Injectable } from "@ogre-tools/injectable";
|
||||||
|
import { getLegacyGlobalDiForExtensionApi } from "./legacy-global-di-for-extension-api";
|
||||||
|
|
||||||
|
type TentativeTuple<T> = T extends object ? [T] : [undefined?];
|
||||||
|
|
||||||
|
export const asLegacyGlobalSingletonForExtensionApi = <
|
||||||
|
TClass extends abstract new (...args: any[]) => any,
|
||||||
|
TInjectable extends Injectable<unknown, unknown, TInstantiationParameter>,
|
||||||
|
TInstantiationParameter,
|
||||||
|
>(
|
||||||
|
Class: TClass,
|
||||||
|
injectableKey: TInjectable,
|
||||||
|
...instantiationParameter: TentativeTuple<TInstantiationParameter>
|
||||||
|
) =>
|
||||||
|
new Proxy(Class, {
|
||||||
|
construct: () => {
|
||||||
|
throw new Error("A legacy singleton class must be created by createInstance()");
|
||||||
|
},
|
||||||
|
|
||||||
|
get: (target: any, propertyName) => {
|
||||||
|
if (propertyName === "getInstance" || propertyName === "createInstance") {
|
||||||
|
return () =>
|
||||||
|
getLegacyGlobalDiForExtensionApi().inject(
|
||||||
|
injectableKey,
|
||||||
|
...instantiationParameter,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyName === "resetInstance") {
|
||||||
|
return () => getLegacyGlobalDiForExtensionApi().purge(injectableKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[propertyName];
|
||||||
|
},
|
||||||
|
}) as InstanceType<TClass> & {
|
||||||
|
getInstance: () => InstanceType<TClass>;
|
||||||
|
createInstance: () => InstanceType<TClass>;
|
||||||
|
resetInstance: () => void;
|
||||||
|
};
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
|
||||||
|
let legacyGlobalDi: DependencyInjectionContainer;
|
||||||
|
|
||||||
|
export const setLegacyGlobalDiForExtensionApi = (di: DependencyInjectionContainer) => {
|
||||||
|
legacyGlobalDi = di;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLegacyGlobalDiForExtensionApi = () => legacyGlobalDi;
|
||||||
@ -18,8 +18,7 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
|
|
||||||
const extensionLoaderInjectable = getInjectable({
|
const extensionLoaderInjectable = getInjectable({
|
||||||
|
|||||||
@ -279,11 +279,7 @@ export class ExtensionLoader {
|
|||||||
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
|
||||||
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
|
||||||
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
|
||||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
|
||||||
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
|
|
||||||
registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners),
|
|
||||||
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
|
||||||
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
@ -315,7 +311,6 @@ export class ExtensionLoader {
|
|||||||
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
|
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
|
||||||
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
|
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
|
||||||
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
|
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
|
||||||
registries.CommandRegistry.getInstance().add(extension.commands),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
this.events.on("remove", (removedExtension: LensRendererExtension) => {
|
||||||
|
|||||||
@ -41,7 +41,6 @@ export const getDiForUnitTesting = () => {
|
|||||||
aliases: [injectable, ...(injectable.aliases || [])],
|
aliases: [injectable, ...(injectable.aliases || [])],
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
.forEach(injectable => di.register(injectable));
|
.forEach(injectable => di.register(injectable));
|
||||||
|
|
||||||
di.preventSideEffects();
|
di.preventSideEffects();
|
||||||
|
|||||||
@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog";
|
|||||||
import type { CatalogEntity } from "../common/catalog";
|
import type { CatalogEntity } from "../common/catalog";
|
||||||
import type { IObservableArray } from "mobx";
|
import type { IObservableArray } from "mobx";
|
||||||
import type { MenuRegistration } from "../main/menu/menu-registration";
|
import type { MenuRegistration } from "../main/menu/menu-registration";
|
||||||
|
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
appMenus: MenuRegistration[] = [];
|
appMenus: MenuRegistration[] = [];
|
||||||
|
trayMenus: TrayMenuRegistration[] = [];
|
||||||
|
|
||||||
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
|
||||||
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
|
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);
|
||||||
|
|||||||
@ -26,7 +26,11 @@ import type { CatalogEntity } from "../common/catalog";
|
|||||||
import type { Disposer } from "../common/utils";
|
import type { Disposer } from "../common/utils";
|
||||||
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
|
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
|
||||||
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
|
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
|
||||||
|
import type { TopBarRegistration } from "../renderer/components/layout/top-bar/top-bar-registration";
|
||||||
import type { KubernetesCluster } from "../common/catalog-entities";
|
import type { KubernetesCluster } from "../common/catalog-entities";
|
||||||
|
import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration";
|
||||||
|
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
|
||||||
|
import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands";
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
globalPages: registries.PageRegistration[] = [];
|
globalPages: registries.PageRegistration[] = [];
|
||||||
@ -39,11 +43,11 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
|
||||||
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
|
||||||
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
|
||||||
commands: registries.CommandRegistration[] = [];
|
commands: CommandRegistration[] = [];
|
||||||
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
|
welcomeMenus: WelcomeMenuRegistration[] = [];
|
||||||
welcomeBanners: registries.WelcomeBannerRegistration[] = [];
|
welcomeBanners: WelcomeBannerRegistration[] = [];
|
||||||
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
|
||||||
topBarItems: registries.TopBarRegistration[] = [];
|
topBarItems: TopBarRegistration[] = [];
|
||||||
|
|
||||||
async navigate<P extends object>(pageId?: string, params?: P) {
|
async navigate<P extends object>(pageId?: string, params?: P) {
|
||||||
const { navigate } = await import("../renderer/navigation");
|
const { navigate } = await import("../renderer/navigation");
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
// Base class for extensions-api registries
|
// Base class for extensions-api registries
|
||||||
import { action, observable, makeObservable } from "mobx";
|
import { action, observable, makeObservable } from "mobx";
|
||||||
import { Singleton } from "../../common/utils";
|
import { Singleton } from "../../common/utils";
|
||||||
import { LensExtension } from "../lens-extension";
|
import type { LensExtension } from "../lens-extension";
|
||||||
|
|
||||||
export class BaseRegistry<T, I = T> extends Singleton {
|
export class BaseRegistry<T, I = T> extends Singleton {
|
||||||
private items = observable.map<T, I>([], { deep: false });
|
private items = observable.map<T, I>([], { deep: false });
|
||||||
|
|||||||
@ -54,7 +54,7 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getItemsForKind(kind: string, apiVersion: string, source?: string) {
|
getItemsForKind = (kind: string, apiVersion: string, source?: string) => {
|
||||||
let items = this.getItems().filter((item) => {
|
let items = this.getItems().filter((item) => {
|
||||||
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
return item.kind === kind && item.apiVersions.includes(apiVersion);
|
||||||
});
|
});
|
||||||
@ -66,5 +66,5 @@ export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
return items.sort((a, b) => (b.priority ?? 50) - (a.priority ?? 50));
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,11 +28,7 @@ export * from "./status-bar-registry";
|
|||||||
export * from "./kube-object-detail-registry";
|
export * from "./kube-object-detail-registry";
|
||||||
export * from "./kube-object-menu-registry";
|
export * from "./kube-object-menu-registry";
|
||||||
export * from "./kube-object-status-registry";
|
export * from "./kube-object-status-registry";
|
||||||
export * from "./command-registry";
|
|
||||||
export * from "./entity-setting-registry";
|
export * from "./entity-setting-registry";
|
||||||
export * from "./welcome-menu-registry";
|
|
||||||
export * from "./welcome-banner-registry";
|
|
||||||
export * from "./catalog-entity-detail-registry";
|
export * from "./catalog-entity-detail-registry";
|
||||||
export * from "./workloads-overview-detail-registry";
|
export * from "./workloads-overview-detail-registry";
|
||||||
export * from "./topbar-registry";
|
|
||||||
export * from "./protocol-handler";
|
export * from "./protocol-handler";
|
||||||
|
|||||||
@ -135,10 +135,7 @@ class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (notAStringValue && !(parse || stringify)) {
|
if (notAStringValue && !(parse || stringify)) {
|
||||||
throw new Error(
|
throw new Error(`PageRegistry: param's "${paramName}" initialization has failed: paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`);
|
||||||
`PageRegistry: param's "${paramName}" initialization has failed:
|
|
||||||
paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
paramInit.defaultValue = value;
|
paramInit.defaultValue = value;
|
||||||
|
|||||||
@ -19,6 +19,9 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import commandOverlayInjectable from "../../renderer/components/command-palette/command-overlay.injectable";
|
||||||
|
import { asLegacyGlobalObjectForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||||
|
|
||||||
// layouts
|
// layouts
|
||||||
export * from "../../renderer/components/layout/main-layout";
|
export * from "../../renderer/components/layout/main-layout";
|
||||||
export * from "../../renderer/components/layout/setting-layout";
|
export * from "../../renderer/components/layout/setting-layout";
|
||||||
@ -36,7 +39,7 @@ export * from "../../renderer/components/switch";
|
|||||||
export * from "../../renderer/components/input/input";
|
export * from "../../renderer/components/input/input";
|
||||||
|
|
||||||
// command-overlay
|
// command-overlay
|
||||||
export { CommandOverlay } from "../../renderer/components/command-palette";
|
export const CommandOverlay = asLegacyGlobalObjectForExtensionApi(commandOverlayInjectable);
|
||||||
|
|
||||||
// other components
|
// other components
|
||||||
export * from "../../renderer/components/icon";
|
export * from "../../renderer/components/icon";
|
||||||
|
|||||||
33
src/extensions/renderer-extensions.injectable.ts
Normal file
33
src/extensions/renderer-extensions.injectable.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import extensionsInjectable from "./extensions.injectable";
|
||||||
|
import type { LensRendererExtension } from "./lens-renderer-extension";
|
||||||
|
|
||||||
|
const rendererExtensionsInjectable = getInjectable({
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|
||||||
|
instantiate: (di) =>
|
||||||
|
di.inject(extensionsInjectable) as IComputedValue<LensRendererExtension[]>,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default rendererExtensionsInjectable;
|
||||||
@ -81,7 +81,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
let contextHandler: ContextHandler;
|
let contextHandler: ContextHandler;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const mockOpts = {
|
mockFs({
|
||||||
"minikube-config.yml": JSON.stringify({
|
"minikube-config.yml": JSON.stringify({
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
clusters: [{
|
clusters: [{
|
||||||
@ -103,9 +103,7 @@ describe("kubeconfig manager tests", () => {
|
|||||||
kind: "Config",
|
kind: "Config",
|
||||||
preferences: {},
|
preferences: {},
|
||||||
}),
|
}),
|
||||||
};
|
});
|
||||||
|
|
||||||
mockFs(mockOpts);
|
|
||||||
|
|
||||||
cluster = new Cluster({
|
cluster = new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
|
|||||||
@ -25,10 +25,9 @@ import { isLinux, isMac, isPublishConfigured, isTestEnv } from "../common/vars";
|
|||||||
import { delay } from "../common/utils";
|
import { delay } from "../common/utils";
|
||||||
import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain, autoUpdater as electronAutoUpdater } from "electron";
|
||||||
import { nextUpdateChannel } from "./utils/update-channel";
|
import { nextUpdateChannel } from "./utils/update-channel";
|
||||||
import { UserStore } from "../common/user-store";
|
import { UserStore } from "../common/user-store";
|
||||||
import { autoUpdater as electronAutoUpdater } from "electron";
|
|
||||||
|
|
||||||
let installVersion: null | string = null;
|
let installVersion: null | string = null;
|
||||||
|
|
||||||
|
|||||||
@ -24,11 +24,15 @@ import { createContainer } from "@ogre-tools/injectable";
|
|||||||
export const getDi = () =>
|
export const getDi = () =>
|
||||||
createContainer(
|
createContainer(
|
||||||
getRequireContextForMainCode,
|
getRequireContextForMainCode,
|
||||||
|
getRequireContextForCommonCode,
|
||||||
getRequireContextForCommonExtensionCode,
|
getRequireContextForCommonExtensionCode,
|
||||||
);
|
);
|
||||||
|
|
||||||
const getRequireContextForMainCode = () =>
|
const getRequireContextForMainCode = () =>
|
||||||
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
require.context("./", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
|
const getRequireContextForCommonCode = () =>
|
||||||
|
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|
||||||
const getRequireContextForCommonExtensionCode = () =>
|
const getRequireContextForCommonExtensionCode = () =>
|
||||||
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as tempy from "tempy";
|
import tempy from "tempy";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import { promiseExecFile } from "../../common/utils/promise-exec";
|
import { promiseExecFile } from "../../common/utils/promise-exec";
|
||||||
|
|||||||
@ -119,7 +119,9 @@ export class HelmRepoManager extends Singleton {
|
|||||||
if (typeof parsedConfig === "object" && parsedConfig) {
|
if (typeof parsedConfig === "object" && parsedConfig) {
|
||||||
return parsedConfig as HelmRepoConfig;
|
return parsedConfig as HelmRepoConfig;
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch {
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
repositories: [],
|
repositories: [],
|
||||||
|
|||||||
@ -60,7 +60,7 @@ import { SentryInit } from "../common/sentry";
|
|||||||
import { ensureDir } from "fs-extra";
|
import { ensureDir } from "fs-extra";
|
||||||
import { Router } from "./router";
|
import { Router } from "./router";
|
||||||
import { initMenu } from "./menu/menu";
|
import { initMenu } from "./menu/menu";
|
||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray/tray";
|
||||||
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
|
||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../common/app-paths";
|
||||||
import { ShellSession } from "./shell-session/shell-session";
|
import { ShellSession } from "./shell-session/shell-session";
|
||||||
@ -68,6 +68,7 @@ import { getDi } from "./getDi";
|
|||||||
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
|
||||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||||
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
|
||||||
|
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
|
||||||
|
|
||||||
const di = getDi();
|
const di = getDi();
|
||||||
|
|
||||||
@ -104,6 +105,7 @@ mangleProxyEnv();
|
|||||||
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
logger.debug("[APP-MAIN] initializing ipc main handlers");
|
||||||
|
|
||||||
const menuItems = di.inject(electronMenuItemsInjectable);
|
const menuItems = di.inject(electronMenuItemsInjectable);
|
||||||
|
const trayMenuItems = di.inject(trayMenuItemsInjectable);
|
||||||
|
|
||||||
initializers.initIpcMainHandlers(menuItems);
|
initializers.initIpcMainHandlers(menuItems);
|
||||||
|
|
||||||
@ -244,7 +246,7 @@ app.on("ready", async () => {
|
|||||||
|
|
||||||
onQuitCleanup.push(
|
onQuitCleanup.push(
|
||||||
initMenu(windowManager, menuItems),
|
initMenu(windowManager, menuItems),
|
||||||
initTray(windowManager),
|
initTray(windowManager, trayMenuItems),
|
||||||
() => ShellSession.cleanup(),
|
() => ShellSession.cleanup(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -97,7 +97,9 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegist
|
|||||||
const localStorageFilePath = path.resolve(AppPaths.get("userData"), "lens-local-storage", `${cluster.id}.json`);
|
const localStorageFilePath = path.resolve(AppPaths.get("userData"), "lens-local-storage", `${cluster.id}.json`);
|
||||||
|
|
||||||
await remove(localStorageFilePath);
|
await remove(localStorageFilePath);
|
||||||
} catch {}
|
} catch {
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
|
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
|
||||||
|
|||||||
@ -27,12 +27,15 @@ import { ensureDir, pathExists } from "fs-extra";
|
|||||||
import * as lockFile from "proper-lockfile";
|
import * as lockFile from "proper-lockfile";
|
||||||
import { helmCli } from "./helm/helm-cli";
|
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 { getBundledKubectlVersion } from "../common/utils/app-version";
|
||||||
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
|
||||||
import { SemVer } from "semver";
|
import { SemVer } from "semver";
|
||||||
import { defaultPackageMirror, packageMirrors } from "../common/user-store/preferences-helpers";
|
import { defaultPackageMirror, packageMirrors } from "../common/user-store/preferences-helpers";
|
||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../common/app-paths";
|
||||||
|
import got from "got/dist/source";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import stream from "stream";
|
||||||
|
import { noop } from "../renderer/utils";
|
||||||
|
|
||||||
const bundledVersion = getBundledKubectlVersion();
|
const bundledVersion = getBundledKubectlVersion();
|
||||||
const kubectlMap: Map<string, string> = new Map([
|
const kubectlMap: Map<string, string> = new Map([
|
||||||
@ -53,7 +56,7 @@ const kubectlMap: Map<string, string> = new Map([
|
|||||||
["1.21", bundledVersion],
|
["1.21", bundledVersion],
|
||||||
]);
|
]);
|
||||||
let bundledPath: string;
|
let bundledPath: string;
|
||||||
const initScriptVersionString = "# lens-initscript v3\n";
|
const initScriptVersionString = "# lens-initscript v3";
|
||||||
|
|
||||||
export function bundledKubectlPath(): string {
|
export function bundledKubectlPath(): string {
|
||||||
if (bundledPath) { return bundledPath; }
|
if (bundledPath) { return bundledPath; }
|
||||||
@ -309,99 +312,92 @@ export class Kubectl {
|
|||||||
|
|
||||||
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
const downloadStream = got.stream({ url: this.url, decompress: true });
|
||||||
const stream = customRequest({
|
const fileWriteStream = fs.createWriteStream(this.path, { mode: 0o755 });
|
||||||
url: this.url,
|
const pipeline = promisify(stream.pipeline);
|
||||||
gzip: true,
|
|
||||||
});
|
|
||||||
const file = fs.createWriteStream(this.path);
|
|
||||||
|
|
||||||
stream.on("complete", () => {
|
try {
|
||||||
logger.debug("kubectl binary download finished");
|
await pipeline(downloadStream, fileWriteStream);
|
||||||
file.end();
|
await fs.promises.chmod(this.path, 0o755);
|
||||||
});
|
logger.debug("kubectl binary download finished");
|
||||||
stream.on("error", (error) => {
|
} catch (error) {
|
||||||
logger.error(error);
|
await fs.promises.unlink(this.path).catch(noop);
|
||||||
fs.unlink(this.path, () => {
|
throw error;
|
||||||
// do nothing
|
}
|
||||||
});
|
|
||||||
reject(error);
|
|
||||||
});
|
|
||||||
file.on("close", () => {
|
|
||||||
logger.debug("kubectl binary download closed");
|
|
||||||
fs.chmod(this.path, 0o755, (err) => {
|
|
||||||
if (err) reject(err);
|
|
||||||
});
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
stream.pipe(file);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async writeInitScripts() {
|
protected async writeInitScripts() {
|
||||||
const kubectlPath = UserStore.getInstance().downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
|
const kubectlPath = UserStore.getInstance().downloadKubectlBinaries
|
||||||
|
? this.dirname
|
||||||
|
: path.dirname(this.getPathFromPreferences());
|
||||||
const helmPath = helmCli.getBinaryDir();
|
const helmPath = helmCli.getBinaryDir();
|
||||||
const fsPromises = fs.promises;
|
|
||||||
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
|
||||||
let bashScript = `${initScriptVersionString}`;
|
const bashScript = [
|
||||||
|
initScriptVersionString,
|
||||||
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
"tempkubeconfig=\"$KUBECONFIG\"",
|
||||||
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
|
"test -f \"/etc/profile\" && . \"/etc/profile\"",
|
||||||
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
|
"if test -f \"$HOME/.bash_profile\"; then",
|
||||||
bashScript += " . \"$HOME/.bash_profile\"\n";
|
" . \"$HOME/.bash_profile\"",
|
||||||
bashScript += "elif test -f \"$HOME/.bash_login\"; then\n";
|
"elif test -f \"$HOME/.bash_login\"; then",
|
||||||
bashScript += " . \"$HOME/.bash_login\"\n";
|
" . \"$HOME/.bash_login\"",
|
||||||
bashScript += "elif test -f \"$HOME/.profile\"; then\n";
|
"elif test -f \"$HOME/.profile\"; then",
|
||||||
bashScript += " . \"$HOME/.profile\"\n";
|
" . \"$HOME/.profile\"",
|
||||||
bashScript += "fi\n";
|
"fi",
|
||||||
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
|
`export PATH="${helmPath}:${kubectlPath}:$PATH"`,
|
||||||
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
|
'export KUBECONFIG="$tempkubeconfig"',
|
||||||
|
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
|
||||||
bashScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`;
|
`NO_PROXY="\${NO_PROXY//,localhost,/,}"`,
|
||||||
bashScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`;
|
`NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"`,
|
||||||
bashScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`;
|
`NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"`,
|
||||||
bashScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`;
|
"export NO_PROXY",
|
||||||
bashScript += "export NO_PROXY\n";
|
"unset tempkubeconfig",
|
||||||
bashScript += "unset tempkubeconfig\n";
|
].join("\n");
|
||||||
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
|
|
||||||
|
|
||||||
const zshScriptPath = path.join(this.dirname, ".zlogin");
|
const zshScriptPath = path.join(this.dirname, ".zlogin");
|
||||||
let zshScript = `${initScriptVersionString}`;
|
const zshScript = [
|
||||||
|
initScriptVersionString,
|
||||||
|
"tempkubeconfig=\"$KUBECONFIG\"",
|
||||||
|
|
||||||
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
|
// restore previous ZDOTDIR
|
||||||
// restore previous ZDOTDIR
|
"export ZDOTDIR=\"$OLD_ZDOTDIR\"",
|
||||||
zshScript += "export ZDOTDIR=\"$OLD_ZDOTDIR\"\n";
|
|
||||||
// source all the files
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"\n";
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"\n";
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"\n";
|
|
||||||
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";
|
|
||||||
|
|
||||||
// voodoo to replace any previous occurrences of kubectl path in the PATH
|
// source all the files
|
||||||
zshScript += `kubectlpath=\"${kubectlPath}"\n`;
|
"test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"",
|
||||||
zshScript += `helmpath=\"${helmPath}"\n`;
|
"test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"",
|
||||||
zshScript += "p=\":$kubectlpath:\"\n";
|
"test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"",
|
||||||
zshScript += "d=\":$PATH:\"\n";
|
"test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"",
|
||||||
zshScript += `d=\${d//$p/:}\n`;
|
|
||||||
zshScript += `d=\${d/#:/}\n`;
|
// voodoo to replace any previous occurrences of kubectl path in the PATH
|
||||||
zshScript += `export PATH=\"$helmpath:$kubectlpath:\${d/%:/}\"\n`;
|
`kubectlpath="${kubectlPath}"`,
|
||||||
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
|
`helmpath="${helmPath}"`,
|
||||||
zshScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`;
|
"p=\":$kubectlpath:\"",
|
||||||
zshScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`;
|
"d=\":$PATH:\"",
|
||||||
zshScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`;
|
`d=\${d//$p/:}`,
|
||||||
zshScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`;
|
`d=\${d/#:/}`,
|
||||||
zshScript += "export NO_PROXY\n";
|
`export PATH="$helmpath:$kubectlpath:\${d/%:/}"`,
|
||||||
zshScript += "unset tempkubeconfig\n";
|
"export KUBECONFIG=\"$tempkubeconfig\"",
|
||||||
zshScript += "unset OLD_ZDOTDIR\n";
|
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
|
||||||
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
|
`NO_PROXY="\${NO_PROXY//,localhost,/,}"`,
|
||||||
|
`NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"`,
|
||||||
|
`NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"`,
|
||||||
|
"export NO_PROXY",
|
||||||
|
"unset tempkubeconfig",
|
||||||
|
"unset OLD_ZDOTDIR",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
fs.promises.writeFile(bashScriptPath, bashScript, { mode: 0o644 }),
|
||||||
|
fs.promises.writeFile(zshScriptPath, zshScript, { mode: 0o644 }),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getDownloadMirror(): string {
|
protected getDownloadMirror(): string {
|
||||||
// MacOS packages are only available from default
|
// MacOS packages are only available from default
|
||||||
|
|
||||||
const mirror = packageMirrors.get(UserStore.getInstance().downloadMirror)
|
const { url } = packageMirrors.get(UserStore.getInstance().downloadMirror)
|
||||||
?? packageMirrors.get(defaultPackageMirror);
|
?? packageMirrors.get(defaultPackageMirror);
|
||||||
|
|
||||||
return mirror.url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({
|
|||||||
const extensions = di.inject(mainExtensionsInjectable);
|
const extensions = di.inject(mainExtensionsInjectable);
|
||||||
|
|
||||||
return computed(() =>
|
return computed(() =>
|
||||||
extensions.get().flatMap((extension) => extension.appMenus),
|
extensions.get().flatMap((extension) => extension.appMenus));
|
||||||
);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -216,8 +216,16 @@ export function getAppMenu(
|
|||||||
label: "Command Palette...",
|
label: "Command Palette...",
|
||||||
accelerator: "Shift+CmdOrCtrl+P",
|
accelerator: "Shift+CmdOrCtrl+P",
|
||||||
id: "command-palette",
|
id: "command-palette",
|
||||||
click() {
|
click(_m, _b, event) {
|
||||||
broadcastMessage("command-palette:open");
|
/**
|
||||||
|
* Don't broadcast unless it was triggered by menu iteration so that
|
||||||
|
* there aren't double events in renderer
|
||||||
|
*
|
||||||
|
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
|
||||||
|
*/
|
||||||
|
if (!event?.triggeredByAccelerator) {
|
||||||
|
broadcastMessage("command-palette:open");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "cluster":
|
case "cluster":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`);
|
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
|
||||||
case "memoryRequests":
|
case "memoryRequests":
|
||||||
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
case "cpuRequests":
|
case "cpuRequests":
|
||||||
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
|
||||||
case "cpuLimits":
|
case "cpuLimits":
|
||||||
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
|
|||||||
case "podAllocatableCapacity":
|
case "podAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "nodes":
|
case "nodes":
|
||||||
switch (queryName) {
|
switch (queryName) {
|
||||||
case "memoryUsage":
|
case "memoryUsage":
|
||||||
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`;
|
return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "workloadMemoryUsage":
|
case "workloadMemoryUsage":
|
||||||
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
|
return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
|
||||||
case "memoryCapacity":
|
case "memoryCapacity":
|
||||||
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
|
||||||
case "memoryAllocatableCapacity":
|
case "memoryAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
|
||||||
case "cpuUsage":
|
case "cpuUsage":
|
||||||
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`;
|
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "cpuCapacity":
|
case "cpuCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "cpuAllocatableCapacity":
|
case "cpuAllocatableCapacity":
|
||||||
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
|
||||||
case "fsSize":
|
case "fsSize":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`;
|
return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
case "fsUsage":
|
case "fsUsage":
|
||||||
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`;
|
return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "pods":
|
case "pods":
|
||||||
|
|||||||
@ -25,7 +25,7 @@ import { exec } from "child_process";
|
|||||||
import fs from "fs-extra";
|
import fs from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as tempy from "tempy";
|
import tempy from "tempy";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { cloneJsonObject } from "../common/utils";
|
import { cloneJsonObject } from "../common/utils";
|
||||||
|
|||||||
@ -182,7 +182,6 @@ export class Router {
|
|||||||
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
// Port-forward API (the container port and local forwarding port are obtained from the query parameters)
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routePortForward);
|
this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routePortForward);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward);
|
||||||
this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forwards` }, PortForwardRoute.routeAllPortForwards);
|
|
||||||
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop);
|
||||||
|
|
||||||
// Helm API
|
// Helm API
|
||||||
|
|||||||
@ -188,31 +188,6 @@ export class PortForwardRoute {
|
|||||||
respondJson(response, { port: portForward?.forwardPort ?? null });
|
respondJson(response, { port: portForward?.forwardPort ?? null });
|
||||||
}
|
}
|
||||||
|
|
||||||
static async routeAllPortForwards(request: LensApiRequest) {
|
|
||||||
const { query, response } = request;
|
|
||||||
const clusterId = query.get("clusterId");
|
|
||||||
|
|
||||||
let portForwards: PortForwardArgs[] = PortForward.portForwards.map(f => (
|
|
||||||
{
|
|
||||||
clusterId: f.clusterId,
|
|
||||||
kind: f.kind,
|
|
||||||
namespace: f.namespace,
|
|
||||||
name: f.name,
|
|
||||||
port: f.port,
|
|
||||||
forwardPort: f.forwardPort,
|
|
||||||
protocol: f.protocol,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (clusterId) {
|
|
||||||
// filter out any not for this cluster
|
|
||||||
portForwards = portForwards.filter(pf => pf.clusterId == clusterId);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
respondJson(response, { portForwards });
|
|
||||||
}
|
|
||||||
|
|
||||||
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
static async routeCurrentPortForwardStop(request: LensApiRequest) {
|
||||||
const { params, query, response, cluster } = request;
|
const { params, query, response, cluster } = request;
|
||||||
const { namespace, resourceType, resourceName } = params;
|
const { namespace, resourceType, resourceName } = params;
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export class NodeShellSession extends ShellSession {
|
|||||||
switch (nodeOs) {
|
switch (nodeOs) {
|
||||||
default:
|
default:
|
||||||
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
|
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
|
||||||
|
// fallthrough
|
||||||
case "linux":
|
case "linux":
|
||||||
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
|
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -134,7 +134,9 @@ export abstract class ShellSession {
|
|||||||
for (const shellProcess of this.processes.values()) {
|
for (const shellProcess of this.processes.values()) {
|
||||||
try {
|
try {
|
||||||
process.kill(shellProcess.pid);
|
process.kill(shellProcess.pid);
|
||||||
} catch {}
|
} catch {
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processes.clear();
|
this.processes.clear();
|
||||||
@ -214,7 +216,9 @@ export abstract class ShellSession {
|
|||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
return potentialCwd;
|
return potentialCwd;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "."; // Always valid
|
return "."; // Always valid
|
||||||
|
|||||||
@ -18,19 +18,19 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
|
||||||
|
|
||||||
import { HotbarStore } from "../../../common/hotbar-store";
|
const trayItemsInjectable = getInjectable({
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
|
||||||
function hotbarIndex(id: string) {
|
instantiate: (di) => {
|
||||||
return HotbarStore.getInstance().hotbarIndex(id) + 1;
|
const extensions = di.inject(mainExtensionsInjectable);
|
||||||
}
|
|
||||||
|
|
||||||
export function hotbarDisplayLabel(id: string) : string {
|
return computed(() =>
|
||||||
const hotbar = HotbarStore.getInstance().getById(id);
|
extensions.get().flatMap(extension => extension.trayMenus));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return `${hotbarIndex(id)}: ${hotbar.name}`;
|
export default trayItemsInjectable;
|
||||||
}
|
|
||||||
|
|
||||||
export function hotbarDisplayIndex(id: string) : string {
|
|
||||||
return hotbarIndex(id).toString();
|
|
||||||
}
|
|
||||||
136
src/main/tray/tray-menu-items.test.ts
Normal file
136
src/main/tray/tray-menu-items.test.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import { LensMainExtension } from "../../extensions/lens-main-extension";
|
||||||
|
import trayItemsInjectable from "./tray-menu-items.injectable";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
|
import { computed, ObservableMap, runInAction } from "mobx";
|
||||||
|
import { getDiForUnitTesting } from "../getDiForUnitTesting";
|
||||||
|
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
|
||||||
|
import type { TrayMenuRegistration } from "./tray-menu-registration";
|
||||||
|
|
||||||
|
describe("tray-menu-items", () => {
|
||||||
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
|
let trayMenuItems: IComputedValue<TrayMenuRegistration[]>;
|
||||||
|
let extensionsStub: ObservableMap<string, LensMainExtension>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
di = getDiForUnitTesting();
|
||||||
|
|
||||||
|
extensionsStub = new ObservableMap();
|
||||||
|
|
||||||
|
di.override(
|
||||||
|
mainExtensionsInjectable,
|
||||||
|
() => computed(() => [...extensionsStub.values()]),
|
||||||
|
);
|
||||||
|
|
||||||
|
trayMenuItems = di.inject(trayItemsInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not have any items yet", () => {
|
||||||
|
expect(trayMenuItems.get()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when extension is enabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someExtension = new SomeTestExtension({
|
||||||
|
id: "some-extension-id",
|
||||||
|
trayMenus: [{ label: "tray-menu-from-some-extension" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.set("some-extension-id", someExtension);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has tray menu items", () => {
|
||||||
|
expect(trayMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
label: "tray-menu-from-some-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when disabling extension, does not have tray menu items", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.delete("some-extension-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trayMenuItems.get()).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when other extension is enabled", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const someOtherExtension = new SomeTestExtension({
|
||||||
|
id: "some-extension-id",
|
||||||
|
trayMenus: [{ label: "some-label-from-second-extension" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.set("some-other-extension-id", someOtherExtension);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("has tray menu items for both extensions", () => {
|
||||||
|
expect(trayMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
label: "tray-menu-from-some-extension",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "some-label-from-second-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("when extension is disabled, still returns tray menu items for extensions that are enabled", () => {
|
||||||
|
runInAction(() => {
|
||||||
|
extensionsStub.delete("some-other-extension-id");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(trayMenuItems.get()).toEqual([
|
||||||
|
{
|
||||||
|
label: "tray-menu-from-some-extension",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class SomeTestExtension extends LensMainExtension {
|
||||||
|
constructor({ id, trayMenus }: {
|
||||||
|
id: string;
|
||||||
|
trayMenus: TrayMenuRegistration[];
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
id,
|
||||||
|
absolutePath: "irrelevant",
|
||||||
|
isBundled: false,
|
||||||
|
isCompatible: false,
|
||||||
|
isEnabled: false,
|
||||||
|
manifest: { name: id, version: "some-version" },
|
||||||
|
manifestPath: "irrelevant",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.trayMenus = trayMenus;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,16 +19,12 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type React from "react";
|
export interface TrayMenuRegistration {
|
||||||
import { BaseRegistry } from "./base-registry";
|
label?: string;
|
||||||
|
click?: (menuItem: TrayMenuRegistration) => void;
|
||||||
interface TopBarComponents {
|
id?: string;
|
||||||
Item: React.ComponentType;
|
type?: "normal" | "separator" | "submenu"
|
||||||
}
|
toolTip?: string;
|
||||||
|
enabled?: boolean;
|
||||||
export interface TopBarRegistration {
|
submenu?: TrayMenuRegistration[]
|
||||||
components: TopBarComponents;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TopBarRegistry extends BaseRegistry<TopBarRegistration> {
|
|
||||||
}
|
}
|
||||||
@ -20,16 +20,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../../package.json";
|
||||||
import { Menu, Tray } from "electron";
|
import { Menu, Tray } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun, IComputedValue } from "mobx";
|
||||||
import { showAbout } from "./menu/menu";
|
import { showAbout } from "../menu/menu";
|
||||||
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
|
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
|
||||||
import type { WindowManager } from "./window-manager";
|
import type { WindowManager } from "../window-manager";
|
||||||
import logger from "./logger";
|
import logger from "../logger";
|
||||||
import { isDevelopment, isWindows, productName } from "../common/vars";
|
import { isDevelopment, isWindows, productName } from "../../common/vars";
|
||||||
import { exitApp } from "./exit-app";
|
import { exitApp } from "../exit-app";
|
||||||
import { preferencesURL } from "../common/routes";
|
import { preferencesURL } from "../../common/routes";
|
||||||
|
import { toJS } from "../../common/utils";
|
||||||
|
import type { TrayMenuRegistration } from "./tray-menu-registration";
|
||||||
|
|
||||||
const TRAY_LOG_PREFIX = "[TRAY]";
|
const TRAY_LOG_PREFIX = "[TRAY]";
|
||||||
|
|
||||||
@ -44,7 +46,10 @@ export function getTrayIcon(): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initTray(windowManager: WindowManager) {
|
export function initTray(
|
||||||
|
windowManager: WindowManager,
|
||||||
|
trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
|
||||||
|
) {
|
||||||
const icon = getTrayIcon();
|
const icon = getTrayIcon();
|
||||||
|
|
||||||
tray = new Tray(icon);
|
tray = new Tray(icon);
|
||||||
@ -62,7 +67,7 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
const disposers = [
|
const disposers = [
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
try {
|
try {
|
||||||
const menu = createTrayMenu(windowManager);
|
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()));
|
||||||
|
|
||||||
tray.setContextMenu(menu);
|
tray.setContextMenu(menu);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -78,8 +83,21 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTrayMenu(windowManager: WindowManager): Menu {
|
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {
|
||||||
const template: Electron.MenuItemConstructorOptions[] = [
|
return {
|
||||||
|
...trayItem,
|
||||||
|
submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
|
||||||
|
click: trayItem.click ? () => {
|
||||||
|
trayItem.click(trayItem);
|
||||||
|
} : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTrayMenu(
|
||||||
|
windowManager: WindowManager,
|
||||||
|
extensionTrayItems: TrayMenuRegistration[],
|
||||||
|
): Menu {
|
||||||
|
let template: Electron.MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: `Open ${productName}`,
|
label: `Open ${productName}`,
|
||||||
click() {
|
click() {
|
||||||
@ -108,6 +126,8 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
|
||||||
|
|
||||||
return Menu.buildFromTemplate(template.concat([
|
return Menu.buildFromTemplate(template.concat([
|
||||||
{
|
{
|
||||||
label: `About ${productName}`,
|
label: `About ${productName}`,
|
||||||
@ -33,7 +33,7 @@ export default {
|
|||||||
const contextName = value[0];
|
const contextName = value[0];
|
||||||
|
|
||||||
// Looping all the keys gives out the store internal stuff too...
|
// Looping all the keys gives out the store internal stuff too...
|
||||||
if (contextName === "__internal__" || value[1].hasOwnProperty("kubeConfig")) continue;
|
if (contextName === "__internal__" || Object.prototype.hasOwnProperty.call(value[1], "kubeConfig")) continue;
|
||||||
store.set(contextName, { kubeConfig: value[1] });
|
store.set(contextName, { kubeConfig: value[1] });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export default {
|
|||||||
if (!cluster.kubeConfig) continue;
|
if (!cluster.kubeConfig) continue;
|
||||||
const config = yaml.load(cluster.kubeConfig);
|
const config = yaml.load(cluster.kubeConfig);
|
||||||
|
|
||||||
if (!config || typeof config !== "object" || !config.hasOwnProperty("users")) {
|
if (!config || typeof config !== "object" || !Object.prototype.hasOwnProperty.call(config, "users")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
import { CatalogEntityRegistry } from "../catalog-entity-registry";
|
||||||
import "../../../common/catalog-entities";
|
|
||||||
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
|
||||||
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
|
||||||
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";
|
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed, observable, makeObservable, action } from "mobx";
|
import { computed, observable, makeObservable, action } from "mobx";
|
||||||
import { ipcRendererOn } from "../../common/ipc";
|
import { catalogEntityRunListener, ipcRendererOn } from "../../common/ipc";
|
||||||
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog";
|
||||||
import "../../common/catalog-entities";
|
import "../../common/catalog-entities";
|
||||||
import type { Cluster } from "../../main/cluster";
|
import type { Cluster } from "../../main/cluster";
|
||||||
@ -28,14 +28,22 @@ import { ClusterStore } from "../../common/cluster-store";
|
|||||||
import { Disposer, iter } from "../utils";
|
import { Disposer, iter } from "../utils";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import logger from "../../common/logger";
|
import logger from "../../common/logger";
|
||||||
import { catalogEntityRunContext } from "./catalog-entity";
|
|
||||||
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
import { CatalogIpcEvents } from "../../common/ipc/catalog";
|
||||||
|
import { navigate } from "../navigation";
|
||||||
|
import { isMainFrame } from "process";
|
||||||
|
|
||||||
export type EntityFilter = (entity: CatalogEntity) => any;
|
export type EntityFilter = (entity: CatalogEntity) => any;
|
||||||
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
|
||||||
|
|
||||||
|
export const catalogEntityRunContext = {
|
||||||
|
navigate: (url: string) => navigate(url),
|
||||||
|
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
||||||
|
catalogEntityRegistry.activeEntity = entity;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export class CatalogEntityRegistry {
|
export class CatalogEntityRegistry {
|
||||||
@observable protected activeEntityId: string | undefined = undefined;
|
@observable protected activeEntityId: string | undefined = undefined;
|
||||||
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
protected _entities = observable.map<string, CatalogEntity>([], { deep: true });
|
||||||
@ -78,6 +86,16 @@ export class CatalogEntityRegistry {
|
|||||||
|
|
||||||
// Make sure that we get items ASAP and not the next time one of them changes
|
// Make sure that we get items ASAP and not the next time one of them changes
|
||||||
ipcRenderer.send(CatalogIpcEvents.INIT);
|
ipcRenderer.send(CatalogIpcEvents.INIT);
|
||||||
|
|
||||||
|
if (isMainFrame) {
|
||||||
|
ipcRendererOn(catalogEntityRunListener, (event, id: string) => {
|
||||||
|
const entity = this.getById(id);
|
||||||
|
|
||||||
|
if (entity) {
|
||||||
|
this.onRun(entity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
@action updateItems(items: (CatalogEntityData & CatalogEntityKindData)[]) {
|
||||||
|
|||||||
@ -19,10 +19,7 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { navigate } from "../navigation";
|
export { catalogEntityRunContext } from "./catalog-entity-registry";
|
||||||
import type { CatalogEntity } from "../../common/catalog";
|
|
||||||
import { catalogEntityRegistry } from "./catalog-entity-registry";
|
|
||||||
|
|
||||||
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
|
||||||
export type {
|
export type {
|
||||||
CatalogEntityData,
|
CatalogEntityData,
|
||||||
@ -33,10 +30,3 @@ export type {
|
|||||||
CatalogEntityContextMenu,
|
CatalogEntityContextMenu,
|
||||||
CatalogEntityContextMenuContext,
|
CatalogEntityContextMenuContext,
|
||||||
} from "../../common/catalog";
|
} from "../../common/catalog";
|
||||||
|
|
||||||
export const catalogEntityRunContext = {
|
|
||||||
navigate: (url: string) => navigate(url),
|
|
||||||
setCommandPaletteContext: (entity?: CatalogEntity) => {
|
|
||||||
catalogEntityRegistry.activeEntity = entity;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@ -21,13 +21,14 @@
|
|||||||
|
|
||||||
import { when } from "mobx";
|
import { when } from "mobx";
|
||||||
import { catalogCategoryRegistry } from "../../../common/catalog";
|
import { catalogCategoryRegistry } from "../../../common/catalog";
|
||||||
import { catalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../catalog-entity-registry";
|
||||||
import { isActiveRoute } from "../../../renderer/navigation";
|
import { isActiveRoute } from "../../navigation";
|
||||||
|
import type { GeneralEntity } from "../../../common/catalog-entities";
|
||||||
|
|
||||||
export async function setEntityOnRouteMatch() {
|
export async function setEntityOnRouteMatch() {
|
||||||
await when(() => catalogEntityRegistry.entities.size > 0);
|
await when(() => catalogEntityRegistry.entities.size > 0);
|
||||||
|
|
||||||
const entities = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General"));
|
const entities: GeneralEntity[] = catalogEntityRegistry.getItemsForCategory(catalogCategoryRegistry.getByName("General"));
|
||||||
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
const activeEntity = entities.find(entity => isActiveRoute(entity.spec.path));
|
||||||
|
|
||||||
if (activeEntity) {
|
if (activeEntity) {
|
||||||
@ -49,7 +49,7 @@ import { SentryInit } from "../common/sentry";
|
|||||||
import { TerminalStore } from "./components/dock/terminal.store";
|
import { TerminalStore } from "./components/dock/terminal.store";
|
||||||
import { AppPaths } from "../common/app-paths";
|
import { AppPaths } from "../common/app-paths";
|
||||||
import { registerCustomThemes } from "./components/monaco-editor";
|
import { registerCustomThemes } from "./components/monaco-editor";
|
||||||
import { getDi } from "./components/getDi";
|
import { getDi } from "./getDi";
|
||||||
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
import { DiContextProvider } from "@ogre-tools/injectable-react";
|
||||||
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
|
||||||
@ -59,6 +59,7 @@ import bindProtocolAddRouteHandlersInjectable
|
|||||||
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
import type { LensProtocolRouterRenderer } from "./protocol-handler";
|
||||||
import lensProtocolRouterRendererInjectable
|
import lensProtocolRouterRendererInjectable
|
||||||
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
|
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
|
||||||
|
import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable";
|
||||||
|
|
||||||
if (process.isMainFrame) {
|
if (process.isMainFrame) {
|
||||||
SentryInit();
|
SentryInit();
|
||||||
@ -102,9 +103,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
logger.info(`${logPrefix} initializing Registries`);
|
logger.info(`${logPrefix} initializing Registries`);
|
||||||
initializers.initRegistries();
|
initializers.initRegistries();
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing CommandRegistry`);
|
|
||||||
initializers.initCommandRegistry();
|
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
|
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
|
||||||
initializers.initEntitySettingsRegistry();
|
initializers.initEntitySettingsRegistry();
|
||||||
|
|
||||||
@ -114,9 +112,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
|
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
|
||||||
initializers.initKubeObjectDetailRegistry();
|
initializers.initKubeObjectDetailRegistry();
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
|
|
||||||
initializers.initWelcomeMenuRegistry();
|
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
|
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
|
||||||
initializers.initWorkloadsOverviewDetailRegistry();
|
initializers.initWorkloadsOverviewDetailRegistry();
|
||||||
|
|
||||||
@ -127,7 +122,9 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
|
|||||||
initializers.initCatalogCategoryRegistryEntries();
|
initializers.initCatalogCategoryRegistryEntries();
|
||||||
|
|
||||||
logger.info(`${logPrefix} initializing Catalog`);
|
logger.info(`${logPrefix} initializing Catalog`);
|
||||||
initializers.initCatalog();
|
initializers.initCatalog({
|
||||||
|
openCommandDialog: di.inject(commandOverlayInjectable).open,
|
||||||
|
});
|
||||||
|
|
||||||
const extensionLoader = di.inject(extensionLoaderInjectable);
|
const extensionLoader = di.inject(extensionLoaderInjectable);
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import { observable, makeObservable, when } from "mobx";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Redirect, Route, Router, Switch } from "react-router";
|
import { Redirect, Route, Router, Switch } from "react-router";
|
||||||
import { history } from "./navigation";
|
import { history } from "./navigation";
|
||||||
import { NotFound } from "./components/+404";
|
|
||||||
import { UserManagement } from "./components/+user-management/user-management";
|
import { UserManagement } from "./components/+user-management/user-management";
|
||||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||||
import { ClusterOverview } from "./components/+cluster/cluster-overview";
|
import { ClusterOverview } from "./components/+cluster/cluster-overview";
|
||||||
@ -230,7 +229,11 @@ export class ClusterFrame extends React.Component {
|
|||||||
{this.renderExtensionTabLayoutRoutes()}
|
{this.renderExtensionTabLayoutRoutes()}
|
||||||
{this.renderExtensionRoutes()}
|
{this.renderExtensionRoutes()}
|
||||||
<Redirect exact from="/" to={this.startUrl}/>
|
<Redirect exact from="/" to={this.startUrl}/>
|
||||||
<Route component={NotFound}/>
|
<Route render={({ location }) => {
|
||||||
|
Notifications.error(`Unknown location ${location.pathname}, redirecting to main page.`);
|
||||||
|
|
||||||
|
return <Redirect to={this.startUrl} />;
|
||||||
|
}} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
<Notifications/>
|
<Notifications/>
|
||||||
|
|||||||
@ -45,15 +45,17 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
|||||||
|
|
||||||
const renderName = (metric: IHpaMetric) => {
|
const renderName = (metric: IHpaMetric) => {
|
||||||
switch (metric.type) {
|
switch (metric.type) {
|
||||||
case HpaMetricType.Resource:
|
case HpaMetricType.Resource: {
|
||||||
const addition = metric.resource.targetAverageUtilization ? <>(as a percentage of request)</> : "";
|
const addition = metric.resource.targetAverageUtilization
|
||||||
|
? "(as a percentage of request)"
|
||||||
|
: "";
|
||||||
|
|
||||||
return <>Resource {metric.resource.name} on Pods {addition}</>;
|
return <>Resource {metric.resource.name} on Pods {addition}</>;
|
||||||
|
}
|
||||||
case HpaMetricType.Pods:
|
case HpaMetricType.Pods:
|
||||||
return <>{metric.pods.metricName} on Pods</>;
|
return <>{metric.pods.metricName} on Pods</>;
|
||||||
|
|
||||||
case HpaMetricType.Object:
|
case HpaMetricType.Object: {
|
||||||
const { target } = metric.object;
|
const { target } = metric.object;
|
||||||
const { kind, name } = target;
|
const { kind, name } = target;
|
||||||
const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa));
|
const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa));
|
||||||
@ -64,6 +66,7 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
|
|||||||
<Link to={objectUrl}>{kind}/{name}</Link>
|
<Link to={objectUrl}>{kind}/{name}</Link>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
case HpaMetricType.External:
|
case HpaMetricType.External:
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -66,22 +66,15 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
|
|||||||
@computed get groups() {
|
@computed get groups() {
|
||||||
const groups: Record<string, CustomResourceDefinition[]> = {};
|
const groups: Record<string, CustomResourceDefinition[]> = {};
|
||||||
|
|
||||||
return this.items.reduce((groups, crd) => {
|
for (const crd of this.items) {
|
||||||
const group = crd.getGroup();
|
(groups[crd.getGroup()] ??= []).push(crd);
|
||||||
|
}
|
||||||
|
|
||||||
if (!groups[group]) groups[group] = [];
|
return groups;
|
||||||
groups[group].push(crd);
|
|
||||||
|
|
||||||
return groups;
|
|
||||||
}, groups);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getByGroup(group: string, pluralName: string) {
|
getByGroup(group: string, pluralName: string) {
|
||||||
const crdInGroup = this.groups[group];
|
return this.groups[group]?.find(crd => crd.getPluralName() === pluralName);
|
||||||
|
|
||||||
if (!crdInGroup) return null;
|
|
||||||
|
|
||||||
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getByObject(obj: KubeObject) {
|
getByObject(obj: KubeObject) {
|
||||||
|
|||||||
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
import { crdStore } from "./crd.store";
|
||||||
|
|
||||||
|
const customResourceDefinitionsInjectable = getInjectable({
|
||||||
|
instantiate: () => computed(() => [...crdStore.items]),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default customResourceDefinitionsInjectable;
|
||||||
@ -34,7 +34,7 @@ import { mockWindow } from "../../../../../__mocks__/windowMock";
|
|||||||
import { AppPaths } from "../../../../common/app-paths";
|
import { AppPaths } from "../../../../common/app-paths";
|
||||||
import extensionLoaderInjectable
|
import extensionLoaderInjectable
|
||||||
from "../../../../extensions/extension-loader/extension-loader.injectable";
|
from "../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
import { DiRender, renderFor } from "../../test-utils/renderFor";
|
||||||
|
|
||||||
mockWindow();
|
mockWindow();
|
||||||
|
|||||||
@ -18,8 +18,7 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
import { lifecycleEnum } from "@ogre-tools/injectable";
|
|
||||||
import { unpackExtension } from "./unpack-extension";
|
import { unpackExtension } from "./unpack-extension";
|
||||||
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
|
||||||
|
|
||||||
|
|||||||
@ -47,8 +47,8 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) =
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>);
|
Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>);
|
||||||
console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error);
|
console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error);
|
||||||
// fallthrough
|
|
||||||
}
|
}
|
||||||
|
// fallthrough
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
case ExtensionRegistryLocation.DEFAULT:
|
case ExtensionRegistryLocation.DEFAULT:
|
||||||
|
|||||||
@ -21,11 +21,10 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { boundMethod, cssNames } from "../../utils";
|
import { boundMethod, cssNames } from "../../utils";
|
||||||
import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward";
|
import { openPortForward, PortForwardItem, removePortForward, PortForwardDialog, startPortForward, stopPortForward } from "../../port-forward";
|
||||||
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
|
||||||
import { MenuItem } from "../menu";
|
import { MenuItem } from "../menu";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { PortForwardDialog } from "../../port-forward";
|
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
|
|
||||||
interface Props extends MenuActionsProps {
|
interface Props extends MenuActionsProps {
|
||||||
@ -45,6 +44,38 @@ export class PortForwardMenu extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startPortForwarding = async () => {
|
||||||
|
const { portForward } = this.props;
|
||||||
|
|
||||||
|
const pf = await startPortForward(portForward);
|
||||||
|
|
||||||
|
if (pf.status === "Disabled") {
|
||||||
|
const { name, kind, forwardPort } = portForward;
|
||||||
|
|
||||||
|
Notifications.error(`Error occurred starting port-forward, the local port ${forwardPort} may not be available or the ${kind} ${name} may not be reachable`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
renderStartStopMenuItem() {
|
||||||
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
|
if (portForward.status === "Active") {
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={() => stopPortForward(portForward)}>
|
||||||
|
<Icon material="stop" tooltip="Stop port-forward" interactive={toolbar} />
|
||||||
|
<span className="title">Stop</span>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem onClick={this.startPortForwarding}>
|
||||||
|
<Icon material="play_arrow" tooltip="Start port-forward" interactive={toolbar} />
|
||||||
|
<span className="title">Start</span>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { portForward, toolbar } = this.props;
|
const { portForward, toolbar } = this.props;
|
||||||
|
|
||||||
@ -52,14 +83,17 @@ export class PortForwardMenu extends React.Component<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={() => openPortForward(this.props.portForward)}>
|
{ portForward.status === "Active" &&
|
||||||
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
<MenuItem onClick={() => openPortForward(portForward)}>
|
||||||
<span className="title">Open</span>
|
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
|
||||||
</MenuItem>
|
<span className="title">Open</span>
|
||||||
|
</MenuItem>
|
||||||
|
}
|
||||||
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
|
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
|
||||||
<Icon material="edit" tooltip="Change port or protocol" interactive={toolbar} />
|
<Icon material="edit" tooltip="Change port or protocol" interactive={toolbar} />
|
||||||
<span className="title">Edit</span>
|
<span className="title">Edit</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{this.renderStartStopMenuItem()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export class PortForwards extends React.Component<Props> {
|
|||||||
showDetails = (item: PortForwardItem) => {
|
showDetails = (item: PortForwardItem) => {
|
||||||
navigation.push(portForwardsURL({
|
navigation.push(portForwardsURL({
|
||||||
params: {
|
params: {
|
||||||
forwardport: String(item.getForwardPort()),
|
forwardport: item.getId(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,13 +24,14 @@ import "./service-port-component.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
import type { Service, ServicePort } from "../../../common/k8s-api/endpoints";
|
||||||
import { observable, makeObservable, reaction } from "mobx";
|
import { observable, makeObservable, reaction, action } from "mobx";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, notifyErrorPortForwarding, openPortForward, PortForwardDialog, predictProtocol, removePortForward, startPortForward } from "../../port-forward";
|
||||||
import type { ForwardedPort } from "../../port-forward";
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
service: Service;
|
service: Service;
|
||||||
@ -42,6 +43,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
@observable forwardPort = 0;
|
@observable forwardPort = 0;
|
||||||
@observable isPortForwarded = false;
|
@observable isPortForwarded = false;
|
||||||
|
@observable isActive = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -51,13 +53,14 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => [portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()),
|
reaction(() => this.props.service, () => this.checkExistingPortForwarding()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async checkExistingPortForwarding() {
|
async checkExistingPortForwarding() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "service",
|
kind: "service",
|
||||||
name: service.getName(),
|
name: service.getName(),
|
||||||
namespace: service.getNs(),
|
namespace: service.getNs(),
|
||||||
@ -65,57 +68,66 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
let activePort: number;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activePort = await getPortForward(portForward) ?? 0;
|
portForward = await getPortForward(portForward);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isPortForwarded = false;
|
this.isPortForwarded = false;
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.forwardPort = activePort;
|
this.forwardPort = portForward.forwardPort;
|
||||||
this.isPortForwarded = activePort ? true : false;
|
this.isPortForwarded = true;
|
||||||
|
this.isActive = portForward.status === "Active";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "service",
|
kind: "service",
|
||||||
name: service.getName(),
|
name: service.getName(),
|
||||||
namespace: service.getNs(),
|
namespace: service.getNs(),
|
||||||
port: port.port,
|
port: port.port,
|
||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
|
status: "Active",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// determine how many port-forwards are already active
|
// determine how many port-forwards already exist
|
||||||
const { length } = await getPortForwards();
|
const { length } = getPortForwards();
|
||||||
|
|
||||||
this.forwardPort = await addPortForward(portForward);
|
if (!this.isPortForwarded) {
|
||||||
|
portForward = await addPortForward(portForward);
|
||||||
|
} else if (!this.isActive) {
|
||||||
|
portForward = await startPortForward(portForward);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.forwardPort) {
|
this.forwardPort = portForward.forwardPort;
|
||||||
portForward.forwardPort = this.forwardPort;
|
|
||||||
|
if (portForward.status === "Active") {
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
this.isPortForwarded = true;
|
|
||||||
|
|
||||||
// if this is the first port-forward show the about notification
|
// if this is the first port-forward show the about notification
|
||||||
if (!length) {
|
if (!length) {
|
||||||
aboutPortForwarding();
|
aboutPortForwarding();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
notifyErrorPortForwarding(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
logger.error("[SERVICE-PORT-COMPONENT]:", error, portForward);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async stopPortForward() {
|
async stopPortForward() {
|
||||||
const { service, port } = this.props;
|
const { service, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
const portForward: ForwardedPort = {
|
||||||
@ -130,11 +142,11 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await removePortForward(portForward);
|
await removePortForward(portForward);
|
||||||
this.isPortForwarded = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
|
this.forwardPort = 0;
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,7 +154,7 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
render() {
|
render() {
|
||||||
const { port, service } = this.props;
|
const { port, service } = this.props;
|
||||||
|
|
||||||
const portForwardAction = async () => {
|
const portForwardAction = action(async () => {
|
||||||
if (this.isPortForwarded) {
|
if (this.isPortForwarded) {
|
||||||
await this.stopPortForward();
|
await this.stopPortForward();
|
||||||
} else {
|
} else {
|
||||||
@ -155,16 +167,16 @@ export class ServicePortComponent extends React.Component<Props> {
|
|||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
PortForwardDialog.open(portForward, { openInBrowser: true });
|
PortForwardDialog.open(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() });
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward()}>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{port.toString()}
|
{port.toString()}
|
||||||
</span>
|
</span>
|
||||||
<Button primary onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
<Button primary onClick={portForwardAction}> {this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."} </Button>
|
||||||
{this.waiting && (
|
{this.waiting && (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ $service-status-color-list: (
|
|||||||
|
|
||||||
$port-forward-status-color-list: (
|
$port-forward-status-color-list: (
|
||||||
active: var(--colorOk),
|
active: var(--colorOk),
|
||||||
|
disabled: var(--colorSoftError)
|
||||||
);
|
);
|
||||||
|
|
||||||
@mixin port-forward-status-colors {
|
@mixin port-forward-status-colors {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store";
|
|||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { isWindows } from "../../../common/vars";
|
import { isWindows } from "../../../common/vars";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
|
||||||
import { action } from "mobx";
|
import { action } from "mobx";
|
||||||
@ -86,16 +86,12 @@ export const Application = observer(() => {
|
|||||||
|
|
||||||
<section id="terminalSelection">
|
<section id="terminalSelection">
|
||||||
<SubTitle title="Terminal copy & paste" />
|
<SubTitle title="Terminal copy & paste" />
|
||||||
<FormSwitch
|
<Switch
|
||||||
label="Copy on select and paste on right-click"
|
checked={userStore.terminalCopyOnSelect}
|
||||||
control={
|
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
|
||||||
<Switcher
|
>
|
||||||
checked={userStore.terminalCopyOnSelect}
|
Copy on select and paste on right-click
|
||||||
onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
|
</Switch>
|
||||||
name="terminalCopyOnSelect"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr/>
|
<hr/>
|
||||||
@ -135,16 +131,9 @@ export const Application = observer(() => {
|
|||||||
|
|
||||||
<section id="other">
|
<section id="other">
|
||||||
<SubTitle title="Start-up"/>
|
<SubTitle title="Start-up"/>
|
||||||
<FormSwitch
|
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
|
||||||
control={
|
Automatically start Lens on login
|
||||||
<Switcher
|
</Switch>
|
||||||
checked={userStore.openAtLogin}
|
|
||||||
onChange={v => userStore.openAtLogin = v.target.checked}
|
|
||||||
name="startup"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Automatically start Lens on login"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { SubHeader } from "../layout/sub-header";
|
import { SubHeader } from "../layout/sub-header";
|
||||||
@ -45,15 +45,12 @@ export const Editor = observer(() => {
|
|||||||
<section>
|
<section>
|
||||||
<div className="flex gaps justify-space-between">
|
<div className="flex gaps justify-space-between">
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<FormSwitch
|
<Switch
|
||||||
label={<SubHeader compact>Show minimap</SubHeader>}
|
checked={editorConfiguration.minimap.enabled}
|
||||||
control={
|
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
|
||||||
<Switcher
|
>
|
||||||
checked={editorConfiguration.minimap.enabled}
|
Show minimap
|
||||||
onChange={(evt, checked) => editorConfiguration.minimap.enabled = checked}
|
</Switch>
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gaps align-center">
|
<div className="flex gaps align-center">
|
||||||
<SubHeader compact>Position</SubHeader>
|
<SubHeader compact>Position</SubHeader>
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-s
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { bundledKubectlPath } from "../../../main/kubectl";
|
import { bundledKubectlPath } from "../../../main/kubectl";
|
||||||
import { SelectOption, Select } from "../select";
|
import { SelectOption, Select } from "../select";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
|
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
|
||||||
|
|
||||||
export const KubectlBinaries = observer(() => {
|
export const KubectlBinaries = observer(() => {
|
||||||
@ -48,16 +48,12 @@ export const KubectlBinaries = observer(() => {
|
|||||||
<>
|
<>
|
||||||
<section>
|
<section>
|
||||||
<SubTitle title="Kubectl binary download"/>
|
<SubTitle title="Kubectl binary download"/>
|
||||||
<FormSwitch
|
<Switch
|
||||||
control={
|
checked={userStore.downloadKubectlBinaries}
|
||||||
<Switcher
|
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
|
||||||
checked={userStore.downloadKubectlBinaries}
|
>
|
||||||
onChange={v => userStore.downloadKubectlBinaries = v.target.checked}
|
Download kubectl binaries matching the Kubernetes cluster version
|
||||||
name="kubectl-download"
|
</Switch>
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Download kubectl binaries matching the Kubernetes cluster version"
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@ -24,10 +24,11 @@ import React from "react";
|
|||||||
import { UserStore } from "../../../common/user-store";
|
import { UserStore } from "../../../common/user-store";
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { SubTitle } from "../layout/sub-title";
|
import { SubTitle } from "../layout/sub-title";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { Switch } from "../switch";
|
||||||
|
|
||||||
export const LensProxy = observer(() => {
|
export const LensProxy = observer(() => {
|
||||||
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
|
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
|
||||||
|
const store = UserStore.getInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="proxy">
|
<section id="proxy">
|
||||||
@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
|
|||||||
|
|
||||||
<section className="small">
|
<section className="small">
|
||||||
<SubTitle title="Certificate Trust"/>
|
<SubTitle title="Certificate Trust"/>
|
||||||
<FormSwitch
|
<Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
|
||||||
control={
|
Allow untrusted Certificate Authorities
|
||||||
<Switcher
|
</Switch>
|
||||||
checked={UserStore.getInstance().allowUntrustedCAs}
|
|
||||||
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
|
|
||||||
name="startup"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Allow untrusted Certificate Authorities"
|
|
||||||
/>
|
|
||||||
<small className="hint">
|
<small className="hint">
|
||||||
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
This will make Lens to trust ANY certificate authority without any validations.{" "}
|
||||||
Needed with some corporate proxies that do certificate re-writing.{" "}
|
Needed with some corporate proxies that do certificate re-writing.{" "}
|
||||||
|
|||||||
@ -20,45 +20,55 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { screen } from "@testing-library/react";
|
||||||
import "@testing-library/jest-dom/extend-expect";
|
import "@testing-library/jest-dom/extend-expect";
|
||||||
import { Welcome } from "../welcome";
|
import { defaultWidth, Welcome } from "../welcome";
|
||||||
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
|
import { computed } from "mobx";
|
||||||
import { defaultWidth } from "../welcome";
|
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
|
||||||
|
import type { DiRender } from "../../test-utils/renderFor";
|
||||||
|
import { renderFor } from "../../test-utils/renderFor";
|
||||||
|
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
|
||||||
|
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||||
|
import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration";
|
||||||
|
|
||||||
jest.mock(
|
jest.mock("electron", () => ({
|
||||||
"electron",
|
ipcRenderer: {
|
||||||
() => ({
|
on: jest.fn(),
|
||||||
ipcRenderer: {
|
},
|
||||||
on: jest.fn(),
|
app: {
|
||||||
},
|
getPath: () => "tmp",
|
||||||
app: {
|
},
|
||||||
getPath: () => "tmp",
|
}));
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("<Welcome/>", () => {
|
describe("<Welcome/>", () => {
|
||||||
beforeEach(() => {
|
let render: DiRender;
|
||||||
TopBarRegistry.createInstance();
|
let di: ConfigurableDependencyInjectionContainer;
|
||||||
WelcomeMenuRegistry.createInstance();
|
let welcomeBannersStub: WelcomeBannerRegistration[];
|
||||||
WelcomeBannerRegistry.createInstance();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
TopBarRegistry.resetInstance();
|
di = getDiForUnitTesting();
|
||||||
WelcomeMenuRegistry.resetInstance();
|
|
||||||
WelcomeBannerRegistry.resetInstance();
|
render = renderFor(di);
|
||||||
|
|
||||||
|
welcomeBannersStub = [];
|
||||||
|
|
||||||
|
di.override(rendererExtensionsInjectable, () =>
|
||||||
|
computed(() => [
|
||||||
|
new TestExtension({
|
||||||
|
id: "some-id",
|
||||||
|
welcomeBanners: welcomeBannersStub,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
|
||||||
const testId = "testId";
|
const testId = "testId";
|
||||||
|
|
||||||
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
welcomeBannersStub.push({
|
||||||
{
|
Banner: () => <div data-testid={testId} />,
|
||||||
Banner: () => <div data-testid={testId} />,
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { container } = render(<Welcome />);
|
const { container } = render(<Welcome />);
|
||||||
|
|
||||||
@ -67,16 +77,15 @@ describe("<Welcome/>", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
|
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
|
||||||
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
|
welcomeBannersStub.push({
|
||||||
{
|
width: 100,
|
||||||
width: 100,
|
Banner: () => <div />,
|
||||||
Banner: () => <div />,
|
});
|
||||||
},
|
|
||||||
{
|
welcomeBannersStub.push({
|
||||||
width: 800,
|
width: 800,
|
||||||
Banner: () => <div />,
|
Banner: () => <div />,
|
||||||
},
|
});
|
||||||
]);
|
|
||||||
|
|
||||||
render(<Welcome />);
|
render(<Welcome />);
|
||||||
|
|
||||||
@ -92,3 +101,25 @@ describe("<Welcome/>", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class TestExtension extends LensRendererExtension {
|
||||||
|
constructor({
|
||||||
|
id,
|
||||||
|
welcomeBanners,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
welcomeBanners: WelcomeBannerRegistration[];
|
||||||
|
}) {
|
||||||
|
super({
|
||||||
|
id,
|
||||||
|
absolutePath: "irrelevant",
|
||||||
|
isBundled: false,
|
||||||
|
isCompatible: false,
|
||||||
|
isEnabled: false,
|
||||||
|
manifest: { name: id, version: "some-version" },
|
||||||
|
manifestPath: "irrelevant",
|
||||||
|
});
|
||||||
|
|
||||||
|
this.welcomeBanners = welcomeBanners;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,18 +18,20 @@
|
|||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
|
||||||
import { catalogURL } from "../../common/routes";
|
const welcomeBannerItemsInjectable = getInjectable({
|
||||||
import { WelcomeMenuRegistry } from "../../extensions/registries";
|
instantiate: (di) => {
|
||||||
import { navigate } from "../navigation";
|
const extensions = di.inject(rendererExtensionsInjectable);
|
||||||
|
|
||||||
export function initWelcomeMenuRegistry() {
|
return computed(() => [
|
||||||
WelcomeMenuRegistry.getInstance()
|
...extensions.get().flatMap((extension) => extension.welcomeBanners),
|
||||||
.add([
|
|
||||||
{
|
|
||||||
title: "Browse Clusters in Catalog",
|
|
||||||
icon: "view_list",
|
|
||||||
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )),
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default welcomeBannerItemsInjectable;
|
||||||
@ -19,8 +19,6 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WelcomeBannerRegistration is for an extension to register
|
* WelcomeBannerRegistration is for an extension to register
|
||||||
* Provide a Banner component to be renderered in the welcome screen.
|
* Provide a Banner component to be renderered in the welcome screen.
|
||||||
@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration {
|
|||||||
*/
|
*/
|
||||||
width?: number
|
width?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WelcomeBannerRegistry extends BaseRegistry<WelcomeBannerRegistration> { }
|
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
|
import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
|
||||||
|
import { navigate } from "../../../navigation";
|
||||||
|
import { catalogURL } from "../../../../common/routes";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
extensions: IComputedValue<LensRendererExtension[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getWelcomeMenuItems = ({ extensions }: Dependencies) => {
|
||||||
|
const browseClusters = {
|
||||||
|
title: "Browse Clusters in Catalog",
|
||||||
|
icon: "view_list",
|
||||||
|
click: () =>
|
||||||
|
navigate(
|
||||||
|
catalogURL({
|
||||||
|
params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
return computed(() => [
|
||||||
|
browseClusters,
|
||||||
|
...extensions.get().flatMap((extension) => extension.welcomeMenus),
|
||||||
|
]);
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 OpenLens Authors
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to
|
||||||
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||||
|
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
|
||||||
|
import { getWelcomeMenuItems } from "./get-welcome-menu-items";
|
||||||
|
|
||||||
|
const welcomeMenuItemsInjectable = getInjectable({
|
||||||
|
instantiate: (di) =>
|
||||||
|
getWelcomeMenuItems({
|
||||||
|
extensions: di.inject(rendererExtensionsInjectable),
|
||||||
|
}),
|
||||||
|
|
||||||
|
lifecycle: lifecycleEnum.singleton,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default welcomeMenuItemsInjectable;
|
||||||
@ -19,12 +19,8 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseRegistry } from "./base-registry";
|
|
||||||
|
|
||||||
export interface WelcomeMenuRegistration {
|
export interface WelcomeMenuRegistration {
|
||||||
title: string | (() => string);
|
title: string | (() => string);
|
||||||
icon: string;
|
icon: string;
|
||||||
click: () => void | Promise<void>;
|
click: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WelcomeMenuRegistry extends BaseRegistry<WelcomeMenuRegistration> {}
|
|
||||||
@ -22,78 +22,129 @@
|
|||||||
import "./welcome.scss";
|
import "./welcome.scss";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import type { IComputedValue } from "mobx";
|
||||||
import Carousel from "react-material-ui-carousel";
|
import Carousel from "react-material-ui-carousel";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { productName, slackUrl } from "../../../common/vars";
|
import { productName, slackUrl } from "../../../common/vars";
|
||||||
import { WelcomeMenuRegistry } from "../../../extensions/registries";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import { WelcomeBannerRegistry } from "../../../extensions/registries";
|
import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable";
|
||||||
|
import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration";
|
||||||
|
import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable";
|
||||||
|
import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration";
|
||||||
|
|
||||||
export const defaultWidth = 320;
|
export const defaultWidth = 320;
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class Welcome extends React.Component {
|
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>
|
||||||
render() {
|
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>
|
||||||
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems();
|
}
|
||||||
|
|
||||||
// if there is banner with specified width, use it to calculate the width of the container
|
const NonInjectedWelcome: React.FC<Dependencies> = ({ welcomeMenuItems, welcomeBannerItems }) => {
|
||||||
const maxWidth = welcomeBanner.reduce((acc, curr) => {
|
const welcomeBanners = welcomeBannerItems.get();
|
||||||
const currWidth = curr.width ?? 0;
|
|
||||||
|
|
||||||
if (acc > currWidth) {
|
// if there is banner with specified width, use it to calculate the width of the container
|
||||||
return acc;
|
const maxWidth = welcomeBanners.reduce((acc, curr) => {
|
||||||
}
|
const currWidth = curr.width ?? 0;
|
||||||
|
|
||||||
return currWidth;
|
if (acc > currWidth) {
|
||||||
}, defaultWidth);
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return currWidth;
|
||||||
<div className="flex justify-center Welcome align-center">
|
}, defaultWidth);
|
||||||
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
|
|
||||||
{welcomeBanner.length > 0 ? (
|
return (
|
||||||
<Carousel
|
<div className="flex justify-center Welcome align-center">
|
||||||
stopAutoPlayOnHover={true}
|
<div
|
||||||
indicators={welcomeBanner.length > 1}
|
style={{ width: `${maxWidth}px` }}
|
||||||
autoPlay={true}
|
data-testid="welcome-banner-container"
|
||||||
navButtonsAlwaysInvisible={true}
|
>
|
||||||
indicatorIconButtonProps={{
|
{welcomeBanners.length > 0 ? (
|
||||||
style: {
|
<Carousel
|
||||||
color: "var(--iconActiveBackground)",
|
stopAutoPlayOnHover={true}
|
||||||
},
|
indicators={welcomeBanners.length > 1}
|
||||||
}}
|
autoPlay={true}
|
||||||
activeIndicatorIconButtonProps={{
|
navButtonsAlwaysInvisible={true}
|
||||||
style: {
|
indicatorIconButtonProps={{
|
||||||
color: "var(--iconActiveColor)",
|
style: {
|
||||||
},
|
color: "var(--iconActiveBackground)",
|
||||||
}}
|
},
|
||||||
interval={8000}
|
}}
|
||||||
|
activeIndicatorIconButtonProps={{
|
||||||
|
style: {
|
||||||
|
color: "var(--iconActiveColor)",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
interval={8000}
|
||||||
|
>
|
||||||
|
{welcomeBanners.map((item, index) => (
|
||||||
|
<item.Banner key={index} />
|
||||||
|
))}
|
||||||
|
</Carousel>
|
||||||
|
) : (
|
||||||
|
<Icon svg="logo-lens" className="logo" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div
|
||||||
|
style={{ width: `${defaultWidth}px` }}
|
||||||
|
data-testid="welcome-text-container"
|
||||||
|
>
|
||||||
|
<h2>Welcome to {productName} 5!</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To get you started we have auto-detected your clusters in your
|
||||||
|
kubeconfig file and added them to the catalog, your centralized
|
||||||
|
view for managing all your cloud-native resources.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you have any questions or feedback, please join our{" "}
|
||||||
|
<a
|
||||||
|
href={slackUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="link"
|
||||||
|
>
|
||||||
|
Lens Community slack channel
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
className="block"
|
||||||
|
style={{ width: `${defaultWidth}px` }}
|
||||||
|
data-testid="welcome-menu-container"
|
||||||
>
|
>
|
||||||
{welcomeBanner.map((item, index) =>
|
{welcomeMenuItems.get().map((item, index) => (
|
||||||
<item.Banner key={index} />,
|
<li
|
||||||
)}
|
key={index}
|
||||||
</Carousel>
|
className="flex grid-12"
|
||||||
) : <Icon svg="logo-lens" className="logo" />}
|
onClick={() => item.click()}
|
||||||
|
>
|
||||||
<div className="flex justify-center">
|
<Icon material={item.icon} className="box col-1" />
|
||||||
<div style={{ width: `${defaultWidth}px` }} data-testid="welcome-text-container">
|
<a className="box col-10">
|
||||||
<h2>Welcome to {productName} 5!</h2>
|
{typeof item.title === "string"
|
||||||
|
? item.title
|
||||||
<p>
|
: item.title()}
|
||||||
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources.
|
</a>
|
||||||
<br /><br />
|
<Icon material="navigate_next" className="box col-1" />
|
||||||
If you have any questions or feedback, please join our <a href={slackUrl} target="_blank" rel="noreferrer" className="link">Lens Community slack channel</a>.
|
</li>
|
||||||
</p>
|
))}
|
||||||
|
</ul>
|
||||||
<ul className="block" style={{ width: `${defaultWidth}px` }} data-testid="welcome-menu-container">
|
|
||||||
{WelcomeMenuRegistry.getInstance().getItems().map((item, index) => (
|
|
||||||
<li key={index} className="flex grid-12" onClick={() => item.click()}>
|
|
||||||
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export const Welcome = withInjectables<Dependencies>(
|
||||||
|
observer(NonInjectedWelcome),
|
||||||
|
|
||||||
|
{
|
||||||
|
getProps: (di) => ({
|
||||||
|
welcomeMenuItems: di.inject(welcomeMenuItemsInjectable),
|
||||||
|
welcomeBannerItems: di.inject(welcomeBannerItemsInjectable),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@ -24,13 +24,14 @@ import "./pod-container-port.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import type { Pod } from "../../../common/k8s-api/endpoints";
|
import type { Pod } from "../../../common/k8s-api/endpoints";
|
||||||
import { observable, makeObservable, reaction } from "mobx";
|
import { action, observable, makeObservable, reaction } from "mobx";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward";
|
import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, notifyErrorPortForwarding, openPortForward, PortForwardDialog, predictProtocol, removePortForward, startPortForward } from "../../port-forward";
|
||||||
import type { ForwardedPort } from "../../port-forward";
|
import type { ForwardedPort } from "../../port-forward";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
|
import logger from "../../../common/logger";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pod: Pod;
|
pod: Pod;
|
||||||
@ -46,6 +47,7 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
@observable waiting = false;
|
@observable waiting = false;
|
||||||
@observable forwardPort = 0;
|
@observable forwardPort = 0;
|
||||||
@observable isPortForwarded = false;
|
@observable isPortForwarded = false;
|
||||||
|
@observable isActive = false;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -55,13 +57,14 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => [portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()),
|
reaction(() => this.props.pod, () => this.checkExistingPortForwarding()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async checkExistingPortForwarding() {
|
async checkExistingPortForwarding() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "pod",
|
kind: "pod",
|
||||||
name: pod.getName(),
|
name: pod.getName(),
|
||||||
namespace: pod.getNs(),
|
namespace: pod.getNs(),
|
||||||
@ -69,57 +72,64 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
};
|
};
|
||||||
|
|
||||||
let activePort: number;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
activePort = await getPortForward(portForward) ?? 0;
|
portForward = await getPortForward(portForward);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.isPortForwarded = false;
|
this.isPortForwarded = false;
|
||||||
|
this.isActive = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.forwardPort = activePort;
|
this.forwardPort = portForward.forwardPort;
|
||||||
this.isPortForwarded = activePort ? true : false;
|
this.isPortForwarded = true;
|
||||||
|
this.isActive = portForward.status === "Active";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async portForward() {
|
async portForward() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
let portForward: ForwardedPort = {
|
||||||
kind: "pod",
|
kind: "pod",
|
||||||
name: pod.getName(),
|
name: pod.getName(),
|
||||||
namespace: pod.getNs(),
|
namespace: pod.getNs(),
|
||||||
port: port.containerPort,
|
port: port.containerPort,
|
||||||
forwardPort: this.forwardPort,
|
forwardPort: this.forwardPort,
|
||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
|
status: "Active",
|
||||||
};
|
};
|
||||||
|
|
||||||
this.waiting = true;
|
this.waiting = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// determine how many port-forwards are already active
|
// determine how many port-forwards already exist
|
||||||
const { length } = await getPortForwards();
|
const { length } = getPortForwards();
|
||||||
|
|
||||||
this.forwardPort = await addPortForward(portForward);
|
if (!this.isPortForwarded) {
|
||||||
|
portForward = await addPortForward(portForward);
|
||||||
|
} else if (!this.isActive) {
|
||||||
|
portForward = await startPortForward(portForward);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.forwardPort) {
|
if (portForward.status === "Active") {
|
||||||
portForward.forwardPort = this.forwardPort;
|
|
||||||
openPortForward(portForward);
|
openPortForward(portForward);
|
||||||
this.isPortForwarded = true;
|
|
||||||
|
|
||||||
// if this is the first port-forward show the about notification
|
// if this is the first port-forward show the about notification
|
||||||
if (!length) {
|
if (!length) {
|
||||||
aboutPortForwarding();
|
aboutPortForwarding();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
notifyErrorPortForwarding(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
|
logger.error("[POD-CONTAINER-PORT]:", error, portForward);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
async stopPortForward() {
|
async stopPortForward() {
|
||||||
const { pod, port } = this.props;
|
const { pod, port } = this.props;
|
||||||
const portForward: ForwardedPort = {
|
const portForward: ForwardedPort = {
|
||||||
@ -134,11 +144,11 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await removePortForward(portForward);
|
await removePortForward(portForward);
|
||||||
this.isPortForwarded = false;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
|
||||||
this.checkExistingPortForwarding();
|
|
||||||
} finally {
|
} finally {
|
||||||
|
this.checkExistingPortForwarding();
|
||||||
|
this.forwardPort = 0;
|
||||||
this.waiting = false;
|
this.waiting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -148,7 +158,7 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
const { name, containerPort, protocol } = port;
|
const { name, containerPort, protocol } = port;
|
||||||
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
|
||||||
|
|
||||||
const portForwardAction = async () => {
|
const portForwardAction = action(async () => {
|
||||||
if (this.isPortForwarded) {
|
if (this.isPortForwarded) {
|
||||||
await this.stopPortForward();
|
await this.stopPortForward();
|
||||||
} else {
|
} else {
|
||||||
@ -161,16 +171,16 @@ export class PodContainerPort extends React.Component<Props> {
|
|||||||
protocol: predictProtocol(port.name),
|
protocol: predictProtocol(port.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
PortForwardDialog.open(portForward, { openInBrowser: true });
|
PortForwardDialog.open(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() });
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
|
||||||
<span title="Open in a browser" onClick={() => this.portForward()}>
|
<span title="Open in a browser" onClick={() => this.portForward()}>
|
||||||
{text}
|
{text}
|
||||||
</span>
|
</span>
|
||||||
<Button primary onClick={() => portForwardAction()}> {this.isPortForwarded ? "Stop" : "Forward..."} </Button>
|
<Button primary onClick={portForwardAction}> {this.isPortForwarded ? (this.isActive ? "Stop/Remove" : "Remove") : "Forward..."} </Button>
|
||||||
{this.waiting && (
|
{this.waiting && (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -19,40 +19,49 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { computed } from "mobx";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import { computed, IComputedValue } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { broadcastMessage, catalogEntityRunListener } from "../../../common/ipc";
|
||||||
import type { CatalogEntity } from "../../api/catalog-entity";
|
import type { CatalogEntity } from "../../api/catalog-entity";
|
||||||
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
|
||||||
import { CommandOverlay } from "../command-palette";
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
import { Select } from "../select";
|
import { Select } from "../select";
|
||||||
|
|
||||||
@observer
|
interface Dependencies {
|
||||||
export class ActivateEntityCommand extends React.Component {
|
closeCommandOverlay: () => void;
|
||||||
@computed get options() {
|
entities: IComputedValue<CatalogEntity[]>;
|
||||||
return catalogEntityRegistry.items.map(entity => ({
|
|
||||||
label: `${entity.kind}: ${entity.getName()}`,
|
|
||||||
value: entity,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
onSelect(entity: CatalogEntity): void {
|
|
||||||
catalogEntityRegistry.onRun(entity);
|
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
menuPortalTarget={null}
|
|
||||||
onChange={(v) => this.onSelect(v.value)}
|
|
||||||
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
|
||||||
menuIsOpen={true}
|
|
||||||
options={this.options}
|
|
||||||
autoFocus={true}
|
|
||||||
escapeClearsValue={false}
|
|
||||||
placeholder="Activate entity ..."
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NonInjectedActivateEntityCommand = observer(({ closeCommandOverlay, entities }: Dependencies) => {
|
||||||
|
const options = entities.get().map(entity => ({
|
||||||
|
label: `${entity.kind}: ${entity.getName()}`,
|
||||||
|
value: entity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onSelect = (entity: CatalogEntity): void => {
|
||||||
|
broadcastMessage(catalogEntityRunListener, entity.getId());
|
||||||
|
closeCommandOverlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
menuPortalTarget={null}
|
||||||
|
onChange={(v) => onSelect(v.value)}
|
||||||
|
components={{ DropdownIndicator: null, IndicatorSeparator: null }}
|
||||||
|
menuIsOpen={true}
|
||||||
|
options={options}
|
||||||
|
autoFocus={true}
|
||||||
|
escapeClearsValue={false}
|
||||||
|
placeholder="Activate entity ..."
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ActivateEntityCommand = withInjectables<Dependencies>(NonInjectedActivateEntityCommand, {
|
||||||
|
getProps: di => ({
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
entities: computed(() => [...catalogEntityRegistry.items]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@ -91,6 +91,15 @@ html, body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#terminal-init {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
|
|||||||
@ -21,21 +21,26 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CommandOverlay } from "../command-palette";
|
|
||||||
import { Input } from "../input";
|
import { Input } from "../input";
|
||||||
import { isUrl } from "../input/input_validators";
|
import { isUrl } from "../input/input_validators";
|
||||||
import { WeblinkStore } from "../../../common/weblink-store";
|
import { WeblinkStore } from "../../../common/weblink-store";
|
||||||
import { computed, makeObservable, observable } from "mobx";
|
import { computed, makeObservable, observable } from "mobx";
|
||||||
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
|
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
|
||||||
|
|
||||||
|
interface Dependencies {
|
||||||
|
closeCommandOverlay: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class WeblinkAddCommand extends React.Component {
|
class NonInjectedWeblinkAddCommand extends React.Component<Dependencies> {
|
||||||
@observable url = "";
|
@observable url = "";
|
||||||
@observable nameHidden = true;
|
@observable nameHidden = true;
|
||||||
@observable dirty = false;
|
@observable dirty = false;
|
||||||
|
|
||||||
constructor(props: {}) {
|
constructor(props: Dependencies) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +60,7 @@ export class WeblinkAddCommand extends React.Component {
|
|||||||
name: name || this.url,
|
name: name || this.url,
|
||||||
url: this.url,
|
url: this.url,
|
||||||
});
|
});
|
||||||
|
this.props.closeCommandOverlay();
|
||||||
CommandOverlay.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get showValidation() {
|
@computed get showValidation() {
|
||||||
@ -100,3 +104,10 @@ export class WeblinkAddCommand extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const WeblinkAddCommand = withInjectables<Dependencies>(NonInjectedWeblinkAddCommand, {
|
||||||
|
getProps: (di, props) => ({
|
||||||
|
closeCommandOverlay: di.inject(commandOverlayInjectable).close,
|
||||||
|
...props,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user