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