share common code: misc

Massimo Melina committed Mar 6, 2022 at 19:56 UTC bc59d22a1e61ad0dc7399db0190d904497eb24f5
11 files changed +182 -174
.gitignore
+1
@@ -6,3 +6,4 @@ counters.yaml
6 *.log
7 config.yaml
8 accounts.yaml
9 +shared/lib
admin/src/misc.ts
+2 -100
@@ -1,101 +1,26 @@
1 // This file is part of HFS - Copyright 2020-2021, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, Fragment, ReactElement,
4 - ReactNode, useCallback, useEffect, useRef, useState } from 'react'
3 +import { createElement as h } from 'react'
4 import { Box, CircularProgress, IconButton, Link, Tooltip } from '@mui/material'
5 import { Link as RouterLink } from 'react-router-dom'
6 import { SxProps } from '@mui/system'
7 import { SvgIconComponent } from '@mui/icons-material'
9 -
10 -export type Dict<T = any> = Record<string, T>
11 -export type Falsy = false | null | undefined | '' | 0
8 +export * from 'shared'
9
10 export function spinner() {
11 return h(CircularProgress)
12 }
13
17 -export function getCookie(name: string) {
18 - const pre = name + '='
19 - let decodedCookie = decodeURIComponent(document.cookie)
20 - let ca = decodedCookie.split(';')
21 - for (let c of ca) {
22 - c = c.trim()
23 - if (c.startsWith(pre))
24 - return c.substring(pre.length, c.length)
25 - }
26 - return ''
27 -}
28 -
29 -export function formatBytes(n: number, post: string = 'B', k=1024) {
30 - if (isNaN(Number(n)) || n < 0)
31 - return ''
32 - let x = ['', 'K', 'M', 'G', 'T']
33 - let prevMul = 1
34 - let mul = k
35 - let i = 0
36 - while (i < x.length && n > mul) {
37 - prevMul = mul
38 - mul *= k
39 - ++i
40 - }
41 - n /= prevMul
42 - return round(n, 1) + ' ' + (x[i]||'') + post
43 -} // formatBytes
44 -
45 -export function round(v: number, decimals: number = 0) {
46 - decimals = Math.pow(10, decimals)
47 - return Math.round(v * decimals) / decimals
48 -} // round
49 -
50 -export function objSameKeys<T,R>(src: Record<string,T>, newValue:(value:T,key:string)=>R) {
51 - return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k)]))
52 -}
53 -
54 -export function enforceFinal(sub:string, s:string) {
55 - return !s || s.endsWith(sub) ? s : s+sub
56 -}
57 -
14 export function isWindowsDrive(s?: string) {
15 return s && /^[a-zA-Z]:$/.test(s)
16 }
17
62 -export function useIsMounted() {
63 - const mountRef = useRef(false)
64 - useEffect(() => {
65 - mountRef.current = true
66 - return () => {
67 - mountRef.current = false
68 - }
69 - }, [])
70 - return useCallback(()=> mountRef.current, [mountRef])
71 -}
72 -
73 -export function useStateMounted<T>(init: T) {
74 - const isMounted = useIsMounted()
75 - const [v, set] = useState(init)
76 - const setIfMounted = useCallback((newValue:T | ((previous:T)=>T)) => {
77 - if (isMounted())
78 - set(newValue)
79 - }, [isMounted, set])
80 - return [v, setIfMounted, isMounted] as [T, typeof setIfMounted, typeof isMounted]
81 -}
82 -
18 export function isEqualLax(a: any,b: any): boolean {
19 return a == b //eslint-disable-line
20 || (a && b && typeof a === 'object' && typeof b === 'object'
21 && Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
22 }
23
89 -type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
90 -
91 -export function truthy<T>(value: T): value is Truthy<T> {
92 - return Boolean(value)
93 -}
94 -
95 -export function onlyTruthy<T>(arr: T[]) {
96 - return arr.filter(truthy)
97 -}
98 -
24 export function IconBtn({ title, icon, ...rest }: { title?: string, icon: SvgIconComponent, [rest:string]:any }) {
25 const ret = h(IconButton, { ...rest }, h(icon))
26 return title ? h(Tooltip, { title, children: ret }) : ret
@@ -105,29 +30,6 @@ export function iconTooltip(icon: SvgIconComponent, tooltip: string, sx?: SxProp
30 return h(Tooltip, { title: tooltip, children: h(icon, { sx }) })
31 }
32
108 -export function prefix(pre:string, v:string|number, post:string='') {
109 - return v ? pre+v+post : ''
110 -}
111 -
112 -export function reactFilter(elements: any[]) {
113 - return elements.filter(x=> x===0 || x && (!Array.isArray(x) || x.length))
114 -}
115 -
116 -export function reactJoin(joiner: string | ReactElement, elements: Parameters<typeof reactFilter>[0]) {
117 - const ret = []
118 - for (const x of reactFilter(elements))
119 - ret.push(x, joiner)
120 - ret.splice(-1,1)
121 - return dontBotherWithKeys(ret)
122 -}
123 -
124 -export function dontBotherWithKeys(elements: ReactNode[]): (ReactNode|string)[] {
125 - return elements.map((e,i)=>
126 - !e || typeof e === 'string' ? e
127 - : Array.isArray(e) ? dontBotherWithKeys(e)
128 - : h(Fragment, { key:i, children:e }) )
129 -}
130 -
33 export function InLink(props:any) {
34 return h(Link, { component: RouterLink, ...props })
35 }
frontend/src/misc.ts
+3 -67
@@ -1,13 +1,11 @@
1 // This file is part of HFS - Copyright 2020-2021, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, useCallback, useState } from 'react'
3 +import { createElement as h } from 'react'
4 import { Spinner } from './components'
5 import { newDialog } from './dialog'
6 import { Icon } from './icons'
7 -
8 -export type Falsy = false | null | undefined | '' | 0
9 -
10 -export type Dict = Record<string, any>
7 +import { Dict } from 'shared'
8 +export * from 'shared'
9
10 export function hIcon(name: string, props?:any) {
11 return h(Icon, { name, ...props })
@@ -17,51 +15,6 @@ export function hError(err: Error | string | null) {
15 return err && h('div', { className:'error-msg' }, typeof err === 'string' ? err : err.message)
16 }
17
20 -export function formatBytes(n: number, post: string = 'B') {
21 - if (isNaN(Number(n)))
22 - return ''
23 - let x = ['', 'K', 'M', 'G', 'T']
24 - let prevMul = 1
25 - let mul = 1024
26 - let i = 0
27 - while (i < x.length && n > mul) {
28 - prevMul = mul
29 - mul *= 1024
30 - ++i
31 - }
32 - n /= prevMul
33 - return round(n, 1) + ' ' + (x[i]||'') + post
34 -} // formatBytes
35 -
36 -export function round(v: number, decimals: number = 0) {
37 - decimals = Math.pow(10, decimals)
38 - return Math.round(v * decimals) / decimals
39 -} // round
40 -
41 -export function prefix(pre:string, v:string|number|undefined|null|false, post:string='') {
42 - return v ? pre+v+post : ''
43 -}
44 -
45 -export function wait(ms: number) {
46 - return new Promise(res=> setTimeout(res,ms))
47 -}
48 -
49 -export function waitFor<T>(cb:()=>T, ms:number=200) : Promise<Exclude<T,Falsy>> {
50 - return new Promise(resolve=>{
51 - let h: NodeJS.Timeout
52 - if (go())
53 - h = setInterval(go, ms)
54 -
55 - function go() {
56 - const v = cb()
57 - if (!v) return true
58 - // @ts-ignore we know it's not falsy
59 - resolve(v)
60 - clearInterval(h)
61 - }
62 - })
63 -}
64 -
18 let isWorking = false // we want the 'working' thing to be singleton
19 export function working() {
20 if (isWorking)
@@ -79,23 +32,6 @@ export function working() {
32 })
33 }
34
82 -export function useForceUpdate(): [()=>void, number] {
83 - const [n, setN] = useState(0)
84 - return [ useCallback(()=> setN(n => n+1), [setN]), n ]
85 -}
86 -
87 -export function getCookie(name: string) {
88 - const pre = name + '='
89 - let decodedCookie = decodeURIComponent(document.cookie)
90 - let ca = decodedCookie.split(';')
91 - for (let c of ca) {
92 - c = c.trim()
93 - if (c.startsWith(pre))
94 - return c.substring(pre.length, c.length)
95 - }
96 - return ''
97 -}
98 -
35 export function hfsEvent(name: string, params?:Dict) {
36 const output: any[] = []
37 document.dispatchEvent(new CustomEvent('hfs.'+name, { detail:{ params, output } }))
package-lock.json
+26 -4
@@ -11,7 +11,8 @@
11 "workspaces": [
12 "server",
13 "admin",
14 - "frontend"
14 + "frontend",
15 + "shared"
16 ],
17 "bin": {
18 "hfs": "dist/src/index.js"
@@ -17585,6 +17586,10 @@
17586 "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
17587 "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
17588 },
17589 + "node_modules/shared": {
17590 + "resolved": "shared",
17591 + "link": true
17592 + },
17593 "node_modules/shebang-command": {
17594 "version": "2.0.0",
17595 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -18883,7 +18888,6 @@
18888 "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
18889 "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
18890 "dev": true,
18886 - "peer": true,
18891 "bin": {
18892 "tsc": "bin/tsc",
18893 "tsserver": "bin/tsserver"
@@ -20372,6 +20376,15 @@
20376 "engines": {
20377 "node": ">=16.13.0"
20378 }
20379 + },
20380 + "shared": {
20381 + "devDependencies": {
20382 + "react": "*",
20383 + "react-dom": "*",
20384 + "react-scripts": "*",
20385 + "sass": "*",
20386 + "typescript": "*"
20387 + }
20388 }
20389 },
20390 "dependencies": {
@@ -33405,6 +33418,16 @@
33418 "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
33419 "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
33420 },
33421 + "shared": {
33422 + "version": "file:shared",
33423 + "requires": {
33424 + "react": "*",
33425 + "react-dom": "*",
33426 + "react-scripts": "*",
33427 + "sass": "*",
33428 + "typescript": "*"
33429 + }
33430 + },
33431 "shebang-command": {
33432 "version": "2.0.0",
33433 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -34401,8 +34424,7 @@
34424 "version": "4.5.2",
34425 "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
34426 "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
34404 - "dev": true,
34405 - "peer": true
34427 + "dev": true
34428 },
34429 "unbox-primitive": {
34430 "version": "1.0.1",
package.json
+2 -1
@@ -9,9 +9,10 @@
9 "homepage": "https://rejetto.com/hfs",
10 "license": "GPL-3.0",
11 "author": "Massimo Melina <a@rejetto.com>",
12 - "workspaces": [ "server", "admin", "frontend" ],
12 + "workspaces": [ "server", "admin", "frontend", "shared" ],
13 "scripts": {
14 "watch-server": "nodemon --ignore server/tests/ --watch server/src -e ts,tsx --exec ts-node server/src/index.ts",
15 + "watch-shared": "cd shared && tsc --watch",
16 "start-frontend": "cd frontend && npm run start",
17 "start-admin": "cd admin && npm run start",
18 "start": "node dist",
shared/package.json new
+16
@@ -0,0 +1,16 @@
1 +{
2 + "name": "shared",
3 + "main": "./lib/index.js",
4 + "types": "./lib/index.d.ts",
5 + "scripts": {
6 + "test": "test",
7 + "build": "tsc && yarn sass src/:lib/"
8 + },
9 + "devDependencies": {
10 + "react": "*",
11 + "react-dom": "*",
12 + "react-scripts": "*",
13 + "typescript": "*",
14 + "sass": "*"
15 + }
16 +}
shared/src/index.ts new
+59
@@ -0,0 +1,59 @@
1 +import _ from 'lodash'
2 +export * from './react'
3 +
4 +export type Dict<T=any> = Record<string, T>
5 +export type Falsy = false | null | undefined | '' | 0
6 +type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
7 +
8 +export function formatBytes(n: number, post: string = 'B', k=1024) {
9 + if (isNaN(Number(n)) || n < 0)
10 + return ''
11 + let x = ['', 'K', 'M', 'G', 'T']
12 + let prevMul = 1
13 + let mul = k
14 + let i = 0
15 + while (i < x.length && n > mul) {
16 + prevMul = mul
17 + mul *= k
18 + ++i
19 + }
20 + n /= prevMul
21 + return _.round(n, 1) + ' ' + (x[i]||'') + post
22 +} // formatBytes
23 +
24 +export function prefix(pre:string, v:string|number|undefined|null|false, post:string='') {
25 + return v ? pre+v+post : ''
26 +}
27 +
28 +export function wait(ms: number) {
29 + return new Promise(res=> setTimeout(res,ms))
30 +}
31 +
32 +export function getCookie(name: string) {
33 + const pre = name + '='
34 + let decodedCookie = decodeURIComponent(document.cookie)
35 + let ca = decodedCookie.split(';')
36 + for (let c of ca) {
37 + c = c.trim()
38 + if (c.startsWith(pre))
39 + return c.substring(pre.length, c.length)
40 + }
41 + return ''
42 +}
43 +
44 +export function objSameKeys<T,R>(src: Record<string,T>, newValue:(value:T,key:string)=>R) {
45 + return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k)]))
46 +}
47 +
48 +export function enforceFinal(sub:string, s:string) {
49 + return !s || s.endsWith(sub) ? s : s+sub
50 +}
51 +
52 +export function truthy<T>(value: T): value is Truthy<T> {
53 + return Boolean(value)
54 +}
55 +
56 +export function onlyTruthy<T>(arr: T[]) {
57 + return arr.filter(truthy)
58 +}
59 +
shared/src/react.ts new
+42
@@ -0,0 +1,42 @@
1 +import { createElement as h, Fragment, ReactElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
2 +
3 +export function useIsMounted() {
4 + const mountRef = useRef(false)
5 + useEffect(() => {
6 + mountRef.current = true
7 + return () => {
8 + mountRef.current = false
9 + }
10 + }, [])
11 + return useCallback(()=> mountRef.current, [mountRef])
12 +}
13 +
14 +export function useStateMounted<T>(init: T) {
15 + const isMounted = useIsMounted()
16 + const [v, set] = useState(init)
17 + const setIfMounted = useCallback((newValue:T | ((previous:T)=>T)) => {
18 + if (isMounted())
19 + set(newValue)
20 + }, [isMounted, set])
21 + return [v, setIfMounted, isMounted] as [T, typeof setIfMounted, typeof isMounted]
22 +}
23 +
24 +export function reactFilter(elements: any[]) {
25 + return elements.filter(x=> x===0 || x && (!Array.isArray(x) || x.length))
26 +}
27 +
28 +export function reactJoin(joiner: string | ReactElement, elements: Parameters<typeof reactFilter>[0]) {
29 + const ret = []
30 + for (const x of reactFilter(elements))
31 + ret.push(x, joiner)
32 + ret.splice(-1,1)
33 + return dontBotherWithKeys(ret)
34 +}
35 +
36 +export function dontBotherWithKeys(elements: ReactNode[]): (ReactNode|string)[] {
37 + return elements.map((e,i)=>
38 + !e || typeof e === 'string' ? e
39 + : Array.isArray(e) ? dontBotherWithKeys(e)
40 + : h(Fragment, { key:i, children:e }) )
41 +}
42 +
shared/tsconfig.json new
+30
@@ -0,0 +1,30 @@
1 +{
2 + "compilerOptions": {
3 + "target": "es2017",
4 + "lib": [
5 + "dom",
6 + "dom.iterable",
7 + "esnext"
8 + ],
9 + "allowJs": true,
10 + "skipLibCheck": true,
11 + "allowSyntheticDefaultImports": true,
12 + "strict": true,
13 + "forceConsistentCasingInFileNames": true,
14 + "module": "esnext",
15 + "moduleResolution": "node",
16 + "resolveJsonModule": true,
17 + "isolatedModules": true,
18 + "outDir": "lib",
19 + "declaration": true
20 + },
21 + "exclude": [
22 + "node_modules",
23 + "lib-esm",
24 + "lib",
25 + "**/*.spec*"
26 + ],
27 + "include": [
28 + "src"
29 + ]
30 +}
todo.md
-1
@@ -5,7 +5,6 @@ aggiornare admin gui
5 counters: non contare richieste fallite
6 consider having mime as ext,ext instead of *.ext|*.ext
7 # To do
8 -- monorepo + share code between apps
8 - admin/accounts: show icon for accounts with (possibly inherited) admin access
9 - expose admin at same port of frontend
10 - admin: improve masks editor
tsconfig.json
+1 -1
@@ -1,5 +1,5 @@
1 {
2 - "exclude": ["frontend","admin","tests","dist"],
2 + "exclude": ["frontend","admin","tests","dist","shared"],
3 "compilerOptions": {
4 /* Visit https://aka.ms/tsconfig.json to read more about this file */
5