1
0
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:
Lauri Nevala 2022-01-17 11:11:02 +02:00 committed by GitHub
commit ee265a9db6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
163 changed files with 3307 additions and 1656 deletions

View File

@ -54,6 +54,7 @@ module.exports = {
"react-hooks",
],
rules: {
"no-constant-condition": ["error", { "checkLoops": false }],
"header/header": [2, "./license-header"],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
@ -107,7 +108,10 @@ module.exports = {
],
parser: "@typescript-eslint/parser",
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
plugins: [
"header",
@ -118,7 +122,7 @@ module.exports = {
sourceType: "module",
},
rules: {
"no-irregular-whitespace": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"header/header": [2, "./license-header"],
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
@ -191,8 +195,11 @@ module.exports = {
"unused-imports",
],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
parserOptions: {
ecmaVersion: 2018,
@ -200,8 +207,9 @@ module.exports = {
jsx: true,
},
rules: {
"no-irregular-whitespace": "error",
"no-constant-condition": ["error", { "checkLoops": false }],
"header/header": [2, "./license-header"],
"react/prop-types": "off",
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
@ -246,7 +254,6 @@ module.exports = {
"objectsInObjects": false,
"arraysInObjects": true,
}],
"react/prop-types": "off",
"semi": "off",
"@typescript-eslint/semi": ["error"],
"linebreak-style": ["error", "unix"],

View File

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

View File

@ -3,7 +3,7 @@
The Main Extension API is the interface to Lens's main process.
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.
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
@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores
### `appMenus`
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.
``` typescript
@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
```
`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:
* `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"`.
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
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).

36
extensions/.eslintrc.js Normal file
View 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"],
}],
},
},
],
};

View File

@ -62,8 +62,7 @@
},
"moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
"src/(.*)": "<rootDir>/__mocks__/windowMock.ts"
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
},
"modulePathIgnorePatterns": [
"<rootDir>/dist",
@ -200,6 +199,7 @@
"@ogre-tools/injectable-react": "2.0.0",
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0",
"@types/circular-dependency-plugin": "5.0.4",
"abort-controller": "^3.0.0",
"auto-bind": "^4.0.0",
"autobind-decorator": "^2.4.0",
@ -214,7 +214,7 @@
"filehound": "^1.17.5",
"fs-extra": "^9.0.1",
"glob-to-regexp": "^0.4.1",
"got": "^11.8.2",
"got": "^11.8.3",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7",
"http-proxy": "^1.18.1",
@ -333,7 +333,7 @@
"concurrently": "^5.3.0",
"css-loader": "^5.2.7",
"deepdash": "^5.3.9",
"dompurify": "^2.3.3",
"dompurify": "^2.3.4",
"electron": "^13.6.1",
"electron-builder": "^22.14.5",
"electron-notarize": "^0.3.0",
@ -341,6 +341,7 @@
"esbuild-loader": "^2.16.0",
"eslint": "^7.32.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unused-imports": "^1.1.5",

View File

@ -472,8 +472,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
it("moves the icon into preferences", async () => {
const storedClusterData = ClusterStore.getInstance().clustersList[0];
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
expect(Object.prototype.hasOwnProperty.call(storedClusterData, "icon")).toBe(false);
expect(Object.prototype.hasOwnProperty.call(storedClusterData.preferences, "icon")).toBe(true);
expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true);
});
});

View File

