Compare commits
4 Commits
001f0487e2
...
16295f34bb
| Author | SHA1 | Date | |
|---|---|---|---|
| 16295f34bb | |||
| 0a04381a28 | |||
| 1317afb6ea | |||
| f4395ce5d2 |
@@ -367,6 +367,7 @@ CREATE TABLE IF NOT EXISTS ssh_servers (
|
|||||||
description TEXT NOT NULL DEFAULT '',
|
description TEXT NOT NULL DEFAULT '',
|
||||||
tags_json TEXT NOT NULL DEFAULT '[]',
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||||
enabled INTEGER NOT NULL DEFAULT 1,
|
enabled INTEGER NOT NULL DEFAULT 1,
|
||||||
|
host_key_policy TEXT NOT NULL DEFAULT 'strict',
|
||||||
created_by_kind TEXT NOT NULL DEFAULT 'user',
|
created_by_kind TEXT NOT NULL DEFAULT 'user',
|
||||||
created_by_subject_id TEXT NOT NULL DEFAULT '',
|
created_by_subject_id TEXT NOT NULL DEFAULT '',
|
||||||
created_by_subject_name TEXT NOT NULL DEFAULT '',
|
created_by_subject_name TEXT NOT NULL DEFAULT '',
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE ssh_servers ADD COLUMN host_key_policy TEXT NOT NULL DEFAULT 'strict';
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import ListItemText from '@mui/material/ListItemText'
|
||||||
|
import type { ListItemTextProps } from '@mui/material/ListItemText'
|
||||||
|
import type { SxProps, Theme } from '@mui/material/styles'
|
||||||
|
|
||||||
|
const compactSx: SxProps<Theme> = { minWidth: 0, m: 0 }
|
||||||
|
|
||||||
|
export default function CompactListItemText(props: ListItemTextProps) {
|
||||||
|
const sx: SxProps<Theme> = (props.sx ? [compactSx, props.sx] : compactSx) as SxProps<Theme>
|
||||||
|
|
||||||
|
return <ListItemText {...props} sx={sx} />
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import FormControlLabel from '@mui/material/FormControlLabel'
|
|||||||
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 ListItemIcon from '@mui/material/ListItemIcon'
|
import ListItemIcon from '@mui/material/ListItemIcon'
|
||||||
import ListItemText from '@mui/material/ListItemText'
|
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import FormDialogContent from './FormDialogContent'
|
import FormDialogContent from './FormDialogContent'
|
||||||
import { SSHAccessProfile, SSHServer } from '../api'
|
import { SSHAccessProfile, SSHServer } from '../api'
|
||||||
|
import CompactListItemText from './CompactListItemText'
|
||||||
|
|
||||||
type SSHServerGroupConnectDialogProps = {
|
type SSHServerGroupConnectDialogProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -120,11 +120,10 @@ export default function SSHServerGroupConnectDialog(props: SSHServerGroupConnect
|
|||||||
sx={{ p: 0.5 }}
|
sx={{ p: 0.5 }}
|
||||||
/>
|
/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={item.name}
|
primary={item.name}
|
||||||
secondary={`${item.host}:${item.port}`}
|
secondary={`${item.host}:${item.port}`}
|
||||||
secondaryTypographyProps={{ variant: 'caption' }}
|
secondaryTypographyProps={{ variant: 'caption' }}
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ import DialogActions from '@mui/material/DialogActions'
|
|||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
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 ListItemText from '@mui/material/ListItemText'
|
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import FormDialogContent from './FormDialogContent'
|
import FormDialogContent from './FormDialogContent'
|
||||||
import { SSHServer, SSHServerGroup } from '../api'
|
import { SSHServer, SSHServerGroup } from '../api'
|
||||||
|
import CompactListItemText from './CompactListItemText'
|
||||||
|
|
||||||
type SSHServerGroupDetailsDialogProps = {
|
type SSHServerGroupDetailsDialogProps = {
|
||||||
item: SSHServerGroup | null
|
item: SSHServerGroup | null
|
||||||
@@ -61,7 +61,7 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
|||||||
<List disablePadding>
|
<List disablePadding>
|
||||||
{memberServers.map((server: SSHServer) => (
|
{memberServers.map((server: SSHServer) => (
|
||||||
<ListItem key={server.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={server.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{server.name}
|
{server.name}
|
||||||
@@ -69,7 +69,6 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={`${server.host}:${server.port}${server.description ? ` · ${server.description}` : ''}`}
|
secondary={`${server.host}:${server.port}${server.description ? ` · ${server.description}` : ''}`}
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
|
||||||
/>
|
/>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
List,
|
List,
|
||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
@@ -19,6 +18,7 @@ import { ListRowActionButton, ListRowActions } from '../components/ListRowAction
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import { AdminAPIKey, api } from '../api'
|
import { AdminAPIKey, api } from '../api'
|
||||||
import SelectField from '../components/SelectField'
|
import SelectField from '../components/SelectField'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function formatUnix(value: number) {
|
function formatUnix(value: number) {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
@@ -203,7 +203,7 @@ export default function AdminApiKeysPage() {
|
|||||||
indeterminate={selected.length > 0 && selected.length < keys.length}
|
indeterminate={selected.length > 0 && selected.length < keys.length}
|
||||||
onChange={(event) => handleSelectAll(event.target.checked)}
|
onChange={(event) => handleSelectAll(event.target.checked)}
|
||||||
/>
|
/>
|
||||||
<ListItemText primary="Select all" />
|
<CompactListItemText primary="Select all" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
{keys.map((key) => (
|
{keys.map((key) => (
|
||||||
@@ -222,15 +222,23 @@ export default function AdminApiKeysPage() {
|
|||||||
/>
|
/>
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{key.name}({key.prefix})
|
{key.name}({key.prefix})
|
||||||
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={`${renderSubject(key)}${key.email ? ` | ${key.email}` : ''} | Created: ${formatUnix(key.created_at)} | Last used: ${formatUnix(key.last_used_at)} | Expires: ${formatUnix(key.expires_at)}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Box sx={{ display: "grid" }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{renderSubject(key)}{key.email ? ` · ${key.email}` : ''}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
Created: {formatUnix(key.created_at)} · Last used: {formatUnix(key.last_used_at)} · Expires: {formatUnix(key.expires_at)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton
|
<ListRowActionButton
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -21,6 +20,7 @@ import PKICADetailsDialog from '../components/PKICADetailsDialog'
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import { api, PKICA, PKICADetail, PKIClientProfile, PKIClientProfileUpsertPayload, User, UserGroup } from '../api'
|
import { api, PKICA, PKICADetail, PKIClientProfile, PKIClientProfileUpsertPayload, User, UserGroup } from '../api'
|
||||||
import { AppBrand, useBrand } from '../branding'
|
import { AppBrand, useBrand } from '../branding'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function fmt(value: number): string {
|
function fmt(value: number): string {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
@@ -289,7 +289,7 @@ export default function AdminPKIClientProfilesPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
@@ -297,7 +297,7 @@ export default function AdminPKIClientProfilesPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
<>
|
<Typography variant="caption" color="text.secondary">
|
||||||
CA:{' '}
|
CA:{' '}
|
||||||
<Link
|
<Link
|
||||||
underline="hover"
|
underline="hover"
|
||||||
@@ -308,9 +308,8 @@ export default function AdminPKIClientProfilesPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
{' '}
|
{' '}
|
||||||
· server auth: {item.allow_server_auth ? 'yes' : 'no'} · perms: {(item.authz_permissions || []).join(', ') || '-'} · scope: {item.authz_scope || '-'} · validity: {item.default_valid_seconds}s / max {item.max_valid_seconds}s · targets: {item.targets.length} · updated: {fmt(item.updated_at)}
|
· server auth: {item.allow_server_auth ? 'yes' : 'no'} · perms: {(item.authz_permissions || []).join(', ') || '-'} · scope: {item.authz_scope || '-'} · validity: {item.default_valid_seconds}s / max {item.max_valid_seconds}s · targets: {item.targets.length} · updated: {fmt(item.updated_at)}
|
||||||
</>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => openView(item)}>
|
<ListRowActionButton onClick={() => openView(item)}>
|
||||||
@@ -335,7 +334,7 @@ export default function AdminPKIClientProfilesPage() {
|
|||||||
))}
|
))}
|
||||||
{!loading && items.length === 0 ? (
|
{!loading && items.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No client certificate profiles." />
|
<CompactListItemText primary="No client certificate profiles." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -28,6 +27,7 @@ import { PKICertImportDialog, PKICertIssueDialog, PKICertRevokeDialog } from '..
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import { ACMEOrder, ACMEOrderCreatePayload, ACMEProfile, ACMEProfileUpsertPayload, api, PKICA, PKICADetail, PKICert, PKICertDetail, PKICertImportPayload, PKICertIssuePayload, PKIIntermediateCARequest, PKIRootCARequest } from '../api'
|
import { ACMEOrder, ACMEOrderCreatePayload, ACMEProfile, ACMEProfileUpsertPayload, api, PKICA, PKICADetail, PKICert, PKICertDetail, PKICertImportPayload, PKICertIssuePayload, PKIIntermediateCARequest, PKIRootCARequest } from '../api'
|
||||||
import SelectField from '../components/SelectField'
|
import SelectField from '../components/SelectField'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function fmt(ts: number): string {
|
function fmt(ts: number): string {
|
||||||
if (!ts || ts <= 0) {
|
if (!ts || ts <= 0) {
|
||||||
@@ -723,9 +723,9 @@ export default function AdminPKIPage() {
|
|||||||
{filteredCAs.map((ca) => (
|
{filteredCAs.map((ca) => (
|
||||||
<ListItem key={ca.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={ca.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={(
|
primary={(
|
||||||
<Box component="span">
|
<Typography>
|
||||||
<Tooltip title={ca.id} arrow>
|
<Tooltip title={ca.id} arrow>
|
||||||
<Link
|
<Link
|
||||||
underline="hover"
|
underline="hover"
|
||||||
@@ -736,10 +736,13 @@ export default function AdminPKIPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{` (${ca.id})`}
|
{` (${ca.id})`}
|
||||||
</Box>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
secondary={`${ca.is_root ? 'root' : 'intermediate'} · status: ${ca.status} · parent: ${ca.parent_ca_id || '-'} · updated: ${fmt(ca.updated_at)}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{ca.is_root ? 'root' : 'intermediate'} · status: {ca.status} · parent: {ca.parent_ca_id || '-'} · updated: {fmt(ca.updated_at)}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton
|
<ListRowActionButton
|
||||||
@@ -776,7 +779,7 @@ export default function AdminPKIPage() {
|
|||||||
))}
|
))}
|
||||||
{filteredCAs.length === 0 ? (
|
{filteredCAs.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No certificate authorities found." />
|
<CompactListItemText primary="No certificate authorities found." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
@@ -861,10 +864,10 @@ export default function AdminPKIPage() {
|
|||||||
{filteredCerts.map((cert) => (
|
{filteredCerts.map((cert) => (
|
||||||
<ListItem key={cert.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={cert.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={`${cert.common_name} (${cert.id})`}
|
primary={<Typography>{cert.common_name} ({cert.id})</Typography>}
|
||||||
secondary={(
|
secondary={(
|
||||||
<Box component="span">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{`serial: ${cert.serial_hex} · ca: `}
|
{`serial: ${cert.serial_hex} · ca: `}
|
||||||
{cert.ca_id ? (
|
{cert.ca_id ? (
|
||||||
<Tooltip title={cert.ca_id} arrow>
|
<Tooltip title={cert.ca_id} arrow>
|
||||||
@@ -880,9 +883,8 @@ export default function AdminPKIPage() {
|
|||||||
'standalone'
|
'standalone'
|
||||||
)}
|
)}
|
||||||
{` · source: ${fmtPKICertSource(cert)} · actor: ${fmtPKICertActor(cert)} · status: ${cert.status} · valid: ${fmt(cert.not_before)} ~ ${fmt(cert.not_after)}${cert.revoked_at ? ` · revoked: ${fmt(cert.revoked_at)}` : ''}`}
|
{` · source: ${fmtPKICertSource(cert)} · actor: ${fmtPKICertActor(cert)} · status: ${cert.status} · valid: ${fmt(cert.not_before)} ~ ${fmt(cert.not_after)}${cert.revoked_at ? ` · revoked: ${fmt(cert.revoked_at)}` : ''}`}
|
||||||
</Box>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => openCertView(cert.id)}>
|
<ListRowActionButton onClick={() => openCertView(cert.id)}>
|
||||||
@@ -911,7 +913,7 @@ export default function AdminPKIPage() {
|
|||||||
))}
|
))}
|
||||||
{filteredCerts.length === 0 ? (
|
{filteredCerts.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No issued certificates found." />
|
<CompactListItemText primary="No issued certificates found." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
@@ -931,10 +933,24 @@ export default function AdminPKIPage() {
|
|||||||
{acmeProfiles.map((item) => (
|
{acmeProfiles.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={`${item.name} (${item.id})`}
|
primary={<Typography>{item.name} ({item.id})</Typography>}
|
||||||
secondary={`${item.directory_url} · solver: ${item.solver_type === 'acme_dns' ? `acme-dns (${item.acme_dns_full_domain || '-'})` : 'manual'} · email: ${item.email || '-'} · account: ${item.account_url || '-'} · enabled: ${item.enabled ? 'yes' : 'no'}${item.last_error ? ` · last error: ${item.last_error}` : ''}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Box sx={{ display: "grid" }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{item.directory_url} · solver: {item.solver_type === 'acme_dns' ? `acme-dns (${item.acme_dns_full_domain || '-'})` : 'manual'} ·
|
||||||
|
email: {item.email || '-'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
account: {item.account_url || '-'} · enabled: {item.enabled ? 'yes' : 'no'}
|
||||||
|
</Typography>
|
||||||
|
{item.last_error? (
|
||||||
|
<Typography variant="caption" color="text.secondary">last error: {
|
||||||
|
<Typography variant="caption" color="error.main">{item.last_error}</Typography>
|
||||||
|
}</Typography>
|
||||||
|
) : ''}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => openEditACME(item)}>
|
<ListRowActionButton onClick={() => openEditACME(item)}>
|
||||||
@@ -1021,10 +1037,22 @@ export default function AdminPKIPage() {
|
|||||||
{acmeOrders.map((item) => (
|
{acmeOrders.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={`${item.common_name} (${item.id})`}
|
primary={
|
||||||
secondary={`profile: ${item.profile_id} · status: ${item.status} · cert: ${item.cert_id || '-'} · created: ${fmt(item.created_at)}${item.last_error ? ` · error: ${item.last_error}` : ''}`}
|
<Typography>{item.common_name} ({item.id})</Typography>
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
}
|
||||||
|
secondary={
|
||||||
|
<Box sx={{ display: "grid" }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
profile: {item.profile_id} · status: {item.status} · cert: {item.cert_id || '-'} · created: {fmt(item.created_at)}
|
||||||
|
</Typography>
|
||||||
|
{item.last_error? (
|
||||||
|
<Typography variant="caption" color="text.secondary">last error: {
|
||||||
|
<Typography variant="caption" color="error.main">{item.last_error}</Typography>
|
||||||
|
}</Typography>
|
||||||
|
) : ''}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setACMEOrderView(item)}>
|
<ListRowActionButton onClick={() => setACMEOrderView(item)}>
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ import Dialog from '@mui/material/Dialog'
|
|||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Link from '@mui/material/Link'
|
import Link from '@mui/material/Link'
|
||||||
import Paper from '@mui/material/Paper'
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||||
import FormDialogContent from '../components/FormDialogContent'
|
import FormDialogContent from '../components/FormDialogContent'
|
||||||
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
|
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
|
||||||
@@ -386,103 +388,92 @@ export default function AdminSSHAccessProfilesPage() {
|
|||||||
Loading...
|
Loading...
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
{filteredItems.length === 0 && !loading ? (
|
||||||
{filteredItems.length === 0 && !loading ? (
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Typography variant="body2" color="text.secondary">
|
No SSH access profiles found.
|
||||||
No SSH access profiles found.
|
</Typography>
|
||||||
</Typography>
|
) : null}
|
||||||
) : null}
|
<List>
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<Paper
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.id}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={
|
||||||
sx={{
|
<Typography>{item.name}</Typography>
|
||||||
p: 1,
|
}
|
||||||
display: 'grid',
|
secondary={
|
||||||
gap: 0.5,
|
<Box sx={{ display: 'grid' }}>
|
||||||
backgroundColor: 'transparent',
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
backgroundImage: 'none',
|
{item.server_target_type === 'group' ? (
|
||||||
borderLeft: 'none',
|
<Tooltip title={item.server_group_id || '-'} arrow>
|
||||||
borderRight: 'none',
|
<Link
|
||||||
borderTop: 'none',
|
underline="hover"
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
onClick={() => openServerGroupView(item)}
|
||||||
borderRadius: 0,
|
sx={{ cursor: 'default' }}
|
||||||
boxShadow: 'none'
|
>
|
||||||
}}
|
Group: {item.server_group?.name || item.server_group_id || '-'}
|
||||||
>
|
</Link>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
</Tooltip>
|
||||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
) : (
|
||||||
{item.name}
|
<Tooltip title={item.server_id} arrow>
|
||||||
</Typography>
|
<Link
|
||||||
<ListRowActions>
|
underline="hover"
|
||||||
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
onClick={() => void openServerView(item.server_id)}
|
||||||
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
|
sx={{ cursor: 'default' }}
|
||||||
<ListRowActionButton color="error" onClick={() => { setDeleteItem(item); setDeleteConfirm('') }}>
|
>
|
||||||
Delete
|
Server: {item.server?.name || item.server_id}
|
||||||
</ListRowActionButton>
|
</Link>
|
||||||
</ListRowActions>
|
</Tooltip>
|
||||||
</Box>
|
)}
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
{item.server_target_type === 'group' ? '' : ` · ${item.server?.host}:${item.server?.port}`} · {item.remote_username} · {item.auth_method} · 2FA: {item.second_factor_mode || 'none'} · {item.enabled ? 'enabled' : 'disabled'}
|
||||||
{item.server_target_type === 'group' ? (
|
</Typography>
|
||||||
<Tooltip title={item.server_group_id || '-'} arrow>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
<Link
|
Targets: {(item.targets || []).map((target) => `${target.target_type}:${target.target_name}`).join(', ') || '-'}
|
||||||
underline="hover"
|
</Typography>
|
||||||
onClick={() => openServerGroupView(item)}
|
{item.auth_method === 'managed_ssh_cert' ? (
|
||||||
sx={{ cursor: 'default' }}
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
>
|
CA:{' '}
|
||||||
Group: {item.server_group?.name || item.server_group_id || '-'}
|
{item.ssh_user_ca_id ? (
|
||||||
</Link>
|
<Tooltip title={item.ssh_user_ca_id} arrow>
|
||||||
</Tooltip>
|
<Link
|
||||||
) : (
|
underline="hover"
|
||||||
<Tooltip title={item.server_id} arrow>
|
onClick={() => void openCAView(item.ssh_user_ca_id)}
|
||||||
<Link
|
sx={{ cursor: 'default' }}
|
||||||
underline="hover"
|
>
|
||||||
onClick={() => void openServerView(item.server_id)}
|
{caNameByID[item.ssh_user_ca_id] || item.ssh_user_ca_id}
|
||||||
sx={{ cursor: 'default' }}
|
</Link>
|
||||||
>
|
</Tooltip>
|
||||||
Server: {item.server?.name || item.server_id}
|
) : '-'} · Grants: {(item.ssh_principal_grant_ids || []).join(', ') || '-'}
|
||||||
</Link>
|
</Typography>
|
||||||
</Tooltip>
|
) : (
|
||||||
)}
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
{item.server_target_type === 'group' ? '' : ` · ${item.server?.host}:${item.server?.port}`} · {item.remote_username} · {item.auth_method} · 2FA: {item.second_factor_mode || 'none'} · {item.enabled ? 'enabled' : 'disabled'}
|
Key fingerprint:{' '}
|
||||||
</Typography>
|
{item.ssh_credential_id && credentialByID[item.ssh_credential_id] ? (
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<Tooltip title={item.ssh_credential_id} arrow>
|
||||||
Targets: {(item.targets || []).map((target) => `${target.target_type}:${target.target_name}`).join(', ') || '-'}
|
<Link
|
||||||
</Typography>
|
underline="hover"
|
||||||
{item.auth_method === 'managed_ssh_cert' ? (
|
onClick={() => setViewCredentialItem(credentialByID[item.ssh_credential_id])}
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
sx={{ cursor: 'default' }}
|
||||||
CA:{' '}
|
>
|
||||||
{item.ssh_user_ca_id ? (
|
{item.auth_public_key_fingerprint || credentialByID[item.ssh_credential_id].fingerprint || item.ssh_credential_id}
|
||||||
<Tooltip title={item.ssh_user_ca_id} arrow>
|
</Link>
|
||||||
<Link
|
</Tooltip>
|
||||||
underline="hover"
|
) : item.auth_public_key_fingerprint || '-'}
|
||||||
onClick={() => void openCAView(item.ssh_user_ca_id)}
|
</Typography>
|
||||||
sx={{ cursor: 'default' }}
|
)}
|
||||||
>
|
</Box>
|
||||||
{caNameByID[item.ssh_user_ca_id] || item.ssh_user_ca_id}
|
}
|
||||||
</Link>
|
disableTypography
|
||||||
</Tooltip>
|
/>
|
||||||
) : '-'} · Grants: {(item.ssh_principal_grant_ids || []).join(', ') || '-'}
|
<ListRowActions>
|
||||||
</Typography>
|
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
||||||
) : (
|
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<ListRowActionButton color="error" onClick={() => { setDeleteItem(item); setDeleteConfirm('') }}>
|
||||||
Key fingerprint:{' '}
|
Delete
|
||||||
{item.ssh_credential_id && credentialByID[item.ssh_credential_id] ? (
|
</ListRowActionButton>
|
||||||
<Tooltip title={item.ssh_credential_id} arrow>
|
</ListRowActions>
|
||||||
<Link
|
</ListItem>
|
||||||
underline="hover"
|
|
||||||
onClick={() => setViewCredentialItem(credentialByID[item.ssh_credential_id])}
|
|
||||||
sx={{ cursor: 'default' }}
|
|
||||||
>
|
|
||||||
{item.auth_public_key_fingerprint || credentialByID[item.ssh_credential_id].fingerprint || item.ssh_credential_id}
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
) : item.auth_public_key_fingerprint || '-'}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</List>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<SSHAccessProfileFormDialog
|
<SSHAccessProfileFormDialog
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import DialogTitle from '@mui/material/DialogTitle'
|
|||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
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 ListItemText from '@mui/material/ListItemText'
|
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -22,6 +21,7 @@ import MonospaceTextField from '../components/MonospaceTextField'
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import SSHCredentialDetailsDialog from '../components/SSHCredentialDetailsDialog'
|
import SSHCredentialDetailsDialog from '../components/SSHCredentialDetailsDialog'
|
||||||
import { api, SSHCredential, SSHCredentialUpsertPayload } from '../api'
|
import { api, SSHCredential, SSHCredentialUpsertPayload } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
name: string
|
name: string
|
||||||
@@ -145,15 +145,18 @@ export default function AdminSSHCredentialsPage() {
|
|||||||
<List>
|
<List>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={`${item.fingerprint || item.id} · ${item.type} · Updated: ${fmt(item.updated_at)}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{item.fingerprint || item.id} · {item.type} · Updated: {fmt(item.updated_at)}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
@@ -24,6 +23,7 @@ import SSHPrincipalGrantDetailsDialog from '../components/SSHPrincipalGrantDetai
|
|||||||
import SSHPrincipalGrantFormDialog, { SSHPrincipalGrantFormState } from '../components/SSHPrincipalGrantFormDialog'
|
import SSHPrincipalGrantFormDialog, { SSHPrincipalGrantFormState } from '../components/SSHPrincipalGrantFormDialog'
|
||||||
import { api, SSHPrincipalGrant, SSHPrincipalGrantUpsertPayload, User, UserGroup } from '../api'
|
import { api, SSHPrincipalGrant, SSHPrincipalGrantUpsertPayload, User, UserGroup } from '../api'
|
||||||
import SelectField from '../components/SelectField'
|
import SelectField from '../components/SelectField'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function fmt(value: number): string {
|
function fmt(value: number): string {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
@@ -309,10 +309,21 @@ export default function AdminSSHPrincipalGrantsPage() {
|
|||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={`${item.name || item.principal}${item.disabled ? ' (disabled)' : ''}`}
|
primary={
|
||||||
secondary={`principals: ${(item.principals || []).join(', ') || '-'} · targets: ${(item.targets || []).map((target) => `${target.target_type}:${target.target_name || target.target_id}`).join(', ') || '-'} · max-valid: ${item.max_cert_valid_seconds || 0}s · uses: ${item.used_count}/${item.max_uses || '∞'} · valid: ${fmt(item.valid_after)} ~ ${fmt(item.valid_before)}`}
|
<Typography>
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
{item.name || item.principal}{item.disabled ? ' (disabled)' : ''}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
principals: {(item.principals || []).join(', ') || '-'} ·
|
||||||
|
targets: {(item.targets || []).map((target) => `${target.target_type}:${target.target_name || target.target_id}`).join(', ') || '-'} ·
|
||||||
|
max-valid: {item.max_cert_valid_seconds || 0}s ·
|
||||||
|
uses: {item.used_count}/{item.max_uses || '∞'} ·
|
||||||
|
valid: {fmt(item.valid_after)} ~ {fmt(item.valid_before)}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => openView(item)}>
|
<ListRowActionButton onClick={() => openView(item)}>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import Button from '@mui/material/Button'
|
|||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
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 ListItemText from '@mui/material/ListItemText'
|
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
@@ -20,6 +19,7 @@ import { SSHServerGroupFormState } from '../components/SSHServerGroupFormDialog'
|
|||||||
import SSHServerGroupMembersDialog from '../components/SSHServerGroupMembersDialog'
|
import SSHServerGroupMembersDialog from '../components/SSHServerGroupMembersDialog'
|
||||||
import SSHHostKeysDialog from '../components/SSHHostKeysDialog'
|
import SSHHostKeysDialog from '../components/SSHHostKeysDialog'
|
||||||
import { api, SSHServer, SSHServerGroup, SSHServerGroupUpsertPayload, SSHServerHostKey, SSHServerUpsertPayload } from '../api'
|
import { api, SSHServer, SSHServerGroup, SSHServerGroupUpsertPayload, SSHServerHostKey, SSHServerUpsertPayload } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
const emptyForm = (): SSHServerFormState => ({
|
const emptyForm = (): SSHServerFormState => ({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -444,7 +444,7 @@ export default function AdminSSHServersPage() {
|
|||||||
<List>
|
<List>
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
@@ -452,12 +452,11 @@ export default function AdminSSHServersPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={
|
secondary={
|
||||||
<Typography variant="body2" color="textSecondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{item.host}:{item.port}· Tags: {(item.tags || []).join(', ') || '-'}· Updated: {fmt(item.updated_at)}
|
{item.host}:{item.port}· Tags: {(item.tags || []).join(', ') || '-'}· Updated: {fmt(item.updated_at)}
|
||||||
{item.description? (<><br />{item.description}</>): null}
|
{item.description? (<><br />{item.description}</>): null}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
||||||
@@ -490,15 +489,18 @@ export default function AdminSSHServersPage() {
|
|||||||
<List>
|
<List>
|
||||||
{groups.map((item) => (
|
{groups.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={`${(item.server_ids || []).length} servers · ${item.description || '-'}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{(item.server_ids || []).length} servers · {item.description || '-'}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setViewGroupItem(item)}>View</ListRowActionButton>
|
<ListRowActionButton onClick={() => setViewGroupItem(item)}>View</ListRowActionButton>
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ 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 Link from '@mui/material/Link'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
|
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
|
||||||
import SSHServerDetailsDialog from '../components/SSHServerDetailsDialog'
|
import SSHServerDetailsDialog from '../components/SSHServerDetailsDialog'
|
||||||
@@ -222,22 +226,22 @@ export default function AdminSSHSessionsPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
{items.length ? (
|
{items.length ? (
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<List>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Box
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.id}
|
<CompactListItemText
|
||||||
sx={{
|
primary={
|
||||||
display: 'grid',
|
<Typography>
|
||||||
gap: 0.5,
|
{sessionTitle(item)}{' '}
|
||||||
py: 1,
|
<Chip
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
label={item.status || 'unknown'}
|
||||||
}}
|
size="small"
|
||||||
>
|
color={item.status === 'connected'? 'success': (item.status === 'error'? 'error' : 'default')}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
/>
|
||||||
<Box sx={{ minWidth: 0, display: 'grid', gap: 0.25, flex: '1 1 320px' }}>
|
|
||||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
{sessionTitle(item)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<Box sx={{ display: 'grid' }}>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
User: {item.username} · Auth: {item.auth_method} · Started: {fmt(item.started_at)} · Ended: {fmt(item.ended_at)}
|
User: {item.username} · Auth: {item.auth_method} · Started: {fmt(item.started_at)} · Ended: {fmt(item.ended_at)}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -272,26 +276,20 @@ export default function AdminSSHSessionsPage() {
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
}
|
||||||
<Chip
|
disableTypography
|
||||||
label={item.status || 'unknown'}
|
/>
|
||||||
size="small"
|
<ListRowActions>
|
||||||
color={item.status === 'connected' ? 'success' : item.status === 'error' ? 'error' : 'default'}
|
<ListRowActionButton
|
||||||
variant={item.status === 'connected' ? 'filled' : 'outlined'}
|
onClick={() => void openTranscript(item)}
|
||||||
/>
|
disabled={!item.transcript_available}
|
||||||
<Button
|
>
|
||||||
variant="outlined"
|
Transcript
|
||||||
size="small"
|
</ListRowActionButton>
|
||||||
onClick={() => void openTranscript(item)}
|
</ListRowActions>
|
||||||
disabled={!item.transcript_available}
|
</ListItem>
|
||||||
>
|
|
||||||
Transcript
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</List>
|
||||||
) : null}
|
) : null}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
Link,
|
Link,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import SSHUserCADetailsDialog from '../components/SSHUserCADetailsDialog'
|
import SSHUserCADetailsDialog from '../components/SSHUserCADetailsDialog'
|
||||||
import SSHPrincipalGrantDetailsDialog from '../components/SSHPrincipalGrantDetailsDialog'
|
import SSHPrincipalGrantDetailsDialog from '../components/SSHPrincipalGrantDetailsDialog'
|
||||||
@@ -190,65 +193,65 @@ export default function AdminSSHSignHistoryPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{!loading && items.length === 0 ? <Typography variant="body2" color="text.secondary">No signing history found.</Typography> : null}
|
{!loading && items.length === 0 ? <Typography variant="body2" color="text.secondary">No signing history found.</Typography> : null}
|
||||||
{items.map((item) => (
|
<List>
|
||||||
<Box
|
{items.map((item: SSHUserCAIssuance) => (
|
||||||
key={item.id}
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
sx={{
|
<CompactListItemText
|
||||||
p: 1,
|
primary={
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
<Typography>
|
||||||
minWidth: 0
|
<Tooltip title={item.ca_id} arrow>
|
||||||
}}
|
<Link
|
||||||
>
|
underline="hover"
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, minWidth: 0 }}>
|
onClick={() => openCAView(item).catch(() => {})}
|
||||||
<Box sx={{ display: 'grid', gap: 0.5, minWidth: 0, flexGrow: 1 }}>
|
sx={{ cursor: 'default' }}
|
||||||
<Typography variant="body2">
|
>
|
||||||
<Tooltip title={item.ca_id} arrow>
|
{item.ca_name || item.ca_id}
|
||||||
<Link
|
</Link>
|
||||||
underline="hover"
|
</Tooltip>
|
||||||
onClick={() => openCAView(item).catch(() => {})}
|
{' '}· serial: {item.serial} · key_id: {item.key_id}
|
||||||
sx={{ cursor: 'default' }}
|
</Typography>
|
||||||
>
|
}
|
||||||
{item.ca_name || item.ca_id}
|
secondary={
|
||||||
</Link>
|
<Box sx={{ display: 'grid' }}>
|
||||||
</Tooltip>
|
<Typography variant="caption" color="text.secondary">
|
||||||
{' '}· serial: {item.serial} · key_id: {item.key_id}
|
by: {item.issuer_username || '-'} ({item.issuer_kind}) · {'principals:'}{' '}
|
||||||
</Typography>
|
{item.grant_ids && item.grant_ids.length > 0 ? item.grant_ids.map((grantID: string, index: number) => (
|
||||||
<Typography variant="caption" color="text.secondary" component="div">
|
<Box key={grantID} component="span">
|
||||||
by: {item.issuer_username || '-'} ({item.issuer_kind}) · {'principals:'}{' '}
|
{index > 0 ? ', ' : ''}
|
||||||
{item.grant_ids && item.grant_ids.length > 0 ? item.grant_ids.map((grantID: string, index: number) => (
|
<Tooltip title={item.grant_names[index] || grantID} arrow>
|
||||||
<Box key={grantID} component="span">
|
<Link
|
||||||
{index > 0 ? ', ' : ''}
|
underline="hover"
|
||||||
<Tooltip title={item.grant_names[index] || grantID} arrow>
|
onClick={() => openPrincipalGrantView(grantID).catch(() => {})}
|
||||||
<Link
|
sx={{ cursor: 'default' }}
|
||||||
underline="hover"
|
>
|
||||||
onClick={() => openPrincipalGrantView(grantID).catch(() => {})}
|
{
|
||||||
sx={{ cursor: 'default' }}
|
//(item.grant_names || [])[index] || grantID
|
||||||
>
|
// show stored principals rather than the grant info
|
||||||
{
|
// but upon clocking on it, it shows the grant info dialog.
|
||||||
//(item.grant_names || [])[index] || grantID
|
((item.principals || []).join(', ') || '-')
|
||||||
// show stored principals rather than the grant info
|
}
|
||||||
// but upon clocking on it, it shows the grant info dialog.
|
</Link>
|
||||||
((item.principals || []).join(', ') || '-')
|
</Tooltip>
|
||||||
}
|
</Box>
|
||||||
</Link>
|
)) : ((item.principals || []).join(', ') || '-')}
|
||||||
</Tooltip>
|
</Typography>
|
||||||
</Box>
|
<Typography variant="caption" color="text.secondary">
|
||||||
)) : ((item.principals || []).join(', ') || '-')}
|
issued: {fmt(item.created_at)} · valid: {fmt(item.valid_after)} ~ {fmt(item.valid_before)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
issued: {fmt(item.created_at)} · valid: {fmt(item.valid_after)} ~ {fmt(item.valid_before)}
|
source fp: {item.source_public_key_fingerprint} · remote: {item.remote_addr || '-'}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
</Box>
|
||||||
source fp: {item.source_public_key_fingerprint} · remote: {item.remote_addr || '-'}
|
}
|
||||||
</Typography>
|
disableTypography
|
||||||
</Box>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => copyCert(item)}>Copy Cert</ListRowActionButton>
|
<ListRowActionButton onClick={() => copyCert(item)}>Copy Cert</ListRowActionButton>
|
||||||
<ListRowActionButton onClick={() => inspectCert(item)} disabled={inspectBusy}>{inspectBusy ? 'Inspecting...' : 'Inspect'}</ListRowActionButton>
|
<ListRowActionButton onClick={() => inspectCert(item)} disabled={inspectBusy}>{inspectBusy ? 'Inspecting...' : 'Inspect'}</ListRowActionButton>
|
||||||
</ListRowActions>
|
</ListRowActions>
|
||||||
</Box>
|
</ListItem>
|
||||||
</Box>
|
))}
|
||||||
))}
|
</List>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<SSHUserCADetailsDialog
|
<SSHUserCADetailsDialog
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
@@ -25,6 +24,7 @@ import SSHUserCADetailsDialog from '../components/SSHUserCADetailsDialog'
|
|||||||
import SSHUserCAFormDialog, { SSHUserCAFormState } from '../components/SSHUserCAFormDialog'
|
import SSHUserCAFormDialog, { SSHUserCAFormState } from '../components/SSHUserCAFormDialog'
|
||||||
import SSHCertificateInspectDialog from '../components/SSHCertificateInspectDialog'
|
import SSHCertificateInspectDialog from '../components/SSHCertificateInspectDialog'
|
||||||
import { api, SSHSignUserKeyResponse, SSHUserCA, SSHUserCACreatePayload, SSHUserCASignPayload, SSHUserCAUpdatePayload } from '../api'
|
import { api, SSHSignUserKeyResponse, SSHUserCA, SSHUserCACreatePayload, SSHUserCASignPayload, SSHUserCAUpdatePayload } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function fmt(value: number): string {
|
function fmt(value: number): string {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
@@ -324,15 +324,25 @@ export default function AdminSSHUserCAPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={`${item.algorithm} · ${item.fingerprint} · serial: ${item.serial_counter} · self-sign: ${item.allow_user_sign ? 'on' : 'off'} (max ${item.max_user_valid_seconds}s) · updated: ${fmt(item.updated_at)}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Box sx={{ display: 'grid' }}>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{item.algorithm} · {item.fingerprint}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
serial: {item.serial_counter} ·
|
||||||
|
self-sign: {item.allow_user_sign ? 'on' : 'off'} (max {item.max_user_valid_seconds}s) ·
|
||||||
|
updated: {fmt(item.updated_at)}`
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => openView(item)}>
|
<ListRowActionButton onClick={() => openView(item)}>
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
@@ -22,6 +23,7 @@ import { ListRowActionButton, ListRowActions } from '../components/ListRowAction
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import { api, CertPrincipalBinding, PKICert, PKICertDetail, PrincipalAPIKey, PrincipalProjectRole, Project, ServicePrincipal } from '../api'
|
import { api, CertPrincipalBinding, PKICert, PKICertDetail, PrincipalAPIKey, PrincipalProjectRole, Project, ServicePrincipal } from '../api'
|
||||||
import SelectField from '../components/SelectField'
|
import SelectField from '../components/SelectField'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function fmt(ts: number): string {
|
function fmt(ts: number): string {
|
||||||
if (!ts || ts <= 0) return '-'
|
if (!ts || ts <= 0) return '-'
|
||||||
@@ -563,57 +565,43 @@ export default function AdminServicePrincipalsPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{loading && principals.length === 0 ? <Typography variant="body2" color="text.secondary">Loading...</Typography> : null}
|
{loading && principals.length === 0 ? <Typography variant="body2" color="text.secondary">Loading...</Typography> : null}
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<List>
|
||||||
{principals.map((item) => (
|
{principals.map((item) => (
|
||||||
<Paper
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.id}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={
|
||||||
sx={{
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||||
p: 1,
|
<Typography variant="body2">{item.name} ({item.id})</Typography>
|
||||||
backgroundColor: 'transparent',
|
<Chip size="small" color={item.disabled ? 'default' : 'success'} label={item.disabled ? 'Disabled' : 'Active'} />
|
||||||
backgroundImage: 'none',
|
{item.is_admin ? <Chip size="small" color="warning" label="Principal Admin" /> : null}
|
||||||
borderLeft: 'none',
|
</Box>
|
||||||
borderRight: 'none',
|
}
|
||||||
borderTop: 'none',
|
secondary={`${item.description || '(no description)'} · updated: ${fmt(item.updated_at)}`}
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
/>
|
||||||
borderRadius: 0,
|
<ListRowActions>
|
||||||
boxShadow: 'none'
|
<ListRowActionButton onClick={() => openKeys(item)}>
|
||||||
}}
|
API Keys
|
||||||
>
|
</ListRowActionButton>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
<ListRowActionButton color={item.is_admin ? 'warning' : 'primary'} onClick={() => togglePrincipalAdmin(item)}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
{item.is_admin ? 'Unset Admin' : 'Set Admin'}
|
||||||
<Typography variant="body2">{item.name} ({item.id})</Typography>
|
</ListRowActionButton>
|
||||||
<Chip size="small" color={item.disabled ? 'default' : 'success'} label={item.disabled ? 'Disabled' : 'Active'} />
|
<ListRowActionButton color={item.disabled ? 'success' : 'warning'} onClick={() => togglePrincipal(item)}>
|
||||||
{item.is_admin ? <Chip size="small" color="warning" label="Principal Admin" /> : null}
|
{item.disabled ? 'Enable' : 'Disable'}
|
||||||
</Box>
|
</ListRowActionButton>
|
||||||
<ListRowActions>
|
<ListRowActionButton
|
||||||
<ListRowActionButton onClick={() => openKeys(item)}>
|
color="error"
|
||||||
API Keys
|
onClick={() => {
|
||||||
</ListRowActionButton>
|
setDeleteError(null)
|
||||||
<ListRowActionButton color={item.is_admin ? 'warning' : 'primary'} onClick={() => togglePrincipalAdmin(item)}>
|
setDeletePrincipalTarget(item)
|
||||||
{item.is_admin ? 'Unset Admin' : 'Set Admin'}
|
setDeletePrincipalConfirm('')
|
||||||
</ListRowActionButton>
|
}}
|
||||||
<ListRowActionButton color={item.disabled ? 'success' : 'warning'} onClick={() => togglePrincipal(item)}>
|
>
|
||||||
{item.disabled ? 'Enable' : 'Disable'}
|
Delete
|
||||||
</ListRowActionButton>
|
</ListRowActionButton>
|
||||||
<ListRowActionButton
|
</ListRowActions>
|
||||||
color="error"
|
</ListItem>
|
||||||
onClick={() => {
|
|
||||||
setDeleteError(null)
|
|
||||||
setDeletePrincipalTarget(item)
|
|
||||||
setDeletePrincipalConfirm('')
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</ListRowActionButton>
|
|
||||||
</ListRowActions>
|
|
||||||
</Box>
|
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
{item.description || '(no description)'} · updated: {fmt(item.updated_at)}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</List>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
||||||
Global permissions are managed on each subject page. No service-principal global permissions are available yet.
|
Global permissions are managed on each subject page. No service-principal global permissions are available yet.
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -688,48 +676,45 @@ export default function AdminServicePrincipalsPage() {
|
|||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<List>
|
||||||
{filteredPrincipals.map((principal) => (
|
{filteredPrincipals.map((principal) => (
|
||||||
<Paper
|
<ListItem key={principal.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={principal.id}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={<Typography variant="subtitle2">{principal.name}</Typography>}
|
||||||
sx={{
|
secondary={
|
||||||
p: 1,
|
(principalRoles[principal.id] || []).length === 0 ? (
|
||||||
backgroundColor: 'transparent',
|
<Typography variant="caption" color="text.secondary">No project roles</Typography>
|
||||||
backgroundImage: 'none',
|
) : (
|
||||||
borderLeft: 'none',
|
<List disablePadding>
|
||||||
borderRight: 'none',
|
{(principalRoles[principal.id] || []).map((role) => (
|
||||||
borderTop: 'none',
|
<ListItem
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
key={`${role.principal_id}:${role.project_id}`}
|
||||||
borderRadius: 0,
|
disableGutters
|
||||||
boxShadow: 'none'
|
secondaryAction={
|
||||||
}}
|
<ListRowActionButton
|
||||||
>
|
color="error"
|
||||||
<Typography variant="subtitle2">{principal.name}</Typography>
|
onClick={() => {
|
||||||
{(principalRoles[principal.id] || []).length === 0 ? (
|
setDeleteError(null)
|
||||||
<Typography variant="caption" color="text.secondary">No project roles</Typography>
|
setDeleteRoleTarget({ principalID: role.principal_id, projectID: role.project_id })
|
||||||
) : (
|
setDeleteRoleConfirm('')
|
||||||
<Box sx={{ display: 'grid', gap: 0.75, mt: 0.75 }}>
|
}}
|
||||||
{(principalRoles[principal.id] || []).map((role) => (
|
>
|
||||||
<Box key={`${role.principal_id}:${role.project_id}`} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
Delete
|
||||||
<Typography variant="body2" color="text.secondary">{projectName(role.project_id)} · {role.role}</Typography>
|
</ListRowActionButton>
|
||||||
<ListRowActionButton
|
}
|
||||||
color="error"
|
>
|
||||||
onClick={() => {
|
<CompactListItemText
|
||||||
setDeleteError(null)
|
primary={<Typography variant="body2" color="text.secondary">{projectName(role.project_id)} · {role.role}</Typography>}
|
||||||
setDeleteRoleTarget({ principalID: role.principal_id, projectID: role.project_id })
|
/>
|
||||||
setDeleteRoleConfirm('')
|
</ListItem>
|
||||||
}}
|
))}
|
||||||
>
|
</List>
|
||||||
Delete
|
)
|
||||||
</ListRowActionButton>
|
}
|
||||||
</Box>
|
/>
|
||||||
))}
|
</ListItem>
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Paper>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</List>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<SectionCard
|
<SectionCard
|
||||||
@@ -742,66 +727,54 @@ export default function AdminServicePrincipalsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<List>
|
||||||
{bindings.map((item) => (
|
{bindings.map((item) => (
|
||||||
<Paper
|
<ListItem key={item.fingerprint} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.fingerprint}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={
|
||||||
sx={{
|
pkiCertByFingerprint(item.fingerprint) ? (
|
||||||
p: 1,
|
<Link
|
||||||
backgroundColor: 'transparent',
|
underline="hover"
|
||||||
backgroundImage: 'none',
|
onClick={() => void openCertView(pkiCertByFingerprint(item.fingerprint)?.id || '')}
|
||||||
borderLeft: 'none',
|
sx={{ cursor: 'default' }}
|
||||||
borderRight: 'none',
|
>
|
||||||
borderTop: 'none',
|
{pkiCertByFingerprint(item.fingerprint)?.common_name || pkiCertByFingerprint(item.fingerprint)?.serial_hex} ({pkiCertByFingerprint(item.fingerprint)?.id})
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
</Link>
|
||||||
borderRadius: 0,
|
|
||||||
boxShadow: 'none'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
|
||||||
<Box sx={{ display: 'grid' }}>
|
|
||||||
{pkiCertByFingerprint(item.fingerprint) ? (
|
|
||||||
<Typography variant="body2">
|
|
||||||
<Link
|
|
||||||
underline="hover"
|
|
||||||
onClick={() => void openCertView(pkiCertByFingerprint(item.fingerprint)?.id || '')}
|
|
||||||
sx={{ cursor: 'default' }}
|
|
||||||
>
|
|
||||||
{pkiCertByFingerprint(item.fingerprint)?.common_name || pkiCertByFingerprint(item.fingerprint)?.serial_hex} ({pkiCertByFingerprint(item.fingerprint)?.id})
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2">{item.fingerprint}</Typography>
|
<Typography>{item.fingerprint}</Typography>
|
||||||
)}
|
)
|
||||||
<Typography variant="caption" color="text.secondary">principal: {principalName(item.principal_id)} ({item.principal_id})</Typography>
|
}
|
||||||
{pkiCertByFingerprint(item.fingerprint) ? (
|
secondary={
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Box sx={{ display: 'grid' }}>
|
||||||
source: pki cert · fingerprint: {item.fingerprint}
|
<Typography variant="caption" color="text.secondary">principal: {principalName(item.principal_id)} ({item.principal_id})</Typography>
|
||||||
</Typography>
|
{pkiCertByFingerprint(item.fingerprint) ? (
|
||||||
) : (
|
<Typography variant="caption" color="text.secondary">
|
||||||
<Typography variant="caption" color="text.secondary">source: manual fingerprint</Typography>
|
source: pki cert · fingerprint: {item.fingerprint}
|
||||||
)}
|
</Typography>
|
||||||
</Box>
|
) : (
|
||||||
<ListRowActions>
|
<Typography variant="caption" color="text.secondary">source: manual fingerprint</Typography>
|
||||||
<ListRowActionButton color={item.enabled ? 'warning' : 'success'} onClick={() => toggleBinding(item)}>
|
)}
|
||||||
{item.enabled ? 'Disable' : 'Enable'}
|
</Box>
|
||||||
</ListRowActionButton>
|
}
|
||||||
<ListRowActionButton
|
/>
|
||||||
color="error"
|
<ListRowActions>
|
||||||
onClick={() => {
|
<ListRowActionButton color={item.enabled ? 'warning' : 'success'} onClick={() => toggleBinding(item)}>
|
||||||
setDeleteError(null)
|
{item.enabled ? 'Disable' : 'Enable'}
|
||||||
setDeleteBindingTarget(item)
|
</ListRowActionButton>
|
||||||
setDeleteBindingConfirm('')
|
<ListRowActionButton
|
||||||
}}
|
color="error"
|
||||||
>
|
onClick={() => {
|
||||||
Delete
|
setDeleteError(null)
|
||||||
</ListRowActionButton>
|
setDeleteBindingTarget(item)
|
||||||
</ListRowActions>
|
setDeleteBindingConfirm('')
|
||||||
</Box>
|
}}
|
||||||
</Paper>
|
>
|
||||||
|
Delete
|
||||||
|
</ListRowActionButton>
|
||||||
|
</ListRowActions>
|
||||||
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</List>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -925,53 +898,35 @@ export default function AdminServicePrincipalsPage() {
|
|||||||
/>
|
/>
|
||||||
{keysLoading ? <Typography variant="body2" color="text.secondary">Loading API keys...</Typography> : null}
|
{keysLoading ? <Typography variant="body2" color="text.secondary">Loading API keys...</Typography> : null}
|
||||||
{!keysLoading ? (
|
{!keysLoading ? (
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<List>
|
||||||
{principalKeys.map((item) => (
|
{principalKeys.map((item) => (
|
||||||
<Paper
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.id}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={item.disabled ? `${item.name} (disabled)` : item.name}
|
||||||
sx={{
|
secondary={`Prefix: ${item.prefix} · Created: ${formatUnix(item.created_at)} · Last used: ${formatUnix(item.last_used_at)} · Expires: ${formatUnix(item.expires_at)}`}
|
||||||
p: 1,
|
/>
|
||||||
backgroundColor: 'transparent',
|
<ListRowActions>
|
||||||
backgroundImage: 'none',
|
<ListRowActionButton color={item.disabled ? 'success' : 'warning'} onClick={() => togglePrincipalKey(item)} disabled={keysBusy}>
|
||||||
borderLeft: 'none',
|
{item.disabled ? 'Enable' : 'Disable'}
|
||||||
borderRight: 'none',
|
</ListRowActionButton>
|
||||||
borderTop: 'none',
|
<ListRowActionButton
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
color="error"
|
||||||
borderRadius: 0,
|
onClick={() => {
|
||||||
boxShadow: 'none'
|
setDeleteError(null)
|
||||||
}}
|
setDeletePrincipalKeyTarget(item)
|
||||||
>
|
setDeletePrincipalKeyConfirm('')
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 1 }}>
|
}}
|
||||||
<Box sx={{ minWidth: 0 }}>
|
disabled={keysBusy}
|
||||||
<Typography variant="body2">{item.disabled ? `${item.name} (disabled)` : item.name}</Typography>
|
>
|
||||||
<Typography variant="caption" color="text.secondary">
|
Delete
|
||||||
Prefix: {item.prefix} · Created: {formatUnix(item.created_at)} · Last used: {formatUnix(item.last_used_at)} · Expires: {formatUnix(item.expires_at)}
|
</ListRowActionButton>
|
||||||
</Typography>
|
</ListRowActions>
|
||||||
</Box>
|
</ListItem>
|
||||||
<ListRowActions>
|
|
||||||
<ListRowActionButton color={item.disabled ? 'success' : 'warning'} onClick={() => togglePrincipalKey(item)} disabled={keysBusy}>
|
|
||||||
{item.disabled ? 'Enable' : 'Disable'}
|
|
||||||
</ListRowActionButton>
|
|
||||||
<ListRowActionButton
|
|
||||||
color="error"
|
|
||||||
onClick={() => {
|
|
||||||
setDeleteError(null)
|
|
||||||
setDeletePrincipalKeyTarget(item)
|
|
||||||
setDeletePrincipalKeyConfirm('')
|
|
||||||
}}
|
|
||||||
disabled={keysBusy}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</ListRowActionButton>
|
|
||||||
</ListRowActions>
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
))}
|
))}
|
||||||
{!principalKeys.length ? (
|
{!principalKeys.length ? (
|
||||||
<Typography variant="body2" color="text.secondary">No API keys yet.</Typography>
|
<Typography variant="body2" color="text.secondary">No API keys yet.</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</List>
|
||||||
) : null}
|
) : null}
|
||||||
</FormDialogContent>
|
</FormDialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import Dialog from '@mui/material/Dialog'
|
|||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
import Paper from '@mui/material/Paper'
|
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||||
import FormDialogContent from '../components/FormDialogContent'
|
import FormDialogContent from '../components/FormDialogContent'
|
||||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||||
@@ -206,57 +208,48 @@ export default function AdminTLSAuthPoliciesPage() {
|
|||||||
Loading...
|
Loading...
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
{filteredItems.length === 0 && !loading ? (
|
||||||
{filteredItems.length === 0 && !loading ? (
|
<Typography variant="body2" color="text.secondary">
|
||||||
<Typography variant="body2" color="text.secondary">
|
No auth policies found.
|
||||||
No auth policies found.
|
</Typography>
|
||||||
</Typography>
|
) : null}
|
||||||
) : null}
|
<List>
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.map((item) => (
|
||||||
<Paper
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.id}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={
|
||||||
sx={{
|
<Typography sx={{ wordBreak: 'break-word' }}>
|
||||||
p: 1,
|
{item.name}
|
||||||
display: 'grid',
|
</Typography>
|
||||||
gap: 0.5,
|
}
|
||||||
backgroundColor: 'transparent',
|
secondary={
|
||||||
backgroundImage: 'none',
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
borderLeft: 'none',
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
borderRight: 'none',
|
Read: {item.read_mode} · Write: {item.write_mode} · Principal for write: {item.require_principal_for_write ? 'yes' : 'no'}
|
||||||
borderTop: 'none',
|
</Typography>
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
borderRadius: 0,
|
Required perms ({item.permission_match}): {(item.required_permissions || []).join(', ') || '-'} · Scope: {item.required_scope || '-'}
|
||||||
boxShadow: 'none'
|
</Typography>
|
||||||
}}
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
>
|
Allowed PKI certs: {(item.allowed_pki_client_cert_ids || []).length} · Allowed fingerprints: {(item.allowed_cert_fingerprints || []).length} · {item.id}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
</Typography>
|
||||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
</Box>
|
||||||
{item.name}
|
}
|
||||||
</Typography>
|
disableTypography
|
||||||
<ListRowActions>
|
/>
|
||||||
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
|
<ListRowActions>
|
||||||
<ListRowActionButton color="error" onClick={() => {
|
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
|
||||||
setDeleteError(null)
|
<ListRowActionButton color="error" onClick={() => {
|
||||||
setConfirmPolicy(item)
|
setDeleteError(null)
|
||||||
setConfirmText('')
|
setConfirmPolicy(item)
|
||||||
}}>
|
setConfirmText('')
|
||||||
Delete
|
}}>
|
||||||
</ListRowActionButton>
|
Delete
|
||||||
</ListRowActions>
|
</ListRowActionButton>
|
||||||
</Box>
|
</ListRowActions>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
</ListItem>
|
||||||
Read: {item.read_mode} · Write: {item.write_mode} · Principal for write: {item.require_principal_for_write ? 'yes' : 'no'}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Required perms ({item.permission_match}): {(item.required_permissions || []).join(', ') || '-'} · Scope: {item.required_scope || '-'}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Allowed PKI certs: {(item.allowed_pki_client_cert_ids || []).length} · Allowed fingerprints: {(item.allowed_cert_fingerprints || []).length} · {item.id}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
))}
|
))}
|
||||||
</Box>
|
</List>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="md" fullWidth>
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="md" fullWidth>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||||
import FormDialogContent from '../components/FormDialogContent'
|
import FormDialogContent from '../components/FormDialogContent'
|
||||||
import {
|
import {
|
||||||
@@ -8,8 +9,9 @@ import {
|
|||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
|
||||||
TextField,
|
TextField,
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
@@ -399,108 +401,101 @@ export default function AdminTLSSettingsPage() {
|
|||||||
Loading...
|
Loading...
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<List>
|
||||||
<Paper
|
<ListItem divider sx={{ alignItems: 'flex-start' }}>
|
||||||
variant="outlined"
|
<CompactListItemText
|
||||||
sx={{
|
primary={
|
||||||
p: 1,
|
<Typography component="div">
|
||||||
display: 'grid',
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||||
gap: 0.5,
|
<Box component="span">Main Listener</Box>
|
||||||
backgroundColor: 'transparent',
|
<Chip size="small" color="info" label="Main" />
|
||||||
backgroundImage: 'none',
|
</Box>
|
||||||
borderLeft: 'none',
|
</Typography>
|
||||||
borderRight: 'none',
|
}
|
||||||
borderTop: 'none',
|
secondary={
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
borderRadius: 0,
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
boxShadow: 'none'
|
HTTP: {(settings.http_addrs || []).join(', ') || '(none)'}
|
||||||
}}
|
</Typography>
|
||||||
>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
HTTPS: {(settings.https_addrs || []).join(', ') || '(none)'}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
</Typography>
|
||||||
<Typography variant="subtitle2">Main Listener</Typography>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
<Chip size="small" color="info" label="Main" />
|
Policies: {summarizeEndpointPolicies(settings.endpoint_policies || [])}
|
||||||
</Box>
|
</Typography>
|
||||||
<ListRowActions>
|
<Typography variant="caption" color="warning.main" sx={{ wordBreak: 'break-word' }}>
|
||||||
<ListRowActionButton onClick={openMainEditDialog}>Edit</ListRowActionButton>
|
Main listener transport changes require backend restart to take effect.
|
||||||
</ListRowActions>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
}
|
||||||
HTTP: {(settings.http_addrs || []).join(', ') || '(none)'}
|
disableTypography
|
||||||
</Typography>
|
/>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
<ListRowActions>
|
||||||
HTTPS: {(settings.https_addrs || []).join(', ') || '(none)'}
|
<ListRowActionButton onClick={openMainEditDialog}>Edit</ListRowActionButton>
|
||||||
</Typography>
|
</ListRowActions>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
</ListItem>
|
||||||
Policies: {summarizeEndpointPolicies(settings.endpoint_policies || [])}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="warning.main" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Main listener transport changes require backend restart to take effect.
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
{!listenersLoading && (listeners || []).length === 0 ? (
|
{!listenersLoading && (listeners || []).length === 0 ? (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<ListItem>
|
||||||
No additional listeners configured.
|
<CompactListItemText
|
||||||
</Typography>
|
primary={
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
No additional listeners configured.
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
{(listeners || []).map((item) => (
|
{(listeners || []).map((item) => (
|
||||||
<Paper
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
key={item.id}
|
<CompactListItemText
|
||||||
variant="outlined"
|
primary={
|
||||||
sx={{
|
<Typography component="div" sx={{ wordBreak: 'break-word' }}>
|
||||||
p: 1,
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
||||||
display: 'grid',
|
<Box component="span">{item.name}</Box>
|
||||||
gap: 0.5,
|
{item.enabled ? (
|
||||||
backgroundColor: 'transparent',
|
(runtimeCounts[item.id] || 0) > 0 ? (
|
||||||
backgroundImage: 'none',
|
<Chip size="small" color="success" label={`Running (${runtimeCounts[item.id]} endpoint${runtimeCounts[item.id] > 1 ? 's' : ''})`} />
|
||||||
borderLeft: 'none',
|
) : (
|
||||||
borderRight: 'none',
|
<Chip size="small" color="warning" label="Enabled (starting...)" />
|
||||||
borderTop: 'none',
|
)
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
) : (
|
||||||
borderRadius: 0,
|
<Chip size="small" label="Disabled" />
|
||||||
boxShadow: 'none'
|
)}
|
||||||
}}
|
</Box>
|
||||||
>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0, flexWrap: 'wrap' }}>
|
secondary={
|
||||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>{item.name}</Typography>
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
{item.enabled ? (
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
(runtimeCounts[item.id] || 0) > 0 ? (
|
HTTP: {(item.http_addrs || []).join(', ') || '(none)'}
|
||||||
<Chip size="small" color="success" label={`Running (${runtimeCounts[item.id]} endpoint${runtimeCounts[item.id] > 1 ? 's' : ''})`} />
|
</Typography>
|
||||||
) : (
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
<Chip size="small" color="warning" label="Enabled (starting...)" />
|
HTTPS: {(item.https_addrs || []).join(', ') || '(none)'}
|
||||||
)
|
</Typography>
|
||||||
) : (
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
<Chip size="small" label="Disabled" />
|
Policies: {summarizeEndpointPolicies(item.endpoint_policies || [])}
|
||||||
)}
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<ListRowActions>
|
}
|
||||||
<ListRowActionButton
|
disableTypography
|
||||||
color={item.enabled ? 'warning' : 'success'}
|
/>
|
||||||
onClick={() => handleToggleListener(item)}
|
<ListRowActions>
|
||||||
>
|
<ListRowActionButton
|
||||||
{item.enabled ? 'Disable' : 'Enable'}
|
color={item.enabled ? 'warning' : 'success'}
|
||||||
</ListRowActionButton>
|
onClick={() => handleToggleListener(item)}
|
||||||
<ListRowActionButton onClick={() => openEditDialog(item)}>
|
>
|
||||||
Edit
|
{item.enabled ? 'Disable' : 'Enable'}
|
||||||
</ListRowActionButton>
|
</ListRowActionButton>
|
||||||
<ListRowActionButton color="error" onClick={() => handleDeleteListener(item)}>
|
<ListRowActionButton onClick={() => openEditDialog(item)}>
|
||||||
Delete
|
Edit
|
||||||
</ListRowActionButton>
|
</ListRowActionButton>
|
||||||
</ListRowActions>
|
<ListRowActionButton color="error" onClick={() => handleDeleteListener(item)}>
|
||||||
</Box>
|
Delete
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
</ListRowActionButton>
|
||||||
HTTP: {(item.http_addrs || []).join(', ') || '(none)'}
|
</ListRowActions>
|
||||||
</Typography>
|
</ListItem>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
))}
|
||||||
HTTPS: {(item.https_addrs || []).join(', ') || '(none)'}
|
</List>
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Policies: {summarizeEndpointPolicies(item.endpoint_policies || [])}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="md" fullWidth>
|
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="md" fullWidth>
|
||||||
@@ -552,7 +547,7 @@ export default function AdminTLSSettingsPage() {
|
|||||||
{cert.common_name || cert.serial_hex} ({cert.id.slice(0, 8)})
|
{cert.common_name || cert.serial_hex} ({cert.id.slice(0, 8)})
|
||||||
{cert.status !== 'active' || cert.is_ca || cert.has_server_auth === false ? ' - current selection not server-usable' : ''}
|
{cert.status !== 'active' || cert.is_ca || cert.has_server_auth === false ? ' - current selection not server-usable' : ''}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<SelectField
|
<SelectField
|
||||||
select
|
select
|
||||||
@@ -582,7 +577,7 @@ export default function AdminTLSSettingsPage() {
|
|||||||
<MenuItem key={ca.id} value={ca.id}>
|
<MenuItem key={ca.id} value={ca.id}>
|
||||||
{ca.name} ({ca.id.slice(0, 8)})
|
{ca.name} ({ca.id.slice(0, 8)})
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<SelectField
|
<SelectField
|
||||||
select
|
select
|
||||||
@@ -610,9 +605,9 @@ export default function AdminTLSSettingsPage() {
|
|||||||
<MenuItem key={item.id} value={item.id}>
|
<MenuItem key={item.id} value={item.id}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</SelectField>
|
</SelectField>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</FormDialogContent>
|
</FormDialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import AddIcon from '@mui/icons-material/Add'
|
|||||||
import DeleteIcon from '@mui/icons-material/Delete'
|
import DeleteIcon from '@mui/icons-material/Delete'
|
||||||
import EditIcon from '@mui/icons-material/Edit'
|
import EditIcon from '@mui/icons-material/Edit'
|
||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||||
import FormDialogContent from '../components/FormDialogContent'
|
import FormDialogContent from '../components/FormDialogContent'
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -299,7 +299,7 @@ export default function AdminUserGroupsPage() {
|
|||||||
<Button variant="outlined" startIcon={<AddIcon />} onClick={openCreate}>New Group</Button>
|
<Button variant="outlined" startIcon={<AddIcon />} onClick={openCreate}>New Group</Button>
|
||||||
</Box>
|
</Box>
|
||||||
{error ? <Alert severity="error" sx={{ mb: 1 }}>{error}</Alert> : null}
|
{error ? <Alert severity="error" sx={{ mb: 1 }}>{error}</Alert> : null}
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '360px minmax(0, 1fr)' }, gap: 1, alignItems: 'start' }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '40% minmax(0, 1fr)' }, gap: 1, alignItems: 'start' }}>
|
||||||
<TintedPanel>
|
<TintedPanel>
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
@@ -324,19 +324,19 @@ export default function AdminUserGroupsPage() {
|
|||||||
onClick={() => setSelectedGroupID(group.id)}
|
onClick={() => setSelectedGroupID(group.id)}
|
||||||
sx={{ alignItems: 'flex-start', gap: 1, cursor: 'pointer' }}
|
sx={{ alignItems: 'flex-start', gap: 1, cursor: 'pointer' }}
|
||||||
>
|
>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
sx={{ minWidth: 0, flex: 1 }}
|
primary={
|
||||||
primary={group.name}
|
<Typography noWrap>
|
||||||
secondary={`${group.id} · ${group.scope === 'all_users' ? 'all users' : 'explicit'} · updated: ${fmt(group.updated_at)}${group.totp_required ? ' · TOTP required' : ''}${hasDirectProjectCreate(group.id) ? ' · project.create' : ''}`}
|
{group.name}
|
||||||
primaryTypographyProps={{
|
{group.disabled? (<> <Chip size="small" label="Disabled" color="error" sx={{ mt: 0.25, flexShrink: 0 }} /></>): null}
|
||||||
noWrap: true,
|
</Typography>
|
||||||
title: group.name + (group.disabled ? ' (disabled)' : '')
|
}
|
||||||
}}
|
secondary={
|
||||||
secondaryTypographyProps={{
|
<Typography variant="caption" color="text.secondary" sx={{ whiteSpace: 'normal', wordBreak: 'break-all' }}>
|
||||||
sx: { whiteSpace: 'normal', wordBreak: 'break-all' }
|
{group.id} · {group.scope === 'all_users' ? 'all users' : 'explicit'} · updated: {fmt(group.updated_at)}{group.totp_required ? ' · TOTP required' : ''}{hasDirectProjectCreate(group.id) ? ' · project.create' : ''}
|
||||||
}}
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
{group.disabled ? <Chip size="small" label="Disabled" sx={{ mt: 0.25, flexShrink: 0 }} /> : null}
|
|
||||||
{group.scope === 'all_users' ? <Chip size="small" label="All Users" sx={{ mt: 0.25, flexShrink: 0 }} /> : null}
|
{group.scope === 'all_users' ? <Chip size="small" label="All Users" sx={{ mt: 0.25, flexShrink: 0 }} /> : null}
|
||||||
<Box sx={{ display: 'flex', gap: 0.5, mt: 0.25, flexShrink: 0 }}>
|
<Box sx={{ display: 'flex', gap: 0.5, mt: 0.25, flexShrink: 0 }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
@@ -374,10 +374,10 @@ export default function AdminUserGroupsPage() {
|
|||||||
<Typography variant="h6" sx={{ minWidth: 0 }}>
|
<Typography variant="h6" sx={{ minWidth: 0 }}>
|
||||||
{selectedGroup.name}
|
{selectedGroup.name}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ display: 'flex', gap: 0.75, flexWrap: 'wrap' }}>
|
<Typography>
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
color={selectedGroup.disabled ? 'default' : 'success'}
|
color={selectedGroup.disabled ? 'error' : 'success'}
|
||||||
label={selectedGroup.disabled ? 'Disabled' : 'Active'}
|
label={selectedGroup.disabled ? 'Disabled' : 'Active'}
|
||||||
/>
|
/>
|
||||||
{hasDirectProjectCreate(selectedGroup.id) ? (
|
{hasDirectProjectCreate(selectedGroup.id) ? (
|
||||||
@@ -389,20 +389,16 @@ export default function AdminUserGroupsPage() {
|
|||||||
{selectedGroup.scope === 'all_users' ? (
|
{selectedGroup.scope === 'all_users' ? (
|
||||||
<Chip size="small" variant="outlined" label="All users" />
|
<Chip size="small" variant="outlined" label="All users" />
|
||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{selectedGroup.id} · scope: {selectedGroup.scope === 'all_users' ? 'all users' : 'explicit'} · updated: {fmt(selectedGroup.updated_at)}
|
{selectedGroup.id} · scope: {selectedGroup.scope === 'all_users' ? 'all users' : 'explicit'} · updated: {fmt(selectedGroup.updated_at)}
|
||||||
</Typography>
|
</Typography>
|
||||||
{selectedGroup.description ? (
|
{selectedGroup.description ? (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{selectedGroup.description}
|
{selectedGroup.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : (
|
) : null}
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
No description.
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
Chip,
|
Chip,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -15,6 +14,7 @@ import { ListRowActionButton, ListRowActions } from '../components/ListRowAction
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import UserFormDialog, { UserFormState } from '../components/UserFormDialog'
|
import UserFormDialog, { UserFormState } from '../components/UserFormDialog'
|
||||||
import { api, SubjectPermission, User, UserCreatePayload, UserUpdatePayload } from '../api'
|
import { api, SubjectPermission, User, UserCreatePayload, UserUpdatePayload } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
const projectCreatePermission = 'project.create'
|
const projectCreatePermission = 'project.create'
|
||||||
|
|
||||||
@@ -267,40 +267,41 @@ export default function AdminUsersPage() {
|
|||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<CompactListItemText
|
||||||
<ListItemText
|
primary={
|
||||||
primary={
|
<Typography>
|
||||||
<Typography>
|
{u.display_name || u.username}({u.username})
|
||||||
{u.display_name || u.username}({u.username})
|
{u.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||||
{u.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
</Typography>
|
||||||
</Typography>
|
}
|
||||||
}
|
secondary={
|
||||||
secondary={`${u.is_admin ? 'admin' : 'user'} · source: ${u.auth_source || 'db'}${u.totp_required ? ' · TOTP required' : ''}${u.totp_enabled ? ' · TOTP enabled' : ''}${hasDirectProjectCreate(u.id) ? ' · direct project.create' : ''}`}
|
<Typography variant="caption" color="text.secondary">
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
{u.is_admin ? 'admin' : 'user'} · source: {u.auth_source || 'db'}{u.totp_required ? ' · TOTP required' : ''}{u.totp_enabled ? ' · TOTP enabled' : ''}{hasDirectProjectCreate(u.id) ? ' · direct project.create' : ''}
|
||||||
/>
|
</Typography>
|
||||||
<ListRowActions>
|
}
|
||||||
<ListRowActionButton onClick={() => openEditUser(u)}>
|
/>
|
||||||
Edit
|
<ListRowActions>
|
||||||
</ListRowActionButton>
|
<ListRowActionButton onClick={() => openEditUser(u)}>
|
||||||
<ListRowActionButton
|
Edit
|
||||||
color={u.disabled ? 'success' : 'warning'}
|
</ListRowActionButton>
|
||||||
onClick={() => toggleUserState(u)}
|
<ListRowActionButton
|
||||||
disabled={togglingID === u.id}
|
color={u.disabled ? 'success' : 'warning'}
|
||||||
>
|
onClick={() => toggleUserState(u)}
|
||||||
{u.disabled ? 'Enable' : 'Disable'}
|
disabled={togglingID === u.id}
|
||||||
</ListRowActionButton>
|
>
|
||||||
<ListRowActionButton
|
{u.disabled ? 'Enable' : 'Disable'}
|
||||||
color="warning"
|
</ListRowActionButton>
|
||||||
onClick={() => setResetTOTPUser(u)}
|
<ListRowActionButton
|
||||||
disabled={!u.totp_enabled}
|
color="warning"
|
||||||
>
|
onClick={() => setResetTOTPUser(u)}
|
||||||
Reset TOTP
|
disabled={!u.totp_enabled}
|
||||||
</ListRowActionButton>
|
>
|
||||||
<ListRowActionButton color="error" onClick={() => setDeletingUser(u)}>
|
Reset TOTP
|
||||||
Delete
|
</ListRowActionButton>
|
||||||
</ListRowActionButton>
|
<ListRowActionButton color="error" onClick={() => setDeletingUser(u)}>
|
||||||
</ListRowActions>
|
Delete
|
||||||
</Box>
|
</ListRowActionButton>
|
||||||
|
</ListRowActions>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ import FormDialogContent from '../components/FormDialogContent'
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Chip,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
Typography
|
Typography
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import { api, APIKey } from '../api'
|
import { api, APIKey } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function formatUnix(value: number) {
|
function formatUnix(value: number) {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
@@ -194,10 +195,19 @@ export default function ApiKeysPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={key.disabled ? `${key.name} (disabled)` : key.name}
|
primary={
|
||||||
secondary={`Prefix: ${key.prefix} | Created: ${formatUnix(key.created_at)} | Last used: ${formatUnix(key.last_used_at)} | Expires: ${key.expires_at > 0 ? formatUnix(key.expires_at) : 'Never'}`}
|
<Typography>
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
{key.name}({key.prefix})
|
||||||
|
{key.disabled? (<> <Chip size="small" color='error' label='Disabled'/></>): null}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
secondary={
|
||||||
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
prefix: {key.prefix} · created: {formatUnix(key.created_at)} · last used: {formatUnix(key.last_used_at)} ·
|
||||||
|
expires: {key.expires_at > 0 ? formatUnix(key.expires_at) : 'Never'}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton
|
<ListRowActionButton
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Link,
|
Link,
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
@@ -23,6 +22,7 @@ import PKICADetailsDialog from '../components/PKICADetailsDialog'
|
|||||||
import PKICertDetailsDialog from '../components/PKICertDetailsDialog'
|
import PKICertDetailsDialog from '../components/PKICertDetailsDialog'
|
||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import { api, PKICADetail, PKIClientIssueResponse, PKIClientIssuance, PKIClientProfile, PKICertDetail } from '../api'
|
import { api, PKICADetail, PKIClientIssueResponse, PKIClientIssuance, PKIClientProfile, PKICertDetail } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
function fmt(value: number): string {
|
function fmt(value: number): string {
|
||||||
if (!value || value <= 0) {
|
if (!value || value <= 0) {
|
||||||
@@ -304,7 +304,7 @@ export default function ClientCertificatesPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const issuanceStatusColor = (status: string): 'default' | 'success' | 'warning' | 'error' => {
|
const issuanceStatusColor = (status: string): 'default' | 'success' | 'warning' | 'error' => {
|
||||||
if (status === 'issued' || status === 'valid') {
|
if (status === 'issued' || status === 'valid' || status === 'active') {
|
||||||
return 'success'
|
return 'success'
|
||||||
}
|
}
|
||||||
if (status === 'revoked') {
|
if (status === 'revoked') {
|
||||||
@@ -347,9 +347,9 @@ export default function ClientCertificatesPage() {
|
|||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<CompactListItemText
|
||||||
<ListItemText
|
primary={
|
||||||
primary={(
|
<Typography>
|
||||||
<Tooltip title={profile.id} arrow>
|
<Tooltip title={profile.id} arrow>
|
||||||
<Link
|
<Link
|
||||||
underline="hover"
|
underline="hover"
|
||||||
@@ -359,40 +359,39 @@ export default function ClientCertificatesPage() {
|
|||||||
{`${profile.name} (${profile.id})`}
|
{`${profile.name} (${profile.id})`}
|
||||||
</Link>
|
</Link>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
</Typography>
|
||||||
secondary={(
|
}
|
||||||
<Typography component="span" variant="body2" color="text.secondary">
|
secondary={
|
||||||
CA:{' '}
|
<Typography variant="caption" color="text.secondary">
|
||||||
<Tooltip title={profile.ca_id} arrow>
|
CA:{' '}
|
||||||
<Link
|
<Tooltip title={profile.ca_id} arrow>
|
||||||
underline="hover"
|
<Link
|
||||||
onClick={() => openCAView(profile).catch(() => {})}
|
underline="hover"
|
||||||
sx={{ cursor: 'default' }}
|
onClick={() => openCAView(profile).catch(() => {})}
|
||||||
>
|
sx={{ cursor: 'default' }}
|
||||||
{profile.ca_name || profile.ca_id}
|
>
|
||||||
</Link>
|
{profile.ca_name || profile.ca_id}
|
||||||
</Tooltip>
|
</Link>
|
||||||
{' '}· server auth: {profile.allow_server_auth ? 'yes' : 'no'} · perms: {(profile.authz_permissions || []).join(', ') || '-'} · scope: {profile.authz_scope || '-'} · validity: {profile.default_valid_seconds}s / max {profile.max_valid_seconds}s
|
</Tooltip>
|
||||||
</Typography>
|
{' '}· server auth: {profile.allow_server_auth ? 'yes' : 'no'} · perms: {(profile.authz_permissions || []).join(', ') || '-'} · scope: {profile.authz_scope || '-'} · validity: {profile.default_valid_seconds}s / max {profile.max_valid_seconds}s
|
||||||
)}
|
</Typography>
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton startIcon={<KeyIcon />} onClick={() => openIssue(profile)}>
|
<ListRowActionButton startIcon={<KeyIcon />} onClick={() => openIssue(profile)}>
|
||||||
Issue
|
Issue
|
||||||
</ListRowActionButton>
|
</ListRowActionButton>
|
||||||
</ListRowActions>
|
</ListRowActions>
|
||||||
</Box>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
{!loading && profiles.length === 0 ? (
|
{!loading && profiles.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No client certificate profiles are available to you." />
|
<CompactListItemText primary="No client certificate profiles are available to you." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
{!loading && profiles.length > 0 && filteredProfiles.length === 0 ? (
|
{!loading && profiles.length > 0 && filteredProfiles.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No matching client certificate profiles found." />
|
<CompactListItemText primary="No matching client certificate profiles found." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
@@ -424,23 +423,23 @@ export default function ClientCertificatesPage() {
|
|||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
borderBottom: (theme) => `1px solid ${theme.palette.divider}`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, width: '100%', minWidth: 0 }}>
|
<CompactListItemText
|
||||||
<ListItemText
|
primary={
|
||||||
primary={(
|
<Typography>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
{item.common_name} ({item.cert_id})
|
||||||
<Box component="span">{`${item.common_name} (${item.cert_id})`}</Box>
|
{' '}
|
||||||
<Chip
|
<Chip
|
||||||
label={item.status === 'deleted' ? 'Deleted from Admin PKI' : item.status}
|
label={item.status === 'deleted' ? 'Deleted from Admin PKI' : item.status}
|
||||||
color={issuanceStatusColor(item.status)}
|
color={issuanceStatusColor(item.status)}
|
||||||
size="small"
|
size="small"
|
||||||
variant={item.status === 'issued' || item.status === 'valid' ? 'filled' : 'outlined'}
|
variant={item.status === 'deleted' ? 'outlined' : 'filled'}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Typography>
|
||||||
)}
|
}
|
||||||
secondary={(
|
secondary={
|
||||||
<Typography component="span" variant="body2" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
profile:{' '}
|
profile:{' '}
|
||||||
{profiles.some((profile) => profile.id === item.profile_id) ? (
|
{profiles.some((profile) => profile.id === item.profile_id) ? (
|
||||||
<Tooltip title={item.profile_id} arrow>
|
<Tooltip title={item.profile_id} arrow>
|
||||||
<Link
|
<Link
|
||||||
underline="hover"
|
underline="hover"
|
||||||
@@ -453,38 +452,36 @@ export default function ClientCertificatesPage() {
|
|||||||
) : (
|
) : (
|
||||||
item.profile_name
|
item.profile_name
|
||||||
)}
|
)}
|
||||||
{' '}· serial: {item.serial_hex} · perms: {(item.authz_permissions || []).join(', ') || '-'} · scope: {item.authz_scope || '-'} · uri: {item.san_uri} · valid: {fmt(item.not_before)} → {fmt(item.not_after)}{item.revocation_reason ? ` · reason: ${item.revocation_reason}` : ''}{item.status === 'deleted' ? ' · deleted from Admin PKI' : ''}
|
{' '}· serial: {item.serial_hex} · perms: {(item.authz_permissions || []).join(', ') || '-'} · scope: {item.authz_scope || '-'} · uri: {item.san_uri} · valid: {fmt(item.not_before)} → {fmt(item.not_after)}{item.revocation_reason ? ` · reason: ${item.revocation_reason}` : ''}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
}
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
/>
|
||||||
/>
|
<ListRowActions>
|
||||||
<ListRowActions>
|
<ListRowActionButton startIcon={<VisibilityIcon />} onClick={() => openView(item)} disabled={busy || item.status === 'deleted'}>
|
||||||
<ListRowActionButton startIcon={<VisibilityIcon />} onClick={() => openView(item)} disabled={busy || item.status === 'deleted'}>
|
View
|
||||||
View
|
</ListRowActionButton>
|
||||||
</ListRowActionButton>
|
<ListRowActionButton startIcon={<DownloadIcon />} onClick={() => downloadBundle(item)} disabled={busy || item.status === 'deleted'}>
|
||||||
<ListRowActionButton startIcon={<DownloadIcon />} onClick={() => downloadBundle(item)} disabled={busy || item.status === 'deleted'}>
|
Bundle
|
||||||
Bundle
|
</ListRowActionButton>
|
||||||
</ListRowActionButton>
|
<ListRowActionButton
|
||||||
<ListRowActionButton
|
color="error"
|
||||||
color="error"
|
startIcon={<BlockIcon />}
|
||||||
startIcon={<BlockIcon />}
|
onClick={() => setRevokeItem(item)}
|
||||||
onClick={() => setRevokeItem(item)}
|
disabled={busy || item.status === 'revoked' || item.status === 'deleted'}
|
||||||
disabled={busy || item.status === 'revoked' || item.status === 'deleted'}
|
>
|
||||||
>
|
Revoke
|
||||||
Revoke
|
</ListRowActionButton>
|
||||||
</ListRowActionButton>
|
</ListRowActions>
|
||||||
</ListRowActions>
|
|
||||||
</Box>
|
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
{!loading && issuances.length === 0 ? (
|
{!loading && issuances.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No client certificates issued yet." />
|
<CompactListItemText primary="No client certificates issued yet." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
{!loading && issuances.length > 0 && filteredIssuances.length === 0 ? (
|
{!loading && issuances.length > 0 && filteredIssuances.length === 0 ? (
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemText primary="No matching client certificates found." />
|
<CompactListItemText primary="No matching client certificates found." />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
) : null}
|
) : null}
|
||||||
</List>
|
</List>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Link,
|
Link,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -13,6 +15,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import Autocomplete from '../components/Autocomplete'
|
import Autocomplete from '../components/Autocomplete'
|
||||||
import HeaderActionButton from '../components/HeaderActionButton'
|
import HeaderActionButton from '../components/HeaderActionButton'
|
||||||
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import MonospaceTextField from '../components/MonospaceTextField'
|
import MonospaceTextField from '../components/MonospaceTextField'
|
||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import SSHCertificateInspectDialog from '../components/SSHCertificateInspectDialog'
|
import SSHCertificateInspectDialog from '../components/SSHCertificateInspectDialog'
|
||||||
@@ -378,42 +381,43 @@ export default function SSHCertificatesPage() {
|
|||||||
>
|
>
|
||||||
{issuancesLoading ? <Typography variant="body2" color="text.secondary">Loading...</Typography> : null}
|
{issuancesLoading ? <Typography variant="body2" color="text.secondary">Loading...</Typography> : null}
|
||||||
{!issuancesLoading && issuances.length === 0 ? <Typography variant="body2" color="text.secondary">No issuance history found.</Typography> : null}
|
{!issuancesLoading && issuances.length === 0 ? <Typography variant="body2" color="text.secondary">No issuance history found.</Typography> : null}
|
||||||
{issuances.map((item) => (
|
<List>
|
||||||
<Box
|
{issuances.map((item: SSHUserCAIssuance) => (
|
||||||
key={item.id}
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
sx={{
|
<CompactListItemText
|
||||||
p: 1,
|
primary={
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
<Typography>
|
||||||
}}
|
<Tooltip title={item.ca_id} arrow>
|
||||||
>
|
<Link
|
||||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, minWidth: 0 }}>
|
underline="hover"
|
||||||
<Box sx={{ display: 'grid', gap: 0.5, minWidth: 0, flexGrow: 1 }}>
|
onClick={() => openCAView(item).catch(() => {})}
|
||||||
<Typography variant="body2">
|
sx={{ cursor: 'default' }}
|
||||||
<Tooltip title={item.ca_id} arrow>
|
>
|
||||||
<Link
|
{item.ca_name || item.ca_id}
|
||||||
underline="hover"
|
</Link>
|
||||||
onClick={() => openCAView(item).catch(() => {})}
|
</Tooltip>
|
||||||
sx={{ cursor: 'default' }}
|
{' '}· serial: {item.serial} · key_id: {item.key_id}
|
||||||
>
|
</Typography>
|
||||||
{item.ca_name || item.ca_id}
|
}
|
||||||
</Link>
|
secondary={
|
||||||
</Tooltip>
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
{' '}· serial: {item.serial} · key_id: {item.key_id}
|
<Typography variant="caption" color="text.secondary">
|
||||||
</Typography>
|
{fmt(item.created_at)} · valid: {fmt(item.valid_after)} ~ {fmt(item.valid_before)}
|
||||||
<Typography variant="caption" color="text.secondary">
|
</Typography>
|
||||||
{fmt(item.created_at)} · valid: {fmt(item.valid_after)} ~ {fmt(item.valid_before)}
|
<Typography variant="caption" color="text.secondary">
|
||||||
</Typography>
|
fp: {item.source_public_key_fingerprint}
|
||||||
<Typography variant="caption" color="text.secondary">
|
</Typography>
|
||||||
fp: {item.source_public_key_fingerprint}
|
</Box>
|
||||||
</Typography>
|
}
|
||||||
</Box>
|
disableTypography
|
||||||
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton startIcon={<ContentCopyIcon />} onClick={() => copyIssuanceCert(item)}>Copy Cert</ListRowActionButton>
|
<ListRowActionButton startIcon={<ContentCopyIcon />} onClick={() => copyIssuanceCert(item)}>Copy Cert</ListRowActionButton>
|
||||||
<ListRowActionButton startIcon={<VisibilityIcon />} onClick={() => viewIssuanceCert(item)} disabled={inspectBusy}>{inspectBusy ? 'Loading...' : 'View'}</ListRowActionButton>
|
<ListRowActionButton startIcon={<VisibilityIcon />} onClick={() => viewIssuanceCert(item)} disabled={inspectBusy}>{inspectBusy ? 'Loading...' : 'View'}</ListRowActionButton>
|
||||||
</ListRowActions>
|
</ListRowActions>
|
||||||
</Box>
|
</ListItem>
|
||||||
</Box>
|
))}
|
||||||
))}
|
</List>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
|
|
||||||
<SSHCertificateInspectDialog
|
<SSHCertificateInspectDialog
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import DialogTitle from '@mui/material/DialogTitle'
|
|||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
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 ListItemText from '@mui/material/ListItemText'
|
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -22,6 +21,7 @@ import MonospaceTextField from '../components/MonospaceTextField'
|
|||||||
import SectionCard from '../components/SectionCard'
|
import SectionCard from '../components/SectionCard'
|
||||||
import SSHCredentialDetailsDialog from '../components/SSHCredentialDetailsDialog'
|
import SSHCredentialDetailsDialog from '../components/SSHCredentialDetailsDialog'
|
||||||
import { api, SSHCredential, SSHCredentialUpsertPayload } from '../api'
|
import { api, SSHCredential, SSHCredentialUpsertPayload } from '../api'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
name: string
|
name: string
|
||||||
@@ -145,15 +145,18 @@ export default function SSHCredentialsPage() {
|
|||||||
<List>
|
<List>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
<ListItemText
|
<CompactListItemText
|
||||||
primary={
|
primary={
|
||||||
<Typography>
|
<Typography>
|
||||||
{item.name}({item.id})
|
{item.name}({item.id})
|
||||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
secondary={`${item.fingerprint || item.id} · ${item.type} · Updated: ${fmt(item.updated_at)}`}
|
secondary={
|
||||||
sx={{ minWidth: 0, m: 0 }}
|
<Typography variant="caption" color="text.secondary">
|
||||||
|
{item.fingerprint || item.id} · {item.type} · Updated: {fmt(item.updated_at)}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import Alert from '@mui/material/Alert'
|
|||||||
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 Link from '@mui/material/Link'
|
import Link from '@mui/material/Link'
|
||||||
import Paper from '@mui/material/Paper'
|
import List from '@mui/material/List'
|
||||||
|
import ListItem from '@mui/material/ListItem'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import CompactListItemText from '../components/CompactListItemText'
|
||||||
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
|
||||||
import SSHCredentialsPromptDialog from '../components/SSHCredentialsPromptDialog'
|
import SSHCredentialsPromptDialog from '../components/SSHCredentialsPromptDialog'
|
||||||
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
|
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
|
||||||
@@ -614,34 +616,58 @@ export default function SSHServersPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const renderProfileList = (list: SSHAccessProfile[], emptyText: string) => (
|
const renderProfileList = (list: SSHAccessProfile[], emptyText: string) => (
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<Box>
|
||||||
{!list.length ? (
|
{!list.length ? (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{emptyText}
|
{emptyText}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
{list.map((item) => (
|
<List>
|
||||||
<Paper
|
{list.map((item: SSHAccessProfile) => (
|
||||||
key={item.id}
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
variant="outlined"
|
<CompactListItemText
|
||||||
sx={{
|
primary={
|
||||||
p: 1,
|
<Typography sx={{ wordBreak: 'break-word' }}>
|
||||||
display: 'grid',
|
{item.name}
|
||||||
gap: 0.5,
|
</Typography>
|
||||||
backgroundColor: 'transparent',
|
}
|
||||||
backgroundImage: 'none',
|
secondary={
|
||||||
borderLeft: 'none',
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
borderRight: 'none',
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
borderTop: 'none',
|
{item.server_target_type === 'group' ? (
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
<Link
|
||||||
borderRadius: 0,
|
underline="hover"
|
||||||
boxShadow: 'none'
|
onClick={() => void openServerGroupView(item)}
|
||||||
}}
|
sx={{ cursor: 'default' }}
|
||||||
>
|
>
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
Group: {item.server_group?.name || item.server_group_id || '-'}
|
||||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
</Link>
|
||||||
{item.name}
|
) : (
|
||||||
</Typography>
|
<Link
|
||||||
|
underline="hover"
|
||||||
|
onClick={() => openSharedServerView(item).catch(() => {})}
|
||||||
|
sx={{ cursor: 'default' }}
|
||||||
|
>
|
||||||
|
Server: {item.server?.name}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{item.server_target_type === 'group' ? '' : ` · ${item.server?.host}:${item.server?.port}`} · {item.remote_username} · {item.auth_method} · 2FA: {item.second_factor_mode || 'none'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
|
Key fingerprint:{' '}
|
||||||
|
{item.ssh_credential_id ? (
|
||||||
|
<Link
|
||||||
|
underline="hover"
|
||||||
|
onClick={() => void openCredentialView(item)}
|
||||||
|
sx={{ cursor: 'default' }}
|
||||||
|
>
|
||||||
|
{item.auth_public_key_fingerprint || item.ssh_credential_id}
|
||||||
|
</Link>
|
||||||
|
) : item.auth_public_key_fingerprint || '-'}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
|
||||||
{item.owner_scope === 'user' ? (
|
{item.owner_scope === 'user' ? (
|
||||||
@@ -656,73 +682,44 @@ export default function SSHServersPage() {
|
|||||||
{connectingID === item.id ? 'Connecting...' : 'Connect'}
|
{connectingID === item.id ? 'Connecting...' : 'Connect'}
|
||||||
</ListRowActionButton>
|
</ListRowActionButton>
|
||||||
</ListRowActions>
|
</ListRowActions>
|
||||||
</Box>
|
</ListItem>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
))}
|
||||||
{item.server_target_type === 'group' ? (
|
</List>
|
||||||
<Link
|
|
||||||
underline="hover"
|
|
||||||
onClick={() => void openServerGroupView(item)}
|
|
||||||
sx={{ cursor: 'default' }}
|
|
||||||
>
|
|
||||||
Group: {item.server_group?.name || item.server_group_id || '-'}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
underline="hover"
|
|
||||||
onClick={() => openSharedServerView(item).catch(() => {})}
|
|
||||||
sx={{ cursor: 'default' }}
|
|
||||||
>
|
|
||||||
Server: {item.server?.name}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{item.server_target_type === 'group' ? '' : ` · ${item.server?.host}:${item.server?.port}`} · {item.remote_username} · {item.auth_method} · 2FA: {item.second_factor_mode || 'none'}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Key fingerprint:{' '}
|
|
||||||
{item.ssh_credential_id ? (
|
|
||||||
<Link
|
|
||||||
underline="hover"
|
|
||||||
onClick={() => void openCredentialView(item)}
|
|
||||||
sx={{ cursor: 'default' }}
|
|
||||||
>
|
|
||||||
{item.auth_public_key_fingerprint || item.ssh_credential_id}
|
|
||||||
</Link>
|
|
||||||
) : item.auth_public_key_fingerprint || '-'}
|
|
||||||
</Typography>
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderServerList = (list: SSHServer[], emptyText: string) => (
|
const renderServerList = (list: SSHServer[], emptyText: string) => (
|
||||||
<Box sx={{ display: 'grid', gap: 0 }}>
|
<Box>
|
||||||
{!list.length ? (
|
{!list.length ? (
|
||||||
<Typography variant="body2" color="text.secondary">
|
<Typography variant="body2" color="text.secondary">
|
||||||
{emptyText}
|
{emptyText}
|
||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
{list.map((item) => (
|
<List>
|
||||||
<Paper
|
{list.map((item: SSHServer) => (
|
||||||
key={item.id}
|
<ListItem key={item.id} divider sx={{ alignItems: 'flex-start' }}>
|
||||||
variant="outlined"
|
<CompactListItemText
|
||||||
sx={{
|
primary={
|
||||||
p: 1,
|
<Typography sx={{ wordBreak: 'break-word' }}>
|
||||||
display: 'grid',
|
{item.name}
|
||||||
gap: 0.5,
|
</Typography>
|
||||||
backgroundColor: 'transparent',
|
}
|
||||||
backgroundImage: 'none',
|
secondary={
|
||||||
borderLeft: 'none',
|
<Box sx={{ display: 'grid', gap: 0.5 }}>
|
||||||
borderRight: 'none',
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
borderTop: 'none',
|
{item.host}:{item.port} · {item.enabled ? 'enabled' : 'disabled'}
|
||||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
</Typography>
|
||||||
borderRadius: 0,
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
boxShadow: 'none'
|
Tags: {(item.tags || []).join(', ') || '-'} · {item.id}
|
||||||
}}
|
</Typography>
|
||||||
>
|
{item.description ? (
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexWrap: 'wrap' }}>
|
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||||
<Typography variant="subtitle2" sx={{ wordBreak: 'break-word' }}>
|
{item.description}
|
||||||
{item.name}
|
</Typography>
|
||||||
</Typography>
|
) : null}
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ListRowActions>
|
<ListRowActions>
|
||||||
<ListRowActionButton onClick={() => setViewServerItem(item)}>View</ListRowActionButton>
|
<ListRowActionButton onClick={() => setViewServerItem(item)}>View</ListRowActionButton>
|
||||||
<ListRowActionButton onClick={() => openServerEdit(item)}>Edit</ListRowActionButton>
|
<ListRowActionButton onClick={() => openServerEdit(item)}>Edit</ListRowActionButton>
|
||||||
@@ -731,20 +728,9 @@ export default function SSHServersPage() {
|
|||||||
</ListRowActionButton>
|
</ListRowActionButton>
|
||||||
<ListRowActionButton onClick={() => void openHostKeys(item)}>Host Keys</ListRowActionButton>
|
<ListRowActionButton onClick={() => void openHostKeys(item)}>Host Keys</ListRowActionButton>
|
||||||
</ListRowActions>
|
</ListRowActions>
|
||||||
</Box>
|
</ListItem>
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
))}
|
||||||
{item.host}:{item.port} · {item.enabled ? 'enabled' : 'disabled'}
|
</List>
|
||||||
</Typography>
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
Tags: {(item.tags || []).join(', ') || '-'} · {item.id}
|
|
||||||
</Typography>
|
|
||||||
{item.description ? (
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
|
||||||
{item.description}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</Paper>
|
|
||||||
))}
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user