1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into enhancement/group-app-preferences-by-extension

This commit is contained in:
Alex Andreev 2022-01-10 08:14:01 +03:00
commit 89d90127dd
110 changed files with 1510 additions and 527 deletions

View File

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

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

@ -45,7 +45,6 @@ For more details on accessing Lens state data, please see the [Stores](../stores
### `appMenus` ### `appMenus`
The Main Extension API allows you to customize the UI application menu. The Main Extension API allows you to customize the UI application menu.
Note that this is the only UI feature that the Main Extension API allows you to customize.
The following example demonstrates adding an item to the **Help** menu. The following example demonstrates adding an item to the **Help** menu.
``` typescript ``` typescript
@ -65,7 +64,7 @@ export default class SamplePageMainExtension extends Main.LensExtension {
``` ```
`appMenus` is an array of objects that satisfy the `MenuRegistration` interface. `appMenus` is an array of objects that satisfy the `MenuRegistration` interface.
`MenuRegistration` extends React's `MenuItemConstructorOptions` interface. `MenuRegistration` extends Electron's `MenuItemConstructorOptions` interface.
The properties of the appMenus array objects are defined as follows: The properties of the appMenus array objects are defined as follows:
* `parentId` is the name of the menu where your new menu item will be listed. * `parentId` is the name of the menu where your new menu item will be listed.
@ -96,6 +95,35 @@ export default class SamplePageMainExtension extends Main.LensExtension {
When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`. When the menu item is clicked the `navigate()` method looks for and displays a global page with id `"myGlobalPage"`.
This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)). This page would be defined in your extension's `Renderer.LensExtension` implementation (See [`Renderer.LensExtension`](renderer-extension.md)).
### `trayMenus`
`trayMenus` is an array of `TrayMenuRegistration` objects. Most importantly you can define a `label` and a `click` handler. Other properties are `submenu`, `enabled`, `toolTip`, `id` and `type`.
``` typescript
interface TrayMenuRegistration {
label?: string;
click?: (menuItem: TrayMenuRegistration) => void;
id?: string;
type?: "normal" | "separator" | "submenu"
toolTip?: string;
enabled?: boolean;
submenu?: TrayMenuRegistration[]
}
```
The following example demonstrates how tray menus can be added from extension:
``` typescript
import { Main } from "@k8slens/extensions";
export default class SampleTrayMenuMainExtension extends Main.LensExtension {
trayMenus = [{
label: "menu from the extension",
click: () => { console.log("tray menu clicked!") }
}]
}
```
### `addCatalogSource()` and `removeCatalogSource()` Methods ### `addCatalogSource()` and `removeCatalogSource()` Methods
The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities). The `Main.LensExtension` class also provides the `addCatalogSource()` and `removeCatalogSource()` methods, for managing custom catalog items (or entities).

View File

@ -19,17 +19,18 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { catalogURL } from "../../common/routes"; module.exports = {
import { WelcomeMenuRegistry } from "../../extensions/registries"; "overrides": [
import { navigate } from "../navigation"; {
files: [
export function initWelcomeMenuRegistry() { "**/*.ts",
WelcomeMenuRegistry.getInstance() "**/*.tsx",
.add([ ],
{ rules: {
title: "Browse Clusters in Catalog", "import/no-unresolved": ["error", {
icon: "view_list", ignore: ["@k8slens/extensions"],
click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )), }],
}, },
]); },
} ],
};

View File

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

View File

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

View File

@ -42,9 +42,9 @@ import { Console } from "console";
import { SemVer } from "semver"; import { SemVer } from "semver";
import electron from "electron"; import electron from "electron";
import { stdout, stderr } from "process"; import { stdout, stderr } from "process";
import { ThemeStore } from "../../renderer/theme.store";
import type { ClusterStoreModel } from "../cluster-store"; import type { ClusterStoreModel } from "../cluster-store";
import { AppPaths } from "../app-paths"; import { AppPaths } from "../app-paths";
import { defaultTheme } from "../vars";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
AppPaths.init(); AppPaths.init();
@ -75,7 +75,7 @@ describe("user store tests", () => {
us.httpsProxy = "abcd://defg"; us.httpsProxy = "abcd://defg";
expect(us.httpsProxy).toBe("abcd://defg"); expect(us.httpsProxy).toBe("abcd://defg");
expect(us.colorTheme).toBe(ThemeStore.defaultTheme); expect(us.colorTheme).toBe(defaultTheme);
us.colorTheme = "light"; us.colorTheme = "light";
expect(us.colorTheme).toBe("light"); expect(us.colorTheme).toBe("light");
@ -86,7 +86,7 @@ describe("user store tests", () => {
us.colorTheme = "some other theme"; us.colorTheme = "some other theme";
us.resetTheme(); us.resetTheme();
expect(us.colorTheme).toBe(ThemeStore.defaultTheme); expect(us.colorTheme).toBe(defaultTheme);
}); });
it("correctly calculates if the last seen version is an old release", () => { it("correctly calculates if the last seen version is an old release", () => {

View File

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

View File

@ -20,11 +20,10 @@
*/ */
import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { catalogCategoryRegistry } from "../catalog/catalog-category-registry";
import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategory, CatalogCategorySpec } from "../catalog";
import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { broadcastMessage, requestMain } from "../ipc"; import { broadcastMessage, requestMain } from "../ipc";
import { CatalogCategory, CatalogCategorySpec } from "../catalog";
import { app } from "electron"; import { app } from "electron";
import type { CatalogEntitySpec } from "../catalog/catalog-entity"; import type { CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";

View File

@ -30,7 +30,15 @@ import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils"; import type { Disposer } from "../utils";
import type remote from "@electron/remote"; import type remote from "@electron/remote";
const electronRemote = ipcMain ? null : require("@electron/remote"); const electronRemote = (() => {
if (ipcRenderer) {
try {
return require("@electron/remote");
} catch {}
}
return null;
})();
const subFramesChannel = "ipc:get-sub-frames"; 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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { CustomResourceDefinition } from "../endpoints"; import { CustomResourceDefinition, CustomResourceDefinitionSpec } from "../endpoints";
describe("Crds", () => { describe("Crds", () => {
describe("getVersion", () => { describe("getVersion()", () => {
it("should throw if none of the versions are served", () => { it("should throw if none of the versions are served", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "apiextensions.k8s.io/v1", apiVersion: "apiextensions.k8s.io/v1",
@ -136,7 +136,7 @@ describe("Crds", () => {
expect(crd.getVersion()).toBe("123"); expect(crd.getVersion()).toBe("123");
}); });
it("should get the version name from the version field", () => { it("should get the version name from the version field, ignoring versions on v1beta", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "apiextensions.k8s.io/v1beta1", apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition", kind: "CustomResourceDefinition",
@ -147,7 +147,14 @@ describe("Crds", () => {
}, },
spec: { spec: {
version: "abc", version: "abc",
}, versions: [
{
name: "foobar",
served: true,
storage: true,
},
],
} as CustomResourceDefinitionSpec,
}); });
expect(crd.getVersion()).toBe("abc"); expect(crd.getVersion()).toBe("abc");

View File

@ -164,14 +164,14 @@ describe("KubeObject", () => {
describe("isJsonApiDataList", () => { describe("isJsonApiDataList", () => {
function isAny(val: unknown): val is any { function isAny(val: unknown): val is any {
return !Boolean(void val); return true;
} }
function isNotAny(val: unknown): val is any { function isNotAny(val: unknown): val is any {
return Boolean(void val); return false;
} }
function isBoolean(val: unknown): val is Boolean { function isBoolean(val: unknown): val is boolean {
return typeof val === "boolean"; return typeof val === "boolean";
} }

View File

@ -48,34 +48,36 @@ export interface CRDVersion {
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]; additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
} }
export interface CustomResourceDefinition { export interface CustomResourceDefinitionSpec {
spec: { group: string;
group: string; /**
/** * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
* @deprecated for apiextensions.k8s.io/v1 but used previously */
*/ version?: string;
version?: string; names: {
names: { plural: string;
plural: string; singular: string;
singular: string; kind: string;
kind: string; listKind: string;
listKind: string;
};
scope: "Namespaced" | "Cluster" | string;
/**
* @deprecated for apiextensions.k8s.io/v1 but used previously
*/
validation?: object;
versions?: CRDVersion[];
conversion: {
strategy?: string;
webhook?: any;
};
/**
* @deprecated for apiextensions.k8s.io/v1 but used previously
*/
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
}; };
scope: "Namespaced" | "Cluster";
/**
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/
validation?: object;
versions?: CRDVersion[];
conversion: {
strategy?: string;
webhook?: any;
};
/**
* @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
}
export interface CustomResourceDefinition {
spec: CustomResourceDefinitionSpec;
status: { status: {
conditions: { conditions: {
lastTransitionTime: string; lastTransitionTime: string;
@ -150,27 +152,32 @@ export class CustomResourceDefinition extends KubeObject {
} }
getPreferedVersion(): CRDVersion { getPreferedVersion(): CRDVersion {
// Prefer the modern `versions` over the legacy `version` const { apiVersion } = this;
if (this.spec.versions) {
for (const version of this.spec.versions) {
if (version.storage) {
return version;
}
}
} else if (this.spec.version) {
const { additionalPrinterColumns: apc } = this.spec;
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
return { switch (apiVersion) {
name: this.spec.version, case "apiextensions.k8s.io/v1":
served: true, for (const version of this.spec.versions) {
storage: true, if (version.storage) {
schema: this.spec.validation, return version;
additionalPrinterColumns, }
}; }
break;
case "apiextensions.k8s.io/v1beta1": {
const { additionalPrinterColumns: apc } = this.spec;
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
return {
name: this.spec.version,
served: true,
storage: true,
schema: this.spec.validation,
additionalPrinterColumns,
};
}
} }
throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`); throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
} }
getVersion() { getVersion() {
@ -197,7 +204,7 @@ export class CustomResourceDefinition extends KubeObject {
const columns = this.getPreferedVersion().additionalPrinterColumns ?? []; const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
return columns return columns
.filter(column => column.name != "Age" && (ignorePriority || !column.priority)); .filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority));
} }
getValidation() { getValidation() {

View File

@ -187,7 +187,7 @@ export class Ingress extends KubeObject {
const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort; const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort;
if (rules && rules.length > 0) { if (rules && rules.length > 0) {
if (rules.some(rule => rule.hasOwnProperty("http"))) { if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) {
ports.push(httpPort); ports.push(httpPort);
} }
} else if (servicePort !== undefined) { } else if (servicePort !== undefined) {

View File

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

View File

@ -34,6 +34,9 @@ import type { IKubeWatchEvent } from "./kube-watch-api";
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api"; import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
import { noop } from "../utils"; import { noop } from "../utils";
import type { RequestInit } from "node-fetch"; import type { RequestInit } from "node-fetch";
// BUG: https://github.com/mysticatea/abort-controller/pull/22
// eslint-disable-next-line import/no-named-as-default
import AbortController from "abort-controller"; import AbortController from "abort-controller";
import { Agent, AgentOptions } from "https"; import { Agent, AgentOptions } from "https";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
@ -698,21 +701,16 @@ export class KubeApi<T extends KubeObject> {
} }
protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) { protected modifyWatchEvent(event: IKubeWatchEvent<KubeJsonApiData>) {
if (event.type === "ERROR") {
return;
switch (event.type) {
case "ADDED":
case "DELETED":
case "MODIFIED": {
ensureObjectSelfLink(this, event.object);
const { namespace, resourceVersion } = event.object.metadata;
this.setResourceVersion(namespace, resourceVersion);
this.setResourceVersion("", resourceVersion);
break;
}
} }
ensureObjectSelfLink(this, event.object);
const { namespace, resourceVersion } = event.object.metadata;
this.setResourceVersion(namespace, resourceVersion);
this.setResourceVersion("", resourceVersion);
} }
} }

