From e862d5bf1d2ce4bc7c617d94b76c4a119e6f42d3 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 18 Jan 2022 17:16:50 +0100 Subject: [PATCH 1/8] Allow to customize terminal theme (#4666) (#4690) Allow to customize terminal theme, fix #2224 (cherry picked from commit a5e89b79d671968df3e44e47b26178ebb10c83e8) --- src/common/user-store/preferences-helpers.ts | 10 ++++ src/common/user-store/user-store.ts | 3 + .../components/+preferences/application.tsx | 24 ++++++-- src/renderer/components/dock/dock.scss | 5 +- src/renderer/components/dock/dock.tsx | 2 +- .../components/dock/terminal/terminal.ts | 25 ++------ src/renderer/theme.store.ts | 60 +++++++++++++------ src/renderer/themes/lens-dark.json | 2 +- src/renderer/themes/lens-light.json | 40 ++++++------- 9 files changed, 106 insertions(+), 65 deletions(-) diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index da4b7893b7..c2e9adce7e 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -67,6 +67,15 @@ const colorTheme: PreferenceDescription = { }, }; +const terminalTheme: PreferenceDescription = { + fromStore(val) { + return val || ""; + }, + toStore(val) { + return val || undefined; + }, +}; + const localeTimezone: PreferenceDescription = { fromStore(val) { return val || moment.tz.guess(true) || "UTC"; @@ -335,6 +344,7 @@ export const DESCRIPTORS = { httpsProxy, shell, colorTheme, + terminalTheme, localeTimezone, allowUntrustedCAs, allowTelemetry, diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index c6e0051e99..b0726d62ca 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -49,6 +49,7 @@ export class UserStore extends BaseStore /* implements UserStore @observable allowErrorReporting: boolean; @observable allowUntrustedCAs: boolean; @observable colorTheme: string; + @observable terminalTheme: string; @observable localeTimezone: string; @observable downloadMirror: string; @observable httpsProxy?: string; @@ -170,6 +171,7 @@ export class UserStore extends BaseStore /* implements UserStore this.httpsProxy = DESCRIPTORS.httpsProxy.fromStore(preferences?.httpsProxy); this.shell = DESCRIPTORS.shell.fromStore(preferences?.shell); this.colorTheme = DESCRIPTORS.colorTheme.fromStore(preferences?.colorTheme); + this.terminalTheme = DESCRIPTORS.terminalTheme.fromStore(preferences?.terminalTheme); this.localeTimezone = DESCRIPTORS.localeTimezone.fromStore(preferences?.localeTimezone); this.allowUntrustedCAs = DESCRIPTORS.allowUntrustedCAs.fromStore(preferences?.allowUntrustedCAs); this.allowTelemetry = DESCRIPTORS.allowTelemetry.fromStore(preferences?.allowTelemetry); @@ -194,6 +196,7 @@ export class UserStore extends BaseStore /* implements UserStore httpsProxy: DESCRIPTORS.httpsProxy.toStore(this.httpsProxy), shell: DESCRIPTORS.shell.toStore(this.shell), colorTheme: DESCRIPTORS.colorTheme.toStore(this.colorTheme), + terminalTheme: DESCRIPTORS.terminalTheme.toStore(this.terminalTheme), localeTimezone: DESCRIPTORS.localeTimezone.toStore(this.localeTimezone), allowUntrustedCAs: DESCRIPTORS.allowUntrustedCAs.toStore(this.allowUntrustedCAs), allowTelemetry: DESCRIPTORS.allowTelemetry.toStore(this.allowTelemetry), diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index bd1e940ca6..99edcfe84e 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -41,24 +41,38 @@ export const Application = observer(() => { const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const [shell, setShell] = React.useState(userStore.shell || ""); const extensionSettings = AppPreferenceRegistry.getInstance().getItems().filter((preference) => preference.showInPreferencesTab === "application"); + const themeStore = ThemeStore.getInstance(); return (

Application

- + userStore.terminalTheme = value} + /> +
- + {
-
+
diff --git a/src/renderer/components/dock/dock.scss b/src/renderer/components/dock/dock.scss index ffd28c4bb4..7a543ce934 100644 --- a/src/renderer/components/dock/dock.scss +++ b/src/renderer/components/dock/dock.scss @@ -60,11 +60,14 @@ .tab-content { position: relative; - background: var(--terminalBackground); flex: 1; overflow: hidden; transition: flex-basis 25ms ease-in; + &.terminal { + background: var(--terminalBackground); + } + > *:not(.Spinner) { position: absolute; left: 0; diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index 762a538c7b..5a9f1b26e1 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -97,7 +97,7 @@ class NonInjectedDock extends React.Component { if (!isOpen || !selectedTab) return null; return ( -
+
{this.renderTab(selectedTab)}
); diff --git a/src/renderer/components/dock/terminal/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts index 1b4ab74d16..39b42d6521 100644 --- a/src/renderer/components/dock/terminal/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -10,9 +10,9 @@ import { FitAddon } from "xterm-addon-fit"; import type { DockStore, TabId } from "../dock-store/dock.store"; import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; import { ThemeStore } from "../../../theme.store"; -import { boundMethod, disposer } from "../../../utils"; +import { disposer } from "../../../utils"; import { isMac } from "../../../../common/vars"; -import { camelCase, once } from "lodash"; +import { once } from "lodash"; import { UserStore } from "../../../../common/user-store"; import { clipboard } from "electron"; import logger from "../../../../common/logger"; @@ -44,23 +44,6 @@ export class Terminal { private scrollPos = 0; private disposer = disposer(); - @boundMethod - protected setTheme(colors: Record) { - if (!this.xterm) { - return; - } - - // Replacing keys stored in styles to format accepted by terminal - // E.g. terminalBrightBlack -> brightBlack - const colorPrefix = "terminal"; - const terminalColorEntries = Object.entries(colors) - .filter(([name]) => name.startsWith(colorPrefix)) - .map(([name, color]) => [camelCase(name.slice(colorPrefix.length)), color]); - const terminalColors = Object.fromEntries(terminalColorEntries); - - this.xterm.setOption("theme", terminalColors); - } - get elem() { return this.xterm?.element; } @@ -109,7 +92,9 @@ export class Terminal { window.addEventListener("resize", this.onResize); this.disposer.push( - reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, { + reaction(() => ThemeStore.getInstance().xtermColors, colors => { + this.xterm?.setOption("theme", colors); + }, { fireImmediately: true, }), dependencies.dockStore.onResize(this.onResize), diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 3c20c26544..19180285dd 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { computed, makeObservable, observable, reaction } from "mobx"; +import { comparer, computed, makeObservable, observable, reaction } from "mobx"; import { autoBind, Singleton } from "./utils"; import { UserStore } from "../common/user-store"; import logger from "../main/logger"; @@ -12,6 +12,7 @@ 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"; +import { camelCase } from "lodash"; export type ThemeId = string; @@ -25,7 +26,7 @@ export interface Theme { } export class ThemeStore extends Singleton { - protected styles: HTMLStyleElement; + private terminalColorPrefix = "terminal"; // bundled themes from `themes/${themeId}.json` private themes = observable.map({ @@ -33,14 +34,37 @@ export class ThemeStore extends Singleton { "lens-light": lensLightThemeJson as Theme, }); - @computed get activeThemeId(): string { + @computed get activeThemeId(): ThemeId { return UserStore.getInstance().colorTheme; } + @computed get terminalThemeId(): ThemeId { + return UserStore.getInstance().terminalTheme; + } + @computed get activeTheme(): Theme { return this.themes.get(this.activeThemeId) ?? this.themes.get(defaultTheme); } + @computed get terminalColors(): [string, string][] { + const theme = this.themes.get(this.terminalThemeId) ?? this.activeTheme; + + return Object + .entries(theme.colors) + .filter(([name]) => name.startsWith(this.terminalColorPrefix)); + } + + // Replacing keys stored in styles to format accepted by terminal + // E.g. terminalBrightBlack -> brightBlack + @computed get xtermColors(): Record { + return Object.fromEntries( + this.terminalColors.map(([name, color]) => [ + camelCase(name.replace(this.terminalColorPrefix, "")), + color, + ]), + ); + } + @computed get themeOptions(): SelectOption[] { return Array.from(this.themes).map(([themeId, theme]) => ({ label: theme.name, @@ -55,15 +79,19 @@ export class ThemeStore extends Singleton { autoBind(this); // auto-apply active theme - reaction(() => this.activeThemeId, themeId => { + reaction(() => ({ + themeId: this.activeThemeId, + terminalThemeId: this.terminalThemeId, + }), ({ themeId }) => { try { - this.applyTheme(this.getThemeById(themeId)); + this.applyTheme(themeId); } catch (err) { logger.error(err); UserStore.getInstance().resetTheme(); } }, { fireImmediately: true, + equals: comparer.shallow, }); } @@ -71,20 +99,18 @@ export class ThemeStore extends Singleton { return this.themes.get(themeId); } - protected applyTheme(theme: Theme) { - if (!this.styles) { - this.styles = document.createElement("style"); - this.styles.id = "lens-theme"; - document.head.append(this.styles); - } - const cssVars = Object.entries(theme.colors).map(([cssName, color]) => { - return `--${cssName}: ${color};`; + protected applyTheme(themeId: ThemeId) { + const theme = this.getThemeById(themeId); + const colors = Object.entries({ + ...theme.colors, + ...Object.fromEntries(this.terminalColors), }); - this.styles.textContent = `:root {\n${cssVars.join("\n")}}`; - // Adding universal theme flag which can be used in component styles - const body = document.querySelector("body"); + colors.forEach(([name, value]) => { + document.documentElement.style.setProperty(`--${name}`, value); + }); - body.classList.toggle("theme-light", theme.type === "light"); + // Adding universal theme flag which can be used in component styles + document.body.classList.toggle("theme-light", theme.type === "light"); } } diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.json index 023f9f6ccf..b48ed84cc7 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -1,5 +1,5 @@ { - "name": "Dark (Lens)", + "name": "Dark", "type": "dark", "description": "Original Lens dark theme", "author": "Mirantis", diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index 8525d31a56..7891f53321 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -1,5 +1,5 @@ { - "name": "Light (Lens)", + "name": "Light", "type": "light", "description": "Original Lens light theme", "author": "Mirantis", @@ -76,26 +76,26 @@ "logsBackground": "#24292e", "logsForeground": "#ffffff", "logRowHoverBackground": "#35373a", - "terminalBackground": "#24292e", - "terminalForeground": "#ffffff", - "terminalCursor": "#ffffff", - "terminalCursorAccent": "#000000", - "terminalSelection": "#ffffff77", - "terminalBlack": "#2e3436", - "terminalRed": "#cc0000", - "terminalGreen": "#4e9a06", - "terminalYellow": "#c4a000", - "terminalBlue": "#3465a4", - "terminalMagenta": "#75507b", - "terminalCyan": "#06989a", + "terminalBackground": "#ffffff", + "terminalForeground": "#2d2d2d", + "terminalCursor": "#2d2d2d", + "terminalCursorAccent": "#ffffff", + "terminalSelection": "#bfbfbf", + "terminalBlack": "#2d2d2d", + "terminalRed": "#cd3734 ", + "terminalGreen": "#18cf12", + "terminalYellow": "#acb300", + "terminalBlue": "#3d90ce", + "terminalMagenta": "#c100cd", + "terminalCyan": "#07c4b9", "terminalWhite": "#d3d7cf", - "terminalBrightBlack": "#555753", - "terminalBrightRed": "#ef2929", - "terminalBrightGreen": "#8ae234", - "terminalBrightYellow": "#fce94f", - "terminalBrightBlue": "#729fcf", - "terminalBrightMagenta": "#ad7fa8", - "terminalBrightCyan": "#34e2e2", + "terminalBrightBlack": "#a8a8a8", + "terminalBrightRed": "#ff6259", + "terminalBrightGreen": "#5cdb59", + "terminalBrightYellow": "#f8c000", + "terminalBrightBlue": "#008db6", + "terminalBrightMagenta": "#ee55f8", + "terminalBrightCyan": "#50e8df", "terminalBrightWhite": "#eeeeec", "dialogTextColor": "#87909c", "dialogBackground": "#ffffff", From 74d92d09d9da58a012f8973e940ba979e9e5d148 Mon Sep 17 00:00:00 2001 From: Alex Andreev Date: Wed, 19 Jan 2022 14:31:15 +0300 Subject: [PATCH 2/8] Fix: remove hotbar name from status bar (#4679) * Show tooltip with hotbar name manually Signed-off-by: Alex Andreev * Remove this.refreshPosition() call Signed-off-by: Alex Andreev * Remove hotbar name from status bar Signed-off-by: Alex Andreev * Convert HotbarSelector to use css modules Signed-off-by: Alex Andreev * Remove unused export Signed-off-by: Alex Andreev * Fixing linter Signed-off-by: Alex Andreev * Clean up Signed-off-by: Alex Andreev * Invert invisible prop Signed-off-by: Alex Andreev * Linter fix Signed-off-by: Alex Andreev --- src/renderer/bootstrap.tsx | 3 - .../cluster-manager/active-hotbar-name.tsx | 36 -------- .../cluster-manager/bottom-bar.test.tsx | 77 +---------------- ...ector.scss => hotbar-selector.module.scss} | 10 ++- .../components/hotbar/hotbar-selector.tsx | 83 +++++++++++++------ src/renderer/components/tooltip/tooltip.scss | 13 +-- src/renderer/components/tooltip/tooltip.tsx | 10 ++- src/renderer/initializers/index.ts | 1 - .../initializers/status-bar-registry.tsx | 19 ----- 9 files changed, 85 insertions(+), 167 deletions(-) delete mode 100644 src/renderer/components/cluster-manager/active-hotbar-name.tsx rename src/renderer/components/hotbar/{hotbar-selector.scss => hotbar-selector.module.scss} (85%) delete mode 100644 src/renderer/initializers/status-bar-registry.tsx diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 81f4a52e45..9be509ab61 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -98,9 +98,6 @@ export async function bootstrap(di: DependencyInjectionContainer) { logger.info(`${logPrefix} initializing IpcRendererListeners`); initializers.initIpcRendererListeners(extensionLoader); - logger.info(`${logPrefix} initializing StatusBarRegistry`); - initializers.initStatusBarRegistry(); - extensionLoader.init(); const extensionDiscovery = di.inject(extensionDiscoveryInjectable); diff --git a/src/renderer/components/cluster-manager/active-hotbar-name.tsx b/src/renderer/components/cluster-manager/active-hotbar-name.tsx deleted file mode 100644 index 191a30957e..0000000000 --- a/src/renderer/components/cluster-manager/active-hotbar-name.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import React from "react"; -import { observer } from "mobx-react"; -import { Icon } from "../icon"; -import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; -import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; - -interface Dependencies { - openCommandOverlay: (component: React.ReactElement) => void; - activeHotbarName: () => string | undefined; -} - -const NonInjectedActiveHotbarName = observer(({ openCommandOverlay, activeHotbarName }: Dependencies) => ( -
openCommandOverlay()} - > - - {activeHotbarName()} -
-)); - -export const ActiveHotbarName = withInjectables(NonInjectedActiveHotbarName, { - getProps: (di, props) => ({ - activeHotbarName: () => di.inject(hotbarManagerInjectable).getActive()?.name, - openCommandOverlay: di.inject(commandOverlayInjectable).open, - ...props, - }), -}); diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx index dcad07d592..97539f0ea3 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx @@ -4,68 +4,24 @@ */ import React from "react"; -import mockFs from "mock-fs"; -import { fireEvent } from "@testing-library/react"; +import { render } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import { BottomBar } from "./bottom-bar"; import { StatusBarRegistry } from "../../../extensions/registries"; -import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; -import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command"; -import { ActiveHotbarName } from "./active-hotbar-name"; -import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import { DiRender, renderFor } from "../test-utils/renderFor"; -import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; -import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; -import { getEmptyHotbar } from "../../../common/hotbar-types"; - jest.mock("electron", () => ({ app: { - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - }, - ipcMain: { - handle: jest.fn(), - on: jest.fn(), - removeAllListeners: jest.fn(), - off: jest.fn(), - send: jest.fn(), + getPath: () => "/foo", }, })); -const foobarHotbar = getEmptyHotbar("foobar"); - describe("", () => { - let di: DependencyInjectionContainer; - let render: DiRender; - - beforeEach(async () => { - const mockOpts = { - "tmp": { - "test-store.json": JSON.stringify({}), - }, - }; - - di = getDiForUnitTesting({ doGeneralOverrides: true }); - - mockFs(mockOpts); - - render = renderFor(di); - - di.override(hotbarManagerInjectable, () => ({ - getActive: () => foobarHotbar, - } as any)); - - await di.runSetups(); - + beforeEach(() => { StatusBarRegistry.createInstance(); }); afterEach(() => { StatusBarRegistry.resetInstance(); - mockFs.restore(); }); it("renders w/o errors", () => { @@ -111,33 +67,6 @@ describe("", () => { expect(getByTestId(testId)).toHaveTextContent(text); }); - it("shows active hotbar name", () => { - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { item: () => }, - ]); - const { getByTestId } = render(); - - expect(getByTestId("current-hotbar-name")).toHaveTextContent("foobar"); - }); - - it("opens command palette on click", () => { - const mockOpen = jest.fn(); - - di.override(commandOverlayInjectable, () => ({ - open: mockOpen, - }) as any); - - StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ - { item: () => }, - ]); - const { getByTestId } = render(); - const activeHotbar = getByTestId("current-hotbar-name"); - - fireEvent.click(activeHotbar); - - - expect(mockOpen).toHaveBeenCalledWith(); - }); it("sort positioned items properly", () => { StatusBarRegistry.getInstance().getItems = jest.fn().mockImplementationOnce(() => [ diff --git a/src/renderer/components/hotbar/hotbar-selector.scss b/src/renderer/components/hotbar/hotbar-selector.module.scss similarity index 85% rename from src/renderer/components/hotbar/hotbar-selector.scss rename to src/renderer/components/hotbar/hotbar-selector.module.scss index 711486ba49..b95823f7fe 100644 --- a/src/renderer/components/hotbar/hotbar-selector.scss +++ b/src/renderer/components/hotbar/hotbar-selector.module.scss @@ -4,6 +4,8 @@ */ .HotbarSelector { + display: flex; + align-items: center; height: 26px; background-color: var(--layoutBackground); position: relative; @@ -17,7 +19,13 @@ top: -20px; } - .SelectorIndex { + .HotbarIndex { + display: flex; + flex-grow: 1; + align-items: center; + } + + .Badge { cursor: pointer; background: var(--secondaryBackground); width: 100%; diff --git a/src/renderer/components/hotbar/hotbar-selector.tsx b/src/renderer/components/hotbar/hotbar-selector.tsx index 3922108ddc..68a8f0e81b 100644 --- a/src/renderer/components/hotbar/hotbar-selector.tsx +++ b/src/renderer/components/hotbar/hotbar-selector.tsx @@ -3,21 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import "./hotbar-selector.scss"; -import React from "react"; +import styles from "./hotbar-selector.module.scss"; +import React, { useRef, useState } from "react"; import { Icon } from "../icon"; import { Badge } from "../badge"; import hotbarManagerInjectable from "../../../common/hotbar-store.injectable"; import { HotbarSwitchCommand } from "./hotbar-switch-command"; -import { TooltipPosition } from "../tooltip"; +import { Tooltip, TooltipPosition } from "../tooltip"; import { observer } from "mobx-react"; import type { Hotbar } from "../../../common/hotbar-types"; import { withInjectables } from "@ogre-tools/injectable-react"; import commandOverlayInjectable from "../command-palette/command-overlay.injectable"; - -export interface HotbarSelectorProps { - hotbar: Hotbar; -} +import { cssNames } from "../../utils"; interface Dependencies { hotbarManager: { @@ -29,25 +26,63 @@ interface Dependencies { openCommandOverlay: (component: React.ReactElement) => void; } -const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => ( -
- hotbarManager.switchToPrevious()} /> -
- openCommandOverlay()} - tooltip={{ - preferredPositions: [TooltipPosition.TOP, TooltipPosition.TOP_LEFT], - children: hotbar.name, - }} - className="SelectorIndex" +export interface HotbarSelectorProps extends Partial { + hotbar: Hotbar; +} + +const NonInjectedHotbarSelector = observer(({ hotbar, hotbarManager, openCommandOverlay }: HotbarSelectorProps & Dependencies) => { + const [tooltipVisible, setTooltipVisible] = useState(false); + const tooltipTimeout = useRef(); + + function clearTimer() { + clearTimeout(tooltipTimeout.current); + } + + function onTooltipShow() { + setTooltipVisible(true); + clearTimer(); + tooltipTimeout.current = setTimeout(() => setTooltipVisible(false), 1500); + } + + function onArrowClick(switchTo: () => void) { + onTooltipShow(); + switchTo(); + } + + function onMouseEvent(event: React.MouseEvent) { + clearTimer(); + setTooltipVisible(event.type == "mouseenter"); + } + + return ( +
+ onArrowClick(hotbarManager.switchToPrevious)} /> +
+ openCommandOverlay()} + className={styles.Badge} + onMouseEnter={onMouseEvent} + onMouseLeave={onMouseEvent} + /> + + {hotbar.name} + +
+ onArrowClick(hotbarManager.switchToNext)} />
- hotbarManager.switchToNext()} /> -
-)); + ); +}); export const HotbarSelector = withInjectables(NonInjectedHotbarSelector, { getProps: (di, props) => ({ diff --git a/src/renderer/components/tooltip/tooltip.scss b/src/renderer/components/tooltip/tooltip.scss index 8747cbf2c6..10b7669679 100644 --- a/src/renderer/components/tooltip/tooltip.scss +++ b/src/renderer/components/tooltip/tooltip.scss @@ -25,14 +25,15 @@ pointer-events: none; transition: opacity 150ms 150ms ease-in-out; z-index: 100000; - opacity: 1; box-shadow: 0 8px 16px rgba(0,0,0,0.24); + left: 0; + top: 0; + opacity: 0; + visibility: hidden; - &.invisible { - left: 0; - top: 0; - opacity: 0; - visibility: hidden; + &.visible { + opacity: 1; + visibility: visible; } &:empty { diff --git a/src/renderer/components/tooltip/tooltip.tsx b/src/renderer/components/tooltip/tooltip.tsx index cf0811b7a6..3e35c837a5 100644 --- a/src/renderer/components/tooltip/tooltip.tsx +++ b/src/renderer/components/tooltip/tooltip.tsx @@ -54,7 +54,7 @@ export class Tooltip extends React.Component { @observable.ref elem: HTMLElement; @observable activePosition: TooltipPosition; - @observable isVisible = !!this.props.visible; + @observable isVisible = false; constructor(props: TooltipProps) { super(props); @@ -78,6 +78,10 @@ export class Tooltip extends React.Component { this.hoverTarget.addEventListener("mouseleave", this.onLeaveTarget); } + componentDidUpdate() { + this.refreshPosition(); + } + componentWillUnmount() { this.hoverTarget.removeEventListener("mouseenter", this.onEnterTarget); this.hoverTarget.removeEventListener("mouseleave", this.onLeaveTarget); @@ -210,9 +214,9 @@ export class Tooltip extends React.Component { } render() { - const { style, formatters, usePortal, children } = this.props; + const { style, formatters, usePortal, children, visible } = this.props; const className = cssNames("Tooltip", this.props.className, formatters, this.activePosition, { - invisible: !this.isVisible, + visible: visible ?? this.isVisible, formatter: !!formatters, }); const tooltip = ( diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index 27fd3bcd6e..3965d45fcf 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -12,4 +12,3 @@ export * from "./kube-object-menu-registry"; export * from "./registries"; export * from "./workloads-overview-detail-registry"; export * from "./catalog-category-registry"; -export * from "./status-bar-registry"; diff --git a/src/renderer/initializers/status-bar-registry.tsx b/src/renderer/initializers/status-bar-registry.tsx deleted file mode 100644 index c0a255a610..0000000000 --- a/src/renderer/initializers/status-bar-registry.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import React from "react"; -import { StatusBarRegistry } from "../../extensions/registries"; -import { ActiveHotbarName } from "../components/cluster-manager/active-hotbar-name"; - -export function initStatusBarRegistry() { - StatusBarRegistry.getInstance().add([ - { - components: { - Item: () => , - position: "left", - }, - }, - ]); -} From 79c01daf6ab8d055d49f005153f1d61b7758793f Mon Sep 17 00:00:00 2001 From: Panu Horsmalahti Date: Wed, 19 Jan 2022 14:57:42 +0200 Subject: [PATCH 3/8] Support extending KubernetesCluster in extensions (#4702) * Support extending KubernetesCluster in extensions Signed-off-by: Panu Horsmalahti * Simplify getItemsByEntityClass Signed-off-by: Panu Horsmalahti * Make apiVersion string. Signed-off-by: Panu Horsmalahti * Improve entity loading for extension custom types. Signed-off-by: Panu Horsmalahti * Improve comment. Signed-off-by: Panu Horsmalahti * Fix lint. Signed-off-by: Panu Horsmalahti * Properly handle loading custom entity in cluster-frame Signed-off-by: Panu Horsmalahti * Avoid .bind with .loadOnClusterRenderer Signed-off-by: Panu Horsmalahti * Fix lint. Signed-off-by: Panu Horsmalahti * Revert style change. Signed-off-by: Panu Horsmalahti * Make loadOnClusterRenderer arrow function again, revert autoInitExtensions change as unnecessary Signed-off-by: Panu Horsmalahti * Remove commented code. Signed-off-by: Panu Horsmalahti * Document extending KubernetesCluster in extension guides. Signed-off-by: Panu Horsmalahti --- docs/extensions/guides/README.md | 1 + .../guides/extending-kubernetes-cluster.md | 69 +++++++++++++++++++ mkdocs.yml | 1 + .../catalog-entities/kubernetes-cluster.ts | 4 +- .../extension-loader/extension-loader.ts | 68 ++++++++++++------ src/extensions/lens-extension.ts | 6 +- src/main/catalog/catalog-entity-registry.ts | 6 +- src/renderer/api/catalog-entity-registry.ts | 17 ++++- .../init-cluster-frame/init-cluster-frame.ts | 9 +-- .../init-root-frame/init-root-frame.ts | 8 +-- 10 files changed, 154 insertions(+), 35 deletions(-) create mode 100644 docs/extensions/guides/extending-kubernetes-cluster.md diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md index 434aec7444..09a0bce0a1 100644 --- a/docs/extensions/guides/README.md +++ b/docs/extensions/guides/README.md @@ -20,6 +20,7 @@ Each guide or code sample includes the following: | [Main process extension](main-extension.md) | Main.LensExtension | | [Renderer process extension](renderer-extension.md) | Renderer.LensExtension | | [Resource stack (cluster feature)](resource-stack.md) | | +| [Extending KubernetesCluster)](extending-kubernetes-cluster.md) | | | [Stores](stores.md) | | | [Components](components.md) | | | [KubeObjectListLayout](kube-object-list-layout.md) | | diff --git a/docs/extensions/guides/extending-kubernetes-cluster.md b/docs/extensions/guides/extending-kubernetes-cluster.md new file mode 100644 index 0000000000..5c8170a2fe --- /dev/null +++ b/docs/extensions/guides/extending-kubernetes-cluster.md @@ -0,0 +1,69 @@ +# Extending KubernetesCluster + +Extension can specify it's own subclass of Common.Catalog.KubernetesCluster. Extension can also specify a new Category for it in the Catalog. + +## Extending Common.Catalog.KubernetesCluster + +``` typescript +import { Common } from "@k8slens/extensions"; + +// The kind must be different from KubernetesCluster's kind +export const kind = "ManagedDevCluster"; + +export class ManagedDevCluster extends Common.Catalog.KubernetesCluster { + public static readonly kind = kind; + + public readonly kind = kind; +} +``` + +## Extending Common.Catalog.CatalogCategory + +These custom Catalog entities can be added a new Category in the Catalog. + +``` typescript +import { Common } from "@k8slens/extensions"; +import { kind, ManagedDevCluster } from "../entities/ManagedDevCluster"; + +class ManagedDevClusterCategory extends Common.Catalog.CatalogCategory { + public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; + public readonly kind = "CatalogCategory"; + public metadata = { + name: "Managed Dev Clusters", + icon: "" + }; + public spec: Common.Catalog.CatalogCategorySpec = { + group: "entity.k8slens.dev", + versions: [ + { + name: "v1alpha1", + entityClass: ManagedDevCluster as any, + }, + ], + names: { + kind + }, + }; +} + +export { ManagedDevClusterCategory }; +export type { ManagedDevClusterCategory as ManagedDevClusterCategoryType }; +``` + +The category needs to be registered in the `onActivate()` method both in main and renderer + +``` typescript +// in main's on onActivate +Main.Catalog.catalogCategories.add(new ManagedDevClusterCategory()); +``` + +``` typescript +// in renderer's on onActivate +Renderer.Catalog.catalogCategories.add(new ManagedDevClusterCategory()); +``` + +You can then add the entities to the Catalog as a new source: + +``` typescript +this.addCatalogSource("managedDevClusters", this.managedDevClusters); +``` diff --git a/mkdocs.yml b/mkdocs.yml index 4ed763e6d3..b869a63ee9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -24,6 +24,7 @@ nav: - Renderer Extension: extensions/guides/renderer-extension.md - Catalog: extensions/guides/catalog.md - Resource Stack: extensions/guides/resource-stack.md + - Extending KubernetesCluster: extensions/guides/extending-kubernetes-cluster.md - Stores: extensions/guides/stores.md - Working with MobX: extensions/guides/working-with-mobx.md - Protocol Handlers: extensions/guides/protocol-handlers.md diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 2dfb3750ff..813d5d3866 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -59,8 +59,8 @@ export interface KubernetesClusterStatus extends CatalogEntityStatus { } export class KubernetesCluster extends CatalogEntity { - public static readonly apiVersion = "entity.k8slens.dev/v1alpha1"; - public static readonly kind = "KubernetesCluster"; + public static readonly apiVersion: string = "entity.k8slens.dev/v1alpha1"; + public static readonly kind: string = "KubernetesCluster"; public readonly apiVersion = KubernetesCluster.apiVersion; public readonly kind = KubernetesCluster.kind; diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index 675786dfe3..dd86857cf3 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -270,11 +270,12 @@ export class ExtensionLoader { }); }; - loadOnClusterRenderer = (entity: KubernetesCluster) => { + loadOnClusterRenderer = (getCluster: () => KubernetesCluster) => { logger.debug(`${logModule}: load on cluster renderer (dashboard)`); this.autoInitExtensions(async (extension: LensRendererExtension) => { - if ((await extension.isEnabledForCluster(entity)) === false) { + // getCluster must be a callback, as the entity might be available only after an extension has been loaded + if ((await extension.isEnabledForCluster(getCluster())) === false) { return []; } @@ -299,11 +300,15 @@ export class ExtensionLoader { }); }; - protected autoInitExtensions(register: (ext: LensExtension) => Promise) { - const loadingExtensions: ExtensionLoading[] = []; + protected async loadExtensions(installedExtensions: Map, register: (ext: LensExtension) => Promise) { + // Steps of the function: + // 1. require and call .activate for each Extension + // 2. Wait until every extension's onActivate has been resolved + // 3. Call .enable for each extension + // 4. Return ExtensionLoading[] - reaction(() => this.toJSON(), async installedExtensions => { - for (const [extId, extension] of installedExtensions) { + const extensions = [...installedExtensions.entries()] + .map(([extId, extension]) => { const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); if (extension.isCompatible && extension.isEnabled && !alreadyInit) { @@ -312,7 +317,8 @@ export class ExtensionLoader { if (!LensExtensionClass) { this.nonInstancesByName.add(extension.manifest.name); - continue; + + return null; } const instance = this.dependencies.createExtensionInstance( @@ -320,27 +326,49 @@ export class ExtensionLoader { extension, ); - const loaded = instance.enable(register).catch((err) => { - logger.error(`${logModule}: failed to enable`, { ext: extension, err }); - }); - - loadingExtensions.push({ + return { + extId, + instance, isBundled: extension.isBundled, - loaded, - }); - this.instances.set(extId, instance); + activated: instance.activate(), + }; } catch (err) { logger.error(`${logModule}: activation extension error`, { ext: extension, err }); } } else if (!extension.isEnabled && alreadyInit) { this.removeInstance(extId); } - } - }, { - fireImmediately: true, - }); - return loadingExtensions; + return null; + }) + // Remove null values + .filter(extension => Boolean(extension)); + + // We first need to wait until each extension's `onActivate` is resolved, + // as this might register new catalog categories. Afterwards we can safely .enable the extension. + await Promise.all(extensions.map(extension => extension.activated)); + + // Return ExtensionLoading[] + return extensions.map(extension => { + const loaded = extension.instance.enable(register).catch((err) => { + logger.error(`${logModule}: failed to enable`, { ext: extension, err }); + }); + + this.instances.set(extension.extId, extension.instance); + + return { + isBundled: extension.isBundled, + loaded, + }; + }); + } + + protected autoInitExtensions(register: (ext: LensExtension) => Promise) { + // Setup reaction to load extensions on JSON changes + reaction(() => this.toJSON(), installedExtensions => this.loadExtensions(installedExtensions, register)); + + // Load initial extensions + return this.loadExtensions(this.toJSON(), register); } protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | null { diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index ae42ec37ff..c892464ab8 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -86,7 +86,6 @@ export class LensExtension { } try { - await this.onActivate(); this._isEnabled = true; this[Disposers].push(...await register(this)); @@ -113,6 +112,11 @@ export class LensExtension { } } + @action + activate() { + return this.onActivate(); + } + protected onActivate(): Promise | void { return; } diff --git a/src/main/catalog/catalog-entity-registry.ts b/src/main/catalog/catalog-entity-registry.ts index 9e9c92f526..65f60d8e4e 100644 --- a/src/main/catalog/catalog-entity-registry.ts +++ b/src/main/catalog/catalog-entity-registry.ts @@ -4,7 +4,7 @@ */ import { action, computed, IComputedValue, IObservableArray, makeObservable, observable } from "mobx"; -import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor, CatalogEntityKindData } from "../../common/catalog"; +import { CatalogCategoryRegistry, catalogCategoryRegistry, CatalogEntity, CatalogEntityConstructor } from "../../common/catalog"; import { iter } from "../../common/utils"; export class CatalogEntityRegistry { @@ -43,8 +43,8 @@ export class CatalogEntityRegistry { return this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind) as T[]; } - getItemsByEntityClass({ apiVersion, kind }: CatalogEntityKindData & CatalogEntityConstructor): T[] { - return this.getItemsForApiKind(apiVersion, kind); + getItemsByEntityClass(constructor: CatalogEntityConstructor): T[] { + return this.items.filter((item) => item instanceof constructor) as T[]; } } diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index e6becb6cc1..85477245a4 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -47,10 +47,25 @@ export class CatalogEntityRegistry { makeObservable(this); } - get activeEntity(): CatalogEntity | null { + protected getActiveEntityById() { return this._entities.get(this.activeEntityId) || null; } + get activeEntity(): CatalogEntity | null { + const entity = this.getActiveEntityById(); + + // If the entity was not found but there are rawEntities to be processed, + // try to process them and return the entity. + // This might happen if an extension registered a new Catalog category. + if (this.activeEntityId && !entity && this.rawEntities.length > 0) { + this.processRawEntities(); + + return this.getActiveEntityById(); + } + + return entity; + } + set activeEntity(raw: CatalogEntity | string | null) { if (raw) { const id = typeof raw === "string" diff --git a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts index a8ff4e6ad9..22acc43be5 100644 --- a/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts +++ b/src/renderer/frames/cluster-frame/init-cluster-frame/init-cluster-frame.ts @@ -19,7 +19,7 @@ import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; interface Dependencies { hostedCluster: Cluster; - loadExtensions: (entity: CatalogEntity) => void; + loadExtensions: (getCluster: () => CatalogEntity) => void; catalogEntityRegistry: CatalogEntityRegistry; frameRoutingId: number; emitEvent: (event: AppEvent) => void; @@ -47,11 +47,12 @@ export const initClusterFrame = catalogEntityRegistry.activeEntity = hostedCluster.id; - // Only load the extensions once the catalog has been populated + // Only load the extensions once the catalog has been populated. + // Note that the Catalog might still have unprocessed entities until the extensions are fully loaded. when( - () => Boolean(catalogEntityRegistry.activeEntity), + () => catalogEntityRegistry.items.length > 0, () => - loadExtensions(catalogEntityRegistry.activeEntity as KubernetesCluster), + loadExtensions(() => catalogEntityRegistry.activeEntity as KubernetesCluster), { timeout: 15_000, onError: (error) => { diff --git a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts index 61d1d7883e..c166847e2c 100644 --- a/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts +++ b/src/renderer/frames/root-frame/init-root-frame/init-root-frame.ts @@ -11,15 +11,15 @@ import type { ExtensionLoading } from "../../../../extensions/extension-loader"; import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; interface Dependencies { - loadExtensions: () => ExtensionLoading[] + loadExtensions: () => Promise; // TODO: Move usages of third party library behind abstraction - ipcRenderer: { send: (name: string) => void } + ipcRenderer: { send: (name: string) => void }; // TODO: Remove dependencies being here only for correct timing of initialization bindProtocolAddRouteHandlers: () => void; lensProtocolRouterRenderer: { init: () => void }; - catalogEntityRegistry: CatalogEntityRegistry + catalogEntityRegistry: CatalogEntityRegistry; } const logPrefix = "[ROOT-FRAME]:"; @@ -40,7 +40,7 @@ export const initRootFrame = // maximum time to let bundled extensions finish loading const timeout = delay(10000); - const loadingExtensions = loadExtensions(); + const loadingExtensions = await loadExtensions(); const loadingBundledExtensions = loadingExtensions .filter((e) => e.isBundled) From b7cb10521ef8dec5846b88bf6d32cbdb2c9c7f5b Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Wed, 19 Jan 2022 15:27:34 +0200 Subject: [PATCH 4/8] Drop old (< 3.0) cluster-store migrations (#4718) --- src/common/__tests__/cluster-store.test.ts | 154 ------------------- src/migrations/cluster-store/2.0.0-beta.2.ts | 24 --- src/migrations/cluster-store/2.4.1.ts | 22 --- src/migrations/cluster-store/2.6.0-beta.2.ts | 27 ---- src/migrations/cluster-store/2.6.0-beta.3.ts | 53 ------- src/migrations/cluster-store/2.7.0-beta.0.ts | 22 --- src/migrations/cluster-store/2.7.0-beta.1.ts | 35 ----- src/migrations/cluster-store/index.ts | 12 -- 8 files changed, 349 deletions(-) delete mode 100644 src/migrations/cluster-store/2.0.0-beta.2.ts delete mode 100644 src/migrations/cluster-store/2.4.1.ts delete mode 100644 src/migrations/cluster-store/2.6.0-beta.2.ts delete mode 100644 src/migrations/cluster-store/2.6.0-beta.3.ts delete mode 100644 src/migrations/cluster-store/2.7.0-beta.0.ts delete mode 100644 src/migrations/cluster-store/2.7.0-beta.1.ts diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index ee93106adf..f11bd2d593 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -5,7 +5,6 @@ import fs from "fs"; import mockFs from "mock-fs"; -import yaml from "js-yaml"; import path from "path"; import fse from "fs-extra"; import type { Cluster } from "../cluster/cluster"; @@ -334,159 +333,6 @@ users: }); }); - describe("pre 2.0 config with an existing cluster", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - - const mockOpts = { - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "1.0.0", - }, - }, - cluster1: minimalValidKubeConfig, - }), - }, - }; - - mockFs(mockOpts); - - clusterStore = mainDi.inject(clusterStoreInjectable); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("migrates to modern format with kubeconfig in a file", async () => { - const config = clusterStore.clustersList[0].kubeConfigPath; - - expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`); - }); - }); - - describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "2.4.1", - }, - }, - cluster1: { - kubeConfig: JSON.stringify({ - apiVersion: "v1", - clusters: [ - { - cluster: { - server: "https://10.211.55.6:8443", - }, - name: "minikube", - }, - ], - contexts: [ - { - context: { - cluster: "minikube", - user: "minikube", - name: "minikube", - }, - name: "minikube", - }, - ], - "current-context": "minikube", - kind: "Config", - preferences: {}, - users: [ - { - name: "minikube", - user: { - "client-certificate": "/Users/foo/.minikube/client.crt", - "client-key": "/Users/foo/.minikube/client.key", - "auth-provider": { - config: { - "access-token": ["should be string"], - expiry: ["should be string"], - }, - }, - }, - }, - ], - }), - }, - }), - }, - }; - - mockFs(mockOpts); - - clusterStore = mainDi.inject(clusterStoreInjectable); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("replaces array format access token and expiry into string", async () => { - const file = clusterStore.clustersList[0].kubeConfigPath; - const config = fs.readFileSync(file, "utf8"); - const kc = yaml.load(config) as Record; - - expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe( - "should be string", - ); - expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe( - "should be string", - ); - }); - }); - - describe("pre 2.6.0 config with a cluster icon", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "some-directory-for-user-data": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "2.4.1", - }, - }, - cluster1: { - kubeConfig: minimalValidKubeConfig, - icon: "icon_path", - preferences: { - terminalCWD: "/some-directory-for-user-data", - }, - }, - }), - icon_path: testDataIcon, - }, - }; - - mockFs(mockOpts); - - clusterStore = mainDi.inject(clusterStoreInjectable); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("moves the icon into preferences", async () => { - const storedClusterData = clusterStore.clustersList[0]; - - 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); - }); - }); - describe("pre 3.6.0-beta.1 config with an existing cluster", () => { beforeEach(() => { ClusterStore.resetInstance(); diff --git a/src/migrations/cluster-store/2.0.0-beta.2.ts b/src/migrations/cluster-store/2.0.0-beta.2.ts deleted file mode 100644 index dafd87bf7f..0000000000 --- a/src/migrations/cluster-store/2.0.0-beta.2.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { MigrationDeclaration } from "../helpers"; - -/** - * Early store format had the kubeconfig directly under context name, this moves - * it under the kubeConfig key - */ - -export default { - version: "2.0.0-beta.2", - run(store) { - for (const value of store) { - const contextName = value[0]; - - // Looping all the keys gives out the store internal stuff too... - if (contextName === "__internal__" || Object.prototype.hasOwnProperty.call(value[1], "kubeConfig")) continue; - store.set(contextName, { kubeConfig: value[1] }); - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/2.4.1.ts b/src/migrations/cluster-store/2.4.1.ts deleted file mode 100644 index d53d2536e5..0000000000 --- a/src/migrations/cluster-store/2.4.1.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { MigrationDeclaration } from "../helpers"; - -// Cleans up a store that had the state related data stored - -export default { - version: "2.4.1", - run(store) { - for (const value of store) { - const contextName = value[0]; - - if (contextName === "__internal__") continue; - const cluster = value[1]; - - store.set(contextName, { kubeConfig: cluster.kubeConfig, icon: cluster.icon || null, preferences: cluster.preferences || {}}); - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/2.6.0-beta.2.ts b/src/migrations/cluster-store/2.6.0-beta.2.ts deleted file mode 100644 index a91c97a3a7..0000000000 --- a/src/migrations/cluster-store/2.6.0-beta.2.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -// Move cluster icon from root to preferences -import type { MigrationDeclaration } from "../helpers"; - -export default { - version: "2.6.0-beta.2", - run(store) { - for (const value of store) { - const clusterKey = value[0]; - - if (clusterKey === "__internal__") continue; - const cluster = value[1]; - - if (!cluster.preferences) cluster.preferences = {}; - - if (cluster.icon) { - cluster.preferences.icon = cluster.icon; - delete (cluster["icon"]); - } - store.set(clusterKey, { contextName: clusterKey, kubeConfig: value[1].kubeConfig, preferences: value[1].preferences }); - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/2.6.0-beta.3.ts b/src/migrations/cluster-store/2.6.0-beta.3.ts deleted file mode 100644 index 56d6f39e37..0000000000 --- a/src/migrations/cluster-store/2.6.0-beta.3.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import yaml from "js-yaml"; -import { MigrationDeclaration, migrationLog } from "../helpers"; - -export default { - version: "2.6.0-beta.3", - run(store) { - for (const value of store) { - const clusterKey = value[0]; - - if (clusterKey === "__internal__") continue; - const cluster = value[1]; - - if (!cluster.kubeConfig) continue; - const config = yaml.load(cluster.kubeConfig); - - if (!config || typeof config !== "object" || !Object.prototype.hasOwnProperty.call(config, "users")) { - continue; - } - - const kubeConfig = config as Record; - const userObj = kubeConfig.users[0]; - - if (userObj) { - const user = userObj.user; - - if (user["auth-provider"] && user["auth-provider"].config) { - const authConfig = user["auth-provider"].config; - - if (authConfig["access-token"]) { - authConfig["access-token"] = `${authConfig["access-token"]}`; - } - - if (authConfig.expiry) { - authConfig.expiry = `${authConfig.expiry}`; - } - migrationLog(authConfig); - user["auth-provider"].config = authConfig; - kubeConfig.users = [{ - name: userObj.name, - user, - }]; - cluster.kubeConfig = yaml.dump(kubeConfig); - store.set(clusterKey, cluster); - } - } - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/2.7.0-beta.0.ts b/src/migrations/cluster-store/2.7.0-beta.0.ts deleted file mode 100644 index 591ce312f7..0000000000 --- a/src/migrations/cluster-store/2.7.0-beta.0.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -// Add existing clusters to "default" workspace -import type { MigrationDeclaration } from "../helpers"; - -export default { - version: "2.7.0-beta.0", - run(store) { - for (const value of store) { - const clusterKey = value[0]; - - if (clusterKey === "__internal__") continue; - const cluster = value[1]; - - cluster.workspace = "default"; - store.set(clusterKey, cluster); - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/2.7.0-beta.1.ts b/src/migrations/cluster-store/2.7.0-beta.1.ts deleted file mode 100644 index 1270c3c712..0000000000 --- a/src/migrations/cluster-store/2.7.0-beta.1.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -// Add id for clusters and store them to array -import { v4 as uuid } from "uuid"; -import type { MigrationDeclaration } from "../helpers"; - -export default { - version: "2.7.0-beta.1", - run(store) { - const clusters: any[] = []; - - for (const value of store) { - const clusterKey = value[0]; - - if (clusterKey === "__internal__") continue; - if (clusterKey === "clusters") continue; - const cluster = value[1]; - - cluster.id = uuid(); - - if (!cluster.preferences.clusterName) { - cluster.preferences.clusterName = clusterKey; - } - clusters.push(cluster); - store.delete(clusterKey); - } - - if (clusters.length > 0) { - store.set("clusters", clusters); - } - }, -} as MigrationDeclaration; diff --git a/src/migrations/cluster-store/index.ts b/src/migrations/cluster-store/index.ts index 21b3b31e78..4851d01cae 100644 --- a/src/migrations/cluster-store/index.ts +++ b/src/migrations/cluster-store/index.ts @@ -7,24 +7,12 @@ import { joinMigrations } from "../helpers"; -import version200Beta2 from "./2.0.0-beta.2"; -import version241 from "./2.4.1"; -import version260Beta2 from "./2.6.0-beta.2"; -import version260Beta3 from "./2.6.0-beta.3"; -import version270Beta0 from "./2.7.0-beta.0"; -import version270Beta1 from "./2.7.0-beta.1"; import version360Beta1 from "./3.6.0-beta.1"; import version500Beta10 from "./5.0.0-beta.10"; import version500Beta13 from "./5.0.0-beta.13"; import snap from "./snap"; export default joinMigrations( - version200Beta2, - version241, - version260Beta2, - version260Beta3, - version270Beta0, - version270Beta1, version360Beta1, version500Beta10, version500Beta13, From 86f14a9cdfb058a258fe843143c08c80b4fbcd9d Mon Sep 17 00:00:00 2001 From: Andreas Schmidt Date: Wed, 19 Jan 2022 14:27:55 +0100 Subject: [PATCH 5/8] fix inputs not blurring when pressing enter (#4692) --- .../editable-list/editable-list.tsx | 1 + src/renderer/components/input/input.tsx | 7 +++++++ src/renderer/components/wizard/wizard.tsx | 19 +++++++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/editable-list/editable-list.tsx b/src/renderer/components/editable-list/editable-list.tsx index 930b35aee5..a8e1c4c5ff 100644 --- a/src/renderer/components/editable-list/editable-list.tsx +++ b/src/renderer/components/editable-list/editable-list.tsx @@ -54,6 +54,7 @@ export class EditableList extends React.Component> { onSubmit={this.onSubmit} validators={validators} placeholder={placeholder} + blurOnEnter={false} iconRight={({ isDirty }) => isDirty ? : null} />
diff --git a/src/renderer/components/input/input.tsx b/src/renderer/components/input/input.tsx index 3ad385293b..36461fd684 100644 --- a/src/renderer/components/input/input.tsx +++ b/src/renderer/components/input/input.tsx @@ -52,6 +52,7 @@ export type InputProps = Omit & { iconRight?: IconData; contentRight?: string | React.ReactNode; // Any component of string goes after iconRight validators?: InputValidator | InputValidator[]; + blurOnEnter?: boolean; onChange?(value: string, evt: React.ChangeEvent): void; onSubmit?(value: string, evt: React.KeyboardEvent): void; }; @@ -70,6 +71,7 @@ const defaultProps: Partial = { maxRows: 10000, showValidationLine: true, validators: [], + blurOnEnter: true, }; export class Input extends React.Component { @@ -267,6 +269,11 @@ export class Input extends React.Component { } else { this.setDirty(); } + + if(this.props.blurOnEnter){ + //pressing enter indicates that the edit is complete, we can unfocus now + this.blur(); + } } } diff --git a/src/renderer/components/wizard/wizard.tsx b/src/renderer/components/wizard/wizard.tsx index b80804befa..3db32133ac 100755 --- a/src/renderer/components/wizard/wizard.tsx +++ b/src/renderer/components/wizard/wizard.tsx @@ -10,6 +10,7 @@ import { Button } from "../button"; import { Stepper } from "../stepper"; import { SubTitle } from "../layout/sub-title"; import { Spinner } from "../spinner"; +import { debounce } from "lodash"; interface WizardCommonProps { data?: Partial; @@ -179,14 +180,16 @@ export class WizardStep extends React.Component { + //because submit MIGHT be called through pressing enter, it might be fired twice. + //we'll debounce it to ensure it isn't + submit = debounce(() => { if (!this.form.noValidate) { const valid = this.form.checkValidity(); if (!valid) return; } this.next(); - }; + }, 100); renderLoading() { return ( @@ -196,6 +199,17 @@ export class WizardStep extends React.Component) { + if (evt.shiftKey || evt.metaKey || evt.altKey || evt.ctrlKey || evt.repeat) { + return; + } + + if(evt.key === "Enter"){ + this.submit(); + } + } + render() { const { step, isFirst, isLast, children, @@ -216,6 +230,7 @@ export class WizardStep extends React.Component this.keyDown(evt)} ref={e => this.form = e}> {beforeContent}
From d31ab690c2dec8559c2d67e6086ba8e8a37a3bb5 Mon Sep 17 00:00:00 2001 From: Jim Ehrismann <40840436+jim-docker@users.noreply.github.com> Date: Wed, 19 Jan 2022 08:54:45 -0500 Subject: [PATCH 6/8] Refactor app-preferences-registry to use di (#4671) * converting app-preferences to use di Signed-off-by: Jim Ehrismann * address review comments and fix lint Signed-off-by: Jim Ehrismann * use compact license header Signed-off-by: Jim Ehrismann --- .eslintrc.js | 6 ++ package.json | 1 + src/extensions/common-api/registrations.ts | 2 +- .../extension-loader/extension-loader.ts | 1 - src/extensions/lens-renderer-extension.ts | 3 +- src/extensions/registries/index.ts | 1 - .../app-preference-registration.d.ts} | 9 --- .../app-preferences.injectable.ts | 19 +++++ .../app-preferences/get-app-preferences.ts | 28 ++++++++ .../components/+preferences/application.tsx | 32 ++++++--- .../+preferences/extension-settings.tsx | 4 +- .../components/+preferences/extensions.tsx | 26 +++++-- .../components/+preferences/preferences.tsx | 69 ++++++++++--------- .../components/+preferences/telemetry.tsx | 25 +++++-- src/renderer/initializers/registries.ts | 1 - yarn.lock | 13 +++- 16 files changed, 176 insertions(+), 64 deletions(-) rename src/{extensions/registries/app-preference-registry.ts => renderer/components/+preferences/app-preferences/app-preference-registration.d.ts} (60%) create mode 100644 src/renderer/components/+preferences/app-preferences/app-preferences.injectable.ts create mode 100644 src/renderer/components/+preferences/app-preferences/get-app-preferences.ts diff --git a/.eslintrc.js b/.eslintrc.js index a80fcdc805..3fda195286 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,6 +16,12 @@ module.exports = { react: { version: packageJson.devDependencies.react || "detect", }, + // the package eslint-import-resolver-typescript is required for this line which fixes errors when using .d.ts files + "import/resolver": { + "typescript": { + "alwaysTryTypes": true, + }, + }, }, overrides: [ { diff --git a/package.json b/package.json index 0443b60eea..2667dea25b 100644 --- a/package.json +++ b/package.json @@ -340,6 +340,7 @@ "esbuild": "^0.13.15", "esbuild-loader": "^2.16.0", "eslint": "^7.32.0", + "eslint-import-resolver-typescript": "^2.5.0", "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.25.3", "eslint-plugin-react": "^7.27.1", diff --git a/src/extensions/common-api/registrations.ts b/src/extensions/common-api/registrations.ts index be1f24d025..666ed01a26 100644 --- a/src/extensions/common-api/registrations.ts +++ b/src/extensions/common-api/registrations.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export type { AppPreferenceRegistration, AppPreferenceComponents } from "../registries/app-preference-registry"; +export type { AppPreferenceRegistration, AppPreferenceComponents } from "../../renderer/components/+preferences/app-preferences/app-preference-registration"; export type { KubeObjectDetailRegistration, KubeObjectDetailComponents } from "../registries/kube-object-detail-registry"; export type { KubeObjectMenuRegistration, KubeObjectMenuComponents } from "../registries/kube-object-menu-registry"; export type { KubeObjectStatusRegistration } from "../registries/kube-object-status-registry"; diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index dd86857cf3..3a0539666a 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -252,7 +252,6 @@ export class ExtensionLoader { return this.autoInitExtensions(async (extension: LensRendererExtension) => { const removeItems = [ registries.GlobalPageRegistry.getInstance().add(extension.globalPages, extension), - registries.AppPreferenceRegistry.getInstance().add(extension.appPreferences), registries.EntitySettingRegistry.getInstance().add(extension.entitySettings), registries.StatusBarRegistry.getInstance().add(extension.statusBarItems), registries.CatalogEntityDetailRegistry.getInstance().add(extension.catalogEntityDetailItems), diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 9504b7b70a..26d2f2d810 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -15,13 +15,14 @@ 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"; import type { CommandRegistration } from "../renderer/components/command-palette/registered-commands/commands"; +import type { AppPreferenceRegistration } from "../renderer/components/+preferences/app-preferences/app-preference-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; clusterPages: registries.PageRegistration[] = []; clusterPageMenus: registries.ClusterPageMenuRegistration[] = []; kubeObjectStatusTexts: registries.KubeObjectStatusRegistration[] = []; - appPreferences: registries.AppPreferenceRegistration[] = []; + appPreferences: AppPreferenceRegistration[] = []; entitySettings: registries.EntitySettingRegistration[] = []; statusBarItems: registries.StatusBarRegistration[] = []; kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; diff --git a/src/extensions/registries/index.ts b/src/extensions/registries/index.ts index 2d49670973..477f406b2c 100644 --- a/src/extensions/registries/index.ts +++ b/src/extensions/registries/index.ts @@ -7,7 +7,6 @@ export * from "./page-registry"; export * from "./page-menu-registry"; -export * from "./app-preference-registry"; export * from "./status-bar-registry"; export * from "./kube-object-detail-registry"; export * from "./kube-object-menu-registry"; diff --git a/src/extensions/registries/app-preference-registry.ts b/src/renderer/components/+preferences/app-preferences/app-preference-registration.d.ts similarity index 60% rename from src/extensions/registries/app-preference-registry.ts rename to src/renderer/components/+preferences/app-preferences/app-preference-registration.d.ts index f533418ca6..4c76722dd5 100644 --- a/src/extensions/registries/app-preference-registry.ts +++ b/src/renderer/components/+preferences/app-preferences/app-preference-registration.d.ts @@ -4,7 +4,6 @@ */ import type React from "react"; -import { BaseRegistry } from "./base-registry"; export interface AppPreferenceComponents { Hint: React.ComponentType; @@ -22,11 +21,3 @@ export interface RegisteredAppPreference extends AppPreferenceRegistration { id: string; } -export class AppPreferenceRegistry extends BaseRegistry { - getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference { - return { - id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"), - ...item, - }; - } -} diff --git a/src/renderer/components/+preferences/app-preferences/app-preferences.injectable.ts b/src/renderer/components/+preferences/app-preferences/app-preferences.injectable.ts new file mode 100644 index 0000000000..fc9deb8656 --- /dev/null +++ b/src/renderer/components/+preferences/app-preferences/app-preferences.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import rendererExtensionsInjectable from "../../../../extensions/renderer-extensions.injectable"; +import { getAppPreferences } from "./get-app-preferences"; + +const appPreferencesInjectable = getInjectable({ + instantiate: (di) => + getAppPreferences({ + extensions: di.inject(rendererExtensionsInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default appPreferencesInjectable; diff --git a/src/renderer/components/+preferences/app-preferences/get-app-preferences.ts b/src/renderer/components/+preferences/app-preferences/get-app-preferences.ts new file mode 100644 index 0000000000..3120e14f77 --- /dev/null +++ b/src/renderer/components/+preferences/app-preferences/get-app-preferences.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { computed, IComputedValue } from "mobx"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import type { AppPreferenceRegistration, RegisteredAppPreference } from "./app-preference-registration"; + +interface Dependencies { + extensions: IComputedValue; +} + +function getRegisteredItem(item: AppPreferenceRegistration): RegisteredAppPreference { + return { + id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"), + ...item, + }; +} + + +export const getAppPreferences = ({ extensions }: Dependencies) => { + return computed(() => ( + extensions.get() + .flatMap((extension) => extension.appPreferences) + .map(getRegisteredItem) + )); +}; diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx index 99edcfe84e..deaae917d0 100644 --- a/src/renderer/components/+preferences/application.tsx +++ b/src/renderer/components/+preferences/application.tsx @@ -14,10 +14,12 @@ import { isWindows } from "../../../common/vars"; import { Switch } from "../switch"; import moment from "moment-timezone"; import { CONSTANTS, defaultExtensionRegistryUrl, ExtensionRegistryLocation } from "../../../common/user-store/preferences-helpers"; -import { action } from "mobx"; +import { action, IComputedValue } from "mobx"; import { isUrl } from "../input/input_validators"; -import { AppPreferenceRegistry } from "../../../extensions/registries"; import { ExtensionSettings } from "./extension-settings"; +import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import appPreferencesInjectable from "./app-preferences/app-preferences.injectable"; const timezoneOptions: SelectOption[] = moment.tz.names().map(zone => ({ label: zone, @@ -28,7 +30,11 @@ const updateChannelOptions: SelectOption[] = Array.from( ([value, { label }]) => ({ value, label }), ); -export const Application = observer(() => { +interface Dependencies { + appPreferenceItems: IComputedValue +} + +const NonInjectedApplication: React.FC = ({ appPreferenceItems }) => { const userStore = UserStore.getInstance(); const defaultShell = process.env.SHELL || process.env.PTYSHELL @@ -40,7 +46,7 @@ export const Application = observer(() => { const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || ""); const [shell, setShell] = React.useState(userStore.shell || ""); - const extensionSettings = AppPreferenceRegistry.getInstance().getItems().filter((preference) => preference.showInPreferencesTab === "application"); + const extensionSettings = appPreferenceItems.get().filter((preference) => preference.showInPreferencesTab === "application"); const themeStore = ThemeStore.getInstance(); return ( @@ -125,10 +131,10 @@ export const Application = observer(() => { />
-
+
- + userStore.openAtLogin = !userStore.openAtLogin}> Automatically start Lens on login @@ -141,7 +147,7 @@ export const Application = observer(() => { ))}
- +