@samitouri / QOSami-HFS / commits / 02739e4e

fix: admin/plugins: ongoing downloads of check-updates were not displayed if page was reloaded

Massimo Melina committed Sep 30, 2024 at 15:34 UTC 02739e4e1014c29c8d5a7c88c2f8cfee41359eea
4 files changed +37 -26
admin/src/InstalledPlugins.ts
+2 -2
@@ -56,15 +56,15 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
56 actions: ({ row, id }) => updates ? [
57 h(IconBtn, {
58 icon: Upgrade,
59 - title: row.updated ? "Already updated" : "Update",
59 + title: row.downloading ? "Downloading" : row.updated ? "Already updated" : "Update",
60 disabled: row.updated,
61 + progress: row.downloading,
62 size,
63 async onClick() {
64 await apiCall('update_plugin', { id, branch: row.branch }, { timeout: false }).catch(e => {
65 throw e.code !== HTTP_FAILED_DEPENDENCY ? e
66 : Error("Failed dependencies: " + e.cause?.map((x: any) => prefix(`plugin "`, x.id || x.repo, `" `) + x.error).join('; '))
67 })
67 - updateEntry({ id }, { updated: true })
68 toast("Plugin updated")
69 }
70 })
admin/src/mui.ts
+1 -1
@@ -143,7 +143,7 @@ export const Btn = forwardRef(({ icon, title, onClick, disabled, progress, link,
143 const [loadingState, setLoadingState] = useStateMounted(false)
144 if (typeof disabled === 'string')
145 title = disabled
146 - disabled = loadingState || Boolean(progress) || disabled === undefined ? undefined : Boolean(disabled)
146 + disabled = loadingState || progress || disabled ? true : undefined
147 if (link)
148 onClick = () => window.open(link)
149 const showLabel = useBreakpoint(labelFrom || 'xs')
src/api.plugins.ts
+24 -14
@@ -3,14 +3,16 @@
3 import {
4 AvailablePlugin, enablePlugins, getAvailablePlugins, getPluginConfigFields, mapPlugins, Plugin, pluginsConfig,
5 PATH as PLUGINS_PATH, enablePlugin, getPluginInfo, setPluginConfig, isPluginRunning,
6 - stopPlugin, startPlugin, CommonPluginInterface, getMissingDependencies,
6 + stopPlugin, startPlugin, CommonPluginInterface, getMissingDependencies, findPluginByRepo,
7 } from './plugins'
8 import _ from 'lodash'
9 import assert from 'assert'
10 import { HTTP_CONFLICT, newObj, waitFor } from './misc'
11 import { ApiError, ApiHandlers } from './apiMiddleware'
12 import { rm } from 'fs/promises'
13 -import { downloadPlugin, getFolder2repo, readOnlineCompatiblePlugin, readOnlinePlugin, searchPlugins } from './github'
13 +import {
14 + downloadPlugin, getFolder2repo, readOnlineCompatiblePlugin, readOnlinePlugin, searchPlugins, downloading
15 +} from './github'
16 import { HTTP_FAILED_DEPENDENCY, HTTP_NOT_FOUND, HTTP_SERVER_ERROR } from './const'
17 import { SendListReadable } from './SendList'
18
@@ -28,10 +30,18 @@ const apis: ApiHandlers = {
30 })
31 },
32
31 - async get_plugin_updates() {
33 + async get_plugin_updates({}, ctx) {
34 return new SendListReadable({
35 async doAtStart(list) {
36 const errs: string[] = []
37 + list.events(ctx, {
38 + pluginDownload({ repo, status }) {
39 + list.update({ id: findPluginByRepo(repo)?.id }, { downloading: status ?? null })
40 + },
41 + pluginDownloaded({ id }) {
42 + list.update({ id }, { updated: true })
43 + }
44 + })
45 await Promise.allSettled(_.map(getFolder2repo(), async (repo, folder) => {
46 try {
47 if (!repo) return
@@ -39,13 +49,13 @@ const apis: ApiHandlers = {
49 if (!online) return
50 const disk = getPluginInfo(folder)
51 if (!disk) return // plugin removed in the meantime?
42 - if (online.version !== disk.version) { // not just newer one, in case a version was retired
43 - online.id = disk.id // id is installation-dependant, and online cannot know
44 - online.repo = serialize(disk).repo // show the user the current repo we are getting this update from, not a possibly-changed future one
45 - if (online.version! < disk.version)
46 - (online as any).downgrade = true
47 - list.add(online)
48 - }
52 + if (online.version === disk.version) return // different, not just newer ones, in case a version was retired
53 + list.add(Object.assign(online, {
54 + id: disk.id, // id is installation-dependant, and online cannot know
55 + repo: serialize(disk).repo, // show the user the current repo we are getting this update from, not a possibly-changed future one
56 + downgrade: online.version! < disk.version,
57 + downloading: _.isString(online.repo) && downloading[online.repo],
58 + }))
59 } catch (err: any) {
60 if (err.message !== '404') // the plugin is declaring a wrong repo
61 errs.push(err.code || err.message)
@@ -53,7 +63,7 @@ const apis: ApiHandlers = {
63 }))
64 for (const x of _.uniq(errs))
65 list.error(x)
56 - list.close()
66 + list.ready()
67 }
68 })
69 },
@@ -106,9 +116,9 @@ const apis: ApiHandlers = {
116 if (repos.includes(repo))
117 list.update({ id: repo }, { installed: false })
118 },
109 - pluginDownload({ id, status }) {
110 - if (repos.includes(id))
111 - list.update({ id }, { downloading: status ?? null })
119 + pluginDownload({ repo, status }) {
120 + if (repos.includes(repo))
121 + list.update({ id: repo }, { downloading: status ?? null })
122 }
123 })
124 try {
src/github.ts
+10 -9
@@ -20,14 +20,14 @@ import { storedMap } from './persistence'
20 const DIST_ROOT = 'dist'
21
22 type DownloadStatus = true | undefined
23 -const downloading: Record<string, DownloadStatus> = {}
23 +export const downloading: { [repo:string]: DownloadStatus } = {}
24
25 -function downloadProgress(id: string, status: DownloadStatus) {
25 +function downloadProgress(repo: string, status: DownloadStatus) {
26 if (status === undefined)
27 - delete downloading[id]
27 + delete downloading[repo]
28 else
29 - downloading[id] = status
30 - events.emit('pluginDownload', { id, status })
29 + downloading[repo] = status
30 + events.emit('pluginDownload', { repo, status })
31 }
32
33 // determine default branch, possibly without consuming api quota
@@ -47,8 +47,8 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
47 console.log('downloading plugin', repo)
48 downloadProgress(repo, true)
49 try {
50 + const pl = findPluginByRepo(repo)
51 if (repo.includes('//')) { // custom repo
51 - const pl = findPluginByRepo(repo)
52 if (!pl)
53 throw new ApiError(HTTP_BAD_REQUEST, "bad repo")
54 const customRepo = ((pl as any).getData?.() || pl).repo
@@ -63,9 +63,9 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
63 const short = repo.split('/')[1] // second part, repo without the owner
64 if (!short)
65 throw new ApiError(HTTP_BAD_REQUEST, "bad repo")
66 - const folder = overwrite ? _.findKey(getFolder2repo(), x => x===repo)! // use existing folder
67 - : getFolder2repo().hasOwnProperty(short) ? repo.replace('/','-') // longer form only if another plugin is using short form, to avoid overwriting
68 - : short
66 + const folder = overwrite && pl?.id // use existing folder
67 + || (getFolder2repo().hasOwnProperty(short) ? repo.replace('/','-') // longer form only if another plugin is using short form, to avoid overwriting
68 + : short)
69 const GITHUB_ZIP_ROOT = short + '-' + branch // GitHub puts everything within this folder
70 return await go(`https://github.com/${repo}/archive/refs/heads/${branch}.zip`, folder, GITHUB_ZIP_ROOT + '/' + DIST_ROOT)
71
@@ -105,6 +105,7 @@ export async function downloadPlugin(repo: Repo, { branch='', overwrite=false }=
105 .catch(e => { throw e.code !== 'ENOENT' ? e : new ApiError(HTTP_NOT_ACCEPTABLE, "missing main file") })
106 if (wasEnabled)
107 void startPlugin(folder) // don't wait, in case it fails to start
108 + events.emit('pluginDownloaded', { id: folder, repo })
109 return folder
110 }
111 }