View File

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

View File

@ -88,7 +88,7 @@ export abstract class LensProtocolRouter {
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]"; public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
constructor(protected dependencies: Dependencies) {} constructor(protected dependencies: Dependencies) {}

View File

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

View File

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

View File

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

View File

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

View File

@ -31,12 +31,13 @@ export interface ReadFileFromTarOpts {
} }
export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise<R> { export function readFileFromTar<R = Buffer>({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise<R> {
return new Promise(async (resolve, reject) => { return new Promise((resolve, reject) => {
const fileChunks: Buffer[] = []; const fileChunks: Buffer[] = [];
await tar.list({ tar.list({
file: tarPath, file: tarPath,
filter: entryPath => path.normalize(entryPath) === filePath, filter: entryPath => path.normalize(entryPath) === filePath,
sync: true,
onentry(entry: FileStat) { onentry(entry: FileStat) {
entry.on("data", chunk => { entry.on("data", chunk => {
fileChunks.push(chunk); fileChunks.push(chunk);

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { array } from "../utils"; import * as array from "../utils/array";
/** /**
* A strict N-tuple of type T * A strict N-tuple of type T

View File

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

View File

@ -18,8 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader"; import { ExtensionLoader } from "./extension-loader";
const extensionLoaderInjectable = getInjectable({ const extensionLoaderInjectable = getInjectable({

View File

@ -263,10 +263,7 @@ export class ExtensionLoader {
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
registries.CommandRegistry.getInstance().add(extension.commands), registries.CommandRegistry.getInstance().add(extension.commands),
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners),
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
]; ];
this.events.on("remove", (removedExtension: LensRendererExtension) => { this.events.on("remove", (removedExtension: LensRendererExtension) => {

View File

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

View File

@ -26,7 +26,10 @@ import type { CatalogEntity } from "../common/catalog";
import type { Disposer } from "../common/utils"; import type { Disposer } from "../common/utils";
import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry"; import { catalogEntityRegistry, EntityFilter } from "../renderer/api/catalog-entity-registry";
import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry"; import { catalogCategoryRegistry, CategoryFilter } from "../renderer/api/catalog-category-registry";
import type { TopBarRegistration } from "../renderer/components/layout/top-bar/top-bar-registration";
import type { KubernetesCluster } from "../common/catalog-entities"; import type { KubernetesCluster } from "../common/catalog-entities";
import type { WelcomeMenuRegistration } from "../renderer/components/+welcome/welcome-menu-items/welcome-menu-registration";
import type { WelcomeBannerRegistration } from "../renderer/components/+welcome/welcome-banner-items/welcome-banner-registration";
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
globalPages: registries.PageRegistration[] = []; globalPages: registries.PageRegistration[] = [];
@ -40,10 +43,10 @@ export class LensRendererExtension extends LensExtension {
kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = []; kubeObjectMenuItems: registries.KubeObjectMenuRegistration[] = [];
kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = []; kubeWorkloadsOverviewItems: registries.WorkloadsOverviewDetailRegistration[] = [];
commands: registries.CommandRegistration[] = []; commands: registries.CommandRegistration[] = [];
welcomeMenus: registries.WelcomeMenuRegistration[] = []; welcomeMenus: WelcomeMenuRegistration[] = [];
welcomeBanners: registries.WelcomeBannerRegistration[] = []; welcomeBanners: WelcomeBannerRegistration[] = [];
catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = []; catalogEntityDetailItems: registries.CatalogEntityDetailRegistration<CatalogEntity>[] = [];
topBarItems: registries.TopBarRegistration[] = []; topBarItems: TopBarRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P) { async navigate<P extends object>(pageId?: string, params?: P) {
const { navigate } = await import("../renderer/navigation"); const { navigate } = await import("../renderer/navigation");

View File

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

View File

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

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

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

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import * as tempy from "tempy"; import tempy from "tempy";
import fse from "fs-extra"; import fse from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { promiseExecFile } from "../../common/utils/promise-exec"; import { promiseExecFile } from "../../common/utils/promise-exec";

View File

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

View File

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

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`); const localStorageFilePath = path.resolve(AppPaths.get("userData"), "lens-local-storage", `${cluster.id}.json`);
await remove(localStorageFilePath); await remove(localStorageFilePath);
} catch {} } catch {
// ignore error
}
}); });
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => { ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {

View File

@ -357,10 +357,10 @@ export class Kubectl {
bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`; bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`;
bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\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//,localhost,/,}\"\n`; bashScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`;
bashScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`; bashScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`;
bashScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`; bashScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`;
bashScript += "export NO_PROXY\n"; bashScript += "export NO_PROXY\n";
bashScript += "unset tempkubeconfig\n"; bashScript += "unset tempkubeconfig\n";
await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }); await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 });
@ -378,18 +378,18 @@ export class Kubectl {
zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n"; zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n";
// voodoo to replace any previous occurrences of kubectl path in the PATH // voodoo to replace any previous occurrences of kubectl path in the PATH
zshScript += `kubectlpath=\"${kubectlPath}"\n`; zshScript += `kubectlpath="${kubectlPath}"\n`;
zshScript += `helmpath=\"${helmPath}"\n`; zshScript += `helmpath="${helmPath}"\n`;
zshScript += "p=\":$kubectlpath:\"\n"; zshScript += "p=\":$kubectlpath:\"\n";
zshScript += "d=\":$PATH:\"\n"; zshScript += "d=\":$PATH:\"\n";
zshScript += `d=\${d//$p/:}\n`; zshScript += `d=\${d//$p/:}\n`;
zshScript += `d=\${d/#:/}\n`; zshScript += `d=\${d/#:/}\n`;
zshScript += `export PATH=\"$helmpath:$kubectlpath:\${d/%:/}\"\n`; zshScript += `export PATH="$helmpath:$kubectlpath:\${d/%:/}"\n`;
zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\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//,localhost,/,}\"\n`; zshScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`;
zshScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`; zshScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`;
zshScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`; zshScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`;
zshScript += "export NO_PROXY\n"; zshScript += "export NO_PROXY\n";
zshScript += "unset tempkubeconfig\n"; zshScript += "unset tempkubeconfig\n";
zshScript += "unset OLD_ZDOTDIR\n"; zshScript += "unset OLD_ZDOTDIR\n";

View File

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

View File

@ -38,7 +38,7 @@ export class PrometheusOperator extends PrometheusProvider {
case "cluster": case "cluster":
switch (queryName) { switch (queryName) {
case "memoryUsage": case "memoryUsage":
return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes{node=~"${opts.nodes}"}`); return `sum(node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes))`.replace(/_bytes/g, `_bytes * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"}`);
case "workloadMemoryUsage": case "workloadMemoryUsage":
return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`; return `sum(container_memory_working_set_bytes{container!="", instance=~"${opts.nodes}"}) by (component)`;
case "memoryRequests": case "memoryRequests":
@ -50,7 +50,7 @@ export class PrometheusOperator extends PrometheusProvider {
case "memoryAllocatableCapacity": case "memoryAllocatableCapacity":
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`; return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="memory"})`;
case "cpuUsage": case "cpuUsage":
return `sum(rate(node_cpu_seconds_total{node=~"${opts.nodes}", mode=~"user|system"}[${this.rateAccuracy}]))`; return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])* on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
case "cpuRequests": case "cpuRequests":
return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`; return `sum(kube_pod_container_resource_requests{node=~"${opts.nodes}", resource="cpu"})`;
case "cpuLimits": case "cpuLimits":
@ -66,31 +66,31 @@ export class PrometheusOperator extends PrometheusProvider {
case "podAllocatableCapacity": case "podAllocatableCapacity":
return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`; return `sum(kube_node_status_allocatable{node=~"${opts.nodes}", resource="pods"})`;
case "fsSize": case "fsSize":
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
case "fsUsage": case "fsUsage":
return `sum(node_filesystem_size_bytes{node=~"${opts.nodes}", mountpoint="/"} - node_filesystem_avail_bytes{node=~"${opts.nodes}", mountpoint="/"}) by (node)`; return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"} - node_filesystem_avail_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info{node=~"${opts.nodes}"})`;
} }
break; break;
case "nodes": case "nodes":
switch (queryName) { switch (queryName) {
case "memoryUsage": case "memoryUsage":
return `sum (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) by (node)`; return `sum((node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
case "workloadMemoryUsage": case "workloadMemoryUsage":
return `sum(container_memory_working_set_bytes{container!=""}) by (node)`; return `sum(container_memory_working_set_bytes{container!="POD", container!=""}) by (node)`;
case "memoryCapacity": case "memoryCapacity":
return `sum(kube_node_status_capacity{resource="memory"}) by (node)`; return `sum(kube_node_status_capacity{resource="memory"}) by (node)`;
case "memoryAllocatableCapacity": case "memoryAllocatableCapacity":
return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`; return `sum(kube_node_status_allocatable{resource="memory"}) by (node)`;
case "cpuUsage": case "cpuUsage":
return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}])) by(node)`; return `sum(rate(node_cpu_seconds_total{mode=~"user|system"}[${this.rateAccuracy}]) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
case "cpuCapacity": case "cpuCapacity":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
case "cpuAllocatableCapacity": case "cpuAllocatableCapacity":
return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`; return `sum(kube_node_status_allocatable{resource="cpu"}) by (node)`;
case "fsSize": case "fsSize":
return `sum(node_filesystem_size_bytes{mountpoint="/"}) by (node)`; return `sum(node_filesystem_size_bytes{mountpoint="/"} * on (pod,namespace) group_left(node) kube_pod_info) by (node)`;
case "fsUsage": case "fsUsage":
return `sum(node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) by (node)`; return `sum((node_filesystem_size_bytes{mountpoint="/"} - node_filesystem_avail_bytes{mountpoint="/"}) * on (pod, namespace) group_left(node) kube_pod_info) by (node)`;
} }
break; break;
case "pods": case "pods":

View File

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

View File

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

View File

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

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.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
const trayItemsInjectable = getInjectable({
lifecycle: lifecycleEnum.singleton,
instantiate: (di) => {
const extensions = di.inject(mainExtensionsInjectable);
return computed(() =>
extensions.get().flatMap(extension => extension.trayMenus));
},
});
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

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

View File

@ -33,7 +33,7 @@ export default {
const contextName = value[0]; const contextName = value[0];
// Looping all the keys gives out the store internal stuff too... // Looping all the keys gives out the store internal stuff too...
if (contextName === "__internal__" || value[1].hasOwnProperty("kubeConfig")) continue; if (contextName === "__internal__" || Object.prototype.hasOwnProperty.call(value[1], "kubeConfig")) continue;
store.set(contextName, { kubeConfig: value[1] }); store.set(contextName, { kubeConfig: value[1] });
} }
}, },

View File

@ -34,7 +34,7 @@ export default {
if (!cluster.kubeConfig) continue; if (!cluster.kubeConfig) continue;
const config = yaml.load(cluster.kubeConfig); const config = yaml.load(cluster.kubeConfig);
if (!config || typeof config !== "object" || !config.hasOwnProperty("users")) { if (!config || typeof config !== "object" || !Object.prototype.hasOwnProperty.call(config, "users")) {
continue; continue;
} }

View File

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

View File

@ -28,14 +28,21 @@ import { ClusterStore } from "../../common/cluster-store";
import { Disposer, iter } from "../utils"; import { Disposer, iter } from "../utils";
import { once } from "lodash"; import { once } from "lodash";
import logger from "../../common/logger"; import logger from "../../common/logger";
import { catalogEntityRunContext } from "./catalog-entity";
import { CatalogRunEvent } from "../../common/catalog/catalog-run-event"; import { CatalogRunEvent } from "../../common/catalog/catalog-run-event";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import { CatalogIpcEvents } from "../../common/ipc/catalog"; import { CatalogIpcEvents } from "../../common/ipc/catalog";
import { navigate } from "../navigation";
export type EntityFilter = (entity: CatalogEntity) => any; export type EntityFilter = (entity: CatalogEntity) => any;
export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>; export type CatalogEntityOnBeforeRun = (event: CatalogRunEvent) => void | Promise<void>;
export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url),
setCommandPaletteContext: (entity?: CatalogEntity) => {
catalogEntityRegistry.activeEntity = entity;
},
};
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
@observable protected activeEntityId: string | undefined = undefined; @observable protected activeEntityId: string | undefined = undefined;
protected _entities = observable.map<string, CatalogEntity>([], { deep: true }); protected _entities = observable.map<string, CatalogEntity>([], { deep: true });

View File

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

View File

@ -114,9 +114,6 @@ export async function bootstrap(comp: () => Promise<AppComponent>, di: Dependenc
logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`);
initializers.initKubeObjectDetailRegistry(); initializers.initKubeObjectDetailRegistry();
logger.info(`${logPrefix} initializing WelcomeMenuRegistry`);
initializers.initWelcomeMenuRegistry();
logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`); logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`);
initializers.initWorkloadsOverviewDetailRegistry(); initializers.initWorkloadsOverviewDetailRegistry();

View File

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

View File

@ -18,8 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { unpackExtension } from "./unpack-extension"; import { unpackExtension } from "./unpack-extension";
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";

View File

@ -47,8 +47,8 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) =
} catch (error) { } catch (error) {
Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>); Notifications.error(<p>Failed to get configured registry from <code>.npmrc</code>. Falling back to default registry</p>);
console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error); console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error);
// fallthrough
} }
// fallthrough
} }
default: default:
case ExtensionRegistryLocation.DEFAULT: case ExtensionRegistryLocation.DEFAULT:

View File

@ -21,11 +21,10 @@
import React from "react"; import React from "react";
import { boundMethod, cssNames } from "../../utils"; import { boundMethod, cssNames } from "../../utils";
import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward"; import { openPortForward, PortForwardItem, removePortForward, PortForwardDialog } from "../../port-forward";
import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { PortForwardDialog } from "../../port-forward";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
interface Props extends MenuActionsProps { interface Props extends MenuActionsProps {

View File

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

View File

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

View File

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

View File

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

View File

@ -20,45 +20,55 @@
*/ */
import React from "react"; import React from "react";
import { render, screen } from "@testing-library/react"; import { screen } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { Welcome } from "../welcome"; import { defaultWidth, Welcome } from "../welcome";
import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries"; import { computed } from "mobx";
import { defaultWidth } from "../welcome"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { DiRender } from "../../test-utils/renderFor";
import { renderFor } from "../../test-utils/renderFor";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension";
import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration";
jest.mock( jest.mock("electron", () => ({
"electron", ipcRenderer: {
() => ({ on: jest.fn(),
ipcRenderer: { },
on: jest.fn(), app: {
}, getPath: () => "tmp",
app: { },
getPath: () => "tmp", }));
},
}),
);
describe("<Welcome/>", () => { describe("<Welcome/>", () => {
beforeEach(() => { let render: DiRender;
TopBarRegistry.createInstance(); let di: ConfigurableDependencyInjectionContainer;
WelcomeMenuRegistry.createInstance(); let welcomeBannersStub: WelcomeBannerRegistration[];
WelcomeBannerRegistry.createInstance();
});
afterEach(() => { beforeEach(() => {
TopBarRegistry.resetInstance(); di = getDiForUnitTesting();
WelcomeMenuRegistry.resetInstance();
WelcomeBannerRegistry.resetInstance(); render = renderFor(di);
welcomeBannersStub = [];
di.override(rendererExtensionsInjectable, () =>
computed(() => [
new TestExtension({
id: "some-id",
welcomeBanners: welcomeBannersStub,
}),
]),
);
}); });
it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => { it("renders <Banner /> registered in WelcomeBannerRegistry and hide logo", async () => {
const testId = "testId"; const testId = "testId";
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ welcomeBannersStub.push({
{ Banner: () => <div data-testid={testId} />,
Banner: () => <div data-testid={testId} />, });
},
]);
const { container } = render(<Welcome />); const { container } = render(<Welcome />);
@ -67,16 +77,15 @@ describe("<Welcome/>", () => {
}); });
it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => { it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => {
WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ welcomeBannersStub.push({
{ width: 100,
width: 100, Banner: () => <div />,
Banner: () => <div />, });
},
{ welcomeBannersStub.push({
width: 800, width: 800,
Banner: () => <div />, Banner: () => <div />,
}, });
]);
render(<Welcome />); render(<Welcome />);
@ -92,3 +101,25 @@ describe("<Welcome/>", () => {
}); });
}); });
}); });
class TestExtension extends LensRendererExtension {
constructor({
id,
welcomeBanners,
}: {
id: string;
welcomeBanners: WelcomeBannerRegistration[];
}) {
super({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: id, version: "some-version" },
manifestPath: "irrelevant",
});
this.welcomeBanners = welcomeBanners;
}
}

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable";
import { computed } from "mobx";
const welcomeBannerItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
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. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { BaseRegistry } from "./base-registry";
/** /**
* WelcomeBannerRegistration is for an extension to register * WelcomeBannerRegistration is for an extension to register
* Provide a Banner component to be renderered in the welcome screen. * Provide a Banner component to be renderered in the welcome screen.
@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration {
*/ */
width?: number width?: number
} }
export class WelcomeBannerRegistry extends BaseRegistry<WelcomeBannerRegistration> { }

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

View File

@ -22,78 +22,129 @@
import "./welcome.scss"; import "./welcome.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { IComputedValue } from "mobx";
import Carousel from "react-material-ui-carousel"; import Carousel from "react-material-ui-carousel";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { productName, slackUrl } from "../../../common/vars"; import { productName, slackUrl } from "../../../common/vars";
import { WelcomeMenuRegistry } from "../../../extensions/registries"; import { withInjectables } from "@ogre-tools/injectable-react";
import { WelcomeBannerRegistry } from "../../../extensions/registries"; import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable";
import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration";
import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable";
import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration";
export const defaultWidth = 320; export const defaultWidth = 320;
@observer interface Dependencies {
export class Welcome extends React.Component { welcomeMenuItems: IComputedValue<WelcomeMenuRegistration[]>
render() { welcomeBannerItems: IComputedValue<WelcomeBannerRegistration[]>
const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); }
// if there is banner with specified width, use it to calculate the width of the container const NonInjectedWelcome: React.FC<Dependencies> = ({ welcomeMenuItems, welcomeBannerItems }) => {
const maxWidth = welcomeBanner.reduce((acc, curr) => { const welcomeBanners = welcomeBannerItems.get();
const currWidth = curr.width ?? 0;
if (acc > currWidth) { // if there is banner with specified width, use it to calculate the width of the container
return acc; const maxWidth = welcomeBanners.reduce((acc, curr) => {
} const currWidth = curr.width ?? 0;
return currWidth; if (acc > currWidth) {
}, defaultWidth); return acc;
}
return ( return currWidth;
<div className="flex justify-center Welcome align-center"> }, defaultWidth);
<div style={{ width: `${maxWidth}px` }} data-testid="welcome-banner-container">
{welcomeBanner.length > 0 ? ( return (
<Carousel <div className="flex justify-center Welcome align-center">
stopAutoPlayOnHover={true} <div
indicators={welcomeBanner.length > 1} style={{ width: `${maxWidth}px` }}
autoPlay={true} data-testid="welcome-banner-container"
navButtonsAlwaysInvisible={true} >
indicatorIconButtonProps={{ {welcomeBanners.length > 0 ? (
style: { <Carousel
color: "var(--iconActiveBackground)", stopAutoPlayOnHover={true}
}, indicators={welcomeBanners.length > 1}
}} autoPlay={true}
activeIndicatorIconButtonProps={{ navButtonsAlwaysInvisible={true}
style: { indicatorIconButtonProps={{
color: "var(--iconActiveColor)", style: {
}, color: "var(--iconActiveBackground)",
}} },
interval={8000} }}
activeIndicatorIconButtonProps={{
style: {
color: "var(--iconActiveColor)",
},
}}
interval={8000}
>
{welcomeBanners.map((item, index) => (
<item.Banner key={index} />
))}
</Carousel>
) : (
<Icon svg="logo-lens" className="logo" />
)}
<div className="flex justify-center">
<div
style={{ width: `${defaultWidth}px` }}
data-testid="welcome-text-container"
>
<h2>Welcome to {productName} 5!</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our{" "}
<a
href={slackUrl}
target="_blank"
rel="noreferrer"
className="link"
>
Lens Community slack channel
</a>
.
</p>
<ul
className="block"
style={{ width: `${defaultWidth}px` }}
data-testid="welcome-menu-container"
> >
{welcomeBanner.map((item, index) => {welcomeMenuItems.get().map((item, index) => (
<item.Banner key={index} />, <li
)} key={index}
</Carousel> className="flex grid-12"
) : <Icon svg="logo-lens" className="logo" />} onClick={() => item.click()}
>
<div className="flex justify-center"> <Icon material={item.icon} className="box col-1" />
<div style={{ width: `${defaultWidth}px` }} data-testid="welcome-text-container"> <a className="box col-10">
<h2>Welcome to {productName} 5!</h2> {typeof item.title === "string"
? item.title
<p> : item.title()}
To get you started we have auto-detected your clusters in your kubeconfig file and added them to the catalog, your centralized view for managing all your cloud-native resources. </a>
<br /><br /> <Icon material="navigate_next" className="box col-1" />
If you have any questions or feedback, please join our <a href={slackUrl} target="_blank" rel="noreferrer" className="link">Lens Community slack channel</a>. </li>
</p> ))}
</ul>
<ul className="block" style={{ width: `${defaultWidth}px` }} data-testid="welcome-menu-container">
{WelcomeMenuRegistry.getInstance().getItems().map((item, index) => (
<li key={index} className="flex grid-12" onClick={() => item.click()}>
<Icon material={item.icon} className="box col-1" /> <a className="box col-10">{typeof item.title === "string" ? item.title : item.title()}</a> <Icon material="navigate_next" className="box col-1" />
</li>
))}
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
); </div>
} );
} };
export const Welcome = withInjectables<Dependencies>(
observer(NonInjectedWelcome),
{
getProps: (di) => ({
welcomeMenuItems: di.inject(welcomeMenuItemsInjectable),
welcomeBannerItems: di.inject(welcomeBannerItemsInjectable),
}),
},
);

View File

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

View File

@ -39,8 +39,8 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog";
import { reaction } from "mobx"; import { reaction } from "mobx";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync"; import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync";
import { TopBar } from "../layout/topbar";
import { catalogURL, getPreviousTabUrl } from "../../../common/routes"; import { catalogURL, getPreviousTabUrl } from "../../../common/routes";
import { TopBar } from "../layout/top-bar/top-bar";
@observer @observer
export class ClusterManager extends React.Component { export class ClusterManager extends React.Component {

View File

@ -23,7 +23,7 @@ import { KubeConfig } from "@kubernetes/client-node";
import { fireEvent, render } from "@testing-library/react"; import { fireEvent, render } from "@testing-library/react";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import React from "react"; import React from "react";
import selectEvent from "react-select-event"; import * as selectEvent from "react-select-event";
import { Cluster } from "../../../../main/cluster"; import { Cluster } from "../../../../main/cluster";
import { DeleteClusterDialog } from "../delete-cluster-dialog"; import { DeleteClusterDialog } from "../delete-cluster-dialog";

View File

@ -22,7 +22,7 @@
import React from "react"; import React from "react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { render } from "@testing-library/react"; import { render } from "@testing-library/react";
import selectEvent from "react-select-event"; import * as selectEvent from "react-select-event";
import { Pod } from "../../../../common/k8s-api/endpoints"; import { Pod } from "../../../../common/k8s-api/endpoints";
import { LogResourceSelector } from "../log-resource-selector"; import { LogResourceSelector } from "../log-resource-selector";

View File

@ -34,17 +34,9 @@ import { clipboard } from "electron";
import logger from "../../../common/logger"; import logger from "../../../common/logger";
export class Terminal { export class Terminal {
public static readonly spawningPool = (() => { public static get spawningPool() {
// terminal element must be in DOM before attaching via xterm.open(elem) return document.getElementById("terminal-init");
// https://xtermjs.org/docs/api/terminal/classes/terminal/#open }
const pool = document.createElement("div");
pool.className = "terminal-init";
pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden";
document.body.appendChild(pool);
return pool;
})();
static async preloadFonts() { static async preloadFonts() {
const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires

View File

@ -133,7 +133,8 @@ export class FilePicker extends React.Component<Props> {
switch (onOverSizeLimit) { switch (onOverSizeLimit) {
case OverSizeLimitStyle.FILTER: case OverSizeLimitStyle.FILTER:
return files.filter(file => file.size <= maxSize ); return files.filter(file => file.size <= maxSize );
case OverSizeLimitStyle.REJECT:
case OverSizeLimitStyle.REJECT: {
const firstFileToLarge = files.find(file => file.size > maxSize); const firstFileToLarge = files.find(file => file.size > maxSize);
if (firstFileToLarge) { if (firstFileToLarge) {
@ -141,6 +142,7 @@ export class FilePicker extends React.Component<Props> {
} }
return files; return files;
}
} }
} }
@ -156,7 +158,9 @@ export class FilePicker extends React.Component<Props> {
switch (onOverTotalSizeLimit) { switch (onOverTotalSizeLimit) {
case OverTotalSizeLimitStyle.FILTER_LARGEST: case OverTotalSizeLimitStyle.FILTER_LARGEST:
files = _.orderBy(files, ["size"]); files = _.orderBy(files, ["size"]);
case OverTotalSizeLimitStyle.FILTER_LAST:
// fallthrough
case OverTotalSizeLimitStyle.FILTER_LAST: {
let newTotalSize = totalSize; let newTotalSize = totalSize;
for (;files.length > 0;) { for (;files.length > 0;) {
@ -168,6 +172,7 @@ export class FilePicker extends React.Component<Props> {
} }
return files; return files;
}
case OverTotalSizeLimitStyle.REJECT: case OverTotalSizeLimitStyle.REJECT:
throw `Total file size to upload is too large. Expected at most ${maxTotalSize}. Found ${totalSize}.`; throw `Total file size to upload is too large. Expected at most ${maxTotalSize}. Found ${totalSize}.`;
} }

View File

@ -73,6 +73,7 @@ export class Icon extends React.PureComponent<IconProps> {
switch (evt.nativeEvent.code) { switch (evt.nativeEvent.code) {
case "Space": case "Space":
// fallthrough
case "Enter": { case "Enter": {
// eslint-disable-next-line react/no-find-dom-node // eslint-disable-next-line react/no-find-dom-node
const icon = findDOMNode(this) as HTMLElement; const icon = findDOMNode(this) as HTMLElement;

View File

@ -39,7 +39,7 @@ export const isRequired: InputValidator = {
export const isEmail: InputValidator = { export const isEmail: InputValidator = {
condition: ({ type }) => type === "email", condition: ({ type }) => type === "email",
message: () => `Wrong email format`, message: () => `Wrong email format`,
validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/), validate: value => !!value.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/),
}; };
export const isNumber: InputValidator = { export const isNumber: InputValidator = {

View File

@ -31,7 +31,6 @@ import { apiManager } from "../../../common/k8s-api/api-manager";
import { crdStore } from "../+custom-resources/crd.store"; import { crdStore } from "../+custom-resources/crd.store";
import { KubeObjectMenu } from "../kube-object-menu"; import { KubeObjectMenu } from "../kube-object-menu";
import { KubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { KubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import logger from "../../../main/logger";
import { CrdResourceDetails } from "../+custom-resources"; import { CrdResourceDetails } from "../+custom-resources";
import { KubeObjectMeta } from "../kube-object-meta"; import { KubeObjectMeta } from "../kube-object-meta";
import { hideDetails, kubeDetailsUrlParam } from "../kube-detail-params"; import { hideDetails, kubeDetailsUrlParam } from "../kube-detail-params";
@ -62,7 +61,7 @@ export class KubeObjectDetails extends React.Component {
.getStore(this.path) .getStore(this.path)
?.getByPath(this.path); ?.getByPath(this.path);
} catch (error) { } catch (error) {
logger.error(`[KUBE-OBJECT-DETAILS]: failed to get store or object: ${error}`, { path: this.path }); console.error(`[KUBE-OBJECT-DETAILS]: failed to get store or object: ${error}`, { path: this.path });
return undefined; return undefined;
} }

View File

@ -21,7 +21,7 @@
import { hideDetails } from "../../kube-detail-params"; import { hideDetails } from "../../kube-detail-params";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
export const hideDetailsInjectable = getInjectable({ const hideDetailsInjectable = getInjectable({
instantiate: () => hideDetails, instantiate: () => hideDetails,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}); });

View File

@ -0,0 +1,52 @@
/**
* 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.
*/
.closeButton {
width: 35px;
height: 35px;
display: grid;
place-items: center;
cursor: pointer;
border: 2px solid var(--textColorDimmed);
border-radius: 50%;
&:hover {
background-color: #72767d25;
}
&:active {
transform: translateY(1px);
}
}
.icon {
color: var(--textColorAccent);
opacity: 0.6;
}
.esc {
text-align: center;
margin-top: var(--margin);
font-weight: bold;
user-select: none;
color: var(--textColorDimmed);
pointer-events: none;
}

View File

@ -0,0 +1,41 @@
/**
* 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 styles from "./close-button.module.scss";
import React, { HTMLAttributes } from "react";
import { Icon } from "../icon";
interface Props extends HTMLAttributes<HTMLDivElement> {
}
export function CloseButton(props: Props) {
return (
<div {...props}>
<div className={styles.closeButton} role="button" aria-label="Close">
<Icon material="close" className={styles.icon}/>
</div>
<div className={styles.esc} aria-hidden="true">
ESC
</div>
</div>
);
}

View File

@ -129,41 +129,7 @@
} }
> .toolsRegion { > .toolsRegion {
.fixedTools { width: 45px;
position: fixed;
top: 60px;
.closeBtn {
width: 35px;
height: 35px;
display: grid;
place-items: center;
border: 2px solid var(--textColorDimmed);
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: #72767d4d;
}
&:active {
transform: translateY(1px);
}
.Icon {
color: var(--textColorTertiary);
}
}
.esc {
text-align: center;
margin-top: 4px;
font-weight: 600;
font-size: 14px;
color: var(--textColorDimmed);
pointer-events: none;
}
}
} }
} }

View File

@ -25,8 +25,8 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { navigation } from "../../navigation"; import { navigation } from "../../navigation";
import { Icon } from "../icon";
import { catalogURL } from "../../../common/routes"; import { catalogURL } from "../../../common/routes";
import { CloseButton } from "./close-button";
export interface SettingLayoutProps extends React.DOMAttributes<any> { export interface SettingLayoutProps extends React.DOMAttributes<any> {
className?: IClassName; className?: IClassName;
@ -104,13 +104,8 @@ export class SettingLayout extends React.Component<SettingLayoutProps> {
<div className="toolsRegion"> <div className="toolsRegion">
{ {
this.props.provideBackButtonNavigation && ( this.props.provideBackButtonNavigation && (
<div className="fixedTools"> <div className="fixed top-[60px]">
<div className="closeBtn" role="button" aria-label="Close" onClick={back}> <CloseButton onClick={back}/>
<Icon material="close" />
</div>
<div className="esc" aria-hidden="true">
ESC
</div>
</div> </div>
) )
} }

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable";
const topBarItemsInjectable = getInjectable({
instantiate: (di) => {
const extensions = di.inject(rendererExtensionsInjectable);
return computed(() =>
extensions.get().flatMap((extension) => extension.topBarItems),
);
},
lifecycle: lifecycleEnum.singleton,
});
export default topBarItemsInjectable;

View File

@ -18,10 +18,6 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type React from "react";
import { BaseRegistry } from "./base-registry";
interface TopBarComponents { interface TopBarComponents {
Item: React.ComponentType; Item: React.ComponentType;
} }
@ -29,6 +25,3 @@ interface TopBarComponents {
export interface TopBarRegistration { export interface TopBarRegistration {
components: TopBarComponents; components: TopBarComponents;
} }
export class TopBarRegistry extends BaseRegistry<TopBarRegistration> {
}

View File

@ -20,23 +20,29 @@
*/ */
import React from "react"; import React from "react";
import { render, fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "../topbar"; import { TopBar } from "./top-bar";
import { TopBarRegistry } from "../../../../extensions/registries";
import { IpcMainWindowEvents } from "../../../../main/window-manager"; import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { broadcastMessage } from "../../../../common/ipc"; import { broadcastMessage } from "../../../../common/ipc";
import * as vars from "../../../../common/vars"; import * as vars from "../../../../common/vars";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { DiRender, renderFor } from "../../test-utils/renderFor";
const mockConfig = vars as { isWindows: boolean, isLinux: boolean }; const mockConfig = vars as { isWindows: boolean; isLinux: boolean };
jest.mock("../../../../common/ipc"); jest.mock("../../../../common/ipc");
jest.mock("../../../../common/vars", () => { jest.mock("../../../../common/vars", () => {
const SemVer = require("semver").SemVer;
const versionStub = new SemVer("1.0.0");
return { return {
__esModule: true, __esModule: true,
isWindows: null, isWindows: null,
isLinux: null, isLinux: null,
appSemVer: versionStub,
}; };
}); });
@ -57,20 +63,20 @@ jest.mock("@electron/remote", () => {
}; };
}); });
describe("<Tobar/> in Windows and Linux", () => { describe("<TopBar/> in Windows and Linux", () => {
beforeEach(() => { let render: DiRender;
TopBarRegistry.createInstance();
});
afterEach(() => { beforeEach(() => {
TopBarRegistry.resetInstance(); const di = getDiForUnitTesting();
render = renderFor(di);
}); });
it("shows window controls on Windows", () => { it("shows window controls on Windows", () => {
mockConfig.isWindows = true; mockConfig.isWindows = true;
mockConfig.isLinux = false; mockConfig.isLinux = false;
const { getByTestId } = render(<TopBar/>); const { getByTestId } = render(<TopBar />);
expect(getByTestId("window-menu")).toBeInTheDocument(); expect(getByTestId("window-menu")).toBeInTheDocument();
expect(getByTestId("window-minimize")).toBeInTheDocument(); expect(getByTestId("window-minimize")).toBeInTheDocument();
@ -82,7 +88,7 @@ describe("<Tobar/> in Windows and Linux", () => {
mockConfig.isWindows = false; mockConfig.isWindows = false;
mockConfig.isLinux = true; mockConfig.isLinux = true;
const { getByTestId } = render(<TopBar/>); const { getByTestId } = render(<TopBar />);
expect(getByTestId("window-menu")).toBeInTheDocument(); expect(getByTestId("window-menu")).toBeInTheDocument();
expect(getByTestId("window-minimize")).toBeInTheDocument(); expect(getByTestId("window-minimize")).toBeInTheDocument();
@ -93,7 +99,7 @@ describe("<Tobar/> in Windows and Linux", () => {
it("triggers ipc events on click", () => { it("triggers ipc events on click", () => {
mockConfig.isWindows = true; mockConfig.isWindows = true;
const { getByTestId } = render(<TopBar/>); const { getByTestId } = render(<TopBar />);
const menu = getByTestId("window-menu"); const menu = getByTestId("window-menu");
const minimize = getByTestId("window-minimize"); const minimize = getByTestId("window-minimize");

View File

@ -20,14 +20,23 @@
*/ */
import React from "react"; import React from "react";
import { render, fireEvent } from "@testing-library/react"; import { fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect"; import "@testing-library/jest-dom/extend-expect";
import { TopBar } from "../topbar"; import { TopBar } from "./top-bar";
import { TopBarRegistry } from "../../../../extensions/registries"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import { DiRender, renderFor } from "../../test-utils/renderFor";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { computed } from "mobx";
jest.mock("../../../../common/vars", () => { jest.mock("../../../../common/vars", () => {
const SemVer = require("semver").SemVer;
const versionStub = new SemVer("1.0.0");
return { return {
isMac: true, isMac: true,
appSemVer: versionStub,
}; };
}); });
@ -76,12 +85,13 @@ jest.mock("@electron/remote", () => {
}); });
describe("<TopBar/>", () => { describe("<TopBar/>", () => {
beforeEach(() => { let di: ConfigurableDependencyInjectionContainer;
TopBarRegistry.createInstance(); let render: DiRender;
});
afterEach(() => { beforeEach(() => {
TopBarRegistry.resetInstance(); di = getDiForUnitTesting();
render = renderFor(di);
}); });
it("renders w/o errors", () => { it("renders w/o errors", () => {
@ -129,13 +139,13 @@ describe("<TopBar/>", () => {
const testId = "testId"; const testId = "testId";
const text = "an item"; const text = "an item";
TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ di.override(topBarItemsInjectable, () => computed(() => [
{ {
components: { components: {
Item: () => <span data-testid={testId}>{text}</span>, Item: () => <span data-testid={testId}>{text}</span>,
}, },
}, },
]); ]));
const { getByTestId } = render(<TopBar/>); const { getByTestId } = render(<TopBar/>);

View File

@ -19,22 +19,28 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import styles from "./topbar.module.scss"; import styles from "./top-bar.module.scss";
import React, { useEffect, useMemo, useRef } from "react"; import React, { useEffect, useMemo, useRef } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { TopBarRegistry } from "../../../extensions/registries"; import type { IComputedValue } from "mobx";
import { Icon } from "../icon"; import { Icon } from "../../icon";
import { webContents, getCurrentWindow } from "@electron/remote"; import { webContents, getCurrentWindow } from "@electron/remote";
import { observable } from "mobx"; import { observable } from "mobx";
import { broadcastMessage, ipcRendererOn } from "../../../common/ipc"; import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc";
import { watchHistoryState } from "../../remote-helpers/history-updater"; import { watchHistoryState } from "../../../remote-helpers/history-updater";
import { isActiveRoute, navigate } from "../../navigation"; import { isActiveRoute, navigate } from "../../../navigation";
import { catalogRoute, catalogURL } from "../../../common/routes"; import { catalogRoute, catalogURL } from "../../../../common/routes";
import { IpcMainWindowEvents } from "../../../main/window-manager"; import { IpcMainWindowEvents } from "../../../../main/window-manager";
import { isLinux, isWindows } from "../../../common/vars"; import { isLinux, isWindows } from "../../../../common/vars";
import { cssNames } from "../../utils"; import { cssNames } from "../../../utils";
import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { TopBarRegistration } from "./top-bar-registration";
interface Props extends React.HTMLAttributes<any> { interface Props extends React.HTMLAttributes<any> {}
interface Dependencies {
items: IComputedValue<TopBarRegistration[]>;
} }
const prevEnabled = observable.box(false); const prevEnabled = observable.box(false);
@ -48,34 +54,10 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => {
nextEnabled.set(state); nextEnabled.set(state);
}); });
export const TopBar = observer(({ children, ...rest }: Props) => { const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => {
const elem = useRef<HTMLDivElement>(); const elem = useRef<HTMLDivElement>();
const window = useMemo(() => getCurrentWindow(), []); const window = useMemo(() => getCurrentWindow(), []);
const renderRegisteredItems = () => {
const items = TopBarRegistry.getInstance().getItems();
if (!Array.isArray(items)) {
return null;
}
return (
<div>
{items.map((registration, index) => {
if (!registration?.components?.Item) {
return null;
}
return (
<div key={index}>
<registration.components.Item />
</div>
);
})}
</div>
);
};
const openContextMenu = () => { const openContextMenu = () => {
broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU); broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU);
}; };
@ -156,7 +138,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
/> />
</div> </div>
<div className={styles.controls}> <div className={styles.controls}>
{renderRegisteredItems()} {renderRegisteredItems(items.get())}
{children} {children}
{(isWindows || isLinux) && ( {(isWindows || isLinux) && (
<div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}> <div className={cssNames(styles.windowButtons, { [styles.linuxButtons]: isLinux })}>
@ -174,3 +156,29 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
</div> </div>
); );
}); });
const renderRegisteredItems = (items: TopBarRegistration[]) => (
<div>
{items.map((registration, index) => {
if (!registration?.components?.Item) {
return null;
}
return (
<div key={index}>
<registration.components.Item />
</div>
);
})}
</div>
);
export const TopBar = withInjectables(observer(NonInjectedTopBar), {
getProps: (di, props) => ({
items: di.inject(topBarItemsInjectable),
...props,
}),
});

View File

@ -27,7 +27,7 @@ import { observer } from "mobx-react";
import { boundMethod, cssNames } from "../../utils"; import { boundMethod, cssNames } from "../../utils";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { Icon, IconProps } from "../icon"; import { Icon, IconProps } from "../icon";
import { Menu, MenuItem, MenuProps } from "../menu"; import { Menu, MenuItem, MenuProps } from "./menu";
import uniqueId from "lodash/uniqueId"; import uniqueId from "lodash/uniqueId";
import isString from "lodash/isString"; import isString from "lodash/isString";

View File

@ -243,7 +243,9 @@ export class Menu extends React.Component<MenuProps, State> {
break; break;
case "Space": case "Space":
case "Enter": // fallthrough
case "Enter": {
const focusedItem = this.focusedItem; const focusedItem = this.focusedItem;
if (focusedItem) { if (focusedItem) {
@ -251,10 +253,12 @@ export class Menu extends React.Component<MenuProps, State> {
evt.preventDefault(); evt.preventDefault();
} }
break; break;
}
case "ArrowUp": case "ArrowUp":
this.focusNextItem(true); this.focusNextItem(true);
break; break;
case "ArrowDown": case "ArrowDown":
this.focusNextItem(); this.focusNextItem();
break; break;

View File

@ -24,7 +24,8 @@ import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { action, computed, makeObservable, observable, reaction } from "mobx"; import { action, computed, makeObservable, observable, reaction } from "mobx";
import { editor, Uri } from "monaco-editor"; import { editor, Uri } from "monaco-editor";
import { MonacoTheme, MonacoValidator, monacoValidators } from "./index"; import type { MonacoTheme } from "./monaco-themes";
import { MonacoValidator, monacoValidators } from "./monaco-validators";
import { debounce, merge } from "lodash"; import { debounce, merge } from "lodash";
import { cssNames, disposer } from "../../utils"; import { cssNames, disposer } from "../../utils";
import { UserStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";

View File

@ -228,10 +228,15 @@ html {
} }
.Select { .Select {
&__value-container {
margin-top: 2px;
margin-bottom: 2px;
}
&__control { &__control {
box-shadow: 0 0 0 1px var(--inputControlBorder); box-shadow: 0 0 0 1px var(--inputControlBorder);
background: var(--inputControlBackground); background: var(--inputControlBackground);
border-radius: 5px; border-radius: var(--border-radius);
} }
&__single-value { &__single-value {

View File

@ -0,0 +1,67 @@
/**
* 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 React from "react";
import { fireEvent, render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import { Switch } from "..";
describe("<Switch/>", () => {
it("renders w/o errors", () => {
const { container } = render(<Switch />);
expect(container).toBeInstanceOf(HTMLElement);
});
it("render label text", () => {
const { getByLabelText } = render(<Switch>Test label</Switch>);
expect(getByLabelText("Test label")).toBeTruthy();
});
it("passes disabled and checked attributes to input", () => {
const { container } = render(<Switch checked disabled/>);
const checkbox = container.querySelector("input[type=checkbox]");
expect(checkbox).toHaveAttribute("disabled");
expect(checkbox).toHaveAttribute("checked");
});
it("onClick event fired", () => {
const onClick = jest.fn();
const { getByTestId } = render(<Switch onClick={onClick}/>);
const switcher = getByTestId("switch");
fireEvent.click(switcher);
expect(onClick).toHaveBeenCalled();
});
it("onClick event not fired for disabled item", () => {
const onClick = jest.fn();
const { getByTestId } = render(<Switch onClick={onClick} disabled/>);
const switcher = getByTestId("switch");
fireEvent.click(switcher);
expect(onClick).not.toHaveBeenCalled();
});
});

View File

@ -35,6 +35,9 @@ const useStyles = makeStyles({
}, },
}); });
/**
* @deprecated Use <Switch/> instead from "../switch.tsx".
*/
export function FormSwitch(props: FormControlLabelProps) { export function FormSwitch(props: FormControlLabelProps) {
const classes = useStyles(); const classes = useStyles();

View File

@ -21,3 +21,4 @@
export * from "./switcher"; export * from "./switcher";
export * from "./form-switcher"; export * from "./form-switcher";
export * from "./switch";

View File

@ -0,0 +1,121 @@
/**
* 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.
*/
.Switch {
--thumb-size: 2rem;
--thumb-color: hsl(0 0% 100%);
--thumb-color-highlight: hsl(0 0% 100% / 25%);
--track-size: calc(var(--thumb-size) * 2);
--track-padding: 2px;
--track-color-inactive: hsl(80 0% 35%);
--track-color-active: hsl(110, 60%, 60%);
--thumb-position: 0%;
--thumb-transition-duration: .25s;
--hover-highlight-size: 0;
display: flex;
align-items: center;
gap: 2ch;
justify-content: space-between;
cursor: pointer;
user-select: none;
color: var(--textColorAccent);
font-weight: 500;
& > input {
padding: var(--track-padding);
background: var(--track-color-inactive);
inline-size: var(--track-size);
block-size: var(--thumb-size);
border-radius: var(--track-size);
appearance: none;
pointer-events: none;
border: none;
outline-offset: 5px;
box-sizing: content-box;
flex-shrink: 0;
display: grid;
align-items: center;
grid: [track] 1fr / [track] 1fr;
transition: background-color .25s ease;
&::before {
content: "";
cursor: pointer;
pointer-events: auto;
grid-area: track;
inline-size: var(--thumb-size);
block-size: var(--thumb-size);
background: var(--thumb-color);
box-shadow: 0 0 0 var(--hover-highlight-size) var(--thumb-color-highlight);
border-radius: 50%;
transform: translateX(var(--thumb-position));
transition:
transform var(--thumb-transition-duration) ease,
box-shadow .25s ease;
}
&:not(:disabled):hover::before {
--hover-highlight-size: .5rem;
}
&:checked {
background: var(--track-color-active);
--thumb-position: 100%;
}
&:disabled {
--track-color-inactive: hsl(80 0% 30%);
--thumb-color: transparent;
&::before {
cursor: not-allowed;
box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 40%);
}
}
&:focus-visible {
box-shadow: 0 0 0 2px var(--blue);
}
}
&.disabled {
cursor: not-allowed;
}
}
@include theme-light {
.Switch {
--thumb-color-highlight: hsl(0 0% 0% / 25%);
& > input {
&:disabled {
--track-color-inactive: hsl(80 0% 80%);
}
}
}
}

View File

@ -0,0 +1,38 @@
/**
* 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 styles from "./switch.module.scss";
import React, { ChangeEvent, HTMLProps } from "react";
import { cssNames } from "../../utils";
interface Props extends Omit<HTMLProps<HTMLInputElement>, "onChange"> {
onChange?: (checked: boolean, event: ChangeEvent<HTMLInputElement>) => void;
}
export function Switch({ children, disabled, onChange, ...props }: Props) {
return (
<label className={cssNames(styles.Switch, { [styles.disabled]: disabled })} data-testid="switch">
{children}
<input type="checkbox" role="switch" disabled={disabled} onChange={(event) => onChange?.(props.checked, event)} {...props}/>
</label>
);
}

View File

@ -31,6 +31,9 @@ interface Props extends SwitchProps {
classes: Styles; classes: Styles;
} }
/**
* @deprecated Use <Switch/> instead from "../switch.tsx".
*/
export const Switcher = withStyles((theme: Theme) => export const Switcher = withStyles((theme: Theme) =>
createStyles({ createStyles({
root: { root: {

View File

@ -20,8 +20,7 @@
*/ */
import styles from "./react-table.module.scss"; import styles from "./react-table.module.scss";
import React from "react"; import React, { useCallback, useMemo } from "react";
import { useCallback, useMemo } from "react";
import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table"; import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";

View File

@ -27,7 +27,6 @@ export * from "./ipc";
export * from "./kube-object-detail-registry"; export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry"; export * from "./kube-object-menu-registry";
export * from "./registries"; export * from "./registries";
export * from "./welcome-menu-registry";
export * from "./workloads-overview-detail-registry"; export * from "./workloads-overview-detail-registry";
export * from "./catalog-category-registry"; export * from "./catalog-category-registry";
export * from "./status-bar-registry"; export * from "./status-bar-registry";

Some files were not shown because too many files have changed in this diff Show More