mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into fix/namespace-filter-jumping-scroll
This commit is contained in:
commit
8b81442edd
17
.dependabot/config.yml
Normal file
17
.dependabot/config.yml
Normal file
@ -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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 23 KiB |
BIN
build/icons/512x512@2x.png
Normal file
BIN
build/icons/512x512@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@ -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.
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
3
src/common/ipc/index.ts
Normal file
3
src/common/ipc/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./ipc";
|
||||
export * from "./update-available";
|
||||
export * from "./type-enforced-ipc";
|
||||
@ -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);
|
||||
}
|
||||
|
||||
71
src/common/ipc/type-enforced-ipc.ts
Normal file
71
src/common/ipc/type-enforced-ipc.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { EventEmitter } from "events";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
||||
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
|
||||
export type Rest<T> = 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<EM>, ...args: any[]) => any
|
||||
>({
|
||||
source,
|
||||
channel,
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: EM,
|
||||
channel: string | symbol,
|
||||
listener: L,
|
||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||
}): void {
|
||||
function handler(event: HandlerEvent<EM>, ...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<EM>, ...args: any[]) => any
|
||||
>({
|
||||
source,
|
||||
channel,
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: EM,
|
||||
channel: string | symbol,
|
||||
listener: L,
|
||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||
}): 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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
48
src/common/ipc/update-available/index.ts
Normal file
48
src/common/ipc/update-available/index.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { UpdateInfo } from "electron-updater";
|
||||
|
||||
export const UpdateAvailableChannel = "update-available";
|
||||
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
||||
|
||||
/**
|
||||
* [<back-channel>, <update-info>]
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@ -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<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
@ -18,3 +18,4 @@ export * from "./openExternal";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./tar";
|
||||
export * from "./delay";
|
||||
|
||||
@ -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<void> {
|
||||
try {
|
||||
logger.info(`📡 Checking for app updates`);
|
||||
|
||||
await autoUpdater.checkForUpdates();
|
||||
} catch (error) {
|
||||
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<WindowManager>(proxyPort);
|
||||
windowManager.whenLoaded.then(() => startUpdateChecking());
|
||||
|
||||
logger.info("🧩 Initializing extensions");
|
||||
|
||||
|
||||
@ -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();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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<string, Function> = {};
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import { KubeApi, parseKubeApi } from "./kube-api";
|
||||
@autobind()
|
||||
export class ApiManager {
|
||||
private apis = observable.map<string, KubeApi>();
|
||||
private stores = observable.map<KubeApi, KubeObjectStore>();
|
||||
private stores = observable.map<string, KubeObjectStore>();
|
||||
|
||||
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<S extends KubeObjectStore>(api: string | KubeApi): S {
|
||||
return this.stores.get(this.resolveApi(api)) as S;
|
||||
return this.stores.get(this.resolveApi(api)?.apiBase) as S;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -57,6 +57,7 @@ export interface IReleaseRevision {
|
||||
updated: string;
|
||||
status: string;
|
||||
chart: string;
|
||||
app_version: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
|
||||
@ -77,7 +77,8 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
|
||||
themeName="light"
|
||||
value={revision}
|
||||
options={revisions}
|
||||
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}`}
|
||||
formatOptionLabel={({ value }: SelectOption<IReleaseRevision>) => `${value.revision} - ${value.chart}
|
||||
- ${value.app_version}, updated: ${new Date(value.updated).toLocaleString()}`}
|
||||
onChange={({ value }: SelectOption<IReleaseRevision>) => this.revision = value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -76,4 +76,3 @@ export class NamespaceSelect extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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[] = [
|
||||
|
||||
@ -21,10 +21,16 @@
|
||||
&.primary {
|
||||
background: $buttonPrimaryBackground;
|
||||
}
|
||||
|
||||
&.accent {
|
||||
background: $buttonAccentBackground;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: $buttonLightBackground;
|
||||
color: #505050;
|
||||
}
|
||||
|
||||
&.plain {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
|
||||
@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, 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<ButtonProps, {}> {
|
||||
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<ButtonProps>;
|
||||
const {
|
||||
className, waiting, label, primary, accent, plain, hidden, active, big,
|
||||
round, outlined, tooltip, light, children, ...props
|
||||
} = this.props;
|
||||
const btnProps: Partial<ButtonProps> = 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 = (
|
||||
|
||||
@ -50,6 +50,10 @@ export class BarChart extends React.Component<Props> {
|
||||
})
|
||||
};
|
||||
|
||||
if (chartData.datasets.length == 0) {
|
||||
return <NoMetrics/>;
|
||||
}
|
||||
|
||||
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<Props> {
|
||||
};
|
||||
const options = merge(barOptions, customOptions);
|
||||
|
||||
if (chartData.datasets.length == 0) {
|
||||
return <NoMetrics/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chart
|
||||
className={cssNames("BarChart flex box grow column", className)}
|
||||
|
||||
@ -146,11 +146,10 @@ export class Dialog extends React.PureComponent<DialogProps, DialogState> {
|
||||
{dialog}
|
||||
</Animate>
|
||||
);
|
||||
}
|
||||
else if (!this.isOpen) {
|
||||
} else if (!this.isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(dialog, document.body);
|
||||
return createPortal(dialog, document.body) as React.ReactPortal;
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,5 +42,9 @@
|
||||
box-shadow: 0 0 20px $boxShadow;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
@ -72,23 +72,26 @@ export class Notifications extends React.Component {
|
||||
return (
|
||||
<div className="Notifications flex column align-flex-end" ref={e => this.elem = e}>
|
||||
{notifications.map(notification => {
|
||||
const { id, status } = notification;
|
||||
const { id, status, onClose } = notification;
|
||||
const msgText = this.getMessage(notification);
|
||||
|
||||
return (
|
||||
<Animate key={id}>
|
||||
<div
|
||||
className={cssNames("notification flex align-center", status)}
|
||||
className={cssNames("notification flex", status)}
|
||||
onMouseLeave={() => addAutoHideTimer(id)}
|
||||
onMouseEnter={() => removeAutoHideTimer(id)}>
|
||||
<div className="box center">
|
||||
<div className="box">
|
||||
<Icon material="info_outline"/>
|
||||
</div>
|
||||
<div className="message box grow">{msgText}</div>
|
||||
<div className="box center">
|
||||
<div className="box">
|
||||
<Icon
|
||||
material="close" className="close"
|
||||
onClick={prevDefault(() => remove(id))}
|
||||
onClick={prevDefault(() => {
|
||||
remove(id);
|
||||
onClose?.();
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
61
src/renderer/ipc/index.tsx
Normal file
61
src/renderer/ipc/index.tsx
Normal file
@ -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 <Button light label="Yes" onClick={() => sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: true })} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button light label="Yes, now" onClick={() => sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: true })} />
|
||||
<Button active outlined label="Yes, later" onClick={() => sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: false })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableFromMain): void {
|
||||
const notificationId = uuid.v4();
|
||||
|
||||
Notifications.info(
|
||||
(
|
||||
<>
|
||||
<b>Update Available</b>
|
||||
<p>Version {updateInfo.version} of Lens IDE is now available. Would you like to update?</p>
|
||||
<div className="flex gaps row align-left box grow">
|
||||
<RenderYesButtons backchannel={backchannel} notificationId={notificationId} />
|
||||
<Button active outlined label="No" onClick={() => sendToBackchannel(backchannel, notificationId, { doUpdate: false })} />
|
||||
</div>
|
||||
</>
|
||||
), {
|
||||
id: notificationId,
|
||||
onClose() {
|
||||
sendToBackchannel(backchannel, notificationId, { doUpdate: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function registerIpcHandlers() {
|
||||
onCorrect({
|
||||
source: ipcRenderer,
|
||||
channel: UpdateAvailableChannel,
|
||||
listener: UpdateAvailableHandler,
|
||||
verifier: areArgsUpdateAvailableFromMain,
|
||||
});
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"sidebarSubmenuActiveColor": "#ffffff",
|
||||
"buttonPrimaryBackground": "#3d90ce",
|
||||
"buttonDefaultBackground": "#414448",
|
||||
"buttonLightBackground": "#f1f1f1",
|
||||
"buttonAccentBackground": "#e85555",
|
||||
"buttonDisabledBackground": "#808080",
|
||||
"tableBgcStripe": "#2a2d33",
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"sidebarBackground": "#e8e8e8",
|
||||
"buttonPrimaryBackground": "#3d90ce",
|
||||
"buttonDefaultBackground": "#414448",
|
||||
"buttonLightBackground": "#f1f1f1",
|
||||
"buttonAccentBackground": "#e85555",
|
||||
"buttonDisabledBackground": "#808080",
|
||||
"tableBgcStripe": "#f8f8f8",
|
||||
|
||||
@ -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);
|
||||
$menuSelectedOptionBgc: var(--menuSelectedOptionBgc);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: [
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user