ux: admin/fs: suggest what to do
Massimo Melina committed
Mar 18, 2022 at 15:46 UTC
73516e0b0c8b277d4c22bbae6605ee3a61cc5429
6 files changed
+68
-22
admin/src/VfsPage.ts
+27
-3
@@ -1,13 +1,16 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import { createElement as h, isValidElement, useEffect, useMemo, useState } from 'react'
4
-import { useApiComp } from './api'
5
-import { Grid, Typography } from '@mui/material'
4
+import { useApi, useApiComp } from './api'
5
+import { Alert, Grid, Link, Typography } from '@mui/material'
6
import { state, useSnapState } from './state'
7
import FileCard from './FileCard'
8
import VfsMenuBar from './VfsMenuBar'
9
import VfsTree from './VfsTree'
10
import { onlyTruthy } from './misc'
11
+import { reactJoin } from '@hfs/shared'
12
+import _ from 'lodash'
13
+import { AlertProps } from '@mui/material/Alert/Alert'
14
15
let selectOnReload: string[] | undefined
16
@@ -43,17 +46,38 @@ export default function VfsPage() {
46
}
47
48
}, [res, id2node])
49
+ const [status] = useApi(window.location.host === 'localhost' && 'get_status')
50
+ const urls = useMemo(() =>
51
+ typeof status === 'object'
52
+ && _.sortBy(
53
+ onlyTruthy(Object.values(status.urls?.https || status.urls?.http || {}).map(u => typeof u === 'string' && u)),
54
+ url => url.includes('[')
55
+ ),
56
+ [status])
57
if (isValidElement(res)) {
58
id2node.clear()
59
return res
60
}
61
+ const anythingShared = !res?.root?.children?.length && !res?.root?.source
62
+ const alert: AlertProps | false = anythingShared ? {
63
+ severity: 'warning',
64
+ children: "Add something to your shared files — click Add"
65
+ } : urls && {
66
+ severity: 'info',
67
+ children: [
68
+ "Your shared files can be browsed from ",
69
+ reactJoin(" or ", urls.slice(0,3).map(href => h(Link, { href }, href)))
70
+ ]
71
+ }
72
return h(Grid, { container:true, rowSpacing: 1, maxWidth: '80em' },
73
+ alert && h(Grid, { item: true, mb: 2, xs: 12 }, h(Alert, alert)),
74
h(Grid, { item:true, sm: 6, lg: 5 },
75
h(Typography, { variant: 'h6', mb:1, }, "Virtual File System"),
76
h(VfsMenuBar),
77
snap.vfs && h(VfsTree, { id2node })),
78
h(Grid, { item:true, sm: 6, lg: 7, maxWidth:'100%' },
56
- h(FileCard)))
79
+ h(FileCard))
80
+ )
81
}
82
83
export function reloadVfs(pleaseSelect?: string[]) {
server/src/adminApis.ts
+2
-1
@@ -2,7 +2,7 @@
2
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import { getConfig, getWholeConfig, setConfig } from './config'
5
-import { getStatus } from './listen'
5
+import { getStatus, getUrls } from './listen'
6
import { BUILD_TIMESTAMP, FORBIDDEN, HFS_STARTED, VERSION } from './const'
7
import vfsApis from './api.vfs'
8
import accountsApis from './api.accounts'
@@ -43,6 +43,7 @@ export const adminApis: ApiHandlers = {
43
version: VERSION,
44
http: serverStatus(st.httpSrv, getConfig('port')),
45
https: serverStatus(st.httpsSrv, getConfig('https_port')),
46
+ urls: getUrls(),
47
}
48
49
function serverStatus(h: typeof st.httpSrv, configuredPort?: number) {
server/src/listen.ts
+35
-15
@@ -8,12 +8,12 @@ import { watchLoad } from './watchLoad'
8
import { networkInterfaces } from 'os';
9
import { newConnection } from './connections'
10
import open from 'open'
11
-import { prefix, wait } from './misc'
11
+import { onlyTruthy, prefix, wait } from './misc'
12
import { ADMIN_URI, DEV } from './const'
13
import findProcess from 'find-process'
14
import _ from 'lodash'
15
16
-interface ServerExtra { error?: string, busy?: string }
16
+interface ServerExtra { name: string, error?: string, busy?: string }
17
let httpSrv: http.Server & ServerExtra
18
let httpsSrv: http.Server & ServerExtra
19
@@ -21,8 +21,8 @@ subscribeConfig<number>({ k:'port', defaultValue: 80 }, async port => {
21
while (!app)
22
await wait(100)
23
stopServer(httpSrv).then()
24
- httpSrv = http.createServer(app.callback())
25
- port = await startServer(httpSrv, { port, name:'http' })
24
+ httpSrv = Object.assign(http.createServer(app.callback()), { name: 'http' })
25
+ port = await startServer(httpSrv, { port })
26
if (!port) return
27
httpSrv.on('connection', newConnection)
28
printUrls(port, 'http')
@@ -58,7 +58,10 @@ async function considerHttps() {
58
stopServer(httpsSrv).then()
59
let port = getConfig('https_port')
60
try {
61
- httpsSrv = https.createServer(port < 0 ? {} : { key: httpsNeeds.private_key, cert: httpsNeeds.cert }, app.callback())
61
+ httpsSrv = Object.assign(
62
+ https.createServer(port < 0 ? {} : { key: httpsNeeds.private_key, cert: httpsNeeds.cert }, app.callback()),
63
+ { name: 'https' }
64
+ )
65
const missingKey = _.findKey(httpsNeeds, v => !v) as keyof typeof httpsNeeds
66
httpsSrv.error = port < 0 ? undefined
67
: missingKey && prefix(getConfig(missingKey) ? "cannot read file for " : "missing ", httpsNeedsNames[missingKey])
@@ -70,18 +73,15 @@ async function considerHttps() {
73
console.log("failed to create https server: check your private key and certificate", String(e))
74
return
75
}
73
- port = await startServer(httpsSrv, {
74
- port: getConfig('https_port'),
75
- name: 'https'
76
- })
76
+ port = await startServer(httpsSrv, { port: getConfig('https_port') })
77
if (!port) return
78
httpsSrv.on('connection', socket =>
79
newConnection(socket, true))
80
printUrls(port, 'https')
81
}
82
83
-interface StartServer { port: number, name:string, net?:string }
84
-function startServer(srv: typeof httpSrv, { port, name, net='0.0.0.0' }: StartServer) {
83
+interface StartServer { port: number, net?:string }
84
+function startServer(srv: typeof httpSrv, { port, net='0.0.0.0' }: StartServer) {
85
return new Promise<number>((resolve, reject) => {
86
try {
87
if (port < 0)
@@ -94,7 +94,7 @@ function startServer(srv: typeof httpSrv, { port, name, net='0.0.0.0' }: StartSe
94
srv.close()
95
return reject('type of socket not supported')
96
}
97
- console.log(name, "serving on", net, ':', ad.port)
97
+ console.log(srv.name, "serving on", net, ':', ad.port)
98
resolve(ad.port)
99
}).on('error', async e => {
100
srv.error = String(e)
@@ -104,14 +104,14 @@ function startServer(srv: typeof httpSrv, { port, name, net='0.0.0.0' }: StartSe
104
srv.busy = res[0]?.name
105
srv.error = `couldn't listen on port ${port} used by ${srv.busy}`
106
}
107
- console.error(name, srv.error)
107
+ console.error(srv.name, srv.error)
108
console.log(" >> try specifying a different port like: --port 8011")
109
resolve(0)
110
})
111
}
112
catch(e) {
113
srv.error = String(e)
114
- console.error(name, "couldn't listen on port", port, srv.error)
114
+ console.error(srv.name, "couldn't listen on port", port, srv.error)
115
resolve(0)
116
}
117
})
@@ -139,9 +139,29 @@ export function getStatus() {
139
}
140
}
141
142
+const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*)$/i // avoid giving too much information
143
+
144
+export function getUrls() {
145
+ return Object.fromEntries(onlyTruthy([httpSrv, httpsSrv].map(srv => {
146
+ if (!srv.listening)
147
+ return false
148
+ const port = (srv?.address() as any)?.port
149
+ const appendPort = port === (srv.name === 'https' ? 443 : 80) ? '' : ':' + port
150
+ const urls = onlyTruthy(Object.entries(networkInterfaces()).map(([name, nets]) =>
151
+ nets && !ignore.test(name) && nets.map(net => {
152
+ if (net.internal) return
153
+ let { address } = net
154
+ if (address.includes(':'))
155
+ address = '[' + address + ']'
156
+ return srv.name + '://' + address + appendPort
157
+ })
158
+ ).flat())
159
+ return urls.length && [srv.name, urls]
160
+ })))
161
+}
162
+
163
function printUrls(port: number, proto: string) {
164
if (!port) return
144
- const ignore = /^(lo|.*loopback.*|virtualbox.*|.*\(wsl\).*)$/i // avoid giving too much information
165
for (const [name, nets] of Object.entries(networkInterfaces())) {
166
if (!nets || ignore.test(name)) continue
167
console.log('network', name)
server/src/misc.ts
+1
-1
@@ -36,7 +36,7 @@ export function setHidden(dest: object, src:object) {
36
})))
37
}
38
39
-export function objSameKeys<S extends object,VR=any>(src: S, newValue:(value:any, key:keyof S)=>any) {
39
+export function objSameKeys<S extends object,VR=any>(src: S, newValue:(value:Truthy<S[keyof S]>, key:keyof S)=>any) {
40
return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k as keyof S)])) as { [K in keyof S]:VR }
41
}
42
shared/src/index.ts
+2
-2
@@ -43,8 +43,8 @@ export function getCookie(name: string) {
43
return ''
44
}
45
46
-export function objSameKeys<T,R>(src: Record<string,T>, newValue:(value:T,key:string)=>R) {
47
- return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k)]))
46
+export function objSameKeys<S extends object,VR=any>(src: S, newValue:(value:Truthy<S[keyof S]>, key:keyof S)=>any) {
47
+ return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k as keyof S)])) as { [K in keyof S]:VR }
48
}
49
50
export function enforceFinal(sub:string, s:string) {
todo.md
+1
@@ -4,6 +4,7 @@
4
- admin/fs: drag&drop to move items around
5
- admin/fs: support insert/delete key
6
- admin/fs: button "copy url to clipboard"
7
+- admin/fs: make possible to bind source to home
8
- admin/monitor: show some info on what folder is browsing
9
- if specified config is a folder, check for file config.yaml inside
10
- merge accounts in config