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:
parent
1a4b6cddb5
commit
df47d1713c
@ -37,7 +37,7 @@ jobs:
|
||||
displayName: Cache Yarn packages
|
||||
- script: make install-deps
|
||||
displayName: Install dependencies
|
||||
- script: make integration-win
|
||||
- script: yarn integration:win
|
||||
displayName: Run integration tests
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
@ -76,7 +76,7 @@ jobs:
|
||||
displayName: Install dependencies
|
||||
- script: make test
|
||||
displayName: Run tests
|
||||
- script: make integration-mac
|
||||
- script: yarn integration:mac
|
||||
displayName: Run integration tests
|
||||
- script: make build
|
||||
condition: "and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/'))"
|
||||
@ -126,13 +126,13 @@ jobs:
|
||||
- bash: |
|
||||
sudo apt-get update
|
||||
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 minikube start --driver=none
|
||||
# Although the kube and minikube config files are in placed $HOME they are owned by root
|
||||
sudo chown -R $USER $HOME/.kube $HOME/.minikube
|
||||
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
|
||||
- bash: |
|
||||
sudo chown root:root /
|
||||
|
||||
14
Makefile
14
Makefile
@ -31,18 +31,6 @@ lint:
|
||||
test: download-bins
|
||||
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:
|
||||
yarn test
|
||||
|
||||
@ -62,4 +50,4 @@ else
|
||||
rm -rf binaries/client/*
|
||||
rm -rf dist/*
|
||||
rm -rf static/build/*
|
||||
endif
|
||||
endif
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
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
|
||||
cluster and vice versa.
|
||||
*/
|
||||
@ -38,7 +38,7 @@ describe("Lens integration tests", () => {
|
||||
beforeAll(appStart, 20000)
|
||||
|
||||
afterAll(async () => {
|
||||
if (app && app.isRunning()) {
|
||||
if (app?.isRunning()) {
|
||||
return util.tearDown(app)
|
||||
}
|
||||
})
|
||||
@ -140,7 +140,7 @@ describe("Lens integration tests", () => {
|
||||
await addCluster()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
describe("cluster pages", () => {
|
||||
|
||||
beforeAll(appStartAddCluster, 40000)
|
||||
@ -150,7 +150,7 @@ describe("Lens integration tests", () => {
|
||||
return util.tearDown(app)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const tests : {
|
||||
drawer?: string
|
||||
drawerId?: string
|
||||
@ -393,7 +393,7 @@ describe("Lens integration tests", () => {
|
||||
await app.client.click(`.sidebar-nav #${drawerId} span.link-text`)
|
||||
await app.client.waitUntilTextExists(`a[href="/${pages[0].href}"]`, pages[0].name)
|
||||
})
|
||||
}
|
||||
}
|
||||
pages.forEach(({name, href, expectedSelector, expectedText}) => {
|
||||
it(`shows ${drawer}->${name} page`, async () => {
|
||||
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()
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("cluster operations", () => {
|
||||
@ -420,7 +420,7 @@ describe("Lens integration tests", () => {
|
||||
return util.tearDown(app)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
it('shows default namespace', async () => {
|
||||
expect(clusterAdded).toBe(true)
|
||||
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.waitUntilTextExists("div.drawer-title-text", "Pod: nginx-create-pod-test")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,7 +23,10 @@
|
||||
"build:mac": "yarn compile && electron-builder --mac --dir -c.productName=Lens",
|
||||
"build:win": "yarn compile && electron-builder --win --dir -c.productName=Lens",
|
||||
"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:win": "yarn compile && electron-builder --publish onTag --x64 --ia32",
|
||||
"dist:dir": "yarn dist --dir -c.compression=store -c.mac.identity=null",
|
||||
@ -195,6 +198,7 @@
|
||||
"mobx": "^5.15.5",
|
||||
"mobx-observable-history": "^1.0.3",
|
||||
"mock-fs": "^4.12.0",
|
||||
"moment": "^2.26.0",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"node-pty": "^0.9.0",
|
||||
"openid-client": "^3.15.2",
|
||||
@ -294,7 +298,6 @@
|
||||
"material-design-icons": "^3.0.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mobx-react": "^6.2.2",
|
||||
"moment": "^2.26.0",
|
||||
"node-loader": "^0.6.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"nodemon": "^2.0.4",
|
||||
|
||||
30
scripts/integration.sh
Executable file
30
scripts/integration.sh
Executable 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
|
||||
@ -5,6 +5,9 @@
|
||||
import { ipcMain, ipcRenderer, WebContents, webContents } from "electron"
|
||||
import logger from "../main/logger";
|
||||
|
||||
export const NotificationChannelPrefix: IpcChannel = "notications:";
|
||||
export const NotificationChannelAdd: IpcChannel = `${NotificationChannelPrefix}add`;
|
||||
|
||||
export type IpcChannel = string;
|
||||
|
||||
export interface IpcChannelOptions {
|
||||
@ -50,7 +53,7 @@ export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, c
|
||||
return ipcChannel;
|
||||
}
|
||||
|
||||
export interface IpcBroadcastParams<A extends any[] = any> {
|
||||
export interface IpcBroadcastParams<A extends any[] = any[]> {
|
||||
channel: IpcChannel
|
||||
webContentId?: number; // send to single webContents view
|
||||
frameId?: number; // send to inner frame of webContents
|
||||
|
||||
3
src/common/utils/delay.ts
Normal file
3
src/common/utils/delay.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function delay(duration: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, duration));
|
||||
}
|
||||
@ -5,3 +5,4 @@ export * from "./camelCase"
|
||||
export * from "./splitArray"
|
||||
export * from "./getRandId"
|
||||
export * from "./cloneJson"
|
||||
export * from "./delay"
|
||||
|
||||
@ -1,19 +1,140 @@
|
||||
import { autoUpdater } from "electron-updater"
|
||||
import logger from "./logger"
|
||||
import { autoUpdater, UpdateInfo } from "electron-updater";
|
||||
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
|
||||
|
||||
constructor() {
|
||||
autoUpdater.logger = logger
|
||||
}
|
||||
|
||||
public start() {
|
||||
setInterval(() => {
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
}, this.updateInterval)
|
||||
|
||||
return autoUpdater.checkForUpdatesAndNotify()
|
||||
static nextId(): IpcChannel {
|
||||
return `${NotificationChannelPrefix}${NotificationBackchannel._id++}`
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ 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 { startUpdateChecking } from "./app-updater"
|
||||
import { shellSync } from "./shell-sync"
|
||||
import { getFreePort } from "./port"
|
||||
import { mangleProxyEnv } from "./proxy-env"
|
||||
@ -39,8 +39,6 @@ async function main() {
|
||||
logger.info(`🚀 Starting Lens from "${workingDir}"`)
|
||||
|
||||
tracker.event("app", "start");
|
||||
const updater = new AppUpdater()
|
||||
updater.start();
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
@ -75,6 +73,8 @@ async function main() {
|
||||
|
||||
// create window manager and open app
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
|
||||
startUpdateChecking(windowManager);
|
||||
}
|
||||
|
||||
app.on("ready", main);
|
||||
|
||||
@ -94,7 +94,7 @@ export class KubeAuthProxy {
|
||||
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
||||
const channel = `kube-auth:${this.cluster.id}`
|
||||
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
||||
broadcastIpc({ channel: channel, args: [res] });
|
||||
broadcastIpc({ channel, args: [res] });
|
||||
}
|
||||
|
||||
public exit() {
|
||||
|
||||
@ -7,7 +7,7 @@ import { initMenu } from "./menu";
|
||||
import { tracker } from "../common/tracker";
|
||||
|
||||
export class WindowManager {
|
||||
protected mainView: BrowserWindow;
|
||||
public readonly mainView: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected windowState: windowStateKeeper.State;
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import { i18nStore } from "./i18n";
|
||||
import { themeStore } from "./theme.store";
|
||||
import { App } from "./components/app";
|
||||
import { LensApp } from "./lens-app";
|
||||
import { notificationsStore } from "./components/notifications/notifications.store";
|
||||
|
||||
type AppComponent = React.ComponentType & {
|
||||
init?(): Promise<void>;
|
||||
|
||||
@ -20,10 +20,19 @@
|
||||
&.primary {
|
||||
background: $buttonPrimaryBackground;
|
||||
}
|
||||
|
||||
&.accent {
|
||||
background: $buttonAccentBackground;
|
||||
}
|
||||
|
||||
&.action {
|
||||
background: $buttonActionBackground;
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
background: $buttonSecondaryBackground;
|
||||
}
|
||||
|
||||
&.plain {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
|
||||
@ -8,6 +8,8 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, TooltipDecorator
|
||||
waiting?: boolean;
|
||||
primary?: boolean;
|
||||
accent?: boolean;
|
||||
secondary?: boolean;
|
||||
action?: boolean;
|
||||
plain?: boolean;
|
||||
hidden?: boolean;
|
||||
active?: boolean;
|
||||
@ -23,12 +25,15 @@ export class Button extends React.PureComponent<ButtonProps, {}> {
|
||||
private button: HTMLButtonElement;
|
||||
|
||||
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>;
|
||||
if (hidden) return null;
|
||||
|
||||
btnProps.className = cssNames('Button', className, {
|
||||
waiting, primary, accent, plain, active, big, round,
|
||||
waiting, primary, accent, plain, active, big, round, secondary, action,
|
||||
});
|
||||
|
||||
const btnContent: ReactNode = (
|
||||
|
||||
@ -25,6 +25,10 @@
|
||||
margin-bottom: $margin * 2;
|
||||
}
|
||||
|
||||
.ButtonPannel button:not(:last-of-type) {
|
||||
margin-right: $margin;
|
||||
}
|
||||
|
||||
> .message {
|
||||
white-space: pre-line;
|
||||
padding-left: $padding;
|
||||
|
||||
@ -4,6 +4,10 @@ import { autobind } from "../../utils";
|
||||
import isObject from "lodash/isObject"
|
||||
import uniqueId from "lodash/uniqueId";
|
||||
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 IMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
|
||||
@ -19,6 +23,38 @@ export interface INotification {
|
||||
message: IMessage;
|
||||
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"
|
||||
}
|
||||
|
||||
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()
|
||||
@ -27,6 +63,28 @@ export class NotificationsStore {
|
||||
|
||||
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) {
|
||||
this.removeAutoHideTimer(notification);
|
||||
const { id, timeout } = notification;
|
||||
@ -84,7 +84,10 @@ export class Notifications extends React.Component {
|
||||
<div className="box center">
|
||||
<Icon
|
||||
material="close" className="close"
|
||||
onClick={prevDefault(() => remove(notification))}
|
||||
onClick={prevDefault(() => {
|
||||
remove(notification);
|
||||
notification.onClose?.();
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -12,6 +12,7 @@ import { ErrorBoundary } from "./components/error-boundary";
|
||||
import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
|
||||
import { Notifications } from "./components/notifications";
|
||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||
import { notificationsStore } from "./components/notifications/notifications.store";
|
||||
|
||||
@observer
|
||||
export class LensApp extends React.Component {
|
||||
@ -22,6 +23,8 @@ export class LensApp extends React.Component {
|
||||
window.addEventListener("online", () => {
|
||||
ipcRenderer.send("network:online")
|
||||
})
|
||||
|
||||
notificationsStore.registerIpcListener();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@ -29,12 +29,8 @@ export class ThemeStore {
|
||||
{ id: "kontena-light", type: ThemeType.LIGHT },
|
||||
];
|
||||
|
||||
@computed get activeThemeId() {
|
||||
return userStore.preferences.colorTheme;
|
||||
}
|
||||
|
||||
@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 {
|
||||
colors: {},
|
||||
...activeTheme,
|
||||
@ -43,9 +39,9 @@ export class ThemeStore {
|
||||
|
||||
constructor() {
|
||||
// auto-apply active theme
|
||||
reaction(() => this.activeThemeId, async themeId => {
|
||||
reaction(() => this.activeTheme, async ({ id }) => {
|
||||
try {
|
||||
await this.loadTheme(themeId);
|
||||
await this.loadTheme(id);
|
||||
this.applyTheme();
|
||||
} catch (err) {
|
||||
userStore.resetTheme();
|
||||
|
||||
@ -26,6 +26,8 @@
|
||||
"buttonPrimaryBackground": "#3d90ce",
|
||||
"buttonDefaultBackground": "#414448",
|
||||
"buttonAccentBackground": "#e85555",
|
||||
"buttonActionBackground": "#1dcc1f",
|
||||
"buttonSecondaryBackground": "#edd70c",
|
||||
"buttonDisabledBackground": "#808080",
|
||||
"tableBgcStripe": "#2a2d33",
|
||||
"tableBgcSelected": "#383c42",
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
"buttonPrimaryBackground": "#3d90ce",
|
||||
"buttonDefaultBackground": "#414448",
|
||||
"buttonAccentBackground": "#e85555",
|
||||
"buttonActionBackground": "#1dcc1f",
|
||||
"buttonSecondaryBackground": "#edd70c",
|
||||
"buttonDisabledBackground": "#808080",
|
||||
"tableBgcStripe": "#f8f8f8",
|
||||
"tableBgcSelected": "#f4f5f5",
|
||||
|
||||
@ -35,6 +35,8 @@ $sidebarBackground: var(--sidebarBackground);
|
||||
$buttonPrimaryBackground: var(--buttonPrimaryBackground);
|
||||
$buttonDefaultBackground: var(--buttonDefaultBackground);
|
||||
$buttonAccentBackground: var(--buttonAccentBackground);
|
||||
$buttonSecondaryBackground: var(--buttonSecondaryBackground);
|
||||
$buttonActionBackground: var(--buttonActionBackground);
|
||||
$buttonDisabledBackground: var(--buttonDisabledBackground);
|
||||
|
||||
// Tables
|
||||
@ -125,4 +127,4 @@ $filterAreaBackground: var(--filterAreaBackground);
|
||||
$selectOptionHoveredColor: var(--selectOptionHoveredColor);
|
||||
$lineProgressBackground: var(--lineProgressBackground);
|
||||
$radioActiveBackground: var(--radioActiveBackground);
|
||||
$menuActiveBackground: var(--menuActiveBackground);
|
||||
$menuActiveBackground: var(--menuActiveBackground);
|
||||
|
||||
@ -159,4 +159,4 @@ export default function (): webpack.Configuration {
|
||||
}),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user