1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix auto-update to use Lens notifications, and add update confirmation (#1831)

- Add auto-update and pre-release update user settings

- Add settings in user preferences for auto-updating (default false) and
  for allowing pre-release versions (default false)

- Use in-Lens notifications instead of OS notifications as those were
  found to be flaky

- Add rudimentary main->renderer notification system.

- Remove options, always confirm, never auto prelease

- Changed "yes later" to "yes on quit"

- move register IpcHandlers

- use moment instead of dateformat

- moved formatting notification buttons to renderer

- move to RenderButtons as function component

- explicitly only send notifications to main view

- move delay to utils, always retry even if check failed

- fix notification rendering and disabled the auto-updater for integration tests

- update integration runner to output logs on failure

- pin minikube version

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-01-11 09:08:47 -05:00 committed by GitHub
parent 1a4b6cddb5
commit df47d1713c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 295 additions and 61 deletions

View File

@ -37,7 +37,7 @@ jobs:
displayName: Cache Yarn packages displayName: Cache Yarn packages
- script: make install-deps - script: make install-deps
displayName: Install dependencies displayName: Install dependencies
- script: make integration-win - script: yarn integration:win
displayName: Run integration tests displayName: Run integration tests
- script: make build - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
@ -76,7 +76,7 @@ jobs:
displayName: Install dependencies displayName: Install dependencies
- script: make test - script: make test
displayName: Run tests displayName: Run tests
- script: make integration-mac - script: yarn integration:mac
displayName: Run integration tests displayName: Run integration tests
- script: make build - script: make build
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))" condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
@ -126,13 +126,13 @@ jobs:
- bash: | - bash: |
sudo apt-get update sudo apt-get update
sudo apt-get install libgconf-2-4 conntrack -y sudo apt-get install libgconf-2-4 conntrack -y
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 curl -LO https://storage.googleapis.com/minikube/releases/v1.15.1/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube sudo install minikube-linux-amd64 /usr/local/bin/minikube
sudo minikube start --driver=none sudo minikube start --driver=none
# Although the kube and minikube config files are in placed $HOME they are owned by root # Although the kube and minikube config files are in placed $HOME they are owned by root
sudo chown -R $USER $HOME/.kube $HOME/.minikube sudo chown -R $USER $HOME/.kube $HOME/.minikube
displayName: Install integration test dependencies displayName: Install integration test dependencies
- script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' make integration-linux - script: xvfb-run --auto-servernum --server-args='-screen 0, 1600x900x24' yarn integration:linux
displayName: Run integration tests displayName: Run integration tests
- bash: | - bash: |
sudo chown root:root / sudo chown root:root /

View File

