hfs 0.29.0 → 0.29.1-rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/admin/.DS_Store +0 -0
  2. package/admin/.eslintrc +8 -0
  3. package/admin/.gitignore +23 -0
  4. package/admin/index.html +1 -3
  5. package/admin/package.json +66 -0
  6. package/admin/{logo.svg → public/logo.svg} +0 -0
  7. package/admin/src/AccountForm.ts +95 -0
  8. package/admin/src/AccountsPage.ts +144 -0
  9. package/admin/src/App.ts +83 -0
  10. package/admin/src/ArrayField.ts +86 -0
  11. package/admin/src/ConfigPage.ts +314 -0
  12. package/admin/src/FileField.ts +54 -0
  13. package/admin/src/FileForm.ts +147 -0
  14. package/admin/src/FilePicker.ts +166 -0
  15. package/admin/src/HomePage.ts +96 -0
  16. package/admin/src/InstalledPlugins.ts +163 -0
  17. package/admin/src/LoginRequired.ts +82 -0
  18. package/admin/src/LogoutPage.ts +29 -0
  19. package/admin/src/LogsPage.ts +80 -0
  20. package/admin/src/MainMenu.ts +74 -0
  21. package/admin/src/MenuButton.ts +38 -0
  22. package/admin/src/MonitorPage.ts +205 -0
  23. package/admin/src/OnlinePlugins.ts +103 -0
  24. package/admin/src/PermField.ts +80 -0
  25. package/admin/src/PluginsPage.ts +27 -0
  26. package/admin/src/VfsMenuBar.ts +58 -0
  27. package/admin/src/VfsPage.ts +124 -0
  28. package/admin/src/VfsTree.ts +99 -0
  29. package/admin/src/addFiles.ts +59 -0
  30. package/admin/src/api.ts +250 -0
  31. package/admin/src/dialog.ts +203 -0
  32. package/admin/src/index.css +22 -0
  33. package/admin/src/index.ts +10 -0
  34. package/admin/src/md.ts +31 -0
  35. package/admin/src/misc.ts +141 -0
  36. package/admin/src/react-app-env.d.ts +1 -0
  37. package/admin/src/reportWebVitals.ts +15 -0
  38. package/admin/src/setupTests.ts +5 -0
  39. package/admin/src/state.ts +40 -0
  40. package/admin/src/theme.ts +37 -0
  41. package/admin/tsconfig.json +26 -0
  42. package/admin/vite.config.ts +32 -0
  43. package/frontend/.DS_Store +0 -0
  44. package/frontend/.eslintrc +8 -0
  45. package/frontend/.gitignore +23 -0
  46. package/frontend/index.html +1 -3
  47. package/frontend/package.json +50 -0
  48. package/frontend/{fontello.css → public/fontello.css} +0 -0
  49. package/frontend/{fontello.woff2 → public/fontello.woff2} +0 -0
  50. package/frontend/src/App.ts +25 -0
  51. package/frontend/src/Breadcrumbs.ts +43 -0
  52. package/frontend/src/BrowseFiles.ts +141 -0
  53. package/frontend/src/Head.ts +45 -0
  54. package/frontend/src/UserPanel.ts +57 -0
  55. package/frontend/src/api.ts +93 -0
  56. package/frontend/src/components.ts +55 -0
  57. package/frontend/src/dialog.css +83 -0
  58. package/frontend/src/dialog.ts +125 -0
  59. package/frontend/src/icons.ts +46 -0
  60. package/frontend/src/index.scss +324 -0
  61. package/frontend/src/index.ts +10 -0
  62. package/frontend/src/login.ts +63 -0
  63. package/frontend/src/menu.ts +187 -0
  64. package/frontend/src/misc.ts +54 -0
  65. package/frontend/src/options.ts +56 -0
  66. package/frontend/src/react-app-env.d.ts +1 -0
  67. package/frontend/src/reportWebVitals.ts +15 -0
  68. package/frontend/src/setupTests.ts +5 -0
  69. package/frontend/src/state.ts +84 -0
  70. package/frontend/src/upload.ts +306 -0
  71. package/frontend/src/useAuthorized.ts +19 -0
  72. package/frontend/src/useFetchList.ts +162 -0
  73. package/frontend/src/useTheme.ts +23 -0
  74. package/frontend/tsconfig.json +26 -0
  75. package/frontend/vite.config.ts +21 -0
  76. package/package.json +2 -2
  77. package/src/QuickZipStream.ts +279 -0
  78. package/src/ThrottledStream.ts +98 -0
  79. package/src/adminApis.ts +161 -0
  80. package/src/api.accounts.ts +78 -0
  81. package/src/api.auth.ts +142 -0
  82. package/src/api.file_list.ts +109 -0
  83. package/src/api.helpers.ts +30 -0
  84. package/src/api.monitor.ts +109 -0
  85. package/src/api.plugins.ts +141 -0
  86. package/src/api.vfs.ts +182 -0
  87. package/src/apiMiddleware.ts +131 -0
  88. package/src/block.ts +37 -0
  89. package/src/commands.ts +124 -0
  90. package/src/config.ts +167 -0
  91. package/src/connections.ts +60 -0
  92. package/src/const.ts +69 -0
  93. package/src/crypt.ts +16 -0
  94. package/src/debounceAsync.ts +53 -0
  95. package/src/events.ts +6 -0
  96. package/src/frontEndApis.ts +37 -0
  97. package/src/github.ts +107 -0
  98. package/src/index.ts +55 -0
  99. package/src/listen.ts +223 -0
  100. package/src/log.ts +128 -0
  101. package/src/middlewares.ts +183 -0
  102. package/src/misc.ts +176 -0
  103. package/src/pbkdf2.ts +83 -0
  104. package/src/perm.ts +197 -0
  105. package/src/plugins.ts +340 -0
  106. package/src/serveFile.ts +114 -0
  107. package/src/serveGuiFiles.ts +95 -0
  108. package/src/sse.ts +30 -0
  109. package/src/throttler.ts +106 -0
  110. package/src/update.ts +69 -0
  111. package/src/upload.ts +98 -0
  112. package/src/util-files.ts +141 -0
  113. package/src/util-generators.ts +31 -0
  114. package/src/util-http.ts +32 -0
  115. package/src/util-os.ts +38 -0
  116. package/src/vfs.ts +264 -0
  117. package/src/watchLoad.ts +75 -0
  118. package/src/zip.ts +71 -0
  119. package/admin/assets/index-cbb42a0e.js +0 -415
  120. package/admin/assets/index-f8049da8.css +0 -1
  121. package/admin/assets/sha512-3273321f.js +0 -8
  122. package/frontend/assets/index-72e96bb2.js +0 -85
  123. package/frontend/assets/index-cbcc6ac5.css +0 -1
  124. package/frontend/assets/sha512-2c2fa926.js +0 -8
  125. package/src/QuickZipStream.js +0 -262
  126. package/src/ThrottledStream.js +0 -93
  127. package/src/adminApis.js +0 -167
  128. package/src/api.accounts.js +0 -59
  129. package/src/api.auth.js +0 -132
  130. package/src/api.file_list.js +0 -110
  131. package/src/api.helpers.js +0 -32
  132. package/src/api.monitor.js +0 -104
  133. package/src/api.plugins.js +0 -128
  134. package/src/api.vfs.js +0 -162
  135. package/src/apiMiddleware.js +0 -127
  136. package/src/block.js +0 -34
  137. package/src/commands.js +0 -125
  138. package/src/config.js +0 -169
  139. package/src/connections.js +0 -57
  140. package/src/const.js +0 -95
  141. package/src/crypt.js +0 -21
  142. package/src/debounceAsync.js +0 -49
  143. package/src/events.js +0 -9
  144. package/src/frontEndApis.js +0 -59
  145. package/src/github.js +0 -106
  146. package/src/index.js +0 -58
  147. package/src/listen.js +0 -238
  148. package/src/log.js +0 -137
  149. package/src/middlewares.js +0 -181
  150. package/src/misc.js +0 -185
  151. package/src/pbkdf2.js +0 -74
  152. package/src/perm.js +0 -183
  153. package/src/plugins.js +0 -341
  154. package/src/serveFile.js +0 -107
  155. package/src/serveGuiFiles.js +0 -90
  156. package/src/sse.js +0 -30
  157. package/src/throttler.js +0 -91
  158. package/src/update.js +0 -70
  159. package/src/upload.js +0 -92
  160. package/src/util-files.js +0 -154
  161. package/src/util-generators.js +0 -31
  162. package/src/util-http.js +0 -32
  163. package/src/util-os.js +0 -41
  164. package/src/vfs.js +0 -237
  165. package/src/watchLoad.js +0 -73
  166. package/src/zip.js +0 -75
