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
}