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