@ -251,7 +251,7 @@ describe("HotbarStore", () => {
const hotbarStore = HotbarStore.getInstance();
hotbarStore.add({ name: "hottest", id: "hottest" });
hotbarStore.activeHotbarId = "hottest";
hotbarStore.setActiveHotbar("hottest");
const { error } = logger;
const mocked = jest.fn();

View File

@ -42,9 +42,9 @@ import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process";
import { ThemeStore } from "../../renderer/theme.store";
import type { ClusterStoreModel } from "../cluster-store";
import { AppPaths } from "../app-paths";
import { defaultTheme } from "../vars";
console = new Console(stdout, stderr);
AppPaths.init();
@ -75,7 +75,7 @@ describe("user store tests", () => {
us.httpsProxy = "abcd://defg";
expect(us.httpsProxy).toBe("abcd://defg");
expect(us.colorTheme).toBe(ThemeStore.defaultTheme);
expect(us.colorTheme).toBe(defaultTheme);
us.colorTheme = "light";
expect(us.colorTheme).toBe("light");
@ -86,7 +86,7 @@ describe("user store tests", () => {
us.colorTheme = "some other theme";
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", () => {

View File

@ -23,7 +23,8 @@ import { app, ipcMain, ipcRenderer } from "electron";
import { observable, when } from "mobx";
import path from "path";
import logger from "./logger";
import { fromEntries, toJS } from "./utils";
import { fromEntries } from "./utils/objects";
import { toJS } from "./utils/toJS";
import { isWindows } from "./vars";
export type PathName = Parameters<typeof app["getPath"]>[0];

View File

@ -20,11 +20,10 @@
*/
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 { ClusterStore } from "../cluster-store";
import { broadcastMessage, requestMain } from "../ipc";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";

View File

@ -19,7 +19,7 @@
* 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 { productName } from "../vars";
import { WeblinkStore } from "../weblink-store";
@ -86,21 +86,6 @@ export class WebLinkCategory extends CatalogCategory {
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());

View File

@ -18,18 +18,12 @@
* 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 { HotbarStore } from "./hotbar-store";
import React from "react";
import { TabLayout } from "../layout/tab-layout";
const hotbarManagerInjectable = getInjectable({
instantiate: () => HotbarStore.getInstance(),
lifecycle: lifecycleEnum.singleton,
});
export class NotFound extends React.Component {
render() {
return (
<TabLayout className="NotFound" contentClass="flex">
<p className="box center">
Page not found
</p>
</TabLayout>
);
}
}
export default hotbarManagerInjectable;

View File

@ -19,7 +19,7 @@
* 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 migrations from "../migrations/hotbar-store";
import { toJS } from "./utils";
@ -27,7 +27,7 @@ import { CatalogEntity } from "./catalog";
import { catalogEntity } from "../main/catalog-sources/general";
import logger from "../main/logger";
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 {
hotbars: Hotbar[];
@ -52,22 +52,40 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
this.load();
}
get activeHotbarId() {
@computed get activeHotbarId() {
return this._activeHotbarId;
}
set activeHotbarId(id: string) {
if (this.getById(id)) {
this._activeHotbarId = id;
/**
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
* @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);
}
get activeHotbarIndex() {
return this.hotbarIndex(this.activeHotbarId);
private hotbarIndex(hotbar: Hotbar) {
return this.hotbars.indexOf(hotbar);
}
@computed get activeHotbarIndex() {
return this.hotbarIndexById(this.activeHotbarId);
}
@action
@ -87,13 +105,11 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
this.hotbars.forEach(ensureExactHotbarItemLength);
if (data.activeHotbarId) {
if (this.getById(data.activeHotbarId)) {
this.activeHotbarId = data.activeHotbarId;
}
this.setActiveHotbar(data.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);
}
@action
add(data: HotbarCreateOptions, { setActive = false } = {}) {
add = action((data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) => {
const hotbar = getEmptyHotbar(data.name, data.id);
this.hotbars.push(hotbar);
@ -127,29 +142,29 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
if (setActive) {
this._activeHotbarId = hotbar.id;
}
}
});
@action
setHotbarName(id: string, name: string) {
setHotbarName = action((id: string, name: string) => {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if(index < 0) {
console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
return;
if (index < 0) {
return void console.warn(`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id });
}
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);
if (this.activeHotbarId === hotbar.id) {
this.activeHotbarId = this.hotbars[0].id;
this.setActiveHotbar(0);
}
}
});
@action
addToHotbar(item: CatalogEntity, cellIndex?: number) {
@ -263,7 +278,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
index = hotbarStore.hotbars.length - 1;
}
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
hotbarStore.setActiveHotbar(index);
}
switchToNext() {
@ -274,7 +289,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
index = 0;
}
hotbarStore.activeHotbarId = hotbarStore.hotbars[index].id;
hotbarStore.setActiveHotbar(index);
}
/**
@ -284,6 +299,20 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
isAddedToActive(entity: CatalogEntity) {
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
*/
function ensureExactHotbarItemLength(hotbar: Hotbar) {
if (hotbar.items.length === defaultHotbarCells) {
// if we already have `defaultHotbarCells` then we are good to stop
return;
}
// otherwise, keep adding empty entries until full
// if there are not enough items
while (hotbar.items.length < defaultHotbarCells) {
hotbar.items.push(null);
}

View File

@ -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;
name: string;
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 function getEmptyHotbar(name: string, id: string = uuid.v4()): Hotbar {

View File

@ -20,6 +20,7 @@
*/
export const dialogShowOpenDialogHandler = "dialog:show-open-dialog";
export const catalogEntityRunListener = "catalog-entity:run";
export * from "./ipc";
export * from "./invalid-kubeconfig";

View File

@ -30,7 +30,17 @@ import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils";
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";

View File

@ -19,10 +19,10 @@
* 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("getVersion", () => {
describe("getVersion()", () => {
it("should throw if none of the versions are served", () => {
const crd = new CustomResourceDefinition({
apiVersion: "apiextensions.k8s.io/v1",
@ -136,7 +136,7 @@ describe("Crds", () => {
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({
apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
@ -147,7 +147,14 @@ describe("Crds", () => {
},
spec: {
version: "abc",
},
versions: [
{
name: "foobar",
served: true,
storage: true,
},
],
} as CustomResourceDefinitionSpec,
});
expect(crd.getVersion()).toBe("abc");

View File

@ -164,14 +164,14 @@ describe("KubeObject", () => {
describe("isJsonApiDataList", () => {
function isAny(val: unknown): val is any {
return !Boolean(void val);
return true;
}
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";
}

View File

@ -48,34 +48,36 @@ export interface CRDVersion {
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
}
export interface CustomResourceDefinition {
spec: {
group: string;
/**
* @deprecated for apiextensions.k8s.io/v1 but used previously
*/
version?: string;
names: {
plural: string;
singular: string;
kind: 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[];
export interface CustomResourceDefinitionSpec {
group: string;
/**
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/
version?: string;
names: {
plural: string;
singular: string;
kind: string;
listKind: string;
};
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: {
conditions: {
lastTransitionTime: string;
@ -150,27 +152,32 @@ export class CustomResourceDefinition extends KubeObject {
}
getPreferedVersion(): CRDVersion {
// Prefer the modern `versions` over the legacy `version`
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 }));
const { apiVersion } = this;
return {
name: this.spec.version,
served: true,
storage: true,
schema: this.spec.validation,
additionalPrinterColumns,
};
switch (apiVersion) {
case "apiextensions.k8s.io/v1":
for (const version of this.spec.versions) {
if (version.storage) {
return version;
}
}
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() {
@ -197,7 +204,7 @@ export class CustomResourceDefinition extends KubeObject {
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
return columns
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
.filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
}
getValidation() {

View File

@ -187,7 +187,7 @@ export class Ingress extends KubeObject {
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
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);
}
} else if (servicePort !== undefined) {

View File

@ -184,7 +184,8 @@ export function getMetricLastPoints(metrics: Record<string, IMetrics>) {
if (metric.data.result.length) {
result[metricName] = +metric.data.result[0].values.slice(-1)[0][1];
}
} catch (e) {
} catch {
// ignore error
}
return result;

View File

@ -34,6 +34,9 @@ import type { IKubeWatchEvent } from "./kube-watch-api";
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
import { noop } from "../utils";
import type { RequestInit } from "node-fetch";
// BUG: https://github.com/mysticatea/abort-controller/pull/22
// eslint-disable-next-line import/no-named-as-default
import AbortController from "abort-controller";
import { Agent, AgentOptions } from "https";
import type { Patch } from "rfc6902";
@ -698,21 +701,16 @@ export class KubeApi<T extends KubeObject> {
}
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);
}
}

View File

@ -30,6 +30,9 @@ import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse";
import type { KubeJsonApiData } from "./kube-json-api";
import type { RequestInit } from "node-fetch";
// BUG: https://github.com/mysticatea/abort-controller/pull/22
// eslint-disable-next-line import/no-named-as-default
import AbortController from "abort-controller";
import type { Patch } from "rfc6902";
@ -235,8 +238,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}
@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;
namespaces ??= this.context.contextNamespaces;
this.isLoading = true;
try {
@ -469,7 +473,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
switch (type) {
case "ADDED":
case "MODIFIED":
// falls through
case "MODIFIED": {
const newItem = new this.api.objectConstructor(object);
if (!item) {
@ -477,7 +483,9 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} else {
items[index] = newItem;
}
break;
}
case "DELETED":
if (item) {
items.splice(index, 1);

View File

@ -88,7 +88,7 @@ export abstract class LensProtocolRouter {
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) {}

View File

@ -29,7 +29,7 @@ export class SearchStore {
* @param value Unescaped string
*/
public static escapeRegex(value?: string): string {
return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : "";
return value ? value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") : "";
}
/**

View File

@ -22,11 +22,11 @@
import moment from "moment-timezone";
import path from "path";
import os from "os";
import { ThemeStore } from "../../renderer/theme.store";
import { getAppVersion, ObservableToggleSet } from "../utils";
import type { editor } from "monaco-editor";
import merge from "lodash/merge";
import { SemVer } from "semver";
import { defaultTheme } from "../vars";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string;
@ -72,10 +72,10 @@ const shell: PreferenceDescription<string | undefined> = {
const colorTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || ThemeStore.defaultTheme;
return val || defaultTheme;
},
toStore(val) {
if (!val || val === ThemeStore.defaultTheme) {
if (!val || val === defaultTheme) {
return undefined;
}

View File

@ -26,7 +26,7 @@
export function defineGlobal(propName: string, descriptor: PropertyDescriptor) {
const scope = typeof global !== "undefined" ? global : window;
if (scope.hasOwnProperty(propName)) {
if (Object.prototype.hasOwnProperty.call(scope, propName)) {
return;
}

View File

@ -25,6 +25,7 @@ export type Falsey = false | 0 | "" | null | undefined;
* Create a new type safe empty Iterable
* @returns An `Iterable` that yields 0 items
*/
// eslint-disable-next-line require-yield
export function* newEmpty<T>(): IterableIterator<T> {
return;
}

View File

@ -31,12 +31,13 @@ export interface ReadFileFromTarOpts {
}
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[] = [];
await tar.list({
tar.list({
file: tarPath,
filter: entryPath => path.normalize(entryPath) === filePath,
sync: true,
onentry(entry: FileStat) {
entry.on("data", chunk => {
fileChunks.push(chunk);

View File

@ -19,7 +19,7 @@
* 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

View File

@ -41,6 +41,7 @@ export const isIntegrationTesting = process.argv.includes(integrationTestingArg)
export const productName = packageInfo.productName;
export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
export const publicPath = "/build/" as string;
export const defaultTheme = "lens-dark" as string;
// Webpack build paths
export const contextDir = process.cwd();

View File

@ -19,7 +19,6 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import mockFs from "mock-fs";
import { watch } from "chokidar";
import { ExtensionsStore } from "../extensions-store";
import path from "path";
@ -30,6 +29,7 @@ import { AppPaths } from "../../common/app-paths";
import type { ExtensionLoader } from "../extension-loader";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import * as fse from "fs-extra";
jest.setTimeout(60_000);
@ -43,6 +43,7 @@ jest.mock("../extension-installer", () => ({
installPackage: jest.fn(),
},
}));
jest.mock("fs-extra");
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
@ -63,6 +64,7 @@ AppPaths.init();
console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
const mockedFse = fse as jest.Mocked<typeof fse>;
describe("ExtensionDiscovery", () => {
let extensionLoader: ExtensionLoader;
@ -77,63 +79,60 @@ describe("ExtensionDiscovery", () => {
extensionLoader = di.inject(extensionLoaderInjectable);
});
describe("with mockFs", () => {
beforeEach(() => {
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({
name: "my-extension",
}),
});
});
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
afterEach(() => {
mockFs.restore();
});
mockedFse.readJson.mockImplementation((p) => {
expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json"));
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
const mockWatchInstance: any = {
on: jest.fn((event: string, handler: typeof addHandler) => {
if (event === "add") {
addHandler = handler;
}
return mockWatchInstance;
}),
return {
name: "my-extension",
version: "1.0.0",
};
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 => {
@ -150,7 +149,7 @@ describe("ExtensionDiscovery", () => {
};
mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any,
(mockWatchInstance) as any,
);
const extensionDiscovery = ExtensionDiscovery.createInstance(
extensionLoader,
@ -173,3 +172,4 @@ describe("ExtensionDiscovery", () => {
}, 10);
});
});

View File

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

View File

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

View File

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

View File

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

View File

@ -18,8 +18,7 @@
* 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 } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader";
const extensionLoaderInjectable = getInjectable({

View File

@ -279,11 +279,7 @@ export class ExtensionLoader {
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
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.TopBarRegistry.getInstance().add(extension.topBarItems),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
@ -315,7 +311,6 @@ export class ExtensionLoader {
registries.KubeObjectDetailRegistry.getInstance().add(extension.kubeObjectDetailItems),
registries.KubeObjectStatusRegistry.getInstance().add(extension.kubeObjectStatusTexts),
registries.WorkloadsOverviewDetailRegistry.getInstance().add(extension.kubeWorkloadsOverviewItems),
registries.CommandRegistry.getInstance().add(extension.commands),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {

View File

@ -41,7 +41,6 @@ export const getDiForUnitTesting = () => {
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();

View File

@ -25,9 +25,10 @@ import { catalogEntityRegistry } from "../main/catalog";
import type { CatalogEntity } from "../common/catalog";
import type { IObservableArray } from "mobx";
import type { MenuRegistration } from "../main/menu/menu-registration";
import type { TrayMenuRegistration } from "../main/tray/tray-menu-registration";
export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = [];
trayMenus: TrayMenuRegistration[] = [];
async navigate(pageId?: string, params?: Record<string, any>, frameId?: number) {
return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId);

View File

@ -26,7 +26,11 @@ import type { CatalogEntity } from "../common/catalog";
import type { Disposer } from "../common/utils";
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-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 { 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 {
globalPages: registries.PageRegistration[] = [];
@ -39,11 +43,11 @@ export class LensRendererExtension extends LensExtension {
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
commands: registries.CommandRegistration[] = [];
welcomeMenus: registries.WelcomeMenuRegistration[] = [];
welcomeBanners: registries.WelcomeBannerRegistration[] = [];
commands: CommandRegistration[] = [];
welcomeMenus: WelcomeMenuRegistration[] = [];
welcomeBanners: WelcomeBannerRegistration[] = [];
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
topBarItems: registries.TopBarRegistration[] = [];
topBarItems: TopBarRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation");

View File

@ -22,7 +22,7 @@
// Base class for extensions-api registries
import { action, observable, makeObservable } from "mobx";
import { Singleton } from "../../common/utils";
import { LensExtension } from "../lens-extension";
import type { LensExtension } from "../lens-extension";
export class BaseRegistry<T, I = T> extends Singleton {
private items = observable.map<T, I>([], { deep: false });

View File

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

View File

@ -28,11 +28,7 @@ export * from "./status-bar-registry";
export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry";
export * from "./kube-object-status-registry";
export * from "./command-registry";
export * from "./entity-setting-registry";
export * from "./welcome-menu-registry";
export * from "./welcome-banner-registry";
export * from "./catalog-entity-detail-registry";
export * from "./workloads-overview-detail-registry";
export * from "./topbar-registry";
export * from "./protocol-handler";

View File

@ -135,10 +135,7 @@ class PageRegistry extends BaseRegistry<PageRegistration, RegisteredPage> {
);
if (notAStringValue && !(parse || stringify)) {
throw new Error(
`PageRegistry: param's "${paramName}" initialization has failed:
paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`,
);
throw new Error(`PageRegistry: param's "${paramName}" initialization has failed: paramInit.parse() and paramInit.stringify() are required for non string | string[] "defaultValue"`);
}
paramInit.defaultValue = value;

View File

@ -19,6 +19,9 @@
* 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
export * from "../../renderer/components/layout/main-layout";
export * from "../../renderer/components/layout/setting-layout";
@ -36,7 +39,7 @@ export * from "../../renderer/components/switch";
export * from "../../renderer/components/input/input";
// command-overlay
export { CommandOverlay } from "../../renderer/components/command-palette";
export const CommandOverlay = asLegacyGlobalObjectForExtensionApi(commandOverlayInjectable);
// other components
export * from "../../renderer/components/icon";

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

View File

@ -81,7 +81,7 @@ describe("kubeconfig manager tests", () => {
let contextHandler: ContextHandler;
beforeEach(() => {
const mockOpts = {
mockFs({
"minikube-config.yml": JSON.stringify({
apiVersion: "v1",
clusters: [{
@ -103,9 +103,7 @@ describe("kubeconfig manager tests", () => {
kind: "Config",
preferences: {},
}),
};
mockFs(mockOpts);
});
cluster = new Cluster({
id: "foo",

View File

@ -25,10 +25,9 @@ import { isLinux, isMac, isPublishConfigured, isTestEnv } from "../common/vars";
import { delay } from "../common/utils";
import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
import { once } from "lodash";
import { ipcMain } from "electron";
import { ipcMain, autoUpdater as electronAutoUpdater } from "electron";
import { nextUpdateChannel } from "./utils/update-channel";
import { UserStore } from "../common/user-store";
import { autoUpdater as electronAutoUpdater } from "electron";
let installVersion: null | string = null;

View File

@ -24,11 +24,15 @@ import { createContainer } from "@ogre-tools/injectable";
export const getDi = () =>
createContainer(
getRequireContextForMainCode,
getRequireContextForCommonCode,
getRequireContextForCommonExtensionCode,
);
const getRequireContextForMainCode = () =>
require.context("./", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonCode = () =>
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonExtensionCode = () =>
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);

View File

@ -19,7 +19,7 @@
* 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 * as yaml from "js-yaml";
import { promiseExecFile } from "../../common/utils/promise-exec";

View File

@ -119,7 +119,9 @@ export class HelmRepoManager extends Singleton {
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig as HelmRepoConfig;
}
} catch { }
} catch {
// ignore error
}
return {
repositories: [],

View File

@ -60,7 +60,7 @@ import { SentryInit } from "../common/sentry";
import { ensureDir } from "fs-extra";
import { Router } from "./router";
import { initMenu } from "./menu/menu";
import { initTray } from "./tray";
import { initTray } from "./tray/tray";
import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions";
import { AppPaths } from "../common/app-paths";
import { ShellSession } from "./shell-session/shell-session";
@ -68,6 +68,7 @@ import { getDi } from "./getDi";
import electronMenuItemsInjectable from "./menu/electron-menu-items.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 trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
const di = getDi();
@ -104,6 +105,7 @@ mangleProxyEnv();
logger.debug("[APP-MAIN] initializing ipc main handlers");
const menuItems = di.inject(electronMenuItemsInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
initializers.initIpcMainHandlers(menuItems);
@ -244,7 +246,7 @@ app.on("ready", async () => {
onQuitCleanup.push(
initMenu(windowManager, menuItems),
initTray(windowManager),
initTray(windowManager, trayMenuItems),
() => ShellSession.cleanup(),
);

View File

@ -97,7 +97,9 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue<MenuRegist
const localStorageFilePath = path.resolve(AppPaths.get("userData"), "lens-local-storage", `${cluster.id}.json`);
await remove(localStorageFilePath);
} catch {}
} catch {
// ignore error
}
});
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {

View File

@ -27,12 +27,15 @@ import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile";
import { helmCli } from "./helm/helm-cli";
import { UserStore } from "../common/user-store";
import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
import { SemVer } from "semver";
import { defaultPackageMirror, packageMirrors } from "../common/user-store/preferences-helpers";
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 kubectlMap: Map<string, string> = new Map([
@ -53,7 +56,7 @@ const kubectlMap: Map<string, string> = new Map([
["1.21", bundledVersion],
]);
let bundledPath: string;
const initScriptVersionString = "# lens-initscript v3\n";
const initScriptVersionString = "# lens-initscript v3";
export function bundledKubectlPath(): string {
if (bundledPath) { return bundledPath; }
@ -309,99 +312,92 @@ export class Kubectl {
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
return new Promise<void>((resolve, reject) => {
const stream = customRequest({
url: this.url,
gzip: true,
});
const file = fs.createWriteStream(this.path);
const downloadStream = got.stream({ url: this.url, decompress: true });
const fileWriteStream = fs.createWriteStream(this.path, { mode: 0o755 });
const pipeline = promisify(stream.pipeline);
stream.on("complete", () => {
logger.debug("kubectl binary download finished");
file.end();
});
stream.on("error", (error) => {
logger.error(error);
fs.unlink(this.path, () => {
// 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);
});
try {
await pipeline(downloadStream, fileWriteStream);
await fs.promises.chmod(this.path, 0o755);
logger.debug("kubectl binary download finished");
} catch (error) {
await fs.promises.unlink(this.path).catch(noop);
throw error;
}
}
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 fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
let bashScript = `${initScriptVersionString}`;
bashScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
bashScript += "test -f \"/etc/profile\" && . \"/etc/profile\"\n";
bashScript += "if test -f \"$HOME/.bash_profile\"; then\n";
bashScript += " . \"$HOME/.bash_profile\"\n";
bashScript += "elif test -f \"$HOME/.bash_login\"; then\n";
bashScript += " . \"$HOME/.bash_login\"\n";
bashScript += "elif test -f \"$HOME/.profile\"; then\n";
bashScript += " . \"$HOME/.profile\"\n";
bashScript += "fi\n";
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
bashScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`;
bashScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`;
bashScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`;
bashScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`;
bashScript += "export NO_PROXY\n";
bashScript += "unset tempkubeconfig\n";
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
const bashScript = [
initScriptVersionString,
"tempkubeconfig=\"$KUBECONFIG\"",
"test -f \"/etc/profile\" && . \"/etc/profile\"",
"if test -f \"$HOME/.bash_profile\"; then",
" . \"$HOME/.bash_profile\"",
"elif test -f \"$HOME/.bash_login\"; then",
" . \"$HOME/.bash_login\"",
"elif test -f \"$HOME/.profile\"; then",
" . \"$HOME/.profile\"",
"fi",
`export PATH="${helmPath}:${kubectlPath}:$PATH"`,
'export KUBECONFIG="$tempkubeconfig"',
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
`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",
].join("\n");
const zshScriptPath = path.join(this.dirname, ".zlogin");
let zshScript = `${initScriptVersionString}`;
const zshScript = [
initScriptVersionString,
"tempkubeconfig=\"$KUBECONFIG\"",
zshScript += "tempkubeconfig=\"$KUBECONFIG\"\n";
// restore previous 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";
// restore previous ZDOTDIR
"export ZDOTDIR=\"$OLD_ZDOTDIR\"",
// voodoo to replace any previous occurrences of kubectl path in the PATH
zshScript += `kubectlpath=\"${kubectlPath}"\n`;
zshScript += `helmpath=\"${helmPath}"\n`;
zshScript += "p=\":$kubectlpath:\"\n";
zshScript += "d=\":$PATH:\"\n";
zshScript += `d=\${d//$p/:}\n`;
zshScript += `d=\${d/#:/}\n`;
zshScript += `export PATH=\"$helmpath:$kubectlpath:\${d/%:/}\"\n`;
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n";
zshScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`;
zshScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`;
zshScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`;
zshScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`;
zshScript += "export NO_PROXY\n";
zshScript += "unset tempkubeconfig\n";
zshScript += "unset OLD_ZDOTDIR\n";
await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 });
// source all the files
"test -f \"$OLD_ZDOTDIR/.zshenv\" && . \"$OLD_ZDOTDIR/.zshenv\"",
"test -f \"$OLD_ZDOTDIR/.zprofile\" && . \"$OLD_ZDOTDIR/.zprofile\"",
"test -f \"$OLD_ZDOTDIR/.zlogin\" && . \"$OLD_ZDOTDIR/.zlogin\"",
"test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"",
// voodoo to replace any previous occurrences of kubectl path in the PATH
`kubectlpath="${kubectlPath}"`,
`helmpath="${helmPath}"`,
"p=\":$kubectlpath:\"",
"d=\":$PATH:\"",
`d=\${d//$p/:}`,
`d=\${d/#:/}`,
`export PATH="$helmpath:$kubectlpath:\${d/%:/}"`,
"export KUBECONFIG=\"$tempkubeconfig\"",
`NO_PROXY=",\${NO_PROXY:-localhost},"`,
`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 {
// MacOS packages are only available from default
const mirror = packageMirrors.get(UserStore.getInstance().downloadMirror)
const { url } = packageMirrors.get(UserStore.getInstance().downloadMirror)
?? packageMirrors.get(defaultPackageMirror);
return mirror.url;
return url;
}
}

View File

@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({
const extensions = di.inject(mainExtensionsInjectable);
return computed(() =>
extensions.get().flatMap((extension) => extension.appMenus),
);
extensions.get().flatMap((extension) => extension.appMenus));
},
});

View File

@ -216,8 +216,16 @@ export function getAppMenu(
label: "Command Palette...",
accelerator: "Shift+CmdOrCtrl+P",
id: "command-palette",
click() {
broadcastMessage("command-palette:open");
click(_m, _b, event) {
/**
* 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" },

View File

@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider {
case "cluster":
switch (queryName) {
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":
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
case "memoryRequests":
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
case "memoryAllocatableCapacity":
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
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":
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
case "cpuLimits":
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
case "podAllocatableCapacity":
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
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":
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;
case "nodes":
switch (queryName) {
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":
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`;
return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
case "memoryCapacity":
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
case "memoryAllocatableCapacity":
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
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":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
case "cpuAllocatableCapacity":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
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":
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;
case "pods":

View File

@ -25,7 +25,7 @@ import { exec } from "child_process";
import fs from "fs-extra";
import * as yaml from "js-yaml";
import path from "path";
import * as tempy from "tempy";
import tempy from "tempy";
import logger from "./logger";
import { appEventBus } from "../common/event-bus";
import { cloneJsonObject } from "../common/utils";

View File

@ -182,7 +182,6 @@ export class Router {
// 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: "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);
// Helm API

View File

@ -188,31 +188,6 @@ export class PortForwardRoute {
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) {
const { params, query, response, cluster } = request;
const { namespace, resourceType, resourceName } = params;

View File

@ -72,6 +72,7 @@ export class NodeShellSession extends ShellSession {
switch (nodeOs) {
default:
logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`);
// fallthrough
case "linux":
args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))");
break;

View File

@ -134,7 +134,9 @@ export abstract class ShellSession {
for (const shellProcess of this.processes.values()) {
try {
process.kill(shellProcess.pid);
} catch {}
} catch {
// ignore error
}
}
this.processes.clear();
@ -214,7 +216,9 @@ export abstract class ShellSession {
if (stats.isDirectory()) {
return potentialCwd;
}
} catch {}
} catch {
// ignore error
}
}
return "."; // Always valid

View File

@ -18,19 +18,19 @@
* 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 mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
import { HotbarStore } from "../../../common/hotbar-store";
const trayItemsInjectable = getInjectable({
lifecycle: lifecycleEnum.singleton,
function hotbarIndex(id: string) {
return HotbarStore.getInstance().hotbarIndex(id) + 1;
}
instantiate: (di) => {
const extensions = di.inject(mainExtensionsInjectable);
export function hotbarDisplayLabel(id: string) : string {
const hotbar = HotbarStore.getInstance().getById(id);
return computed(() =>
extensions.get().flatMap(extension => extension.trayMenus));
},
});
return `${hotbarIndex(id)}: ${hotbar.name}`;
}
export function hotbarDisplayIndex(id: string) : string {
return hotbarIndex(id).toString();
}
export default trayItemsInjectable;

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

View File

@ -19,16 +19,12 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type React from "react";
import { BaseRegistry } from "./base-registry";
interface TopBarComponents {
Item: React.ComponentType;
}
export interface TopBarRegistration {
components: TopBarComponents;
}
export class TopBarRegistry extends BaseRegistry<TopBarRegistration> {
export interface TrayMenuRegistration {
label?: string;
click?: (menuItem: TrayMenuRegistration) => void;
id?: string;
type?: "normal" | "separator" | "submenu"
toolTip?: string;
enabled?: boolean;
submenu?: TrayMenuRegistration[]
}

View File

@ -20,16 +20,18 @@
*/
import path from "path";
import packageInfo from "../../package.json";
import packageInfo from "../../../package.json";
import { Menu, Tray } from "electron";
import { autorun } from "mobx";
import { showAbout } from "./menu/menu";
import { checkForUpdates, isAutoUpdateEnabled } from "./app-updater";
import type { WindowManager } from "./window-manager";
import logger from "./logger";
import { isDevelopment, isWindows, productName } from "../common/vars";
import { exitApp } from "./exit-app";
import { preferencesURL } from "../common/routes";
import { autorun, IComputedValue } from "mobx";
import { showAbout } from "../menu/menu";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { WindowManager } from "../window-manager";
import logger from "../logger";
import { isDevelopment, isWindows, productName } from "../../common/vars";
import { exitApp } from "../exit-app";
import { preferencesURL } from "../../common/routes";
import { toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration";
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();
tray = new Tray(icon);
@ -62,7 +67,7 @@ export function initTray(windowManager: WindowManager) {
const disposers = [
autorun(() => {
try {
const menu = createTrayMenu(windowManager);
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()));
tray.setContextMenu(menu);
} catch (error) {
@ -78,8 +83,21 @@ export function initTray(windowManager: WindowManager) {
};
}
function createTrayMenu(windowManager: WindowManager): Menu {
const template: Electron.MenuItemConstructorOptions[] = [
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): 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}`,
click() {
@ -108,6 +126,8 @@ function createTrayMenu(windowManager: WindowManager): Menu {
});
}
template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
return Menu.buildFromTemplate(template.concat([
{
label: `About ${productName}`,

View File

@ -33,7 +33,7 @@ export default {
const contextName = value[0];
// 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] });
}
},

View File

@ -34,7 +34,7 @@ export default {
if (!cluster.kubeConfig) continue;
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;
}

View File

@ -20,7 +20,6 @@
*/
import { CatalogEntityRegistry } from "../catalog-entity-registry";
import "../../../common/catalog-entities";
import { catalogCategoryRegistry } from "../../../common/catalog/catalog-category-registry";
import { CatalogCategory, CatalogEntityData, CatalogEntityKindData } from "../catalog-entity";
import { KubernetesCluster, WebLink } from "../../../common/catalog-entities";

View File

@ -20,7 +20,7 @@
*/
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 "../../common/catalog-entities";
import type { Cluster } from "../../main/cluster";
@ -28,14 +28,22 @@ import { ClusterStore } from "../../common/cluster-store";
import { Disposer, iter } from "../utils";
import { once } from "lodash";
import logger from "../../common/logger";
import { catalogEntityRunContext } from "./catalog-entity";
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
import { ipcRenderer } from "electron";
import { CatalogIpcEvents } from "../../common/ipc/catalog";
import { navigate } from "../navigation";
import { isMainFrame } from "process";
export type EntityFilter = (entity: CatalogEntity) => any;
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 {
@observable protected activeEntityId: string | undefined = undefined;
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
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)[]) {

View File

@ -19,10 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { navigate } from "../navigation";
import type { CatalogEntity } from "../../common/catalog";
import { catalogEntityRegistry } from "./catalog-entity-registry";
export { catalogEntityRunContext } from "./catalog-entity-registry";
export { CatalogCategory, CatalogEntity } from "../../common/catalog";
export type {
CatalogEntityData,
@ -33,10 +30,3 @@ export type {
CatalogEntityContextMenu,
CatalogEntityContextMenuContext,
} from "../../common/catalog";
export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url),
setCommandPaletteContext: (entity?: CatalogEntity) => {
catalogEntityRegistry.activeEntity = entity;
},
};

View File

@ -21,13 +21,14 @@
import { when } from "mobx";
import { catalogCategoryRegistry } from "../../../common/catalog";
import { catalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
import { isActiveRoute } from "../../../renderer/navigation";
import { catalogEntityRegistry } from "../catalog-entity-registry";
import { isActiveRoute } from "../../navigation";
import type { GeneralEntity } from "../../../common/catalog-entities";
export async function setEntityOnRouteMatch() {
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));
if (activeEntity) {

View File

@ -49,7 +49,7 @@ import { SentryInit } from "../common/sentry";
import { TerminalStore } from "./components/dock/terminal.store";
import { AppPaths } from "../common/app-paths";
import { registerCustomThemes } from "./components/monaco-editor";
import { getDi } from "./components/getDi";
import { getDi } from "./getDi";
import { DiContextProvider } from "@ogre-tools/injectable-react";
import type { DependencyInjectionContainer } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
@ -59,6 +59,7 @@ import bindProtocolAddRouteHandlersInjectable
import type { LensProtocolRouterRenderer } from "./protocol-handler";
import lensProtocolRouterRendererInjectable
from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable";
import commandOverlayInjectable from "./components/command-palette/command-overlay.injectable";
if (process.isMainFrame) {
SentryInit();
@ -102,9 +103,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing Registries`);
initializers.initRegistries();
logger.info(`${logPrefix} initializing CommandRegistry`);
initializers.initCommandRegistry();
logger.info(`${logPrefix} initializing EntitySettingsRegistry`);
initializers.initEntitySettingsRegistry();
@ -114,9 +112,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
initializers.initKubeObjectDetailRegistry();
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
initializers.initWelcomeMenuRegistry();
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
initializers.initWorkloadsOverviewDetailRegistry();
@ -127,7 +122,9 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
initializers.initCatalogCategoryRegistryEntries();
logger.info(`${logPrefix} initializing Catalog`);
initializers.initCatalog();
initializers.initCatalog({
openCommandDialog: di.inject(commandOverlayInjectable).open,
});
const extensionLoader = di.inject(extensionLoaderInjectable);

View File

@ -23,7 +23,6 @@ import { observable, makeObservable, when } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react";
import { Redirect, Route, Router, Switch } from "react-router";
import { history } from "./navigation";
import { NotFound } from "./components/+404";
import { UserManagement } from "./components/+user-management/user-management";
import { ConfirmDialog } from "./components/confirm-dialog";
import { ClusterOverview } from "./components/+cluster/cluster-overview";
@ -230,7 +229,11 @@ export class ClusterFrame extends React.Component {
{this.renderExtensionTabLayoutRoutes()}
{this.renderExtensionRoutes()}
<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>
</MainLayout>
<Notifications/>

View File

@ -45,15 +45,17 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
const renderName = (metric: IHpaMetric) => {
switch (metric.type) {
case HpaMetricType.Resource:
const addition = metric.resource.targetAverageUtilization ? <>(as a percentage of request)</> : "";
case HpaMetricType.Resource: {
const addition = metric.resource.targetAverageUtilization
? "(as a percentage of request)"
: "";
return <>Resource {metric.resource.name} on Pods {addition}</>;
}
case HpaMetricType.Pods:
return <>{metric.pods.metricName} on Pods</>;
case HpaMetricType.Object:
case HpaMetricType.Object: {
const { target } = metric.object;
const { kind, name } = target;
const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa));
@ -64,6 +66,7 @@ export class HpaDetails extends React.Component<HpaDetailsProps> {
<Link to={objectUrl}>{kind}/{name}</Link>
</>
);
}
case HpaMetricType.External:
return (
<>

View File

@ -66,22 +66,15 @@ export class CRDStore extends KubeObjectStore<CustomResourceDefinition> {
@computed get groups() {
const groups: Record<string, CustomResourceDefinition[]> = {};
return this.items.reduce((groups, crd) => {
const group = crd.getGroup();
for (const crd of this.items) {
(groups[crd.getGroup()] ??= []).push(crd);
}
if (!groups[group]) groups[group] = [];
groups[group].push(crd);
return groups;
}, groups);
return groups;
}
getByGroup(group: string, pluralName: string) {
const crdInGroup = this.groups[group];
if (!crdInGroup) return null;
return crdInGroup.find(crd => crd.getPluralName() === pluralName);
return this.groups[group]?.find(crd => crd.getPluralName() === pluralName);
}
getByObject(obj: KubeObject) {

View File

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

View File

@ -34,7 +34,7 @@ import { mockWindow } from "../../../../../__mocks__/windowMock";
import { AppPaths } from "../../../../common/app-paths";
import extensionLoaderInjectable
from "../../../../extensions/extension-loader/extension-loader.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor";
mockWindow();

View File

@ -18,8 +18,7 @@
* 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 } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { unpackExtension } from "./unpack-extension";
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";

View File

@ -47,8 +47,8 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) =
} catch (error) {
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);
// fallthrough
}
// fallthrough
}
default:
case ExtensionRegistryLocation.DEFAULT:

View File

@ -21,11 +21,10 @@
import React from "react";
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 { MenuItem } from "../menu";
import { Icon } from "../icon";
import { PortForwardDialog } from "../../port-forward";
import { Notifications } from "../notifications";
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() {
const { portForward, toolbar } = this.props;
@ -52,14 +83,17 @@ export class PortForwardMenu extends React.Component<Props> {
return (
<>
<MenuItem onClick={() => openPortForward(this.props.portForward)}>
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
<span className="title">Open</span>
</MenuItem>
{ portForward.status === "Active" &&
<MenuItem onClick={() => openPortForward(portForward)}>
<Icon material="open_in_browser" interactive={toolbar} tooltip="Open in browser" />
<span className="title">Open</span>
</MenuItem>
}
<MenuItem onClick={() => PortForwardDialog.open(portForward)}>
<Icon material="edit" tooltip="Change port or protocol" interactive={toolbar} />
<span className="title">Edit</span>
</MenuItem>
{this.renderStartStopMenuItem()}
</>
);
}

View File

@ -70,7 +70,7 @@ export class PortForwards extends React.Component<Props> {
showDetails = (item: PortForwardItem) => {
navigation.push(portForwardsURL({
params: {
forwardport: String(item.getForwardPort()),
forwardport: item.getId(),
},
}));
};

View File

@ -24,13 +24,14 @@ import "./service-port-component.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
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 { Notifications } from "../notifications";
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 { Spinner } from "../spinner";
import logger from "../../../common/logger";
interface Props {
service: Service;
@ -42,6 +43,7 @@ export class ServicePortComponent extends React.Component<Props> {
@observable waiting = false;
@observable forwardPort = 0;
@observable isPortForwarded = false;
@observable isActive = false;
constructor(props: Props) {
super(props);
@ -51,13 +53,14 @@ export class ServicePortComponent extends React.Component<Props> {
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => [portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()),
reaction(() => this.props.service, () => this.checkExistingPortForwarding()),
]);
}
@action
async checkExistingPortForwarding() {
const { service, port } = this.props;
const portForward: ForwardedPort = {
let portForward: ForwardedPort = {
kind: "service",
name: service.getName(),
namespace: service.getNs(),
@ -65,57 +68,66 @@ export class ServicePortComponent extends React.Component<Props> {
forwardPort: this.forwardPort,
};
let activePort: number;
try {
activePort = await getPortForward(portForward) ?? 0;
portForward = await getPortForward(portForward);
} catch (error) {
this.isPortForwarded = false;
this.isActive = false;
return;
}
this.forwardPort = activePort;
this.isPortForwarded = activePort ? true : false;
this.forwardPort = portForward.forwardPort;
this.isPortForwarded = true;
this.isActive = portForward.status === "Active";
}
@action
async portForward() {
const { service, port } = this.props;
const portForward: ForwardedPort = {
let portForward: ForwardedPort = {
kind: "service",
name: service.getName(),
namespace: service.getNs(),
port: port.port,
forwardPort: this.forwardPort,
protocol: predictProtocol(port.name),
status: "Active",
};
this.waiting = true;
try {
// determine how many port-forwards are already active
const { length } = await getPortForwards();
// determine how many port-forwards already exist
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) {
portForward.forwardPort = this.forwardPort;
this.forwardPort = portForward.forwardPort;
if (portForward.status === "Active") {
openPortForward(portForward);
this.isPortForwarded = true;
// if this is the first port-forward show the about notification
if (!length) {
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) {
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
this.checkExistingPortForwarding();
logger.error("[SERVICE-PORT-COMPONENT]:", error, portForward);
} finally {
this.checkExistingPortForwarding();
this.waiting = false;
}
}
@action
async stopPortForward() {
const { service, port } = this.props;
const portForward: ForwardedPort = {
@ -130,11 +142,11 @@ export class ServicePortComponent extends React.Component<Props> {
try {
await removePortForward(portForward);
this.isPortForwarded = false;
} catch (error) {
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
this.checkExistingPortForwarding();
} finally {
this.checkExistingPortForwarding();
this.forwardPort = 0;
this.waiting = false;
}
}
@ -142,7 +154,7 @@ export class ServicePortComponent extends React.Component<Props> {
render() {
const { port, service } = this.props;
const portForwardAction = async () => {
const portForwardAction = action(async () => {
if (this.isPortForwarded) {
await this.stopPortForward();
} else {
@ -155,16 +167,16 @@ export class ServicePortComponent extends React.Component<Props> {
protocol: predictProtocol(port.name),
};
PortForwardDialog.open(portForward, { openInBrowser: true });
PortForwardDialog.open(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() });
}
};
});
return (
<div className={cssNames("ServicePortComponent", { waiting: this.waiting })}>
<span title="Open in a browser" onClick={() => this.portForward()}>
{port.toString()}
</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 && (
<Spinner />
)}

View File

@ -34,6 +34,7 @@ $service-status-color-list: (
$port-forward-status-color-list: (
active: var(--colorOk),
disabled: var(--colorSoftError)
);
@mixin port-forward-status-colors {

View File

@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store";
import { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { isWindows } from "../../../common/vars";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
import moment from "moment-timezone";
import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers";
import { action } from "mobx";
@ -86,16 +86,12 @@ export const Application = observer(() => {
<section id="terminalSelection">
<SubTitle title="Terminal copy & paste" />
<FormSwitch
label="Copy on select and paste on right-click"
control={
<Switcher
checked={userStore.terminalCopyOnSelect}
onChange={v => userStore.terminalCopyOnSelect = v.target.checked}
name="terminalCopyOnSelect"
/>
}
/>
<Switch
checked={userStore.terminalCopyOnSelect}
onChange={() => userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect}
>
Copy on select and paste on right-click
</Switch>
</section>
<hr/>
@ -135,16 +131,9 @@ export const Application = observer(() => {
<section id="other">
<SubTitle title="Start-up"/>
<FormSwitch
control={
<Switcher
checked={userStore.openAtLogin}
onChange={v => userStore.openAtLogin = v.target.checked}
name="startup"
/>
}
label="Automatically start Lens on login"
/>
<Switch checked={userStore.openAtLogin} onChange={() => userStore.openAtLogin = !userStore.openAtLogin}>
Automatically start Lens on login
</Switch>
</section>
<hr />

View File

@ -21,7 +21,7 @@
import { observer } from "mobx-react";
import React from "react";
import { UserStore } from "../../../common/user-store";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
import { Select } from "../select";
import { SubTitle } from "../layout/sub-title";
import { SubHeader } from "../layout/sub-header";
@ -45,15 +45,12 @@ export const Editor = observer(() => {
<section>
<div className="flex gaps justify-space-between">
<div className="flex gaps align-center">
<FormSwitch
label={<SubHeader compact>Show minimap</SubHeader>}
control={
<Switcher
checked={editorConfiguration.minimap.enabled}
onChange={(evt, checked) => editorConfiguration.minimap.enabled = checked}
/>
}
/>
<Switch
checked={editorConfiguration.minimap.enabled}
onChange={() => editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled}
>
Show minimap
</Switch>
</div>
<div className="flex gaps align-center">
<SubHeader compact>Position</SubHeader>

View File

@ -26,7 +26,7 @@ import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-s
import { observer } from "mobx-react";
import { bundledKubectlPath } from "../../../main/kubectl";
import { SelectOption, Select } from "../select";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
import { packageMirrors } from "../../../common/user-store/preferences-helpers";
export const KubectlBinaries = observer(() => {
@ -48,16 +48,12 @@ export const KubectlBinaries = observer(() => {
<>
<section>
<SubTitle title="Kubectl binary download"/>
<FormSwitch
control={
<Switcher
checked={userStore.downloadKubectlBinaries}
onChange={v => userStore.downloadKubectlBinaries = v.target.checked}
name="kubectl-download"
/>
}
label="Download kubectl binaries matching the Kubernetes cluster version"
/>
<Switch
checked={userStore.downloadKubectlBinaries}
onChange={() => userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries}
>
Download kubectl binaries matching the Kubernetes cluster version
</Switch>
</section>
<section>

View File

@ -24,10 +24,11 @@ import React from "react";
import { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { SubTitle } from "../layout/sub-title";
import { FormSwitch, Switcher } from "../switch";
import { Switch } from "../switch";
export const LensProxy = observer(() => {
const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || "");
const store = UserStore.getInstance();
return (
<section id="proxy">
@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
<section className="small">
<SubTitle title="Certificate Trust"/>
<FormSwitch
control={
<Switcher
checked={UserStore.getInstance().allowUntrustedCAs}
onChange={v => UserStore.getInstance().allowUntrustedCAs = v.target.checked}
name="startup"
/>
}
label="Allow untrusted Certificate Authorities"
/>
<Switch checked={store.allowUntrustedCAs} onChange={() => store.allowUntrustedCAs = !store.allowUntrustedCAs}>
Allow untrusted Certificate Authorities
</Switch>
<small className="hint">
This will make Lens to trust ANY certificate authority without any validations.{" "}
Needed with some corporate proxies that do certificate re-writing.{" "}

View File

@ -20,45 +20,55 @@
*/
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 { Welcome } from "../welcome";
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries";
import { defaultWidth } from "../welcome";
import { defaultWidth, Welcome } from "../welcome";
import { computed } from "mobx";
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(
"electron",
() => ({
ipcRenderer: {
on: jest.fn(),
},
app: {
getPath: () => "tmp",
},
}),
);
jest.mock("electron", () => ({
ipcRenderer: {
on: jest.fn(),
},
app: {
getPath: () => "tmp",
},
}));
describe("<Welcome/>", () => {
beforeEach(() => {
TopBarRegistry.createInstance();
WelcomeMenuRegistry.createInstance();
WelcomeBannerRegistry.createInstance();
});
let render: DiRender;
let di: ConfigurableDependencyInjectionContainer;
let welcomeBannersStub: WelcomeBannerRegistration[];
afterEach(() => {
TopBarRegistry.resetInstance();
WelcomeMenuRegistry.resetInstance();
WelcomeBannerRegistry.resetInstance();
beforeEach(() => {
di = getDiForUnitTesting();
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 () => {
const testId = "testId";
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{
Banner: () => <div data-testid={testId} />,
},
]);
welcomeBannersStub.push({
Banner: () => <div data-testid={testId} />,
});
const { container } = render(<Welcome />);
@ -67,16 +77,15 @@ describe("<Welcome/>", () => {
});
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [
{
width: 100,
Banner: () => <div />,
},
{
width: 800,
Banner: () => <div />,
},
]);
welcomeBannersStub.push({
width: 100,
Banner: () => <div />,
});
welcomeBannersStub.push({
width: 800,
Banner: () => <div />,
});
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;
}
}

View File

@ -18,18 +18,20 @@
* 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 { computed } from "mobx";
import { catalogURL } from "../../common/routes";
import { WelcomeMenuRegistry } from "../../extensions/registries";
import { navigate } from "../navigation";
const welcomeBannerItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
export function initWelcomeMenuRegistry() {
WelcomeMenuRegistry.getInstance()
.add([
{
title: "Browse Clusters in Catalog",
icon: "view_list",
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )),
},
return computed(() => [
...extensions.get().flatMap((extension) => extension.welcomeBanners),
]);
}
},
lifecycle: lifecycleEnum.singleton,
});
export default welcomeBannerItemsInjectable;

View File

@ -19,8 +19,6 @@
* 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
* Provide a Banner component to be renderered in the welcome screen.
@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration {
*/
width?: number
}
export class WelcomeBannerRegistry extends BaseRegistry<WelcomeBannerRegistration> { }

View File

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

View File

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

View File

@ -19,12 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { BaseRegistry } from "./base-registry";
export interface WelcomeMenuRegistration {
title: string | (() => string);
icon: string;
click: () => void | Promise<void>;
}
export class WelcomeMenuRegistry extends BaseRegistry<WelcomeMenuRegistration> {}

View File

@ -22,78 +22,129 @@
import "./welcome.scss";
import React from "react";
import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import Carousel from "react-material-ui-carousel";
import { Icon } from "../icon";
import { productName, slackUrl } from "../../../common/vars";
import { WelcomeMenuRegistry } from "../../../extensions/registries";
import { WelcomeBannerRegistry } from "../../../extensions/registries";
import { withInjectables } from "@ogre-tools/injectable-react";
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;
@observer
export class Welcome extends React.Component {
render() {
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems();
interface Dependencies {
welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>
welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>
}
// if there is banner with specified width, use it to calculate the width of the container
const maxWidth = welcomeBanner.reduce((acc, curr) => {
const currWidth = curr.width ?? 0;
const NonInjectedWelcome: React.FC<Dependencies> = ({ welcomeMenuItems, welcomeBannerItems }) => {
const welcomeBanners = welcomeBannerItems.get();
if (acc > currWidth) {
return acc;
}
// if there is banner with specified width, use it to calculate the width of the container
const maxWidth = welcomeBanners.reduce((acc, curr) => {
const currWidth = curr.width ?? 0;
return currWidth;
}, defaultWidth);
if (acc > currWidth) {
return acc;
}
return (
<div className="flex justify-center Welcome align-center">
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
{welcomeBanner.length > 0 ? (
<Carousel
stopAutoPlayOnHover={true}
indicators={welcomeBanner.length > 1}
autoPlay={true}
navButtonsAlwaysInvisible={true}
indicatorIconButtonProps={{
style: {
color: "var(--iconActiveBackground)",
},
}}
activeIndicatorIconButtonProps={{
style: {
color: "var(--iconActiveColor)",
},
}}
interval={8000}
return currWidth;
}, defaultWidth);
return (
<div className="flex justify-center Welcome align-center">
<div
style={{ width: `${maxWidth}px` }}
data-testid="welcome-banner-container"
>
{welcomeBanners.length > 0 ? (
<Carousel
stopAutoPlayOnHover={true}
indicators={welcomeBanners.length > 1}
autoPlay={true}
navButtonsAlwaysInvisible={true}
indicatorIconButtonProps={{
style: {
color: "var(--iconActiveBackground)",
},
}}
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) =>
<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">
{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>
{welcomeMenuItems.get().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>
);
};
export const Welcome = withInjectables<Dependencies>(
observer(NonInjectedWelcome),
{
getProps: (di) => ({
welcomeMenuItems: di.inject(welcomeMenuItemsInjectable),
welcomeBannerItems: di.inject(welcomeBannerItemsInjectable),
}),
},
);

View File

@ -24,13 +24,14 @@ import "./pod-container-port.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
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 { Notifications } from "../notifications";
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 { Spinner } from "../spinner";
import logger from "../../../common/logger";
interface Props {
pod: Pod;
@ -46,6 +47,7 @@ export class PodContainerPort extends React.Component<Props> {
@observable waiting = false;
@observable forwardPort = 0;
@observable isPortForwarded = false;
@observable isActive = false;
constructor(props: Props) {
super(props);
@ -55,13 +57,14 @@ export class PodContainerPort extends React.Component<Props> {
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => [portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()),
reaction(() => this.props.pod, () => this.checkExistingPortForwarding()),
]);
}
@action
async checkExistingPortForwarding() {
const { pod, port } = this.props;
const portForward: ForwardedPort = {
let portForward: ForwardedPort = {
kind: "pod",
name: pod.getName(),
namespace: pod.getNs(),
@ -69,57 +72,64 @@ export class PodContainerPort extends React.Component<Props> {
forwardPort: this.forwardPort,
};
let activePort: number;
try {
activePort = await getPortForward(portForward) ?? 0;
portForward = await getPortForward(portForward);
} catch (error) {
this.isPortForwarded = false;
this.isActive = false;
return;
}
this.forwardPort = activePort;
this.isPortForwarded = activePort ? true : false;
this.forwardPort = portForward.forwardPort;
this.isPortForwarded = true;
this.isActive = portForward.status === "Active";
}
@action
async portForward() {
const { pod, port } = this.props;
const portForward: ForwardedPort = {
let portForward: ForwardedPort = {
kind: "pod",
name: pod.getName(),
namespace: pod.getNs(),
port: port.containerPort,
forwardPort: this.forwardPort,
protocol: predictProtocol(port.name),
status: "Active",
};
this.waiting = true;
try {
// determine how many port-forwards are already active
const { length } = await getPortForwards();
// determine how many port-forwards already exist
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) {
portForward.forwardPort = this.forwardPort;
if (portForward.status === "Active") {
openPortForward(portForward);
this.isPortForwarded = true;
// if this is the first port-forward show the about notification
if (!length) {
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) {
Notifications.error(`Error occurred starting port-forward, the local port may not be available or the ${portForward.kind} ${portForward.name} may not be reachable`);
this.checkExistingPortForwarding();
logger.error("[POD-CONTAINER-PORT]:", error, portForward);
} finally {
this.checkExistingPortForwarding();
this.waiting = false;
}
}
@action
async stopPortForward() {
const { pod, port } = this.props;
const portForward: ForwardedPort = {
@ -134,11 +144,11 @@ export class PodContainerPort extends React.Component<Props> {
try {
await removePortForward(portForward);
this.isPortForwarded = false;
} catch (error) {
Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`);
this.checkExistingPortForwarding();
} finally {
this.checkExistingPortForwarding();
this.forwardPort = 0;
this.waiting = false;
}
}
@ -148,7 +158,7 @@ export class PodContainerPort extends React.Component<Props> {
const { name, containerPort, protocol } = port;
const text = `${name ? `${name}: ` : ""}${containerPort}/${protocol}`;
const portForwardAction = async () => {
const portForwardAction = action(async () => {
if (this.isPortForwarded) {
await this.stopPortForward();
} else {
@ -161,16 +171,16 @@ export class PodContainerPort extends React.Component<Props> {
protocol: predictProtocol(port.name),
};
PortForwardDialog.open(portForward, { openInBrowser: true });
PortForwardDialog.open(portForward, { openInBrowser: true, onClose: () => this.checkExistingPortForwarding() });
}
};
});
return (
<div className={cssNames("PodContainerPort", { waiting: this.waiting })}>
<span title="Open in a browser" onClick={() => this.portForward()}>
{text}
</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 && (
<Spinner />
)}

View File

@ -19,40 +19,49 @@
* 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 React from "react";
import { broadcastMessage, catalogEntityRunListener } from "../../../common/ipc";
import type { CatalogEntity } from "../../api/catalog-entity";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { CommandOverlay } from "../command-palette";
import commandOverlayInjectable from "../command-palette/command-overlay.injectable";
import { Select } from "../select";
@observer
export class ActivateEntityCommand extends React.Component {
@computed get options() {
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 ..."
/>
);
}
interface Dependencies {
closeCommandOverlay: () => void;
entities: IComputedValue<CatalogEntity[]>;
}
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]),
}),
});

View File

@ -91,6 +91,15 @@ html, body {
overflow: hidden;
}
#terminal-init {
position: absolute;
top: 0;
left: 0;
height: 0;
visibility: hidden;
overflow: hidden;
}
#app {
height: 100%;
min-height: 100%;

View File

@ -21,21 +21,26 @@
import React from "react";
import { observer } from "mobx-react";
import { CommandOverlay } from "../command-palette";
import { Input } from "../input";
import { isUrl } from "../input/input_validators";
import { WeblinkStore } from "../../../common/weblink-store";
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
export class WeblinkAddCommand extends React.Component {
class NonInjectedWeblinkAddCommand extends React.Component<Dependencies> {
@observable url = "";
@observable nameHidden = true;
@observable dirty = false;
constructor(props: {}) {
constructor(props: Dependencies) {
super(props);
makeObservable(this);
}
@ -55,8 +60,7 @@ export class WeblinkAddCommand extends React.Component {
name: name || this.url,
url: this.url,
});
CommandOverlay.close();
this.props.closeCommandOverlay();
}
@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