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