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

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

This commit is contained in:
Jim Ehrismann 2022-01-05 15:13:24 -05:00 committed by GitHub
commit 9ebbe5b84f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
206 changed files with 2126 additions and 1217 deletions

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"],

View File

@ -37,9 +37,9 @@ export default class ExampleMainExtension extends Main.LensExtension {
} }
``` ```
### App Menus ### Menus
This extension can register custom app menus that will be displayed on OS native menus. This extension can register custom app and tray menus that will be displayed on OS native menus.
Example: Example:
@ -56,6 +56,29 @@ export default class ExampleMainExtension extends Main.LensExtension {
} }
} }
] ]
trayMenus = [
{
label: "My links",
submenu: [
{
label: "Lens",
click() {
Main.Navigation.navigate("https://k8slens.dev");
}
},
{
type: "separator"
},
{
label: "Lens Github",
click() {
Main.Navigation.navigate("https://github.com/lensapp/lens");
}
}
]
}
]
} }
``` ```

View File

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

36
extensions/.eslintrc.js Normal file
View File

@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
module.exports = {
"overrides": [
{
files: [
"**/*.ts",
"**/*.tsx",
],
rules: {
"import/no-unresolved": ["error", {
ignore: ["@k8slens/extensions"],
}],
},
},
],
};

View File

