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",
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
plugins: [
"header",
@ -193,6 +195,8 @@ module.exports = {
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:import/recommended",
"plugin:import/typescript",
],
parserOptions: {
ecmaVersion: 2018,
@ -202,6 +206,7 @@ module.exports = {
rules: {
"no-irregular-whitespace": "error",
"header/header": [2, "./license-header"],
"react/prop-types": "off",
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off",
@ -246,7 +251,6 @@ module.exports = {
"objectsInObjects": false,
"arraysInObjects": true,
}],
"react/prop-types": "off",
"semi": "off",
"@typescript-eslint/semi": ["error"],
"linebreak-style": ["error", "unix"],

View File

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

View File

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

36
extensions/.eslintrc.js Normal file
View File

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

View File

@ -62,8 +62,7 @@
},
"moduleNameMapper": {
"\\.(css|scss)$": "<rootDir>/__mocks__/styleMock.ts",
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts",
"src/(.*)": "<rootDir>/__mocks__/windowMock.ts"
"\\.(svg)$": "<rootDir>/__mocks__/imageMock.ts"
},
"modulePathIgnorePatterns": [
"<rootDir>/dist",
@ -196,10 +195,11 @@
"@hapi/call": "^8.0.1",
"@hapi/subtext": "^7.0.3",
"@kubernetes/client-node": "^0.16.1",
"@ogre-tools/injectable": "1.5.0",
"@ogre-tools/injectable-react": "1.5.2",
"@ogre-tools/injectable": "2.0.0",
"@ogre-tools/injectable-react": "2.0.0",
"@sentry/electron": "^2.5.4",
"@sentry/integrations": "^6.15.0",
"@types/circular-dependency-plugin": "5.0.4",
"abort-controller": "^3.0.0",
"auto-bind": "^4.0.0",
"autobind-decorator": "^2.4.0",
@ -214,7 +214,7 @@
"filehound": "^1.17.5",
"fs-extra": "^9.0.1",
"glob-to-regexp": "^0.4.1",
"got": "^11.8.2",
"got": "^11.8.3",
"grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7",
"http-proxy": "^1.18.1",
@ -265,7 +265,7 @@
"ws": "^7.5.5"
},
"devDependencies": {
"@async-fn/jest": "^1.5.0",
"@async-fn/jest": "1.5.3",
"@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.60",
@ -333,7 +333,7 @@
"concurrently": "^5.3.0",
"css-loader": "^5.2.7",
"deepdash": "^5.3.9",
"dompurify": "^2.3.3",
"dompurify": "^2.3.4",
"electron": "^13.6.1",
"electron-builder": "^22.14.5",
"electron-notarize": "^0.3.0",
@ -341,6 +341,7 @@
"esbuild-loader": "^2.16.0",
"eslint": "^7.32.0",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-react": "^7.27.1",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unused-imports": "^1.1.5",
@ -361,8 +362,7 @@
"nodemon": "^2.0.14",
"playwright": "^1.17.1",
"postcss": "^8.4.5",
"postcss-loader": "4.3.0",
"postinstall-postinstall": "^2.1.0",
"postcss-loader": "^4.3.0",
"progress-bar-webpack-plugin": "^2.1.0",
"randomcolor": "^0.6.2",
"raw-loader": "^4.0.2",
@ -373,11 +373,11 @@
"react-select-event": "^5.1.0",
"react-table": "^7.7.0",
"react-window": "^1.8.6",
"sass": "^1.44.0",
"sass-loader": "^8.0.2",
"sass": "^1.45.1",
"sass-loader": "^10.2.0",
"sharp": "^0.29.3",
"style-loader": "^2.0.0",
"tailwindcss": "^2.2.19",
"tailwindcss": "^3.0.7",
"ts-jest": "26.5.6",
"ts-loader": "^7.0.5",
"ts-node": "^10.4.0",

View File

@ -20,9 +20,11 @@
*/
import { anyObject } from "jest-mock-extended";
import { merge } from "lodash";
import mockFs from "mock-fs";
import logger from "../../main/logger";
import { AppPaths } from "../app-paths";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
@ -54,68 +56,58 @@ jest.mock("../../main/catalog/catalog-entity-registry", () => ({
},
}));
const testCluster = {
uid: "test",
name: "test",
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return merge(data, {
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",
kind: "Cluster",
status: {
phase: "Running",
},
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {
uid: "test",
name: "test",
labels: {},
},
};
});
const minikubeCluster = {
uid: "minikube",
name: "minikube",
const minikubeCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {
uid: "minikube",
name: "minikube",
labels: {},
},
};
});
const awsCluster = {
uid: "aws",
name: "aws",
const awsCluster = getMockCatalogEntity({
apiVersion: "v1",
kind: "Cluster",
status: {
phase: "Running",
},
spec: {},
getName: jest.fn(),
getId: jest.fn(),
onDetailsOpen: jest.fn(),
onContextMenuOpen: jest.fn(),
onSettingsOpen: jest.fn(),
metadata: {
uid: "aws",
name: "aws",
labels: {},
},
};
});
jest.mock("electron", () => ({
app: {

View File

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

View File

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

View File

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

View File

@ -38,14 +38,44 @@ export type CatalogEntityConstructor<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;
/**
* The constructor for the entities.
*/
entityClass: CatalogEntityConstructor<Entity>;
}
export interface CatalogCategorySpec {
/**
* The grouping for for the category. This MUST be a DNS label.
*/
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>[];
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;
};
}
@ -114,6 +144,7 @@ export abstract class CatalogCategory extends (EventEmitter as new () => TypedEm
export interface CatalogEntityMetadata {
uid: string;
name: string;
shortName?: string;
description?: string;
source?: string;
labels: Record<string, string>;
@ -211,7 +242,14 @@ export abstract class CatalogEntity<
Status extends CatalogEntityStatus = CatalogEntityStatus,
Spec extends CatalogEntitySpec = CatalogEntitySpec,
> implements CatalogEntityKindData {
/**
* The group and version of this class.
*/
public abstract readonly apiVersion: string;
/**
* A DNS label name of the entity.
*/
public abstract readonly kind: string;
@observable metadata: Metadata;
@ -225,14 +263,35 @@ export abstract class CatalogEntity<
this.spec = data.spec;
}
/**
* Get the UID of this entity
*/
public getId(): string {
return this.metadata.uid;
}
/**
* Get the name of this entity
*/
public getName(): string {
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 onContextMenuOpen(context: CatalogEntityContextMenuContext): 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
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
module.exports = {
plugins: [
require("tailwindcss/nesting"),
require("tailwindcss"),
],
};
export const BundledExtensionsLoaded = "extension-loader:bundled-extensions-loaded";

View File

@ -27,3 +27,4 @@ export * from "./update-available.ipc";
export * from "./cluster.ipc";
export * from "./type-enforced-ipc";
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 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";

View File

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

View File

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

View File

@ -34,6 +34,9 @@ import type { IKubeWatchEvent } from "./kube-watch-api";
import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api";
import { noop } from "../utils";
import type { RequestInit } from "node-fetch";
// BUG: https://github.com/mysticatea/abort-controller/pull/22
// eslint-disable-next-line import/no-named-as-default
import AbortController from "abort-controller";
import { Agent, AgentOptions } from "https";
import type { Patch } from "rfc6902";

View File

@ -30,6 +30,9 @@ import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse";
import type { KubeJsonApiData } from "./kube-json-api";
import type { RequestInit } from "node-fetch";
// BUG: https://github.com/mysticatea/abort-controller/pull/22
// eslint-disable-next-line import/no-named-as-default
import AbortController from "abort-controller";
import type { Patch } from "rfc6902";

View File

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

View File

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

View File

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

View File

@ -18,14 +18,12 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader";
const extensionLoaderInjectable: Injectable<ExtensionLoader> = {
getDependencies: () => ({}),
const extensionLoaderInjectable = getInjectable({
instantiate: () => new ExtensionLoader(),
lifecycle: lifecycleEnum.singleton,
};
});
export default extensionLoaderInjectable;

View File

@ -255,17 +255,15 @@ export class ExtensionLoader {
loadOnClusterManagerRenderer() {
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
return this.autoInitExtensions(async (extension: LensRendererExtension) => {
const removeItems = [
registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension),
registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences),
registries.EntitySettingRegistry.getInstance().add(extension.entitySettings),
registries.StatusBarRegistry.getInstance().add(extension.statusBarItems),
registries.CommandRegistry.getInstance().add(extension.commands),
registries.WelcomeMenuRegistry.getInstance().add(extension.welcomeMenus),
registries.WelcomeBannerRegistry.getInstance().add(extension.welcomeBanners),
registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems),
registries.TopBarRegistry.getInstance().add(extension.topBarItems),
];
this.events.on("remove", (removedExtension: LensRendererExtension) => {
@ -311,7 +309,9 @@ export class ExtensionLoader {
}
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) {
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
@ -326,7 +326,14 @@ export class ExtensionLoader {
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);
} catch (err) {
logger.error(`${logModule}: activation extension error`, { ext: extension, err });
@ -338,6 +345,8 @@ export class ExtensionLoader {
}, {
fireImmediately: true,
});
return loadingExtensions;
}
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
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { LensExtension } from "./lens-extension";
import type { ExtensionLoader } from "./extension-loader";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import extensionLoaderInjectable from "./extension-loader/extension-loader.injectable";
const extensionsInjectable: Injectable<
IComputedValue<LensExtension[]>,
{ extensionLoader: ExtensionLoader }
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
const extensionsInjectable = getInjectable({
instantiate: (di) => {
const extensionLoader = di.inject(extensionLoaderInjectable);
return computed(() => extensionLoader.enabledExtensionInstances);
},
lifecycle: lifecycleEnum.singleton,
instantiate: ({ extensionLoader }) =>
computed(() => extensionLoader.enabledExtensionInstances),
};
});
export default extensionsInjectable;

View File

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

View File

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

View File

@ -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
import { action, observable, makeObservable } from "mobx";
import { Singleton } from "../../common/utils";
import { LensExtension } from "../lens-extension";
import type { LensExtension } from "../lens-extension";
export class BaseRegistry<T, I = T> extends Singleton {
private items = observable.map<T, I>([], { deep: false });

View File

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

View File

@ -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 { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
import { once } from "lodash";
import { ipcMain } from "electron";
import { ipcMain, autoUpdater as electronAutoUpdater } from "electron";
import { nextUpdateChannel } from "./utils/update-channel";
import { UserStore } from "../common/user-store";
import { autoUpdater as electronAutoUpdater } from "electron";
let installVersion: null | string = null;

View File

@ -19,7 +19,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import * as tempy from "tempy";
import tempy from "tempy";
import fse from "fs-extra";
import * as yaml from "js-yaml";
import { promiseExecFile } from "../../common/utils/promise-exec";
@ -43,6 +43,7 @@ async function execHelm(args: string[], options?: BaseEncodingOptions & ExecFile
export async function listReleases(pathToKubeconfig: string, namespace?: string): Promise<Record<string, any>[]> {
const args = [
"ls",
"--all",
"--output", "json",
];

View File

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

View File

@ -18,24 +18,19 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Injectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed, IComputedValue } from "mobx";
import type { LensMainExtension } from "../../extensions/lens-main-extension";
import extensionsInjectable from "../../extensions/extensions.injectable";
import type { MenuRegistration } from "./menu-registration";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { computed } from "mobx";
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
const electronMenuItemsInjectable: Injectable<
IComputedValue<MenuRegistration[]>,
{ extensions: IComputedValue<LensMainExtension[]> }
> = {
const electronMenuItemsInjectable = getInjectable({
lifecycle: lifecycleEnum.singleton,
getDependencies: di => ({
extensions: di.inject(extensionsInjectable),
}),
instantiate: (di) => {
const extensions = di.inject(mainExtensionsInjectable);
instantiate: ({ extensions }) =>
computed(() => extensions.get().flatMap(extension => extension.appMenus)),
};
return computed(() =>
extensions.get().flatMap((extension) => extension.appMenus));
},
});
export default electronMenuItemsInjectable;

View File

@ -24,8 +24,8 @@ import electronMenuItemsInjectable from "./electron-menu-items.injectable";
import type { IComputedValue } from "mobx";
import { computed, ObservableMap, runInAction } from "mobx";
import type { MenuRegistration } from "./menu-registration";
import extensionsInjectable from "../../extensions/extensions.injectable";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import mainExtensionsInjectable from "../../extensions/main-extensions.injectable";
describe("electron-menu-items", () => {
let di: ConfigurableDependencyInjectionContainer;
@ -38,8 +38,8 @@ describe("electron-menu-items", () => {
extensionsStub = new ObservableMap();
di.override(
extensionsInjectable,
computed(() => [...extensionsStub.values()]),
mainExtensionsInjectable,
() => computed(() => [...extensionsStub.values()]),
);
electronMenuItems = di.inject(electronMenuItemsInjectable);

View File

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

View File

@ -18,23 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/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";
const lensProtocolRouterMainInjectable: Injectable<
LensProtocolRouterMain,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: dependencies => new LensProtocolRouterMain(dependencies),
const lensProtocolRouterMainInjectable = getInjectable({
instantiate: (di) =>
new LensProtocolRouterMain({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
lifecycle: lifecycleEnum.singleton,
};
});
export default lensProtocolRouterMainInjectable;

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ import { makeObservable, observable } from "mobx";
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron";
import windowStateKeeper from "electron-window-state";
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 { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
@ -181,7 +181,7 @@ export class WindowManager extends Singleton {
if (!this.mainWindow) {
viewHasLoaded = new Promise<void>(resolve => {
ipcMain.once(IpcRendererNavigationEvents.LOADED, () => resolve());
ipcMain.once(BundledExtensionsLoaded, () => resolve());
});
await this.initMainWindow(showSplash);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@
border-radius: var(--border-radius);
}
:global(.theme-light) {
@include theme-light {
.editor {
border-color: var(--borderFaintColor);
}

View File

@ -19,7 +19,7 @@
* 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 fse from "fs-extra";

View File

@ -19,7 +19,7 @@
* 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 { observer } from "mobx-react";
import { Drawer, DrawerItem } from "../drawer";
@ -27,14 +27,15 @@ import type { CatalogCategory, CatalogEntity } from "../../../common/catalog";
import { Icon } from "../icon";
import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { isDevelopment } from "../../../common/vars";
import { cssNames } from "../../utils";
import { Avatar } from "../avatar";
import { getLabelBadges } from "./helpers";
interface Props<T extends CatalogEntity> {
item: CatalogEntityItem<T> | null | undefined;
entity: T;
hideDetails(): void;
onRun: () => void;
}
@observer
@ -47,32 +48,30 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
}
}
renderContent(item: CatalogEntityItem<T>) {
const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(item.kind, item.apiVersion);
const details = detailItems.map(({ components }, index) => {
return <components.Details entity={item.entity} key={index}/>;
});
const showDetails = detailItems.find((item) => item.priority > 999) === undefined;
renderContent(entity: T) {
const { onRun, hideDetails } = this.props;
const detailItems = CatalogEntityDetailRegistry.getInstance().getItemsForKind(entity.kind, entity.apiVersion);
const details = detailItems.map(({ components }, index) => <components.Details entity={entity} key={index} />);
const showDefaultDetails = detailItems.find((item) => item.priority > 999) === undefined;
return (
<>
{showDetails && (
{showDefaultDetails && (
<div className="flex">
<div className={styles.entityIcon}>
<Avatar
title={item.name}
colorHash={`${item.name}-${item.source}`}
title={entity.getName()}
colorHash={`${entity.getName()}-${entity.getSource()}`}
size={128}
src={item.entity.spec.icon?.src}
src={entity.spec.icon?.src}
data-testid="detail-panel-hot-bar-icon"
background={item.entity.spec.icon?.background}
onClick={() => item.onRun()}
background={entity.spec.icon?.background}
onClick={onRun}
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>
{item?.enabled && (
{entity.isEnabled() && (
<div className={styles.hint}>
Click to open
</div>
@ -80,23 +79,23 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
</div>
<div className={cssNames("box grow", styles.metadata)}>
<DrawerItem name="Name">
{item.name}
{entity.getName()}
</DrawerItem>
<DrawerItem name="Kind">
{item.kind}
{entity.kind}
</DrawerItem>
<DrawerItem name="Source">
{item.source}
{entity.getSource()}
</DrawerItem>
<DrawerItem name="Status">
{item.phase}
{entity.status.phase}
</DrawerItem>
<DrawerItem name="Labels">
{...item.getLabelBadges(this.props.hideDetails)}
{getLabelBadges(entity, hideDetails)}
</DrawerItem>
{isDevelopment && (
<DrawerItem name="Id">
{item.getId()}
{entity.getId()}
</DrawerItem>
)}
</div>
@ -110,19 +109,18 @@ export class CatalogEntityDetails<T extends CatalogEntity> extends Component<Pro
}
render() {
const { item, hideDetails } = this.props;
const title = `${item.kind}: ${item.name}`;
const { entity, hideDetails } = this.props;
return (
<Drawer
className={styles.entityDetails}
usePortal={true}
open={true}
title={title}
toolbar={<CatalogEntityDrawerMenu item={item} key={item.getId()} />}
title={`${entity.kind}: ${entity.getName()}`}
toolbar={<CatalogEntityDrawerMenu entity={entity} key={entity.getId()} />}
onClose={hideDetails}
>
{item && this.renderContent(item)}
{this.renderContent(entity)}
</Drawer>
);
}

View File

@ -29,11 +29,10 @@ import { navigate } from "../../navigation";
import { MenuItem } from "../menu";
import { ConfirmDialog } from "../confirm-dialog";
import { Icon } from "../icon";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
export interface CatalogEntityDrawerMenuProps<T extends CatalogEntity> extends MenuActionsProps {
item: CatalogEntityItem<T> | null | undefined;
entity: T;
}
@observer
@ -50,7 +49,7 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
menuItems: [],
navigate: (url: string) => navigate(url),
};
this.props.item?.onContextMenuOpen(this.contextMenu);
this.props.entity?.onContextMenuOpen(this.contextMenu);
}
onMenuItemClick(menuItem: CatalogEntityContextMenu) {
@ -108,9 +107,9 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
}
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;
}
@ -120,7 +119,7 @@ export class CatalogEntityDrawerMenu<T extends CatalogEntity> extends React.Comp
toolbar
{...menuProps}
>
{this.getMenuItems(entity.entity)}
{this.getMenuItems(entity)}
</MenuActions>
);
}

View File

@ -18,7 +18,7 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import styles from "./catalog.module.css";
import styles from "./catalog.module.scss";
import React from "react";
import { action, computed } from "mobx";
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 { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog";
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) {
super();
makeObservable(this);
@ -39,10 +38,10 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem<CatalogEntit
@computed get entities() {
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() {
@ -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
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.
*/
import treeStyles from "./catalog-tree.module.css";
import styles from "./catalog-menu.module.css";
import treeStyles from "./catalog-tree.module.scss";
import styles from "./catalog-menu.module.scss";
import React from "react";
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 { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry";
import { CatalogEntityDetailRegistry } from "../../../extensions/registries";
import { CatalogEntityItem } from "./catalog-entity-item";
import { CatalogEntityStore } from "./catalog-entity.store";
import { AppPaths } from "../../../common/app-paths";
@ -130,7 +129,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
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
jest
@ -166,7 +165,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
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
jest
@ -200,7 +199,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
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
jest
@ -235,7 +234,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
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
jest
@ -265,7 +264,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
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
jest
@ -302,7 +301,7 @@ describe("<Catalog />", () => {
const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry);
const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry);
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
jest

View File

@ -19,14 +19,13 @@
* 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 { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list";
import { action, makeObservable, observable, reaction, runInAction, when } from "mobx";
import { CatalogEntityStore } from "./catalog-entity.store";
import type { CatalogEntityItem } from "./catalog-entity-item";
import { navigate } from "../../navigation";
import { MenuItem, MenuActions } from "../menu";
import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity";
@ -45,6 +44,8 @@ import { RenderDelay } from "../render-delay/render-delay";
import { Icon } from "../icon";
import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item";
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);
@ -125,19 +126,19 @@ export class Catalog extends React.Component<Props> {
}));
}
addToHotbar(item: CatalogEntityItem<CatalogEntity>): void {
HotbarStore.getInstance().addToHotbar(item.entity);
addToHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().addToHotbar(entity);
}
removeFromHotbar(item: CatalogEntityItem<CatalogEntity>): void {
HotbarStore.getInstance().removeFromHotbar(item.getId());
removeFromHotbar(entity: CatalogEntity): void {
HotbarStore.getInstance().removeFromHotbar(entity.getId());
}
onDetails = (item: CatalogEntityItem<CatalogEntity>) => {
onDetails = (entity: CatalogEntity) => {
if (this.catalogEntityStore.selectedItemId) {
this.catalogEntityStore.selectedItemId = null;
} 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 = () => {
this.contextMenu.menuItems = [];
item.onContextMenuOpen(this.contextMenu);
entity.onContextMenuOpen(this.contextMenu);
};
return (
<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
</MenuItem>
{
@ -200,7 +201,7 @@ export class Catalog extends React.Component<Props> {
}
<HotbarToggleMenuItem
key="hotbar-toggle"
entity={item.entity}
entity={entity}
addContent="Add to Hotbar"
removeContent="Remove from Hotbar"
/>
@ -208,29 +209,29 @@ export class Catalog extends React.Component<Props> {
);
};
renderName(item: CatalogEntityItem<CatalogEntity>) {
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(item.entity);
renderName(entity: CatalogEntity) {
const isItemInHotbar = HotbarStore.getInstance().isAddedToActive(entity);
return (
<>
<Avatar
title={item.getName()}
colorHash={`${item.getName()}-${item.source}`}
src={item.entity.spec.icon?.src}
background={item.entity.spec.icon?.background}
title={entity.getName()}
colorHash={`${entity.getName()}-${entity.getSource()}`}
src={entity.spec.icon?.src}
background={entity.spec.icon?.background}
className={styles.catalogAvatar}
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>
<span>{item.name}</span>
<span>{entity.getName()}</span>
<Icon
small
className={styles.pinIcon}
material={!isItemInHotbar && "push_pin"}
svg={isItemInHotbar ? "push_off" : "push_pin"}
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}
store={this.catalogEntityStore}
sortingCallbacks={{
[sortBy.name]: item => item.name,
[sortBy.source]: item => item.source,
[sortBy.status]: item => item.phase,
[sortBy.kind]: item => item.kind,
[sortBy.name]: entity => entity.getName(),
[sortBy.source]: entity => entity.getSource(),
[sortBy.status]: entity => entity.status.phase,
[sortBy.kind]: entity => entity.kind,
}}
searchFilters={[
entity => entity.searchFields,
entity => [
entity.getName(),
entity.getId(),
entity.status.phase,
`source=${entity.getSource()}`,
...KubeObject.stringifyLabels(entity.metadata.labels),
],
]}
renderTableHeader={[
{ 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: "Status", className: styles.statusCell, sortBy: sortBy.status, id: "status" },
].filter(Boolean)}
customizeTableRowProps={item => ({
disabled: !item.enabled,
customizeTableRowProps={entity => ({
disabled: !entity.isEnabled(),
})}
renderTableContents={item => [
this.renderName(item),
!activeCategory && item.kind,
item.source,
item.getLabelBadges(),
<span key="phase" className={item.phase}>{item.phase}</span>,
renderTableContents={entity => [
this.renderName(entity),
!activeCategory && entity.kind,
entity.getSource(),
getLabelBadges(entity),
<span key="phase" className={entity.status.phase}>{entity.status.phase}</span>,
].filter(Boolean)}
onDetails={this.onDetails}
renderItemMenu={this.renderItemMenu}
@ -289,16 +296,19 @@ export class Catalog extends React.Component<Props> {
return null;
}
const selectedEntity = this.catalogEntityStore.selectedItem;
return (
<MainLayout sidebar={this.renderNavigation()}>
<div className="p-6 h-full">
{this.renderList()}
</div>
{
this.catalogEntityStore.selectedItem
selectedEntity
? <CatalogEntityDetails
item={this.catalogEntityStore.selectedItem}
entity={selectedEntity}
hideDetails={() => this.catalogEntityStore.selectedItemId = null}
onRun={() => this.catalogEntityStore.onRun(selectedEntity)}
/>
: (
<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;
}
.noIssues {
.ok-title {
font-size: large;
color: var(--textColorAccent);
font-weight: bold;
}
.noIssues {
.Icon {
color: white;
}
.allGood {
color: white;
}
}
.title {
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.
*/
import styles from "./cluster-issues.module.css";
import styles from "./cluster-issues.module.scss";
import React from "react";
import { observer } from "mobx-react";
@ -149,9 +149,9 @@ export class ClusterIssues extends React.Component<Props> {
if (!warnings.length) {
return (
<div className={cssNames(styles.noIssues, "flex column box grow gaps align-center justify-center")}>
<div><Icon className={styles.allGood} material="check" big sticker/></div>
<div className="ok-title">No issues found</div>
<span>Everything is fine in the Cluster</span>
<Icon className={styles.Icon} material="check" big sticker/>
<p className={styles.title}>No issues found</p>
<p>Everything is fine in the Cluster</p>
</div>
);
}

View File

@ -19,7 +19,7 @@
* 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 { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* 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 { reaction } from "mobx";

View File

@ -19,7 +19,7 @@
* 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 { observer } from "mobx-react";

View File

@ -19,7 +19,7 @@
* 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 { observable, makeObservable } from "mobx";

View File

@ -18,20 +18,19 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstallByInfo, ExtensionInfo } from "./attempt-install-by-info";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstallByInfo } from "./attempt-install-by-info";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import getBaseRegistryUrlInjectable from "../get-base-registry-url/get-base-registry-url.injectable";
const attemptInstallByInfoInjectable: Injectable<(extensionInfo: ExtensionInfo) => Promise<void>, {}> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable),
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
}),
const attemptInstallByInfoInjectable = getInjectable({
instantiate: (di) =>
attemptInstallByInfo({
attemptInstall: di.inject(attemptInstallInjectable),
getBaseRegistryUrl: di.inject(getBaseRegistryUrlInjectable),
}),
instantiate: attemptInstallByInfo,
lifecycle: lifecycleEnum.singleton,
};
});
export default attemptInstallByInfoInjectable;

View File

@ -35,7 +35,7 @@ export interface ExtensionInfo {
requireConfirmation?: boolean;
}
export interface Dependencies {
interface Dependencies {
attemptInstall: (request: InstallRequest, d: ExtendableDisposer) => Promise<void>;
getBaseRegistryUrl: () => Promise<string>;
}

View File

@ -18,29 +18,21 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { ExtendableDisposer } from "../../../../common/utils";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
import type { Dependencies } from "./attempt-install";
import { attemptInstall } from "./attempt-install";
import type { InstallRequest } from "./install-request";
import unpackExtensionInjectable from "./unpack-extension/unpack-extension.injectable";
const attemptInstallInjectable: Injectable<
(request: InstallRequest, d?: ExtendableDisposer) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
uninstallExtension: di.inject(uninstallExtensionInjectable),
unpackExtension: di.inject(unpackExtensionInjectable),
}),
const attemptInstallInjectable = getInjectable({
instantiate: (di) =>
attemptInstall({
extensionLoader: di.inject(extensionLoaderInjectable),
uninstallExtension: di.inject(uninstallExtensionInjectable),
unpackExtension: di.inject(unpackExtensionInjectable),
}),
instantiate: attemptInstall,
lifecycle: lifecycleEnum.singleton,
};
});
export default attemptInstallInjectable;

View File

@ -41,7 +41,7 @@ import {
import { getExtensionDestFolder } from "./get-extension-dest-folder/get-extension-dest-folder";
import type { InstallRequest } from "./install-request";
export interface Dependencies {
interface Dependencies {
extensionLoader: ExtensionLoader;
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
unpackExtension: (

View File

@ -18,26 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
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 { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { unpackExtension } from "./unpack-extension";
import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable";
const unpackExtensionInjectable: Injectable<
(
request: InstallRequestValidated,
disposeDownloading?: Disposer,
) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
const unpackExtensionInjectable = getInjectable({
instantiate: (di) =>
unpackExtension({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: unpackExtension,
lifecycle: lifecycleEnum.singleton,
};
});
export default unpackExtensionInjectable;

View File

@ -32,7 +32,7 @@ import fse from "fs-extra";
import { when } from "mobx";
import React from "react";
export interface Dependencies {
interface Dependencies {
extensionLoader: ExtensionLoader
}

View File

@ -18,21 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstalls, Dependencies } from "./attempt-installs";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { attemptInstalls } from "./attempt-installs";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
const attemptInstallsInjectable: Injectable<
(filePaths: string[]) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable),
}),
const attemptInstallsInjectable = getInjectable({
instantiate: (di) =>
attemptInstalls({
attemptInstall: di.inject(attemptInstallInjectable),
}),
instantiate: attemptInstalls,
lifecycle: lifecycleEnum.singleton,
};
});
export default attemptInstallsInjectable;

View File

@ -22,7 +22,7 @@ import { readFileNotify } from "../read-file-notify/read-file-notify";
import path from "path";
import type { InstallRequest } from "../attempt-install/install-request";
export interface Dependencies {
interface Dependencies {
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
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import {
confirmUninstallExtension,
Dependencies,
} from "./confirm-uninstall-extension";
import type { InstalledExtension } from "../../../../extensions/extension-discovery";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { confirmUninstallExtension } from "./confirm-uninstall-extension";
import uninstallExtensionInjectable from "../uninstall-extension/uninstall-extension.injectable";
const confirmUninstallExtensionInjectable: Injectable<
(extension: InstalledExtension) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
uninstallExtension: di.inject(uninstallExtensionInjectable),
}),
const confirmUninstallExtensionInjectable = getInjectable({
instantiate: (di) =>
confirmUninstallExtension({
uninstallExtension: di.inject(uninstallExtensionInjectable),
}),
instantiate: confirmUninstallExtension,
lifecycle: lifecycleEnum.singleton,
};
});
export default confirmUninstallExtensionInjectable;

View File

@ -24,8 +24,8 @@ import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { extensionDisplayName } from "../../../../extensions/lens-extension";
import { ConfirmDialog } from "../../confirm-dialog";
export interface Dependencies {
uninstallExtension: (id: LensExtensionId) => Promise<void>;
interface Dependencies {
uninstallExtension: (id: LensExtensionId) => Promise<boolean>;
}
export const confirmUninstallExtension =

View File

@ -18,23 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { Dependencies, disableExtension } from "./disable-extension";
import { disableExtension } from "./disable-extension";
const disableExtensionInjectable: Injectable<
(id: LensExtensionId) => void,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: disableExtension,
const disableExtensionInjectable = getInjectable({
instantiate: (di) =>
disableExtension({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
lifecycle: lifecycleEnum.singleton,
};
});
export default disableExtensionInjectable;

View File

@ -21,7 +21,7 @@
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
export interface Dependencies {
interface Dependencies {
extensionLoader: ExtensionLoader;
}

View File

@ -18,23 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import { Dependencies, enableExtension } from "./enable-extension";
import { enableExtension } from "./enable-extension";
const enableExtensionInjectable: Injectable<
(id: LensExtensionId) => void,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: enableExtension,
const enableExtensionInjectable = getInjectable({
instantiate: (di) =>
enableExtension({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
lifecycle: lifecycleEnum.singleton,
};
});
export default enableExtensionInjectable;

View File

@ -21,7 +21,7 @@
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
export interface Dependencies {
interface Dependencies {
extensionLoader: ExtensionLoader;
}

View File

@ -19,18 +19,20 @@
* 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 { Dependencies, getBaseRegistryUrl } from "./get-base-registry-url";
import { getBaseRegistryUrl } from "./get-base-registry-url";
const getBaseRegistryUrlInjectable: Injectable<() => Promise<string>, Dependencies> = {
getDependencies: () => ({
const getBaseRegistryUrlInjectable = getInjectable({
instantiate: () => getBaseRegistryUrl({
// TODO: use injection
getRegistryUrlPreference: () => UserStore.getInstance().extensionRegistryUrl,
}),
instantiate: getBaseRegistryUrl,
lifecycle: lifecycleEnum.singleton,
};
});
export default getBaseRegistryUrlInjectable;

View File

@ -24,7 +24,7 @@ import { defaultExtensionRegistryUrl, ExtensionRegistry, ExtensionRegistryLocati
import { promiseExecFile } from "../../../utils";
import { Notifications } from "../../notifications";
export interface Dependencies {
interface Dependencies {
getRegistryUrlPreference: () => ExtensionRegistry,
}

View File

@ -18,25 +18,19 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import attemptInstallInjectable from "../attempt-install/attempt-install.injectable";
import type { Dependencies } from "./install-from-input";
import { installFromInput } from "./install-from-input";
import attemptInstallByInfoInjectable
from "../attempt-install-by-info/attempt-install-by-info.injectable";
import attemptInstallByInfoInjectable from "../attempt-install-by-info/attempt-install-by-info.injectable";
const installFromInputInjectable: Injectable<
(input: string) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
attemptInstall: di.inject(attemptInstallInjectable),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
}),
const installFromInputInjectable = getInjectable({
instantiate: (di) =>
installFromInput({
attemptInstall: di.inject(attemptInstallInjectable),
attemptInstallByInfo: di.inject(attemptInstallByInfoInjectable),
}),
instantiate: installFromInput,
lifecycle: lifecycleEnum.singleton,
};
});
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 { ExtensionInfo } from "../attempt-install-by-info/attempt-install-by-info";
export interface Dependencies {
interface Dependencies {
attemptInstall: (request: InstallRequest, disposer?: ExtendableDisposer) => 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
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { Dependencies, installFromSelectFileDialog } from "./install-from-select-file-dialog";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { installFromSelectFileDialog } from "./install-from-select-file-dialog";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installFromSelectFileDialogInjectable: Injectable<() => Promise<void>, Dependencies> = {
getDependencies: di => ({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
instantiate: installFromSelectFileDialog,
const installFromSelectFileDialogInjectable = getInjectable({
instantiate: (di) =>
installFromSelectFileDialog({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
lifecycle: lifecycleEnum.singleton,
};
});
export default installFromSelectFileDialogInjectable;

View File

@ -22,7 +22,7 @@ import { dialog } from "../../../remote-helpers";
import { AppPaths } from "../../../../common/app-paths";
import { supportedExtensionFormats } from "../supported-extension-formats";
export interface Dependencies {
interface Dependencies {
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
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import { Dependencies, installOnDrop } from "./install-on-drop";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { installOnDrop } from "./install-on-drop";
import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable";
const installOnDropInjectable: Injectable<
(files: File[]) => Promise<void>,
Dependencies
> = {
getDependencies: di => ({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
const installOnDropInjectable = getInjectable({
instantiate: (di) =>
installOnDrop({
attemptInstalls: di.inject(attemptInstallsInjectable),
}),
instantiate: installOnDrop,
lifecycle: lifecycleEnum.singleton,
};
});
export default installOnDropInjectable;

View File

@ -20,7 +20,7 @@
*/
import logger from "../../../../main/logger";
export interface Dependencies {
interface Dependencies {
attemptInstalls: (filePaths: string[]) => Promise<void>;
}

View File

@ -19,7 +19,7 @@
* 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 { prevDefault } from "../../utils";
import { Button } from "../button";

View File

@ -19,7 +19,7 @@
* 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 { ExtensionDiscovery, InstalledExtension } from "../../../extensions/extension-discovery";
import { Icon } from "../icon";

View File

@ -19,7 +19,7 @@
* 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 { cssNames } from "../../utils";

View File

@ -18,23 +18,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Injectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable";
import type { LensExtensionId } from "../../../../extensions/lens-extension";
import extensionLoaderInjectable
from "../../../../extensions/extension-loader/extension-loader.injectable";
import { Dependencies, uninstallExtension } from "./uninstall-extension";
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable";
import { uninstallExtension } from "./uninstall-extension";
const uninstallExtensionInjectable: Injectable<
(extensionId: LensExtensionId) => Promise<boolean>,
Dependencies
> = {
getDependencies: di => ({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
const uninstallExtensionInjectable = getInjectable({
instantiate: (di) =>
uninstallExtension({
extensionLoader: di.inject(extensionLoaderInjectable),
}),
instantiate: uninstallExtension,
lifecycle: lifecycleEnum.singleton,
};
});
export default uninstallExtensionInjectable;

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