diff --git a/.eslintrc.js b/.eslintrc.js index 1415136093..514ab56bd7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -54,6 +54,7 @@ module.exports = { "react-hooks", ], rules: { + "no-constant-condition": ["error", { "checkLoops": false }], "header/header": [2, "./license-header"], "comma-dangle": ["error", "always-multiline"], "comma-spacing": "error", @@ -107,7 +108,10 @@ module.exports = { ], parser: "@typescript-eslint/parser", extends: [ + "eslint:recommended", "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", ], plugins: [ "header", @@ -118,7 +122,7 @@ module.exports = { sourceType: "module", }, rules: { - "no-irregular-whitespace": "error", + "no-constant-condition": ["error", { "checkLoops": false }], "header/header": [2, "./license-header"], "no-invalid-this": "off", "@typescript-eslint/no-invalid-this": ["error"], @@ -191,8 +195,11 @@ module.exports = { "unused-imports", ], extends: [ + "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", + "plugin:import/recommended", + "plugin:import/typescript", ], parserOptions: { ecmaVersion: 2018, @@ -200,8 +207,9 @@ module.exports = { jsx: true, }, rules: { - "no-irregular-whitespace": "error", + "no-constant-condition": ["error", { "checkLoops": false }], "header/header": [2, "./license-header"], + "react/prop-types": "off", "no-invalid-this": "off", "@typescript-eslint/no-invalid-this": ["error"], "@typescript-eslint/explicit-function-return-type": "off", @@ -246,7 +254,6 @@ module.exports = { "objectsInObjects": false, "arraysInObjects": true, }], - "react/prop-types": "off", "semi": "off", "@typescript-eslint/semi": ["error"], "linebreak-style": ["error", "unix"], diff --git a/docs/extensions/capabilities/common-capabilities.md b/docs/extensions/capabilities/common-capabilities.md index 8ba03253c5..e7b585af66 100644 --- a/docs/extensions/capabilities/common-capabilities.md +++ b/docs/extensions/capabilities/common-capabilities.md @@ -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"); + } + } + ] + } + ] } ``` diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md index fec937db17..a0e20880bf 100644 --- a/docs/extensions/guides/main-extension.md +++ b/docs/extensions/guides/main-extension.md @@ -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). diff --git a/src/renderer/initializers/welcome-menu-registry.ts b/extensions/.eslintrc.js similarity index 71% rename from src/renderer/initializers/welcome-menu-registry.ts rename to extensions/.eslintrc.js index f658fe5715..d0daadb1aa 100644 --- a/src/renderer/initializers/welcome-menu-registry.ts +++ b/extensions/.eslintrc.js @@ -19,17 +19,18 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { catalogURL } from "../../common/routes"; -import { WelcomeMenuRegistry } from "../../extensions/registries"; -import { navigate } from "../navigation"; - -export function initWelcomeMenuRegistry() { - WelcomeMenuRegistry.getInstance() - .add([ - { - title: "Browse Clusters in Catalog", - icon: "view_list", - click: () => navigate(catalogURL({ params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }} )), +module.exports = { + "overrides": [ + { + files: [ + "**/*.ts", + "**/*.tsx", + ], + rules: { + "import/no-unresolved": ["error", { + ignore: ["@k8slens/extensions"], + }], }, - ]); -} + }, + ], +}; diff --git a/package.json b/package.json index b3fa600b11..43497e5edd 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,7 @@ }, "moduleNameMapper": { "\\.(css|scss)$": "/__mocks__/styleMock.ts", - "\\.(svg)$": "/__mocks__/imageMock.ts", - "src/(.*)": "/__mocks__/windowMock.ts" + "\\.(svg)$": "/__mocks__/imageMock.ts" }, "modulePathIgnorePatterns": [ "/dist", @@ -200,6 +199,7 @@ "@ogre-tools/injectable-react": "2.0.0", "@sentry/electron": "^2.5.4", "@sentry/integrations": "^6.15.0", + "@types/circular-dependency-plugin": "5.0.4", "abort-controller": "^3.0.0", "auto-bind": "^4.0.0", "autobind-decorator": "^2.4.0", @@ -214,7 +214,7 @@ "filehound": "^1.17.5", "fs-extra": "^9.0.1", "glob-to-regexp": "^0.4.1", - "got": "^11.8.2", + "got": "^11.8.3", "grapheme-splitter": "^1.0.4", "handlebars": "^4.7.7", "http-proxy": "^1.18.1", @@ -333,7 +333,7 @@ "concurrently": "^5.3.0", "css-loader": "^5.2.7", "deepdash": "^5.3.9", - "dompurify": "^2.3.3", + "dompurify": "^2.3.4", "electron": "^13.6.1", "electron-builder": "^22.14.5", "electron-notarize": "^0.3.0", @@ -341,6 +341,7 @@ "esbuild-loader": "^2.16.0", "eslint": "^7.32.0", "eslint-plugin-header": "^3.1.1", + "eslint-plugin-import": "^2.25.3", "eslint-plugin-react": "^7.27.1", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-unused-imports": "^1.1.5", diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 1452dfadd5..81923d93ea 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -472,8 +472,8 @@ describe("pre 2.6.0 config with a cluster icon", () => { it("moves the icon into preferences", async () => { const storedClusterData = ClusterStore.getInstance().clustersList[0]; - expect(storedClusterData.hasOwnProperty("icon")).toBe(false); - expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); + expect(Object.prototype.hasOwnProperty.call(storedClusterData, "icon")).toBe(false); + expect(Object.prototype.hasOwnProperty.call(storedClusterData.preferences, "icon")).toBe(true); expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true); }); }); diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index ba7272f4c9..f53e3286c2 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -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", () => { diff --git a/src/common/app-paths.ts b/src/common/app-paths.ts index d23cc8c1a4..6802cd8f88 100644 --- a/src/common/app-paths.ts +++ b/src/common/app-paths.ts @@ -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[0]; diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 1f4f933620..6901a9dad2 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -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"; diff --git a/src/common/ipc/ipc.ts b/src/common/ipc/ipc.ts index b0eaa01e2a..0442d07630 100644 --- a/src/common/ipc/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -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"; diff --git a/src/common/k8s-api/__tests__/crd.test.ts b/src/common/k8s-api/__tests__/crd.test.ts index 0eba45e2cd..4403a9c7a4 100644 --- a/src/common/k8s-api/__tests__/crd.test.ts +++ b/src/common/k8s-api/__tests__/crd.test.ts @@ -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"); diff --git a/src/common/k8s-api/__tests__/kube-object.test.ts b/src/common/k8s-api/__tests__/kube-object.test.ts index cb2a412a5e..edc7a52505 100644 --- a/src/common/k8s-api/__tests__/kube-object.test.ts +++ b/src/common/k8s-api/__tests__/kube-object.test.ts @@ -164,14 +164,14 @@ describe("KubeObject", () => { describe("isJsonApiDataList", () => { function isAny(val: unknown): val is any { - return !Boolean(void val); + return true; } function isNotAny(val: unknown): val is any { - return Boolean(void val); + return false; } - function isBoolean(val: unknown): val is Boolean { + function isBoolean(val: unknown): val is boolean { return typeof val === "boolean"; } diff --git a/src/common/k8s-api/endpoints/crd.api.ts b/src/common/k8s-api/endpoints/crd.api.ts index 4e2244d8a8..4892a8a262 100644 --- a/src/common/k8s-api/endpoints/crd.api.ts +++ b/src/common/k8s-api/endpoints/crd.api.ts @@ -48,34 +48,36 @@ export interface CRDVersion { additionalPrinterColumns?: AdditionalPrinterColumnsV1[]; } -export interface CustomResourceDefinition { - spec: { - group: string; - /** - * @deprecated for apiextensions.k8s.io/v1 but used previously - */ - version?: string; - names: { - plural: string; - singular: string; - kind: string; - listKind: string; - }; - scope: "Namespaced" | "Cluster" | string; - /** - * @deprecated for apiextensions.k8s.io/v1 but used previously - */ - validation?: object; - versions?: CRDVersion[]; - conversion: { - strategy?: string; - webhook?: any; - }; - /** - * @deprecated for apiextensions.k8s.io/v1 but used previously - */ - additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; +export interface CustomResourceDefinitionSpec { + group: string; + /** + * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1 + */ + version?: string; + names: { + plural: string; + singular: string; + kind: string; + listKind: string; }; + scope: "Namespaced" | "Cluster"; + /** + * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1 + */ + validation?: object; + versions?: CRDVersion[]; + conversion: { + strategy?: string; + webhook?: any; + }; + /** + * @deprecated for apiextensions.k8s.io/v1 but used in v1beta1 + */ + additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; +} + +export interface CustomResourceDefinition { + spec: CustomResourceDefinitionSpec; status: { conditions: { lastTransitionTime: string; @@ -150,27 +152,32 @@ export class CustomResourceDefinition extends KubeObject { } getPreferedVersion(): CRDVersion { - // Prefer the modern `versions` over the legacy `version` - if (this.spec.versions) { - for (const version of this.spec.versions) { - if (version.storage) { - return version; - } - } - } else if (this.spec.version) { - const { additionalPrinterColumns: apc } = this.spec; - const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath })); + const { apiVersion } = this; - return { - name: this.spec.version, - served: true, - storage: true, - schema: this.spec.validation, - additionalPrinterColumns, - }; + switch (apiVersion) { + case "apiextensions.k8s.io/v1": + for (const version of this.spec.versions) { + if (version.storage) { + return version; + } + } + break; + + case "apiextensions.k8s.io/v1beta1": { + const { additionalPrinterColumns: apc } = this.spec; + const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc }) => ({ ...apc, jsonPath: JSONPath })); + + return { + name: this.spec.version, + served: true, + storage: true, + schema: this.spec.validation, + additionalPrinterColumns, + }; + } } - throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`); + throw new Error(`Unknown apiVersion=${apiVersion}: Failed to find a version for CustomResourceDefinition ${this.metadata.name}`); } getVersion() { @@ -197,7 +204,7 @@ export class CustomResourceDefinition extends KubeObject { const columns = this.getPreferedVersion().additionalPrinterColumns ?? []; return columns - .filter(column => column.name != "Age" && (ignorePriority || !column.priority)); + .filter(column => column.name.toLowerCase() != "age" && (ignorePriority || !column.priority)); } getValidation() { diff --git a/src/common/k8s-api/endpoints/ingress.api.ts b/src/common/k8s-api/endpoints/ingress.api.ts index 7b80dca29e..8e46021eae 100644 --- a/src/common/k8s-api/endpoints/ingress.api.ts +++ b/src/common/k8s-api/endpoints/ingress.api.ts @@ -187,7 +187,7 @@ export class Ingress extends KubeObject { const servicePort = defaultBackend?.service.port.number ?? backend?.servicePort; if (rules && rules.length > 0) { - if (rules.some(rule => rule.hasOwnProperty("http"))) { + if (rules.some(rule => Object.prototype.hasOwnProperty.call(rule, "http"))) { ports.push(httpPort); } } else if (servicePort !== undefined) { diff --git a/src/common/k8s-api/endpoints/metrics.api.ts b/src/common/k8s-api/endpoints/metrics.api.ts index bd6bc4709c..0fdadbe0dc 100644 --- a/src/common/k8s-api/endpoints/metrics.api.ts +++ b/src/common/k8s-api/endpoints/metrics.api.ts @@ -184,7 +184,8 @@ export function getMetricLastPoints(metrics: Record) { if (metric.data.result.length) { result[metricName] = +metric.data.result[0].values.slice(-1)[0][1]; } - } catch (e) { + } catch { + // ignore error } return result; diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 0f31758ab2..b6a823935a 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -34,6 +34,9 @@ import type { IKubeWatchEvent } from "./kube-watch-api"; import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api"; import { noop } from "../utils"; import type { RequestInit } from "node-fetch"; + +// BUG: https://github.com/mysticatea/abort-controller/pull/22 +// eslint-disable-next-line import/no-named-as-default import AbortController from "abort-controller"; import { Agent, AgentOptions } from "https"; import type { Patch } from "rfc6902"; @@ -698,21 +701,16 @@ export class KubeApi { } protected modifyWatchEvent(event: IKubeWatchEvent) { + if (event.type === "ERROR") { + return; - switch (event.type) { - case "ADDED": - case "DELETED": - - case "MODIFIED": { - ensureObjectSelfLink(this, event.object); - - const { namespace, resourceVersion } = event.object.metadata; - - this.setResourceVersion(namespace, resourceVersion); - this.setResourceVersion("", resourceVersion); - - break; - } } + + ensureObjectSelfLink(this, event.object); + + const { namespace, resourceVersion } = event.object.metadata; + + this.setResourceVersion(namespace, resourceVersion); + this.setResourceVersion("", resourceVersion); } } diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 1832cdfa10..1eb7d9ff50 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -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"; @@ -469,7 +472,9 @@ export abstract class KubeObjectStore extends ItemStore switch (type) { case "ADDED": - case "MODIFIED": + + // falls through + case "MODIFIED": { const newItem = new this.api.objectConstructor(object); if (!item) { @@ -477,7 +482,9 @@ export abstract class KubeObjectStore extends ItemStore } else { items[index] = newItem; } + break; + } case "DELETED": if (item) { items.splice(index, 1); diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts index afb0f4c2af..b51e687677 100644 --- a/src/common/protocol-handler/router.ts +++ b/src/common/protocol-handler/router.ts @@ -88,7 +88,7 @@ export abstract class LensProtocolRouter { public static readonly LoggingPrefix = "[PROTOCOL ROUTER]"; - static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(\@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; + static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; constructor(protected dependencies: Dependencies) {} diff --git a/src/common/search-store.ts b/src/common/search-store.ts index ac77e0ec94..6827548bda 100644 --- a/src/common/search-store.ts +++ b/src/common/search-store.ts @@ -29,7 +29,7 @@ export class SearchStore { * @param value Unescaped string */ public static escapeRegex(value?: string): string { - return value ? value.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&") : ""; + return value ? value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") : ""; } /** diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index 450c75ae89..1ded307f0d 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -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 = { const colorTheme: PreferenceDescription = { fromStore(val) { - return val || ThemeStore.defaultTheme; + return val || defaultTheme; }, toStore(val) { - if (!val || val === ThemeStore.defaultTheme) { + if (!val || val === defaultTheme) { return undefined; } diff --git a/src/common/utils/defineGlobal.ts b/src/common/utils/defineGlobal.ts index eeaea8baa5..1c205a0632 100755 --- a/src/common/utils/defineGlobal.ts +++ b/src/common/utils/defineGlobal.ts @@ -26,7 +26,7 @@ export function defineGlobal(propName: string, descriptor: PropertyDescriptor) { const scope = typeof global !== "undefined" ? global : window; - if (scope.hasOwnProperty(propName)) { + if (Object.prototype.hasOwnProperty.call(scope, propName)) { return; } diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 6271a05969..9d185ab5d7 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -25,6 +25,7 @@ export type Falsey = false | 0 | "" | null | undefined; * Create a new type safe empty Iterable * @returns An `Iterable` that yields 0 items */ +// eslint-disable-next-line require-yield export function* newEmpty(): IterableIterator { return; } diff --git a/src/common/utils/tar.ts b/src/common/utils/tar.ts index d0c67976e9..b3ef949173 100644 --- a/src/common/utils/tar.ts +++ b/src/common/utils/tar.ts @@ -31,12 +31,13 @@ export interface ReadFileFromTarOpts { } export function readFileFromTar({ tarPath, filePath, parseJson }: ReadFileFromTarOpts): Promise { - return new Promise(async (resolve, reject) => { + return new Promise((resolve, reject) => { const fileChunks: Buffer[] = []; - await tar.list({ + tar.list({ file: tarPath, filter: entryPath => path.normalize(entryPath) === filePath, + sync: true, onentry(entry: FileStat) { entry.on("data", chunk => { fileChunks.push(chunk); diff --git a/src/common/utils/tuple.ts b/src/common/utils/tuple.ts index 5a252cf89c..b7b751bb30 100644 --- a/src/common/utils/tuple.ts +++ b/src/common/utils/tuple.ts @@ -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 diff --git a/src/common/vars.ts b/src/common/vars.ts index 138626a04b..b3d75f9bf3 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -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(); diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 3bc959bd20..ffb87e3b97 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -18,8 +18,7 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { getInjectable } from "@ogre-tools/injectable"; -import { lifecycleEnum } from "@ogre-tools/injectable"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { ExtensionLoader } from "./extension-loader"; const extensionLoaderInjectable = getInjectable({ diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 8edec6c219..67ec52c0b3 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -263,10 +263,7 @@ export class ExtensionLoader { 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) => { diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index c0c0a5674a..00f08ca5e5 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -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, frameId?: number) { return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId); diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 67567969ee..9f7b947d82 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -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[] = []; - topBarItems: registries.TopBarRegistration[] = []; + topBarItems: TopBarRegistration[] = []; async navigate

(pageId?: string, params?: P) { const { navigate } = await import("../renderer/navigation"); diff --git a/src/extensions/registries/base-registry.ts b/src/extensions/registries/base-registry.ts index 9b596172c9..1fe7530779 100644 --- a/src/extensions/registries/base-registry.ts +++ b/src/extensions/registries/base-registry.ts @@ -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 extends Singleton { private items = observable.map([], { deep: false }); diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 4dd64a9c82..76f6c05d11 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -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"; diff --git a/src/extensions/renderer-extensions.injectable.ts b/src/extensions/renderer-extensions.injectable.ts new file mode 100644 index 0000000000..dddcf11e80 --- /dev/null +++ b/src/extensions/renderer-extensions.injectable.ts @@ -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, +}); + +export default rendererExtensionsInjectable; diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 3c358acde6..fb4300b266 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -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; diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 25c8b67737..d7ce9db5c1 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -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"; diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index f8edc12e00..8c8c0c6996 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -119,7 +119,9 @@ export class HelmRepoManager extends Singleton { if (typeof parsedConfig === "object" && parsedConfig) { return parsedConfig as HelmRepoConfig; } - } catch { } + } catch { + // ignore error + } return { repositories: [], diff --git a/src/main/index.ts b/src/main/index.ts index 1f7b26e773..f40b92aa21 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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(), ); diff --git a/src/main/initializers/ipc.ts b/src/main/initializers/ipc.ts index e3d4553935..0ad881f36e 100644 --- a/src/main/initializers/ipc.ts +++ b/src/main/initializers/ipc.ts @@ -97,7 +97,9 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue { diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 2ac1831c41..1c2fe98631 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -357,10 +357,10 @@ export class Kubectl { bashScript += `export PATH="${helmPath}:${kubectlPath}:$PATH"\n`; bashScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"; - bashScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`; - bashScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`; - bashScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`; - bashScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`; + bashScript += `NO_PROXY=",\${NO_PROXY:-localhost},"\n`; + bashScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`; + bashScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`; + bashScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`; bashScript += "export NO_PROXY\n"; bashScript += "unset tempkubeconfig\n"; await fsPromises.writeFile(bashScriptPath, bashScript.toString(), { mode: 0o644 }); @@ -378,18 +378,18 @@ export class Kubectl { zshScript += "test -f \"$OLD_ZDOTDIR/.zshrc\" && . \"$OLD_ZDOTDIR/.zshrc\"\n"; // voodoo to replace any previous occurrences of kubectl path in the PATH - zshScript += `kubectlpath=\"${kubectlPath}"\n`; - zshScript += `helmpath=\"${helmPath}"\n`; + zshScript += `kubectlpath="${kubectlPath}"\n`; + zshScript += `helmpath="${helmPath}"\n`; zshScript += "p=\":$kubectlpath:\"\n"; zshScript += "d=\":$PATH:\"\n"; zshScript += `d=\${d//$p/:}\n`; zshScript += `d=\${d/#:/}\n`; - zshScript += `export PATH=\"$helmpath:$kubectlpath:\${d/%:/}\"\n`; + zshScript += `export PATH="$helmpath:$kubectlpath:\${d/%:/}"\n`; zshScript += "export KUBECONFIG=\"$tempkubeconfig\"\n"; - zshScript += `NO_PROXY=\",\${NO_PROXY:-localhost},\"\n`; - zshScript += `NO_PROXY=\"\${NO_PROXY//,localhost,/,}\"\n`; - zshScript += `NO_PROXY=\"\${NO_PROXY//,127.0.0.1,/,}\"\n`; - zshScript += `NO_PROXY=\"localhost,127.0.0.1\${NO_PROXY%,}\"\n`; + zshScript += `NO_PROXY=",\${NO_PROXY:-localhost},"\n`; + zshScript += `NO_PROXY="\${NO_PROXY//,localhost,/,}"\n`; + zshScript += `NO_PROXY="\${NO_PROXY//,127.0.0.1,/,}"\n`; + zshScript += `NO_PROXY="localhost,127.0.0.1\${NO_PROXY%,}"\n`; zshScript += "export NO_PROXY\n"; zshScript += "unset tempkubeconfig\n"; zshScript += "unset OLD_ZDOTDIR\n"; diff --git a/src/main/menu/electron-menu-items.injectable.ts b/src/main/menu/electron-menu-items.injectable.ts index dd70812d89..d0e5a650cf 100644 --- a/src/main/menu/electron-menu-items.injectable.ts +++ b/src/main/menu/electron-menu-items.injectable.ts @@ -29,8 +29,7 @@ const electronMenuItemsInjectable = getInjectable({ const extensions = di.inject(mainExtensionsInjectable); return computed(() => - extensions.get().flatMap((extension) => extension.appMenus), - ); + extensions.get().flatMap((extension) => extension.appMenus)); }, }); diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index 83638e4ec1..140b432baf 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -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": diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index 7be1eec94d..61ca1dc1d0 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -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"; diff --git a/src/main/shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session.ts index b865fc411d..0b7d673de9 100644 --- a/src/main/shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session.ts @@ -72,6 +72,7 @@ export class NodeShellSession extends ShellSession { switch (nodeOs) { default: logger.warn(`[NODE-SHELL-SESSION]: could not determine node OS, falling back with assumption of linux`); + // fallthrough case "linux": args.push("sh", "-c", "((clear && bash) || (clear && ash) || (clear && sh))"); break; diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index 677a637cc0..b17ef22689 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -134,7 +134,9 @@ export abstract class ShellSession { for (const shellProcess of this.processes.values()) { try { process.kill(shellProcess.pid); - } catch {} + } catch { + // ignore error + } } this.processes.clear(); @@ -214,7 +216,9 @@ export abstract class ShellSession { if (stats.isDirectory()) { return potentialCwd; } - } catch {} + } catch { + // ignore error + } } return "."; // Always valid diff --git a/src/main/tray/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-items.injectable.ts new file mode 100644 index 0000000000..8a31cc6af5 --- /dev/null +++ b/src/main/tray/tray-menu-items.injectable.ts @@ -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; diff --git a/src/main/tray/tray-menu-items.test.ts b/src/main/tray/tray-menu-items.test.ts new file mode 100644 index 0000000000..b46bce3671 --- /dev/null +++ b/src/main/tray/tray-menu-items.test.ts @@ -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; + let extensionsStub: ObservableMap; + + 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; + } +} diff --git a/src/main/tray/tray-menu-registration.d.ts b/src/main/tray/tray-menu-registration.d.ts new file mode 100644 index 0000000000..0cf7fe611b --- /dev/null +++ b/src/main/tray/tray-menu-registration.d.ts @@ -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[] +} diff --git a/src/main/tray.ts b/src/main/tray/tray.ts similarity index 70% rename from src/main/tray.ts rename to src/main/tray/tray.ts index 850d19fe21..ac3e140e3b 100644 --- a/src/main/tray.ts +++ b/src/main/tray/tray.ts @@ -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, +) { 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}`, diff --git a/src/migrations/cluster-store/2.0.0-beta.2.ts b/src/migrations/cluster-store/2.0.0-beta.2.ts index e3a89b37d3..8d49aa3ab0 100644 --- a/src/migrations/cluster-store/2.0.0-beta.2.ts +++ b/src/migrations/cluster-store/2.0.0-beta.2.ts @@ -33,7 +33,7 @@ export default { const contextName = value[0]; // Looping all the keys gives out the store internal stuff too... - if (contextName === "__internal__" || value[1].hasOwnProperty("kubeConfig")) continue; + if (contextName === "__internal__" || Object.prototype.hasOwnProperty.call(value[1], "kubeConfig")) continue; store.set(contextName, { kubeConfig: value[1] }); } }, diff --git a/src/migrations/cluster-store/2.6.0-beta.3.ts b/src/migrations/cluster-store/2.6.0-beta.3.ts index a33dd7523f..82f1ac0f88 100644 --- a/src/migrations/cluster-store/2.6.0-beta.3.ts +++ b/src/migrations/cluster-store/2.6.0-beta.3.ts @@ -34,7 +34,7 @@ export default { if (!cluster.kubeConfig) continue; const config = yaml.load(cluster.kubeConfig); - if (!config || typeof config !== "object" || !config.hasOwnProperty("users")) { + if (!config || typeof config !== "object" || !Object.prototype.hasOwnProperty.call(config, "users")) { continue; } diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts index f4261df995..26a4aaeb03 100644 --- a/src/renderer/api/__tests__/catalog-entity-registry.test.ts +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -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"; diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 4885c8f265..735e66a7a3 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -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; +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([], { deep: true }); diff --git a/src/renderer/api/catalog-entity.ts b/src/renderer/api/catalog-entity.ts index 1d07291659..debb235c8f 100644 --- a/src/renderer/api/catalog-entity.ts +++ b/src/renderer/api/catalog-entity.ts @@ -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; - }, -}; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 1de360d18a..6e9e9fb471 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -114,9 +114,6 @@ export async function bootstrap(comp: () => Promise, di: Dependenc logger.info(`${logPrefix} initializing KubeObjectDetailRegistry`); initializers.initKubeObjectDetailRegistry(); - logger.info(`${logPrefix} initializing WelcomeMenuRegistry`); - initializers.initWelcomeMenuRegistry(); - logger.info(`${logPrefix} initializing WorkloadsOverviewDetailRegistry`); initializers.initWorkloadsOverviewDetailRegistry(); diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx index 3e6c74b9bb..78a0457293 100644 --- a/src/renderer/components/+config-autoscalers/hpa-details.tsx +++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx @@ -45,15 +45,17 @@ export class HpaDetails extends React.Component { const renderName = (metric: IHpaMetric) => { switch (metric.type) { - case HpaMetricType.Resource: - const addition = metric.resource.targetAverageUtilization ? <>(as a percentage of request) : ""; + case HpaMetricType.Resource: { + const addition = metric.resource.targetAverageUtilization + ? "(as a percentage of request)" + : ""; return <>Resource {metric.resource.name} on Pods {addition}; - + } case HpaMetricType.Pods: return <>{metric.pods.metricName} on Pods; - case HpaMetricType.Object: + case HpaMetricType.Object: { const { target } = metric.object; const { kind, name } = target; const objectUrl = getDetailsUrl(apiManager.lookupApiLink(target, hpa)); @@ -64,6 +66,7 @@ export class HpaDetails extends React.Component { {kind}/{name} ); + } case HpaMetricType.External: return ( <> diff --git a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx index 05d970c129..054adf45ad 100644 --- a/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx +++ b/src/renderer/components/+extensions/attempt-install/unpack-extension/unpack-extension.injectable.tsx @@ -18,8 +18,7 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { getInjectable } from "@ogre-tools/injectable"; -import { lifecycleEnum } from "@ogre-tools/injectable"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { unpackExtension } from "./unpack-extension"; import extensionLoaderInjectable from "../../../../../extensions/extension-loader/extension-loader.injectable"; diff --git a/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx b/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx index c811a21319..1e9c582d39 100644 --- a/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx +++ b/src/renderer/components/+extensions/get-base-registry-url/get-base-registry-url.tsx @@ -47,8 +47,8 @@ export const getBaseRegistryUrl = ({ getRegistryUrlPreference }: Dependencies) = } catch (error) { Notifications.error(

Failed to get configured registry from .npmrc. Falling back to default registry

); console.warn("[EXTENSIONS]: failed to get configured registry from .npmrc", error); - // fallthrough } + // fallthrough } default: case ExtensionRegistryLocation.DEFAULT: diff --git a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx index 5b49c3f85c..864735f3b7 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx +++ b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx @@ -21,11 +21,10 @@ import React from "react"; import { boundMethod, cssNames } from "../../utils"; -import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward"; +import { openPortForward, PortForwardItem, removePortForward, PortForwardDialog } from "../../port-forward"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; -import { PortForwardDialog } from "../../port-forward"; import { Notifications } from "../notifications"; interface Props extends MenuActionsProps { diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 40859cd5e0..1e3c75b68f 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -27,7 +27,7 @@ import { ThemeStore } from "../../theme.store"; import { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { isWindows } from "../../../common/vars"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import moment from "moment-timezone"; import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers"; import { action } from "mobx"; @@ -86,16 +86,12 @@ export const Application = observer(() => {
- userStore.terminalCopyOnSelect = v.target.checked} - name="terminalCopyOnSelect" - /> - } - /> + userStore.terminalCopyOnSelect = !userStore.terminalCopyOnSelect} + > + Copy on select and paste on right-click +

@@ -135,16 +131,9 @@ export const Application = observer(() => {
- userStore.openAtLogin = v.target.checked} - name="startup" - /> - } - label="Automatically start Lens on login" - /> + userStore.openAtLogin = !userStore.openAtLogin}> + Automatically start Lens on login +

diff --git a/src/renderer/components/+preferences/editor.tsx b/src/renderer/components/+preferences/editor.tsx index 6f346eb63d..523f9bd278 100644 --- a/src/renderer/components/+preferences/editor.tsx +++ b/src/renderer/components/+preferences/editor.tsx @@ -21,7 +21,7 @@ import { observer } from "mobx-react"; import React from "react"; import { UserStore } from "../../../common/user-store"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import { Select } from "../select"; import { SubTitle } from "../layout/sub-title"; import { SubHeader } from "../layout/sub-header"; @@ -45,15 +45,12 @@ export const Editor = observer(() => {
- Show minimap} - control={ - editorConfiguration.minimap.enabled = checked} - /> - } - /> + editorConfiguration.minimap.enabled = !editorConfiguration.minimap.enabled} + > + Show minimap +
Position diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index bffc69e3c0..13a0299ee3 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -26,7 +26,7 @@ import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-s import { observer } from "mobx-react"; import { bundledKubectlPath } from "../../../main/kubectl"; import { SelectOption, Select } from "../select"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; import { packageMirrors } from "../../../common/user-store/preferences-helpers"; export const KubectlBinaries = observer(() => { @@ -48,16 +48,12 @@ export const KubectlBinaries = observer(() => { <>
- userStore.downloadKubectlBinaries = v.target.checked} - name="kubectl-download" - /> - } - label="Download kubectl binaries matching the Kubernetes cluster version" - /> + userStore.downloadKubectlBinaries = !userStore.downloadKubectlBinaries} + > + Download kubectl binaries matching the Kubernetes cluster version +
diff --git a/src/renderer/components/+preferences/proxy.tsx b/src/renderer/components/+preferences/proxy.tsx index b85bd5e0e1..f25c1fbc9b 100644 --- a/src/renderer/components/+preferences/proxy.tsx +++ b/src/renderer/components/+preferences/proxy.tsx @@ -24,10 +24,11 @@ import React from "react"; import { UserStore } from "../../../common/user-store"; import { Input } from "../input"; import { SubTitle } from "../layout/sub-title"; -import { FormSwitch, Switcher } from "../switch"; +import { Switch } from "../switch"; export const LensProxy = observer(() => { const [proxy, setProxy] = React.useState(UserStore.getInstance().httpsProxy || ""); + const store = UserStore.getInstance(); return (
@@ -50,16 +51,9 @@ export const LensProxy = observer(() => {
- UserStore.getInstance().allowUntrustedCAs = v.target.checked} - name="startup" - /> - } - label="Allow untrusted Certificate Authorities" - /> + store.allowUntrustedCAs = !store.allowUntrustedCAs}> + Allow untrusted Certificate Authorities + This will make Lens to trust ANY certificate authority without any validations.{" "} Needed with some corporate proxies that do certificate re-writing.{" "} diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index 2024d69c7e..74cf65b20d 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -20,45 +20,55 @@ */ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { Welcome } from "../welcome"; -import { TopBarRegistry, WelcomeMenuRegistry, WelcomeBannerRegistry } from "../../../../extensions/registries"; -import { defaultWidth } from "../welcome"; +import { defaultWidth, Welcome } from "../welcome"; +import { computed } from "mobx"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import type { WelcomeBannerRegistration } from "../welcome-banner-items/welcome-banner-registration"; -jest.mock( - "electron", - () => ({ - ipcRenderer: { - on: jest.fn(), - }, - app: { - getPath: () => "tmp", - }, - }), -); +jest.mock("electron", () => ({ + ipcRenderer: { + on: jest.fn(), + }, + app: { + getPath: () => "tmp", + }, +})); describe("", () => { - beforeEach(() => { - TopBarRegistry.createInstance(); - WelcomeMenuRegistry.createInstance(); - WelcomeBannerRegistry.createInstance(); - }); + let render: DiRender; + let di: ConfigurableDependencyInjectionContainer; + let welcomeBannersStub: WelcomeBannerRegistration[]; - afterEach(() => { - TopBarRegistry.resetInstance(); - WelcomeMenuRegistry.resetInstance(); - WelcomeBannerRegistry.resetInstance(); + beforeEach(() => { + di = getDiForUnitTesting(); + + render = renderFor(di); + + welcomeBannersStub = []; + + di.override(rendererExtensionsInjectable, () => + computed(() => [ + new TestExtension({ + id: "some-id", + welcomeBanners: welcomeBannersStub, + }), + ]), + ); }); it("renders registered in WelcomeBannerRegistry and hide logo", async () => { const testId = "testId"; - WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - Banner: () =>
, - }, - ]); + welcomeBannersStub.push({ + Banner: () =>
, + }); const { container } = render(); @@ -67,16 +77,15 @@ describe("", () => { }); it("calculates max width from WelcomeBanner.width registered in WelcomeBannerRegistry", async () => { - WelcomeBannerRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { - width: 100, - Banner: () =>
, - }, - { - width: 800, - Banner: () =>
, - }, - ]); + welcomeBannersStub.push({ + width: 100, + Banner: () =>
, + }); + + welcomeBannersStub.push({ + width: 800, + Banner: () =>
, + }); render(); @@ -92,3 +101,25 @@ describe("", () => { }); }); }); + +class TestExtension extends LensRendererExtension { + constructor({ + id, + welcomeBanners, + }: { + id: string; + welcomeBanners: WelcomeBannerRegistration[]; + }) { + super({ + id, + absolutePath: "irrelevant", + isBundled: false, + isCompatible: false, + isEnabled: false, + manifest: { name: id, version: "some-version" }, + manifestPath: "irrelevant", + }); + + this.welcomeBanners = welcomeBanners; + } +} diff --git a/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts new file mode 100644 index 0000000000..288bb540ce --- /dev/null +++ b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-items.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { computed } from "mobx"; + +const welcomeBannerItemsInjectable = getInjectable({ + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => [ + ...extensions.get().flatMap((extension) => extension.welcomeBanners), + ]); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default welcomeBannerItemsInjectable; diff --git a/src/extensions/registries/welcome-banner-registry.ts b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts similarity index 91% rename from src/extensions/registries/welcome-banner-registry.ts rename to src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts index 1102dc8a3f..ec7cf3cbdb 100644 --- a/src/extensions/registries/welcome-banner-registry.ts +++ b/src/renderer/components/+welcome/welcome-banner-items/welcome-banner-registration.d.ts @@ -19,8 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BaseRegistry } from "./base-registry"; - /** * WelcomeBannerRegistration is for an extension to register * Provide a Banner component to be renderered in the welcome screen. @@ -35,5 +33,3 @@ export interface WelcomeBannerRegistration { */ width?: number } - -export class WelcomeBannerRegistry extends BaseRegistry { } diff --git a/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts new file mode 100644 index 0000000000..c766704cef --- /dev/null +++ b/src/renderer/components/+welcome/welcome-menu-items/get-welcome-menu-items.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { computed, IComputedValue } from "mobx"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import { navigate } from "../../../navigation"; +import { catalogURL } from "../../../../common/routes"; + +interface Dependencies { + extensions: IComputedValue; +} + +export const getWelcomeMenuItems = ({ extensions }: Dependencies) => { + const browseClusters = { + title: "Browse Clusters in Catalog", + icon: "view_list", + click: () => + navigate( + catalogURL({ + params: { group: "entity.k8slens.dev", kind: "KubernetesCluster" }, + }), + ), + }; + + return computed(() => [ + browseClusters, + ...extensions.get().flatMap((extension) => extension.welcomeMenus), + ]); +}; diff --git a/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts new file mode 100644 index 0000000000..384b0b07bc --- /dev/null +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-items.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { getWelcomeMenuItems } from "./get-welcome-menu-items"; + +const welcomeMenuItemsInjectable = getInjectable({ + instantiate: (di) => + getWelcomeMenuItems({ + extensions: di.inject(rendererExtensionsInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default welcomeMenuItemsInjectable; diff --git a/src/extensions/registries/welcome-menu-registry.ts b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts similarity index 90% rename from src/extensions/registries/welcome-menu-registry.ts rename to src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts index 7092028459..8f4d9833b3 100644 --- a/src/extensions/registries/welcome-menu-registry.ts +++ b/src/renderer/components/+welcome/welcome-menu-items/welcome-menu-registration.d.ts @@ -19,12 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { BaseRegistry } from "./base-registry"; - export interface WelcomeMenuRegistration { title: string | (() => string); icon: string; click: () => void | Promise; } - -export class WelcomeMenuRegistry extends BaseRegistry {} diff --git a/src/renderer/components/+welcome/welcome.tsx b/src/renderer/components/+welcome/welcome.tsx index 669e768782..56aefbfc3c 100644 --- a/src/renderer/components/+welcome/welcome.tsx +++ b/src/renderer/components/+welcome/welcome.tsx @@ -22,78 +22,129 @@ import "./welcome.scss"; import React from "react"; import { observer } from "mobx-react"; +import type { IComputedValue } from "mobx"; import Carousel from "react-material-ui-carousel"; import { Icon } from "../icon"; import { productName, slackUrl } from "../../../common/vars"; -import { WelcomeMenuRegistry } from "../../../extensions/registries"; -import { WelcomeBannerRegistry } from "../../../extensions/registries"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import welcomeMenuItemsInjectable from "./welcome-menu-items/welcome-menu-items.injectable"; +import type { WelcomeMenuRegistration } from "./welcome-menu-items/welcome-menu-registration"; +import welcomeBannerItemsInjectable from "./welcome-banner-items/welcome-banner-items.injectable"; +import type { WelcomeBannerRegistration } from "./welcome-banner-items/welcome-banner-registration"; export const defaultWidth = 320; -@observer -export class Welcome extends React.Component { - render() { - const welcomeBanner = WelcomeBannerRegistry.getInstance().getItems(); +interface Dependencies { + welcomeMenuItems: IComputedValue + welcomeBannerItems: IComputedValue +} - // if there is banner with specified width, use it to calculate the width of the container - const maxWidth = welcomeBanner.reduce((acc, curr) => { - const currWidth = curr.width ?? 0; +const NonInjectedWelcome: React.FC = ({ welcomeMenuItems, welcomeBannerItems }) => { + const welcomeBanners = welcomeBannerItems.get(); - if (acc > currWidth) { - return acc; - } + // if there is banner with specified width, use it to calculate the width of the container + const maxWidth = welcomeBanners.reduce((acc, curr) => { + const currWidth = curr.width ?? 0; - return currWidth; - }, defaultWidth); + if (acc > currWidth) { + return acc; + } - return ( -
-
- {welcomeBanner.length > 0 ? ( - 1} - autoPlay={true} - navButtonsAlwaysInvisible={true} - indicatorIconButtonProps={{ - style: { - color: "var(--iconActiveBackground)", - }, - }} - activeIndicatorIconButtonProps={{ - style: { - color: "var(--iconActiveColor)", - }, - }} - interval={8000} + return currWidth; + }, defaultWidth); + + return ( +
+
+ {welcomeBanners.length > 0 ? ( + 1} + autoPlay={true} + navButtonsAlwaysInvisible={true} + indicatorIconButtonProps={{ + style: { + color: "var(--iconActiveBackground)", + }, + }} + activeIndicatorIconButtonProps={{ + style: { + color: "var(--iconActiveColor)", + }, + }} + interval={8000} + > + {welcomeBanners.map((item, index) => ( + + ))} + + ) : ( + + )} + +
+
+

Welcome to {productName} 5!

+ +

+ To get you started we have auto-detected your clusters in your + kubeconfig file and added them to the catalog, your centralized + view for managing all your cloud-native resources. +
+
+ If you have any questions or feedback, please join our{" "} + + Lens Community slack channel + + . +

+ +
- ); - } -} +
+ ); +}; + +export const Welcome = withInjectables( + observer(NonInjectedWelcome), + + { + getProps: (di) => ({ + welcomeMenuItems: di.inject(welcomeMenuItemsInjectable), + welcomeBannerItems: di.inject(welcomeBannerItemsInjectable), + }), + }, +); diff --git a/src/renderer/components/app.scss b/src/renderer/components/app.scss index 0482dd0dd8..40ab38ffdb 100755 --- a/src/renderer/components/app.scss +++ b/src/renderer/components/app.scss @@ -91,6 +91,15 @@ html, body { overflow: hidden; } +#terminal-init { + position: absolute; + top: 0; + left: 0; + height: 0; + visibility: hidden; + overflow: hidden; +} + #app { height: 100%; min-height: 100%; diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 7ffcd69187..c5b2effc84 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -39,8 +39,8 @@ import { DeleteClusterDialog } from "../delete-cluster-dialog"; import { reaction } from "mobx"; import { navigation } from "../../navigation"; import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync"; -import { TopBar } from "../layout/topbar"; import { catalogURL, getPreviousTabUrl } from "../../../common/routes"; +import { TopBar } from "../layout/top-bar/top-bar"; @observer export class ClusterManager extends React.Component { diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index 8270e6e31a..36c9ddbc22 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -23,7 +23,7 @@ import { KubeConfig } from "@kubernetes/client-node"; import { fireEvent, render } from "@testing-library/react"; import mockFs from "mock-fs"; import React from "react"; -import selectEvent from "react-select-event"; +import * as selectEvent from "react-select-event"; import { Cluster } from "../../../../main/cluster"; import { DeleteClusterDialog } from "../delete-cluster-dialog"; diff --git a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx index b87adba0a7..9fe75a9756 100644 --- a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx @@ -22,7 +22,7 @@ import React from "react"; import "@testing-library/jest-dom/extend-expect"; import { render } from "@testing-library/react"; -import selectEvent from "react-select-event"; +import * as selectEvent from "react-select-event"; import { Pod } from "../../../../common/k8s-api/endpoints"; import { LogResourceSelector } from "../log-resource-selector"; diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal.ts index e88bfc067e..383accab20 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal.ts @@ -34,17 +34,9 @@ import { clipboard } from "electron"; import logger from "../../../common/logger"; export class Terminal { - public static readonly spawningPool = (() => { - // terminal element must be in DOM before attaching via xterm.open(elem) - // https://xtermjs.org/docs/api/terminal/classes/terminal/#open - const pool = document.createElement("div"); - - pool.className = "terminal-init"; - pool.style.cssText = "position: absolute; top: 0; left: 0; height: 0; visibility: hidden; overflow: hidden"; - document.body.appendChild(pool); - - return pool; - })(); + public static get spawningPool() { + return document.getElementById("terminal-init"); + } static async preloadFonts() { const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires diff --git a/src/renderer/components/file-picker/file-picker.tsx b/src/renderer/components/file-picker/file-picker.tsx index 3661bb2cb2..ed955c4a22 100644 --- a/src/renderer/components/file-picker/file-picker.tsx +++ b/src/renderer/components/file-picker/file-picker.tsx @@ -133,7 +133,8 @@ export class FilePicker extends React.Component { switch (onOverSizeLimit) { case OverSizeLimitStyle.FILTER: return files.filter(file => file.size <= maxSize ); - case OverSizeLimitStyle.REJECT: + + case OverSizeLimitStyle.REJECT: { const firstFileToLarge = files.find(file => file.size > maxSize); if (firstFileToLarge) { @@ -141,6 +142,7 @@ export class FilePicker extends React.Component { } return files; + } } } @@ -156,7 +158,9 @@ export class FilePicker extends React.Component { switch (onOverTotalSizeLimit) { case OverTotalSizeLimitStyle.FILTER_LARGEST: files = _.orderBy(files, ["size"]); - case OverTotalSizeLimitStyle.FILTER_LAST: + + // fallthrough + case OverTotalSizeLimitStyle.FILTER_LAST: { let newTotalSize = totalSize; for (;files.length > 0;) { @@ -168,6 +172,7 @@ export class FilePicker extends React.Component { } return files; + } case OverTotalSizeLimitStyle.REJECT: throw `Total file size to upload is too large. Expected at most ${maxTotalSize}. Found ${totalSize}.`; } diff --git a/src/renderer/components/icon/icon.tsx b/src/renderer/components/icon/icon.tsx index e5258f1a14..f5943ba109 100644 --- a/src/renderer/components/icon/icon.tsx +++ b/src/renderer/components/icon/icon.tsx @@ -73,6 +73,7 @@ export class Icon extends React.PureComponent { switch (evt.nativeEvent.code) { case "Space": + // fallthrough case "Enter": { // eslint-disable-next-line react/no-find-dom-node const icon = findDOMNode(this) as HTMLElement; diff --git a/src/renderer/components/input/input_validators.ts b/src/renderer/components/input/input_validators.ts index 8ea1a7e3c2..36086aca24 100644 --- a/src/renderer/components/input/input_validators.ts +++ b/src/renderer/components/input/input_validators.ts @@ -39,7 +39,7 @@ export const isRequired: InputValidator = { export const isEmail: InputValidator = { condition: ({ type }) => type === "email", message: () => `Wrong email format`, - validate: value => !!value.match(/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/), + validate: value => !!value.match(/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/), }; export const isNumber: InputValidator = { diff --git a/src/renderer/components/kube-object-details/kube-object-details.tsx b/src/renderer/components/kube-object-details/kube-object-details.tsx index be888c6505..e01c109fc5 100644 --- a/src/renderer/components/kube-object-details/kube-object-details.tsx +++ b/src/renderer/components/kube-object-details/kube-object-details.tsx @@ -31,7 +31,6 @@ import { apiManager } from "../../../common/k8s-api/api-manager"; import { crdStore } from "../+custom-resources/crd.store"; import { KubeObjectMenu } from "../kube-object-menu"; import { KubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; -import logger from "../../../main/logger"; import { CrdResourceDetails } from "../+custom-resources"; import { KubeObjectMeta } from "../kube-object-meta"; import { hideDetails, kubeDetailsUrlParam } from "../kube-detail-params"; @@ -62,7 +61,7 @@ export class KubeObjectDetails extends React.Component { .getStore(this.path) ?.getByPath(this.path); } catch (error) { - logger.error(`[KUBE-OBJECT-DETAILS]: failed to get store or object: ${error}`, { path: this.path }); + console.error(`[KUBE-OBJECT-DETAILS]: failed to get store or object: ${error}`, { path: this.path }); return undefined; } diff --git a/src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts b/src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts index 4937f97b26..fab0639190 100644 --- a/src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts +++ b/src/renderer/components/kube-object-menu/dependencies/hide-details.injectable.ts @@ -21,7 +21,7 @@ import { hideDetails } from "../../kube-detail-params"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -export const hideDetailsInjectable = getInjectable({ +const hideDetailsInjectable = getInjectable({ instantiate: () => hideDetails, lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/layout/close-button.module.scss b/src/renderer/components/layout/close-button.module.scss new file mode 100644 index 0000000000..57a7296339 --- /dev/null +++ b/src/renderer/components/layout/close-button.module.scss @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.closeButton { + width: 35px; + height: 35px; + display: grid; + place-items: center; + cursor: pointer; + border: 2px solid var(--textColorDimmed); + border-radius: 50%; + + &:hover { + background-color: #72767d25; + } + + &:active { + transform: translateY(1px); + } +} + +.icon { + color: var(--textColorAccent); + opacity: 0.6; +} + +.esc { + text-align: center; + margin-top: var(--margin); + font-weight: bold; + user-select: none; + color: var(--textColorDimmed); + pointer-events: none; +} \ No newline at end of file diff --git a/src/renderer/components/layout/close-button.tsx b/src/renderer/components/layout/close-button.tsx new file mode 100644 index 0000000000..9bdf78f0bd --- /dev/null +++ b/src/renderer/components/layout/close-button.tsx @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import styles from "./close-button.module.scss"; + +import React, { HTMLAttributes } from "react"; +import { Icon } from "../icon"; + +interface Props extends HTMLAttributes { +} + +export function CloseButton(props: Props) { + return ( +
+
+ +
+ +
+ ); +} diff --git a/src/renderer/components/layout/setting-layout.scss b/src/renderer/components/layout/setting-layout.scss index 4fd61bfa93..de84137818 100644 --- a/src/renderer/components/layout/setting-layout.scss +++ b/src/renderer/components/layout/setting-layout.scss @@ -129,41 +129,7 @@ } > .toolsRegion { - .fixedTools { - position: fixed; - top: 60px; - - .closeBtn { - width: 35px; - height: 35px; - display: grid; - place-items: center; - border: 2px solid var(--textColorDimmed); - border-radius: 50%; - cursor: pointer; - - &:hover { - background-color: #72767d4d; - } - - &:active { - transform: translateY(1px); - } - - .Icon { - color: var(--textColorTertiary); - } - } - - .esc { - text-align: center; - margin-top: 4px; - font-weight: 600; - font-size: 14px; - color: var(--textColorDimmed); - pointer-events: none; - } - } + width: 45px; } } diff --git a/src/renderer/components/layout/setting-layout.tsx b/src/renderer/components/layout/setting-layout.tsx index 282eed0dcb..96fad8ea1f 100644 --- a/src/renderer/components/layout/setting-layout.tsx +++ b/src/renderer/components/layout/setting-layout.tsx @@ -25,8 +25,8 @@ import React from "react"; import { observer } from "mobx-react"; import { cssNames, IClassName } from "../../utils"; import { navigation } from "../../navigation"; -import { Icon } from "../icon"; import { catalogURL } from "../../../common/routes"; +import { CloseButton } from "./close-button"; export interface SettingLayoutProps extends React.DOMAttributes { className?: IClassName; @@ -104,13 +104,8 @@ export class SettingLayout extends React.Component {
{ this.props.provideBackButtonNavigation && ( -
-
- -
- +
+
) } diff --git a/src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts b/src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts new file mode 100644 index 0000000000..9c5bd3d1e8 --- /dev/null +++ b/src/renderer/components/layout/top-bar/top-bar-items/top-bar-items.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import rendererExtensionsInjectable from "../../../../../extensions/renderer-extensions.injectable"; + +const topBarItemsInjectable = getInjectable({ + instantiate: (di) => { + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => + extensions.get().flatMap((extension) => extension.topBarItems), + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default topBarItemsInjectable; diff --git a/src/extensions/registries/topbar-registry.ts b/src/renderer/components/layout/top-bar/top-bar-registration.d.ts similarity index 88% rename from src/extensions/registries/topbar-registry.ts rename to src/renderer/components/layout/top-bar/top-bar-registration.d.ts index 37b55faaaa..5e470d4d26 100644 --- a/src/extensions/registries/topbar-registry.ts +++ b/src/renderer/components/layout/top-bar/top-bar-registration.d.ts @@ -18,10 +18,6 @@ * 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 React from "react"; -import { BaseRegistry } from "./base-registry"; - interface TopBarComponents { Item: React.ComponentType; } @@ -29,6 +25,3 @@ interface TopBarComponents { export interface TopBarRegistration { components: TopBarComponents; } - -export class TopBarRegistry extends BaseRegistry { -} diff --git a/src/renderer/components/layout/__tests__/topbar-win-linux.test.tsx b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx similarity index 83% rename from src/renderer/components/layout/__tests__/topbar-win-linux.test.tsx rename to src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx index 1307e181cd..6d8014004e 100644 --- a/src/renderer/components/layout/__tests__/topbar-win-linux.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar-win-linux.test.tsx @@ -20,23 +20,29 @@ */ import React from "react"; -import { render, fireEvent } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { TopBar } from "../topbar"; -import { TopBarRegistry } from "../../../../extensions/registries"; +import { TopBar } from "./top-bar"; import { IpcMainWindowEvents } from "../../../../main/window-manager"; import { broadcastMessage } from "../../../../common/ipc"; import * as vars from "../../../../common/vars"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { DiRender, renderFor } from "../../test-utils/renderFor"; -const mockConfig = vars as { isWindows: boolean, isLinux: boolean }; +const mockConfig = vars as { isWindows: boolean; isLinux: boolean }; jest.mock("../../../../common/ipc"); jest.mock("../../../../common/vars", () => { + const SemVer = require("semver").SemVer; + + const versionStub = new SemVer("1.0.0"); + return { __esModule: true, isWindows: null, isLinux: null, + appSemVer: versionStub, }; }); @@ -57,20 +63,20 @@ jest.mock("@electron/remote", () => { }; }); -describe(" in Windows and Linux", () => { - beforeEach(() => { - TopBarRegistry.createInstance(); - }); +describe(" in Windows and Linux", () => { + let render: DiRender; - afterEach(() => { - TopBarRegistry.resetInstance(); + beforeEach(() => { + const di = getDiForUnitTesting(); + + render = renderFor(di); }); it("shows window controls on Windows", () => { mockConfig.isWindows = true; mockConfig.isLinux = false; - const { getByTestId } = render(); + const { getByTestId } = render(); expect(getByTestId("window-menu")).toBeInTheDocument(); expect(getByTestId("window-minimize")).toBeInTheDocument(); @@ -82,7 +88,7 @@ describe(" in Windows and Linux", () => { mockConfig.isWindows = false; mockConfig.isLinux = true; - const { getByTestId } = render(); + const { getByTestId } = render(); expect(getByTestId("window-menu")).toBeInTheDocument(); expect(getByTestId("window-minimize")).toBeInTheDocument(); @@ -93,7 +99,7 @@ describe(" in Windows and Linux", () => { it("triggers ipc events on click", () => { mockConfig.isWindows = true; - const { getByTestId } = render(); + const { getByTestId } = render(); const menu = getByTestId("window-menu"); const minimize = getByTestId("window-minimize"); diff --git a/src/renderer/components/layout/topbar.module.scss b/src/renderer/components/layout/top-bar/top-bar.module.scss similarity index 100% rename from src/renderer/components/layout/topbar.module.scss rename to src/renderer/components/layout/top-bar/top-bar.module.scss diff --git a/src/renderer/components/layout/__tests__/topbar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx similarity index 84% rename from src/renderer/components/layout/__tests__/topbar.test.tsx rename to src/renderer/components/layout/top-bar/top-bar.test.tsx index 110ddef800..a2a93c0c4c 100644 --- a/src/renderer/components/layout/__tests__/topbar.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx @@ -20,14 +20,23 @@ */ import React from "react"; -import { render, fireEvent } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; -import { TopBar } from "../topbar"; -import { TopBarRegistry } from "../../../../extensions/registries"; +import { TopBar } from "./top-bar"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import { DiRender, renderFor } from "../../test-utils/renderFor"; +import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; +import { computed } from "mobx"; jest.mock("../../../../common/vars", () => { + const SemVer = require("semver").SemVer; + + const versionStub = new SemVer("1.0.0"); + return { isMac: true, + appSemVer: versionStub, }; }); @@ -76,12 +85,13 @@ jest.mock("@electron/remote", () => { }); describe("", () => { - beforeEach(() => { - TopBarRegistry.createInstance(); - }); + let di: ConfigurableDependencyInjectionContainer; + let render: DiRender; - afterEach(() => { - TopBarRegistry.resetInstance(); + beforeEach(() => { + di = getDiForUnitTesting(); + + render = renderFor(di); }); it("renders w/o errors", () => { @@ -129,13 +139,13 @@ describe("", () => { const testId = "testId"; const text = "an item"; - TopBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ + di.override(topBarItemsInjectable, () => computed(() => [ { components: { Item: () => {text}, }, }, - ]); + ])); const { getByTestId } = render(); diff --git a/src/renderer/components/layout/topbar.tsx b/src/renderer/components/layout/top-bar/top-bar.tsx similarity index 76% rename from src/renderer/components/layout/topbar.tsx rename to src/renderer/components/layout/top-bar/top-bar.tsx index 4705c6993c..a0a785666b 100644 --- a/src/renderer/components/layout/topbar.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.tsx @@ -19,22 +19,28 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import styles from "./topbar.module.scss"; +import styles from "./top-bar.module.scss"; import React, { useEffect, useMemo, useRef } from "react"; import { observer } from "mobx-react"; -import { TopBarRegistry } from "../../../extensions/registries"; -import { Icon } from "../icon"; +import type { IComputedValue } from "mobx"; +import { Icon } from "../../icon"; import { webContents, getCurrentWindow } from "@electron/remote"; import { observable } from "mobx"; -import { broadcastMessage, ipcRendererOn } from "../../../common/ipc"; -import { watchHistoryState } from "../../remote-helpers/history-updater"; -import { isActiveRoute, navigate } from "../../navigation"; -import { catalogRoute, catalogURL } from "../../../common/routes"; -import { IpcMainWindowEvents } from "../../../main/window-manager"; -import { isLinux, isWindows } from "../../../common/vars"; -import { cssNames } from "../../utils"; +import { broadcastMessage, ipcRendererOn } from "../../../../common/ipc"; +import { watchHistoryState } from "../../../remote-helpers/history-updater"; +import { isActiveRoute, navigate } from "../../../navigation"; +import { catalogRoute, catalogURL } from "../../../../common/routes"; +import { IpcMainWindowEvents } from "../../../../main/window-manager"; +import { isLinux, isWindows } from "../../../../common/vars"; +import { cssNames } from "../../../utils"; +import topBarItemsInjectable from "./top-bar-items/top-bar-items.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { TopBarRegistration } from "./top-bar-registration"; -interface Props extends React.HTMLAttributes { +interface Props extends React.HTMLAttributes {} + +interface Dependencies { + items: IComputedValue; } const prevEnabled = observable.box(false); @@ -48,34 +54,10 @@ ipcRendererOn("history:can-go-forward", (event, state: boolean) => { nextEnabled.set(state); }); -export const TopBar = observer(({ children, ...rest }: Props) => { +const NonInjectedTopBar = (({ items, children, ...rest }: Props & Dependencies) => { const elem = useRef(); const window = useMemo(() => getCurrentWindow(), []); - const renderRegisteredItems = () => { - const items = TopBarRegistry.getInstance().getItems(); - - if (!Array.isArray(items)) { - return null; - } - - return ( -
- {items.map((registration, index) => { - if (!registration?.components?.Item) { - return null; - } - - return ( -
- -
- ); - })} -
- ); - }; - const openContextMenu = () => { broadcastMessage(IpcMainWindowEvents.OPEN_CONTEXT_MENU); }; @@ -156,7 +138,7 @@ export const TopBar = observer(({ children, ...rest }: Props) => { />
- {renderRegisteredItems()} + {renderRegisteredItems(items.get())} {children} {(isWindows || isLinux) && (
@@ -174,3 +156,29 @@ export const TopBar = observer(({ children, ...rest }: Props) => {
); }); + +const renderRegisteredItems = (items: TopBarRegistration[]) => ( +
+ {items.map((registration, index) => { + if (!registration?.components?.Item) { + return null; + } + + return ( +
+ +
+ ); + })} +
+); + + + +export const TopBar = withInjectables(observer(NonInjectedTopBar), { + getProps: (di, props) => ({ + items: di.inject(topBarItemsInjectable), + + ...props, + }), +}); diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx index 7c2ed5b7be..957147cd1b 100644 --- a/src/renderer/components/menu/menu-actions.tsx +++ b/src/renderer/components/menu/menu-actions.tsx @@ -27,7 +27,7 @@ import { observer } from "mobx-react"; import { boundMethod, cssNames } from "../../utils"; import { ConfirmDialog } from "../confirm-dialog"; import { Icon, IconProps } from "../icon"; -import { Menu, MenuItem, MenuProps } from "../menu"; +import { Menu, MenuItem, MenuProps } from "./menu"; import uniqueId from "lodash/uniqueId"; import isString from "lodash/isString"; diff --git a/src/renderer/components/menu/menu.tsx b/src/renderer/components/menu/menu.tsx index 71ca69c6a8..2ef570a938 100644 --- a/src/renderer/components/menu/menu.tsx +++ b/src/renderer/components/menu/menu.tsx @@ -243,7 +243,9 @@ export class Menu extends React.Component { break; case "Space": - case "Enter": + // fallthrough + + case "Enter": { const focusedItem = this.focusedItem; if (focusedItem) { @@ -251,10 +253,12 @@ export class Menu extends React.Component { evt.preventDefault(); } break; + } case "ArrowUp": this.focusNextItem(true); break; + case "ArrowDown": this.focusNextItem(); break; diff --git a/src/renderer/components/monaco-editor/monaco-editor.tsx b/src/renderer/components/monaco-editor/monaco-editor.tsx index 96301e8a7f..4a6a4320ba 100644 --- a/src/renderer/components/monaco-editor/monaco-editor.tsx +++ b/src/renderer/components/monaco-editor/monaco-editor.tsx @@ -24,7 +24,8 @@ import React from "react"; import { observer } from "mobx-react"; import { action, computed, makeObservable, observable, reaction } from "mobx"; import { editor, Uri } from "monaco-editor"; -import { MonacoTheme, MonacoValidator, monacoValidators } from "./index"; +import type { MonacoTheme } from "./monaco-themes"; +import { MonacoValidator, monacoValidators } from "./monaco-validators"; import { debounce, merge } from "lodash"; import { cssNames, disposer } from "../../utils"; import { UserStore } from "../../../common/user-store"; diff --git a/src/renderer/components/select/select.scss b/src/renderer/components/select/select.scss index 8f5a3fa0e9..7e731a9aca 100644 --- a/src/renderer/components/select/select.scss +++ b/src/renderer/components/select/select.scss @@ -228,10 +228,15 @@ html { } .Select { + &__value-container { + margin-top: 2px; + margin-bottom: 2px; + } + &__control { box-shadow: 0 0 0 1px var(--inputControlBorder); background: var(--inputControlBackground); - border-radius: 5px; + border-radius: var(--border-radius); } &__single-value { diff --git a/src/renderer/components/switch/__tests__/switch.test.tsx b/src/renderer/components/switch/__tests__/switch.test.tsx new file mode 100644 index 0000000000..d5187e1417 --- /dev/null +++ b/src/renderer/components/switch/__tests__/switch.test.tsx @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import { fireEvent, render } from "@testing-library/react"; +import "@testing-library/jest-dom/extend-expect"; +import { Switch } from ".."; + +describe("", () => { + it("renders w/o errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("render label text", () => { + const { getByLabelText } = render(Test label); + + expect(getByLabelText("Test label")).toBeTruthy(); + }); + + it("passes disabled and checked attributes to input", () => { + const { container } = render(); + const checkbox = container.querySelector("input[type=checkbox]"); + + expect(checkbox).toHaveAttribute("disabled"); + expect(checkbox).toHaveAttribute("checked"); + }); + + it("onClick event fired", () => { + const onClick = jest.fn(); + const { getByTestId } = render(); + const switcher = getByTestId("switch"); + + fireEvent.click(switcher); + + expect(onClick).toHaveBeenCalled(); + }); + + it("onClick event not fired for disabled item", () => { + const onClick = jest.fn(); + const { getByTestId } = render(); + const switcher = getByTestId("switch"); + + fireEvent.click(switcher); + + expect(onClick).not.toHaveBeenCalled(); + }); +}); diff --git a/src/renderer/components/switch/form-switcher.tsx b/src/renderer/components/switch/form-switcher.tsx index 14df95f676..6499fb0b0f 100644 --- a/src/renderer/components/switch/form-switcher.tsx +++ b/src/renderer/components/switch/form-switcher.tsx @@ -35,6 +35,9 @@ const useStyles = makeStyles({ }, }); +/** + * @deprecated Use instead from "../switch.tsx". + */ export function FormSwitch(props: FormControlLabelProps) { const classes = useStyles(); diff --git a/src/renderer/components/switch/index.ts b/src/renderer/components/switch/index.ts index 60d44f3324..6987ce5b38 100644 --- a/src/renderer/components/switch/index.ts +++ b/src/renderer/components/switch/index.ts @@ -21,3 +21,4 @@ export * from "./switcher"; export * from "./form-switcher"; +export * from "./switch"; diff --git a/src/renderer/components/switch/switch.module.scss b/src/renderer/components/switch/switch.module.scss new file mode 100644 index 0000000000..b890919c61 --- /dev/null +++ b/src/renderer/components/switch/switch.module.scss @@ -0,0 +1,121 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +.Switch { + --thumb-size: 2rem; + --thumb-color: hsl(0 0% 100%); + --thumb-color-highlight: hsl(0 0% 100% / 25%); + + --track-size: calc(var(--thumb-size) * 2); + --track-padding: 2px; + --track-color-inactive: hsl(80 0% 35%); + --track-color-active: hsl(110, 60%, 60%); + + --thumb-position: 0%; + --thumb-transition-duration: .25s; + + --hover-highlight-size: 0; + + display: flex; + align-items: center; + gap: 2ch; + justify-content: space-between; + cursor: pointer; + user-select: none; + color: var(--textColorAccent); + font-weight: 500; + + & > input { + padding: var(--track-padding); + background: var(--track-color-inactive); + inline-size: var(--track-size); + block-size: var(--thumb-size); + border-radius: var(--track-size); + + appearance: none; + pointer-events: none; + border: none; + outline-offset: 5px; + box-sizing: content-box; + + flex-shrink: 0; + display: grid; + align-items: center; + grid: [track] 1fr / [track] 1fr; + + transition: background-color .25s ease; + + &::before { + content: ""; + cursor: pointer; + pointer-events: auto; + grid-area: track; + inline-size: var(--thumb-size); + block-size: var(--thumb-size); + background: var(--thumb-color); + box-shadow: 0 0 0 var(--hover-highlight-size) var(--thumb-color-highlight); + border-radius: 50%; + transform: translateX(var(--thumb-position)); + transition: + transform var(--thumb-transition-duration) ease, + box-shadow .25s ease; + } + + &:not(:disabled):hover::before { + --hover-highlight-size: .5rem; + } + + &:checked { + background: var(--track-color-active); + --thumb-position: 100%; + } + + &:disabled { + --track-color-inactive: hsl(80 0% 30%); + --thumb-color: transparent; + + &::before { + cursor: not-allowed; + box-shadow: inset 0 0 0 2px hsl(0 0% 0% / 40%); + } + } + + &:focus-visible { + box-shadow: 0 0 0 2px var(--blue); + } + } + + &.disabled { + cursor: not-allowed; + } +} + +@include theme-light { + .Switch { + --thumb-color-highlight: hsl(0 0% 0% / 25%); + + & > input { + &:disabled { + --track-color-inactive: hsl(80 0% 80%); + } + } + } +} diff --git a/src/renderer/components/switch/switch.tsx b/src/renderer/components/switch/switch.tsx new file mode 100644 index 0000000000..7bf5f2b3be --- /dev/null +++ b/src/renderer/components/switch/switch.tsx @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import styles from "./switch.module.scss"; + +import React, { ChangeEvent, HTMLProps } from "react"; +import { cssNames } from "../../utils"; + +interface Props extends Omit, "onChange"> { + onChange?: (checked: boolean, event: ChangeEvent) => void; +} + +export function Switch({ children, disabled, onChange, ...props }: Props) { + return ( + + ); +} diff --git a/src/renderer/components/switch/switcher.tsx b/src/renderer/components/switch/switcher.tsx index 749e5134c9..136175e394 100644 --- a/src/renderer/components/switch/switcher.tsx +++ b/src/renderer/components/switch/switcher.tsx @@ -31,6 +31,9 @@ interface Props extends SwitchProps { classes: Styles; } +/** + * @deprecated Use instead from "../switch.tsx". + */ export const Switcher = withStyles((theme: Theme) => createStyles({ root: { diff --git a/src/renderer/components/table/react-table.tsx b/src/renderer/components/table/react-table.tsx index de001c2a9c..e5cedff78f 100644 --- a/src/renderer/components/table/react-table.tsx +++ b/src/renderer/components/table/react-table.tsx @@ -20,8 +20,7 @@ */ import styles from "./react-table.module.scss"; -import React from "react"; -import { useCallback, useMemo } from "react"; +import React, { useCallback, useMemo } from "react"; import { useFlexLayout, useSortBy, useTable, UseTableOptions } from "react-table"; import { Icon } from "../icon"; import { cssNames } from "../../utils"; diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 55d1bbbb1b..b865368988 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -27,7 +27,6 @@ export * from "./ipc"; export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; export * from "./registries"; -export * from "./welcome-menu-registry"; export * from "./workloads-overview-detail-registry"; export * from "./catalog-category-registry"; export * from "./status-bar-registry"; diff --git a/src/renderer/initializers/registries.ts b/src/renderer/initializers/registries.ts index 3063213bc8..a27c47153b 100644 --- a/src/renderer/initializers/registries.ts +++ b/src/renderer/initializers/registries.ts @@ -33,8 +33,5 @@ export function initRegistries() { registries.KubeObjectMenuRegistry.createInstance(); registries.KubeObjectStatusRegistry.createInstance(); registries.StatusBarRegistry.createInstance(); - registries.WelcomeMenuRegistry.createInstance(); - registries.WelcomeBannerRegistry.createInstance(); registries.WorkloadsOverviewDetailRegistry.createInstance(); - registries.TopBarRegistry.createInstance(); } diff --git a/src/renderer/port-forward/port-forward-dialog.tsx b/src/renderer/port-forward/port-forward-dialog.tsx index c306b0aaf3..b50dbae596 100644 --- a/src/renderer/port-forward/port-forward-dialog.tsx +++ b/src/renderer/port-forward/port-forward-dialog.tsx @@ -31,7 +31,8 @@ import { Notifications } from "../components/notifications"; import { cssNames } from "../utils"; import { addPortForward, getPortForwards, modifyPortForward } from "./port-forward.store"; import type { ForwardedPort } from "./port-forward-item"; -import { aboutPortForwarding, openPortForward } from "."; +import { openPortForward } from "./port-forward-utils"; +import { aboutPortForwarding } from "./port-forward-notify"; import { Checkbox } from "../components/checkbox"; interface Props extends Partial { diff --git a/src/renderer/template.html b/src/renderer/template.html index c7df1ee507..fd4d35c5a6 100755 --- a/src/renderer/template.html +++ b/src/renderer/template.html @@ -6,6 +6,7 @@
+
diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 144ecf7d47..15aa055f92 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -27,6 +27,7 @@ import lensDarkThemeJson from "./themes/lens-dark.json"; import lensLightThemeJson from "./themes/lens-light.json"; import type { SelectOption } from "./components/select"; import type { MonacoEditorProps } from "./components/monaco-editor"; +import { defaultTheme } from "../common/vars"; export type ThemeId = string; @@ -40,7 +41,6 @@ export interface Theme { } export class ThemeStore extends Singleton { - static readonly defaultTheme = "lens-dark"; protected styles: HTMLStyleElement; // bundled themes from `themes/${themeId}.json` @@ -54,7 +54,7 @@ export class ThemeStore extends Singleton { } @computed get activeTheme(): Theme { - return this.themes.get(this.activeThemeId) ?? this.themes.get(ThemeStore.defaultTheme); + return this.themes.get(this.activeThemeId) ?? this.themes.get(defaultTheme); } @computed get themeOptions(): SelectOption[] { diff --git a/src/renderer/utils/createStorage.ts b/src/renderer/utils/createStorage.ts index 92c92c8f5a..c282994e61 100755 --- a/src/renderer/utils/createStorage.ts +++ b/src/renderer/utils/createStorage.ts @@ -50,7 +50,9 @@ export function createStorage(key: string, defaultValue: T) { try { storage.data = await fse.readJson(filePath); - } catch {} finally { + } catch { + // ignore error + } finally { if (!isTestEnv) { logger.info(`${logPrefix} loading finished for ${filePath}`); } diff --git a/src/renderer/utils/storageHelper.ts b/src/renderer/utils/storageHelper.ts index 805296b8d9..b5d5e6abde 100755 --- a/src/renderer/utils/storageHelper.ts +++ b/src/renderer/utils/storageHelper.ts @@ -21,7 +21,7 @@ // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) import { action, comparer, makeObservable, observable, toJS, when } from "mobx"; -import produce, { Draft, isDraft } from "immer"; +import { produce, Draft, isDraft } from "immer"; import { isEqual, isPlainObject } from "lodash"; import logger from "../../main/logger"; import { getHostedClusterId } from "../../common/utils"; diff --git a/tailwind.config.js b/tailwind.config.js index 1d2f2c9506..7357650c87 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -26,7 +26,14 @@ module.exports = { fontFamily: { sans: ["Roboto", "Helvetica", "Arial", "sans-serif"], }, - extend: {}, + extend: { + colors: { + textAccent: "var(--textColorAccent)", + textPrimary: "var(--textColorPrimary)", + textTertiary: "var(--textColorTertiary)", + textDimmed: "var(--textColorDimmed)", + }, + }, }, variants: { extend: {}, diff --git a/webpack.main.ts b/webpack.main.ts index f18dba4c0b..a3967dd071 100755 --- a/webpack.main.ts +++ b/webpack.main.ts @@ -27,6 +27,7 @@ import nodeExternals from "webpack-node-externals"; import ProgressBarPlugin from "progress-bar-webpack-plugin"; import * as vars from "./src/common/vars"; import getTSLoader from "./src/common/getTSLoader"; +import CircularDependencyPlugin from "circular-dependency-plugin"; const configs: { (): webpack.Configuration }[] = []; @@ -64,6 +65,12 @@ configs.push((): webpack.Configuration => { plugins: [ new ProgressBarPlugin(), new ForkTsCheckerPlugin(), + + new CircularDependencyPlugin({ + cwd: __dirname, + exclude: /node_modules/, + failOnError: true, + }), ].filter(Boolean), }; }); diff --git a/webpack.renderer.ts b/webpack.renderer.ts index 4e07d42ffb..8be6eb35dc 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -30,6 +30,7 @@ import ProgressBarPlugin from "progress-bar-webpack-plugin"; import ReactRefreshWebpackPlugin from "@pmmmwh/react-refresh-webpack-plugin"; import MonacoWebpackPlugin from "monaco-editor-webpack-plugin"; import getTSLoader from "./src/common/getTSLoader"; +import CircularDependencyPlugin from "circular-dependency-plugin"; export default [ webpackLensRenderer, @@ -173,6 +174,12 @@ export function webpackLensRenderer({ showVars = true } = {}): webpack.Configura inject: true, }), + new CircularDependencyPlugin({ + cwd: __dirname, + exclude: /node_modules/, + failOnError: true, + }), + new MiniCssExtractPlugin({ filename: "[name].css", }), diff --git a/yarn.lock b/yarn.lock index 76479fa361..70fb22d9fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1358,6 +1358,13 @@ dependencies: moment "^2.10.2" +"@types/circular-dependency-plugin@5.0.4": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@types/circular-dependency-plugin/-/circular-dependency-plugin-5.0.4.tgz#c5ccbd1d2bbb39b60e9859b39c6b826f60567ef2" + integrity sha512-J4XkMJfkGv3o3q2Ca821cufIBNBFms45fz+xD9tEESR0YqL5BlwETOwm2desSCdki2zdcPRhG9ZQCm/WITCEPQ== + dependencies: + "@types/webpack" "^4" + "@types/clean-css@*": version "4.2.1" resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.1.tgz#cb0134241ec5e6ede1b5344bc829668fd9871a8d" @@ -2711,6 +2718,15 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= +array.prototype.flat@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" + integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + array.prototype.flatmap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446" @@ -3415,17 +3431,17 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -cacheable-request@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" - integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== +cacheable-request@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" + integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== dependencies: clone-response "^1.0.2" get-stream "^5.1.0" http-cache-semantics "^4.0.0" keyv "^4.0.0" lowercase-keys "^2.0.0" - normalize-url "^4.1.0" + normalize-url "^6.0.1" responselike "^2.0.0" call-bind@^1.0.0: @@ -4431,21 +4447,28 @@ debug@3.1.0, debug@~3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: +debug@4, debug@^4.3.2: version "4.3.2" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== dependencies: ms "2.1.2" -debug@4.3.1: +debug@4.3.1, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== dependencies: ms "2.1.2" -debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: +debug@^3.0.0, debug@^3.1.1, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^3.1.0, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -4853,10 +4876,10 @@ domhandler@^2.3.0: dependencies: domelementtype "1" -dompurify@^2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c" - integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg== +dompurify@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.4.tgz#1cf5cf0105ccb4debdf6db162525bd41e6ddacc6" + integrity sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ== domutils@1.5.1: version "1.5.1" @@ -5436,11 +5459,47 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-module-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c" + integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ== + dependencies: + debug "^3.2.7" + find-up "^2.1.0" + pkg-dir "^2.0.0" + eslint-plugin-header@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz#6ce512432d57675265fac47292b50d1eff11acd6" integrity sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg== +eslint-plugin-import@^2.25.3: + version "2.25.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766" + integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.1" + has "^1.0.3" + is-core-module "^2.8.0" + is-glob "^4.0.3" + minimatch "^3.0.4" + object.values "^1.1.5" + resolve "^1.20.0" + tsconfig-paths "^3.11.0" + eslint-plugin-react-hooks@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz#318dbf312e06fab1c835a4abef00121751ac1172" @@ -6039,7 +6098,7 @@ find-root@^1.1.0: resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== -find-up@^2.0.0: +find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= @@ -6593,17 +6652,17 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -got@^11.8.0, got@^11.8.2: - version "11.8.2" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.2.tgz#7abb3959ea28c31f3576f1576c1effce23f33599" - integrity sha512-D0QywKgIe30ODs+fm8wMZiAcZjypcCodPNuMz5H9Mny7RJ+IjJ10BdmGW7OM7fHXP+O7r6ZwapQ/YQmMSvB0UQ== +got@^11.8.0, got@^11.8.3: + version "11.8.3" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.3.tgz#f496c8fdda5d729a90b4905d2b07dbd148170770" + integrity sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg== dependencies: "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" cacheable-lookup "^5.0.3" - cacheable-request "^7.0.1" + cacheable-request "^7.0.2" decompress-response "^6.0.0" http2-wrapper "^1.0.0-beta.5.2" lowercase-keys "^2.0.0" @@ -7439,6 +7498,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-core-module@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" + integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== + dependencies: + has "^1.0.3" + is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" @@ -9885,6 +9951,11 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a" integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + npm-audit-report@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/npm-audit-report/-/npm-audit-report-1.3.3.tgz#8226deeb253b55176ed147592a3995442f2179ed" @@ -10823,6 +10894,13 @@ pirates@^4.0.1: dependencies: node-modules-regexp "^1.0.0" +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -13527,9 +13605,9 @@ truncate-utf8-bytes@^1.0.0: utf8-byte-length "^1.0.1" ts-essentials@^7.0.2: - version "7.0.3" - resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" - integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== + version "7.0.2" + resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.2.tgz#e21142df8034dbd444cb9573ed204d0b85fc64fb" + integrity sha512-qWPVC1xZGdefbsgFP7tPo+bsgSA2ZIXL1XeEe5M2WoMZxIOr/HbsHxP/Iv75IFhiMHMDGL7cOOwi5SXcgx9mHw== ts-jest@26.5.6: version "26.5.6" @@ -13576,6 +13654,16 @@ ts-node@^10.4.0: make-error "^1.1.1" yn "3.1.1" +tsconfig-paths@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" + integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + tsconfig-paths@^3.9.0: version "3.9.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" @@ -13674,7 +13762,12 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== -type-fest@^1.0.2, type-fest@^1.4.0: +type-fest@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.0.2.tgz#3f9c39982859f385c77c38b7e5f1432b8a3661c6" + integrity sha512-a720oz3Kjbp3ll0zkeN9qjRhO7I34MKMhPGQiQJAmaZQZQ1lo+NWThK322f7sXV+kTg9B1Ybt16KgBXWgteT8w== + +type-fest@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==