merge from local

This commit is contained in:
SrGooglo 2024-04-02 22:04:44 +02:00
parent 86a6effeb1
commit e187a49947
21 changed files with 508 additions and 147 deletions

View File

@ -33,6 +33,7 @@ export default async (pkg, step) => {
//`--depth ${step.depth ?? 1}`, //`--depth ${step.depth ?? 1}`,
//"--filter=blob:none", //"--filter=blob:none",
//"--filter=tree:0", //"--filter=tree:0",
"--progress",
"--recurse-submodules", "--recurse-submodules",
"--remote-submodules", "--remote-submodules",
step.url, step.url,

View File

@ -83,6 +83,7 @@ export default async function apply(pkg_id, changes = {}) {
return pkg return pkg
} catch (error) { } catch (error) {
global._relic_eventBus.emit(`pkg:error`, { global._relic_eventBus.emit(`pkg:error`, {
event: "apply",
id: pkg_id, id: pkg_id,
error error
}) })

View File

@ -19,6 +19,20 @@ export default async function execute(pkg_id, { useRemote = false, force = false
return false return false
} }
if (pkg.last_status !== "installed") {
if (!force) {
BaseLog.info(`Package not installed [${pkg_id}], aborting execution`)
global._relic_eventBus.emit(`pkg:error`, {
id: pkg_id,
event: "execute",
error: new Error("Package not valid or not installed"),
})
return false
}
}
const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest
if (!fs.existsSync(manifestPath)) { if (!fs.existsSync(manifestPath)) {

View File

@ -164,12 +164,13 @@ export default async function install(manifest) {
return pkg return pkg
} catch (error) { } catch (error) {
global._relic_eventBus.emit(`pkg:error`, { global._relic_eventBus.emit(`pkg:error`, {
id: id, id: id ?? manifest.constructor.id,
error event: "install",
error,
}) })
global._relic_eventBus.emit(`pkg:update:state`, { global._relic_eventBus.emit(`pkg:update:state`, {
id: id, id: id ?? manifest.constructor.id,
last_status: "failed", last_status: "failed",
status_text: `Installation failed`, status_text: `Installation failed`,
}) })

View File

@ -0,0 +1,76 @@
import fs from "node:fs"
import path from "node:path"
import Logger from "../logger"
import DB from "../db"
import PackageInstall from "./install"
import PackageUpdate from "./update"
import PackageUninstall from "./uninstall"
import Vars from "../vars"
export default async function lastOperationRetry(pkg_id) {
try {
const Log = Logger.child({ service: `OPERATION_RETRY|${pkg_id}` })
const pkg = await DB.getPackages(pkg_id)
if (!pkg) {
Log.error(`This package doesn't exist`)
return null
}
Log.info(`Try performing last operation retry...`)
global._relic_eventBus.emit(`pkg:update:state`, {
id: pkg.id,
status_text: `Performing last operation retry...`,
})
switch (pkg.last_status) {
case "installing":
await PackageInstall(pkg.local_manifest)
break
case "updating":
await PackageUpdate(pkg_id)
break
case "uninstalling":
await PackageUninstall(pkg_id)
break
case "failed": {
// copy pkg.local_manifest to cache after uninstall
const cachedManifest = path.join(Vars.cache_path, path.basename(pkg.local_manifest))
await fs.promises.copyFile(pkg.local_manifest, cachedManifest)
await PackageUninstall(pkg_id)
await PackageInstall(cachedManifest)
break
}
default: {
Log.error(`Invalid last status: ${pkg.last_status}`)
global._relic_eventBus.emit(`pkg:error`, {
id: pkg.id,
event: "retrying last operation",
status_text: `Performing last operation retry...`,
})
return null
}
}
return pkg
} catch (error) {
Logger.error(`Failed to perform last operation retry of [${pkg_id}]`)
Logger.error(error)
global._relic_eventBus.emit(`pkg:error`, {
event: "retrying last operation",
id: pkg_id,
error: error,
})
return null
}
}

View File

@ -20,21 +20,33 @@ export default async function uninstall(pkg_id) {
const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` })
Log.info(`Uninstalling package...`) Log.info(`Uninstalling package...`)
global._relic_eventBus.emit(`pkg:update:state`, { global._relic_eventBus.emit(`pkg:update:state`, {
id: pkg.id, id: pkg.id,
status_text: `Uninstalling package...`, status_text: `Uninstalling package...`,
}) })
const ManifestRead = await ManifestReader(pkg.local_manifest) try {
const manifest = await ManifestVM(ManifestRead.code) const ManifestRead = await ManifestReader(pkg.local_manifest)
const manifest = await ManifestVM(ManifestRead.code)
if (typeof manifest.uninstall === "function") { if (typeof manifest.uninstall === "function") {
Log.info(`Performing uninstall hook...`) Log.info(`Performing uninstall hook...`)
global._relic_eventBus.emit(`pkg:update:state`, {
global._relic_eventBus.emit(`pkg:update:state`, {
id: pkg.id,
status_text: `Performing uninstall hook...`,
})
await manifest.uninstall(pkg)
}
} catch (error) {
Log.error(`Failed to perform uninstall hook`, error)
global._relic_eventBus.emit(`pkg:error`, {
event: "uninstall",
id: pkg.id, id: pkg.id,
status_text: `Performing uninstall hook...`, error
}) })
await manifest.uninstall(pkg)
} }
Log.info(`Deleting package directory...`) Log.info(`Deleting package directory...`)
@ -62,6 +74,7 @@ export default async function uninstall(pkg_id) {
return pkg return pkg
} catch (error) { } catch (error) {
global._relic_eventBus.emit(`pkg:error`, { global._relic_eventBus.emit(`pkg:error`, {
event: "uninstall",
id: pkg_id, id: pkg_id,
error error
}) })

View File

@ -116,10 +116,21 @@ export default async function update(pkg_id) {
return pkg return pkg
} catch (error) { } catch (error) {
global._relic_eventBus.emit(`pkg:error`, { global._relic_eventBus.emit(`pkg:error`, {
event: "update",
id: pkg_id, id: pkg_id,
error error,
last_status: "failed"
}) })
try {
await DB.updatePackageById(pkg_id, {
last_status: "failed",
})
} catch (error) {
BaseLog.error(`Failed to update status of pkg [${pkg_id}]`)
BaseLog.error(error.stack)
}
BaseLog.error(`Failed to update package [${pkg_id}]`, error) BaseLog.error(`Failed to update package [${pkg_id}]`, error)
BaseLog.error(error.stack) BaseLog.error(error.stack)

View File

@ -18,6 +18,7 @@ import PackageList from "./handlers/list"
import PackageRead from "./handlers/read" import PackageRead from "./handlers/read"
import PackageAuthorize from "./handlers/authorize" import PackageAuthorize from "./handlers/authorize"
import PackageCheckUpdate from "./handlers/checkUpdate" import PackageCheckUpdate from "./handlers/checkUpdate"
import PackageLastOperationRetry from "./handlers/lastOperationRetry"
export default class RelicCore { export default class RelicCore {
constructor(params) { constructor(params) {
@ -55,7 +56,8 @@ export default class RelicCore {
list: PackageList, list: PackageList,
read: PackageRead, read: PackageRead,
authorize: PackageAuthorize, authorize: PackageAuthorize,
checkUpdate: PackageCheckUpdate checkUpdate: PackageCheckUpdate,
lastOperationRetry: PackageLastOperationRetry,
} }
openPath(pkg_id) { openPath(pkg_id) {

View File

@ -1,4 +1,5 @@
import winston from "winston" import winston from "winston"
import WinstonTransport from "winston-transport"
import colors from "cli-color" import colors from "cli-color"
const servicesToColor = { const servicesToColor = {
@ -6,10 +7,6 @@ const servicesToColor = {
color: "whiteBright", color: "whiteBright",
background: "bgBlackBright", background: "bgBlackBright",
}, },
"INSTALL": {
color: "whiteBright",
background: "bgBlueBright",
},
} }
const paintText = (level, service, ...args) => { const paintText = (level, service, ...args) => {
@ -27,6 +24,13 @@ const format = winston.format.printf(({ timestamp, service = "CORE", level, mess
return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}` return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}`
}) })
class EventBusTransport extends WinstonTransport {
log(info, next) {
global._relic_eventBus.emit(`logger:new`, info)
next()
}
}
export default winston.createLogger({ export default winston.createLogger({
format: winston.format.combine( format: winston.format.combine(
winston.format.timestamp(), winston.format.timestamp(),
@ -34,6 +38,7 @@ export default winston.createLogger({
), ),
transports: [ transports: [
new winston.transports.Console(), new winston.transports.Console(),
new EventBusTransport(),
//new winston.transports.File({ filename: "error.log", level: "error" }), //new winston.transports.File({ filename: "error.log", level: "error" }),
//new winston.transports.File({ filename: "combined.log" }), //new winston.transports.File({ filename: "combined.log" }),
], ],

View File

@ -39,6 +39,15 @@ export async function readManifest(manifest) {
throw new Error(`Manifest is not a file: ${target}`) throw new Error(`Manifest is not a file: ${target}`)
} }
// copy to cache
const cachedManifest = path.join(Vars.cache_path, path.basename(target))
await fs.promises.copyFile(target, cachedManifest)
if (!fs.existsSync(cachedManifest)) {
throw new Error(`Manifest copy failed: ${target}`)
}
return { return {
remote_manifest: undefined, remote_manifest: undefined,
local_manifest: target, local_manifest: target,

View File

@ -1,14 +1,76 @@
import sendToRender from "../utils/sendToRender" import sendToRender from "../utils/sendToRender"
import { ipcMain } from "electron"
export default class CoreAdapter { export default class CoreAdapter {
constructor(electronApp, RelicCore) { constructor(electronApp, RelicCore) {
this.app = electronApp this.app = electronApp
this.core = RelicCore this.core = RelicCore
this.initialized = false
this.initialize()
} }
events = { loggerWindow = null
ipcEvents = {
"pkg:list": async () => {
return await this.core.package.list()
},
"pkg:get": async (event, pkg_id) => {
return await this.core.db.getPackages(pkg_id)
},
"pkg:read": async (event, manifest_path, options = {}) => {
const manifest = await this.core.package.read(manifest_path, options)
return JSON.stringify({
...this.core.db.defaultPackageState({ ...manifest }),
...manifest,
name: manifest.pkg_name,
})
},
"pkg:install": async (event, manifest_path) => {
return await this.core.package.install(manifest_path)
},
"pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => {
await this.core.package.update(pkg_id)
if (execOnFinish) {
await this.core.package.execute(pkg_id)
}
return true
},
"pkg:apply": async (event, pkg_id, changes) => {
return await this.core.package.apply(pkg_id, changes)
},
"pkg:uninstall": async (event, pkg_id) => {
return await this.core.package.uninstall(pkg_id)
},
"pkg:execute": async (event, pkg_id, { force = false } = {}) => {
// check for updates first
if (!force) {
const update = await this.core.package.checkUpdate(pkg_id)
if (update) {
return sendToRender("pkg:update_available", update)
}
}
return await this.core.package.execute(pkg_id)
},
"pkg:open": async (event, pkg_id) => {
return await this.core.openPath(pkg_id)
},
"pkg:last_operation_retry": async (event, pkg_id) => {
return await this.core.package.lastOperationRetry(pkg_id)
},
"pkg:cancel_current_operation": async (event, pkg_id) => {
return await this.core.package.cancelCurrentOperation(pkg_id)
},
"core:open-path": async (event, pkg_id) => {
return await this.core.openPath(pkg_id)
},
}
coreEvents = {
"pkg:new": (pkg) => { "pkg:new": (pkg) => {
sendToRender("pkg:new", pkg) sendToRender("pkg:new", pkg)
}, },
@ -51,22 +113,53 @@ export default class CoreAdapter {
sendToRender(`new:notification`, { sendToRender(`new:notification`, {
type: "error", type: "error",
message: `An error occurred`, message: `An error occurred`,
description: `Something failed to ${data.event} package ${data.pkg_id}`, description: `Something failed to ${data.event} package ${data.id}`,
}) })
sendToRender(`pkg:update:state`, data) sendToRender(`pkg:update:state`, data)
},
"logger:new": (data) => {
if (this.loggerWindow) {
this.loggerWindow.webContents.send("logger:new", data)
}
} }
} }
initialize = () => { attachLogger = (window) => {
for (const [key, handler] of Object.entries(this.events)) { this.loggerWindow = window
window.webContents.send("logger:new", {
timestamp: new Date().getTime(),
message: "Core adapter Logger attached",
})
}
initialize = async () => {
if (this.initialized) {
return
}
for (const [key, handler] of Object.entries(this.coreEvents)) {
global._relic_eventBus.on(key, handler) global._relic_eventBus.on(key, handler)
} }
for (const [key, handler] of Object.entries(this.ipcEvents)) {
ipcMain.handle(key, handler)
}
await this.core.initialize()
await this.core.setup()
this.initialized = true
} }
deinitialize = () => { deinitialize = () => {
for (const [key, handler] of Object.entries(this.events)) { for (const [key, handler] of Object.entries(this.coreEvents)) {
global._relic_eventBus.off(key, handler) global._relic_eventBus.off(key, handler)
} }
for (const [key, handler] of Object.entries(this.ipcEvents)) {
ipcMain.removeHandler(key, handler)
}
} }
} }

View File

@ -26,62 +26,54 @@ const ProtocolRegistry = require("protocol-registry")
const protocolRegistryNamespace = "relic" const protocolRegistryNamespace = "relic"
class LogsViewer {
window = null
async createWindow() {
this.window = new BrowserWindow({
width: 800,
height: 600,
show: false,
resizable: true,
autoHideMenuBar: true,
icon: "../../resources/icon.png",
webPreferences: {
preload: path.join(__dirname, "../preload/index.js"),
sandbox: false,
},
})
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/logs`)
} else {
this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
}
await new Promise((resolve) => this.window.once("ready-to-show", resolve))
this.window.show()
return this.window
}
closeWindow() {
if (this.window) {
this.window.close()
}
}
}
class ElectronApp { class ElectronApp {
constructor() { constructor() {
this.win = null
this.core = new RelicCore() this.core = new RelicCore()
this.adapter = new CoreAdapter(this, this.core) this.adapter = new CoreAdapter(this, this.core)
} }
window = null
logsViewer = new LogsViewer()
handlers = { handlers = {
"pkg:list": async () => {
return await this.core.package.list()
},
"pkg:get": async (event, pkg_id) => {
return await this.core.db.getPackages(pkg_id)
},
"pkg:read": async (event, manifest_path, options = {}) => {
const manifest = await this.core.package.read(manifest_path, options)
return JSON.stringify({
...this.core.db.defaultPackageState({ ...manifest }),
...manifest,
name: manifest.pkg_name,
})
},
"pkg:install": async (event, manifest_path) => {
return await this.core.package.install(manifest_path)
},
"pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => {
await this.core.package.update(pkg_id)
if (execOnFinish) {
await this.core.package.execute(pkg_id)
}
return true
},
"pkg:apply": async (event, pkg_id, changes) => {
return await this.core.package.apply(pkg_id, changes)
},
"pkg:uninstall": async (event, pkg_id) => {
return await this.core.package.uninstall(pkg_id)
},
"pkg:execute": async (event, pkg_id, { force = false } = {}) => {
// check for updates first
if (!force) {
const update = await this.core.package.checkUpdate(pkg_id)
if (update) {
return sendToRender("pkg:update_available", update)
}
}
return await this.core.package.execute(pkg_id)
},
"pkg:open": async (event, pkg_id) => {
return await this.core.openPath(pkg_id)
},
"updater:check": () => { "updater:check": () => {
autoUpdater.checkForUpdates() autoUpdater.checkForUpdates()
}, },
@ -90,22 +82,31 @@ class ElectronApp {
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall()
}, 3000) }, 3000)
}, },
"settings:get": (e, key) => { "settings:get": (event, key) => {
return global.SettingsStore.get(key) return global.SettingsStore.get(key)
}, },
"settings:set": (e, key, value) => { "settings:set": (event, key, value) => {
return global.SettingsStore.set(key, value) return global.SettingsStore.set(key, value)
}, },
"settings:delete": (e, key) => { "settings:delete": (event, key) => {
return global.SettingsStore.delete(key) return global.SettingsStore.delete(key)
}, },
"settings:has": (e, key) => { "settings:has": (event, key) => {
return global.SettingsStore.has(key) return global.SettingsStore.has(key)
}, },
"app:open-logs": async (event) => {
const loggerWindow = await this.logsViewer.createWindow()
this.adapter.attachLogger(loggerWindow)
loggerWindow.webContents.send("logger:new", {
timestamp: new Date().getTime(),
message: "Logger opened, starting watching logs",
})
},
"app:init": async (event, data) => { "app:init": async (event, data) => {
try { try {
await this.core.initialize() await this.adapter.initialize()
await this.core.setup()
return { return {
pkg: pkg, pkg: pkg,
@ -126,19 +127,8 @@ class ElectronApp {
} }
} }
events = {
"open-runtime-path": () => {
return this.core.openPath()
},
"open-dev-logs": () => {
return sendToRender("new:message", {
message: "Not implemented yet",
})
}
}
createWindow() { createWindow() {
this.win = global.win = new BrowserWindow({ this.window = global.mainWindow = new BrowserWindow({
width: 450, width: 450,
height: 670, height: 670,
show: false, show: false,
@ -151,20 +141,20 @@ class ElectronApp {
} }
}) })
this.win.on("ready-to-show", () => { this.window.on("ready-to-show", () => {
this.win.show() this.window.show()
}) })
this.win.webContents.setWindowOpenHandler((details) => { this.window.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url) shell.openExternal(details.url)
return { action: "deny" } return { action: "deny" }
}) })
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.win.loadURL(process.env["ELECTRON_RENDERER_URL"]) this.window.loadURL(process.env["ELECTRON_RENDERER_URL"])
} else { } else {
this.win.loadFile(path.join(__dirname, "../renderer/index.html")) this.window.loadFile(path.join(__dirname, "../renderer/index.html"))
} }
} }
@ -206,12 +196,12 @@ class ElectronApp {
event.preventDefault() event.preventDefault()
// Someone tried to run a second instance, we should focus our window. // Someone tried to run a second instance, we should focus our window.
if (this.win) { if (this.window) {
if (this.win.isMinimized()) { if (this.window.isMinimized()) {
this.win.restore() this.window.restore()
} }
this.win.focus() this.window.focus()
} }
console.log(`Second instance >`, commandLine) console.log(`Second instance >`, commandLine)
@ -235,10 +225,6 @@ class ElectronApp {
ipcMain.handle(key, this.handlers[key]) ipcMain.handle(key, this.handlers[key])
} }
for (const key in this.events) {
ipcMain.on(key, this.events[key])
}
app.on("second-instance", this.handleOnSecondInstance) app.on("second-instance", this.handleOnSecondInstance)
app.on("open-url", (event, url) => { app.on("open-url", (event, url) => {

View File

@ -32,7 +32,7 @@ export default (event, data) => {
return copy return copy
} }
global.win.webContents.send(event, serializeIpc(data)) global.mainWindow.webContents.send(event, serializeIpc(data))
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }

View File

@ -12,14 +12,19 @@ import AppDrawer from "layout/components/Drawer"
import { InternalRouter, PageRender } from "./router.jsx" import { InternalRouter, PageRender } from "./router.jsx"
import CrashError from "components/Crash"
import LogsViewer from "./pages/logs"
// create a global app context // create a global app context
window.app = GlobalApp window.app = GlobalApp
class App extends React.Component { class App extends React.Component {
state = { state = {
initializing: true,
pkg: null, pkg: null,
crash: null,
initializing: true,
appSetup: { appSetup: {
error: false, error: false,
installed: false, installed: false,
@ -98,6 +103,15 @@ class App extends React.Component {
console.log(`React version > ${versions["react"]}`) console.log(`React version > ${versions["react"]}`)
console.log(`DOMRouter version > ${versions["react-router-dom"]}`) console.log(`DOMRouter version > ${versions["react-router-dom"]}`)
//check if path is /logs
if (window.location.pathname === "/logs") {
return await this.setState({
initializing: false,
no_layout: true,
log_viewer_mode: true,
})
}
window.app.style.appendClassname("initializing") window.app.style.appendClassname("initializing")
for (const event in this.ipcEvents) { for (const event in this.ipcEvents) {
@ -133,17 +147,34 @@ class App extends React.Component {
algorithm: antd.theme.darkAlgorithm algorithm: antd.theme.darkAlgorithm
}} }}
> >
<InternalRouter> {
<GlobalStateContext.Provider value={this.state}> this.state.log_viewer_mode && <LogsViewer />
}
<AppDrawer /> {
<AppModalDialog /> !this.state.log_viewer_mode && <>
<InternalRouter>
<GlobalStateContext.Provider value={this.state}>
{
!this.state.crash && <>
<AppDrawer />
<AppModalDialog />
<AppLayout> <AppLayout>
<PageRender /> <PageRender />
</AppLayout> </AppLayout>
</GlobalStateContext.Provider> </>
</InternalRouter> }
{
this.state.crash && <CrashError
crash={this.state.crash}
/>
}
</GlobalStateContext.Provider>
</InternalRouter>
</>
}
</antd.ConfigProvider> </antd.ConfigProvider>
} }
} }

View File

@ -6,6 +6,8 @@
gap: 20px; gap: 20px;
padding: 20px;
h1 { h1 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: bold; font-weight: bold;

View File

@ -14,7 +14,7 @@ const PackageItem = (props) => {
const isLoading = manifest.last_status === "loading" || manifest.last_status === "installing" || manifest.last_status === "updating" const isLoading = manifest.last_status === "loading" || manifest.last_status === "installing" || manifest.last_status === "updating"
const isInstalling = manifest.last_status === "installing" const isInstalling = manifest.last_status === "installing"
const isInstalled = !!manifest.installed_at const isInstalled = !!manifest.installed_at
const isFailed = manifest.last_status === "error" const isFailed = manifest.last_status === "failed"
console.log(manifest, { console.log(manifest, {
isLoading, isLoading,
@ -38,7 +38,7 @@ const PackageItem = (props) => {
} }
const onClickFolder = () => { const onClickFolder = () => {
ipc.exec("pkg:open", manifest.id) ipc.exec("core:open-path", manifest.id)
} }
const onClickDelete = () => { const onClickDelete = () => {
@ -60,7 +60,7 @@ const PackageItem = (props) => {
} }
const onClickRetryInstall = () => { const onClickRetryInstall = () => {
ipc.exec("pkg:retry_install", manifest.id) ipc.exec("pkg:last_operation_retry", manifest.id)
} }
function handleUpdate(event, data) { function handleUpdate(event, data) {
@ -75,7 +75,7 @@ const PackageItem = (props) => {
return manifest.last_status return manifest.last_status
} }
return `v${manifest.version}` ?? "N/A" return `${isFailed ? "failed |" : ""} v${manifest.version}` ?? "N/A"
} }
const MenuProps = { const MenuProps = {
@ -148,7 +148,6 @@ const PackageItem = (props) => {
manifest.icon && <img src={manifest.icon} className="installation_item_icon" /> manifest.icon && <img src={manifest.icon} className="installation_item_icon" />
} }
<div className="installation_item_info"> <div className="installation_item_info">
<h2> <h2>
{ {
@ -164,16 +163,24 @@ const PackageItem = (props) => {
<div className="installation_item_actions"> <div className="installation_item_actions">
{ {
isFailed && <antd.Button isFailed && <>
type="primary" <antd.Button
onClick={onClickRetryInstall} type="primary"
> onClick={onClickRetryInstall}
Retry >
</antd.Button> Retry
</antd.Button>
<antd.Button
icon={<MdDelete />}
type="primary"
onClick={onClickDelete}
/>
</>
} }
{ {
isInstalled && manifest.executable && <antd.Dropdown.Button !isFailed && isInstalled && manifest.executable && <antd.Dropdown.Button
menu={MenuProps} menu={MenuProps}
onClick={onClickPlay} onClick={onClickPlay}
type="primary" type="primary"
@ -186,7 +193,7 @@ const PackageItem = (props) => {
} }
{ {
isInstalled && !manifest.executable && <antd.Dropdown isFailed && isInstalled && !manifest.executable && <antd.Dropdown
menu={MenuProps} menu={MenuProps}
disabled={isLoading} disabled={isLoading}
> >
@ -199,7 +206,7 @@ const PackageItem = (props) => {
} }
{ {
isInstalling && <antd.Button isFailed && isInstalling && <antd.Button
type="primary" type="primary"
onClick={onClickCancelInstall} onClick={onClickCancelInstall}
> >

View File

@ -0,0 +1,67 @@
import React from "react"
import "./index.less"
const Timestamp = ({ timestamp }) => {
if (isNaN(timestamp)) {
return <span className="timestamp">{timestamp}</span>
}
return <span
className="timestamp"
>
{
new Date(timestamp).toLocaleString().split(", ").join("|")
}
</span>
}
const LogEntry = ({ log }) => {
return <div className="log-entry">
<span className="line_indicator">
{">"}
</span>
{log.timestamp && <Timestamp timestamp={log.timestamp} />}
{!log.timestamp && <span className="timestamp">- no timestamp -</span>}
<p>
{log.message ?? "No message"}
</p>
</div>
}
const LogsViewer = () => {
const listRef = React.useRef()
const [timeline, setTimeline] = React.useState([])
const events = {
"logger:new": (event, log) => {
setTimeline((timeline) => [...timeline, log])
listRef.current.scrollTop = listRef.current.scrollHeight
}
}
React.useEffect(() => {
for (const event in events) {
ipc.exclusiveListen(event, events[event])
}
}, [])
return <div
className="app-logs"
ref={listRef}
>
{
timeline.length === 0 && <p>No logs</p>
}
{
timeline.map((log) => <LogEntry key={log.id} log={log} />)
}
</div>
}
export default LogsViewer

View File

@ -0,0 +1,47 @@
.app-logs {
display: flex;
flex-direction: column;
padding: 10px;
font-family: "DM Mono", monospace;
overflow-x: hidden;
overflow-y: scroll;
height: 100vh;
.log-entry {
display: flex;
flex-direction: row;
align-items: flex-start;
gap: 7px;
font-size: 0.8rem;
line-height: 0.8rem;
border-radius: 8px;
padding: 8px;
color: var(--text-color);
span {
color: var(--text-color);
white-space: nowrap;
word-break: break-all;
}
.timestamp {
opacity: 0.9;
font-size: 0.7rem;
}
&:nth-child(odd) {
background-color: var(--background-color-secondary);
}
}
}

View File

@ -307,8 +307,6 @@ const PackageOptionsLoader = (props) => {
}) })
} }
console.log(manifest)
if (!manifest) { if (!manifest) {
return <antd.Skeleton active /> return <antd.Skeleton active />
} }

View File

@ -7,7 +7,6 @@ import loadable from "@loadable/component"
import GlobalStateContext from "contexts/global" import GlobalStateContext from "contexts/global"
import SplashScreen from "components/Splash" import SplashScreen from "components/Splash"
import CrashError from "components/Crash"
const DefaultNotFoundRender = () => { const DefaultNotFoundRender = () => {
return <div>Not found</div> return <div>Not found</div>
@ -131,18 +130,6 @@ export const InternalRouter = (props) => {
} }
export const PageRender = (props) => { export const PageRender = (props) => {
const globalState = React.useContext(GlobalStateContext)
if (globalState.crash) {
return <CrashError
crash={globalState.crash}
/>
}
if (globalState.initializing) {
return <SplashScreen />
}
const routes = React.useMemo(() => { const routes = React.useMemo(() => {
let paths = { let paths = {
...import.meta.glob("/src/pages/**/[a-z[]*.jsx"), ...import.meta.glob("/src/pages/**/[a-z[]*.jsx"),
@ -167,6 +154,12 @@ export const PageRender = (props) => {
return paths return paths
}, []) }, [])
const globalState = React.useContext(GlobalStateContext)
if (globalState.initializing) {
return <SplashScreen />
}
return <Routes> return <Routes>
{ {
routes.map((route, index) => { routes.map((route, index) => {

View File

@ -20,6 +20,7 @@ export default [
render: (props) => { render: (props) => {
return ( return (
<Button <Button
disabled
type={props.value ? "primary" : "default"} type={props.value ? "primary" : "default"}
onClick={() => { onClick={() => {
if (!props.value) { if (!props.value) {
@ -65,7 +66,10 @@ export default [
icon: "MdUpdate", icon: "MdUpdate",
type: "switch", type: "switch",
storaged: true, storaged: true,
defaultValue: false defaultValue: false,
props: {
disabled: true
}
} }
] ]
}, },
@ -83,7 +87,7 @@ export default [
props: { props: {
children: "Open", children: "Open",
onClick: () => { onClick: () => {
ipc.send("open-runtime-path") ipc.exec("core:open-path")
} }
}, },
storaged: false storaged: false
@ -97,7 +101,7 @@ export default [
props: { props: {
children: "Open", children: "Open",
onClick: () => { onClick: () => {
ipc.send("open-dev-logs") ipc.exec("app:open-logs")
} }
}, },
storaged: false storaged: false