@ -31,18 +31,6 @@ lint:
test: download-bins test: download-bins
yarn test yarn test
integration-linux:
yarn build:linux
yarn integration
integration-mac:
yarn build:mac
yarn integration
integration-win:
yarn build:win
yarn integration
test-app: test-app:
yarn test yarn test
@ -62,4 +50,4 @@ else
rm -rf binaries/client/* rm -rf binaries/client/*
rm -rf dist/* rm -rf dist/*
rm -rf static/build/* rm -rf static/build/*
endif endif

View File

@ -1,6 +1,6 @@
/* /*
Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE Cluster tests are run if there is a pre-existing minikube cluster. Before running cluster tests the TEST_NAMESPACE
namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the namespace is removed, if it exists, from the minikube cluster. Resources are created as part of the cluster tests in the
TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube TEST_NAMESPACE namespace. This is done to minimize destructive impact of the cluster tests on an existing minikube
cluster and vice versa. cluster and vice versa.
*/ */
@ -38,7 +38,7 @@ describe("Lens integration tests", () => {
beforeAll(appStart, 20000) beforeAll(appStart, 20000)
afterAll(async () => { afterAll(async () => {
if (app && app.isRunning()) { if (app?.isRunning()) {
return util.tearDown(app) return util.tearDown(app)
} }
}) })
@ -140,7 +140,7 @@ describe("Lens integration tests", () => {
await addCluster() await addCluster()
} }
} }
describe("cluster pages", () => { describe("cluster pages", () => {
beforeAll(appStartAddCluster, 40000) beforeAll(appStartAddCluster, 40000)
@ -150,7 +150,7 @@ describe("Lens integration tests", () => {
return util.tearDown(app) return util.tearDown(app)
} }
}) })
const tests : { const tests : {
drawer?: string drawer?: string
drawerId?: string drawerId?: string
@ -393,7 +393,7 @@ describe("Lens integration tests", () => {
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`) await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name) await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name)
}) })
} }
pages.forEach(({name, href, expectedSelector, expectedText}) => { pages.forEach(({name, href, expectedSelector, expectedText}) => {
it(`shows ${drawer}->${name} page`, async () => { it(`shows ${drawer}->${name} page`, async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true)
@ -409,7 +409,7 @@ describe("Lens integration tests", () => {
await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow() await expect(app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name, 100)).rejects.toThrow()
}) })
} }
}) })
}) })
describe("cluster operations", () => { describe("cluster operations", () => {
@ -420,7 +420,7 @@ describe("Lens integration tests", () => {
return util.tearDown(app) return util.tearDown(app)
} }
}) })
it('shows default namespace', async () => { it('shows default namespace', async () => {
expect(clusterAdded).toBe(true) expect(clusterAdded).toBe(true)
await app.client.click('a[href="/namespaces"]') await app.client.click('a[href="/namespaces"]')
@ -468,6 +468,6 @@ describe("Lens integration tests", () => {
await app.client.click(".name=nginx-create-pod-test") await app.client.click(".name=nginx-create-pod-test")
await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test") await app.client.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test")
}) })
}) })
}) })
}) })

View File

@ -23,7 +23,10 @@
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens", "build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
"build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens", "build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",
"test": "jest --env=jsdom src $@", "test": "jest --env=jsdom src $@",
"integration": "jest --coverage integration $@", "integration-runner": "jest --coverage integration $@",
"integration:linux": "scripts/integration.sh linux",
"integration:mac": "scripts/integration.sh mac",
"integration:win": "scripts/integration.sh win",
"dist": "yarn compile && electron-builder --publish onTag", "dist": "yarn compile && electron-builder --publish onTag",
"dist:win": "yarn compile && electron-builder --publish onTag --x64 --ia32", "dist:win": "yarn compile && electron-builder --publish onTag --x64 --ia32",
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null", "dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
@ -195,6 +198,7 @@
"mobx": "^5.15.5", "mobx": "^5.15.5",
"mobx-observable-history": "^1.0.3", "mobx-observable-history": "^1.0.3",
"mock-fs": "^4.12.0", "mock-fs": "^4.12.0",
"moment": "^2.26.0",
"node-machine-id": "^1.1.12", "node-machine-id": "^1.1.12",
"node-pty": "^0.9.0", "node-pty": "^0.9.0",
"openid-client": "^3.15.2", "openid-client": "^3.15.2",
@ -294,7 +298,6 @@
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"mobx-react": "^6.2.2", "mobx-react": "^6.2.2",
"moment": "^2.26.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"nodemon": "^2.0.4", "nodemon": "^2.0.4",

30
scripts/integration.sh Executable file
View File

@ -0,0 +1,30 @@
case $1 in
mac)
find ~/Library/Logs/Lens -type f -name *.log -delete
;;
linux)
find ~/.config/Logs/Lens -type f -name *.log -delete
;;
win)
find %APPDATA%/Logs/Lens -type f -name *.log -delete
;;
esac
yarn build:$1
DEBUG=true yarn integration-runner
if [ $? -ne 0 ]; then
case $1 in
mac)
find ~/Library/Logs/Lens -type f -name *.log -exec cat >&2 {} \;
;;
linux)
find ~/.config/Logs/Lens -type f -name *.log -exec cat >&2 {} \;
;;
win)
find %APPDATA%/Logs/Lens -type f -name *.log -exec cat >&2 {} \;
;;
esac
exit 1
fi

View File

@ -5,6 +5,9 @@
import { ipcMain, ipcRenderer, WebContents, webContents } from "electron" import { ipcMain, ipcRenderer, WebContents, webContents } from "electron"
import logger from "../main/logger"; import logger from "../main/logger";
export const NotificationChannelPrefix: IpcChannel = "notications:";
export const NotificationChannelAdd: IpcChannel = `${NotificationChannelPrefix}add`;
export type IpcChannel = string; export type IpcChannel = string;
export interface IpcChannelOptions { export interface IpcChannelOptions {
@ -50,7 +53,7 @@ export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, c
return ipcChannel; return ipcChannel;
} }
export interface IpcBroadcastParams<A extends any[] = any> { export interface IpcBroadcastParams<A extends any[] = any[]> {
channel: IpcChannel channel: IpcChannel
webContentId?: number; // send to single webContents view webContentId?: number; // send to single webContents view
frameId?: number; // send to inner frame of webContents frameId?: number; // send to inner frame of webContents

View File

@ -0,0 +1,3 @@
export function delay(duration: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, duration));
}

View File

@ -5,3 +5,4 @@ export * from "./camelCase"
export * from "./splitArray" export * from "./splitArray"
export * from "./getRandId" export * from "./getRandId"
export * from "./cloneJson" export * from "./cloneJson"
export * from "./delay"

View File

@ -1,19 +1,140 @@
import { autoUpdater } from "electron-updater" import { autoUpdater, UpdateInfo } from "electron-updater";
import logger from "./logger" import logger from "./logger";
import { IpcChannel, NotificationChannelAdd, NotificationChannelPrefix } from "../common/ipc";
import { ipcMain } from "electron";
import { isDevelopment, isTestEnv } from "../common/vars";
import { SemVer } from "semver";
import moment from "moment";
import { WindowManager } from "./window-manager"
import { delay } from "../common/utils";
export default class AppUpdater { class NotificationBackchannel {
private static _id = 0;
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day static nextId(): IpcChannel {
return `${NotificationChannelPrefix}${NotificationBackchannel._id++}`
constructor() {
autoUpdater.logger = logger
}
public start() {
setInterval(() => {
autoUpdater.checkForUpdatesAndNotify()
}, this.updateInterval)
return autoUpdater.checkForUpdatesAndNotify()
} }
} }
const title = "Lens Updater";
async function autoUpdateCheck(windowManager: WindowManager): Promise<void> {
return new Promise(async resolve => {
const body = "Install and restart Lens?";
const yesNowChannel = NotificationBackchannel.nextId();
const yesLaterChannel = NotificationBackchannel.nextId();
const noChannel = NotificationBackchannel.nextId();
function cleanupChannels() {
ipcMain.removeAllListeners(yesNowChannel);
ipcMain.removeAllListeners(yesLaterChannel);
ipcMain.removeAllListeners(noChannel);
}
ipcMain
.on(yesNowChannel, async () => {
logger.info("[UPDATE CHECKER]: User chose to update immediately");
cleanupChannels();
await autoUpdater.downloadUpdate();
autoUpdater.quitAndInstall();
resolve();
})
.on(yesLaterChannel, async () => {
logger.info("[UPDATE CHECKER]: User chose to update on quit");
cleanupChannels();
await autoUpdater.downloadUpdate();
autoUpdater.autoInstallOnAppQuit = true;
resolve();
})
.on(noChannel, () => {
logger.info("[UPDATE CHECKER]: User chose not to update");
cleanupChannels();
resolve();
});
windowManager.mainView.webContents.send(NotificationChannelAdd, {
title,
body,
status: "info",
buttons: [
{
label: "Yes, now",
backchannel: yesNowChannel,
action: true,
},
{
label: "Yes, on quit",
backchannel: yesLaterChannel,
action: true,
},
{
label: "No",
backchannel: noChannel,
secondary: true
}
],
closeChannel: noChannel,
});
});
}
/**
* starts the automatic update checking
* @param interval milliseconds between interval to check on, defaults to 24h
*/
export function startUpdateChecking(windowManager: WindowManager, interval = 1000 * 60 * 60 * 24): void {
if (isDevelopment || isTestEnv) {
return;
}
autoUpdater.logger = logger;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = false;
autoUpdater
.on("update-available", async (args: UpdateInfo) => {
try {
const releaseDate = moment(args.releaseDate);
const body = `Version ${args.version} was released on ${releaseDate.format("dddd, MMMM Do, yyyy")}.`;
windowManager.mainView.webContents.send(NotificationChannelAdd, {
title,
body,
status: "info",
timeout: 5000,
});
await autoUpdateCheck(windowManager);
} catch (error) {
logger.error("[UPDATE CHECKER]: notification failed", { error: String(error) })
}
})
.on("update-not-available", (args: UpdateInfo) => {
try {
const version = new SemVer(args.version);
const stream = version.prerelease === null ? "stable" : "prerelease";
const body = `Lens is running the latest ${stream} version.`;
windowManager.mainView.webContents.send(NotificationChannelAdd, {
title,
body,
status: "info",
timeout: 5000,
})
} catch (error) {
logger.error("[UPDATE CHECKER]: notification failed", { error: String(error) })
}
})
async function helper() {
while (true) {
await autoUpdater.checkForUpdates()
.catch(error => logger.error("[UPDATE CHECKER]: failed with an error", { error: String(error) }));
await delay(interval);
}
}
helper();
}

View File

@ -8,7 +8,7 @@ import path from "path"
import { LensProxy } from "./lens-proxy" import { LensProxy } from "./lens-proxy"
import { WindowManager } from "./window-manager"; import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import AppUpdater from "./app-updater" import { startUpdateChecking } from "./app-updater"
import { shellSync } from "./shell-sync" import { shellSync } from "./shell-sync"
import { getFreePort } from "./port" import { getFreePort } from "./port"
import { mangleProxyEnv } from "./proxy-env" import { mangleProxyEnv } from "./proxy-env"
@ -39,8 +39,6 @@ async function main() {
logger.info(`🚀 Starting Lens from "${workingDir}"`) logger.info(`🚀 Starting Lens from "${workingDir}"`)
tracker.event("app", "start"); tracker.event("app", "start");
const updater = new AppUpdater()
updater.start();
registerFileProtocol("static", __static); registerFileProtocol("static", __static);
@ -75,6 +73,8 @@ async function main() {
// create window manager and open app // create window manager and open app
windowManager = new WindowManager(proxyPort); windowManager = new WindowManager(proxyPort);
startUpdateChecking(windowManager);
} }
app.on("ready", main); app.on("ready", main);

View File

@ -94,7 +94,7 @@ export class KubeAuthProxy {
protected async sendIpcLogMessage(res: KubeAuthProxyLog) { protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
const channel = `kube-auth:${this.cluster.id}` const channel = `kube-auth:${this.cluster.id}`
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() }); logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
broadcastIpc({ channel: channel, args: [res] }); broadcastIpc({ channel, args: [res] });
} }
public exit() { public exit() {

View File

@ -7,7 +7,7 @@ import { initMenu } from "./menu";
import { tracker } from "../common/tracker"; import { tracker } from "../common/tracker";
export class WindowManager { export class WindowManager {
protected mainView: BrowserWindow; public readonly mainView: BrowserWindow;
protected splashWindow: BrowserWindow; protected splashWindow: BrowserWindow;
protected windowState: windowStateKeeper.State; protected windowState: windowStateKeeper.State;

View File

@ -9,6 +9,7 @@ import { i18nStore } from "./i18n";
import { themeStore } from "./theme.store"; import { themeStore } from "./theme.store";
import { App } from "./components/app"; import { App } from "./components/app";
import { LensApp } from "./lens-app"; import { LensApp } from "./lens-app";
import { notificationsStore } from "./components/notifications/notifications.store";
type AppComponent = React.ComponentType & { type AppComponent = React.ComponentType & {
init?(): Promise<void>; init?(): Promise<void>;

View File

@ -20,10 +20,19 @@
&.primary { &.primary {
background: $buttonPrimaryBackground; background: $buttonPrimaryBackground;
} }
&.accent { &.accent {
background: $buttonAccentBackground; background: $buttonAccentBackground;
} }
&.action {
background: $buttonActionBackground;
}
&.secondary {
background: $buttonSecondaryBackground;
}
&.plain { &.plain {
color: inherit; color: inherit;
background: transparent; background: transparent;

View File

@ -8,6 +8,8 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, TooltipDecorator
waiting?: boolean; waiting?: boolean;
primary?: boolean; primary?: boolean;
accent?: boolean; accent?: boolean;
secondary?: boolean;
action?: boolean;
plain?: boolean; plain?: boolean;
hidden?: boolean; hidden?: boolean;
active?: boolean; active?: boolean;
@ -23,12 +25,15 @@ export class Button extends React.PureComponent<ButtonProps, {}> {
private button: HTMLButtonElement; private button: HTMLButtonElement;
render() { render() {
const { className, waiting, label, primary, accent, plain, hidden, active, big, round, tooltip, children, ...props } = this.props; const {
className, waiting, label, primary, accent, plain, hidden, active, big,
round, tooltip, children, secondary, action, ...props
} = this.props;
const btnProps = props as Partial<ButtonProps>; const btnProps = props as Partial<ButtonProps>;
if (hidden) return null; if (hidden) return null;
btnProps.className = cssNames('Button', className, { btnProps.className = cssNames('Button', className, {
waiting, primary, accent, plain, active, big, round, waiting, primary, accent, plain, active, big, round, secondary, action,
}); });
const btnContent: ReactNode = ( const btnContent: ReactNode = (

View File

@ -25,6 +25,10 @@
margin-bottom: $margin * 2; margin-bottom: $margin * 2;
} }
.ButtonPannel button:not(:last-of-type) {
margin-right: $margin;
}
> .message { > .message {
white-space: pre-line; white-space: pre-line;
padding-left: $padding; padding-left: $padding;

View File

@ -4,6 +4,10 @@ import { autobind } from "../../utils";
import isObject from "lodash/isObject" import isObject from "lodash/isObject"
import uniqueId from "lodash/uniqueId"; import uniqueId from "lodash/uniqueId";
import { JsonApiErrorParsed } from "../../api/json-api"; import { JsonApiErrorParsed } from "../../api/json-api";
import logger from "../../../main/logger";
import { ipcRenderer } from "electron";
import { IpcChannel, NotificationChannelAdd } from "../../../common/ipc";
import { Button, ButtonProps } from "../button";
export type IMessageId = string | number; export type IMessageId = string | number;
export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed; export type IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
@ -19,6 +23,38 @@ export interface INotification {
message: IMessage; message: IMessage;
status?: NotificationStatus; status?: NotificationStatus;
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide 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"
}
export interface MainNotification {
title: string;
body: string;
buttons?: ({
backchannel: IpcChannel;
} & ButtonProps)[];
status: NotificationStatus;
timeout?: number;
closeChannel?: IpcChannel;
}
function RenderButtons({ id, buttons }: { id: IpcChannel, buttons?: MainNotification["buttons"] }) {
if (!buttons) {
return null;
}
return (
<>
<br />
<div className="ButtonPannel flex row align-right box grow">
{buttons.map(({ backchannel, ...props}) => (
<Button {...props} onClick={() => {
ipcRenderer.send(backchannel);
notificationsStore.remove(id);
}} />
))}
</div>
</>
)
} }
@autobind() @autobind()
@ -27,6 +63,28 @@ export class NotificationsStore {
protected autoHideTimers = new Map<IMessageId, number>(); protected autoHideTimers = new Map<IMessageId, number>();
registerIpcListener(): void {
logger.info(`[NOTIFICATION-STORE] start to listen for notifications requests from main`);
ipcRenderer.on(NotificationChannelAdd, (event, model: MainNotification) => {
const id = uniqueId("notification_");
this.add({
message: (
<>
<b>{model.title}</b>
<p>{model.body}</p>
<RenderButtons id={id} buttons={model.buttons}/>
</>
),
id,
status: model.status,
timeout: model.timeout,
onClose: () => {
model.closeChannel && ipcRenderer.send(model.closeChannel);
}
});
})
}
addAutoHideTimer(notification: INotification) { addAutoHideTimer(notification: INotification) {
this.removeAutoHideTimer(notification); this.removeAutoHideTimer(notification);
const { id, timeout } = notification; const { id, timeout } = notification;

View File

@ -84,7 +84,10 @@ export class Notifications extends React.Component {
<div className="box center"> <div className="box center">
<Icon <Icon
material="close" className="close" material="close" className="close"
onClick={prevDefault(() => remove(notification))} onClick={prevDefault(() => {
remove(notification);
notification.onClose?.();
})}
/> />
</div> </div>
</div> </div>

View File

@ -12,6 +12,7 @@ import { ErrorBoundary } from "./components/error-boundary";
import { WhatsNew, whatsNewRoute } from "./components/+whats-new"; import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
import { Notifications } from "./components/notifications"; import { Notifications } from "./components/notifications";
import { ConfirmDialog } from "./components/confirm-dialog"; import { ConfirmDialog } from "./components/confirm-dialog";
import { notificationsStore } from "./components/notifications/notifications.store";
@observer @observer
export class LensApp extends React.Component { export class LensApp extends React.Component {
@ -22,6 +23,8 @@ export class LensApp extends React.Component {
window.addEventListener("online", () => { window.addEventListener("online", () => {
ipcRenderer.send("network:online") ipcRenderer.send("network:online")
}) })
notificationsStore.registerIpcListener();
} }
render() { render() {

View File

@ -29,12 +29,8 @@ export class ThemeStore {
{ id: "kontena-light", type: ThemeType.LIGHT }, { id: "kontena-light", type: ThemeType.LIGHT },
]; ];
@computed get activeThemeId() {
return userStore.preferences.colorTheme;
}
@computed get activeTheme(): Theme { @computed get activeTheme(): Theme {
const activeTheme = this.themes.find(theme => theme.id === this.activeThemeId) || this.themes[0]; const activeTheme = this.themes.find(theme => theme.id === userStore.preferences.colorTheme) || this.themes[0];
return { return {
colors: {}, colors: {},
...activeTheme, ...activeTheme,
@ -43,9 +39,9 @@ export class ThemeStore {
constructor() { constructor() {
// auto-apply active theme // auto-apply active theme
reaction(() => this.activeThemeId, async themeId => { reaction(() => this.activeTheme, async ({ id }) => {
try { try {
await this.loadTheme(themeId); await this.loadTheme(id);
this.applyTheme(); this.applyTheme();
} catch (err) { } catch (err) {
userStore.resetTheme(); userStore.resetTheme();

View File

@ -26,6 +26,8 @@
"buttonPrimaryBackground": "#3d90ce", "buttonPrimaryBackground": "#3d90ce",
"buttonDefaultBackground": "#414448", "buttonDefaultBackground": "#414448",
"buttonAccentBackground": "#e85555", "buttonAccentBackground": "#e85555",
"buttonActionBackground": "#1dcc1f",
"buttonSecondaryBackground": "#edd70c",
"buttonDisabledBackground": "#808080", "buttonDisabledBackground": "#808080",
"tableBgcStripe": "#2a2d33", "tableBgcStripe": "#2a2d33",
"tableBgcSelected": "#383c42", "tableBgcSelected": "#383c42",

View File

@ -27,6 +27,8 @@
"buttonPrimaryBackground": "#3d90ce", "buttonPrimaryBackground": "#3d90ce",
"buttonDefaultBackground": "#414448", "buttonDefaultBackground": "#414448",
"buttonAccentBackground": "#e85555", "buttonAccentBackground": "#e85555",
"buttonActionBackground": "#1dcc1f",
"buttonSecondaryBackground": "#edd70c",
"buttonDisabledBackground": "#808080", "buttonDisabledBackground": "#808080",
"tableBgcStripe": "#f8f8f8", "tableBgcStripe": "#f8f8f8",
"tableBgcSelected": "#f4f5f5", "tableBgcSelected": "#f4f5f5",

View File

@ -35,6 +35,8 @@ $sidebarBackground: var(--sidebarBackground);
$buttonPrimaryBackground: var(--buttonPrimaryBackground); $buttonPrimaryBackground: var(--buttonPrimaryBackground);
$buttonDefaultBackground: var(--buttonDefaultBackground); $buttonDefaultBackground: var(--buttonDefaultBackground);
$buttonAccentBackground: var(--buttonAccentBackground); $buttonAccentBackground: var(--buttonAccentBackground);
$buttonSecondaryBackground: var(--buttonSecondaryBackground);
$buttonActionBackground: var(--buttonActionBackground);
$buttonDisabledBackground: var(--buttonDisabledBackground); $buttonDisabledBackground: var(--buttonDisabledBackground);
// Tables // Tables
@ -125,4 +127,4 @@ $filterAreaBackground: var(--filterAreaBackground);
$selectOptionHoveredColor: var(--selectOptionHoveredColor); $selectOptionHoveredColor: var(--selectOptionHoveredColor);
$lineProgressBackground: var(--lineProgressBackground); $lineProgressBackground: var(--lineProgressBackground);
$radioActiveBackground: var(--radioActiveBackground); $radioActiveBackground: var(--radioActiveBackground);
$menuActiveBackground: var(--menuActiveBackground); $menuActiveBackground: var(--menuActiveBackground);

View File

@ -159,4 +159,4 @@ export default function (): webpack.Configuration {
}), }),
], ],
} }
} }