Binary file
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "react-app",
3
+ "rules": {
4
+ "no-mixed-operators": 0,
5
+ "no-ex-assign": 0,
6
+ "no-throw-literal": 0
7
+ }
8
+ }
@@ -0,0 +1,23 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # production
12
+ /build
13
+
14
+ # misc
15
+ .DS_Store
16
+ .env.local
17
+ .env.development.local
18
+ .env.test.local
19
+ .env.production.local
20
+
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
package/admin/index.html CHANGED
@@ -7,11 +7,9 @@
7
7
  <script async src='https://unpkg.com/node-forge@1.3.1/dist/forge.min.js'></script>
8
8
  <script>SESSION = _HFS_SESSION_</script>
9
9
  <title>HFS Admin</title>
10
- <script type="module" crossorigin src="/assets/index-cbb42a0e.js"></script>
11
- <link rel="stylesheet" href="/assets/index-f8049da8.css">
12
10
  </head>
13
11
  <body>
14
12
  <div id="root"></div>
15
-
13
+ <script type="module" src="/src/index.ts"></script>
16
14
  </body>
17
15
  </html>
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@hfs/admin",
3
+ "private": true,
4
+ "proxy": "http://localhost",
5
+ "scripts": {
6
+ "start": "vite",
7
+ "build": "tsc && vite build",
8
+ "preview": "vite preview",
9
+ "test-dep": "npm audit --production"
10
+ },
11
+ "dependencies": {
12
+ "@emotion/react": "^11.10.0",
13
+ "@hfs/mui-grid-form": "*",
14
+ "@hfs/shared": "*",
15
+ "@emotion/styled": "^11.10.0",
16
+ "@mui/icons-material": "^5.8.4",
17
+ "@mui/lab": "^5.0.0-alpha.94",
18
+ "@mui/material": "^5.10.0",
19
+ "@mui/x-data-grid": "^5.15.1",
20
+ "react": "^18.2.0",
21
+ "react-dom": "^18.2.0",
22
+ "react-router-dom": "^6.2.1",
23
+ "react-window": "^1.8.6",
24
+ "tssrp6a": "^3.0.0",
25
+ "valtio": "^1.2.9",
26
+ "immer": "^9.0.15",
27
+ "web-vitals": "^2.1.4"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^16.11.21",
31
+ "@types/react": "^18.0.15",
32
+ "@types/react-dom": "^18.0.6",
33
+ "@types/react-window": "^1.8.5",
34
+ "@types/react-virtualized-auto-sizer": "^1.0.1",
35
+ "vite": "^4.0.4",
36
+ "vite-plugin-babel-import": "github:rejetto/vite-plugin-babel-import"
37
+ },
38
+ "eslintConfig": {
39
+ "extends": [
40
+ "react-app",
41
+ "react-app/jest"
42
+ ],
43
+ "overrides": [
44
+ {
45
+ "files": [
46
+ "*.ts"
47
+ ],
48
+ "rules": {
49
+ "no-mixed-operators": "off"
50
+ }
51
+ }
52
+ ]
53
+ },
54
+ "browserslist": {
55
+ "production": [
56
+ ">0.2%",
57
+ "not dead",
58
+ "not op_mini all"
59
+ ],
60
+ "development": [
61
+ "last 1 chrome version",
62
+ "last 1 firefox version",
63
+ "last 1 safari version"
64
+ ]
65
+ }
66
+ }
File without changes
@@ -0,0 +1,95 @@
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, useEffect, useRef, useState } from 'react'
4
+ import { BoolField, Form, MultiSelectField } from '@hfs/mui-grid-form'
5
+ import { Box, Button } from '@mui/material'
6
+ import { apiCall } from './api'
7
+ import { alertDialog } from './dialog'
8
+ import { isEqualLax, modifiedSx } from './misc'
9
+ import { Account, account2icon } from './AccountsPage'
10
+ import { createVerifierAndSalt, SRPParameters, SRPRoutines } from 'tssrp6a'
11
+
12
+ interface FormProps { account: Account, groups: string[], done: (username: string)=>void, close: ()=>void }
13
+ export default function AccountForm({ account, done, groups, close }: FormProps) {
14
+ const [values, setValues] = useState<Account & { password?: string, password2?: string }>(account)
15
+ const [belongsOptions, setBelongOptions] = useState<string[]>([])
16
+ useEffect(() => {
17
+ setValues(account)
18
+ setBelongOptions(groups.filter(x => x !== account.username ))
19
+ ref.current?.querySelector('input')?.focus()
20
+ }, [JSON.stringify(account)]) //eslint-disable-line
21
+ const add = !account.username
22
+ const group = !values.hasPassword
23
+ const ref = useRef<HTMLFormElement>()
24
+ return h(Form, {
25
+ formRef: ref,
26
+ values,
27
+ set(v, k) {
28
+ setValues({ ...values, [k]: v })
29
+ },
30
+ addToBar: [
31
+ h(Button, { onClick: close, sx: { ml: 2 } }, "Close"),
32
+ h(Box, { flex:1 }),
33
+ account2icon(values, { fontSize: 'large', sx: { p: 1 }})
34
+ ],
35
+ fields: [
36
+ { k: 'username', label: group ? 'Group name' : undefined, autoComplete: 'off', required: true, xl: group ? 12 : 4,
37
+ getError: v => v !== account.username && apiCall('get_account', { username: v })
38
+ .then(got => got.username === account.username ? "usernames are case-insensitive" : "already used", () => false),
39
+ },
40
+ !group && { k: 'password', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', required: add,
41
+ label: add ? "Password" : "Change password"
42
+ },
43
+ !group && { k: 'password2', md: 6, xl: 4, type: 'password', autoComplete: 'new-password', label: 'Repeat password',
44
+ getError: (x, { values }) => (x||'') !== (values.password||'') && "Enter same password" },
45
+ { k: 'ignore_limits', comp: BoolField, xl: 6,
46
+ helperText: values.ignore_limits ? "Speed limits don't apply to this account" : "Speed limits apply to this account" },
47
+ { k: 'admin', comp: BoolField, xl: 6, fromField: (v:boolean) => v||null, label: "Permission to access Admin-panel",
48
+ helperText: "To access THIS interface you are using right now",
49
+ ...!account.admin && account.adminActualAccess && { value: true, helperText: "This permission is inherited" },
50
+ },
51
+ { k: 'belongs', comp: MultiSelectField, label: "Inherits from", options: belongsOptions,
52
+ helperText: "Specify groups to inherit permissions from."
53
+ + (belongsOptions.length ? '' : " There are no groups available, create one first.")
54
+ },
55
+ { k: 'redirect', helperText: "If you want this account to be redirected to a specific folder/address at login time" },
56
+ ],
57
+ onError: alertDialog,
58
+ save: {
59
+ sx: modifiedSx( !isEqualLax(values, account)),
60
+ async onClick() {
61
+ const { password='', password2, adminActualAccess, ...withoutPassword } = values
62
+ const { username } = values
63
+ if (add) {
64
+ const got = await apiCall('add_account', withoutPassword)
65
+ if (password)
66
+ try { await apiNewPassword(username, password) }
67
+ catch(e) {
68
+ apiCall('del_account', { username }).then() // best effort, don't wait
69
+ throw e
70
+ }
71
+ done(got.username)
72
+ return alertDialog("Account created", 'success')
73
+ }
74
+ const got = await apiCall('set_account', {
75
+ username: account.username,
76
+ changes: withoutPassword,
77
+ })
78
+ if (password)
79
+ await apiNewPassword(username, password)
80
+ done(got.username)
81
+ return alertDialog("Account modified", 'success')
82
+ }
83
+ }
84
+ })
85
+ }
86
+
87
+ async function apiNewPassword(username: string, password: string) {
88
+ const srp6aNimbusRoutines = new SRPRoutines(new SRPParameters())
89
+ const res = await createVerifierAndSalt(srp6aNimbusRoutines, username, password)
90
+ return apiCall('change_srp_others', { username, salt: String(res.s), verifier: String(res.v) }).catch(e => {
91
+ if (e.code !== 406) // 406 = server was configured to support clear text authentication
92
+ throw e
93
+ return apiCall('change_password_others', { username, newPassword: password }) // unencrypted version
94
+ })
95
+ }
@@ -0,0 +1,144 @@
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, useState, useEffect, Fragment } from "react"
4
+ import { apiCall, useApiEx } from './api'
5
+ import { Alert, Box, Button, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6
+ import { Delete, Group, MilitaryTech, Person, PersonAdd, Refresh } from '@mui/icons-material'
7
+ import { alertDialog, confirmDialog } from './dialog'
8
+ import { iconTooltip, onlyTruthy } from './misc'
9
+ import { TreeItem, TreeView } from '@mui/lab'
10
+ import MenuButton from './MenuButton'
11
+ import AccountForm from './AccountForm'
12
+ import md from './md'
13
+
14
+ export interface Account {
15
+ username: string
16
+ hasPassword?: boolean
17
+ admin?: boolean
18
+ adminActualAccess?: boolean
19
+ ignore_limits?: boolean
20
+ redirect?: string
21
+ belongs?: string[]
22
+ }
23
+
24
+ export default function AccountsPage() {
25
+ const { data, reload, element } = useApiEx('get_accounts')
26
+ const [sel, setSel] = useState<string[] | 'new-group' | 'new-user'>([])
27
+ const selectionMode = Array.isArray(sel)
28
+ useEffect(() => { // if accounts are reloaded, review the selection to remove elements that don't exist anymore
29
+ if (Array.isArray(data?.list) && selectionMode)
30
+ setSel( sel.filter(u => data.list.find((e:any) => e?.username === u)) ) // remove elements that don't exist anymore
31
+ }, [data]) //eslint-disable-line -- Don't fall for its suggestion to add `sel` here: we modify it and declaring it as a dependency would cause a logical loop
32
+ if (element)
33
+ return element
34
+ const { list }: { list: Account[] } = data
35
+ return h(Grid, { container: true, maxWidth: '80em' },
36
+ h(Grid, { item: true, xs: 12 },
37
+ h(Box, {
38
+ display: 'flex',
39
+ flexWrap: 'wrap',
40
+ gap: 2,
41
+ mb: 2,
42
+ sx: {
43
+ position: 'sticky',
44
+ top: 0,
45
+ zIndex: 2,
46
+ backgroundColor: 'background.paper',
47
+ width: 'fit-content',
48
+ },
49
+ },
50
+ h(MenuButton, {
51
+ variant: 'contained',
52
+ startIcon: h(PersonAdd),
53
+ items: [
54
+ { children: "user", onClick: () => setSel('new-user') },
55
+ { children: "group", onClick: () => setSel('new-group') }
56
+ ]
57
+ }, 'Add'),
58
+ h(Button, {
59
+ disabled: !selectionMode || !sel.length,
60
+ startIcon: h(Delete),
61
+ async onClick(){
62
+ if (!selectionMode) return
63
+ if (!await confirmDialog(`You are going to delete ${sel.length} account(s)`))
64
+ return
65
+ const errors = onlyTruthy(await Promise.all(sel.map(username =>
66
+ apiCall('del_account', { username }).then(() => null, () => username) )))
67
+ if (errors.length)
68
+ return alertDialog(errors.length === sel.length ? "Request failed" : hList("Some accounts were not deleted", errors), 'error')
69
+ reload()
70
+ }
71
+ }, "Remove"),
72
+ h(Button, { onClick: reload, startIcon: h(Refresh) }, "Reload"),
73
+ list.length > 0 && h(Typography, { p: 1 }, `${list.length} account(s)`),
74
+ ) ),
75
+ h(Grid, { item: true, md: 5 },
76
+ !list.length && h(Alert, { severity: 'info' }, md`To access administration _remotely_ you will need to create a user account with admin permission`),
77
+ h(TreeView, {
78
+ multiSelect: true,
79
+ sx: { pr: 4, pb: 2, minWidth: '15em' },
80
+ selected: selectionMode ? sel : [],
81
+ onNodeSelect(ev, ids) {
82
+ setSel(ids)
83
+ }
84
+ },
85
+ list.map((ac: Account) =>
86
+ h(TreeItem, {
87
+ key: ac.username,
88
+ nodeId: ac.username,
89
+ label: h(Box, {
90
+ sx: {
91
+ display: 'flex',
92
+ flexWrap: 'wrap',
93
+ padding: '.2em 0',
94
+ gap: '.5em',
95
+ alignItems: 'center',
96
+ }
97
+ },
98
+ account2icon(ac),
99
+ ac.adminActualAccess && iconTooltip(MilitaryTech, "Can login into Admin"),
100
+ ac.username,
101
+ Boolean(ac.belongs?.length) && h(Box, { sx: { color: 'text.secondary', fontSize: 'small' } },
102
+ '(', ac.belongs?.join(', '), ')')
103
+ ),
104
+ })
105
+ )
106
+ )
107
+ ),
108
+ sel.length > 0 // this clever test is true both when some accounts are selected and when we are in "new account" modes
109
+ && h(Grid, { item: true, md: 7 },
110
+ h(Card, {},
111
+ h(CardContent, {},
112
+ selectionMode && sel.length > 1 ? h(Box, {},
113
+ h(Typography, {}, sel.length + " selected"),
114
+ h(List, {},
115
+ sel.map(username =>
116
+ h(ListItem, { key: username },
117
+ h(ListItemText, {}, username))))
118
+ ) : h(AccountForm, {
119
+ account: selectionMode && list.find(x => x.username === sel[0])
120
+ || { username: '', hasPassword: sel === 'new-user' },
121
+ groups: list.filter(x => !x.hasPassword).map( x => x.username ),
122
+ close(){ setSel([]) },
123
+ done(username) {
124
+ setSel([username])
125
+ reload()
126
+ }
127
+ })
128
+ )))
129
+ )
130
+ }
131
+
132
+ function hList(heading: string, list: any[]) {
133
+ return h(Fragment, {},
134
+ heading>'' && h(Typography, {}, heading),
135
+ h(List, {},
136
+ list.map((text,key) =>
137
+ h(ListItem, { key },
138
+ typeof text === 'string' ? h(ListItemText, {}, text) : text) ))
139
+ )
140
+ }
141
+
142
+ export function account2icon(ac: Account, props={}) {
143
+ return h(ac.hasPassword ? Person : Group, props)
144
+ }
@@ -0,0 +1,83 @@
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, useState } from 'react'
4
+ import { HashRouter, Routes, Route, useLocation } from 'react-router-dom'
5
+ import MainMenu, { getMenuLabel, mainMenu } from './MainMenu'
6
+ import { AppBar, Box, Drawer, IconButton, ThemeProvider, Toolbar, Typography } from '@mui/material'
7
+ import { Dialogs } from './dialog'
8
+ import { useMyTheme } from './theme'
9
+ import { useBreakpoint} from './misc'
10
+ import { LoginRequired } from './LoginRequired'
11
+ import { Menu } from '@mui/icons-material'
12
+
13
+ function App() {
14
+ return h(ThemeProvider, { theme: useMyTheme() },
15
+ h(ApplyTheme, {},
16
+ h(LoginRequired, {},
17
+ h(HashRouter, {}, h(Routed)) ) ) )
18
+ }
19
+
20
+ function ApplyTheme(props:any) {
21
+ return h(Box, {
22
+ sx: {
23
+ bgcolor: 'background.default', color: 'text.primary',
24
+ display: 'flex', flexDirection: 'column',
25
+ minHeight: '100%', flex: 1,
26
+ },
27
+ ...props
28
+ })
29
+ }
30
+
31
+ function Routed() {
32
+ const loc = useLocation().pathname.slice(1)
33
+ const current = mainMenu.find(x => x.path === loc)
34
+ const title = current && (current.title || getMenuLabel(current))
35
+ const [open, setOpen] = useState(false)
36
+ const large = useBreakpoint('lg')
37
+ return h(Fragment, {},
38
+ !large && h(StickyBar, { title, openMenu: () => setOpen(true) }),
39
+ !large && h(Drawer, { anchor:'left', open, onClose(){ setOpen(false) } },
40
+ h(MainMenu, {
41
+ onSelect: () => setOpen(false)
42
+ })),
43
+ h(Box, { display: 'flex', flex: 1, }, // horizontal layout for menu-content
44
+ large && h(MainMenu),
45
+ h(Box, {
46
+ component: 'main',
47
+ sx: {
48
+ background: 'url(logo.svg) no-repeat right fixed',
49
+ backgroundSize: 'contain',
50
+ px: { xs: 2, md: 3 },
51
+ pb: '1em',
52
+ position: 'relative',
53
+ display: 'flex',
54
+ flexDirection: 'column',
55
+ width: '100%',
56
+ }
57
+ },
58
+ title && large && h(Typography, { variant:'h2', mb:2 }, title),
59
+ h(Routes, {}, mainMenu.map((it,idx) =>
60
+ h(Route, { key: idx, path: it.path, element: h(it.comp) })) )
61
+ ),
62
+ h(Dialogs)
63
+ )
64
+ )
65
+ }
66
+
67
+ function StickyBar({ title, openMenu }: { title?: string, openMenu: ()=>void }) {
68
+ return h(AppBar, { position: 'sticky', sx: { mb: 2 } },
69
+ h(Toolbar, {},
70
+ h(IconButton, {
71
+ size: 'large',
72
+ edge: 'start',
73
+ color: 'inherit',
74
+ sx: { mr: 2 },
75
+ 'aria-label': "menu",
76
+ onClick: openMenu
77
+ }, h(Menu)),
78
+ title,
79
+ )
80
+ )
81
+ }
82
+
83
+ export default App
@@ -0,0 +1,86 @@
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, useMemo } from 'react'
4
+ import { IconBtn, setHidden } from './misc'
5
+ import { Add, Edit, Delete } from '@mui/icons-material'
6
+ import { confirmDialog, formDialog } from './dialog'
7
+ import { DataGrid, GridAlignment } from '@mui/x-data-grid'
8
+ import { FieldDescriptor, FieldProps, labelFromKey } from '@hfs/mui-grid-form'
9
+ import { Box, FormHelperText, FormLabel } from '@mui/material'
10
+
11
+ export function ArrayField<T=any>({ label, helperText, fields, value, onChange, onError, getApi, ...rest }: FieldProps<T[]> & { fields: FieldDescriptor[], height?: number }) {
12
+ const rows = useMemo(() => (value||[]).map((x,$idx) =>
13
+ setHidden({ ...x } as any, 'id' in x ? { $idx } : { id: $idx })),
14
+ [JSON.stringify(value)]) //eslint-disable-line
15
+ const columns = useMemo(() => {
16
+ return [
17
+ ...fields.map(f => ({
18
+ field: f.k,
19
+ headerName: f.headerName ?? (typeof f.label === 'string' ? f.label : labelFromKey(f.k)),
20
+ disableColumnMenu: true,
21
+ ...f.$width >= 8 ? { width: f.$width } : { flex: f.$width || 1 },
22
+ ...f.$column,
23
+ })),
24
+ {
25
+ field: '',
26
+ width: 80,
27
+ disableColumnMenu: true,
28
+ sortable: false,
29
+ align: 'center' as GridAlignment,
30
+ headerAlign: 'center' as GridAlignment,
31
+ renderHeader(){
32
+ return h(IconBtn, {
33
+ icon: Add,
34
+ title: "Add",
35
+ onClick: (event:any) =>
36
+ formDialog({ fields }).then(o => // @ts-ignore
37
+ o && onChange([...value||[], o], { was: value, event }))
38
+ })
39
+ },
40
+ renderCell({ row }: any) {
41
+ const { $idx=row.id } = row
42
+ return h('div', {},
43
+ h(IconBtn, {
44
+ icon: Edit,
45
+ title: "Modify",
46
+ onClick: (event:any) =>
47
+ formDialog({ fields, values: row }).then(newRec => {
48
+ if (!newRec) return
49
+ const newValue = value!.map((oldRec, i) => i === $idx ? newRec : oldRec)
50
+ onChange(newValue, { was: value, event })
51
+ }),
52
+ }),
53
+ h(IconBtn, {
54
+ icon: Delete,
55
+ title: "Delete",
56
+ onClick: (event:any) =>
57
+ confirmDialog("Delete?").then(ok => {
58
+ if (!ok) return
59
+ const newValue = value!.filter((rec, i) => i !== $idx)
60
+ onChange(newValue, { was: value, event })
61
+ }),
62
+ }),
63
+ )
64
+ }
65
+ }
66
+ ]
67
+ }, [fields, value, onChange])
68
+ return h(Fragment, {},
69
+ label && h(FormLabel, { sx: { ml: 1 } }, label),
70
+ helperText && h(FormHelperText, {}, helperText),
71
+ h(Box, { height: '20em', ...rest },
72
+ h(DataGrid, {
73
+ columns,
74
+ rows,
75
+ hideFooterSelectedRowCount: true,
76
+ hideFooter: true,
77
+ componentsProps: {
78
+ pagination: {
79
+ showFirstButton: true,
80
+ showLastButton: true,
81
+ }
82
+ },
83
+ })
84
+ )
85
+ )
86
+ }