admin/plugins: uninstall button

Massimo Melina committed May 11, 2022 at 20:21 UTC ed4bfdd837178988abcd431beacee9e00de8ce43
3 files changed +38 -13
admin/src/InstalledPlugins.ts
+11 -3
@@ -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 { Error, PlayCircle, Settings, StopCircle } from '@mui/icons-material'
5 +import { Delete, Error, PlayCircle, Settings, StopCircle } from '@mui/icons-material'
6 import { IconBtn } from './misc'
7 import { formDialog, toast } from './dialog'
8 import _ from 'lodash'
@@ -46,7 +46,7 @@ export default function InstalledPlugins() {
46 },
47 {
48 field: "actions",
49 - width: 80,
49 + width: 120,
50 align: 'center',
51 hideSortIcons: true,
52 disableColumnMenu: true,
@@ -69,7 +69,7 @@ export default function InstalledPlugins() {
69 h(IconBtn, {
70 icon: Settings,
71 title: "Configuration",
72 - disabled: !config,
72 + disabled: !config && "No configuration available for this plugin",
73 async onClick() {
74 const pl = await apiCall('get_plugin', { id })
75 const values = await formDialog({
@@ -83,6 +83,14 @@ export default function InstalledPlugins() {
83 toast("Configuration saved")
84 }
85 }),
86 + h(IconBtn, {
87 + icon: Delete,
88 + title: "Uninstall",
89 + async onClick() {
90 + await apiCall('uninstall_plugin', { id })
91 + toast("Plugin uninstalled")
92 + }
93 + }),
94 )
95 }
96 },
server/src/api.plugins.ts
+15 -10
@@ -6,16 +6,16 @@ import {
6 mapPlugins,
7 parsePluginSource,
8 Plugin, pluginsConfig,
9 - PATH as PLUGINS_PATH
9 + PATH as PLUGINS_PATH, isPluginRunning, enablePlugin
10 } from './plugins'
11 import _ from 'lodash'
12 import assert from 'assert'
13 -import { httpsString, httpsStream, objSameKeys, onOff, same } from './misc'
13 +import { httpsString, httpsStream, objSameKeys, onOff, same, wait } from './misc'
14 import { ApiError, ApiHandlers, sendList } from './apiMiddleware'
15 import events from './events'
16 import unzipper from 'unzipper'
17 -import { createWriteStream } from 'fs'
18 -import { access, mkdir } from 'fs/promises'
17 +import { createWriteStream, mkdirSync } from 'fs'
18 +import { rm } from 'fs/promises'
19
20 const DIST_ROOT = 'dist/'
21
@@ -39,11 +39,7 @@ const apis: ApiHandlers = {
39 async set_plugin({ id, enabled, config }) {
40 assert(id, 'id')
41 if (enabled !== undefined)
42 - enablePlugins.set( arr =>
43 - arr.includes(id) === enabled ? arr
44 - : enabled ? [...arr, id]
45 - : arr.filter((x: string) => x !== id)
46 - )
42 + enablePlugin(id, enabled)
43 if (config) {
44 const fields = getPluginConfigFields(id)
45 config = _.pickBy(config, (v, k) =>
@@ -108,10 +104,19 @@ const apis: ApiHandlers = {
104 async download_plugin({ id }) {
105 if (downloading[id])
106 return new ApiError(409, "already downloading")
111 - downloadPlugin(id).then()
107 + await downloadPlugin(id)
108 return {}
109 },
110
111 + async uninstall_plugin({ id }) {
112 + while (isPluginRunning(id)) {
113 + enablePlugin(id, false)
114 + await wait(500)
115 + }
116 + await rm(PLUGINS_PATH + '/' + id, { recursive: true, force: true })
117 + return {}
118 + }
119 +
120 }
121
122 type DownloadStatus = true | undefined
server/src/plugins.ts
+12
@@ -22,6 +22,18 @@ export const DISABLING_POSTFIX = '-disabled'
22
23 const plugins: Record<string, Plugin> = {}
24
25 +export function isPluginRunning(id: string) {
26 + return plugins[id]?.started
27 +}
28 +
29 +export function enablePlugin(id: string, state=true) {
30 + enablePlugins.set( arr =>
31 + arr.includes(id) === state ? arr
32 + : state ? [...arr, id]
33 + : arr.filter((x: string) => x !== id)
34 + )
35 +}
36 +
37 export function mapPlugins<T>(cb:(plugin:Readonly<Plugin>, pluginName:string)=> T) {
38 return _.map(plugins, (pl,plName) => {
39 try { return cb(pl,plName) }