1
0
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:
Alex Andreev 2021-02-09 19:57:08 +03:00
commit 8b81442edd
35 changed files with 481 additions and 70 deletions

17
.dependabot/config.yml Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

View File

@ -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.

View File

@ -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",

View 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
View File

@ -0,0 +1,3 @@
export * from "./ipc";
export * from "./update-available";
export * from "./type-enforced-ipc";

View File

@ -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);
}

View 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 });
}
});
}

View 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;
}

View File

@ -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));
}

View File

@ -18,3 +18,4 @@ export * from "./openExternal";
export * from "./downloadFile";
export * from "./escapeRegExp";
export * from "./tar";
export * from "./delay";

View File

@ -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) });
}
}

View File

@ -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");

View File

@ -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();
},
},
{

View File

@ -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());
}

View File

@ -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;
}
}

View File

@ -57,6 +57,7 @@ export interface IReleaseRevision {
updated: string;
status: string;
chart: string;
app_version: string;
description: string;
}

View File

@ -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>

View File

@ -76,4 +76,3 @@ export class NamespaceSelect extends React.Component<Props> {
);
}
}

View File

@ -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[] = [

View File

@ -21,10 +21,16 @@
&.primary {
background: $buttonPrimaryBackground;
}
&.accent {
background: $buttonAccentBackground;
}
&.light {
background-color: $buttonLightBackground;
color: #505050;
}
&.plain {
color: inherit;
background: transparent;

View File

@ -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 = (

View File

@ -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)}

View File

@ -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;
}
}

View File

@ -42,5 +42,9 @@
box-shadow: 0 0 20px $boxShadow;
}
}
.close {
margin-top: -2px;
}
}
}

View File

@ -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()

View File

@ -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>

View 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,
});
}

View File

@ -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() {

View File

@ -25,6 +25,7 @@
"sidebarSubmenuActiveColor": "#ffffff",
"buttonPrimaryBackground": "#3d90ce",
"buttonDefaultBackground": "#414448",
"buttonLightBackground": "#f1f1f1",
"buttonAccentBackground": "#e85555",
"buttonDisabledBackground": "#808080",
"tableBgcStripe": "#2a2d33",

View File

@ -26,6 +26,7 @@
"sidebarBackground": "#e8e8e8",
"buttonPrimaryBackground": "#3d90ce",
"buttonDefaultBackground": "#414448",
"buttonLightBackground": "#f1f1f1",
"buttonAccentBackground": "#e85555",
"buttonDisabledBackground": "#808080",
"tableBgcStripe": "#f8f8f8",

View File

@ -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);

View File

@ -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

View File

@ -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: [
{