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
|
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 /
|
||||||
|
|||||||
14
Makefile
14
Makefile
@ -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
|
||||||
|
|||||||
@ -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")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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
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 { 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
|
||||||
|
|||||||
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 "./splitArray"
|
||||||
export * from "./getRandId"
|
export * from "./getRandId"
|
||||||
export * from "./cloneJson"
|
export * from "./cloneJson"
|
||||||
|
export * from "./delay"
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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>;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = (
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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() {
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -159,4 +159,4 @@ export default function (): webpack.Configuration {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user