diff --git a/.dependabot/config.yml b/.dependabot/config.yml new file mode 100644 index 0000000000..a77a36c653 --- /dev/null +++ b/.dependabot/config.yml @@ -0,0 +1,17 @@ +# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates +# for config options + +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 4 + reviewers: + - "lensapp/lens-maintainers" + labels: + - "dependencies" + versioning-strategy: + lockfile-only: false + increase: true diff --git a/build/icons/512x512.png b/build/icons/512x512.png index 2c953f6efd..e08b9f5b15 100644 Binary files a/build/icons/512x512.png and b/build/icons/512x512.png differ diff --git a/build/icons/512x512@2x.png b/build/icons/512x512@2x.png new file mode 100644 index 0000000000..da0aa8ae80 Binary files /dev/null and b/build/icons/512x512@2x.png differ diff --git a/docs/extensions/capabilities/color-reference.md b/docs/extensions/capabilities/color-reference.md index 660e0fe067..6a38ba861c 100644 --- a/docs/extensions/capabilities/color-reference.md +++ b/docs/extensions/capabilities/color-reference.md @@ -43,6 +43,7 @@ You can use theme-based CSS Variables to style an extension according to the act ## Button Colors - `--buttonPrimaryBackground`: button background color for primary actions. - `--buttonDefaultBackground`: default button background color. +- `--buttonLightBackground`: light button background color. - `--buttonAccentBackground`: accent button background color. - `--buttonDisabledBackground`: disabled button background color. diff --git a/package.json b/package.json index b0cfd36645..27721b6e7a 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "kontena-lens", "productName": "Lens", "description": "Lens - The Kubernetes IDE", - "version": "4.1.0-alpha.2", + "version": "4.1.0-beta.1", "main": "static/build/main.js", "copyright": "© 2020, Mirantis, Inc.", "license": "MIT", @@ -16,7 +16,7 @@ "dev-run": "nodemon --watch static/build/main.js --exec \"electron --inspect .\"", "dev:main": "yarn run compile:main --watch", "dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts", - "dev:extension-types": "yarn run compile:extension-types --watch", + "dev:extension-types": "yarn run compile:extension-types --watch --progress", "compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile:main": "yarn run webpack --config webpack.main.ts", "compile:renderer": "yarn run webpack --config webpack.renderer.ts", diff --git a/src/common/ipc/__tests__/type-enforced-ipc.test.ts b/src/common/ipc/__tests__/type-enforced-ipc.test.ts new file mode 100644 index 0000000000..4e20249392 --- /dev/null +++ b/src/common/ipc/__tests__/type-enforced-ipc.test.ts @@ -0,0 +1,126 @@ +import { EventEmitter } from "events"; +import { onCorrect, onceCorrect } from "../type-enforced-ipc"; + +describe("type enforced ipc tests", () => { + describe("onCorrect tests", () => { + it("should call the handler if the args are valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(true); + }); + + it("should not call the handler if the args are not valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => false; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(false); + }); + + it("should call the handler twice if the args are valid on two emits", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + expect(called).toBe(2); + }); + + it("should call the handler twice if the args are [valid, invalid, valid]", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const results = [true, false, true]; + const verifier = (args: unknown[]): args is [] => results.pop(); + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + source.emit(channel); + expect(called).toBe(2); + }); + }); + + describe("onceCorrect tests", () => { + it("should call the handler if the args are valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(true); + }); + + it("should not call the handler if the args are not valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => false; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(false); + }); + + it("should call the handler only once even if args are valid multiple times", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + expect(called).toBe(1); + }); + + it("should call the handler on only the first valid set of args", () => { + let called = ""; + let verifierCalled = 0; + const source = new EventEmitter(); + const listener = (info: any, arg: string) => called = arg; + const verifier = (args: unknown[]): args is [string] => (++verifierCalled) % 3 === 0; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel, {}, "a"); + source.emit(channel, {}, "b"); + source.emit(channel, {}, "c"); + source.emit(channel, {}, "d"); + source.emit(channel, {}, "e"); + source.emit(channel, {}, "f"); + source.emit(channel, {}, "g"); + source.emit(channel, {}, "h"); + source.emit(channel, {}, "i"); + expect(called).toBe("c"); + }); + }); +}); diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts new file mode 100644 index 0000000000..a34890472e --- /dev/null +++ b/src/common/ipc/index.ts @@ -0,0 +1,3 @@ +export * from "./ipc"; +export * from "./update-available"; +export * from "./type-enforced-ipc"; diff --git a/src/common/ipc.ts b/src/common/ipc/ipc.ts similarity index 90% rename from src/common/ipc.ts rename to src/common/ipc/ipc.ts index c2f8562cf7..48b0b89153 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -4,12 +4,12 @@ import { ipcMain, ipcRenderer, webContents, remote } from "electron"; import { toJS } from "mobx"; -import logger from "../main/logger"; -import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames"; +import logger from "../../main/logger"; +import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames"; const subFramesChannel = "ipc:get-sub-frames"; -export function handleRequest(channel: string, listener: (...args: any[]) => any) { +export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { ipcMain.handle(channel, listener); } diff --git a/src/common/ipc/type-enforced-ipc.ts b/src/common/ipc/type-enforced-ipc.ts new file mode 100644 index 0000000000..be54992008 --- /dev/null +++ b/src/common/ipc/type-enforced-ipc.ts @@ -0,0 +1,71 @@ +import { EventEmitter } from "events"; +import logger from "../../main/logger"; + +export type HandlerEvent = Parameters[1]>[0]; +export type ListVerifier = (args: unknown[]) => args is T; +export type Rest = T extends [any, ...infer R] ? R : []; + +/** + * Adds a listener to `source` that waits for the first IPC message with the correct + * argument data is sent. + * @param channel The channel to be listened on + * @param listener The function for the channel to be called if the args of the correct type + * @param verifier The function to be called to verify that the args are the correct type + */ +export function onceCorrect< + EM extends EventEmitter, + L extends (event: HandlerEvent, ...args: any[]) => any +>({ + source, + channel, + listener, + verifier, +}: { + source: EM, + channel: string | symbol, + listener: L, + verifier: ListVerifier>>, +}): void { + function handler(event: HandlerEvent, ...args: unknown[]): void { + if (verifier(args)) { + source.removeListener(channel, handler); // remove immediately + + (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject + .catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was emitted with invalid data", { channel, args }); + } + } + + source.on(channel, handler); +} + +/** + * Adds a listener to `source` that checks to verify the arguments before calling the handler. + * @param channel The channel to be listened on + * @param listener The function for the channel to be called if the args of the correct type + * @param verifier The function to be called to verify that the args are the correct type + */ +export function onCorrect< + EM extends EventEmitter, + L extends (event: HandlerEvent, ...args: any[]) => any +>({ + source, + channel, + listener, + verifier, +}: { + source: EM, + channel: string | symbol, + listener: L, + verifier: ListVerifier>>, +}): void { + source.on(channel, (event, ...args: unknown[]) => { + if (verifier(args)) { + (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject + .catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was emitted with invalid data", { channel, args }); + } + }); +} diff --git a/src/common/ipc/update-available/index.ts b/src/common/ipc/update-available/index.ts new file mode 100644 index 0000000000..1e3fcf1268 --- /dev/null +++ b/src/common/ipc/update-available/index.ts @@ -0,0 +1,48 @@ +import { UpdateInfo } from "electron-updater"; + +export const UpdateAvailableChannel = "update-available"; +export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; + +/** + * [, ] + */ +export type UpdateAvailableFromMain = [string, UpdateInfo]; + +export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain { + if (args.length !== 2) { + return false; + } + + if (typeof args[0] !== "string") { + return false; + } + + if (typeof args[1] !== "object" || args[1] === null) { + // TODO: improve this checking + return false; + } + + return true; +} + +export type BackchannelArg = { + doUpdate: false; +} | { + doUpdate: true; + now: boolean; +}; + +export type UpdateAvailableToBackchannel = [BackchannelArg]; + +export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel { + if (args.length !== 1) { + return false; + } + + if (typeof args[0] !== "object" || args[0] === null) { + // TODO: improve this checking + return false; + } + + return true; +} diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts index 208e042759..7d0686d29b 100644 --- a/src/common/utils/delay.ts +++ b/src/common/utils/delay.ts @@ -1,6 +1,8 @@ -// Create async delay for provided timeout in milliseconds - -export async function delay(timeoutMs = 1000) { - if (!timeoutMs) return; - await new Promise(resolve => setTimeout(resolve, timeoutMs)); +/** + * Return a promise that will be resolved after at least `timeout` ms have + * passed + * @param timeout The number of milliseconds before resolving + */ +export function delay(timeout = 1000): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); } diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 942c675f0a..2b8147fad9 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -18,3 +18,4 @@ export * from "./openExternal"; export * from "./downloadFile"; export * from "./escapeRegExp"; export * from "./tar"; +export * from "./delay"; diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index dd9ed97e69..618f714b49 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -1,20 +1,78 @@ -import { autoUpdater } from "electron-updater"; +import { autoUpdater, UpdateInfo } from "electron-updater"; import logger from "./logger"; +import { isDevelopment, isTestEnv } from "../common/vars"; +import { delay } from "../common/utils"; +import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc"; +import { ipcMain } from "electron"; -export class AppUpdater { - static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day - - static checkForUpdates() { - return autoUpdater.checkForUpdatesAndNotify(); - } - - constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) { - autoUpdater.logger = logger; - } - - public start() { - setInterval(AppUpdater.checkForUpdates, this.updateInterval); - - return AppUpdater.checkForUpdates(); +function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) { + if (arg.doUpdate) { + if (arg.now) { + logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); + autoUpdater.downloadUpdate() + .then(() => autoUpdater.quitAndInstall()) + .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error })); + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.downloadUpdate() + .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error })); + } + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose not to update`); + } +} + +/** + * starts the automatic update checking + * @param interval milliseconds between interval to check on, defaults to 24h + */ +export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void { + if (isDevelopment || isTestEnv) { + return; + } + + autoUpdater.logger = logger; + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + + autoUpdater + .on("update-available", (args: UpdateInfo) => { + try { + const backchannel = `auto-update:${args.version}`; + + ipcMain.removeAllListeners(backchannel); // only one handler should be present + + // make sure that the handler is in place before broadcasting (prevent race-condition) + onceCorrect({ + source: ipcMain, + channel: backchannel, + listener: handleAutoUpdateBackChannel, + verifier: areArgsUpdateAvailableToBackchannel, + }); + logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version }); + broadcastMessage(UpdateAvailableChannel, backchannel, args); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error }); + } + }); + + async function helper() { + while (true) { + await checkForUpdates(); + await delay(interval); + } + } + + helper(); +} + +export async function checkForUpdates(): Promise { + try { + logger.info(`📡 Checking for app updates`); + + await autoUpdater.checkForUpdates(); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) }); } } diff --git a/src/main/index.ts b/src/main/index.ts index af24026a5e..50571a8862 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,7 +10,6 @@ import path from "path"; import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; -import { AppUpdater } from "./app-updater"; import { shellSync } from "./shell-sync"; import { getFreePort } from "./port"; import { mangleProxyEnv } from "./proxy-env"; @@ -28,6 +27,7 @@ import { installDeveloperTools } from "./developer-tools"; import { filesystemProvisionerStore } from "./extension-filesystem"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { bindBroadcastHandlers } from "../common/ipc"; +import { startUpdateChecking } from "./app-updater"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -72,11 +72,6 @@ app.on("ready", async () => { app.exit(); }); - logger.info(`📡 Checking for app updates`); - const updater = new AppUpdater(); - - updater.start(); - registerFileProtocol("static", __static); await installDeveloperTools(); @@ -133,6 +128,7 @@ app.on("ready", async () => { logger.info("🖥️ Starting WindowManager"); windowManager = WindowManager.getInstance(proxyPort); + windowManager.whenLoaded.then(() => startUpdateChecking()); logger.info("🧩 Initializing extensions"); diff --git a/src/main/tray.ts b/src/main/tray.ts index 44a22d27bf..3d6d2dd624 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,9 +1,9 @@ import path from "path"; import packageInfo from "../../package.json"; -import { dialog, Menu, NativeImage, Tray } from "electron"; +import { Menu, NativeImage, Tray } from "electron"; import { autorun } from "mobx"; import { showAbout } from "./menu"; -import { AppUpdater } from "./app-updater"; +import { checkForUpdates } from "./app-updater"; import { WindowManager } from "./window-manager"; import { clusterStore } from "../common/cluster-store"; import { workspaceStore } from "../common/workspace-store"; @@ -102,16 +102,8 @@ function createTrayMenu(windowManager: WindowManager): Menu { { label: "Check for updates", async click() { - const result = await AppUpdater.checkForUpdates(); - - if (!result) { - const browserWindow = await windowManager.ensureMainWindow(); - - dialog.showMessageBoxSync(browserWindow, { - message: "No updates available", - type: "info", - }); - } + await checkForUpdates(); + await windowManager.ensureMainWindow(); }, }, { diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 691aa7c66b..c092e186cb 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,5 +1,5 @@ import type { ClusterId } from "../common/cluster-store"; -import { observable } from "mobx"; +import { observable, when } from "mobx"; import { app, BrowserWindow, dialog, shell, webContents } from "electron"; import windowStateKeeper from "electron-window-state"; import { appEventBus } from "../common/event-bus"; @@ -16,6 +16,9 @@ export class WindowManager extends Singleton { protected windowState: windowStateKeeper.State; protected disposers: Record = {}; + @observable mainViewInitiallyLoaded = false; + whenLoaded = when(() => this.mainViewInitiallyLoaded); + @observable activeClusterId: ClusterId; constructor(protected proxyPort: number) { @@ -101,6 +104,7 @@ export class WindowManager extends Singleton { setTimeout(() => { appEventBus.emit({ name: "app", action: "start" }); }, 1000); + this.mainViewInitiallyLoaded = true; } catch (err) { dialog.showErrorBox("ERROR!", err.toString()); } diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 47500adf79..a802851f65 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -7,7 +7,7 @@ import { KubeApi, parseKubeApi } from "./kube-api"; @autobind() export class ApiManager { private apis = observable.map(); - private stores = observable.map(); + private stores = observable.map(); getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { if (typeof pathOrCallback === "string") { @@ -46,12 +46,12 @@ export class ApiManager { @action registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) { apis.forEach(api => { - this.stores.set(api, store); + this.stores.set(api.apiBase, store); }); } getStore(api: string | KubeApi): S { - return this.stores.get(this.resolveApi(api)) as S; + return this.stores.get(this.resolveApi(api)?.apiBase) as S; } } diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 84e095721b..6831c508cb 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -57,6 +57,7 @@ export interface IReleaseRevision { updated: string; status: string; chart: string; + app_version: string; description: string; } diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx index bca0b23c4f..4aef4ec356 100644 --- a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx +++ b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx @@ -77,7 +77,8 @@ export class ReleaseRollbackDialog extends React.Component { themeName="light" value={revision} options={revisions} - formatOptionLabel={({ value }: SelectOption) => `${value.revision} - ${value.chart}`} + formatOptionLabel={({ value }: SelectOption) => `${value.revision} - ${value.chart} + - ${value.app_version}, updated: ${new Date(value.updated).toLocaleString()}`} onChange={({ value }: SelectOption) => this.revision = value} /> diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index ab26b906b3..207a274d2c 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -76,4 +76,3 @@ export class NamespaceSelect extends React.Component { ); } } - diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index 75b1fed064..bc0484dadc 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -8,7 +8,7 @@ import { workloadURL, workloadStores } from "../+workloads"; import { namespaceStore } from "../+namespaces/namespace.store"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { isAllowedResource, KubeResource } from "../../../common/rbac"; -import { ResourceNames } from "../../../renderer/utils/rbac"; +import { ResourceNames } from "../../utils/rbac"; import { autobind } from "../../utils"; const resources: KubeResource[] = [ diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss index b850a3be59..9224b7e5d3 100644 --- a/src/renderer/components/button/button.scss +++ b/src/renderer/components/button/button.scss @@ -21,10 +21,16 @@ &.primary { background: $buttonPrimaryBackground; } + &.accent { background: $buttonAccentBackground; } + &.light { + background-color: $buttonLightBackground; + color: #505050; + } + &.plain { color: inherit; background: transparent; diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx index 9fa822b214..8bcb37bad4 100644 --- a/src/renderer/components/button/button.tsx +++ b/src/renderer/components/button/button.tsx @@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes, TooltipDecorator waiting?: boolean; primary?: boolean; accent?: boolean; + light?: boolean; plain?: boolean; outlined?: boolean; hidden?: boolean; @@ -24,13 +25,16 @@ export class Button extends React.PureComponent { private button: HTMLButtonElement; render() { - const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props; - const btnProps = props as Partial; + const { + className, waiting, label, primary, accent, plain, hidden, active, big, + round, outlined, tooltip, light, children, ...props + } = this.props; + const btnProps: Partial = props; if (hidden) return null; btnProps.className = cssNames("Button", className, { - waiting, primary, accent, plain, active, big, round, outlined + waiting, primary, accent, plain, active, big, round, outlined, light, }); const btnContent: ReactNode = ( diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index 19ef031ba6..5c3e134664 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -50,6 +50,10 @@ export class BarChart extends React.Component { }) }; + if (chartData.datasets.length == 0) { + return ; + } + const formatTimeLabels = (timestamp: string, index: number) => { const label = moment(parseInt(timestamp)).format("HH:mm"); const offset = " "; @@ -143,10 +147,6 @@ export class BarChart extends React.Component { }; const options = merge(barOptions, customOptions); - if (chartData.datasets.length == 0) { - return ; - } - return ( { {dialog} ); - } - else if (!this.isOpen) { + } else if (!this.isOpen) { return null; } - return createPortal(dialog, document.body); + return createPortal(dialog, document.body) as React.ReactPortal; } } diff --git a/src/renderer/components/notifications/notifications.scss b/src/renderer/components/notifications/notifications.scss index 37d4990ee5..4b300edda6 100644 --- a/src/renderer/components/notifications/notifications.scss +++ b/src/renderer/components/notifications/notifications.scss @@ -42,5 +42,9 @@ box-shadow: 0 0 20px $boxShadow; } } + + .close { + margin-top: -2px; + } } } diff --git a/src/renderer/components/notifications/notifications.store.ts b/src/renderer/components/notifications/notifications.store.tsx similarity index 95% rename from src/renderer/components/notifications/notifications.store.ts rename to src/renderer/components/notifications/notifications.store.tsx index 55549b9066..45c1eb9a6b 100644 --- a/src/renderer/components/notifications/notifications.store.ts +++ b/src/renderer/components/notifications/notifications.store.tsx @@ -18,6 +18,7 @@ export interface Notification { message: NotificationMessage; status?: NotificationStatus; timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide + onClose?(): void; // additonal logic on when the notification times out or is closed by the "x" } @autobind() diff --git a/src/renderer/components/notifications/notifications.tsx b/src/renderer/components/notifications/notifications.tsx index 4ab297e289..0c1ac692cf 100644 --- a/src/renderer/components/notifications/notifications.tsx +++ b/src/renderer/components/notifications/notifications.tsx @@ -72,23 +72,26 @@ export class Notifications extends React.Component { return (
this.elem = e}> {notifications.map(notification => { - const { id, status } = notification; + const { id, status, onClose } = notification; const msgText = this.getMessage(notification); return (
addAutoHideTimer(id)} onMouseEnter={() => removeAutoHideTimer(id)}> -
+
{msgText}
-
+
remove(id))} + onClick={prevDefault(() => { + remove(id); + onClose?.(); + })} />
diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx new file mode 100644 index 0000000000..991d9b7616 --- /dev/null +++ b/src/renderer/ipc/index.tsx @@ -0,0 +1,61 @@ +import React from "react"; +import { ipcRenderer, IpcRendererEvent } from "electron"; +import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc"; +import { Notifications, notificationsStore } from "../components/notifications"; +import { Button } from "../components/button"; +import { isMac } from "../../common/vars"; +import * as uuid from "uuid"; + +function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { + notificationsStore.remove(notificationId); + ipcRenderer.send(backchannel, data); +} + +function RenderYesButtons(props: { backchannel: string, notificationId: string }) { + if (isMac) { + /** + * auto-updater's "installOnQuit" is not applicable for macOS as per their docs. + * + * See: https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/AppUpdater.ts#L27-L32 + */ + return
+ + ), { + id: notificationId, + onClose() { + sendToBackchannel(backchannel, notificationId, { doUpdate: false }); + } + } + ); +} + +export function registerIpcHandlers() { + onCorrect({ + source: ipcRenderer, + channel: UpdateAvailableChannel, + listener: UpdateAvailableHandler, + verifier: areArgsUpdateAvailableFromMain, + }); +} diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 5c0dd79a95..963bd43e4e 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -12,6 +12,7 @@ import { ConfirmDialog } from "./components/confirm-dialog"; import { extensionLoader } from "../extensions/extension-loader"; import { broadcastMessage } from "../common/ipc"; import { CommandContainer } from "./components/command-palette/command-container"; +import { registerIpcHandlers } from "./ipc"; @observer export class LensApp extends React.Component { @@ -23,6 +24,8 @@ export class LensApp extends React.Component { window.addEventListener("online", () => { broadcastMessage("network:online"); }); + + registerIpcHandlers(); } render() { diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.json index 4a7a0f2f70..0a56f851ec 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -25,6 +25,7 @@ "sidebarSubmenuActiveColor": "#ffffff", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", + "buttonLightBackground": "#f1f1f1", "buttonAccentBackground": "#e85555", "buttonDisabledBackground": "#808080", "tableBgcStripe": "#2a2d33", diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index f59d3c9555..e65722b889 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -26,6 +26,7 @@ "sidebarBackground": "#e8e8e8", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", + "buttonLightBackground": "#f1f1f1", "buttonAccentBackground": "#e85555", "buttonDisabledBackground": "#808080", "tableBgcStripe": "#f8f8f8", diff --git a/src/renderer/themes/theme-vars.scss b/src/renderer/themes/theme-vars.scss index 6b556bdc88..bb897818f5 100644 --- a/src/renderer/themes/theme-vars.scss +++ b/src/renderer/themes/theme-vars.scss @@ -34,6 +34,7 @@ $sidebarBackground: var(--sidebarBackground); // Elements $buttonPrimaryBackground: var(--buttonPrimaryBackground); $buttonDefaultBackground: var(--buttonDefaultBackground); +$buttonLightBackground: var(--buttonLightBackground); $buttonAccentBackground: var(--buttonAccentBackground); $buttonDisabledBackground: var(--buttonDisabledBackground); @@ -131,4 +132,4 @@ $selectOptionHoveredColor: var(--selectOptionHoveredColor); $lineProgressBackground: var(--lineProgressBackground); $radioActiveBackground: var(--radioActiveBackground); $menuActiveBackground: var(--menuActiveBackground); -$menuSelectedOptionBgc: var(--menuSelectedOptionBgc); \ No newline at end of file +$menuSelectedOptionBgc: var(--menuSelectedOptionBgc); diff --git a/static/RELEASE_NOTES.md b/static/RELEASE_NOTES.md index adbc18c6cf..b40eb828bc 100644 --- a/static/RELEASE_NOTES.md +++ b/static/RELEASE_NOTES.md @@ -2,11 +2,12 @@ Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights! -## 4.1.0-alpha.2 (current version) +## 4.1.0-beta.1 (current version) - Change: list views default to a namespace (insted of listing resources from all namespaces) - Command palette - Generic logs view with Pod selector +- In-app survey extension - Possibility to add custom Helm repository through Lens - Possibility to change visibility of common resource list columns - Suspend / resume buttons for CronJobs @@ -21,6 +22,8 @@ Here you can find description of changes we've built into each release. While we - Lens metrics: Prometheus v2.19.3 - Update bundled kubectl to v1.18.15 - Improve how watch requests are handled +- Helm rollback window with more details +- Log more on start up - Export PodDetailsList component to extension API - Export Wizard components to extension API - Export NamespaceSelect component to extension API diff --git a/webpack.extensions.ts b/webpack.extensions.ts index 0c2829a406..1b9c812868 100644 --- a/webpack.extensions.ts +++ b/webpack.extensions.ts @@ -1,9 +1,9 @@ import path from "path"; import webpack from "webpack"; -import { sassCommonVars } from "./src/common/vars"; +import { sassCommonVars, isDevelopment } from "./src/common/vars"; -export default function (): webpack.Configuration { +export default function generateExtensionTypes(): webpack.Configuration { const entry = "./src/extensions/extension-api.ts"; const outDir = "./src/extensions/npm/extensions/dist"; @@ -22,6 +22,10 @@ export default function (): webpack.Configuration { // e.g. require('@k8slens/extensions') libraryTarget: "commonjs" }, + cache: isDevelopment, + optimization: { + minimize: false, // speed up types compilation + }, module: { rules: [ {