admin: ux improvements home & fs
Massimo Melina committed
Mar 15, 2022 at 13:55 UTC
8eb545d52a4fc126367ea7bebf9ac6d983f7a67f
7 files changed
+27
-18
admin/src/App.ts
+1
-1
@@ -56,7 +56,7 @@ function Routed() {
56
overflowX: 'auto',
57
}
58
},
59
- title && h(Hidden, { mdDown: true }, h(Typography, { variant:'h1', mb:2 }, title) ),
59
+ title && h(Hidden, { mdDown: true }, h(Typography, { variant:'h2', mb:2 }, title) ),
60
h(Routes, {}, mainMenu.map((it,idx) =>
61
h(Route, { key: idx, path: it.path, element: h(it.comp) })) )
62
),
admin/src/FileCard.ts
+11
-8
@@ -84,18 +84,19 @@ function FileForm({ file }: { file: ReturnType<typeof useSnapState>['selectedFil
84
},
85
hasSource && { k: 'source', comp: DisplayField },
86
{ k: 'can_read', label:"Who can download", xl: showCanSee && 6, comp: WhoField, parent, accounts, inherit: inheritedPerms.can_read,
87
- helperText: "Who cannot download also cannot see in list"
87
+ helperText: "Note: who cannot download also cannot see in list"
88
},
89
showCanSee && { k: 'can_see', label:"Who can see", xl: 6, comp: WhoField, parent, accounts, inherit: inheritedPerms.can_see,
90
helperText: "If you hide this element it will not be listed, but will still be accessible if you have a direct link"
91
},
92
hasSource && !realFolder && { k: 'size', comp: DisplayField, toField: formatBytes },
93
- showTimestamps && { k: 'ctime', comp: DisplayField, md: 6, label: 'Created', toField: formatTimestamp },
94
- showTimestamps && { k: 'mtime', comp: DisplayField, md: 6, label: 'Modified', toField: formatTimestamp },
95
- realFolder && { k: 'default', md: 6, comp: BoolField, label:"Act as website",
93
+ showTimestamps && { k: 'ctime', comp: DisplayField, lg: 6, label: 'Created', toField: formatTimestamp },
94
+ showTimestamps && { k: 'mtime', comp: DisplayField, lg: 6, label: 'Modified', toField: formatTimestamp },
95
+ file.website && { k: 'default', comp: BoolField, label:"Serve index.html",
96
toField: Boolean, fromField: (v:boolean) => v ? 'index.html' : null,
97
- helperText: md("If you want this folder to work like a website and load `index.html`") },
98
- isDir && { k: 'masks', multiline: true, md: 6, toField: JSON.stringify, fromField: JSON.parse,
97
+ helperText: md("This folder may be a website because contains `index.html`. Enabling this will show the website instead of the list of files.")
98
+ },
99
+ isDir && { k: 'masks', multiline: true, xl: 6, toField: JSON.stringify, fromField: JSON.parse,
100
helperText: "This is a special field. Leave it empty unless you know what you are doing." }
101
]
102
})
@@ -106,7 +107,7 @@ function formatTimestamp(x: string) {
107
}
108
109
interface WhoFieldProps extends FieldProps<Who> { accounts: Account[] }
109
-function WhoField({ value, onChange, parent, inherit, accounts, ...rest }: WhoFieldProps) {
110
+function WhoField({ value, onChange, parent, inherit, accounts, helperText, ...rest }: WhoFieldProps) {
111
const options = useMemo(() =>
112
onlyTruthy([
113
{ value: null, label: (parent ? "Same as parent: " : "Default: " ) + who2desc(inherit === 0 ? true : inherit) },
@@ -122,6 +123,7 @@ function WhoField({ value, onChange, parent, inherit, accounts, ...rest }: WhoFi
123
return h('div', {},
124
h(SelectField as Field<Who>, {
125
...rest,
126
+ helperText: !arrayMode && helperText,
127
value: arrayMode ? [] : value,
128
onChange(v, { was, event }) {
129
onChange(v, { was , event })
@@ -129,9 +131,10 @@ function WhoField({ value, onChange, parent, inherit, accounts, ...rest }: WhoFi
131
options
132
}),
133
arrayMode && h(MultiSelectField as Field<string[]>, {
132
- label: "Choose accounts for " + rest.label,
134
+ label: accounts?.length ? "Choose accounts for " + rest.label : "You didn't create any account yet",
135
value,
136
onChange,
137
+ helperText,
138
options: accounts?.map(a => ({ value: a.username, label: a.username })) || [],
139
})
140
)
admin/src/HomePage.ts
+4
-5
@@ -31,8 +31,10 @@ export default function HomePage() {
31
!cfg ? spinner() :
32
errors.length ? dontBotherWithKeys(errors.map(msg => entry('error', dontBotherWithKeys(msg))))
33
: entry('success', "Server is working"),
34
- entry('', "Here you manage your server. Access your shared files in the ",
35
- h(Link, { target:'frontend', href: '/' }, "Frontend interface", h(Launch, { sx: { verticalAlign: 'sub', ml: '.2em' } }))),
34
+ !vfs?.root?.children?.length && !vfs?.root?.source
35
+ ? entry('warning', "You have no files shared — ", fsLink("add some"))
36
+ : entry('', "Here you manage your server. There is a SEPARATED interface to access your shared files: ",
37
+ h(Link, { target:'frontend', href: '/' }, "Frontend interface", h(Launch, { sx: { verticalAlign: 'sub', ml: '.2em' } }))),
38
! href && entry('warning', "Frontend unreachable: ",
39
!cfg ? '...'
40
: errors.length === 2 ? "both http and https are in error"
@@ -42,9 +44,6 @@ export default function HomePage() {
44
)
45
),
46
!username && entry('', "You are accessing on localhost without an account — ", h(InLink, { to:'accounts' }, "give admin access to an account to be able to access from other computers")),
45
-
46
- vfs?.root && !vfs.root.children?.length && !vfs.root.source &&
47
- entry('warning', "You have no files shared — ", fsLink("add some"))
47
)
48
}
49
admin/src/MainMenu.ts
+2
-2
@@ -21,10 +21,10 @@ interface MenuEntry {
21
}
22
23
export const mainMenu: MenuEntry[] = [
24
- { path: '', icon: Public, label: 'Home', title: 'Admin interface', comp: HomePage },
24
+ { path: '', icon: Public, label: 'Home', title: "Admin interface", comp: HomePage },
25
{ path: 'monitor', icon: Monitor, comp: MonitorPage },
26
{ path: 'configuration', icon: Settings, comp: ConfigPage },
27
- { path: 'fs', icon: AccountTree, label: 'File System', comp: VfsPage },
27
+ { path: 'fs', icon: AccountTree, label: "Shared files", comp: VfsPage },
28
{ path: 'accounts', icon: ManageAccounts, comp: AccountsPage },
29
{ path: 'Logout', icon: Logout, comp: LogoutPage }
30
]
admin/src/VfsPage.ts
+3
-1
@@ -2,7 +2,7 @@
2
3
import { createElement as h, isValidElement, useEffect, useMemo, useState } from 'react'
4
import { useApiComp } from './api'
5
-import { Grid } from '@mui/material'
5
+import { Grid, Typography } from '@mui/material'
6
import { state, useSnapState } from './state'
7
import FileCard from './FileCard'
8
import VfsMenuBar from './VfsMenuBar'
@@ -49,6 +49,7 @@ export default function VfsPage() {
49
}
50
return h(Grid, { container:true, rowSpacing: 1 },
51
h(Grid, { item:true, sm: 6, lg: 7 },
52
+ h(Typography, { variant: 'h6', mb:1, }, "Virtual File System"),
53
h(VfsMenuBar),
54
snap.vfs && h(VfsTree, { id2node })),
55
h(Grid, { item:true, sm: 6, lg: 5, maxWidth:'100%' },
@@ -73,6 +74,7 @@ export type VfsNode = {
74
parent?: VfsNode
75
can_see: Who
76
can_read: Who
77
+ website?: true
78
masks?: any
79
80
isRoot?: true
admin/src/VfsTree.ts
+3
-1
@@ -63,7 +63,9 @@ export default function VfsTree({ id2node }:{ id2node: Map<string, VfsNode> }) {
63
source = './' + source
64
return h(TreeItem, {
65
label: h('div', { className: styles.label },
66
- h(!name ? Home : folder ? FolderIcon : FileIcon),
66
+ !name ? iconTooltip(Home, "home, or root if you like")
67
+ : folder ? iconTooltip(FolderIcon, "Folder")
68
+ : iconTooltip(FileIcon, "File"),
69
isRestricted(node.can_see) && iconTooltip(RemoveRedEye, "Restrictions on who can see"),
70
isRestricted(node.can_read) && iconTooltip(Lock, "Restrictions on who can download"),
71
node.default && iconTooltip(Web, "Act as website"),
server/src/api.vfs.ts
+3
@@ -16,6 +16,7 @@ type VfsAdmin = {
16
size?: number,
17
ctime?: Date,
18
mtime?: Date,
19
+ website?: true,
20
children?: VfsAdmin[]
21
} & Omit<VfsNode, 'type' | 'children'>
22
@@ -45,6 +46,8 @@ const apis: ApiHandlers = {
46
return {
47
...stats,
48
...node,
49
+ website: dir && node.source && await stat(join(node.source, 'index.html')).then(() => true, () => undefined)
50
+ || undefined,
51
name: getNodeName(node),
52
type: dir ? 'folder' : undefined,
53
children: node.children && await Promise.all(node.children.map(recur)),