Merge branch 'master' into feature/port-forward-disable

This commit is contained in:
Jim Ehrismann authored and GitHub committed 2022-01-05 15:13:24 -05:00
commit 9ebbe5b84f
206 files changed
+1997 -1088

No files matched your search

+5 -1
View File
@@ -108,6 +108,8 @@ module.exports = {
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
extends: [ extends: [
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
], ],
plugins: [ plugins: [
"header", "header",
@@ -193,6 +195,8 @@ module.exports = {
extends: [ extends: [
"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,
@@ -202,6 +206,7 @@ module.exports = {
rules: { rules: {
"no-irregular-whitespace": "error", "no-irregular-whitespace": "error",
"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 +251,6 @@ module.exports = {
"objectsInObjects": false, "objectsInObjects": false,
"arraysInObjects": true, "arraysInObjects": true,
}], }],
"react/prop-types": "off",
"semi": "off", "semi": "off",
"@typescript-eslint/semi": ["error"], "@typescript-eslint/semi": ["error"],
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
@@ -37,9 +37,9 @@ export default class ExampleMainExtension extends Main.LensExtension {
} }
``` ```
### App Menus ### Menus
This extension can register custom app menus that will be displayed on OS native menus. This extension can register custom app and tray menus that will be displayed on OS native menus.
Example: Example:
@@ -56,6 +56,29 @@ export default class ExampleMainExtension extends Main.LensExtension {
} }
} }
] ]
trayMenus = [
{
label: "My links",
submenu: [
{
label: "Lens",
click() {
Main.Navigation.navigate("https://k8slens.dev");
}
},
{
type: "separator"
},
{
label: "Lens Github",
click() {
Main.Navigation.navigate("https://github.com/lensapp/lens");
}
}
]
}
]
} }
``` ```
+30 -2
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).
+36
View File
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
module.exports = {
"overrides": [
{
files: [
"**/*.ts",
"**/*.tsx",
],
rules: {
"import/no-unresolved": ["error", {
ignore: ["@k8slens/extensions"],
}],
},
},
],
};
+12 -12
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",
@@ -196,10 +195,11 @@
"@hapi/call": "^8.0.1", "@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3", "@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1", "@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "1.5.0", "@ogre-tools/injectable": "2.0.0",
"@ogre-tools/injectable-react": "1.5.2", "@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",
@@ -265,7 +265,7 @@
"ws": "^7.5.5" "ws": "^7.5.5"
}, },
"devDependencies": { "devDependencies": {
"@async-fn/jest": "^1.5.0", "@async-fn/jest": "1.5.3",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/lab": "^4.0.0-alpha.60",
@@ -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",
@@ -361,8 +362,7 @@
"nodemon": "^2.0.14", "nodemon": "^2.0.14",
"playwright": "^1.17.1", "playwright": "^1.17.1",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"postcss-loader": "4.3.0", "postcss-loader": "^4.3.0",
"postinstall-postinstall": "^2.1.0",
"progress-bar-webpack-plugin": "^2.1.0", "progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2", "randomcolor": "^0.6.2",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
@@ -373,11 +373,11 @@
"react-select-event": "^5.1.0", "react-select-event": "^5.1.0",
"react-table": "^7.7.0", "react-table": "^7.7.0",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"sass": "^1.44.0", "sass": "^1.45.1",
"sass-loader": "^8.0.2", "sass-loader": "^10.2.0",
"sharp": "^0.29.3", "sharp": "^0.29.3",
"style-loader": "^2.0.0", "style-loader": "^2.0.0",
"tailwindcss": "^2.2.19", "tailwindcss": "^3.0.7",
"ts-jest": "26.5.6", "ts-jest": "26.5.6",
"ts-loader": "^7.0.5", "ts-loader": "^7.0.5",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
+22 -30
View File
@@ -20,9 +20,11 @@
*/ */
import { anyObject } from "jest-mock-extended"; import { anyObject } from "jest-mock-extended";
import { merge } from "lodash";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { AppPaths } from "../app-paths"; import { AppPaths } from "../app-paths";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { ClusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store"; import { HotbarStore } from "../hotbar-store";
@@ -54,68 +56,58 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({
}, },
})); }));
const testCluster = { function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
uid: "test", return merge(data, {
name: "test", getName: jest.fn(() => data.metadata?.name),
getId: jest.fn(() => data.metadata?.uid),
getSource: jest.fn(() => data.metadata?.source ?? "unknown"),
isEnabled: jest.fn(() => data.status?.enabled ?? true),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {},
spec: {},
status: {},
}) as CatalogEntity;
}
const testCluster = getMockCatalogEntity({
apiVersion: "v1", apiVersion: "v1",
kind: "Cluster", kind: "Cluster",
status: { status: {
phase: "Running", phase: "Running",
}, },
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: { metadata: {
uid: "test", uid: "test",
name: "test", name: "test",
labels: {}, labels: {},
}, },
}; });
const minikubeCluster = { const minikubeCluster = getMockCatalogEntity({
uid: "minikube",
name: "minikube",
apiVersion: "v1", apiVersion: "v1",
kind: "Cluster", kind: "Cluster",
status: { status: {
phase: "Running", phase: "Running",
}, },
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: { metadata: {
uid: "minikube", uid: "minikube",
name: "minikube", name: "minikube",
labels: {}, labels: {},
}, },
}; });
const awsCluster = { const awsCluster = getMockCatalogEntity({
uid: "aws",
name: "aws",
apiVersion: "v1", apiVersion: "v1",
kind: "Cluster", kind: "Cluster",
status: { status: {
phase: "Running", phase: "Running",
}, },
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: { metadata: {
uid: "aws", uid: "aws",
name: "aws", name: "aws",
labels: {}, labels: {},
}, },
}; });
jest.mock("electron", () => ({ jest.mock("electron", () => ({
app: { app: {
+3 -3
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", () => {
+2 -1
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];
@@ -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";
+59
View File
@@ -38,14 +38,44 @@ export type CatalogEntityConstructor<Entity extends CatalogEntity> = (
); );
export interface CatalogCategoryVersion<Entity extends CatalogEntity> { export interface CatalogCategoryVersion<Entity extends CatalogEntity> {
/**
* The specific version that the associated constructor is for. This MUST be
* a DNS label and SHOULD be of the form `vN`, `vNalphaY`, or `vNbetaY` where
* `N` and `Y` are both integers greater than 0.
*
* Examples: The following are valid values for this field.
* - `v1`
* - `v1beta1`
* - `v1alpha2`
* - `v3beta2`
*/
name: string; name: string;
/**
* The constructor for the entities.
*/
entityClass: CatalogEntityConstructor<Entity>; entityClass: CatalogEntityConstructor<Entity>;
} }
export interface CatalogCategorySpec { export interface CatalogCategorySpec {
/**
* The grouping for for the category. This MUST be a DNS label.
*/
group: string; group: string;
/**
* The specific versions of the constructors.
*
* NOTE: the field `.apiVersion` after construction MUST match `{.group}/{.versions.[] | .name}`.
* For example, if `group = "entity.k8slens.dev"` and there is an entry in `.versions` with
* `name = "v1alpha1"` then the resulting `.apiVersion` MUST be `entity.k8slens.dev/v1alpha1`
*/
versions: CatalogCategoryVersion<CatalogEntity>[]; versions: CatalogCategoryVersion<CatalogEntity>[];
names: { names: {
/**
* The kind of entity that this category is for. This value MUST be a DNS
* label and MUST be equal to the `kind` fields that are produced by the
* `.versions.[] | .entityClass` fields.
*/
kind: string; kind: string;
}; };
} }
@@ -114,6 +144,7 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
export interface CatalogEntityMetadata { export interface CatalogEntityMetadata {
uid: string; uid: string;
name: string; name: string;
shortName?: string;
description?: string; description?: string;
source?: string; source?: string;
labels: Record<string, string>; labels: Record<string, string>;
@@ -211,7 +242,14 @@ export abstract class CatalogEntity<
Status extends CatalogEntityStatus = CatalogEntityStatus, Status extends CatalogEntityStatus = CatalogEntityStatus,
Spec extends CatalogEntitySpec = CatalogEntitySpec, Spec extends CatalogEntitySpec = CatalogEntitySpec,
> implements CatalogEntityKindData { > implements CatalogEntityKindData {
/**
* The group and version of this class.
*/
public abstract readonly apiVersion: string; public abstract readonly apiVersion: string;
/**
* A DNS label name of the entity.
*/
public abstract readonly kind: string; public abstract readonly kind: string;
@observable metadata: Metadata; @observable metadata: Metadata;
@@ -225,14 +263,35 @@ export abstract class CatalogEntity<
this.spec = data.spec; this.spec = data.spec;
} }
/**
* Get the UID of this entity
*/
public getId(): string { public getId(): string {
return this.metadata.uid; return this.metadata.uid;
} }
/**
* Get the name of this entity
*/
public getName(): string { public getName(): string {
return this.metadata.name; return this.metadata.name;
} }
/**
* Get the specified source of this entity, defaulting to `"unknown"` if not
* provided
*/
public getSource(): string {
return this.metadata.source ?? "unknown";
}
/**
* Get if this entity is enabled.
*/
public isEnabled(): boolean {
return this.status.enabled ?? true;
}
public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>; public abstract onRun?(context: CatalogEntityActionContext): void | Promise<void>;
public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>; public abstract onContextMenuOpen(context: CatalogEntityContextMenuContext): void | Promise<void>;
public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>; public abstract onSettingsOpen(context: CatalogEntitySettingsContext): void | Promise<void>;
@@ -18,10 +18,4 @@
* 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.
*/ */
export const BundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded";
module.exports = {
plugins: [
require("tailwindcss/nesting"),
require("tailwindcss"),
],
};
+1
View File
@@ -27,3 +27,4 @@ export * from "./update-available.ipc";
export * from "./cluster.ipc"; export * from "./cluster.ipc";
export * from "./type-enforced-ipc"; export * from "./type-enforced-ipc";
export * from "./hotbar"; export * from "./hotbar";
export * from "./extension-loader.ipc";
+9 -1
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";
+10 -3
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");
+17 -12
View File
@@ -48,11 +48,10 @@ 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 previously * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/ */
version?: string; version?: string;
names: { names: {
@@ -61,9 +60,9 @@ export interface CustomResourceDefinition {
kind: string; kind: string;
listKind: string; listKind: string;
}; };
scope: "Namespaced" | "Cluster" | string; scope: "Namespaced" | "Cluster";
/** /**
* @deprecated for apiextensions.k8s.io/v1 but used previously * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/ */
validation?: object; validation?: object;
versions?: CRDVersion[]; versions?: CRDVersion[];
@@ -72,10 +71,13 @@ export interface CustomResourceDefinition {
webhook?: any; webhook?: any;
}; };
/** /**
* @deprecated for apiextensions.k8s.io/v1 but used previously * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1
*/ */
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
}; }
export interface CustomResourceDefinition {
spec: CustomResourceDefinitionSpec;
status: { status: {
conditions: { conditions: {
lastTransitionTime: string; lastTransitionTime: string;
@@ -150,14 +152,17 @@ export class CustomResourceDefinition extends KubeObject {
} }
getPreferedVersion(): CRDVersion { getPreferedVersion(): CRDVersion {
// Prefer the modern `versions` over the legacy `version` const { apiVersion } = this;
if (this.spec.versions) {
switch (apiVersion) {
case "apiextensions.k8s.io/v1":
for (const version of this.spec.versions) { for (const version of this.spec.versions) {
if (version.storage) { if (version.storage) {
return version; return version;
} }
} }
} else if (this.spec.version) { break;
case "apiextensions.k8s.io/v1beta1":
const { additionalPrinterColumns: apc } = this.spec; const { additionalPrinterColumns: apc } = this.spec;
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath })); const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath }));
@@ -170,7 +175,7 @@ export class CustomResourceDefinition extends KubeObject {
}; };
} }
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 +202,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() {
+3
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";
+3
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";
+3 -3
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;
} }
+1 -1
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
+1
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();
@@ -18,14 +18,12 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { Injectable } 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: Injectable<ExtensionLoader> = { const extensionLoaderInjectable = getInjectable({
getDependencies: () => ({}),
instantiate: () => new ExtensionLoader(), instantiate: () => new ExtensionLoader(),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default extensionLoaderInjectable; export default extensionLoaderInjectable;
@@ -255,17 +255,15 @@ export class ExtensionLoader {
loadOnClusterManagerRenderer() { loadOnClusterManagerRenderer() {
logger.debug(`${logModule}: load on main renderer (cluster manager)`); logger.debug(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
return this.autoInitExtensions(async (extension: LensRendererExtension) => {
const removeItems = [ const removeItems = [
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension), registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences), registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
registries.CommandRegistry.getInstance().add(extension.commands), registries.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) => {
@@ -311,7 +309,9 @@ export class ExtensionLoader {
} }
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) { protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
return reaction(() => this.toJSON(), installedExtensions => { const loadingExtensions: { isBundled: boolean, loaded: Promise<void> }[] = [];
reaction(() => this.toJSON(), installedExtensions => {
for (const [extId, extension] of installedExtensions) { for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
@@ -326,7 +326,14 @@ export class ExtensionLoader {
const instance = new LensExtensionClass(extension); const instance = new LensExtensionClass(extension);
instance.enable(register); const loaded = instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
});
loadingExtensions.push({
isBundled: extension.isBundled,
loaded,
});
this.instances.set(extId, instance); this.instances.set(extId, instance);
} catch (err) { } catch (err) {
logger.error(`${logModule}: activation extension error`, { ext: extension, err }); logger.error(`${logModule}: activation extension error`, { ext: extension, err });
@@ -338,6 +345,8 @@ export class ExtensionLoader {
}, { }, {
fireImmediately: true, fireImmediately: true,
}); });
return loadingExtensions;
} }
protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null { protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null {
+9 -15
View File
@@ -18,24 +18,18 @@
* 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 { Injectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx"; import { computed } from "mobx";
import type { LensExtension } from "./lens-extension";
import type { ExtensionLoader } from "./extension-loader";
import extensionLoaderInjectable from "./extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "./extension-loader/extension-loader.injectable";
const extensionsInjectable: Injectable< const extensionsInjectable = getInjectable({
IComputedValue<LensExtension[]>, instantiate: (di) => {
{ extensionLoader: ExtensionLoader } const extensionLoader = di.inject(extensionLoaderInjectable);
> = {
getDependencies: di => ({ return computed(() => extensionLoader.enabledExtensionInstances);
extensionLoader: di.inject(extensionLoaderInjectable), },
}),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
});
instantiate: ({ extensionLoader }) =>
computed(() => extensionLoader.enabledExtensionInstances),
};
export default extensionsInjectable; export default extensionsInjectable;
+2 -1
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);
+6 -3
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");
@@ -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 { LensMainExtension } from "./lens-main-extension";
const mainExtensionsInjectable = getInjectable({
lifecycle: lifecycleEnum.singleton,
instantiate: (di) =>
di.inject(extensionsInjectable) as IComputedValue<LensMainExtension[]>,
});
export default mainExtensionsInjectable;
+1 -1
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 });
-3
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";
@@ -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;
+1 -2
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;
+2 -1
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";
@@ -43,6 +43,7 @@ async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFile
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> { export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
const args = [ const args = [
"ls", "ls",
"--all",
"--output", "json", "--output", "json",
]; ];
+4 -2
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(),
); );
+10 -15
View File
@@ -18,24 +18,19 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx"; import { computed } from "mobx";
import type { LensMainExtension } from "../../extensions/lens-main-extension"; import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
import extensionsInjectable from "../../extensions/extensions.injectable";
import type { MenuRegistration } from "./menu-registration";
const electronMenuItemsInjectable: Injectable< const electronMenuItemsInjectable = getInjectable({
IComputedValue<MenuRegistration[]>,
{ extensions: IComputedValue<LensMainExtension[]> }
> = {
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
getDependencies: di => ({ instantiate: (di) => {
extensions: di.inject(extensionsInjectable), const extensions = di.inject(mainExtensionsInjectable);
}),
instantiate: ({ extensions }) => return computed(() =>
computed(() => extensions.get().flatMap(extension => extension.appMenus)), extensions.get().flatMap((extension) => extension.appMenus));
}; },
});
export default electronMenuItemsInjectable; export default electronMenuItemsInjectable;
+3 -3
View File
@@ -24,8 +24,8 @@ import electronMenuItemsInjectable from "./electron-menu-items.injectable";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { computed, ObservableMap, runInAction } from "mobx"; import { computed, ObservableMap, runInAction } from "mobx";
import type { MenuRegistration } from "./menu-registration"; import type { MenuRegistration } from "./menu-registration";
import extensionsInjectable from "../../extensions/extensions.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
describe("electron-menu-items", () => { describe("electron-menu-items", () => {
let di: ConfigurableDependencyInjectionContainer; let di: ConfigurableDependencyInjectionContainer;
@@ -38,8 +38,8 @@ describe("electron-menu-items", () => {
extensionsStub = new ObservableMap(); extensionsStub = new ObservableMap();
di.override( di.override(
extensionsInjectable, mainExtensionsInjectable,
computed(() => [...extensionsStub.values()]), () => computed(() => [...extensionsStub.values()]),
); );
electronMenuItems = di.inject(electronMenuItemsInjectable); electronMenuItems = di.inject(electronMenuItemsInjectable);
+9 -9
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":
@@ -18,23 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
import type { Dependencies } from "./lens-protocol-router-main";
import { LensProtocolRouterMain } from "./lens-protocol-router-main"; import { LensProtocolRouterMain } from "./lens-protocol-router-main";
const lensProtocolRouterMainInjectable: Injectable< const lensProtocolRouterMainInjectable = getInjectable({
LensProtocolRouterMain, instantiate: (di) =>
Dependencies new LensProtocolRouterMain({
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
}), }),
instantiate: dependencies => new LensProtocolRouterMain(dependencies),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default lensProtocolRouterMainInjectable; export default lensProtocolRouterMainInjectable;
@@ -51,7 +51,7 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
} }
} }
export interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader extensionLoader: ExtensionLoader
} }
+1 -1
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";
@@ -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;
+136
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;
}
}
+30
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[]
}
+33 -13
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}`,
+2 -2
View File
@@ -24,7 +24,7 @@ import { makeObservable, observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"; import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state"; import windowStateKeeper from "electron-window-state";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/event-bus";
import { ipcMainOn } from "../common/ipc"; import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc";
import { delay, iter, Singleton } from "../common/utils"; import { delay, iter, Singleton } from "../common/utils";
import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
@@ -181,7 +181,7 @@ export class WindowManager extends Singleton {
if (!this.mainWindow) { if (!this.mainWindow) {
viewHasLoaded = new Promise<void>(resolve => { viewHasLoaded = new Promise<void>(resolve => {
ipcMain.once(IpcRendererNavigationEvents.LOADED, () => resolve()); ipcMain.once(BundledExtensionsLoaded, () => resolve());
}); });
await this.initMainWindow(showSplash); await this.initMainWindow(showSplash);
} }
@@ -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";
+8 -1
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 });
+1 -11
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;
},
};
-3
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();
@@ -38,7 +38,7 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
} }
:global(.theme-light) { @include theme-light {
.editor { .editor {
border-color: var(--borderFaintColor); border-color: var(--borderFaintColor);
} }
@@ -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 styles from "./add-cluster.module.css"; import styles from "./add-cluster.module.scss";
import type { KubeConfig } from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node";
import fse from "fs-extra"; import fse from "fs-extra";
@@ -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 styles from "./catalog-entity-details.module.css"; import styles from "./catalog-entity-details.module.scss";
import React, { Component } from "react"; import React, { Component } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer"; import { Drawer, DrawerItem } from "../drawer";
@@ -27,14 +27,15 @@ import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu"; import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { isDevelopment } from "../../../common/vars"; import { isDevelopment } from "../../../common/vars";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Avatar } from "../avatar"; import { Avatar } from "../avatar";
import { getLabelBadges } from "./helpers";
interface Props<T extends CatalogEntity> { interface Props<T extends CatalogEntity> {
item: CatalogEntityItem<T> | null | undefined; entity: T;
hideDetails(): void; hideDetails(): void;
onRun: () => void;
} }
@observer @observer
@@ -47,32 +48,30 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
} }
} }
renderContent(item: CatalogEntityItem<T>) { renderContent(entity: T) {
const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(item.kind, item.apiVersion); const { onRun, hideDetails } = this.props;
const details = detailItems.map(({ components }, index) => { const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(entity.kind, entity.apiVersion);
return <components.Details entity={item.entity} key={index}/>; const details = detailItems.map(({ components }, index) => <components.Details entity={entity} key={index} />);
}); const showDefaultDetails = detailItems.find((item) => item.priority > 999) === undefined;
const showDetails = detailItems.find((item) => item.priority > 999) === undefined;
return ( return (
<> <>
{showDetails && ( {showDefaultDetails && (
<div className="flex"> <div className="flex">
<div className={styles.entityIcon}> <div className={styles.entityIcon}>
<Avatar <Avatar
title={item.name} title={entity.getName()}
colorHash={`${item.name}-${item.source}`} colorHash={`${entity.getName()}-${entity.getSource()}`}
size={128} size={128}
src={item.entity.spec.icon?.src} src={entity.spec.icon?.src}
data-testid="detail-panel-hot-bar-icon" data-testid="detail-panel-hot-bar-icon"
background={item.entity.spec.icon?.background} background={entity.spec.icon?.background}
onClick={() => item.onRun()} onClick={onRun}
className={styles.avatar} className={styles.avatar}
> >
{item.entity.spec.icon?.material && <Icon material={item.entity.spec.icon?.material}/>} {entity.spec.icon?.material && <Icon material={entity.spec.icon?.material}/>}
</Avatar> </Avatar>
{item?.enabled && ( {entity.isEnabled() && (
<div className={styles.hint}> <div className={styles.hint}>
Click to open Click to open
</div> </div>
@@ -80,23 +79,23 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
</div> </div>
<div className={cssNames("box grow", styles.metadata)}> <div className={cssNames("box grow", styles.metadata)}>
<DrawerItem name="Name"> <DrawerItem name="Name">
{item.name} {entity.getName()}
</DrawerItem> </DrawerItem>
<DrawerItem name="Kind"> <DrawerItem name="Kind">
{item.kind} {entity.kind}
</DrawerItem> </DrawerItem>
<DrawerItem name="Source"> <DrawerItem name="Source">
{item.source} {entity.getSource()}
</DrawerItem> </DrawerItem>
<DrawerItem name="Status"> <DrawerItem name="Status">
{item.phase} {entity.status.phase}
</DrawerItem> </DrawerItem>
<DrawerItem name="Labels"> <DrawerItem name="Labels">
{...item.getLabelBadges(this.props.hideDetails)} {getLabelBadges(entity, hideDetails)}
</DrawerItem> </DrawerItem>
{isDevelopment && ( {isDevelopment && (
<DrawerItem name="Id"> <DrawerItem name="Id">
{item.getId()} {entity.getId()}
</DrawerItem> </DrawerItem>
)} )}
</div> </div>
@@ -110,19 +109,18 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
} }
render() { render() {
const { item, hideDetails } = this.props; const { entity, hideDetails } = this.props;
const title = `${item.kind}: ${item.name}`;
return ( return (
<Drawer <Drawer
className={styles.entityDetails} className={styles.entityDetails}
usePortal={true} usePortal={true}
open={true} open={true}
title={title} title={`${entity.kind}: ${entity.getName()}`}
toolbar={<CatalogEntityDrawerMenu item={item} key={item.getId()} />} toolbar={<CatalogEntityDrawerMenu entity={entity} key={entity.getId()} />}
onClose={hideDetails} onClose={hideDetails}
> >
{item && this.renderContent(item)} {this.renderContent(entity)}
</Drawer> </Drawer>
); );
} }
@@ -29,11 +29,10 @@ import { navigate } from "../../navigation";
import { MenuItem } from "../menu"; import { MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
import { Icon } from "../icon"; import { Icon } from "../icon";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps { export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
item: CatalogEntityItem<T> | null | undefined; entity: T;
} }
@observer @observer
@@ -50,7 +49,7 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
menuItems: [], menuItems: [],
navigate: (url: string) => navigate(url), navigate: (url: string) => navigate(url),
}; };
this.props.item?.onContextMenuOpen(this.contextMenu); this.props.entity?.onContextMenuOpen(this.contextMenu);
} }
onMenuItemClick(menuItem: CatalogEntityContextMenu) { onMenuItemClick(menuItem: CatalogEntityContextMenu) {
@@ -108,9 +107,9 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
} }
render() { render() {
const { className, item: entity, ...menuProps } = this.props; const { className, entity, ...menuProps } = this.props;
if (!this.contextMenu || !entity.enabled) { if (!this.contextMenu || !entity.isEnabled()) {
return null; return null;
} }
@@ -120,7 +119,7 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
toolbar toolbar
{...menuProps} {...menuProps}
> >
{this.getMenuItems(entity.entity)} {this.getMenuItems(entity)}
</MenuActions> </MenuActions>
); );
} }
@@ -18,7 +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 styles from "./catalog.module.css"; import styles from "./catalog.module.scss";
import React from "react"; import React from "react";
import { action, computed } from "mobx"; import { action, computed } from "mobx";
import { CatalogEntity } from "../../api/catalog-entity"; import { CatalogEntity } from "../../api/catalog-entity";
@@ -25,9 +25,8 @@ import type { CatalogEntity } from "../../api/catalog-entity";
import { ItemStore } from "../../../common/item.store"; import { ItemStore } from "../../../common/item.store";
import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
import { autoBind, disposer } from "../../../common/utils"; import { autoBind, disposer } from "../../../common/utils";
import { CatalogEntityItem } from "./catalog-entity-item";
export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntity>> { export class CatalogEntityStore extends ItemStore<CatalogEntity> {
constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) { constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) {
super(); super();
makeObservable(this); makeObservable(this);
@@ -39,10 +38,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
@computed get entities() { @computed get entities() {
if (!this.activeCategory) { if (!this.activeCategory) {
return this.registry.filteredItems.map(entity => new CatalogEntityItem(entity, this.registry)); return this.registry.filteredItems;
} }
return this.registry.getItemsForCategory(this.activeCategory, { filtered: true }).map(entity => new CatalogEntityItem(entity, this.registry)); return this.registry.getItemsForCategory(this.activeCategory, { filtered: true });
} }
@computed get selectedItem() { @computed get selectedItem() {
@@ -68,4 +67,8 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
// concurrency is true to fix bug if catalog filter is removed and added at the same time // concurrency is true to fix bug if catalog filter is removed and added at the same time
return this.loadItems(() => this.entities, undefined, true); return this.loadItems(() => this.entities, undefined, true);
} }
onRun(entity: CatalogEntity): void {
this.registry.onRun(entity);
}
} }
@@ -19,8 +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 treeStyles from "./catalog-tree.module.css"; import treeStyles from "./catalog-tree.module.scss";
import styles from "./catalog-menu.module.css"; import styles from "./catalog-menu.module.scss";
import React from "react"; import React from "react";
import { TreeItem, TreeItemProps, TreeView } from "@material-ui/lab"; import { TreeItem, TreeItemProps, TreeView } from "@material-ui/lab";
@@ -29,7 +29,6 @@ import { kubernetesClusterCategory } from "../../../common/catalog-entities/kube
import { catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; import { catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog";
import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry"; import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { CatalogEntityItem } from "./catalog-entity-item";
import { CatalogEntityStore } from "./catalog-entity.store"; import { CatalogEntityStore } from "./catalog-entity.store";
import { AppPaths } from "../../../common/app-paths"; import { AppPaths } from "../../../common/app-paths";
@@ -130,7 +129,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(); const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); const catalogEntityItem = createMockCatalogEntity(onRun);
// mock as if there is a selected item > the detail panel opens // mock as if there is a selected item > the detail panel opens
jest jest
@@ -166,7 +165,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(); const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); const catalogEntityItem = createMockCatalogEntity(onRun);
// mock as if there is a selected item > the detail panel opens // mock as if there is a selected item > the detail panel opens
jest jest
@@ -200,7 +199,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(); const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); const catalogEntityItem = createMockCatalogEntity(onRun);
// mock as if there is a selected item > the detail panel opens // mock as if there is a selected item > the detail panel opens
jest jest
@@ -235,7 +234,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(() => done()); const onRun = jest.fn(() => done());
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); const catalogEntityItem = createMockCatalogEntity(onRun);
// mock as if there is a selected item > the detail panel opens // mock as if there is a selected item > the detail panel opens
jest jest
@@ -265,7 +264,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(); const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); const catalogEntityItem = createMockCatalogEntity(onRun);
// mock as if there is a selected item > the detail panel opens // mock as if there is a selected item > the detail panel opens
jest jest
@@ -302,7 +301,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
const onRun = jest.fn(); const onRun = jest.fn();
const catalogEntityItem = new CatalogEntityItem(createMockCatalogEntity(onRun), catalogEntityRegistry); const catalogEntityItem = createMockCatalogEntity(onRun);
// mock as if there is a selected item > the detail panel opens // mock as if there is a selected item > the detail panel opens
jest jest
+46 -36
View File
@@ -19,14 +19,13 @@
* 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 "./catalog.module.css"; import styles from "./catalog.module.scss";
import React from "react"; import React from "react";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list"; import { ItemListLayout } from "../item-object-list";
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx"; import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
import { CatalogEntityStore } from "./catalog-entity.store"; import { CatalogEntityStore } from "./catalog-entity.store";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu"; import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
@@ -45,6 +44,8 @@ import { RenderDelay } from "../render-delay/render-delay";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
import { Avatar } from "../avatar"; import { Avatar } from "../avatar";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import { getLabelBadges } from "./helpers";
export const previousActiveTab = createStorage("catalog-previous-active-tab", browseCatalogTab); export const previousActiveTab = createStorage("catalog-previous-active-tab", browseCatalogTab);
@@ -125,19 +126,19 @@ export class Catalog extends React.Component<Props> {
})); }));
} }
addToHotbar(item: CatalogEntityItem<CatalogEntity>): void { addToHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().addToHotbar(item.entity); HotbarStore.getInstance().addToHotbar(entity);
} }
removeFromHotbar(item: CatalogEntityItem<CatalogEntity>): void { removeFromHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().removeFromHotbar(item.getId()); HotbarStore.getInstance().removeFromHotbar(entity.getId());
} }
onDetails = (item: CatalogEntityItem<CatalogEntity>) => { onDetails = (entity: CatalogEntity) => {
if (this.catalogEntityStore.selectedItemId) { if (this.catalogEntityStore.selectedItemId) {
this.catalogEntityStore.selectedItemId = null; this.catalogEntityStore.selectedItemId = null;
} else { } else {
item.onRun(); this.catalogEntityStore.onRun(entity);
} }
}; };
@@ -179,16 +180,16 @@ export class Catalog extends React.Component<Props> {
); );
} }
renderItemMenu = (item: CatalogEntityItem<CatalogEntity>) => { renderItemMenu = (entity: CatalogEntity) => {
const onOpen = () => { const onOpen = () => {
this.contextMenu.menuItems = []; this.contextMenu.menuItems = [];
item.onContextMenuOpen(this.contextMenu); entity.onContextMenuOpen(this.contextMenu);
}; };
return ( return (
<MenuActions onOpen={onOpen}> <MenuActions onOpen={onOpen}>
<MenuItem key="open-details" onClick={() => this.catalogEntityStore.selectedItemId = item.getId()}> <MenuItem key="open-details" onClick={() => this.catalogEntityStore.selectedItemId = entity.getId()}>
View Details View Details
</MenuItem> </MenuItem>
{ {
@@ -200,7 +201,7 @@ export class Catalog extends React.Component<Props> {
} }
<HotbarToggleMenuItem <HotbarToggleMenuItem
key="hotbar-toggle" key="hotbar-toggle"
entity={item.entity} entity={entity}
addContent="Add to Hotbar" addContent="Add to Hotbar"
removeContent="Remove from Hotbar" removeContent="Remove from Hotbar"
/> />
@@ -208,29 +209,29 @@ export class Catalog extends React.Component<Props> {
); );
}; };
renderName(item: CatalogEntityItem<CatalogEntity>) { renderName(entity: CatalogEntity) {
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity); const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(entity);
return ( return (
<> <>
<Avatar <Avatar
title={item.getName()} title={entity.getName()}
colorHash={`${item.getName()}-${item.source}`} colorHash={`${entity.getName()}-${entity.getSource()}`}
src={item.entity.spec.icon?.src} src={entity.spec.icon?.src}
background={item.entity.spec.icon?.background} background={entity.spec.icon?.background}
className={styles.catalogAvatar} className={styles.catalogAvatar}
size={24} size={24}
> >
{item.entity.spec.icon?.material && <Icon material={item.entity.spec.icon?.material} small/>} {entity.spec.icon?.material && <Icon material={entity.spec.icon?.material} small/>}
</Avatar> </Avatar>
<span>{item.name}</span> <span>{entity.getName()}</span>
<Icon <Icon
small small
className={styles.pinIcon} className={styles.pinIcon}
material={!isItemInHotbar && "push_pin"} material={!isItemInHotbar && "push_pin"}
svg={isItemInHotbar ? "push_off" : "push_pin"} svg={isItemInHotbar ? "push_off" : "push_pin"}
tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"} tooltip={isItemInHotbar ? "Remove from Hotbar" : "Add to Hotbar"}
onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(item) : this.addToHotbar(item))} onClick={prevDefault(() => isItemInHotbar ? this.removeFromHotbar(entity) : this.addToHotbar(entity))}
/> />
</> </>
); );
@@ -253,13 +254,19 @@ export class Catalog extends React.Component<Props> {
isConfigurable={true} isConfigurable={true}
store={this.catalogEntityStore} store={this.catalogEntityStore}
sortingCallbacks={{ sortingCallbacks={{
[sortBy.name]: item => item.name, [sortBy.name]: entity => entity.getName(),
[sortBy.source]: item => item.source, [sortBy.source]: entity => entity.getSource(),
[sortBy.status]: item => item.phase, [sortBy.status]: entity => entity.status.phase,
[sortBy.kind]: item => item.kind, [sortBy.kind]: entity => entity.kind,
}} }}
searchFilters={[ searchFilters={[
entity => entity.searchFields, entity => [
entity.getName(),
entity.getId(),
entity.status.phase,
`source=${entity.getSource()}`,
...KubeObject.stringifyLabels(entity.metadata.labels),
],
]} ]}
renderTableHeader={[ renderTableHeader={[
{ title: "Name", className: styles.entityName, sortBy: sortBy.name, id: "name" }, { title: "Name", className: styles.entityName, sortBy: sortBy.name, id: "name" },
@@ -268,15 +275,15 @@ export class Catalog extends React.Component<Props> {
{ title: "Labels", className: `${styles.labelsCell} scrollable`, id: "labels" }, { title: "Labels", className: `${styles.labelsCell} scrollable`, id: "labels" },
{ title: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" }, { title: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" },
].filter(Boolean)} ].filter(Boolean)}
customizeTableRowProps={item => ({ customizeTableRowProps={entity => ({
disabled: !item.enabled, disabled: !entity.isEnabled(),
})} })}
renderTableContents={item => [ renderTableContents={entity => [
this.renderName(item), this.renderName(entity),
!activeCategory && item.kind, !activeCategory && entity.kind,
item.source, entity.getSource(),
item.getLabelBadges(), getLabelBadges(entity),
<span key="phase" className={item.phase}>{item.phase}</span>, <span key="phase" className={entity.status.phase}>{entity.status.phase}</span>,
].filter(Boolean)} ].filter(Boolean)}
onDetails={this.onDetails} onDetails={this.onDetails}
renderItemMenu={this.renderItemMenu} renderItemMenu={this.renderItemMenu}
@@ -289,16 +296,19 @@ export class Catalog extends React.Component<Props> {
return null; return null;
} }
const selectedEntity = this.catalogEntityStore.selectedItem;
return ( return (
<MainLayout sidebar={this.renderNavigation()}> <MainLayout sidebar={this.renderNavigation()}>
<div className="p-6 h-full"> <div className="p-6 h-full">
{this.renderList()} {this.renderList()}
</div> </div>
{ {
this.catalogEntityStore.selectedItem selectedEntity
? <CatalogEntityDetails ? <CatalogEntityDetails
item={this.catalogEntityStore.selectedItem} entity={selectedEntity}
hideDetails={() => this.catalogEntityStore.selectedItemId = null} hideDetails={() => this.catalogEntityStore.selectedItemId = null}
onRun={() => this.catalogEntityStore.onRun(selectedEntity)}
/> />
: ( : (
<RenderDelay> <RenderDelay>
@@ -0,0 +1,49 @@
/**
* 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 "./catalog.module.scss";
import React from "react";
import { KubeObject } from "../../../common/k8s-api/kube-object";
import type { CatalogEntity } from "../../api/catalog-entity";
import { Badge } from "../badge";
import { searchUrlParam } from "../input";
/**
* @param entity The entity to render badge labels for
*/
export function getLabelBadges(entity: CatalogEntity, onClick?: (evt: React.MouseEvent<any, MouseEvent>) => void) {
return KubeObject.stringifyLabels(entity.metadata.labels)
.map(label => (
<Badge
scrollable
className={styles.badge}
key={label}
label={label}
title={label}
onClick={(event) => {
searchUrlParam.set(label);
onClick?.(event);
event.stopPropagation();
}}
expandable={false}
/>
));
}
@@ -61,14 +61,14 @@
} }
.noIssues { .noIssues {
.ok-title { .Icon {
color: white;
}
.title {
font-size: large; font-size: large;
color: var(--textColorAccent); color: var(--textColorAccent);
font-weight: bold; font-weight: bold;
} }
.allGood {
color: white;
}
} }
} }
@@ -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 styles from "./cluster-issues.module.css"; import styles from "./cluster-issues.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@@ -149,9 +149,9 @@ export class ClusterIssues extends React.Component<Props> {
if (!warnings.length) { if (!warnings.length) {
return ( return (
<div className={cssNames(styles.noIssues, "flex column box grow gaps align-center justify-center")}> <div className={cssNames(styles.noIssues, "flex column box grow gaps align-center justify-center")}>
<div><Icon className={styles.allGood} material="check" big sticker/></div> <Icon className={styles.Icon} material="check" big sticker/>
<div className="ok-title">No issues found</div> <p className={styles.title}>No issues found</p>
<span>Everything is fine in the Cluster</span> <p>Everything is fine in the Cluster</p>
</div> </div>
); );
} }
@@ -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 styles from "./cluster-metrics.module.css"; import styles from "./cluster-metrics.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@@ -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 styles from "./cluster-overview.module.css"; import styles from "./cluster-overview.module.scss";
import React from "react"; import React from "react";
import { reaction } from "mobx"; import { reaction } from "mobx";
@@ -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 styles from "./cluster-pie-charts.module.css"; import styles from "./cluster-pie-charts.module.scss";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
@@ -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 styles from "./entity-settings.module.css"; import styles from "./entity-settings.module.scss";
import React from "react"; import React from "react";
import { observable, makeObservable } from "mobx"; import { observable, makeObservable } from "mobx";
@@ -18,20 +18,19 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { attemptInstallByInfo } from "./attempt-install-by-info";
import { attemptInstallByInfo, ExtensionInfo } from "./attempt-install-by-info";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable"; import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable";
const attemptInstallByInfoInjectable: Injectable<(extensionInfo: ExtensionInfo) => Promise<void>, {}> = { const attemptInstallByInfoInjectable = getInjectable({
getDependencies: di => ({ instantiate: (di) =>
attemptInstallByInfo({
attemptInstall: di.inject(attemptInstallInjectable), attemptInstall: di.inject(attemptInstallInjectable),
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
}), }),
instantiate: attemptInstallByInfo,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default attemptInstallByInfoInjectable; export default attemptInstallByInfoInjectable;
@@ -35,7 +35,7 @@ export interface ExtensionInfo {
requireConfirmation?: boolean; requireConfirmation?: boolean;
} }
export interface Dependencies { interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>; attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>; getBaseRegistryUrl: () => Promise<string>;
} }
@@ -18,29 +18,21 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { ExtendableDisposer } from "../../../../common/utils";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
import type { Dependencies } from "./attempt-install";
import { attemptInstall } from "./attempt-install"; import { attemptInstall } from "./attempt-install";
import type { InstallRequest } from "./install-request";
import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable"; import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable";
const attemptInstallInjectable: Injectable< const attemptInstallInjectable = getInjectable({
(request: InstallRequest, d?: ExtendableDisposer) => Promise<void>, instantiate: (di) =>
Dependencies attemptInstall({
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
uninstallExtension: di.inject(uninstallExtensionInjectable), uninstallExtension: di.inject(uninstallExtensionInjectable),
unpackExtension: di.inject(unpackExtensionInjectable), unpackExtension: di.inject(unpackExtensionInjectable),
}), }),
instantiate: attemptInstall,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default attemptInstallInjectable; export default attemptInstallInjectable;
@@ -41,7 +41,7 @@ import {
import { getExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder"; import { getExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder";
import type { InstallRequest } from "./install-request"; import type { InstallRequest } from "./install-request";
export interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; extensionLoader: ExtensionLoader;
uninstallExtension: (id: LensExtensionId) => Promise<boolean>; uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
unpackExtension: ( unpackExtension: (
@@ -18,26 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { unpackExtension } from "./unpack-extension";
import { Dependencies, unpackExtension } from "./unpack-extension";
import type { InstallRequestValidated } from "../create-temp-files-and-validate/create-temp-files-and-validate";
import type { Disposer } from "../../../../../common/utils";
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
const unpackExtensionInjectable: Injectable< const unpackExtensionInjectable = getInjectable({
( instantiate: (di) =>
request: InstallRequestValidated, unpackExtension({
disposeDownloading?: Disposer,
) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
}), }),
instantiate: unpackExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default unpackExtensionInjectable; export default unpackExtensionInjectable;
@@ -32,7 +32,7 @@ import fse from "fs-extra";
import { when } from "mobx"; import { when } from "mobx";
import React from "react"; import React from "react";
export interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader extensionLoader: ExtensionLoader
} }
@@ -18,21 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { attemptInstalls } from "./attempt-installs";
import { attemptInstalls, Dependencies } from "./attempt-installs";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
const attemptInstallsInjectable: Injectable< const attemptInstallsInjectable = getInjectable({
(filePaths: string[]) => Promise<void>, instantiate: (di) =>
Dependencies attemptInstalls({
> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable), attemptInstall: di.inject(attemptInstallInjectable),
}), }),
instantiate: attemptInstalls,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default attemptInstallsInjectable; export default attemptInstallsInjectable;
@@ -22,7 +22,7 @@ import { readFileNotify } from "../read-file-notify/read-file-notify";
import path from "path"; import path from "path";
import type { InstallRequest } from "../attempt-install/install-request"; import type { InstallRequest } from "../attempt-install/install-request";
export interface Dependencies { interface Dependencies {
attemptInstall: (request: InstallRequest) => Promise<void>; attemptInstall: (request: InstallRequest) => Promise<void>;
} }
@@ -18,25 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { confirmUninstallExtension } from "./confirm-uninstall-extension";
import {
confirmUninstallExtension,
Dependencies,
} from "./confirm-uninstall-extension";
import type { InstalledExtension } from "../../../../extensions/extension-discovery";
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable"; import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
const confirmUninstallExtensionInjectable: Injectable< const confirmUninstallExtensionInjectable = getInjectable({
(extension: InstalledExtension) => Promise<void>, instantiate: (di) =>
Dependencies confirmUninstallExtension({
> = {
getDependencies: di => ({
uninstallExtension: di.inject(uninstallExtensionInjectable), uninstallExtension: di.inject(uninstallExtensionInjectable),
}), }),
instantiate: confirmUninstallExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default confirmUninstallExtensionInjectable; export default confirmUninstallExtensionInjectable;
@@ -24,8 +24,8 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { extensionDisplayName } from "../../../../extensions/lens-extension"; import { extensionDisplayName } from "../../../../extensions/lens-extension";
import { ConfirmDialog } from "../../confirm-dialog"; import { ConfirmDialog } from "../../confirm-dialog";
export interface Dependencies { interface Dependencies {
uninstallExtension: (id: LensExtensionId) => Promise<void>; uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
} }
export const confirmUninstallExtension = export const confirmUninstallExtension =
@@ -18,23 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension"; import { disableExtension } from "./disable-extension";
import { Dependencies, disableExtension } from "./disable-extension";
const disableExtensionInjectable: Injectable< const disableExtensionInjectable = getInjectable({
(id: LensExtensionId) => void, instantiate: (di) =>
Dependencies disableExtension({
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
}), }),
instantiate: disableExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default disableExtensionInjectable; export default disableExtensionInjectable;
@@ -21,7 +21,7 @@
import type { LensExtensionId } from "../../../../extensions/lens-extension"; import type { LensExtensionId } from "../../../../extensions/lens-extension";
import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import type { ExtensionLoader } from "../../../../extensions/extension-loader";
export interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; extensionLoader: ExtensionLoader;
} }
@@ -18,23 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension"; import { enableExtension } from "./enable-extension";
import { Dependencies, enableExtension } from "./enable-extension";
const enableExtensionInjectable: Injectable< const enableExtensionInjectable = getInjectable({
(id: LensExtensionId) => void, instantiate: (di) =>
Dependencies enableExtension({
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
}), }),
instantiate: enableExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default enableExtensionInjectable; export default enableExtensionInjectable;
@@ -21,7 +21,7 @@
import type { LensExtensionId } from "../../../../extensions/lens-extension"; import type { LensExtensionId } from "../../../../extensions/lens-extension";
import type { ExtensionLoader } from "../../../../extensions/extension-loader"; import type { ExtensionLoader } from "../../../../extensions/extension-loader";
export interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; extensionLoader: ExtensionLoader;
} }
@@ -19,18 +19,20 @@
* 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 { Injectable, lifecycleEnum } from "@ogre-tools/injectable"; import {
getInjectable,
lifecycleEnum,
} from "@ogre-tools/injectable";
import { UserStore } from "../../../../common/user-store"; import { UserStore } from "../../../../common/user-store";
import { Dependencies, getBaseRegistryUrl } from "./get-base-registry-url"; import { getBaseRegistryUrl } from "./get-base-registry-url";
const getBaseRegistryUrlInjectable: Injectable<() => Promise<string>, Dependencies> = { const getBaseRegistryUrlInjectable = getInjectable({
getDependencies: () => ({ instantiate: () => getBaseRegistryUrl({
// TODO: use injection // TODO: use injection
getRegistryUrlPreference: () => UserStore.getInstance().extensionRegistryUrl, getRegistryUrlPreference: () => UserStore.getInstance().extensionRegistryUrl,
}), }),
instantiate: getBaseRegistryUrl,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default getBaseRegistryUrlInjectable; export default getBaseRegistryUrlInjectable;
@@ -24,7 +24,7 @@ import { defaultExtensionRegistryUrl, ExtensionRegistry, ExtensionRegistryLocati
import { promiseExecFile } from "../../../utils"; import { promiseExecFile } from "../../../utils";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
export interface Dependencies { interface Dependencies {
getRegistryUrlPreference: () => ExtensionRegistry, getRegistryUrlPreference: () => ExtensionRegistry,
} }
@@ -18,25 +18,19 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import type { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable"; import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import type { Dependencies } from "./install-from-input";
import { installFromInput } from "./install-from-input"; import { installFromInput } from "./install-from-input";
import attemptInstallByInfoInjectable import attemptInstallByInfoInjectable from "../attempt-install-by-info/attempt-install-by-info.injectable";
from "../attempt-install-by-info/attempt-install-by-info.injectable";
const installFromInputInjectable: Injectable< const installFromInputInjectable = getInjectable({
(input: string) => Promise<void>, instantiate: (di) =>
Dependencies installFromInput({
> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable), attemptInstall: di.inject(attemptInstallInjectable),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable), attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
}), }),
instantiate: installFromInput,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default installFromInputInjectable; export default installFromInputInjectable;
@@ -30,7 +30,7 @@ import { readFileNotify } from "../read-file-notify/read-file-notify";
import type { InstallRequest } from "../attempt-install/install-request"; import type { InstallRequest } from "../attempt-install/install-request";
import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info"; import type { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info";
export interface Dependencies { interface Dependencies {
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>, attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => Promise<void>,
attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void> attemptInstallByInfo: (extensionInfo: ExtensionInfo) => Promise<void>
} }
@@ -18,18 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { installFromSelectFileDialog } from "./install-from-select-file-dialog";
import { Dependencies, installFromSelectFileDialog } from "./install-from-select-file-dialog";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable"; import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installFromSelectFileDialogInjectable: Injectable<() => Promise<void>, Dependencies> = { const installFromSelectFileDialogInjectable = getInjectable({
getDependencies: di => ({ instantiate: (di) =>
installFromSelectFileDialog({
attemptInstalls: di.inject(attemptInstallsInjectable), attemptInstalls: di.inject(attemptInstallsInjectable),
}), }),
instantiate: installFromSelectFileDialog,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default installFromSelectFileDialogInjectable; export default installFromSelectFileDialogInjectable;
@@ -22,7 +22,7 @@ import { dialog } from "../../../remote-helpers";
import { AppPaths } from "../../../../common/app-paths"; import { AppPaths } from "../../../../common/app-paths";
import { supportedExtensionFormats } from "../supported-extension-formats"; import { supportedExtensionFormats } from "../supported-extension-formats";
export interface Dependencies { interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void> attemptInstalls: (filePaths: string[]) => Promise<void>
} }
@@ -18,21 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { installOnDrop } from "./install-on-drop";
import { Dependencies, installOnDrop } from "./install-on-drop";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable"; import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installOnDropInjectable: Injectable< const installOnDropInjectable = getInjectable({
(files: File[]) => Promise<void>, instantiate: (di) =>
Dependencies installOnDrop({
> = {
getDependencies: di => ({
attemptInstalls: di.inject(attemptInstallsInjectable), attemptInstalls: di.inject(attemptInstallsInjectable),
}), }),
instantiate: installOnDrop,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default installOnDropInjectable; export default installOnDropInjectable;
@@ -20,7 +20,7 @@
*/ */
import logger from "../../../../main/logger"; import logger from "../../../../main/logger";
export interface Dependencies { interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>; attemptInstalls: (filePaths: string[]) => Promise<void>;
} }
@@ -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 styles from "./install.module.css"; import styles from "./install.module.scss";
import React from "react"; import React from "react";
import { prevDefault } from "../../utils"; import { prevDefault } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
@@ -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 styles from "./installed-extensions.module.css"; import styles from "./installed-extensions.module.scss";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery"; import { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
import { Icon } from "../icon"; import { Icon } from "../icon";
@@ -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 styles from "./notice.module.css"; import styles from "./notice.module.scss";
import React, { DOMAttributes } from "react"; import React, { DOMAttributes } from "react";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
@@ -18,23 +18,17 @@
* 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 { Injectable } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension"; import { uninstallExtension } from "./uninstall-extension";
import extensionLoaderInjectable
from "../../../../extensions/extension-loader/extension-loader.injectable";
import { Dependencies, uninstallExtension } from "./uninstall-extension";
const uninstallExtensionInjectable: Injectable< const uninstallExtensionInjectable = getInjectable({
(extensionId: LensExtensionId) => Promise<boolean>, instantiate: (di) =>
Dependencies uninstallExtension({
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
}), }),
instantiate: uninstallExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default uninstallExtensionInjectable; export default uninstallExtensionInjectable;
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff. Show more