admin: use toast-like notification for plugin start/stop and config saving

Massimo Melina committed May 1, 2022 at 13:11 UTC a43f17551d02eb231ee0ce6322fa705ebabf8710
3 files changed +46 -13
admin/src/ConfigPage.ts
+2 -2
@@ -10,7 +10,7 @@ import { subscribeKey } from 'valtio/utils'
10 import { Form, BoolField, NumberField, SelectField, FieldProps, Field } from './Form';
11 import StringStringField from './StringStringField'
12 import FileField from './FileField'
13 -import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, waitDialog } from './dialog'
13 +import { alertDialog, closeDialog, confirmDialog, formDialog, newDialog, toast, waitDialog } from './dialog'
14 import { proxyWarning } from './HomePage'
15
16 let loaded: Dict | undefined
@@ -134,7 +134,7 @@ export default function ConfigPage() {
134 setTimeout(reloadStatus, 1000)
135 Object.assign(loaded, values) // since changes are recalculated subscribing state.config, but it depends on 'loaded' to (which cannot be subscribed), be sure to update loaded first
136 recalculateChanges()
137 - await alertDialog("Changes applied", 'success')
137 + toast("Changes applied", 'success')
138 }
139 }
140
admin/src/PluginsPage.ts
+13 -7
@@ -3,8 +3,8 @@ import { apiCall, useApiList } from './api'
3 import { DataGrid } from '@mui/x-data-grid'
4 import { Alert, Box } from '@mui/material'
5 import { IconBtn } from './misc'
6 -import { PowerSettingsNew, Settings } from '@mui/icons-material'
7 -import { alertDialog, formDialog } from './dialog'
6 +import { PlayCircle, Settings, StopCircle } from '@mui/icons-material'
7 +import { toast, formDialog } from './dialog'
8 import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
9 import { ArrayField } from './ArrayField'
10
@@ -44,12 +44,18 @@ export default function PluginsPage() {
44 renderCell({ row }) {
45 const { config, id } = row
46 return h('div', {},
47 - h(IconBtn, {
48 - icon: PowerSettingsNew,
49 - title: (row.started ? "Stop" : "Start") + ' ' + id,
47 + h(IconBtn, row.started ? {
48 + icon: StopCircle,
49 + title: `Stop ${id}`,
50 + onClick: () =>
51 + apiCall('set_plugin', { id, enabled: false }).then(() =>
52 + toast("Plugin is stopping", h(StopCircle, { color: 'warning' })))
53 + } : {
54 + icon: PlayCircle,
55 + title: `Start ${id}`,
56 onClick: () =>
51 - apiCall('set_plugin', { id, enabled: !row.started }).then(() =>
52 - alertDialog(row.started ? "Plugin is stopping" : "Plugin is starting")),
57 + apiCall('set_plugin', { id, enabled: true }).then(() =>
58 + toast("Plugin is starting", h(PlayCircle, { color: 'success' }))),
59 }),
60 h(IconBtn, {
61 icon: Settings,
admin/src/dialog.ts
+31 -4
@@ -28,12 +28,13 @@ dialogsDefaults.Container = function Container(d:DialogOptions) {
28 }, [])
29 const ref = useRef<HTMLElement>()
30 d = { ...dialogsDefaults, ...d }
31 - const { sx, ...rest } = d.dialogProps||{}
31 + const { sx, root, ...rest } = d.dialogProps||{}
32 const p = d.padding ? 2 : 0
33 return h(MuiDialog, {
34 open: true,
35 maxWidth: 'lg',
36 ...rest,
37 + ...root,
38 onClose: ()=> closeDialog(),
39 },
40 d.title && h(DialogTitle, {}, d.title),
@@ -52,8 +53,9 @@ const type2ico = {
53 info: Info,
54 success: Check,
55 }
55 -
56 -export async function alertDialog(msg: ReactElement | string | Error, type:AlertType='info', icon?: ReactElement) {
56 +export async function alertDialog(msg: ReactElement | string | Error, options?: AlertType | ({ type?:AlertType, icon?: ReactElement } & Partial<DialogOptions>)) {
57 + const opt = typeof options === 'string' ? { type: options } : (options ?? {})
58 + let { type='info', ...rest } = opt
59 if (msg instanceof Error) {
60 msg = msg.message || String(msg)
61 type = 'error'
@@ -62,12 +64,13 @@ export async function alertDialog(msg: ReactElement | string | Error, type:Alert
64 className: 'dialog-alert-'+type,
65 icon: '!',
66 onClose: resolve,
67 + ...rest,
68 Content
69 }))
70
71 function Content(){
72 return h(Box, { display:'flex', flexDirection: 'column', alignItems: 'center', gap: 1 },
70 - icon ?? h(type2ico[type], { color:type }),
73 + opt.icon ?? h(type2ico[type], { color:type }),
74 isValidElement(msg) ? msg : h('div', {}, String(msg))
75 )
76 }
@@ -156,3 +159,27 @@ export async function promptDialog(msg: string, props:any={}) : Promise<string |
159 export function waitDialog() {
160 return newDialog({ Content: CircularProgress, closable: false })
161 }
162 +
163 +export function toast(msg: string | ReactElement, type: AlertType | ReactElement='info') {
164 + const ms = 3000
165 + const close = newDialog({
166 + Content,
167 + dialogProps: {
168 + PaperProps: {
169 + sx: { transition: `opacity ${ms}ms ease-in` },
170 + ref(x: HTMLElement) { // we need to set opacity later, to trigger transition
171 + if (x)
172 + x.style.opacity = '0'
173 + }
174 + }
175 + }
176 + })
177 + setTimeout(close, ms)
178 +
179 + function Content(){
180 + return h(Box, { display:'flex', flexDirection: 'column', alignItems: 'center', gap: 1 },
181 + isValidElement(type) ? type : h(type2ico[type], { color:type }),
182 + isValidElement(msg) ? msg : h('div', {}, String(msg))
183 + )
184 + }
185 +}