@samitouri / QOSami-HFS / commits / 72f624a5

better code: split files

Massimo Melina committed Sep 16, 2023 at 15:54 UTC 72f624a5f92346fa7ff496e40ac299b1d99e5240
5 files changed +217 -212
admin/src/misc.ts
+1 -205
@@ -1,142 +1,9 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, FC, Fragment, ReactNode, KeyboardEvent, forwardRef } from 'react'
4 -import {
5 - Box, BoxProps,
6 - Breakpoint,
7 - ButtonProps,
8 - CircularProgress,
9 - IconButton,
10 - IconButtonProps,
11 - Link, LinkProps,
12 - Tooltip, TooltipProps,
13 - useMediaQuery
14 -} from '@mui/material'
15 -import { Link as RouterLink } from 'react-router-dom'
16 -import { SxProps } from '@mui/system'
17 -import { Refresh, SvgIconComponent } from '@mui/icons-material'
18 -import { alertDialog, confirmDialog } from './dialog'
3 import { apiCall } from './api'
20 -import { dontBotherWithKeys, formatPerc, useStateMounted, WIKI_URL } from '@hfs/shared'
21 -import { Promisable } from '@hfs/mui-grid-form'
22 -import { LoadingButton, LoadingButtonProps } from '@mui/lab'
4 +export * from './mui'
5 export * from '@hfs/shared'
6
25 -export function spinner() {
26 - return h(CircularProgress)
27 -}
28 -
29 -export function isWindowsDrive(s?: string) {
30 - return s && /^[a-zA-Z]:$/.test(s)
31 -}
32 -
33 -export function isEqualLax(a: any,b: any): boolean {
34 - return a == b //eslint-disable-line
35 - || (a && b && typeof a === 'object' && typeof b === 'object'
36 - && Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
37 -}
38 -
39 -export function modifiedSx(is: boolean) {
40 - return is ? { outline: '2px solid' } : undefined
41 -}
42 -interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick'> {
43 - title?: ReactNode
44 - icon: SvgIconComponent
45 - disabled?: boolean | string
46 - progress?: boolean | number
47 - link?: string
48 - confirm?: string
49 - tooltipProps?: Partial<TooltipProps>
50 - onClick: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
51 -}
52 -
53 -export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps, ref: any) => {
54 - const [loading, setLoading] = useStateMounted(false)
55 - if (typeof disabled === 'string')
56 - title = disabled
57 - if (link)
58 - onClick = () => window.open(link)
59 - let ret: ReturnType<FC> = h(IconButton, {
60 - ref,
61 - disabled: Boolean(loading || progress || disabled),
62 - ...rest,
63 - async onClick(...args) {
64 - if (confirm && !await confirmDialog(confirm)) return
65 - const ret = onClick?.apply(this,args)
66 - if (ret && ret instanceof Promise) {
67 - setLoading(true)
68 - ret.catch(alertDialog).finally(()=> setLoading(false))
69 - }
70 - }
71 - },
72 - (progress || loading) && progress !== false // false is also useful to inhibit behavior with loading
73 - && h(CircularProgress, {
74 - ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
75 - style: { position:'absolute', top: '10%', left: '10%', width: '80%', height: '80%' }
76 - }),
77 - h(icon)
78 - )
79 - if (title)
80 - ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
81 - return ret
82 -})
83 -
84 -interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'> {
85 - icon?: SvgIconComponent
86 - title?: ReactNode
87 - disabled?: boolean | string
88 - progress?: boolean | number
89 - link?: string
90 - confirm?: string
91 - tooltipProps?: TooltipProps
92 - onClick: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
93 -}
94 -export function Btn({ icon, title, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: BtnProps) {
95 - const [loading, setLoading] = useStateMounted(false)
96 - if (typeof disabled === 'string') {
97 - title = disabled
98 - disabled = true
99 - }
100 - if (link)
101 - onClick = () => window.open(link)
102 - let ret: ReturnType<FC> = h(LoadingButton, {
103 - variant: 'contained',
104 - startIcon: icon && h(icon),
105 - loading: Boolean(loading || progress),
106 - loadingPosition: icon && 'start',
107 - loadingIndicator: typeof progress !== 'number' ? undefined
108 - : h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
109 - disabled,
110 - ...rest,
111 - async onClick(...args) {
112 - if (confirm && !await confirmDialog(confirm)) return
113 - const ret = onClick?.apply(this,args)
114 - if (ret && ret instanceof Promise) {
115 - setLoading(true)
116 - ret.catch(alertDialog).finally(()=> setLoading(false))
117 - }
118 - }
119 - })
120 - if (title)
121 - ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
122 - return ret
123 -}
124 -
125 -export function iconTooltip(icon: SvgIconComponent, tooltip: ReactNode, sx?: SxProps) {
126 - return h(Tooltip, { title: tooltip, children: h(icon, { sx: { verticalAlign: 'bottom', ...sx } }) })
127 -}
128 -
129 -export function InLink(props:any) {
130 - return h(Link, { component: RouterLink, ...props })
131 -}
132 -
133 -export const Center = forwardRef((props: BoxProps, ref) =>
134 - h(Box, { ref, display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', flexDirection: 'column', ...props }))
135 -
136 -export function LinkBtn({ ...rest }: LinkProps) {
137 - return h(Link, { ...rest, sx: { cursor: 'pointer', ...rest.sx } })
138 -}
139 -
7 export async function manipulateConfig(k: string, work:(data:any) => any) {
8 const cfg = await apiCall('get_config', { only: [k] })
9 const was = cfg[k]
@@ -145,80 +12,9 @@ export async function manipulateConfig(k: string, work:(data:any) => any) {
12 await apiCall('set_config', { values: { [k]: will } })
13 }
14
148 -export function typedKeys<T extends {}>(o: T) {
149 - return Object.keys(o) as (keyof T)[]
150 -}
151 -
152 -export function xlate(input: any, table: Record<string, any>) {
153 - return table[input] ?? input
154 -}
155 -
156 -// return true if same size or larger
157 -export function useBreakpoint(breakpoint: Breakpoint) {
158 - return useMediaQuery((theme: any) => theme.breakpoints.up(breakpoint), { noSsr:true }) // without noSsr, first execution always returns false
159 -}
160 -
15 export function err2msg(code: string) {
16 return {
17 ENOENT: "Not found",
18 ENOTDIR: "Not a folder",
19 }[code] || code
20 }
167 -
168 -export function reloadBtn(onClick: any, props?: any) {
169 - return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
170 -}
171 -
172 -const isMac = navigator.platform.match('Mac')
173 -export function isCtrlKey(ev: KeyboardEvent) {
174 - return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
175 -}
176 -
177 -interface IconProgressProps {
178 - icon: SvgIconComponent,
179 - progress: number,
180 - offset?: number,
181 - sx?: SxProps,
182 - addTitle?: ReactNode
183 -}
184 -export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgressProps) {
185 - return h(Fragment, {},
186 - h(icon, { sx: { position:'absolute', ml: '4px' } }),
187 - h(CircularProgress, {
188 - value: progress * 100,
189 - variant: 'determinate',
190 - size: 32,
191 - sx: { position: 'absolute' },
192 - }),
193 - h(Tooltip, {
194 - title: h(Fragment, {}, formatPerc(progress), addTitle),
195 - children: h(CircularProgress, {
196 - color: 'success',
197 - value: (offset || 1e-7) * 100,
198 - variant: 'determinate',
199 - size: 32,
200 - sx: { display: 'flex', ...sx }, // workaround: without this the element is has 0 width when the space is crammy (monitor/file)
201 - }),
202 - })
203 - )
204 -}
205 -
206 -export function Flex({ gap='.8em', vert=false, center=false, children=null, props={}, ...rest }) {
207 - return h(Box, {
208 - sx: {
209 - display: 'flex',
210 - gap,
211 - flexDirection: vert ? 'column' : undefined,
212 - ...center && { alignItems: 'center', justifyContent: 'center' },
213 - ...rest,
214 - },
215 - ...props
216 - }, children)
217 -}
218 -
219 -
220 -export function wikiLink(uri: string, content: ReactNode) {
221 - if (Array.isArray(content))
222 - content = dontBotherWithKeys(content)
223 - return h(Link, { href: WIKI_URL + uri, target: 'help' }, content)
224 -}
\ No newline at end of file
admin/src/mui.ts new
+185
@@ -0,0 +1,185 @@
1 +// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2 +// all content here is shared between client and server
3 +
4 +import { Refresh, SvgIconComponent } from '@mui/icons-material'
5 +import { SxProps } from '@mui/system'
6 +import { createElement as h, FC, forwardRef, Fragment, ReactNode } from 'react'
7 +import {
8 + Box, BoxProps, Breakpoint,
9 + ButtonProps,
10 + CircularProgress,
11 + IconButton,
12 + IconButtonProps,
13 + Link, LinkProps,
14 + Tooltip,
15 + TooltipProps, useMediaQuery
16 +} from '@mui/material'
17 +import { formatPerc, WIKI_URL } from '../../src/cross'
18 +import { dontBotherWithKeys, useStateMounted } from '@hfs/shared'
19 +import { Promisable } from '@hfs/mui-grid-form'
20 +import { alertDialog, confirmDialog } from './dialog'
21 +import { LoadingButton, LoadingButtonProps } from '@mui/lab'
22 +import { Link as RouterLink } from 'react-router-dom'
23 +
24 +export function spinner() {
25 + return h(CircularProgress)
26 +}
27 +
28 +// return true if same size or larger
29 +export function useBreakpoint(breakpoint: Breakpoint) {
30 + return useMediaQuery((theme: any) => theme.breakpoints.up(breakpoint), { noSsr:true }) // without noSsr, first execution always returns false
31 +}
32 +
33 +interface IconProgressProps {
34 + icon: SvgIconComponent,
35 + progress: number,
36 + offset?: number,
37 + sx?: SxProps,
38 + addTitle?: ReactNode
39 +}
40 +export function IconProgress({ icon, progress, offset, addTitle, sx }: IconProgressProps) {
41 + return h(Fragment, {},
42 + h(icon, { sx: { position:'absolute', ml: '4px' } }),
43 + h(CircularProgress, {
44 + value: progress * 100,
45 + variant: 'determinate',
46 + size: 32,
47 + sx: { position: 'absolute' },
48 + }),
49 + h(Tooltip, {
50 + title: h(Fragment, {}, formatPerc(progress), addTitle),
51 + children: h(CircularProgress, {
52 + color: 'success',
53 + value: (offset || 1e-7) * 100,
54 + variant: 'determinate',
55 + size: 32,
56 + sx: { display: 'flex', ...sx }, // workaround: without this the element is has 0 width when the space is crammy (monitor/file)
57 + }),
58 + })
59 + )
60 +}
61 +
62 +export function Flex({ gap='.8em', vert=false, center=false, children=null, props={}, ...rest }) {
63 + return h(Box, {
64 + sx: {
65 + display: 'flex',
66 + gap,
67 + flexDirection: vert ? 'column' : undefined,
68 + ...center && { alignItems: 'center', justifyContent: 'center' },
69 + ...rest,
70 + },
71 + ...props
72 + }, children)
73 +}
74 +
75 +
76 +export function wikiLink(uri: string, content: ReactNode) {
77 + if (Array.isArray(content))
78 + content = dontBotherWithKeys(content)
79 + return h(Link, { href: WIKI_URL + uri, target: 'help' }, content)
80 +}
81 +
82 +export function reloadBtn(onClick: any, props?: any) {
83 + return h(IconBtn, { icon: Refresh, title: "Reload", onClick, ...props })
84 +}
85 +
86 +export function modifiedSx(is: boolean) {
87 + return is ? { outline: '2px solid' } : undefined
88 +}
89 +interface IconBtnProps extends Omit<IconButtonProps, 'disabled'|'title'|'onClick'> {
90 + title?: ReactNode
91 + icon: SvgIconComponent
92 + disabled?: boolean | string
93 + progress?: boolean | number
94 + link?: string
95 + confirm?: string
96 + tooltipProps?: Partial<TooltipProps>
97 + onClick: (...args: Parameters<NonNullable<IconButtonProps['onClick']>>) => Promisable<any>
98 +}
99 +
100 +export const IconBtn = forwardRef(({ title, icon, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: IconBtnProps, ref: any) => {
101 + const [loading, setLoading] = useStateMounted(false)
102 + if (typeof disabled === 'string')
103 + title = disabled
104 + if (link)
105 + onClick = () => window.open(link)
106 + let ret: ReturnType<FC> = h(IconButton, {
107 + ref,
108 + disabled: Boolean(loading || progress || disabled),
109 + ...rest,
110 + async onClick(...args) {
111 + if (confirm && !await confirmDialog(confirm)) return
112 + const ret = onClick?.apply(this,args)
113 + if (ret && ret instanceof Promise) {
114 + setLoading(true)
115 + ret.catch(alertDialog).finally(()=> setLoading(false))
116 + }
117 + }
118 + },
119 + (progress || loading) && progress !== false // false is also useful to inhibit behavior with loading
120 + && h(CircularProgress, {
121 + ...(typeof progress === 'number' ? { value: progress*100, variant: 'determinate' } : null),
122 + style: { position:'absolute', top: '10%', left: '10%', width: '80%', height: '80%' }
123 + }),
124 + h(icon)
125 + )
126 + if (title)
127 + ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
128 + return ret
129 +})
130 +
131 +interface BtnProps extends Omit<LoadingButtonProps,'disabled'|'title'|'onClick'> {
132 + icon?: SvgIconComponent
133 + title?: ReactNode
134 + disabled?: boolean | string
135 + progress?: boolean | number
136 + link?: string
137 + confirm?: string
138 + tooltipProps?: TooltipProps
139 + onClick: (...args: Parameters<NonNullable<ButtonProps['onClick']>>) => Promisable<any>
140 +}
141 +export function Btn({ icon, title, onClick, disabled, progress, link, tooltipProps, confirm, ...rest }: BtnProps) {
142 + const [loading, setLoading] = useStateMounted(false)
143 + if (typeof disabled === 'string') {
144 + title = disabled
145 + disabled = true
146 + }
147 + if (link)
148 + onClick = () => window.open(link)
149 + let ret: ReturnType<FC> = h(LoadingButton, {
150 + variant: 'contained',
151 + startIcon: icon && h(icon),
152 + loading: Boolean(loading || progress),
153 + loadingPosition: icon && 'start',
154 + loadingIndicator: typeof progress !== 'number' ? undefined
155 + : h(CircularProgress, { size: '1rem', value: progress*100, variant: 'determinate' }),
156 + disabled,
157 + ...rest,
158 + async onClick(...args) {
159 + if (confirm && !await confirmDialog(confirm)) return
160 + const ret = onClick?.apply(this,args)
161 + if (ret && ret instanceof Promise) {
162 + setLoading(true)
163 + ret.catch(alertDialog).finally(()=> setLoading(false))
164 + }
165 + }
166 + })
167 + if (title)
168 + ret = h(Tooltip, { title, ...tooltipProps, children: h('span',{},ret) })
169 + return ret
170 +}
171 +
172 +export function iconTooltip(icon: SvgIconComponent, tooltip: ReactNode, sx?: SxProps) {
173 + return h(Tooltip, { title: tooltip, children: h(icon, { sx: { verticalAlign: 'bottom', ...sx } }) })
174 +}
175 +
176 +export function InLink(props:any) {
177 + return h(Link, { component: RouterLink, ...props })
178 +}
179 +
180 +export const Center = forwardRef((props: BoxProps, ref) =>
181 + h(Box, { ref, display:'flex', height:'100%', width:'100%', justifyContent:'center', alignItems:'center', flexDirection: 'column', ...props }))
182 +
183 +export function LinkBtn({ ...rest }: LinkProps) {
184 + return h(Link, { ...rest, sx: { cursor: 'pointer', ...rest.sx } })
185 +}
shared/react.ts
+15 -1
@@ -1,6 +1,15 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 -import { createElement as h, Fragment, ReactElement, ReactNode, useCallback, useEffect, useState } from 'react'
3 +import {
4 + createElement as h,
5 + Fragment,
6 + KeyboardEvent,
7 + ReactElement,
8 + ReactNode,
9 + useCallback,
10 + useEffect,
11 + useState
12 +} from 'react'
13 import { useIsMounted } from 'usehooks-ts'
14
15 export function useStateMounted<T>(init: T) {
@@ -71,4 +80,9 @@ export function useBatch<Job=unknown,Result=unknown>(
80 if (env && cached === undefined)
81 env.batch.add(job)
82 return { data: cached, ...env } as Env & { data: Result | undefined | null } // so you can cache.clear
83 +}
84 +
85 +const isMac = navigator.platform.match('Mac')
86 +export function isCtrlKey(ev: KeyboardEvent) {
87 + return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
88 }
\ No newline at end of file
src/cross.ts
+15 -1
@@ -234,4 +234,18 @@ export function isPrimitive(x: unknown): x is boolean | string | number | undefi
234
235 export function isIP(address: string) {
236 return /^([.:\da-f]+)$/i.test(address)
237 -}
\ No newline at end of file
237 +}
238 +
239 +export function isWindowsDrive(s?: string) {
240 + return s && /^[a-zA-Z]:$/.test(s)
241 +}
242 +
243 +export function isEqualLax(a: any,b: any): boolean {
244 + return a == b //eslint-disable-line
245 + || (a && b && typeof a === 'object' && typeof b === 'object'
246 + && Object.entries(a).every(([k,v]) => isEqualLax(v, b[k])) )
247 +}
248 +
249 +export function xlate(input: any, table: Record<string, any>) {
250 + return table[input] ?? input
251 +}
src/util-files.ts
+1 -5
@@ -1,7 +1,7 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import fs from 'fs/promises'
4 -import { Promisable, try_, tryJson, wait } from './misc'
4 +import { Promisable, try_, tryJson, wait, isWindowsDrive } from './misc'
5 import { promisify } from 'util'
6 import { createWriteStream, mkdirSync, watch } from 'fs'
7 import { basename, dirname } from 'path'
@@ -69,10 +69,6 @@ export function dirTraversal(s?: string) {
69 return s && /(^|[/\\])\.\.($|[/\\])/.test(s)
70 }
71
72 -export function isWindowsDrive(s?: string) {
73 - return s && /^[a-zA-Z]:$/.test(s)
74 -}
75 -
72 // apply this to paths that may contain \ as separator (not supported by fast-glob) and other special chars to be escaped (parenthesis)
73 export function adjustStaticPathForGlob(path: string) {
74 return glob.escapePath(path.replace(/\\/g, '/'))