Compare commits

..

4 Commits

35 changed files with 466 additions and 386 deletions
@@ -0,0 +1,68 @@
import Alert from '@mui/material/Alert'
import Button from '@mui/material/Button'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogTitle from '@mui/material/DialogTitle'
import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography'
import FormDialogContent from './FormDialogContent'
type ConfirmDeleteDialogProps = {
open: boolean
title: string
prompt: string
expectedText?: string
confirmLabel?: string
confirmValue?: string
onConfirmValueChange?: (value: string) => void
error?: string | null
busy?: boolean
confirmText?: string
busyText?: string
confirmDisabled?: boolean
onClose: () => void
onConfirm: () => void
}
export default function ConfirmDeleteDialog(props: ConfirmDeleteDialogProps) {
const requiresTypedConfirm: boolean = props.expectedText !== undefined
const inputMatches: boolean = !requiresTypedConfirm || props.confirmValue?.trim() === props.expectedText
return (
<Dialog open={props.open} onClose={props.onClose} maxWidth="xs" fullWidth>
<DialogTitle>{props.title}</DialogTitle>
<FormDialogContent>
{props.error ? <Alert severity="error">{props.error}</Alert> : null}
<Typography variant="body2" color="text.secondary">{props.prompt}</Typography>
{requiresTypedConfirm ? (
<>
{props.expectedText ? (
<Typography variant="caption" color="text.secondary">
{props.expectedText}
</Typography>
) : null}
<TextField
autoFocus
fullWidth
size="small"
label={props.confirmLabel || 'Confirm'}
value={props.confirmValue || ''}
onChange={(event) => props.onConfirmValueChange?.(event.target.value)}
/>
</>
) : null}
</FormDialogContent>
<DialogActions>
<Button
color="error"
variant="contained"
disabled={Boolean(props.busy) || Boolean(props.confirmDisabled) || !inputMatches}
onClick={props.onConfirm}
>
{props.busy ? (props.busyText || 'Deleting...') : (props.confirmText || 'Delete')}
</Button>
<Button onClick={props.onClose} disabled={Boolean(props.busy)}>Cancel</Button>
</DialogActions>
</Dialog>
)
}
@@ -94,7 +94,7 @@ export default function PKICertDetailsDialog(props: PKICertDetailsDialogProps) {
{item && props.onDownloadBundle ? <Button onClick={() => props.onDownloadBundle?.(item)}>Download Bundle</Button> : null}
{item && props.onDownloadCert ? <Button onClick={() => props.onDownloadCert?.(item)}>Download Cert</Button> : null}
{item && props.onDownloadKey ? <Button onClick={() => props.onDownloadKey?.(item)}>Download Key</Button> : null}
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -52,7 +52,7 @@ export default function PKIClientProfileDetailsDialog(props: PKIClientProfileDet
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -120,10 +120,10 @@ export default function PKIClientProfileFormDialog(props: PKIClientProfileFormDi
<FormControlLabel control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />} label="Enabled" />
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose} disabled={props.busy}>Cancel</Button>
<Button variant="contained" onClick={props.onSave} disabled={props.busy}>
{props.busy ? 'Saving...' : props.editID ? 'Save' : 'Create'}
</Button>
<Button onClick={props.onClose} disabled={props.busy}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -61,7 +61,7 @@ export default function SSHAccessProfileDetailsDialog(props: SSHAccessProfileDet
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -194,12 +194,12 @@ export default function SSHAccessProfileFormDialog(props: SSHAccessProfileFormDi
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose} disabled={props.saving}>
Cancel
</Button>
<Button variant="contained" onClick={props.onSave} disabled={props.saving}>
Save
</Button>
<Button onClick={props.onClose} disabled={props.saving}>
Cancel
</Button>
</DialogActions>
</Dialog>
)
@@ -56,7 +56,7 @@ export default function SSHPrincipalGrantDetailsDialog(props: SSHPrincipalGrantD
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -114,7 +114,7 @@ export default function SSHPrincipalGrantFormDialog(props: SSHPrincipalGrantForm
>
{props.busy ? 'Saving...' : 'Save'}
</Button>
<Button onClick={props.onClose} disabled={props.busy}>Close</Button>
<Button onClick={props.onClose} disabled={props.busy}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -44,7 +44,7 @@ export default function SSHServerDetailsDialog(props: SSHServerDetailsDialogProp
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -73,12 +73,12 @@ export default function SSHServerFormDialog(props: SSHServerFormDialogProps) {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={props.onClose} disabled={props.saving}>
Cancel
</Button>
<Button variant="contained" onClick={props.onSave} disabled={props.saving}>
Save
</Button>
<Button onClick={props.onClose} disabled={props.saving}>
Cancel
</Button>
</DialogActions>
</Dialog>
)
@@ -42,7 +42,7 @@ export default function SSHUserCADetailsDialog(props: SSHUserCADetailsDialogProp
<DialogActions>
{item && props.onDownloadPublic ? <Button startIcon={<DownloadIcon />} onClick={() => props.onDownloadPublic?.(item)}>Download Public</Button> : null}
{item && props.onDownloadPrivate ? <Button startIcon={<DownloadIcon />} color="warning" onClick={() => props.onDownloadPrivate?.(item)}>Download Private</Button> : null}
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -111,7 +111,7 @@ export default function SSHUserCAFormDialog(props: SSHUserCAFormDialogProps) {
>
{props.busy ? 'Saving...' : 'Save'}
</Button>
<Button onClick={props.onClose}>Close</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
@@ -0,0 +1,97 @@
import Alert from '@mui/material/Alert'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Checkbox from '@mui/material/Checkbox'
import Dialog from '@mui/material/Dialog'
import DialogActions from '@mui/material/DialogActions'
import DialogTitle from '@mui/material/DialogTitle'
import FormControlLabel from '@mui/material/FormControlLabel'
import TextField from '@mui/material/TextField'
import FormDialogContent from './FormDialogContent'
import { User } from '../api'
export type UserFormState = {
username: string
displayName: string
email: string
password: string
passwordConfirm: string
isAdmin: boolean
canCreateProject: boolean
}
type UserFormDialogProps = {
open: boolean
item: User | null
dialogError: string | null
saving: boolean
form: UserFormState
setForm: React.Dispatch<React.SetStateAction<UserFormState>>
onClose: () => void
onSave: () => void
}
export default function UserFormDialog(props: UserFormDialogProps) {
const isEdit: boolean = Boolean(props.item)
const passwordMismatch: boolean = props.form.passwordConfirm !== '' && props.form.password !== props.form.passwordConfirm
return (
<Dialog open={props.open} onClose={props.onClose} maxWidth="sm" fullWidth>
<DialogTitle>{isEdit ? 'Edit User' : 'New User'}</DialogTitle>
<FormDialogContent>
{props.dialogError ? <Alert severity="error" sx={{ mb: 1 }}>{props.dialogError}</Alert> : null}
<Box sx={{ display: 'grid', gap: 1, mt: 1 }}>
<TextField
name="username"
label="Username"
value={isEdit ? (props.item?.username || '') : props.form.username}
onChange={(event) => props.setForm((prev) => ({ ...prev, username: event.target.value }))}
disabled={isEdit}
/>
<TextField
name="display_name"
label="Display Name"
value={props.form.displayName}
onChange={(event) => props.setForm((prev) => ({ ...prev, displayName: event.target.value }))}
/>
<TextField
name="email"
label="Email"
value={props.form.email}
onChange={(event) => props.setForm((prev) => ({ ...prev, email: event.target.value }))}
/>
<TextField
name="password"
label={isEdit ? 'New Password (optional)' : 'Password'}
type="password"
value={props.form.password}
onChange={(event) => props.setForm((prev) => ({ ...prev, password: event.target.value }))}
/>
<TextField
name="password_confirm"
label={isEdit ? 'Confirm New Password' : 'Confirm Password'}
type="password"
value={props.form.passwordConfirm}
error={passwordMismatch}
helperText={passwordMismatch ? 'Passwords do not match.' : ''}
onChange={(event) => props.setForm((prev) => ({ ...prev, passwordConfirm: event.target.value }))}
/>
<FormControlLabel
control={<Checkbox checked={props.form.isAdmin} onChange={(event) => props.setForm((prev) => ({ ...prev, isAdmin: event.target.checked }))} />}
label="Admin"
/>
<FormControlLabel
control={<Checkbox checked={props.form.canCreateProject} onChange={(event) => props.setForm((prev) => ({ ...prev, canCreateProject: event.target.checked }))} />}
label="Allow project creation (direct grant)"
/>
</Box>
</FormDialogContent>
<DialogActions>
<Button variant="contained" onClick={props.onSave} disabled={props.saving}>
{props.saving ? (isEdit ? 'Saving...' : 'Creating...') : (isEdit ? 'Save' : 'Create')}
</Button>
<Button onClick={props.onClose}>Cancel</Button>
</DialogActions>
</Dialog>
)
}
+6 -6
View File
@@ -266,9 +266,6 @@ export default function AdminApiKeysPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => { setDeleteTarget(null); setDeleteConfirm('') }}>
Cancel
</Button>
<Button
variant="contained"
color="error"
@@ -277,6 +274,9 @@ export default function AdminApiKeysPage() {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => { setDeleteTarget(null); setDeleteConfirm('') }}>
Cancel
</Button>
</DialogActions>
</Dialog>
<Dialog open={bulkOpen} onClose={() => setBulkOpen(false)} maxWidth="xs" fullWidth>
@@ -293,9 +293,6 @@ export default function AdminApiKeysPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => { setBulkOpen(false); setBulkConfirm('') }}>
Cancel
</Button>
<Button
variant="contained"
color="error"
@@ -304,6 +301,9 @@ export default function AdminApiKeysPage() {
>
{bulkDeleting ? 'Deleting...' : 'Delete Selected'}
</Button>
<Button onClick={() => { setBulkOpen(false); setBulkConfirm('') }}>
Cancel
</Button>
</DialogActions>
</Dialog>
</Box>
@@ -375,7 +375,6 @@ export default function AdminPKIClientProfilesPage() {
<TextField label="Confirm Name" value={deleteConfirm} onChange={(event) => setDeleteConfirm(event.target.value)} autoFocus />
</FormDialogContent>
<DialogActions>
<Button onClick={() => { setDeleteItem(null); setDeleteConfirm('') }} disabled={busy}>Cancel</Button>
<Button
color="error"
variant="contained"
@@ -384,6 +383,7 @@ export default function AdminPKIClientProfilesPage() {
>
{busy ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => { setDeleteItem(null); setDeleteConfirm('') }} disabled={busy}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+13 -13
View File
@@ -990,7 +990,6 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setACMEOpen(false)}>Cancel</Button>
<Button
variant="contained"
onClick={saveACME}
@@ -1004,6 +1003,7 @@ export default function AdminPKIPage() {
>
{busy ? 'Saving...' : 'Save'}
</Button>
<Button onClick={() => setACMEOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1018,8 +1018,8 @@ export default function AdminPKIPage() {
<Typography variant="body2" color="text.secondary">Delete profile permanently?</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setACMEDeleteID('')}>Cancel</Button>
<Button color="error" variant="contained" onClick={deleteACME} disabled={busy}>{busy ? 'Working...' : 'Delete'}</Button>
<Button onClick={() => setACMEDeleteID('')}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1085,10 +1085,10 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setACMEOrderOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={createACMEOrder} disabled={busy || !acmeOrderProfileID || !acmeOrderCommonName.trim()}>
{busy ? 'Saving...' : 'Create'}
</Button>
<Button onClick={() => setACMEOrderOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1103,8 +1103,8 @@ export default function AdminPKIPage() {
<Typography variant="body2" color="text.secondary">Delete order permanently?</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setACMEOrderDeleteID('')}>Cancel</Button>
<Button color="error" variant="contained" onClick={deleteACMEOrder} disabled={busy}>{busy ? 'Working...' : 'Delete'}</Button>
<Button onClick={() => setACMEOrderDeleteID('')}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1133,7 +1133,6 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setACMEFinalizeID('')}>Cancel</Button>
<Button
variant="contained"
onClick={() => finalizeACMEOrder(acmeFinalizeID, acmeFinalizeMode)}
@@ -1141,6 +1140,7 @@ export default function AdminPKIPage() {
>
{busy ? 'Working...' : 'Finalize'}
</Button>
<Button onClick={() => setACMEFinalizeID('')}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1219,8 +1219,8 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setRootOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={createRoot} disabled={busy}>{busy ? 'Saving...' : 'Create'}</Button>
<Button onClick={() => setRootOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1277,8 +1277,8 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setInterOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={createIntermediate} disabled={busy}>{busy ? 'Saving...' : 'Create'}</Button>
<Button onClick={() => setInterOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1304,8 +1304,8 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setIssueOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={issueCert} disabled={busy}>{busy ? 'Saving...' : 'Issue'}</Button>
<Button onClick={() => setIssueOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1355,8 +1355,8 @@ export default function AdminPKIPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setImportOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={importCert} disabled={busy}>{busy ? 'Saving...' : 'Import'}</Button>
<Button onClick={() => setImportOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1371,8 +1371,8 @@ export default function AdminPKIPage() {
<TextField fullWidth label="Reason (optional)" value={revokeReason} onChange={(event) => setRevokeReason(event.target.value)} />
</FormDialogContent>
<DialogActions>
<Button onClick={() => setRevokeID('')}>Cancel</Button>
<Button color="warning" variant="contained" onClick={revokeCert} disabled={busy}>{busy ? 'Working...' : 'Revoke'}</Button>
<Button onClick={() => setRevokeID('')}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1387,8 +1387,8 @@ export default function AdminPKIPage() {
<Typography variant="body2" color="text.secondary">Delete certificate permanently?</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteID('')}>Cancel</Button>
<Button color="error" variant="contained" onClick={deleteCert} disabled={busy}>{busy ? 'Working...' : 'Delete'}</Button>
<Button onClick={() => setDeleteID('')}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1410,7 +1410,6 @@ export default function AdminPKIPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteCAID('')}>Cancel</Button>
<Button
color="error"
variant="contained"
@@ -1419,6 +1418,7 @@ export default function AdminPKIPage() {
>
{busy ? 'Working...' : 'Delete CA'}
</Button>
<Button onClick={() => setDeleteCAID('')}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -1468,10 +1468,10 @@ export default function AdminPKIPage() {
</SelectField>
</FormDialogContent>
<DialogActions>
<Button onClick={() => { setEditCAID(''); setEditCAParent('') }}>Cancel</Button>
<Button variant="contained" onClick={saveCAEdit} disabled={busy || !editCAName.trim()}>
{busy ? 'Saving...' : 'Save'}
</Button>
<Button onClick={() => { setEditCAID(''); setEditCAParent('') }}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -10,6 +10,7 @@ import TextField from '@mui/material/TextField'
import Tooltip from '@mui/material/Tooltip'
import Typography from '@mui/material/Typography'
import { useEffect, useMemo, useState } from 'react'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import FormDialogContent from '../components/FormDialogContent'
import SSHAccessProfileDetailsDialog from '../components/SSHAccessProfileDetailsDialog'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
@@ -54,6 +55,7 @@ export default function AdminSSHAccessProfilesPage() {
const [dialogError, setDialogError] = useState<string | null>(null)
const [saving, setSaving] = useState(false)
const [deleteItem, setDeleteItem] = useState<SSHAccessProfile | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState('')
const [viewItem, setViewItem] = useState<SSHAccessProfile | null>(null)
const [viewServerItem, setViewServerItem] = useState<SSHServer | null>(null)
const [viewCAItem, setViewCAItem] = useState<SSHUserCA | null>(null)
@@ -279,10 +281,12 @@ export default function AdminSSHAccessProfilesPage() {
try {
await api.deleteSSHAccessProfileAdmin(deleteItem.id)
setDeleteItem(null)
setDeleteConfirm('')
await load()
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete SSH access profile')
setDeleteItem(null)
setDeleteConfirm('')
}
}
@@ -348,7 +352,7 @@ export default function AdminSSHAccessProfilesPage() {
<ListRowActions>
<ListRowActionButton onClick={() => setViewItem(item)}>View</ListRowActionButton>
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
<ListRowActionButton color="error" onClick={() => setDeleteItem(item)}>
<ListRowActionButton color="error" onClick={() => { setDeleteItem(item); setDeleteConfirm('') }}>
Delete
</ListRowActionButton>
</ListRowActions>
@@ -437,20 +441,18 @@ export default function AdminSSHAccessProfilesPage() {
onDownloadPrivate={downloadCAPrivateKey}
/>
<Dialog open={Boolean(deleteItem)} onClose={() => setDeleteItem(null)} maxWidth="xs" fullWidth>
<DialogTitle>Delete SSH Access Profile</DialogTitle>
<FormDialogContent>
<Typography variant="body2">
Delete <strong>{deleteItem?.name}</strong>?
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
<Button variant="contained" color="error" onClick={handleDelete}>
Delete
</Button>
</DialogActions>
</Dialog>
<ConfirmDeleteDialog
open={Boolean(deleteItem)}
title="Delete SSH Access Profile"
prompt="Type the profile name to confirm deletion."
expectedText={deleteItem?.name}
confirmLabel="Profile Name"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
confirmDisabled={!deleteItem}
onConfirm={handleDelete}
onClose={() => { setDeleteItem(null); setDeleteConfirm('') }}
/>
</Box>
)
}
@@ -16,6 +16,7 @@ import {
Typography
} from '@mui/material'
import { useEffect, useState } from 'react'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import HeaderActionButton from '../components/HeaderActionButton'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
import SectionCard from '../components/SectionCard'
@@ -349,36 +350,20 @@ export default function AdminSSHPrincipalGrantsPage() {
onSave={save}
/>
<Dialog open={!!deleteItem} onClose={() => { if (!busy) setDeleteItem(null) }} maxWidth="xs" fullWidth>
<DialogTitle>Delete Principal Grant</DialogTitle>
<FormDialogContent>
{dialogError ? <Alert severity="error">{dialogError}</Alert> : null}
<Typography variant="body2">
Type the grant name to confirm deletion.
</Typography>
<Typography variant="caption" color="text.secondary">
{deleteItem ? `${deleteItem.name || deleteItem.principal}` : ''}
</Typography>
<TextField
autoFocus
size="small"
label="Grant Name"
value={deleteConfirm}
onChange={(event) => setDeleteConfirm(event.target.value)}
<ConfirmDeleteDialog
open={Boolean(deleteItem)}
title="Delete Principal Grant"
prompt="Type the grant name to confirm deletion."
expectedText={deleteItem ? (deleteItem.name || deleteItem.principal) : undefined}
confirmLabel="Grant Name"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
error={dialogError}
busy={busy}
confirmDisabled={!deleteItem}
onConfirm={remove}
onClose={() => { if (!busy) setDeleteItem(null) }}
/>
</FormDialogContent>
<DialogActions>
<Button
color="error"
variant="contained"
onClick={remove}
disabled={busy || !deleteItem || deleteConfirm.trim() !== (deleteItem.name || deleteItem.principal)}
>
{busy ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteItem(null)} disabled={busy}>Close</Button>
</DialogActions>
</Dialog>
</Box>
)
}
+2 -2
View File
@@ -350,10 +350,10 @@ export default function AdminSSHServersPage() {
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
<Button variant="contained" color="error" onClick={handleDelete}>
Delete
</Button>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -443,7 +443,7 @@ export default function AdminSSHServersPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={closeHostKeys}>Close</Button>
<Button onClick={closeHostKeys}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+18 -27
View File
@@ -15,6 +15,7 @@ import {
Typography
} from '@mui/material'
import { useEffect, useState } from 'react'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import HeaderActionButton from '../components/HeaderActionButton'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
import SectionCard from '../components/SectionCard'
@@ -362,31 +363,21 @@ export default function AdminSSHUserCAPage() {
onLoadPrivateKeyFile={loadPrivateKeyFile}
/>
<Dialog open={Boolean(deleteItem)} onClose={() => setDeleteItem(null)} maxWidth="xs" fullWidth>
<DialogTitle>
<Box sx={{ display: 'grid', gap: 1 }}>
<Typography variant="h6">Delete SSH User CA</Typography>
{dialogError ? <Alert severity="error">{dialogError}</Alert> : null}
</Box>
</DialogTitle>
<FormDialogContent>
<Box sx={{ display: 'grid', gap: 1 }}>
<Typography variant="body2" color="text.secondary">Type CA name to confirm deletion.</Typography>
<TextField size="small" label="Confirm Name" value={deleteConfirm} onChange={(event) => setDeleteConfirm(event.target.value)} />
</Box>
</FormDialogContent>
<DialogActions>
<Button
variant="contained"
color="error"
onClick={remove}
disabled={busy || !deleteItem || deleteConfirm.trim() !== deleteItem.name}
>
{busy ? 'Working...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
</DialogActions>
</Dialog>
<ConfirmDeleteDialog
open={Boolean(deleteItem)}
title="Delete SSH User CA"
prompt="Type CA name to confirm deletion."
expectedText={deleteItem?.name}
confirmLabel="Confirm Name"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
error={dialogError}
busy={busy}
busyText="Working..."
confirmDisabled={!deleteItem}
onConfirm={remove}
onClose={() => setDeleteItem(null)}
/>
<SSHUserCADetailsDialog
item={viewItem}
@@ -447,7 +438,7 @@ export default function AdminSSHUserCAPage() {
>
{busy ? 'Working...' : 'Sign'}
</Button>
<Button onClick={() => setSignOpen(false)}>Close</Button>
<Button onClick={() => setSignOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -464,7 +455,7 @@ export default function AdminSSHUserCAPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setInspectOpen(false)}>Close</Button>
<Button onClick={() => setInspectOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
@@ -733,8 +733,8 @@ export default function AdminServicePrincipalsPage() {
<TextField label="Description" value={description} onChange={(event) => setDescription(event.target.value)} />
</FormDialogContent>
<DialogActions>
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={createPrincipal} disabled={busy}>{busy ? 'Saving...' : 'Create'}</Button>
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -810,8 +810,8 @@ export default function AdminServicePrincipalsPage() {
</SelectField>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setBindingOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={upsertBinding} disabled={busy}>{busy ? 'Saving...' : 'Save'}</Button>
<Button onClick={() => setBindingOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -354,10 +354,10 @@ export default function AdminTLSAuthPoliciesPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={handleSave} disabled={saving}>
{saving ? 'Saving...' : editingID ? 'Save' : 'Create'}
</Button>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -369,10 +369,10 @@ export default function AdminTLSAuthPoliciesPage() {
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
<Button variant="contained" color="error" onClick={handleDelete}>
Delete
</Button>
<Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+57 -7
View File
@@ -74,6 +74,9 @@ export default function AdminTLSSettingsPage() {
const [confirmMessage, setConfirmMessage] = useState('')
const [confirmLabel, setConfirmLabel] = useState('Confirm')
const [confirmColor, setConfirmColor] = useState<'primary' | 'warning' | 'error'>('primary')
const [confirmRequiresText, setConfirmRequiresText] = useState(false)
const [confirmExpectedText, setConfirmExpectedText] = useState('')
const [confirmInput, setConfirmInput] = useState('')
const [confirmAction, setConfirmAction] = useState<null | (() => Promise<void>)>(null)
const policyNameByID = useMemo(() => {
const map: Record<string, string> = {}
@@ -324,6 +327,9 @@ export default function AdminTLSSettingsPage() {
setConfirmMessage(`Do you want to ${nextEnabled ? 'enable' : 'disable'} listener "${item.name}"?`)
setConfirmLabel(nextEnabled ? 'Enable' : 'Disable')
setConfirmColor(nextEnabled ? 'primary' : 'warning')
setConfirmRequiresText(false)
setConfirmExpectedText('')
setConfirmInput('')
setConfirmAction(() => async () => {
setError(null)
try {
@@ -340,9 +346,12 @@ export default function AdminTLSSettingsPage() {
const handleDeleteListener = async (item: TLSListener) => {
setConfirmTitle('Delete Listener')
setConfirmMessage(`Delete listener "${item.name}"?`)
setConfirmMessage('Type the listener name to confirm deletion.')
setConfirmLabel('Delete')
setConfirmColor('error')
setConfirmRequiresText(true)
setConfirmExpectedText(item.name)
setConfirmInput('')
setConfirmAction(() => async () => {
setError(null)
try {
@@ -364,6 +373,9 @@ export default function AdminTLSSettingsPage() {
}
await confirmAction()
setConfirmOpen(false)
setConfirmRequiresText(false)
setConfirmExpectedText('')
setConfirmInput('')
setConfirmAction(null)
}
@@ -376,7 +388,7 @@ export default function AdminTLSSettingsPage() {
title="Listeners"
actions={
<Button variant="outlined" onClick={openCreateDialog}>
Add Listener
New Listener
</Button>
}
>
@@ -493,7 +505,7 @@ export default function AdminTLSSettingsPage() {
<Dialog open={dialogOpen} onClose={() => setDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle>
<Box sx={{ display: 'grid', gap: 1 }}>
<Typography variant="h6">{editingMain ? 'Edit Main Listener' : editingID ? 'Edit Listener' : 'Add Listener'}</Typography>
<Typography variant="h6">{editingMain ? 'Edit Main Listener' : editingID ? 'Edit Listener' : 'New Listener'}</Typography>
{listenerError ? <Alert severity="error">{listenerError}</Alert> : null}
{editingMain ? (
<Alert severity="info">Main listener transport changes are saved now but require backend restart to take effect.</Alert>
@@ -603,23 +615,61 @@ export default function AdminTLSSettingsPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={handleSaveDialog} disabled={listenerSaving}>
{listenerSaving ? 'Saving...' : editingMain || editingID ? 'Save' : 'Create'}
</Button>
<Button onClick={() => setDialogOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={confirmOpen} onClose={() => setConfirmOpen(false)} maxWidth="xs" fullWidth>
<Dialog
open={confirmOpen}
onClose={() => {
setConfirmOpen(false)
setConfirmRequiresText(false)
setConfirmExpectedText('')
setConfirmInput('')
}}
maxWidth="xs"
fullWidth
>
<DialogTitle>{confirmTitle}</DialogTitle>
<FormDialogContent>
<Typography variant="body2">{confirmMessage}</Typography>
{confirmRequiresText ? (
<>
<Typography variant="caption" color="text.secondary">
{confirmExpectedText}
</Typography>
<TextField
autoFocus
size="small"
label="Listener Name"
value={confirmInput}
onChange={(event) => setConfirmInput(event.target.value)}
/>
</>
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={() => setConfirmOpen(false)}>Cancel</Button>
<Button variant="contained" color={confirmColor} onClick={handleConfirm}>
<Button
variant="contained"
color={confirmColor}
onClick={handleConfirm}
disabled={confirmRequiresText && confirmInput.trim() !== confirmExpectedText}
>
{confirmLabel}
</Button>
<Button
onClick={() => {
setConfirmOpen(false)
setConfirmRequiresText(false)
setConfirmExpectedText('')
setConfirmInput('')
}}
>
Cancel
</Button>
</DialogActions>
</Dialog>
</Box>
+2 -2
View File
@@ -477,10 +477,10 @@ export default function AdminUserGroupsPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setEditOpen(false)}>Cancel</Button>
<Button variant="contained" onClick={saveGroup} disabled={busy || !name.trim()}>
{busy ? 'Saving...' : 'Save'}
</Button>
<Button onClick={() => setEditOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -498,7 +498,6 @@ export default function AdminUserGroupsPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
<Button
color="error"
variant="contained"
@@ -507,6 +506,7 @@ export default function AdminUserGroupsPage() {
>
{busy ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+72 -161
View File
@@ -4,12 +4,10 @@ import FormDialogContent from '../components/FormDialogContent'
import {
Box,
Button,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
FormControlLabel,
List,
ListItem,
ListItemText,
@@ -17,38 +15,41 @@ import {
Typography
} from '@mui/material'
import { useEffect, useState } from 'react'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import HeaderActionButton from '../components/HeaderActionButton'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
import SectionCard from '../components/SectionCard'
import UserFormDialog, { UserFormState } from '../components/UserFormDialog'
import { api, SubjectPermission, User, UserCreatePayload, UserUpdatePayload } from '../api'
const projectCreatePermission = 'project.create'
function createEmptyForm(): UserFormState {
return {
username: '',
displayName: '',
email: '',
password: '',
passwordConfirm: '',
isAdmin: false,
canCreateProject: false
}
}
export default function AdminUsersPage() {
const [users, setUsers] = useState<User[]>([])
const [subjectPermissions, setSubjectPermissions] = useState<SubjectPermission[]>([])
const [createOpen, setCreateOpen] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
const [creating, setCreating] = useState(false)
const [username, setUsername] = useState('')
const [displayName, setDisplayName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [passwordConfirm, setPasswordConfirm] = useState('')
const [isAdmin, setIsAdmin] = useState(false)
const [createCanCreateProject, setCreateCanCreateProject] = useState(false)
const [createForm, setCreateForm] = useState<UserFormState>(createEmptyForm)
const [error, setError] = useState<string | null>(null)
const [deletingUser, setDeletingUser] = useState<User | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState('')
const [deleting, setDeleting] = useState(false)
const [togglingID, setTogglingID] = useState('')
const [editUser, setEditUser] = useState<User | null>(null)
const [editDisplayName, setEditDisplayName] = useState('')
const [editEmail, setEditEmail] = useState('')
const [editPassword, setEditPassword] = useState('')
const [editPasswordConfirm, setEditPasswordConfirm] = useState('')
const [editIsAdmin, setEditIsAdmin] = useState(false)
const [editCanCreateProject, setEditCanCreateProject] = useState(false)
const [editForm, setEditForm] = useState<UserFormState>(createEmptyForm)
const [editError, setEditError] = useState<string | null>(null)
const [savingEdit, setSavingEdit] = useState(false)
@@ -73,13 +74,7 @@ export default function AdminUsersPage() {
}
const resetCreateForm = () => {
setUsername('')
setDisplayName('')
setEmail('')
setPassword('')
setPasswordConfirm('')
setIsAdmin(false)
setCreateCanCreateProject(false)
setCreateForm(createEmptyForm())
setCreateError(null)
}
@@ -88,28 +83,27 @@ export default function AdminUsersPage() {
resetCreateForm()
}
const handleCreate = async (evt: React.FormEvent) => {
const handleCreate = async () => {
let created: User
let message: string
let payload: UserCreatePayload
evt.preventDefault()
setCreateError(null)
setError(null)
payload = {
username: username.trim(),
display_name: displayName.trim(),
email: email.trim(),
password: password,
is_admin: isAdmin
username: createForm.username.trim(),
display_name: createForm.displayName.trim(),
email: createForm.email.trim(),
password: createForm.password,
is_admin: createForm.isAdmin
}
if (password !== passwordConfirm) {
if (createForm.password !== createForm.passwordConfirm) {
setCreateError('Password and confirmation must match.')
return
}
setCreating(true)
try {
created = await api.createUser(payload)
if (createCanCreateProject) {
if (createForm.canCreateProject) {
try {
await api.createSubjectPermissions({
permission: projectCreatePermission,
@@ -172,23 +166,21 @@ export default function AdminUsersPage() {
const openEditUser = (user: User) => {
setEditUser(user)
setEditDisplayName(user.display_name || '')
setEditEmail(user.email || '')
setEditPassword('')
setEditPasswordConfirm('')
setEditIsAdmin(Boolean(user.is_admin))
setEditCanCreateProject(hasDirectProjectCreate(user.id))
setEditForm({
username: user.username,
displayName: user.display_name || '',
email: user.email || '',
password: '',
passwordConfirm: '',
isAdmin: Boolean(user.is_admin),
canCreateProject: hasDirectProjectCreate(user.id)
})
setEditError(null)
}
const closeEditUser = () => {
setEditUser(null)
setEditDisplayName('')
setEditEmail('')
setEditPassword('')
setEditPasswordConfirm('')
setEditIsAdmin(false)
setEditCanCreateProject(false)
setEditForm(createEmptyForm())
setEditError(null)
}
@@ -200,7 +192,7 @@ export default function AdminUsersPage() {
}
setSavingEdit(true)
setEditError(null)
if (editPassword !== editPasswordConfirm) {
if (editForm.password !== editForm.passwordConfirm) {
setEditError('Password and confirmation must match.')
setSavingEdit(false)
return
@@ -208,19 +200,19 @@ export default function AdminUsersPage() {
hadProjectCreate = hasDirectProjectCreate(editUser.id)
try {
payload = {
display_name: editDisplayName.trim(),
email: editEmail.trim(),
password: editPassword,
is_admin: editIsAdmin
display_name: editForm.displayName.trim(),
email: editForm.email.trim(),
password: editForm.password,
is_admin: editForm.isAdmin
}
await api.updateUser(editUser.id, payload)
if (editCanCreateProject && !hadProjectCreate) {
if (editForm.canCreateProject && !hadProjectCreate) {
await api.createSubjectPermissions({
permission: projectCreatePermission,
targets: [{ subject_type: 'user', subject_id: editUser.id }]
})
}
if (!editCanCreateProject && hadProjectCreate) {
if (!editForm.canCreateProject && hadProjectCreate) {
await api.deleteSubjectPermission(projectCreatePermission, 'user', editUser.id)
}
await reloadUsers()
@@ -280,120 +272,39 @@ export default function AdminUsersPage() {
))}
</List>
</SectionCard>
<Dialog open={createOpen} onClose={closeCreateDialog} maxWidth="sm" fullWidth>
<DialogTitle>New User</DialogTitle>
<FormDialogContent>
{createError ? <Alert severity="error" sx={{ mb: 1 }}>{createError}</Alert> : null}
<Box component="form" onSubmit={handleCreate} sx={{ display: 'grid', gap: 1, mt: 1 }}>
<TextField name="username" label="Username" value={username} onChange={(event) => setUsername(event.target.value)} />
<TextField
name="display_name"
label="Display Name"
value={displayName}
onChange={(event) => setDisplayName(event.target.value)}
<UserFormDialog
open={createOpen}
item={null}
dialogError={createError}
saving={creating}
form={createForm}
setForm={setCreateForm}
onClose={closeCreateDialog}
onSave={handleCreate}
/>
<TextField name="email" label="Email" value={email} onChange={(event) => setEmail(event.target.value)} />
<TextField
name="password"
label="Password"
type="password"
value={password}
onChange={(event) => setPassword(event.target.value)}
<UserFormDialog
open={Boolean(editUser)}
item={editUser}
dialogError={editError}
saving={savingEdit}
form={editForm}
setForm={setEditForm}
onClose={closeEditUser}
onSave={handleSaveEdit}
/>
<TextField
name="password_confirm"
label="Confirm Password"
type="password"
value={passwordConfirm}
error={passwordConfirm !== '' && password !== passwordConfirm}
helperText={passwordConfirm !== '' && password !== passwordConfirm ? 'Passwords do not match.' : ''}
onChange={(event) => setPasswordConfirm(event.target.value)}
<ConfirmDeleteDialog
open={Boolean(deletingUser)}
title="Delete User"
prompt="Type the username to confirm deletion."
expectedText={deletingUser?.username}
confirmLabel="Username"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
busy={deleting}
confirmDisabled={!deletingUser}
onConfirm={handleDeleteUser}
onClose={() => { setDeletingUser(null); setDeleteConfirm('') }}
/>
<FormControlLabel
control={<Checkbox checked={isAdmin} onChange={(event) => setIsAdmin(event.target.checked)} />}
label="Admin"
/>
<FormControlLabel
control={<Checkbox checked={createCanCreateProject} onChange={(event) => setCreateCanCreateProject(event.target.checked)} />}
label="Allow project creation (direct grant)"
/>
<DialogActions sx={{ px: 0 }}>
<Button onClick={closeCreateDialog}>Cancel</Button>
<Button type="submit" variant="contained" disabled={creating}>
{creating ? 'Creating...' : 'Create'}
</Button>
</DialogActions>
</Box>
</FormDialogContent>
</Dialog>
<Dialog open={Boolean(editUser)} onClose={closeEditUser} maxWidth="sm" fullWidth>
<DialogTitle>Edit User</DialogTitle>
<FormDialogContent>
{editError ? <Alert severity="error" sx={{ mb: 1 }}>{editError}</Alert> : null}
<Box sx={{ display: 'grid', gap: 1, mt: 1 }}>
<TextField label="Username" value={editUser?.username || ''} disabled />
<TextField
label="Display Name"
value={editDisplayName}
onChange={(event) => setEditDisplayName(event.target.value)}
/>
<TextField label="Email" value={editEmail} onChange={(event) => setEditEmail(event.target.value)} />
<TextField
label="New Password (optional)"
type="password"
value={editPassword}
onChange={(event) => setEditPassword(event.target.value)}
/>
<TextField
label="Confirm New Password"
type="password"
value={editPasswordConfirm}
error={editPasswordConfirm !== '' && editPassword !== editPasswordConfirm}
helperText={editPasswordConfirm !== '' && editPassword !== editPasswordConfirm ? 'Passwords do not match.' : ''}
onChange={(event) => setEditPasswordConfirm(event.target.value)}
/>
<FormControlLabel
control={<Checkbox checked={editIsAdmin} onChange={(event) => setEditIsAdmin(event.target.checked)} />}
label="Admin"
/>
<FormControlLabel
control={<Checkbox checked={editCanCreateProject} onChange={(event) => setEditCanCreateProject(event.target.checked)} />}
label="Allow project creation (direct grant)"
/>
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={closeEditUser}>Cancel</Button>
<Button variant="contained" onClick={handleSaveEdit} disabled={savingEdit}>
{savingEdit ? 'Saving...' : 'Save'}
</Button>
</DialogActions>
</Dialog>
<Dialog open={Boolean(deletingUser)} onClose={() => setDeletingUser(null)} maxWidth="xs" fullWidth>
<DialogTitle>Delete User</DialogTitle>
<FormDialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Type the username to confirm deletion.
</Typography>
<TextField
fullWidth
label="Username"
value={deleteConfirm}
onChange={(event) => setDeleteConfirm(event.target.value)}
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => { setDeletingUser(null); setDeleteConfirm('') }}>Cancel</Button>
<Button
color="error"
variant="contained"
disabled={deleting || !deletingUser || deleteConfirm !== deletingUser.username}
onClick={handleDeleteUser}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
+14 -25
View File
@@ -5,6 +5,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
import DeleteIcon from '@mui/icons-material/Delete'
import Alert from '@mui/material/Alert'
import FormDialogContent from '../components/FormDialogContent'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import {
Box,
Button,
@@ -282,40 +283,28 @@ export default function ApiKeysPage() {
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={closeCreate}>{newToken ? 'Done' : 'Cancel'}</Button>
{!newToken ? (
<Button onClick={handleCreate} variant="contained" disabled={creating}>
{creating ? 'Creating...' : 'Create'}
</Button>
) : null}
<Button onClick={closeCreate}>{newToken ? 'Done' : 'Cancel'}</Button>
</DialogActions>
</Dialog>
<Dialog open={Boolean(deleteTarget)} onClose={() => setDeleteTarget(null)} maxWidth="xs" fullWidth>
<DialogTitle>Delete API Key</DialogTitle>
<FormDialogContent>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Type the API key name to confirm deletion.
</Typography>
<TextField
fullWidth
label="Key name"
value={deleteConfirm}
onChange={(event) => setDeleteConfirm(event.target.value)}
<ConfirmDeleteDialog
open={Boolean(deleteTarget)}
title="Delete API Key"
prompt="Type the API key name to confirm deletion."
expectedText={deleteTarget?.name}
confirmLabel="Key name"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
busy={deleting}
confirmDisabled={!deleteTarget}
onConfirm={handleDelete}
onClose={() => { setDeleteTarget(null); setDeleteConfirm('') }}
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => { setDeleteTarget(null); setDeleteConfirm('') }}>Cancel</Button>
<Button
color="error"
variant="contained"
disabled={deleting || !deleteTarget || deleteConfirm !== deleteTarget.name}
onClick={handleDelete}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
+14 -27
View File
@@ -7,6 +7,7 @@ import RepoSubNav from '../components/RepoSubNav'
import EditIcon from '@mui/icons-material/Edit'
import DeleteIcon from '@mui/icons-material/Delete'
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import FormDialogContent from '../components/FormDialogContent'
export default function BranchesPage() {
@@ -334,10 +335,10 @@ export default function BranchesPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
<Button onClick={handleCreate} variant="contained">
Create
</Button>
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={renameOpen} onClose={() => setRenameOpen(false)} maxWidth="sm" fullWidth>
@@ -358,38 +359,24 @@ export default function BranchesPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setRenameOpen(false)}>Cancel</Button>
<Button onClick={handleRename} variant="contained">
Rename
</Button>
<Button onClick={() => setRenameOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>Delete Branch</DialogTitle>
<FormDialogContent>
{deleteError ? (
<Typography variant="body2" color="error" sx={{ mb: 1 }}>
{deleteError}
</Typography>
) : null}
<Typography variant="body2">
Delete branch <strong>{deleteTarget}</strong>?
</Typography>
<TextField
label="Type branch name to confirm"
value={deleteConfirm}
onChange={(event) => setDeleteConfirm(event.target.value)}
fullWidth
margin="dense"
<ConfirmDeleteDialog
open={deleteOpen}
title="Delete Branch"
prompt="Type the branch name to confirm deletion."
expectedText={deleteTarget}
confirmLabel="Type branch name to confirm"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
error={deleteError}
onConfirm={handleDelete}
onClose={() => setDeleteOpen(false)}
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
<Button onClick={handleDelete} color="error" variant="contained" disabled={deleteConfirm.trim() !== deleteTarget}>
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
)
}
@@ -543,10 +543,10 @@ export default function ClientCertificatesPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setIssueProfile(null)} disabled={busy}>Cancel</Button>
<Button variant="contained" onClick={submitIssue} disabled={busy}>
{busy ? 'Issuing...' : 'Issue'}
</Button>
<Button onClick={() => setIssueProfile(null)} disabled={busy}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -576,7 +576,7 @@ export default function ClientCertificatesPage() {
<TextField label="CA Certificate" value={issueResult?.ca_cert_pem || ''} multiline minRows={6} InputProps={{ sx: { fontFamily: 'monospace' } }} />
</FormDialogContent>
<DialogActions>
<Button onClick={() => setIssueResult(null)}>Close</Button>
<Button onClick={() => setIssueResult(null)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -652,7 +652,7 @@ export default function ClientCertificatesPage() {
<Button onClick={toggleViewDump} disabled={viewCertDumpLoading}>
{viewCertDumpLoading ? 'Loading Dump...' : viewCertDumpOpen ? 'Hide X509 Dump' : 'Show X509 Dump'}
</Button>
<Button onClick={() => setViewCert(null)}>Close</Button>
<Button onClick={() => setViewCert(null)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -665,10 +665,10 @@ export default function ClientCertificatesPage() {
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setRevokeItem(null)} disabled={busy}>Cancel</Button>
<Button variant="contained" color="error" onClick={revoke} disabled={busy}>
{busy ? 'Revoking...' : 'Revoke'}
</Button>
<Button onClick={() => setRevokeItem(null)} disabled={busy}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+2 -2
View File
@@ -642,10 +642,10 @@ export default function ProjectHomePage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setEditOpen(false)}>Cancel</Button>
<Button onClick={handleEditSave} variant="contained" disabled={savingEdit}>
{savingEdit ? 'Saving...' : 'Save'}
</Button>
<Button onClick={() => setEditOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={deleteOpen} onClose={closeDelete} maxWidth="sm" fullWidth>
@@ -683,7 +683,6 @@ export default function ProjectHomePage() {
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={closeDelete}>Cancel</Button>
<Button
onClick={handleDelete}
variant="contained"
@@ -700,6 +699,7 @@ export default function ProjectHomePage() {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeDelete}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+3 -3
View File
@@ -447,10 +447,10 @@ export default function ProjectsPage() {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setEditProject(null)}>Cancel</Button>
<Button onClick={handleEditSave} variant="contained" disabled={savingEdit}>
{savingEdit ? 'Saving...' : 'Save'}
</Button>
<Button onClick={() => setEditProject(null)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={createOpen} onClose={closeCreate} maxWidth="sm" fullWidth>
@@ -512,10 +512,10 @@ export default function ProjectsPage() {
)}
</Box>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 1 }}>
<Button onClick={closeCreate}>Cancel</Button>
<Button type="submit" variant="contained" disabled={creating}>
{creating ? 'Creating...' : 'Create'}
</Button>
<Button onClick={closeCreate}>Cancel</Button>
</Box>
</Box>
</FormDialogContent>
@@ -555,7 +555,6 @@ export default function ProjectsPage() {
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={closeDelete}>Cancel</Button>
<Button
onClick={handleDelete}
variant="contained"
@@ -572,6 +571,7 @@ export default function ProjectsPage() {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeDelete}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+4 -4
View File
@@ -498,7 +498,6 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteTagOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteTag}
variant="contained"
@@ -507,6 +506,7 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteTagOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={deleteImageOpen} onClose={() => setDeleteImageOpen(false)} maxWidth="xs" fullWidth>
@@ -525,7 +525,6 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteImageOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteImage}
variant="contained"
@@ -534,6 +533,7 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteImageOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={renameTagOpen} onClose={() => setRenameTagOpen(false)} maxWidth="xs" fullWidth>
@@ -550,10 +550,10 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setRenameTagOpen(false)}>Cancel</Button>
<Button onClick={handleRenameTag} variant="contained" disabled={renaming}>
{renaming ? 'Renaming...' : 'Rename'}
</Button>
<Button onClick={() => setRenameTagOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={renameImageOpen} onClose={() => setRenameImageOpen(false)} maxWidth="xs" fullWidth>
@@ -571,10 +571,10 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setRenameImageOpen(false)}>Cancel</Button>
<Button onClick={handleRenameImage} variant="contained" disabled={renaming}>
{renaming ? 'Renaming...' : 'Rename'}
</Button>
<Button onClick={() => setRenameImageOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+7 -7
View File
@@ -1292,10 +1292,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setSubdirOpen(false)}>Cancel</Button>
<Button onClick={handleCreateSubdir} variant="contained" disabled={subdirSaving}>
{subdirSaving ? 'Creating...' : 'Create'}
</Button>
<Button onClick={() => setSubdirOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={uploadOpen} onClose={() => setUploadOpen(false)} maxWidth="sm" fullWidth>
@@ -1319,10 +1319,10 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setUploadOpen(false)}>Cancel</Button>
<Button onClick={handleUpload} variant="contained" disabled={uploading}>
{uploading ? 'Uploading...' : 'Upload'}
</Button>
<Button onClick={() => setUploadOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)} maxWidth="xs" fullWidth>
@@ -1343,7 +1343,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteFolder}
variant="contained"
@@ -1352,6 +1351,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog
@@ -1553,6 +1553,9 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
</Box>
</FormDialogContent>
<DialogActions>
<Button onClick={handleRenameSubdir} variant="contained" disabled={renaming || !renameNewName.trim()}>
{renaming ? (renameIsRepoDir ? 'Saving...' : 'Renaming...') : (renameIsRepoDir ? 'Save' : 'Rename')}
</Button>
<Button
onClick={() => {
setRenameOpen(false)
@@ -1561,9 +1564,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
>
Cancel
</Button>
<Button onClick={handleRenameSubdir} variant="contained" disabled={renaming || !renameNewName.trim()}>
{renaming ? (renameIsRepoDir ? 'Saving...' : 'Renaming...') : (renameIsRepoDir ? 'Save' : 'Rename')}
</Button>
</DialogActions>
</Dialog>
<Dialog
@@ -1579,7 +1579,6 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setClearRunsConfirmOpen(false)}>Cancel</Button>
<Button
color="warning"
variant="contained"
@@ -1590,6 +1589,7 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
>
Clear
</Button>
<Button onClick={() => setClearRunsConfirmOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
</Box>
+4 -4
View File
@@ -553,10 +553,10 @@ export default function ReposPage() {
<MenuItem value="docker">Docker</MenuItem>
</SelectField>
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1, mt: 1 }}>
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
<Button type="submit" variant="contained" disabled={creating}>
{creating ? 'Creating...' : 'Create'}
</Button>
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
</Box>
</Box>
</FormDialogContent>
@@ -575,10 +575,10 @@ export default function ReposPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setEditRepo(null)}>Cancel</Button>
<Button onClick={handleEditSave} variant="contained" disabled={savingEdit}>
{savingEdit ? 'Saving...' : 'Save'}
</Button>
<Button onClick={() => setEditRepo(null)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={Boolean(deleteRepo)} onClose={closeDelete} maxWidth="sm" fullWidth>
@@ -620,7 +620,6 @@ export default function ReposPage() {
)}
</FormDialogContent>
<DialogActions>
<Button onClick={closeDelete}>Cancel</Button>
<Button
onClick={handleDelete}
variant="contained"
@@ -629,6 +628,7 @@ export default function ReposPage() {
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeDelete}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={bulkDeleteOpen} onClose={closeBulkDelete} maxWidth="sm" fullWidth>
@@ -659,7 +659,6 @@ export default function ReposPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={closeBulkDelete}>Cancel</Button>
<Button
onClick={handleBulkDelete}
variant="contained"
@@ -668,6 +667,7 @@ export default function ReposPage() {
>
{bulkDeleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeBulkDelete}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={foreignOpen} onClose={closeForeign} maxWidth="sm" fullWidth>
+2 -2
View File
@@ -654,10 +654,10 @@ export default function SSHServersPage() {
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
<Button variant="contained" color="error" onClick={handleDelete}>
Delete
</Button>
<Button onClick={() => setDeleteItem(null)}>Cancel</Button>
</DialogActions>
</Dialog>
@@ -690,10 +690,10 @@ export default function SSHServersPage() {
</Typography>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setDeleteServerItem(null)}>Cancel</Button>
<Button variant="contained" color="error" onClick={handleServerDelete}>
Delete
</Button>
<Button onClick={() => setDeleteServerItem(null)}>Cancel</Button>
</DialogActions>
</Dialog>
+12 -12
View File
@@ -1634,9 +1634,6 @@ export default function SSHSessionPage() {
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={closeHistoryConnectPrompt} disabled={connectingHistorySession}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => historyConnectItem ? void connectHistorySession(historyConnectItem, historyConnectPassword, historyConnectOTPCode) : undefined}
@@ -1648,6 +1645,9 @@ export default function SSHSessionPage() {
>
{connectingHistorySession ? 'Connecting...' : 'Connect'}
</Button>
<Button onClick={closeHistoryConnectPrompt} disabled={connectingHistorySession}>
Cancel
</Button>
</DialogActions>
</Dialog>
@@ -1684,12 +1684,12 @@ export default function SSHSessionPage() {
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={handleCloseTranscript}>
Close
</Button>
<Button variant="contained" onClick={handleDownloadTranscript} disabled={!transcriptSession || loadingTranscript || transcriptText === ''}>
Download
</Button>
<Button onClick={handleCloseTranscript}>
Close
</Button>
</DialogActions>
</Dialog>
@@ -1705,12 +1705,12 @@ export default function SSHSessionPage() {
/>
</FormDialogContent>
<DialogActions>
<Button onClick={() => setNewSessionOpen(false)} disabled={creatingSession}>
Cancel
</Button>
<Button variant="contained" onClick={() => void handleCreateSession()} disabled={creatingSession || !selectedProfile}>
{creatingSession ? 'Connecting...' : 'Connect'}
</Button>
<Button onClick={() => setNewSessionOpen(false)} disabled={creatingSession}>
Cancel
</Button>
</DialogActions>
</Dialog>
@@ -1740,9 +1740,6 @@ export default function SSHSessionPage() {
) : null}
</FormDialogContent>
<DialogActions>
<Button onClick={closeNewSessionPasswordPrompt} disabled={creatingSession}>
Cancel
</Button>
<Button
variant="contained"
onClick={() => void handleCreateSession(newSessionPassword, newSessionOTPCode)}
@@ -1754,6 +1751,9 @@ export default function SSHSessionPage() {
>
{creatingSession ? 'Connecting...' : 'Connect'}
</Button>
<Button onClick={closeNewSessionPasswordPrompt} disabled={creatingSession}>
Cancel
</Button>
</DialogActions>
</Dialog>
</Box>