admin/plugins/installed: ask to delete plugin's configuration too
Massimo Melina committed
May 17, 2024 at 12:46 UTC
7dedeceff0b24b9f6cb8574a5831671c1fb50827
6 files changed
+35
-18
admin/src/InstalledPlugins.ts
+8
-3
@@ -6,7 +6,7 @@ import { Box, Link } from '@mui/material'
6
import { DataTable, DataTableColumn } from './DataTable'
7
import { Delete, Error as ErrorIcon, FormatPaint as ThemeIcon, PlayCircle, Settings, StopCircle, Upgrade } from '@mui/icons-material'
8
import { HTTP_FAILED_DEPENDENCY, prefix, with_, xlate } from './misc'
9
-import { alertDialog, formDialog, toast } from './dialog'
9
+import { alertDialog, confirmDialog, formDialog, toast } from './dialog'
10
import _ from 'lodash'
11
import { Account } from './AccountsPage'
12
import { BoolField, Field, FieldProps, MultiSelectField, NumberField, SelectField, StringField
@@ -121,9 +121,14 @@ export default function InstalledPlugins({ updates }: { updates?: true }) {
121
icon: Delete,
122
title: "Uninstall",
123
size,
124
- confirm: "Remove?",
124
async onClick() {
126
- await apiCall('uninstall_plugin', { id })
125
+ const res = await confirmDialog(`${id}: delete configuration too?`, {
126
+ trueText: "Yes",
127
+ falseText: "No",
128
+ after: ({ onClick }) => h(Btn, { variant: 'outlined', onClick(){ onClick(undefined) } }, "Abort")
129
+ })
130
+ if (res === undefined) return
131
+ await apiCall('uninstall_plugin', { id, deleteConfig: res })
132
toast("Plugin uninstalled")
133
}
134
}),
admin/src/InternetPage.ts
+6
-6
@@ -211,11 +211,11 @@ export default function InternetPage() {
211
async onClick() {
212
const [domain, ...altNames] = values.acme_domain.split(',')
213
const fresh = domain === cert.data.subject?.CN && Number(new Date(cert.data.validTo)) - Date.now() >= 30 * DAY
214
- if (fresh && !await confirmDialog("Your certificate is still good", { confirmText: "Make a new one anyway" }))
214
+ if (fresh && !await confirmDialog("Your certificate is still good", { trueText: "Make a new one anyway" }))
215
return
216
if (!await confirmDialog("HFS must temporarily serve HTTP on public port 80, and your router must be configured or this operation will fail")) return
217
const res = await apiCall('check_domain', { domain }).catch(e =>
218
- confirmDialog(String(e), { confirmText: "Continue anyway" }) )
218
+ confirmDialog(String(e), { trueText: "Continue anyway" }) )
219
if (res === false) return
220
await apiCall('make_cert', { domain, altNames, email: values.acme_email }, { timeout: 20_000 })
221
.then(async () => {
@@ -231,7 +231,7 @@ export default function InternetPage() {
231
}
232
233
async function notEnabled() {
234
- if (!await confirmDialog("HTTPS is currently disabled.\nFull configuration is available in the Options page.", { confirmText: "Enable it"})) return
234
+ if (!await confirmDialog("HTTPS is currently disabled.\nFull configuration is available in the Options page.", { trueText: "Enable it"})) return
235
const stop = waitDialog()
236
try {
237
await apiCall('set_config', { values: { https_port: 443 } })
@@ -327,7 +327,7 @@ export default function InternetPage() {
327
const hostname = url && new URL(url).hostname
328
const domain = !isIP(hostname) && hostname
329
if (domain && false === await apiCall('check_domain', { domain }).catch(e =>
330
- confirmDialog(String(e), { confirmText: "Continue anyway" }) )) return
330
+ confirmDialog(String(e), { trueText: "Continue anyway" }) )) return
331
}
332
const urlResult = url && await apiCall('self_check', { url }).catch(() =>
333
alertDialog(md(`Sorry, we couldn't verify your configured address ${url} 😰\nstill, we are going to test your IP address 🤞`), 'warning'))
@@ -352,7 +352,7 @@ export default function InternetPage() {
352
return alertDialog(MSG_ISP, 'warning')
353
const msg = "We couldn't reach your server from the Internet. "
354
if (data.upnp && !data!.mapped)
355
- return confirmDialog(msg + "Try port-forwarding on your router", { confirmText: "Fix it" }).then(async go => {
355
+ return confirmDialog(msg + "Try port-forwarding on your router", { trueText: "Fix it" }).then(async go => {
356
if (!go) return
357
try { await mapPort(data!.internalPort!, '', '') }
358
catch { await mapPort(HIGHER_PORT, '') }
@@ -382,7 +382,7 @@ export default function InternetPage() {
382
async function configure() {
383
if (!data) return // shut up ts
384
if (wrongMap)
385
- return await confirmDialog(`There is a port-forwarding but it is pointing to the wrong port (${wrongMap})`, { confirmText: "Fix it" })
385
+ return await confirmDialog(`There is a port-forwarding but it is pointing to the wrong port (${wrongMap})`, { trueText: "Fix it" })
386
&& fixPort()
387
if (!data.upnp)
388
return alertDialog(h(Box, { lineHeight: 1.5 }, md(`We cannot help you configuring your router because UPnP is not available.\nFind more help [on this website](${PORT_FORWARD_URL}).`)), 'info')
admin/src/OnlinePlugins.ts
+1
-1
@@ -105,7 +105,7 @@ export default function OnlinePlugins() {
105
async function installPlugin(id: string, branch?: string): Promise<any> {
106
try {
107
const res = await apiCall('download_plugin', { id, branch, stop: true }, { timeout: false })
108
- if (await confirmDialog(`Plugin ${id} downloaded`, { confirmText: "Start" }))
108
+ if (await confirmDialog(`Plugin ${id} downloaded`, { trueText: "Start" }))
109
await startPlugin(res.id)
110
}
111
catch(e:any) {
admin/src/dialog.ts
+15
-5
@@ -2,7 +2,8 @@
2
3
import { Box, Button, CircularProgress, Dialog as MuiDialog, DialogContent, DialogTitle, Modal
4
} from '@mui/material'
5
-import { createElement as h, Dispatch, Fragment, isValidElement, ReactElement, ReactNode, SetStateAction,
5
+import {
6
+ createElement as h, Dispatch, FC, Fragment, isValidElement, ReactElement, ReactNode, SetStateAction,
7
useEffect, useRef, useState
8
} from 'react'
9
import { Check, Close, Error as ErrorIcon, Forward, Info, Warning } from '@mui/icons-material'
@@ -104,8 +105,15 @@ export function alertDialog(msg: ReactElement | string | Error, options?: AlertT
105
return Object.assign(promise, dialog)
106
}
107
107
-interface ConfirmOptions extends Omit<DialogOptions, 'Content'> { href?: string, confirmText?: string, dontText?: string }
108
-export function confirmDialog(msg: ReactNode, { href, confirmText="Go", dontText="Don't", ...rest }: ConfirmOptions={}) {
108
+interface ConfirmOptions extends Omit<DialogOptions, 'Content'> {
109
+ href?: string,
110
+ trueText?: string,
111
+ falseText?: string,
112
+ before?: FC<{ onClick: (result: any) => unknown }>
113
+ after?: FC<{ onClick: (result: any) => unknown }>
114
+}
115
+
116
+export function confirmDialog(msg: ReactNode, { href, trueText="Go", falseText="Don't", before, after, ...rest }: ConfirmOptions={}) {
117
const promise = pendingPromise<boolean>()
118
const dialog = newDialog({
119
className: 'dialog-confirm',
@@ -119,11 +127,13 @@ export function confirmDialog(msg: ReactNode, { href, confirmText="Go", dontText
127
return h(Fragment, {},
128
h(Box, { mb: 2 }, typeof msg === 'string' ? md(msg) : msg),
129
h(Flex, {},
130
+ before?.({ onClick: (v: any) => dialog.close(v) }),
131
h('a', {
132
href,
133
onClick: () => dialog.close(true),
125
- }, h(Button, { variant: 'contained' }, confirmText)),
126
- h(Button, { onClick: () => dialog.close(false) }, dontText),
134
+ }, h(Button, { variant: 'contained' }, trueText)),
135
+ h(Button, { onClick: () => dialog.close(false) }, falseText),
136
+ after?.({ onClick: (v: any) => dialog.close(v) }),
137
),
138
)
139
}
src/api.plugins.ts
+3
-1
@@ -152,9 +152,11 @@ const apis: ApiHandlers = {
152
return {}
153
},
154
155
- async uninstall_plugin({ id }) {
155
+ async uninstall_plugin({ id, deleteConfig }) {
156
await stopPlugin(id)
157
await rm(PLUGINS_PATH + '/' + id, { recursive: true, force: true })
158
+ if (deleteConfig)
159
+ setPluginConfig(id, null)
160
return {}
161
}
162
src/plugins.ts
+2
-2
@@ -70,11 +70,11 @@ async function waitRunning(id: string, state=true) {
70
}
71
72
// nullish values are equivalent to defaultValues
73
-export function setPluginConfig(id: string, changes: Dict) {
73
+export function setPluginConfig(id: string, changes: Dict | null) {
74
pluginsConfig.set(allConfigs => {
75
const fields = getPluginConfigFields(id)
76
const oldConfig = allConfigs[id]
77
- const newConfig = _.pickBy({ ...oldConfig, ...changes },
77
+ const newConfig = changes && _.pickBy({ ...oldConfig, ...changes },
78
(v, k) => v != null && !same(v, fields?.[k]?.defaultValue))
79
return { ...allConfigs, [id]: _.isEmpty(newConfig) ? undefined : newConfig }
80
})