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 }