admin/plugin: offer update button also in 'search online'
Massimo Melina committed
Aug 7, 2022 at 10:48 UTC
33adec3f149d861f3e37215443282cd755555e47
7 files changed
+57
-23
admin/src/InstalledPlugins.ts
+14
-9
@@ -2,7 +2,7 @@ import { apiCall, useApiList } from './api'
2
import { createElement as h, Fragment } from 'react'
3
import { Alert, Box, Tooltip } from '@mui/material'
4
import { DataGrid } from '@mui/x-data-grid'
5
-import { Delete, Error, GitHub, PlayCircle, Settings, StopCircle, SystemUpdateAlt } from '@mui/icons-material'
5
+import { Delete, Error, GitHub, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
6
import { IconBtn, xlate } from './misc'
7
import { formDialog, toast } from './dialog'
8
import _ from 'lodash'
@@ -55,14 +55,7 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
55
renderCell({ row }) {
56
const { config, id } = row
57
if (updates)
58
- return h(IconBtn, {
59
- icon: SystemUpdateAlt,
60
- title: "Update",
61
- async onClick() {
62
- await apiCall('update_plugin', { id })
63
- setList(list.filter(x => x.id !== id))
64
- }
65
- })
58
+ return h(UpdateButton, { id, then: () => setList(list.filter(x => x.id !== id)) })
59
return h('div', {},
60
h(IconBtn, row.started ? {
61
icon: StopCircle,
@@ -148,3 +141,15 @@ export function showError(error: any) {
141
ENOTFOUND: "Couldn't reach github.com"
142
}))
143
}
144
+
145
+export function UpdateButton({ id, then }: { id: string, then: (id:string)=>void }) {
146
+ return h(IconBtn, {
147
+ icon: Upgrade,
148
+ title: "Update",
149
+ async onClick() {
150
+ await apiCall('update_plugin', { id })
151
+ then?.(id)
152
+ toast("Plugin updated")
153
+ }
154
+ })
155
+}
admin/src/OnlinePlugins.ts
+10
-3
@@ -6,13 +6,14 @@ import { Download, Search } from '@mui/icons-material'
6
import { toast } from './dialog'
7
import { StringField } from '@hfs/mui-grid-form'
8
import { useDebounce } from 'use-debounce'
9
-import { repoLink, showError } from './InstalledPlugins'
9
+import { repoLink, showError, UpdateButton } from './InstalledPlugins'
10
import { state, useSnapState } from './state'
11
+import _ from 'lodash'
12
13
export default function OnlinePlugins() {
14
const [search, setSearch] = useState('')
15
const [debouncedSearch] = useDebounce(search, 1000)
15
- const { list, error, initializing } = useApiList('search_online_plugins', { text: debouncedSearch })
16
+ const { list, error, initializing, updateList } = useApiList('search_online_plugins', { text: debouncedSearch })
17
const snap = useSnapState()
18
if (error)
19
return showError(error)
@@ -70,7 +71,13 @@ export default function OnlinePlugins() {
71
const { id, branch } = row
72
return h('div', {},
73
repoLink(id),
73
- h(IconBtn, {
74
+ row.update ? h(UpdateButton, {
75
+ id,
76
+ then() {
77
+ updateList(list =>
78
+ _.find(list, { id }).update = false )
79
+ }
80
+ }) : h(IconBtn, {
81
icon: Download,
82
title: "Download",
83
progress: row.downloading,
admin/src/api.ts
+11
-4
@@ -6,6 +6,7 @@ import { Alert } from '@mui/material'
6
import _ from 'lodash'
7
import { state } from './state'
8
import { Refresh } from '@mui/icons-material'
9
+import produce, { Draft } from 'immer'
10
11
export function useApiEx<T=any>(...args: Parameters<typeof useApi>) {
12
const [data, error, reload] = useApi<T>(...args)
@@ -168,13 +169,11 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
169
setError("Connection error")
170
return stop()
171
case 'closed':
171
- flush()
172
- setInitializing(false)
172
return stop()
173
case 'msg':
174
if (src?.readyState === src?.CLOSED)
175
return stop()
177
- if (data === 'init') {
176
+ if (data === 'end') {
177
flush()
178
setInitializing(false)
179
return
@@ -227,9 +226,17 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
226
}
227
228
function stop() {
229
+ setInitializing(false)
230
setLoading(false)
231
clearInterval(timer)
232
+ flush()
233
}
234
}, [cmd, JSON.stringify(params)]) //eslint-disable-line
234
- return { list, loading, error, initializing, setList }
235
+ return { list, loading, error, initializing, setList, updateList }
236
+
237
+ function updateList(cb: (toModify: Draft<typeof list>) => void) {
238
+ setList(produce(list, x => {
239
+ cb(x)
240
+ }))
241
+ }
242
}
server/src/api.file_list.ts
+2
-2
@@ -30,7 +30,7 @@ export const file_list: ApiHandler = async ({ path, offset, limit, search, omit,
30
setTimeout(async () => {
31
for await (const entry of produceEntries())
32
list.add(entry)
33
- list.end()
33
+ list.close()
34
})
35
return list
36
@@ -38,7 +38,7 @@ export const file_list: ApiHandler = async ({ path, offset, limit, search, omit,
38
if (!sse)
39
return new ApiError(code)
40
list.error(code)
41
- list.end()
41
+ list.close()
42
return list
43
}
44
server/src/api.plugins.ts
+11
-2
@@ -49,7 +49,7 @@ const apis: ApiHandlers = {
49
catch (err:any) {
50
list.error(err.code || err.message)
51
}
52
- list.end()
52
+ list.close()
53
})
54
return list
55
},
@@ -80,7 +80,12 @@ const apis: ApiHandlers = {
80
const folder2repo = getFolder2repo()
81
for await (const pl of searchPlugins(text)) {
82
const repo = pl.id
83
- Object.assign(pl, { installed: _.includes(folder2repo, repo) })
83
+ const folder = _.findKey(folder2repo, x => x === repo)
84
+ const installed = folder && getPluginInfo(folder)
85
+ Object.assign(pl, {
86
+ installed: _.includes(folder2repo, repo),
87
+ update: installed && installed.version < pl.version!,
88
+ })
89
list.add(pl)
90
// watch for events about this plugin, until this request is closed
91
ctx.req.on('close', onOff(events, {
@@ -92,6 +97,10 @@ const apis: ApiHandlers = {
97
if (repo === getFolder2repo()[folder])
98
list.update({ id: repo }, { installed: false })
99
},
100
+ pluginUpdated: p => {
101
+ if (p.repo === repo)
102
+ list.update({ id: repo }, { update: p.version < pl.version! })
103
+ },
104
['pluginDownload_'+repo](status) {
105
list.update({ id: repo }, { downloading: status ?? null })
106
}
server/src/apiMiddleware.ts
+5
-2
@@ -75,7 +75,7 @@ export class SendListReadable<T> extends Readable {
75
if (addAtStart) {
76
for (const x of addAtStart)
77
this.add(x)
78
- this.push('init')
78
+ this.end()
79
}
80
}
81
add(rec: T) {
@@ -87,9 +87,12 @@ export class SendListReadable<T> extends Readable {
87
update(search: Partial<T>, change: Partial<T>) {
88
this.push({ update:[{ search, change }] })
89
}
90
- end() {
90
+ close() {
91
this.push(null)
92
}
93
+ end() { // useful to indicate the end of an initial phase, but we leave open for updates
94
+ this.push('end')
95
+ }
96
error(msg: NonNullable<typeof this.lastError>) {
97
this.push({ error: msg })
98
this.lastError = msg
server/src/plugins.ts
+4
-1
@@ -215,7 +215,8 @@ export async function rescan() {
215
const module = pathLib.resolve(f)
216
const { unwatch } = watchLoad(f, async () => {
217
try {
218
- console.log(plugins[id] ? "reloading plugin" : "loading plugin", id)
218
+ const reloading = plugins[id]
219
+ console.log(reloading ? "reloading plugin" : "loading plugin", id)
220
const { init, ...data } = await import(module)
221
delete data.default
222
deleteModule(require.resolve(module)) // avoid caching at next import
@@ -249,6 +250,8 @@ export async function rescan() {
250
})
251
Object.assign(data, res)
252
new Plugin(id, data, unwatch)
253
+ if (reloading)
254
+ events.emit('pluginUpdated', getPluginInfo(id))
255
} catch (e) {
256
console.log("plugin error:", e)
257
}