@ -62,8 +62,7 @@
}, },
"moduleNameMapper": { "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",

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -30,7 +30,15 @@ import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
import type { Disposer } from "../utils"; import type { Disposer } from "../utils";
import type remote from "@electron/remote"; import type remote from "@electron/remote";
const electronRemote = ipcMain ? null : require("@electron/remote"); const electronRemote = (() => {
if (ipcRenderer) {
try {
return require("@electron/remote");
} catch {}
}
return null;
})();
const subFramesChannel = "ipc:get-sub-frames"; const subFramesChannel = "ipc:get-sub-frames";

View File

@ -19,10 +19,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { CustomResourceDefinition } from "../endpoints"; import { CustomResourceDefinition, CustomResourceDefinitionSpec } from "../endpoints";
describe("Crds", () => { describe("Crds", () => {
describe("getVersion", () => { describe("getVersion()", () => {
it("should throw if none of the versions are served", () => { it("should throw if none of the versions are served", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "apiextensions.k8s.io/v1", apiVersion: "apiextensions.k8s.io/v1",
@ -136,7 +136,7 @@ describe("Crds", () => {
expect(crd.getVersion()).toBe("123"); expect(crd.getVersion()).toBe("123");
}); });
it("should get the version name from the version field", () => { it("should get the version name from the version field, ignoring versions on v1beta", () => {
const crd = new CustomResourceDefinition({ const crd = new CustomResourceDefinition({
apiVersion: "apiextensions.k8s.io/v1beta1", apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition", kind: "CustomResourceDefinition",
@ -147,7 +147,14 @@ describe("Crds", () => {
}, },
spec: { spec: {
version: "abc", version: "abc",
}, versions: [
{
name: "foobar",
served: true,
storage: true,
},
],
} as CustomResourceDefinitionSpec,
}); });
expect(crd.getVersion()).toBe("abc"); expect(crd.getVersion()).toBe("abc");

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -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({
> = { extensionLoader: di.inject(extensionLoaderInjectable),
getDependencies: di => ({ }),
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: dependencies => new LensProtocolRouterMain(dependencies),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default lensProtocolRouterMainInjectable; export default lensProtocolRouterMainInjectable;

View File

@ -51,7 +51,7 @@ function checkHost<Query>(url: URLParse<Query>): boolean {
} }
} }
export interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader extensionLoader: ExtensionLoader
} }

View File

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

View File

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

View File

@ -0,0 +1,136 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable";
import { LensMainExtension } from "../../extensions/lens-main-extension";
import trayItemsInjectable from "./tray-menu-items.injectable";
import type { IComputedValue } from "mobx";
import { computed, ObservableMap, runInAction } from "mobx";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
import type { TrayMenuRegistration } from "./tray-menu-registration";
describe("tray-menu-items", () => {
let di: ConfigurableDependencyInjectionContainer;
let trayMenuItems: IComputedValue<TrayMenuRegistration[]>;
let extensionsStub: ObservableMap<string, LensMainExtension>;
beforeEach(() => {
di = getDiForUnitTesting();
extensionsStub = new ObservableMap();
di.override(
mainExtensionsInjectable,
() => computed(() => [...extensionsStub.values()]),
);
trayMenuItems = di.inject(trayItemsInjectable);
});
it("does not have any items yet", () => {
expect(trayMenuItems.get()).toHaveLength(0);
});
describe("when extension is enabled", () => {
beforeEach(() => {
const someExtension = new SomeTestExtension({
id: "some-extension-id",
trayMenus: [{ label: "tray-menu-from-some-extension" }],
});
runInAction(() => {
extensionsStub.set("some-extension-id", someExtension);
});
});
it("has tray menu items", () => {
expect(trayMenuItems.get()).toEqual([
{
label: "tray-menu-from-some-extension",
},
]);
});
it("when disabling extension, does not have tray menu items", () => {
runInAction(() => {
extensionsStub.delete("some-extension-id");
});
expect(trayMenuItems.get()).toHaveLength(0);
});
describe("when other extension is enabled", () => {
beforeEach(() => {
const someOtherExtension = new SomeTestExtension({
id: "some-extension-id",
trayMenus: [{ label: "some-label-from-second-extension" }],
});
runInAction(() => {
extensionsStub.set("some-other-extension-id", someOtherExtension);
});
});
it("has tray menu items for both extensions", () => {
expect(trayMenuItems.get()).toEqual([
{
label: "tray-menu-from-some-extension",
},
{
label: "some-label-from-second-extension",
},
]);
});
it("when extension is disabled, still returns tray menu items for extensions that are enabled", () => {
runInAction(() => {
extensionsStub.delete("some-other-extension-id");
});
expect(trayMenuItems.get()).toEqual([
{
label: "tray-menu-from-some-extension",
},
]);
});
});
});
});
class SomeTestExtension extends LensMainExtension {
constructor({ id, trayMenus }: {
id: string;
trayMenus: TrayMenuRegistration[];
}) {
super({
id,
absolutePath: "irrelevant",
isBundled: false,
isCompatible: false,
isEnabled: false,
manifest: { name: id, version: "some-version" },
manifestPath: "irrelevant",
});
this.trayMenus = trayMenus;
}
}

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export interface TrayMenuRegistration {
label?: string;
click?: (menuItem: TrayMenuRegistration) => void;
id?: string;
type?: "normal" | "separator" | "submenu"
toolTip?: string;
enabled?: boolean;
submenu?: TrayMenuRegistration[]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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>

View File

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

View File

@ -60,15 +60,15 @@
flex-grow: 2; flex-grow: 2;
} }
.noIssues { .noIssues {
.ok-title { .Icon {
font-size: large; color: white;
color: var(--textColorAccent); }
font-weight: bold;
}
.allGood { .title {
color: white; font-size: large;
} color: var(--textColorAccent);
} font-weight: bold;
}
}
} }

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

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

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

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

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

View File

@ -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) =>
attemptInstall: di.inject(attemptInstallInjectable), attemptInstallByInfo({
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable), attemptInstall: di.inject(attemptInstallInjectable),
}), getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
}),
instantiate: attemptInstallByInfo,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default attemptInstallByInfoInjectable; export default attemptInstallByInfoInjectable;

View File

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

View File

@ -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({
> = { extensionLoader: di.inject(extensionLoaderInjectable),
getDependencies: di => ({ uninstallExtension: di.inject(uninstallExtensionInjectable),
extensionLoader: di.inject(extensionLoaderInjectable), unpackExtension: di.inject(unpackExtensionInjectable),
uninstallExtension: di.inject(uninstallExtensionInjectable), }),
unpackExtension: di.inject(unpackExtensionInjectable),
}),
instantiate: attemptInstall,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default attemptInstallInjectable; export default attemptInstallInjectable;

View File

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

View File

@ -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, extensionLoader: di.inject(extensionLoaderInjectable),
) => Promise<void>, }),
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: unpackExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default unpackExtensionInjectable; export default unpackExtensionInjectable;

View File

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

View File

@ -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({
> = { attemptInstall: di.inject(attemptInstallInjectable),
getDependencies: di => ({ }),
attemptInstall: di.inject(attemptInstallInjectable),
}),
instantiate: attemptInstalls,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default attemptInstallsInjectable; export default attemptInstallsInjectable;

View File

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

View File

@ -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({
> = { uninstallExtension: di.inject(uninstallExtensionInjectable),
getDependencies: di => ({ }),
uninstallExtension: di.inject(uninstallExtensionInjectable),
}),
instantiate: confirmUninstallExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default confirmUninstallExtensionInjectable; export default confirmUninstallExtensionInjectable;

View File

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

View File

@ -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({
> = { extensionLoader: di.inject(extensionLoaderInjectable),
getDependencies: di => ({ }),
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: disableExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default disableExtensionInjectable; export default disableExtensionInjectable;

View File

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

View File

@ -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({
> = { extensionLoader: di.inject(extensionLoaderInjectable),
getDependencies: di => ({ }),
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: enableExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default enableExtensionInjectable; export default enableExtensionInjectable;

View File

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

View File

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

View File

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

View File

@ -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({
> = { attemptInstall: di.inject(attemptInstallInjectable),
getDependencies: di => ({ attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
attemptInstall: di.inject(attemptInstallInjectable), }),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
}),
instantiate: installFromInput,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default installFromInputInjectable; export default installFromInputInjectable;

View File

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

View File

@ -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) =>
attemptInstalls: di.inject(attemptInstallsInjectable), installFromSelectFileDialog({
}), attemptInstalls: di.inject(attemptInstallsInjectable),
}),
instantiate: installFromSelectFileDialog,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default installFromSelectFileDialogInjectable; export default installFromSelectFileDialogInjectable;

View File

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

View File

@ -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({
> = { attemptInstalls: di.inject(attemptInstallsInjectable),
getDependencies: di => ({ }),
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
instantiate: installOnDrop,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default installOnDropInjectable; export default installOnDropInjectable;

View File

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

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

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

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

View File

@ -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({
> = { extensionLoader: di.inject(extensionLoaderInjectable),
getDependencies: di => ({ }),
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: uninstallExtension,
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,
}; });
export default uninstallExtensionInjectable; export default uninstallExtensionInjectable;

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