diff --git a/apps/zui/dev-app-update.yml b/apps/zui/dev-app-update.yml new file mode 100644 index 0000000000..645c5ffd17 --- /dev/null +++ b/apps/zui/dev-app-update.yml @@ -0,0 +1,4 @@ +owner: brimdata +repo: zui +provider: github +updaterCacheDirName: zui-dev-updater diff --git a/apps/zui/pages/update.tsx b/apps/zui/pages/update.tsx new file mode 100644 index 0000000000..d6f0cb19e4 --- /dev/null +++ b/apps/zui/pages/update.tsx @@ -0,0 +1,19 @@ +import React, {useEffect, useState} from "react" +import {AppProvider} from "src/app/core/context" +import initialize from "src/js/initializers/initialize" +import {UpdateWindow} from "src/views/update-window" + +export default function Update() { + const [app, setApp] = useState(null) + + useEffect(() => { + initialize().then((app) => setApp(app)) + }, []) + + if (!app) return null + return ( + + + + ) +} diff --git a/apps/zui/public/zui-icon.svg b/apps/zui/public/zui-icon.svg new file mode 100644 index 0000000000..16298224b9 --- /dev/null +++ b/apps/zui/public/zui-icon.svg @@ -0,0 +1,54 @@ + + + icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/zui/src/app/core/env.ts b/apps/zui/src/app/core/env.ts index ce3bc05d17..a9730aebc6 100644 --- a/apps/zui/src/app/core/env.ts +++ b/apps/zui/src/app/core/env.ts @@ -1,4 +1,5 @@ import {app as electronApp} from "electron" +import pkg from "src/electron/pkg" const isPackaged = () => electronApp.isPackaged || process.env.NODE_ENV === "production" @@ -31,4 +32,7 @@ export default { get isLinux() { return process.platform === "linux" }, + get isInsiders() { + return pkg.name === "zui-insiders" + }, } diff --git a/apps/zui/src/app/core/links.test.ts b/apps/zui/src/app/core/links.test.ts index 426e761b86..8b2a0dfbb9 100644 --- a/apps/zui/src/app/core/links.test.ts +++ b/apps/zui/src/app/core/links.test.ts @@ -6,7 +6,7 @@ const fetchStatusCode = async (link: string): Promise<[string, number]> => { const controller = new AbortController() const timeout = setTimeout(() => { controller.abort() - }, 1000) + }, 10_000) try { const resp = await fetch(link, {signal: controller.signal}) diff --git a/apps/zui/src/components/forms.module.css b/apps/zui/src/components/forms.module.css index aedfaaabef..3398d30f61 100644 --- a/apps/zui/src/components/forms.module.css +++ b/apps/zui/src/components/forms.module.css @@ -85,6 +85,7 @@ color: white; font-weight: 500; min-width: 80px; + user-select: none; } .form .submit:hover:not(:disabled) { @@ -110,6 +111,7 @@ box-shadow: 0 3px 4px -4px var(--foreground-color-light); font-weight: 500; min-width: 80px; + user-select: none; } .form .button:hover:not(:disabled) { diff --git a/apps/zui/src/core/log.ts b/apps/zui/src/core/log.ts new file mode 100644 index 0000000000..024cbd9240 --- /dev/null +++ b/apps/zui/src/core/log.ts @@ -0,0 +1,5 @@ +import electronLog from "electron-log" + +export const info = electronLog.info +export const debug = electronLog.debug +export const error = electronLog.error diff --git a/apps/zui/src/core/main/main-object.ts b/apps/zui/src/core/main/main-object.ts index 22eabacc96..13f6565b05 100644 --- a/apps/zui/src/core/main/main-object.ts +++ b/apps/zui/src/core/main/main-object.ts @@ -120,8 +120,6 @@ export class MainObject { return this.store.dispatch as AppDispatch } - select - getPath(name: PathName) { return getPath(name) } diff --git a/apps/zui/src/core/on-state-change.ts b/apps/zui/src/core/on-state-change.ts new file mode 100644 index 0000000000..0508785bd3 --- /dev/null +++ b/apps/zui/src/core/on-state-change.ts @@ -0,0 +1,22 @@ +import {Selector} from "@reduxjs/toolkit" +import {Store} from "src/js/state/types" + +export function onStateChange( + store: Store, + selector: Selector, + onChange: (value: any) => void +) { + let current = undefined + + function listener() { + const next = selector(store.getState()) + if (next !== current) { + current = next + onChange(current) + } + } + + const unsubscribe = store.subscribe(listener) + listener() + return unsubscribe +} diff --git a/apps/zui/src/core/operations.ts b/apps/zui/src/core/operations.ts index 5784ea539d..df1e587cc5 100644 --- a/apps/zui/src/core/operations.ts +++ b/apps/zui/src/core/operations.ts @@ -1,10 +1,14 @@ import {ipcMain, IpcMainInvokeEvent} from "electron" import {OperationName} from "src/domain/messages" import {MainObject} from "./main/main-object" +import {Dispatch} from "src/js/state/types" +import {select} from "./main/select" type OperationContext = { + dispatch: Dispatch main: MainObject event?: IpcMainInvokeEvent | null + select: typeof select } let context: OperationContext | null = null diff --git a/apps/zui/src/domain/messages.ts b/apps/zui/src/domain/messages.ts index 9d43e8c0cb..648a67a660 100644 --- a/apps/zui/src/domain/messages.ts +++ b/apps/zui/src/domain/messages.ts @@ -7,6 +7,7 @@ import {WindowHandlers, WindowOperations} from "./window/messages" import {LegacyOperations} from "./legacy-ops/messages" import {E2EOperations} from "./e2e/messages" import {EnvOperations} from "./env/messages" +import {UpdatesOperations} from "./updates/messages" import {LoadsHandlers, LoadsOperations} from "./loads/messages" export type Handlers = ResultsHandlers & @@ -22,6 +23,8 @@ export type Operations = PoolsOperations & E2EOperations & ResultsOperations & EnvOperations & + WindowOperations & + UpdatesOperations & LoadsOperations & WindowOperations diff --git a/apps/zui/src/domain/updates/handlers.ts b/apps/zui/src/domain/updates/handlers.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/zui/src/domain/updates/linux-updater.ts b/apps/zui/src/domain/updates/linux-updater.ts new file mode 100644 index 0000000000..8d3203c726 --- /dev/null +++ b/apps/zui/src/domain/updates/linux-updater.ts @@ -0,0 +1,45 @@ +import {app, shell} from "electron" +import fetch from "node-fetch" +import semver from "semver" +import env from "src/app/core/env" +import links from "src/app/core/links" +import pkg from "src/electron/pkg" +import {Updater} from "./types" +import {getMainObject} from "src/core/main" + +export class LinuxUpdater implements Updater { + async check() { + const latest = await this.latest() + const current = app.getVersion() + if (semver.lt(current, latest)) { + return latest + } else { + return null + } + } + + async install() { + shell.openExternal(this.downloadUrl()) + } + + private async latest() { + const resp = await fetch(this.latestUrl()) + if (resp.status === 204) return app.getVersion() + const data = await resp.json() + return data.name + } + + private latestUrl() { + const repo = getMainObject().appMeta.repo + const platform = "darwin-x64" // If the mac version exists, the linux does too + return `https://update.electronjs.org/${repo}/${platform}/${app.getVersion()}` + } + + private downloadUrl() { + if (env.isInsiders) { + return pkg.repository + "/releases/latest" + } else { + return links.ZUI_DOWNLOAD + } + } +} diff --git a/apps/zui/src/domain/updates/mac-win-updater.ts b/apps/zui/src/domain/updates/mac-win-updater.ts new file mode 100644 index 0000000000..9aab2156b1 --- /dev/null +++ b/apps/zui/src/domain/updates/mac-win-updater.ts @@ -0,0 +1,43 @@ +import {autoUpdater} from "electron-updater" +import {Updater} from "./types" +import semver from "semver" +import {app} from "electron" +import {getMainObject} from "src/core/main" + +autoUpdater.autoDownload = false +autoUpdater.autoInstallOnAppQuit = false + +export class MacWinUpdater implements Updater { + async check() { + const {updateInfo} = await autoUpdater.checkForUpdates() + const latest = updateInfo.version + const current = app.getVersion() + if (semver.lt(current, latest)) { + return latest + } else { + return null + } + } + + async install(onProgress) { + const progress = (r) => { + onProgress(r.percent / 100) + } + autoUpdater.on("error", (e) => { + throw e + }) + autoUpdater.on("download-progress", progress) + + return new Promise((resolve, reject) => { + autoUpdater.on("update-downloaded", resolve) + autoUpdater.on("error", reject) + autoUpdater.downloadUpdate() + }).then(() => { + // `autoUpdater.quitAndInstall()` will close all application windows first and only emit `before-quit` event on `app` after that. + // We have some logic when closing windows that checks to see if we are quitting or not. + // So we call onBeforeQuit manually here to tell the main object we are quitting + getMainObject().onBeforeQuit() + autoUpdater.quitAndInstall() + }) + } +} diff --git a/apps/zui/src/domain/updates/messages.ts b/apps/zui/src/domain/updates/messages.ts new file mode 100644 index 0000000000..9b5e218059 --- /dev/null +++ b/apps/zui/src/domain/updates/messages.ts @@ -0,0 +1,7 @@ +import * as operations from "./operations" + +export type UpdatesOperations = { + "updates.open": typeof operations.open + "updates.check": typeof operations.check + "updates.install": typeof operations.install +} diff --git a/apps/zui/src/domain/updates/operations.ts b/apps/zui/src/domain/updates/operations.ts new file mode 100644 index 0000000000..e784037e55 --- /dev/null +++ b/apps/zui/src/domain/updates/operations.ts @@ -0,0 +1,50 @@ +import {createOperation} from "src/core/operations" +import {updater} from "./updater" +import Updates from "src/js/state/Updates" +import {errorToString} from "src/util/error-to-string" +import {info} from "src/core/log" + +export const open = createOperation("updates.open", ({main}) => { + check() + main.windows.activate("update") +}) + +export const check = createOperation( + "updates.check", + async ({main, dispatch}) => { + try { + info("Checking for Updates...") + dispatch(Updates.setIsChecking(true)) + const newVersion = await updater.check() + if (newVersion) { + info("New Version Found: " + newVersion) + dispatch(Updates.setNextVersion(newVersion)) + main.windows.activate("update") + } + } catch (e) { + info("Error Checking for Update: " + errorToString(e)) + dispatch(Updates.setError(errorToString(e))) + } finally { + dispatch(Updates.setIsChecking(false)) + } + } +) + +export const install = createOperation( + "updates.install", + async ({dispatch, main}) => { + info("Installing Update") + const onProgress = (n: number) => dispatch(Updates.setDownloadProgress(n)) + try { + dispatch(Updates.setIsDownloading(true)) + dispatch(Updates.setDownloadProgress(0)) + await updater.install(onProgress) + main.windows.byName("update").forEach((w) => w.close()) + } catch (e) { + info("Error Installing") + dispatch(Updates.setError(errorToString(e))) + } finally { + dispatch(Updates.setIsDownloading(false)) + } + } +) diff --git a/apps/zui/src/domain/updates/scheduler.ts b/apps/zui/src/domain/updates/scheduler.ts new file mode 100644 index 0000000000..c4271c9d5e --- /dev/null +++ b/apps/zui/src/domain/updates/scheduler.ts @@ -0,0 +1,34 @@ +import {UpdateMode} from "./types" + +export class Scheduler { + static interval = 1000 * 60 * 60 * 24 // 1 day + + start(mode: UpdateMode, check: () => any, args: {delay?: number} = {}) { + switch (mode) { + case "default": + this.delay(check, args.delay) + this.schedule(check) + break + case "startup": + this.delay(check, args.delay) + } + } + + private delayedId: any + private delay(check, ms = 0) { + this.delayedId = setTimeout(check, ms) + } + + private scheduleId: any + private schedule(check: () => any) { + this.scheduleId = setTimeout(() => { + check() + this.schedule(check) + }, Scheduler.interval) + } + + stop() { + clearTimeout(this.delayedId) + clearTimeout(this.scheduleId) + } +} diff --git a/apps/zui/src/domain/updates/types.ts b/apps/zui/src/domain/updates/types.ts new file mode 100644 index 0000000000..e240dd491a --- /dev/null +++ b/apps/zui/src/domain/updates/types.ts @@ -0,0 +1,6 @@ +export type UpdateMode = "manual" | "startup" | "default" + +export interface Updater { + check(): Promise + install(onProgress: (percent: number) => void): Promise +} diff --git a/apps/zui/src/domain/updates/updater.ts b/apps/zui/src/domain/updates/updater.ts new file mode 100644 index 0000000000..0dfed41205 --- /dev/null +++ b/apps/zui/src/domain/updates/updater.ts @@ -0,0 +1,5 @@ +import env from "src/app/core/env" +import {LinuxUpdater} from "./linux-updater" +import {MacWinUpdater} from "./mac-win-updater" + +export const updater = env.isLinux ? new LinuxUpdater() : new MacWinUpdater() diff --git a/apps/zui/src/domain/window/messages.ts b/apps/zui/src/domain/window/messages.ts index 86693a4802..4b27be060c 100644 --- a/apps/zui/src/domain/window/messages.ts +++ b/apps/zui/src/domain/window/messages.ts @@ -1,4 +1,4 @@ -import {showOpenDialog, sync} from "./operations" +import * as ops from "./operations" import * as handlers from "./handlers" export type WindowHandlers = { @@ -11,6 +11,7 @@ export type WindowHandlers = { } export type WindowOperations = { - "window.sync": typeof sync - "window.showOpenDialog": typeof showOpenDialog + "window.sync": typeof ops.sync + "window.showOpenDialog": typeof ops.showOpenDialog + "window.close": typeof ops.close } diff --git a/apps/zui/src/domain/window/operations.ts b/apps/zui/src/domain/window/operations.ts index 8d1a3364fc..0f9a3f6aa6 100644 --- a/apps/zui/src/domain/window/operations.ts +++ b/apps/zui/src/domain/window/operations.ts @@ -13,3 +13,10 @@ export const showOpenDialog = createOperation( "window.showOpenDialog", (ctx, options: OpenDialogOptions = {}) => dialog.showOpenDialog(options) ) + +export const close = createOperation( + "window.close", + ({main}, windowId: string) => { + main.windows.find(windowId)?.close() + } +) diff --git a/apps/zui/src/electron/autoUpdater.test.ts b/apps/zui/src/electron/autoUpdater.test.ts deleted file mode 100644 index 2a2f5d0780..0000000000 --- a/apps/zui/src/electron/autoUpdater.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @jest-envrionment jsdom - */ - -import "src/test/system/real-paths" -import {rest} from "msw" -import {setupServer} from "msw/node" -import {getLatestVersion} from "./autoUpdater" - -// @ts-ignore -global.localStorage = { - getItem: jest.fn(), - setItem: jest.fn(), -} - -const server = setupServer( - rest.get( - "https://update.electronjs.org/brimdata/zui/darwin-x64/0.0.0", - (req, res, ctx) => { - return res(ctx.status(200), ctx.json({name: "0.0.1"})) - } - ) -) - -beforeAll(() => server.listen()) -afterAll(() => server.close()) -afterEach(() => server.resetHandlers()) - -test("get latest version", async () => { - const version = await getLatestVersion("brimdata/zui") - expect(version).toBe("0.0.1") -}) diff --git a/apps/zui/src/electron/autoUpdater.ts b/apps/zui/src/electron/autoUpdater.ts deleted file mode 100644 index 00f952bccc..0000000000 --- a/apps/zui/src/electron/autoUpdater.ts +++ /dev/null @@ -1,97 +0,0 @@ -import env from "src/app/core/env" -import {app, dialog} from "electron" -import log from "electron-log" -import {autoUpdater} from "electron-updater" -import got from "got" -import get from "lodash/get" -import semver from "semver/preload" -import open from "../js/lib/open" -import {MainObject} from "../core/main/main-object" -import links from "src/app/core/links" -import pkg from "../../package.json" - -const getFeedURLForPlatform = (repo, platform) => { - return `https://update.electronjs.org/${repo}/${platform}/${app.getVersion()}` -} - -export const getLatestVersion = async (repo: string): Promise => { - // Check for updates for MacOS and if there are then we assume there is also one for our other supported OSs - const url = getFeedURLForPlatform(repo, "darwin-x64") - const resp = await got(url) - - // the update server responds with a 204 and no body if the current version is the same as the - // latest version, but will otherwise return json naming the latest version published on github - // (even if it is behind the current version) - if (resp.statusCode === 204) return app.getVersion() - - const body = JSON.parse(resp.body) - const latestVersion = get(body, "name", "") - if (!semver.valid(latestVersion)) - log.error(new Error(`Invalid latest version format: ${latestVersion}`)) - - return latestVersion -} - -const autoUpdateLinux = async (main: MainObject) => { - const latestVersion = await getLatestVersion(main.appMeta.repo) - - // up to date - if (semver.gte(app.getVersion(), latestVersion)) return - - const dialogOpts = { - type: "info", - buttons: ["Get Update", "Later"], - title: "Application Update", - message: "A new version of Zui is available.", - detail: `Zui version ${latestVersion} is available for download; you are running v${app.getVersion()}.`, - } - - dialog.showMessageBox(dialogOpts).then((returnValue) => { - const navUrl = - pkg.name == "zui-insiders" - ? pkg.repository + "/releases/latest" - : links.ZUI_DOWNLOAD - if (returnValue.response === 0) open(navUrl) - }) -} - -export async function setupAutoUpdater(main: MainObject) { - if (env.isLinux) { - setUpdateRepeater(() => { - autoUpdateLinux(main).catch((err) => log.error(err)) - }) - - return - } - - autoUpdater.on("update-downloaded", (event, releaseNotes, releaseName) => { - const dialogOpts = { - type: "info", - buttons: ["Restart", "Later"], - title: "Application Update", - // releaseNotes are not available for windows, so use name instead - message: env.isWindows ? releaseNotes : releaseName, - detail: - "A new version of Zui has been downloaded. Restart the application to apply the update.", - } - - dialog.showMessageBox(dialogOpts).then((returnValue) => { - if (returnValue.response === 0) autoUpdater.quitAndInstall() - }) - }) - - autoUpdater.on("error", (err) => { - log.error("There was a problem updating the application: " + err) - }) - - setUpdateRepeater(() => { - autoUpdater.checkForUpdates() - }) -} - -const setUpdateRepeater = (updateCb) => { - // check for updates 30s after startup - setTimeout(updateCb, 30 * 1000) - // then check for updates once a day - setInterval(updateCb, 24 * 60 * 60 * 1000) -} diff --git a/apps/zui/src/electron/initializers/auto-update.ts b/apps/zui/src/electron/initializers/auto-update.ts deleted file mode 100644 index f46f24b557..0000000000 --- a/apps/zui/src/electron/initializers/auto-update.ts +++ /dev/null @@ -1,13 +0,0 @@ -import log from "electron-log" -import {setupAutoUpdater} from "../autoUpdater" -import {MainObject} from "../../core/main/main-object" -import isDev from "../isDev" - -export function initialize(main: MainObject) { - // autoUpdater should not run in dev, and will fail if the code has not been signed - if (!isDev && main.args.autoUpdater) { - setupAutoUpdater(main).catch((err) => { - log.error("Failed to initiate autoUpdater: " + err) - }) - } -} diff --git a/apps/zui/src/electron/meta.ts b/apps/zui/src/electron/meta.ts index 843c6a3709..8636ea9d31 100644 --- a/apps/zui/src/electron/meta.ts +++ b/apps/zui/src/electron/meta.ts @@ -9,6 +9,7 @@ export async function getAppMeta() { version: app.getVersion(), isFirstRun: await isFirstRun(), userName: os.userInfo().username, + name: pkg.name, } } diff --git a/apps/zui/src/electron/ops/get-global-state-op.ts b/apps/zui/src/electron/ops/get-global-state-op.ts index 14381a9f3f..a30f74b126 100644 --- a/apps/zui/src/electron/ops/get-global-state-op.ts +++ b/apps/zui/src/electron/ops/get-global-state-op.ts @@ -1,6 +1,16 @@ import {getPersistedGlobalState} from "src/js/state/stores/get-persistable" import {createOperation} from "../../core/operations" +import {pick} from "lodash" + +const GLOBAL_STATE_KEYS = ["updates"] export const getGlobalStateOp = createOperation("getGlobalState", ({main}) => { - return getPersistedGlobalState(main.store.getState()) + const state = main.store.getState() + // Any global state that we persist should be + // sent to new windows being created. + // But also data that is relevant, but not persisted. + return { + ...getPersistedGlobalState(state), + ...pick(state, GLOBAL_STATE_KEYS), + } }) diff --git a/apps/zui/src/electron/run-main/boot.ts b/apps/zui/src/electron/run-main/boot.ts index 46ce61d3b2..ea33489c44 100644 --- a/apps/zui/src/electron/run-main/boot.ts +++ b/apps/zui/src/electron/run-main/boot.ts @@ -7,20 +7,24 @@ import {runPlugins} from "./run-plugins" import {runMainBindings} from "./run-main-bindings" import {runProtocolHandlers} from "./run-protocol-handlers" import {MainObject} from "src/core/main/main-object" +import {runConfigurations} from "./run-configurations" export async function boot(args: Partial = {}) { const main = await MainObject.boot({...mainDefaults(), ...args}) // 1. Provide the plugin apis with the things they need from main runMainBindings(main) - // 2. Call listen on all operations - await runOperations(main) - // 3. Run all the initialize functions in the initializers folder + // 2. Call listen on all operations (needed for global dispatch) + runOperations(main) + // 3. Set the default values for core configurations + runConfigurations() + // 4. Run all the initialize functions in the initializers folder await runInitializers(main) - // 4. Activate all plugins in the plugins folder + // 5. Activate all plugins in the plugins folder await runPlugins() - // 5. Protocol Handler - await runProtocolHandlers() - // 6. Start the app + // 6. Protocol Handler + runProtocolHandlers() + // 7. Start the app app.whenReady().then(() => main.start()) + return main } diff --git a/apps/zui/src/electron/initializers/preferences.ts b/apps/zui/src/electron/run-main/run-configurations.ts similarity index 77% rename from apps/zui/src/electron/initializers/preferences.ts rename to apps/zui/src/electron/run-main/run-configurations.ts index f8f2f20949..55ca9f6aed 100644 --- a/apps/zui/src/electron/initializers/preferences.ts +++ b/apps/zui/src/electron/run-main/run-configurations.ts @@ -1,7 +1,24 @@ import time from "src/js/models/time" import {configurations} from "src/zui" -export function initialize() { +export function runConfigurations() { + configurations.create({ + name: "application", + title: "Application", + properties: { + updateMode: { + name: "updateMode", + label: "Check for updates...", + type: "string", + enum: [ + ["On Startup & Daily", "default"], + ["On Startup", "startup"], + ["Manually", "manual"], + ], + defaultValue: "startup", + }, + }, + }) configurations.create({ name: "pools", title: "Pools", diff --git a/apps/zui/src/electron/run-main/run-initializers.ts b/apps/zui/src/electron/run-main/run-initializers.ts index 43d2b4593a..aada07bb85 100644 --- a/apps/zui/src/electron/run-main/run-initializers.ts +++ b/apps/zui/src/electron/run-main/run-initializers.ts @@ -1,6 +1,6 @@ import log from "electron-log" import {MainObject} from "../../core/main/main-object" -import * as initializers from "../initializers" +import * as initializers from "../../initializers" export async function runInitializers(main: MainObject) { for (const name in initializers) { diff --git a/apps/zui/src/electron/run-main/run-operations.ts b/apps/zui/src/electron/run-main/run-operations.ts index da4c786030..a3fe3c18fb 100644 --- a/apps/zui/src/electron/run-main/run-operations.ts +++ b/apps/zui/src/electron/run-main/run-operations.ts @@ -1,9 +1,10 @@ import {MainObject} from "src/core/main/main-object" +import {select} from "src/core/main/select" import {setOperationContext} from "src/core/operations" // Importing these will set up the listeners import "src/domain/operations" import "src/electron/ops" export function runOperations(main: MainObject) { - setOperationContext({main}) + setOperationContext({main, dispatch: main.store.dispatch, select}) } diff --git a/apps/zui/src/electron/windows/create.ts b/apps/zui/src/electron/windows/create.ts index d5d371606f..0fd188811a 100644 --- a/apps/zui/src/electron/windows/create.ts +++ b/apps/zui/src/electron/windows/create.ts @@ -5,6 +5,7 @@ import {AboutWindow} from "./about-window" import {SerializedWindow, WindowProps} from "./types" import {WindowName} from "./types" import {HiddenWindow} from "./hidden-window" +import {UpdateWindow} from "./update-window" export function deserializeWindow(data: SerializedWindow) { const props = { @@ -25,6 +26,8 @@ export function createWindow(name: WindowName, props: WindowProps) { return new DetailWindow(props).init() case "hidden": return new HiddenWindow(props).init() + case "update": + return new UpdateWindow(props).init() default: throw new Error("Unknown Window Type: ", name) } diff --git a/apps/zui/src/electron/windows/search/app-menu.ts b/apps/zui/src/electron/windows/search/app-menu.ts index d39a12111e..d03b793384 100644 --- a/apps/zui/src/electron/windows/search/app-menu.ts +++ b/apps/zui/src/electron/windows/search/app-menu.ts @@ -12,6 +12,7 @@ import {showPreferencesOp} from "../../ops/show-preferences-op" import {showReleaseNotesOp} from "../../ops/show-release-notes-op" import {SearchWindow} from "./search-window" import {sendToFocusedWindow} from "src/core/ipc" +import {open as openUpdateWindow} from "src/domain/updates/operations" export const defaultAppMenuState = () => ({ showRightPane: true, @@ -56,9 +57,9 @@ export function compileTemplate( click: () => window.send("closeTab"), } - const preferences: MenuItemConstructorOptions = { + const settings: MenuItemConstructorOptions = { id: "preferences", - label: env.isMac ? "Preferences..." : "Settings", + label: "Settings...", click: () => showPreferencesOp(), } @@ -72,18 +73,25 @@ export function compileTemplate( click: () => window.send("showExportResults"), } + const checkForUpdates: MenuItemConstructorOptions = { + id: "check-for-updates", + label: "Check For Updates...", + click: () => openUpdateWindow(), + } + const openFile: MenuItemConstructorOptions = { label: "Open Data File...", click: () => sendToFocusedWindow("loads.chooseFiles"), accelerator: "CmdOrCtrl+O", } - const brimMenu: MenuItemConstructorOptions = { + const appNameMenu: MenuItemConstructorOptions = { label: app.getName(), submenu: [ aboutApp, + checkForUpdates, __, - preferences, + settings, {role: "services", submenu: []}, __, {role: "hide"}, @@ -104,7 +112,7 @@ export function compileTemplate( openFile, exportResults, __, - preferences, + settings, __, closeTab, closeWindow, @@ -229,27 +237,29 @@ export function compileTemplate( function helpSubmenu() { const submenu: MenuItemConstructorOptions[] = [ { - label: "Release Notes", - click() { - showReleaseNotesOp() - }, + label: "Welcome", + click: () => sendToFocusedWindow("window.showWelcomePage"), }, { - label: "Zui Docs", + label: "Zui Documentation", click() { shell.openExternal(links.ZUI_DOCS_ROOT) }, }, { - label: "Zed Syntax Docs", + label: "Zed Language Documentation", click() { shell.openExternal(links.ZED_DOCS_LANGUAGE) }, }, + { - label: "Show Welcome Page", - click: () => sendToFocusedWindow("window.showWelcomePage"), + label: "Show Release Notes", + click() { + showReleaseNotesOp() + }, }, + __, { label: "Slack Support Channel", click() { @@ -263,7 +273,7 @@ export function compileTemplate( }, }, { - label: "Submit Issue...", + label: "Report Issue", click() { shell.openExternal( "https://zui.brimdata.io/docs/support/Troubleshooting#opening-an-issue" @@ -273,7 +283,7 @@ export function compileTemplate( ] if (!mac) { - submenu.push(__, aboutApp) + submenu.push(__, checkForUpdates, __, aboutApp) } return submenu } @@ -286,7 +296,7 @@ export function compileTemplate( {role: "window", submenu: windowSubmenu()}, {role: "help", submenu: helpSubmenu()}, ] - if (mac) template.unshift(brimMenu) + if (mac) template.unshift(appNameMenu) return template } diff --git a/apps/zui/src/electron/windows/types.ts b/apps/zui/src/electron/windows/types.ts index 48290fcf09..51ca350b69 100644 --- a/apps/zui/src/electron/windows/types.ts +++ b/apps/zui/src/electron/windows/types.ts @@ -16,7 +16,7 @@ export type SerializedWindow = { state: any } -export type WindowName = "search" | "about" | "detail" | "hidden" +export type WindowName = "search" | "about" | "detail" | "hidden" | "update" export type WindowsState = { [key: string]: ZuiWindow diff --git a/apps/zui/src/electron/windows/update-window.ts b/apps/zui/src/electron/windows/update-window.ts new file mode 100644 index 0000000000..16843ee532 --- /dev/null +++ b/apps/zui/src/electron/windows/update-window.ts @@ -0,0 +1,24 @@ +import {BrowserWindowConstructorOptions} from "electron" +import {WindowName} from "./types" +import {ZuiWindow} from "./zui-window" + +export class UpdateWindow extends ZuiWindow { + name: WindowName = "update" + path = "/update" + options: BrowserWindowConstructorOptions = { + titleBarStyle: "hidden", + frame: false, + vibrancy: "window", + width: 260, + height: 335, + resizable: false, + minimizable: false, + maximizable: false, + closable: false, + } + + beforeLoad() { + this.ref.setMenu(null) + this.ref.center() + } +} diff --git a/apps/zui/src/electron/windows/window-manager.ts b/apps/zui/src/electron/windows/window-manager.ts index 0ac4609c4f..a61879ba28 100644 --- a/apps/zui/src/electron/windows/window-manager.ts +++ b/apps/zui/src/electron/windows/window-manager.ts @@ -47,6 +47,12 @@ export class WindowManager extends EventEmitter { ) } + activate(name: WindowName) { + const [window] = this.byName(name) + if (window) window.ref.focus() + else this.create(name) + } + get focused() { return this.all.find( (f) => f.ref.webContents === BrowserWindow.getFocusedWindow()?.webContents diff --git a/apps/zui/src/initializers/auto-update.ts b/apps/zui/src/initializers/auto-update.ts new file mode 100644 index 0000000000..8cbd2c0d20 --- /dev/null +++ b/apps/zui/src/initializers/auto-update.ts @@ -0,0 +1,28 @@ +import env from "src/app/core/env" +import {MainObject} from "../core/main/main-object" +import ConfigPropValues from "src/js/state/ConfigPropValues" +import {Scheduler} from "src/domain/updates/scheduler" +import {check} from "src/domain/updates/operations" +import {info} from "src/core/log" +import {app} from "electron" +import {onStateChange} from "src/core/on-state-change" + +export function initialize(main: MainObject) { + if (env.isTest) { + info("Not Checking for Updates when env.isTest") + return + } + + app.whenReady().then(() => { + const schedule = new Scheduler() + onStateChange( + main.store, + ConfigPropValues.get("application", "updateMode"), + (mode) => { + info("Running Updater in Mode:", mode) + schedule.stop() + schedule.start(mode, check, {delay: 15_000}) + } + ) + }) +} diff --git a/apps/zui/src/electron/initializers/commands.ts b/apps/zui/src/initializers/commands.ts similarity index 100% rename from apps/zui/src/electron/initializers/commands.ts rename to apps/zui/src/initializers/commands.ts diff --git a/apps/zui/src/electron/initializers/custom-protocol.ts b/apps/zui/src/initializers/custom-protocol.ts similarity index 87% rename from apps/zui/src/electron/initializers/custom-protocol.ts rename to apps/zui/src/initializers/custom-protocol.ts index 5a772db1ef..dfc3577553 100644 --- a/apps/zui/src/electron/initializers/custom-protocol.ts +++ b/apps/zui/src/initializers/custom-protocol.ts @@ -1,5 +1,5 @@ import {app} from "electron" -import {MainObject} from "../../core/main/main-object" +import {MainObject} from "../core/main/main-object" export function initialize(main: MainObject) { const brimCustomProtocol = "zui" diff --git a/apps/zui/src/electron/initializers/default-lake.ts b/apps/zui/src/initializers/default-lake.ts similarity index 87% rename from apps/zui/src/electron/initializers/default-lake.ts rename to apps/zui/src/initializers/default-lake.ts index aabbf4e0de..9d20053eb7 100644 --- a/apps/zui/src/electron/initializers/default-lake.ts +++ b/apps/zui/src/initializers/default-lake.ts @@ -1,4 +1,4 @@ -import {MainObject} from "../../core/main/main-object" +import {MainObject} from "../core/main/main-object" import Lakes from "src/js/state/Lakes" /** diff --git a/apps/zui/src/electron/initializers/index.ts b/apps/zui/src/initializers/index.ts similarity index 90% rename from apps/zui/src/electron/initializers/index.ts rename to apps/zui/src/initializers/index.ts index c678264710..93dfda7a00 100644 --- a/apps/zui/src/electron/initializers/index.ts +++ b/apps/zui/src/initializers/index.ts @@ -3,7 +3,6 @@ export * as commands from "./commands" export * as customProtocol from "./custom-protocol" export * as logFilters from "./log-filters" export * as menus from "./menus" -export * as preferences from "./preferences" export * as secureWebContents from "./secure-web-contents" export * as shortcuts from "./shortcuts" export * as userTasks from "./user-tasks" diff --git a/apps/zui/src/electron/initializers/log-filters.ts b/apps/zui/src/initializers/log-filters.ts similarity index 100% rename from apps/zui/src/electron/initializers/log-filters.ts rename to apps/zui/src/initializers/log-filters.ts diff --git a/apps/zui/src/electron/initializers/menus.ts b/apps/zui/src/initializers/menus.ts similarity index 100% rename from apps/zui/src/electron/initializers/menus.ts rename to apps/zui/src/initializers/menus.ts diff --git a/apps/zui/src/electron/initializers/secure-web-contents.ts b/apps/zui/src/initializers/secure-web-contents.ts similarity index 100% rename from apps/zui/src/electron/initializers/secure-web-contents.ts rename to apps/zui/src/initializers/secure-web-contents.ts diff --git a/apps/zui/src/electron/initializers/shortcuts.ts b/apps/zui/src/initializers/shortcuts.ts similarity index 100% rename from apps/zui/src/electron/initializers/shortcuts.ts rename to apps/zui/src/initializers/shortcuts.ts diff --git a/apps/zui/src/electron/initializers/user-tasks.ts b/apps/zui/src/initializers/user-tasks.ts similarity index 95% rename from apps/zui/src/electron/initializers/user-tasks.ts rename to apps/zui/src/initializers/user-tasks.ts index 7240fbc1d9..e791be5d55 100644 --- a/apps/zui/src/electron/initializers/user-tasks.ts +++ b/apps/zui/src/initializers/user-tasks.ts @@ -1,4 +1,4 @@ -import electronIsDev from "../isDev" +import electronIsDev from "../electron/isDev" import path from "path" import {app} from "electron" diff --git a/apps/zui/src/electron/initializers/window-events.ts b/apps/zui/src/initializers/window-events.ts similarity index 75% rename from apps/zui/src/electron/initializers/window-events.ts rename to apps/zui/src/initializers/window-events.ts index 397ae432db..3133131d36 100644 --- a/apps/zui/src/electron/initializers/window-events.ts +++ b/apps/zui/src/initializers/window-events.ts @@ -1,8 +1,9 @@ import {app, autoUpdater} from "electron" import log from "electron-log" import env from "src/app/core/env" -import {MainObject} from "../../core/main/main-object" -import {moveToCurrentDisplayOp} from "../ops/move-to-current-display-op" +import {MainObject} from "../core/main/main-object" +import {moveToCurrentDisplayOp} from "../electron/ops/move-to-current-display-op" +import {debug} from "src/core/log" export function initialize(main: MainObject) { app.on("second-instance", (e, argv) => { @@ -34,6 +35,7 @@ export function initialize(main: MainObject) { }) main.windows.on("window-will-close", (e) => { + debug("window-will-close", "isQuitting:", main.isQuitting) if (!main.isQuitting && main.windows.visible.length === 1) { e.preventDefault() if (env.isMac) { @@ -45,13 +47,20 @@ export function initialize(main: MainObject) { }) // Looks like this gets called twice on linux and windows - app.on("before-quit", () => main.onBeforeQuit()) + app.on("before-quit", () => { + debug("before-quit") + main.onBeforeQuit() + }) // https://www.electronjs.org/docs/latest/api/auto-updater#event-before-quit-for-update // When autoUpdater.quitAndInstall() is called, the "before-quit" event doesn't fire - autoUpdater.on("before-quit-for-update", () => main.onBeforeQuit()) + autoUpdater.on("before-quit-for-update", () => { + debug("before-quit-for-update") + main.onBeforeQuit() + }) app.on("will-quit", () => { + debug("will-quit") main.stop() }) } diff --git a/apps/zui/src/js/components/PoolIcon.tsx b/apps/zui/src/js/components/PoolIcon.tsx deleted file mode 100644 index b34ab9a1fb..0000000000 --- a/apps/zui/src/js/components/PoolIcon.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react" - -import FileBorder from "../icons/FileBorder" - -type Props = { - className?: string -} - -export default function PoolIcon({...rest}: Props) { - return -} diff --git a/apps/zui/src/js/components/ProgressIndicator.tsx b/apps/zui/src/js/components/ProgressIndicator.tsx index 31fb471764..da5d65c73f 100644 --- a/apps/zui/src/js/components/ProgressIndicator.tsx +++ b/apps/zui/src/js/components/ProgressIndicator.tsx @@ -1,7 +1,9 @@ +import classNames from "classnames" import React from "react" type Props = { percent: number + className?: string } export default function ProgressIndicator({percent, ...rest}: Props) { @@ -9,7 +11,7 @@ export default function ProgressIndicator({percent, ...rest}: Props) { width: Math.floor(percent * 100) + "%", } return ( -
+
diff --git a/apps/zui/src/js/icons/FileBorder.tsx b/apps/zui/src/js/icons/FileBorder.tsx deleted file mode 100644 index 2fe46c4c38..0000000000 --- a/apps/zui/src/js/icons/FileBorder.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react" -import {cssVar} from "../lib/cssVar" -import FileBorderSvg from "../../static/icons/file-border" - -const blue = cssVar("--azure") - -export default function FileBorder(props: any) { - return -} diff --git a/apps/zui/src/js/state/Updates/index.ts b/apps/zui/src/js/state/Updates/index.ts new file mode 100644 index 0000000000..ec43a89a0a --- /dev/null +++ b/apps/zui/src/js/state/Updates/index.ts @@ -0,0 +1,8 @@ +import {slice} from "./reducer" +import * as selectors from "./selectors" + +export default { + reducer: slice.reducer, + ...slice.actions, + ...selectors, +} diff --git a/apps/zui/src/js/state/Updates/reducer.ts b/apps/zui/src/js/state/Updates/reducer.ts new file mode 100644 index 0000000000..32eb100f17 --- /dev/null +++ b/apps/zui/src/js/state/Updates/reducer.ts @@ -0,0 +1,32 @@ +import {PayloadAction, createSlice} from "@reduxjs/toolkit" + +export const slice = createSlice({ + name: "$UPDATES", + initialState: { + nextVersion: null as null | string, + isChecking: false, + isDownloading: false, + downloadProgress: null as null | number, + error: null, + }, + reducers: { + reset() { + return slice.initialState() + }, + setNextVersion(s, a: PayloadAction) { + s.nextVersion = a.payload + }, + setIsChecking(s, a: PayloadAction) { + s.isChecking = a.payload + }, + setDownloadProgress(s, a: PayloadAction) { + s.downloadProgress = a.payload + }, + setIsDownloading(s, a: PayloadAction) { + s.isDownloading = a.payload + }, + setError(s, a: PayloadAction) { + s.error = a.payload + }, + }, +}) diff --git a/apps/zui/src/js/state/Updates/selectors.ts b/apps/zui/src/js/state/Updates/selectors.ts new file mode 100644 index 0000000000..070c776d47 --- /dev/null +++ b/apps/zui/src/js/state/Updates/selectors.ts @@ -0,0 +1,8 @@ +import {State} from "../types" + +export const isChecking = (state: State) => state.updates.isChecking +export const getNextVersion = (state: State) => state.updates.nextVersion +export const getDownloadProgress = (state: State) => + state.updates.downloadProgress +export const isDownloading = (state: State) => state.updates.isDownloading +export const getError = (state: State) => state.updates.error diff --git a/apps/zui/src/js/state/Updates/types.ts b/apps/zui/src/js/state/Updates/types.ts new file mode 100644 index 0000000000..2c697e78bd --- /dev/null +++ b/apps/zui/src/js/state/Updates/types.ts @@ -0,0 +1,3 @@ +import {slice} from "./reducer" + +export type UpdatesState = ReturnType diff --git a/apps/zui/src/js/state/stores/root-reducer.ts b/apps/zui/src/js/state/stores/root-reducer.ts index a757089b5c..f1aa4d40d3 100644 --- a/apps/zui/src/js/state/stores/root-reducer.ts +++ b/apps/zui/src/js/state/stores/root-reducer.ts @@ -21,6 +21,7 @@ import SessionHistories from "../SessionHistories" import PoolSettings from "../PoolSettings" import Window from "../Window" import LoadDataForm from "../LoadDataForm" +import Updates from "../Updates" const rootReducer = combineReducers({ appearance: Appearance.reducer, @@ -44,6 +45,7 @@ const rootReducer = combineReducers({ toolbars: Toolbars.reducer, url: Url.reducer, window: Window.reducer, + updates: Updates.reducer, }) // A proof of concept. This would be a much nicer way to go diff --git a/apps/zui/src/js/state/types.ts b/apps/zui/src/js/state/types.ts index ce1da456cb..da41e400a5 100644 --- a/apps/zui/src/js/state/types.ts +++ b/apps/zui/src/js/state/types.ts @@ -20,6 +20,7 @@ import {SessionHistoriesState} from "./SessionHistories/types" import {PoolSettingsState} from "./PoolSettings/types" import {WindowState} from "./Window/types" import {LoadDataFormState} from "./LoadDataForm/types" +import {UpdatesState} from "./Updates/types" export type ThunkExtraArg = { api: ZuiApi @@ -54,4 +55,5 @@ export type State = { tabHistories: TabHistoriesState toolbars: ToolbarsState window: WindowState + updates: UpdatesState } diff --git a/apps/zui/src/static/AppIcon.icns b/apps/zui/src/static/AppIcon.icns deleted file mode 100644 index 915d242357..0000000000 Binary files a/apps/zui/src/static/AppIcon.icns and /dev/null differ diff --git a/apps/zui/src/static/AppIcon.ico b/apps/zui/src/static/AppIcon.ico deleted file mode 100644 index 328cbf1773..0000000000 Binary files a/apps/zui/src/static/AppIcon.ico and /dev/null differ diff --git a/apps/zui/src/static/AppIcon.png b/apps/zui/src/static/AppIcon.png deleted file mode 100644 index d6f7d28f52..0000000000 Binary files a/apps/zui/src/static/AppIcon.png and /dev/null differ diff --git a/apps/zui/src/static/Windows-Install.gif b/apps/zui/src/static/Windows-Install.gif deleted file mode 100644 index 4840d95b3c..0000000000 Binary files a/apps/zui/src/static/Windows-Install.gif and /dev/null differ diff --git a/apps/zui/src/static/fonts/recursive.woff2 b/apps/zui/src/static/fonts/recursive.woff2 deleted file mode 100644 index 353a7d2cb4..0000000000 Binary files a/apps/zui/src/static/fonts/recursive.woff2 and /dev/null differ diff --git a/apps/zui/src/static/icons/file-border.tsx b/apps/zui/src/static/icons/file-border.tsx deleted file mode 100644 index 4b54872a5f..0000000000 --- a/apps/zui/src/static/icons/file-border.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react" - -export default function FileBorder(props: any) { - return ( - - - - - ) -} diff --git a/apps/zui/src/static/icons/subspace-border.svg b/apps/zui/src/static/icons/subspace-border.svg deleted file mode 100644 index 72ebbf49c3..0000000000 --- a/apps/zui/src/static/icons/subspace-border.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/apps/zui/src/static/icons/subspace.svg b/apps/zui/src/static/icons/subspace.svg deleted file mode 100644 index fe50168c66..0000000000 --- a/apps/zui/src/static/icons/subspace.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apps/zui/src/static/pool-wall-background.svg b/apps/zui/src/static/pool-wall-background.svg deleted file mode 100644 index e1110a05a2..0000000000 --- a/apps/zui/src/static/pool-wall-background.svg +++ /dev/null @@ -1,5638 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/zui/src/static/welcome-page-background.svg b/apps/zui/src/static/welcome-page-background.svg deleted file mode 100644 index 456a7266f5..0000000000 --- a/apps/zui/src/static/welcome-page-background.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/zui/src/views/results-pane/errors/missing-pool-error.tsx b/apps/zui/src/views/results-pane/errors/missing-pool-error.tsx index 7adbfa5ee8..ed054b0b7d 100644 --- a/apps/zui/src/views/results-pane/errors/missing-pool-error.tsx +++ b/apps/zui/src/views/results-pane/errors/missing-pool-error.tsx @@ -16,7 +16,6 @@ import {newPool} from "src/domain/pools/handlers" const BG = styled.div` width: 100%; height: 100%; - background-image: url(dist/static/pool-wall-background.svg); background-repeat: no-repeat; background-position-x: 95%; background-position-y: 250px; diff --git a/apps/zui/src/views/update-window/index.module.css b/apps/zui/src/views/update-window/index.module.css new file mode 100644 index 0000000000..4810efc40c --- /dev/null +++ b/apps/zui/src/views/update-window/index.module.css @@ -0,0 +1,71 @@ +.window { + display: flex; + flex-direction: column; + align-items: center; + height: 100%; +} + +.header { + -webkit-app-region: drag; + padding: 3rem 1rem 2.5rem 1rem; + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + user-select: none; +} + +.main { + display: flex; + flex-direction: column; + gap: 0.25rem; + flex: 1; + width: 100%; + overflow: hidden; +} + +.main a { + color: var(--primary-color); +} + +.footer { + display: flex; + width: 100%; + gap: 1rem; + padding: 1rem; +} + +.footer button { + width: 100%; +} + +.subtext { + font-size: 13px; + color: var(--foreground-color-light); + text-align: center; + overflow: auto; + padding: 0 1rem; +} + +.title { + padding: 0 1rem; + text-align: center; + font-weight: bold; +} + +.progress { + margin: 0.25rem 0; + padding: 0 2rem; +} + +@media (prefers-color-scheme: dark) { + .window { + color: white; + } + + .subtext { + color: rgb(255, 255, 255, 0.8); + } +} diff --git a/apps/zui/src/views/update-window/index.tsx b/apps/zui/src/views/update-window/index.tsx new file mode 100644 index 0000000000..479ca3f72a --- /dev/null +++ b/apps/zui/src/views/update-window/index.tsx @@ -0,0 +1,105 @@ +import {useSelector} from "react-redux" +import Updates from "src/js/state/Updates" +import styles from "./index.module.css" +import forms from "src/components/forms.module.css" +import classNames from "classnames" +import ProgressIndicator from "src/js/components/ProgressIndicator" +import {isNumber} from "lodash" +import {invoke} from "src/core/invoke" +import {errorToString} from "src/util/error-to-string" +import Link from "src/js/components/common/Link" + +function useStatus() { + const nextVersion = useSelector(Updates.getNextVersion) + const isChecking = useSelector(Updates.isChecking) + const isDownloading = useSelector(Updates.isDownloading) + const error = useSelector(Updates.getError) + + if (isChecking) return "checking" + if (error) return "error" + if (isDownloading) return "downloading" + if (nextVersion) return "available" + if (!nextVersion) return "not-available" +} + +function useTemplate() { + let status = useStatus() + const nextVersion = useSelector(Updates.getNextVersion) + const downloadProgress = useSelector(Updates.getDownloadProgress) + const error = useSelector(Updates.getError) + const closeWindow = () => invoke("window.close", globalThis.windowId) + const install = () => invoke("updates.install") + const check = () => invoke("updates.check") + switch (status) { + case "error": + return { + title: "Error", + text: errorToString(error), + button: ["OK", closeWindow], + submit: ["Try Again", check], + } + case "checking": + return { + title: "Checking for Updates...", + } + case "not-available": + return { + title: "Up to Date!", + text: ( + + View releases + + ), + button: ["OK", closeWindow], + } + case "available": + return { + title: "Update Available", + text: `Version ${nextVersion}`, + button: ["Later", closeWindow], + submit: ["Install", install], + } + case "downloading": + return { + title: "Downloading...", + text: "Zui will restart when download completes.", + progress: downloadProgress, + } + } +} + +export function UpdateWindow() { + const data = useTemplate() + const version = globalThis.appMeta.version + + return ( +
+
+ Zui Logo +

v{version}

+
+
+ {data.title &&

{data.title}

} + {data.text &&

{data.text}

} + {isNumber(data.progress) && ( + + )} +
+ +
+ ) +}