@samitouri / QOSami-HFS / commits / ec36c5c0

admin/accounts: show as tree

Massimo Melina committed Feb 24, 2025 at 00:50 UTC ec36c5c0da38dbb1f3c4fee9ab24edca013b7c7a
3 files changed +42 -30
admin/src/AccountsPage.ts
+39 -29
@@ -1,24 +1,26 @@
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, useMemo } from "react"
3 +import { createElement as h, useState, useEffect, Fragment, useMemo, ReactNode } from "react"
4 import { apiCall, useApiEx } from './api'
5 import { Alert, Box, Card, CardContent, Grid, List, ListItem, ListItemText, Typography } from '@mui/material'
6 -import { Close, Delete, DoNotDisturb, Group, MilitaryTech, Person, PersonAdd, Schedule } from '@mui/icons-material'
6 +import {
7 + AccountTree, ChevronRight, Close, Delete, DoNotDisturb, ExpandMore, Group, MilitaryTech, Person, PersonAdd, Schedule
8 +} from '@mui/icons-material'
9 import { newDialog, with_, md } from './misc'
8 -import { Btn, Flex, IconBtn, iconTooltip, reloadBtn, useBreakpoint } from './mui'
10 +import { Btn, Flex, IconBtn, iconTooltip, reloadBtn, useBreakpoint, useToggleButton } from './mui'
11 import { TreeItem, TreeView } from '@mui/x-tree-view'
12 import MenuButton from './MenuButton'
13 import AccountForm from './AccountForm'
14 import _ from 'lodash'
15 import { alertDialog, confirmDialog, toast } from './dialog'
14 -import { useSnapState } from './state'
16 +import { state, useSnapState } from './state'
17 import { importAccountsCsv } from './importAccountsCsv'
18 import apiAccounts from '../../src/api.accounts'
19
20 export type Account = ReturnType<typeof apiAccounts.get_accounts>['list'][0]
21
22 export default function AccountsPage() {
21 - const { username } = useSnapState()
23 + const { username, accountsAsTree } = useSnapState()
24 const { data, reload, element } = useApiEx<typeof apiAccounts.get_accounts>('get_accounts')
25 const [sel, setSel] = useState<string[] | 'new-group' | 'new-user'>([])
26 const selectionMode = Array.isArray(sel)
@@ -73,6 +75,8 @@ export default function AccountsPage() {
75 }, [isSideBreakpoint, sel, selectedAccount])
76
77 const scrollProps = { height: '100%', display: 'flex', flexDirection: 'column', overflow: 'auto' } as const
78 + const [showTree, showTreeBtn] = useToggleButton("Show tree", "Show list", () => ({ icon: AccountTree }), accountsAsTree)
79 + state.accountsAsTree = showTree
80 return element || h(Grid, { container: true, rowSpacing: 1, columnSpacing: 2, top: 0, flex: '1 1 auto', height: 0 },
81 h(Grid, { item: true, xs: 12, [sideBreakpoint]: 5, lg: 4, xl: 5, ...scrollProps },
82 h(Box, {
@@ -99,6 +103,7 @@ export default function AccountsPage() {
103 ]
104 }, "Add"),
105 reloadBtn(reload),
106 + showTreeBtn,
107 list?.length! > 0 && h(Typography, { p: 1 }, `${list!.length} account(s)`),
108 ),
109 !list?.length && h(Alert, { severity: 'info' }, md`To access administration <u>remotely</u> you will need to create a user account with admin permission`),
@@ -106,34 +111,38 @@ export default function AccountsPage() {
111 multiSelect: true,
112 sx: { pr: 4, pb: 2, minWidth: '15em' },
113 selected: selectionMode ? sel : [],
114 + defaultCollapseIcon: h(ExpandMore),
115 + defaultExpandIcon: h(ChevronRight),
116 onNodeSelect(ev, ids) {
110 - setSel(ids)
117 + if (!(ev.target as any)?.closest?.('.MuiTreeItem-iconContainer')) // don't select if clicked the expansion button, mostly for mobile users
118 + setSel(ids)
119 }
120 },
113 - list?.map(ac =>
114 - h(TreeItem, {
115 - key: ac.username,
116 - nodeId: ac.username,
117 - label: h(Box, {
118 - sx: {
119 - display: 'flex',
120 - flexWrap: 'wrap',
121 - padding: '.2em 0',
122 - columnGap: '.5em',
123 - alignItems: 'center',
124 - }
125 - },
126 - account2icon(ac),
127 - (ac.disabled || ac.canLogin === false)
121 + list && (function recur(thisLevel): ReactNode {
122 + return thisLevel.map(ac =>
123 + h(TreeItem, {
124 + key: ac.username,
125 + nodeId: ac.username,
126 + label: h(Box, {
127 + sx: {
128 + display: 'flex',
129 + flexWrap: 'wrap',
130 + padding: '.2em 0',
131 + columnGap: '.5em',
132 + alignItems: 'center',
133 + }
134 + },
135 + account2icon(ac),
136 + (ac.disabled || ac.canLogin === false)
137 && iconTooltip(DoNotDisturb, ac.disabled ? "Disabled" : "Disabled by its groups", ac.disabled ? undefined : { color: 'text.secondary' }),
129 - (ac.expire || ac.days_to_live) && h(Schedule),
130 - ac.adminActualAccess && iconTooltip(MilitaryTech, "Can login into Admin"),
131 - ac.username,
132 - Boolean(ac.belongs?.length) && h(Box, { sx: { color: 'text.secondary', fontSize: 'small' } },
133 - '(', ac.belongs?.join(', '), ')')
134 - ),
135 - })
136 - )
138 + (ac.expire || ac.days_to_live) && h(Schedule),
139 + ac.adminActualAccess && iconTooltip(MilitaryTech, "Can login into Admin"),
140 + ac.username,
141 + Boolean(ac.belongs?.length) && h(Box, { sx: { color: 'text.secondary', fontSize: 'small' } },
142 + '(', ac.belongs?.join(', '), ')')
143 + ),
144 + }, showTree && recur(list.filter(x => ac.directMembers?.includes(x.username)))))
145 + })(showTree ? list.filter(ac => !list.some(x => x.members?.includes(ac.username))) : list)
146 )
147 ),
148 isSideBreakpoint && sideContent && h(Grid, { item: true, [sideBreakpoint]: true, maxWidth: '100%', ...scrollProps },
@@ -150,6 +159,7 @@ export default function AccountsPage() {
159 canLogin: true,
160 isGroup: false,
161 members: [],
162 + directMembers: [],
163 } satisfies Account
164 }
165
admin/src/state.ts
+2 -1
@@ -12,6 +12,7 @@ const INIT = {
12 title: '',
13 config: {} as Dict,
14 selectedFiles: [] as VfsNode[],
15 + accountsAsTree: false,
16 movingFile: '',
17 vfs: undefined as VfsNode | undefined,
18 loginRequired: false as boolean | number,
@@ -30,7 +31,7 @@ const INIT = {
31 Object.assign(INIT, JSON.parse(localStorage[STORAGE_KEY]||null))
32 export const state = proxy(INIT)
33
33 -const SETTINGS_TO_STORE: (keyof typeof state)[] = ['onlinePluginsColumns', 'monitorOnlyFiles', 'monitorWithLog', 'customHtmlSection', 'darkTheme', 'dataTablePersistence']
34 +const SETTINGS_TO_STORE: (keyof typeof state)[] = ['onlinePluginsColumns', 'monitorOnlyFiles', 'monitorWithLog', 'customHtmlSection', 'darkTheme', 'dataTablePersistence', 'accountsAsTree']
35 const storeSettings = _.debounce(() =>
36 localStorage[STORAGE_KEY] = JSON.stringify(_.pick(state, SETTINGS_TO_STORE)), 500, { maxWait: 1000 })
37 for (const k of SETTINGS_TO_STORE)
src/api.accounts.ts
+1
@@ -19,6 +19,7 @@ function prepareAccount(ac: Account | undefined) {
19 adminActualAccess: accountCanLoginAdmin(ac),
20 canLogin: accountHasPassword(ac) ? accountCanLogin(ac) : undefined,
21 invalidated: invalidateSessionBefore.get(ac.username),
22 + directMembers: Object.values(accountsConfig.get()).filter(a => a.belongs?.includes(ac.username)).map(x => x.username),
23 members: with_(Object.values(accountsConfig.get()), accounts => {
24 const ret = []
25 let news = [ac.username]