Compare commits

..

2 Commits

+95 -32
View File
@@ -1,6 +1,7 @@
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Chip from '@mui/material/Chip' import Chip from '@mui/material/Chip'
import Link from '@mui/material/Link'
import List from '@mui/material/List' import List from '@mui/material/List'
import ListItem from '@mui/material/ListItem' import ListItem from '@mui/material/ListItem'
import TextField from '@mui/material/TextField' import TextField from '@mui/material/TextField'
@@ -48,6 +49,7 @@ export default function AdminSSHServersPage() {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const [groupSearch, setGroupSearch] = useState('')
const [dialogOpen, setDialogOpen] = useState(false) const [dialogOpen, setDialogOpen] = useState(false)
const [editingID, setEditingID] = useState<string | null>(null) const [editingID, setEditingID] = useState<string | null>(null)
const [form, setForm] = useState<SSHServerFormState>(emptyForm()) const [form, setForm] = useState<SSHServerFormState>(emptyForm())
@@ -119,6 +121,39 @@ export default function AdminSSHServersPage() {
) )
}, [items, search]) }, [items, search])
const filteredGroups = useMemo(() => {
const needle: string = groupSearch.trim().toLowerCase()
if (!needle) {
return groups
}
return groups.filter((item: SSHServerGroup) =>
[
item.name,
item.id,
item.description,
(item.server_ids || []).join(' ')
]
.join(' ')
.toLowerCase()
.includes(needle)
)
}, [groups, groupSearch])
const groupsByServerID = useMemo(() => {
const next: Record<string, SSHServerGroup[]> = {}
groups.forEach((group: SSHServerGroup) => {
(group.server_ids || []).forEach((serverID: string) => {
if (!next[serverID]) {
next[serverID] = []
}
next[serverID].push(group)
})
})
return next
}, [groups])
const openCreate = () => { const openCreate = () => {
setEditingID(null) setEditingID(null)
setForm(emptyForm()) setForm(emptyForm())
@@ -444,36 +479,53 @@ export default function AdminSSHServersPage() {
</Typography> </Typography>
) : null} ) : null}
<List> <List>
{filteredItems.map((item) => ( {filteredItems.map((item) => {
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}> const serverGroups: SSHServerGroup[] = groupsByServerID[item.id] || []
<CompactListItemText
primary={ return (
<Typography> <ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
{item.name}({item.id}) <CompactListItemText
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)} primary={
</Typography> <Typography>
} {item.name}({item.id})
secondary={ {item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
<Typography variant="caption" color="text.secondary"> </Typography>
{item.host}:{item.port}· Tags: {(item.tags || []).join(', ') || '-'}· Updated: {fmt(item.updated_at)} }
{item.description? (<><br />{item.description}</>): null} secondary={
</Typography> <Typography variant="caption" color="text.secondary">
} {item.host}:{item.port}· Tags: {(item.tags || []).join(', ') || '-'}· Updated: {fmt(item.updated_at)}
/> <br />
<ListRowActions> Groups: {serverGroups.length ? serverGroups.map((group: SSHServerGroup, index: number) => (
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton> <Box key={group.id} component="span">
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton> {index > 0 ? ',' : ''}
<ListRowActionButton color="error" onClick={() => { <Link
setDeleteError(null) underline="hover"
setDeleteConfirm('') onClick={() => setViewGroupItem(group)}
setDeleteItem(item) sx={{ cursor: 'default' }}
}}> >
Delete {group.name}
</ListRowActionButton> </Link>
<ListRowActionButton onClick={() => void openHostKeys(item)}>Host Keys</ListRowActionButton> </Box>
</ListRowActions> )) : '-'}
</ListItem> {item.description? (<><br />{item.description}</>): null}
))} </Typography>
}
/>
<ListRowActions>
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
<ListRowActionButton color="error" onClick={() => {
setDeleteError(null)
setDeleteConfirm('')
setDeleteItem(item)
}}>
Delete
</ListRowActionButton>
<ListRowActionButton onClick={() => void openHostKeys(item)}>Host Keys</ListRowActionButton>
</ListRowActions>
</ListItem>
)
})}
</List> </List>
</Box> </Box>
</SectionCard> </SectionCard>
@@ -481,7 +533,18 @@ export default function AdminSSHServersPage() {
<SectionCard <SectionCard
collapsible collapsible
collapseStorageKey="admin-ssh-servers:ssh-server-groups" collapseStorageKey="admin-ssh-servers:ssh-server-groups"
title={`SSH Server Groups (${groups.length})`} title={
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
<Typography variant="h6">SSH Server Groups ({filteredGroups.length})</Typography>
<TextField
size="small"
label="Search"
value={groupSearch}
onChange={(event) => setGroupSearch(event.target.value)}
sx={{ minWidth: 240, maxWidth: 320 }}
/>
</Box>
}
actions={ actions={
<Button variant="outlined" onClick={openGroupCreate}> <Button variant="outlined" onClick={openGroupCreate}>
New Group New Group
@@ -489,7 +552,7 @@ export default function AdminSSHServersPage() {
} }
> >
<List> <List>
{groups.map((item) => ( {filteredGroups.map((item) => (
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}> <ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
<CompactListItemText <CompactListItemText
primary={ primary={