From df47d1713c8ae2750dd99a007bcbf651cfcfc71c Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Mon, 11 Jan 2021 09:08:47 -0500 Subject: [PATCH] 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 --- .azure-pipelines.yml | 8 +- Makefile | 14 +- integration/specs/app_spec.ts | 18 +-- package.json | 7 +- scripts/integration.sh | 30 ++++ src/common/ipc.ts | 5 +- src/common/utils/delay.ts | 3 + src/common/utils/index.ts | 1 + src/main/app-updater.ts | 151 ++++++++++++++++-- src/main/index.ts | 6 +- src/main/kube-auth-proxy.ts | 2 +- src/main/window-manager.ts | 2 +- src/renderer/bootstrap.tsx | 1 + src/renderer/components/button/button.scss | 9 ++ src/renderer/components/button/button.tsx | 9 +- .../notifications/notifications.scss | 4 + ...tions.store.ts => notifications.store.tsx} | 58 +++++++ .../notifications/notifications.tsx | 5 +- src/renderer/lens-app.tsx | 3 + src/renderer/theme.store.ts | 10 +- src/renderer/themes/kontena-dark.json | 2 + src/renderer/themes/kontena-light.json | 2 + src/renderer/themes/theme-vars.scss | 4 +- webpack.renderer.ts | 2 +- 24 files changed, 295 insertions(+), 61 deletions(-) create mode 100755 scripts/integration.sh create mode 100644 src/common/utils/delay.ts rename src/renderer/components/notifications/{notifications.store.ts => notifications.store.tsx} (54%) diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 009418b1c0..88aadfd87b 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -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 / diff --git a/Makefile b/Makefile index e492aa012c..a997542e5c 100644 --- a/Makefile +++ b/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 \ No newline at end of file +endif diff --git a/integration/specs/app_spec.ts b/integration/specs/app_spec.ts index 5cde34afb5..8ac0ba4ac5 100644 --- a/integration/specs/app_spec.ts +++ b/integration/specs/app_spec.ts @@ -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") }) - }) - }) + }) + }) }) diff --git a/package.json b/package.json index 1d6269b0c0..6309d5a0c7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/integration.sh b/scripts/integration.sh new file mode 100755 index 0000000000..122d916b3e --- /dev/null +++ b/scripts/integration.sh @@ -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 diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 97c4dd05cd..6a6c4104dd 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -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 { +export interface IpcBroadcastParams { channel: IpcChannel webContentId?: number; // send to single webContents view frameId?: number; // send to inner frame of webContents diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts new file mode 100644 index 0000000000..538c0a28c2 --- /dev/null +++ b/src/common/utils/delay.ts @@ -0,0 +1,3 @@ +export function delay(duration: number): Promise { + return new Promise(resolve => setTimeout(resolve, duration)); +} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 580a8f15c2..93729ec93b 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -5,3 +5,4 @@ export * from "./camelCase" export * from "./splitArray" export * from "./getRandId" export * from "./cloneJson" +export * from "./delay" diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 7b27a7c3f6..f11c67e673 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -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 { + 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(); +} diff --git a/src/main/index.ts b/src/main/index.ts index e4fd246467..31202314c5 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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); diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 96c3063277..f48954b6e6 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -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() { diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 74aeb010ea..ca5a312e8b 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -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; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index f03df719b6..608a0b4005 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -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; diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss index e85647ca9f..188d82f9cb 100644 --- a/src/renderer/components/button/button.scss +++ b/src/renderer/components/button/button.scss @@ -20,10 +20,19 @@ &.primary { background: $buttonPrimaryBackground; } + &.accent { background: $buttonAccentBackground; } + &.action { + background: $buttonActionBackground; + } + + &.secondary { + background: $buttonSecondaryBackground; + } + &.plain { color: inherit; background: transparent; diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx index c0cb8dcc49..bf065942a7 100644 --- a/src/renderer/components/button/button.tsx +++ b/src/renderer/components/button/button.tsx @@ -8,6 +8,8 @@ export interface ButtonProps extends ButtonHTMLAttributes, 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 { 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; 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 = ( diff --git a/src/renderer/components/notifications/notifications.scss b/src/renderer/components/notifications/notifications.scss index 37d4990ee5..a39a00b227 100644 --- a/src/renderer/components/notifications/notifications.scss +++ b/src/renderer/components/notifications/notifications.scss @@ -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; diff --git a/src/renderer/components/notifications/notifications.store.ts b/src/renderer/components/notifications/notifications.store.tsx similarity index 54% rename from src/renderer/components/notifications/notifications.store.ts rename to src/renderer/components/notifications/notifications.store.tsx index 1873ae91a4..58f9360e1e 100644 --- a/src/renderer/components/notifications/notifications.store.ts +++ b/src/renderer/components/notifications/notifications.store.tsx @@ -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 ( + <> +
+
+ {buttons.map(({ backchannel, ...props}) => ( +
+ + ) } @autobind() @@ -27,6 +63,28 @@ export class NotificationsStore { protected autoHideTimers = new Map(); + 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: ( + <> + {model.title} +

{model.body}

+ + + ), + id, + status: model.status, + timeout: model.timeout, + onClose: () => { + model.closeChannel && ipcRenderer.send(model.closeChannel); + } + }); + }) + } + addAutoHideTimer(notification: INotification) { this.removeAutoHideTimer(notification); const { id, timeout } = notification; diff --git a/src/renderer/components/notifications/notifications.tsx b/src/renderer/components/notifications/notifications.tsx index 35c741505c..6ca687fee4 100644 --- a/src/renderer/components/notifications/notifications.tsx +++ b/src/renderer/components/notifications/notifications.tsx @@ -84,7 +84,10 @@ export class Notifications extends React.Component {
remove(notification))} + onClick={prevDefault(() => { + remove(notification); + notification.onClose?.(); + })} />
diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 70b992a910..4ad795fc2a 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -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() { diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index 5245d2a728..992c280f6c 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -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(); diff --git a/src/renderer/themes/kontena-dark.json b/src/renderer/themes/kontena-dark.json index a0d0eb9869..493219031a 100644 --- a/src/renderer/themes/kontena-dark.json +++ b/src/renderer/themes/kontena-dark.json @@ -26,6 +26,8 @@ "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", "buttonAccentBackground": "#e85555", + "buttonActionBackground": "#1dcc1f", + "buttonSecondaryBackground": "#edd70c", "buttonDisabledBackground": "#808080", "tableBgcStripe": "#2a2d33", "tableBgcSelected": "#383c42", diff --git a/src/renderer/themes/kontena-light.json b/src/renderer/themes/kontena-light.json index f39fa53d00..292863dbeb 100644 --- a/src/renderer/themes/kontena-light.json +++ b/src/renderer/themes/kontena-light.json @@ -27,6 +27,8 @@ "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", "buttonAccentBackground": "#e85555", + "buttonActionBackground": "#1dcc1f", + "buttonSecondaryBackground": "#edd70c", "buttonDisabledBackground": "#808080", "tableBgcStripe": "#f8f8f8", "tableBgcSelected": "#f4f5f5", diff --git a/src/renderer/themes/theme-vars.scss b/src/renderer/themes/theme-vars.scss index c6c3072b8f..10139b83c5 100644 --- a/src/renderer/themes/theme-vars.scss +++ b/src/renderer/themes/theme-vars.scss @@ -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); \ No newline at end of file +$menuActiveBackground: var(--menuActiveBackground); diff --git a/webpack.renderer.ts b/webpack.renderer.ts index 74978bd152..6112d1af36 100755 --- a/webpack.renderer.ts +++ b/webpack.renderer.ts @@ -159,4 +159,4 @@ export default function (): webpack.Configuration { }), ], } -} \ No newline at end of file +}