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 })