Compare commits

...

2 Commits

7 changed files with 206 additions and 303 deletions
+12 -15
View File
@@ -3,6 +3,7 @@ import KeyIcon from '@mui/icons-material/Key'
import BlockIcon from '@mui/icons-material/Block' import BlockIcon from '@mui/icons-material/Block'
import VisibilityIcon from '@mui/icons-material/Visibility' import VisibilityIcon from '@mui/icons-material/Visibility'
import Alert from '@mui/material/Alert' import Alert from '@mui/material/Alert'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import FormDialogContent from '../components/FormDialogContent' import FormDialogContent from '../components/FormDialogContent'
import { import {
Box, Box,
@@ -656,21 +657,17 @@ export default function ClientCertificatesPage() {
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Dialog open={Boolean(revokeItem)} onClose={() => { if (!busy) setRevokeItem(null) }} fullWidth maxWidth="sm"> <ConfirmDeleteDialog
<DialogTitle>Revoke Client Certificate</DialogTitle> open={Boolean(revokeItem)}
<FormDialogContent> title="Revoke Client Certificate"
{dialogError ? <Alert severity="error">{dialogError}</Alert> : null} prompt={`Revoke ${revokeItem?.common_name || ''} (${revokeItem?.cert_id || ''})?`}
<Typography variant="body2" color="text.secondary"> error={dialogError}
Revoke {revokeItem?.common_name} ({revokeItem?.cert_id})? busy={busy}
</Typography> confirmText="Revoke"
</FormDialogContent> busyText="Revoking..."
<DialogActions> onConfirm={revoke}
<Button variant="contained" color="error" onClick={revoke} disabled={busy}> onClose={() => setRevokeItem(null)}
{busy ? 'Revoking...' : 'Revoke'} />
</Button>
<Button onClick={() => setRevokeItem(null)} disabled={busy}>Cancel</Button>
</DialogActions>
</Dialog>
</Box> </Box>
) )
} }
+29 -50
View File
@@ -12,6 +12,7 @@ import EditIcon from '@mui/icons-material/Edit'
import { ListRowActionButton, ListRowActions } from '../components/ListRowActions' import { ListRowActionButton, ListRowActions } from '../components/ListRowActions'
import SectionCard from '../components/SectionCard' import SectionCard from '../components/SectionCard'
import DeleteIcon from '@mui/icons-material/Delete' import DeleteIcon from '@mui/icons-material/Delete'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import FormDialogContent from '../components/FormDialogContent' import FormDialogContent from '../components/FormDialogContent'
import SelectField from '../components/SelectField' import SelectField from '../components/SelectField'
@@ -648,60 +649,38 @@ export default function ProjectHomePage() {
<Button onClick={() => setEditOpen(false)}>Cancel</Button> <Button onClick={() => setEditOpen(false)}>Cancel</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Dialog open={deleteOpen} onClose={closeDelete} maxWidth="sm" fullWidth> <ConfirmDeleteDialog
<DialogTitle>Delete Project</DialogTitle> open={deleteOpen}
<FormDialogContent> title="Delete Project"
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null} prompt={deleteLoading ? 'Checking project contents...' : 'Type the project slug to confirm deletion.'}
{deleteLoading ? ( expectedText={deleteCounts && deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads > 0 ? (project?.slug || undefined) : undefined}
confirmLabel="Project Slug"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
error={deleteError}
busy={deleting}
confirmDisabled={deleteLoading || !project || !deleteCounts}
onConfirm={handleDelete}
onClose={closeDelete}
>
{deleteLoading ? null : deleteCounts ? (
deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads === 0 ? (
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Checking project contents... This project has no repositories, issues, wiki pages, or files.
</Typography> </Typography>
) : deleteCounts ? ( ) : (
deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads === 0 ? ( <Box sx={{ display: 'grid', gap: 1 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
This project has no repositories, issues, wiki pages, or files. This project contains:
</Typography> </Typography>
) : ( <Typography variant="body2">Repositories: {deleteCounts.repos}</Typography>
<Box sx={{ display: 'grid', gap: 1 }}> <Typography variant="body2">Issues: {deleteCounts.issues}</Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2">Wiki pages: {deleteCounts.wiki}</Typography>
This project contains: <Typography variant="body2">Files: {deleteCounts.uploads}</Typography>
</Typography> </Box>
<Typography variant="body2">Repositories: {deleteCounts.repos}</Typography> )
<Typography variant="body2">Issues: {deleteCounts.issues}</Typography> ) : null}
<Typography variant="body2">Wiki pages: {deleteCounts.wiki}</Typography> </ConfirmDeleteDialog>
<Typography variant="body2">Files: {deleteCounts.uploads}</Typography>
<Typography variant="body2" color="text.secondary">
Type the project slug to confirm deletion.
</Typography>
<TextField
label="Project Slug"
value={deleteConfirm}
onChange={(event) => setDeleteConfirm(event.target.value)}
/>
</Box>
)
) : null}
</FormDialogContent>
<DialogActions>
<Button
onClick={handleDelete}
variant="contained"
color="error"
disabled={
deleting ||
deleteLoading ||
!project ||
(deleteCounts
? deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads > 0 &&
deleteConfirm !== project.slug
: true)
}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeDelete}>Cancel</Button>
</DialogActions>
</Dialog>
</Box> </Box>
) )
} }
+29 -50
View File
@@ -2,6 +2,7 @@ import AddIcon from '@mui/icons-material/Add'
import DeleteIcon from '@mui/icons-material/Delete' import DeleteIcon from '@mui/icons-material/Delete'
import EditIcon from '@mui/icons-material/Edit' import EditIcon from '@mui/icons-material/Edit'
import Alert from '@mui/material/Alert' import Alert from '@mui/material/Alert'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemText, MenuItem, Paper, Tab, Tabs, TextField, Typography } from '@mui/material' import { Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, List, ListItem, ListItemText, MenuItem, Paper, Tab, Tabs, TextField, Typography } from '@mui/material'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
@@ -520,60 +521,38 @@ export default function ProjectsPage() {
</Box> </Box>
</FormDialogContent> </FormDialogContent>
</Dialog> </Dialog>
<Dialog open={Boolean(deleteProject)} onClose={closeDelete} maxWidth="sm" fullWidth> <ConfirmDeleteDialog
<DialogTitle>Delete Project</DialogTitle> open={Boolean(deleteProject)}
<FormDialogContent> title="Delete Project"
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null} prompt={deleteLoading ? 'Checking project contents...' : 'Type the project slug to confirm deletion.'}
{deleteLoading ? ( expectedText={deleteCounts && deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads > 0 ? (deleteProject?.slug || undefined) : undefined}
confirmLabel="Project Slug"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
error={deleteError}
busy={deleting}
confirmDisabled={deleteLoading || !deleteProject || !deleteCounts}
onConfirm={handleDelete}
onClose={closeDelete}
>
{deleteLoading ? null : deleteCounts ? (
deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads === 0 ? (
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Checking project contents... This project has no repositories, issues, wiki pages, or files.
</Typography> </Typography>
) : deleteCounts ? ( ) : (
deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads === 0 ? ( <Box sx={{ display: 'grid', gap: 1 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
This project has no repositories, issues, wiki pages, or files. This project contains:
</Typography> </Typography>
) : ( <Typography variant="body2">Repositories: {deleteCounts.repos}</Typography>
<Box sx={{ display: 'grid', gap: 1 }}> <Typography variant="body2">Issues: {deleteCounts.issues}</Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2">Wiki pages: {deleteCounts.wiki}</Typography>
This project contains: <Typography variant="body2">Files: {deleteCounts.uploads}</Typography>
</Typography> </Box>
<Typography variant="body2">Repositories: {deleteCounts.repos}</Typography> )
<Typography variant="body2">Issues: {deleteCounts.issues}</Typography> ) : null}
<Typography variant="body2">Wiki pages: {deleteCounts.wiki}</Typography> </ConfirmDeleteDialog>
<Typography variant="body2">Files: {deleteCounts.uploads}</Typography>
<Typography variant="body2" color="text.secondary">
Type the project slug to confirm deletion.
</Typography>
<TextField
label="Project Slug"
value={deleteConfirm}
onChange={(event) => setDeleteConfirm(event.target.value)}
/>
</Box>
)
) : null}
</FormDialogContent>
<DialogActions>
<Button
onClick={handleDelete}
variant="contained"
color="error"
disabled={
deleting ||
deleteLoading ||
!deleteProject ||
(deleteCounts
? deleteCounts.repos + deleteCounts.issues + deleteCounts.wiki + deleteCounts.uploads > 0 &&
deleteConfirm !== deleteProject.slug
: true)
}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeDelete}>Cancel</Button>
</DialogActions>
</Dialog>
</Box> </Box>
) )
} }
+35 -54
View File
@@ -25,6 +25,7 @@ import RepoSubNav from '../components/RepoSubNav'
import TintedPanel from '../components/TintedPanel' import TintedPanel from '../components/TintedPanel'
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'
import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline' import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutline'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
type RepoDockerDetailPageProps = { type RepoDockerDetailPageProps = {
initialRepo?: Repo initialRepo?: Repo
@@ -482,60 +483,40 @@ export default function RepoDockerDetailPage(props: RepoDockerDetailPageProps) {
)} )}
</TintedPanel> </TintedPanel>
</Box> </Box>
<Dialog open={deleteTagOpen} onClose={() => setDeleteTagOpen(false)} maxWidth="xs" fullWidth> <ConfirmDeleteDialog
<DialogTitle>Delete tag</DialogTitle> open={deleteTagOpen}
<FormDialogContent> title="Delete tag"
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null} prompt="Type the tag name to confirm deletion."
<Typography variant="body2" sx={{ mt: 1 }}> expectedText={deleteTagName}
Delete tag "{deleteTagName}"? confirmLabel="Type tag to confirm"
</Typography> confirmValue={deleteTagConfirm}
<TextField onConfirmValueChange={setDeleteTagConfirm}
label="Type tag to confirm" error={deleteError}
value={deleteTagConfirm} busy={deleting}
onChange={(event) => setDeleteTagConfirm(event.target.value)} onConfirm={handleDeleteTag}
fullWidth onClose={() => setDeleteTagOpen(false)}
sx={{ mt: 1 }} >
/> <Typography variant="body2">
</FormDialogContent> Delete tag "{deleteTagName}"?
<DialogActions> </Typography>
<Button </ConfirmDeleteDialog>
onClick={handleDeleteTag} <ConfirmDeleteDialog
variant="contained" open={deleteImageOpen}
color="error" title="Delete image"
disabled={deleting || deleteTagConfirm.trim() !== deleteTagName} prompt="Type the image name to confirm deletion."
> expectedText={deleteImageLabel}
{deleting ? 'Deleting...' : 'Delete'} confirmLabel="Type image name to confirm"
</Button> confirmValue={deleteImageConfirm}
<Button onClick={() => setDeleteTagOpen(false)}>Cancel</Button> onConfirmValueChange={setDeleteImageConfirm}
</DialogActions> error={deleteError}
</Dialog> busy={deleting}
<Dialog open={deleteImageOpen} onClose={() => setDeleteImageOpen(false)} maxWidth="xs" fullWidth> onConfirm={handleDeleteImage}
<DialogTitle>Delete image</DialogTitle> onClose={() => setDeleteImageOpen(false)}
<FormDialogContent> >
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null} <Typography variant="body2">
<Typography variant="body2" sx={{ mt: 1 }}> Delete image "{deleteImageLabel || '(root)'}" and all tags?
Delete image "{deleteImageLabel || '(root)'}" and all tags? </Typography>
</Typography> </ConfirmDeleteDialog>
<TextField
label="Type image name to confirm"
value={deleteImageConfirm}
onChange={(event) => setDeleteImageConfirm(event.target.value)}
fullWidth
sx={{ mt: 1 }}
/>
</FormDialogContent>
<DialogActions>
<Button
onClick={handleDeleteImage}
variant="contained"
color="error"
disabled={deleting || deleteImageConfirm.trim() !== deleteImageLabel}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteImageOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={renameTagOpen} onClose={() => setRenameTagOpen(false)} maxWidth="xs" fullWidth> <Dialog open={renameTagOpen} onClose={() => setRenameTagOpen(false)} maxWidth="xs" fullWidth>
<DialogTitle>Rename tag</DialogTitle> <DialogTitle>Rename tag</DialogTitle>
<FormDialogContent> <FormDialogContent>
+20 -29
View File
@@ -33,6 +33,7 @@ import DriveFileRenameOutlineIcon from '@mui/icons-material/DriveFileRenameOutli
import FolderIcon from '@mui/icons-material/Folder' import FolderIcon from '@mui/icons-material/Folder'
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile' import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined' import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined' import MonitorHeartOutlinedIcon from '@mui/icons-material/MonitorHeartOutlined'
import ProjectNavBar from '../components/ProjectNavBar' import ProjectNavBar from '../components/ProjectNavBar'
import RepoSubNav from '../components/RepoSubNav' import RepoSubNav from '../components/RepoSubNav'
@@ -1325,35 +1326,25 @@ export default function RepoRpmDetailPage(props: RepoRpmDetailPageProps) {
<Button onClick={() => setUploadOpen(false)}>Cancel</Button> <Button onClick={() => setUploadOpen(false)}>Cancel</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Dialog open={deleteOpen} onClose={() => setDeleteOpen(false)} maxWidth="xs" fullWidth> <ConfirmDeleteDialog
<DialogTitle>{deleteIsFile ? 'Delete file' : 'Delete folder'}</DialogTitle> open={deleteOpen}
<FormDialogContent> title={deleteIsFile ? 'Delete file' : 'Delete folder'}
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null} prompt="Type the name to confirm deletion."
<Typography variant="body2" sx={{ mt: 1 }}> expectedText={deleteName}
{deleteIsFile confirmLabel="Type name to confirm"
? `Delete file "${deleteName}"?` confirmValue={deleteConfirm}
: `Delete folder "${deleteName}" and all of its contents?`} onConfirmValueChange={setDeleteConfirm}
</Typography> error={deleteError}
<TextField busy={deleting}
label="Type name to confirm" onConfirm={handleDeleteFolder}
value={deleteConfirm} onClose={() => setDeleteOpen(false)}
onChange={(event) => setDeleteConfirm(event.target.value)} >
fullWidth <Typography variant="body2">
sx={{ mt: 1 }} {deleteIsFile
/> ? `Delete file "${deleteName}"?`
</FormDialogContent> : `Delete folder "${deleteName}" and all of its contents?`}
<DialogActions> </Typography>
<Button </ConfirmDeleteDialog>
onClick={handleDeleteFolder}
variant="contained"
color="error"
disabled={deleting || deleteConfirm.trim() !== deleteName}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={() => setDeleteOpen(false)}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog <Dialog
open={statusOpen} open={statusOpen}
onClose={() => { onClose={() => {
+48 -75
View File
@@ -4,6 +4,7 @@ import EditIcon from '@mui/icons-material/Edit'
import LinkOffIcon from '@mui/icons-material/LinkOff' import LinkOffIcon from '@mui/icons-material/LinkOff'
import LinkIcon from '@mui/icons-material/Link' import LinkIcon from '@mui/icons-material/Link'
import Alert from '@mui/material/Alert' import Alert from '@mui/material/Alert'
import ConfirmDeleteDialog from '../components/ConfirmDeleteDialog'
import FormDialogContent from '../components/FormDialogContent' import FormDialogContent from '../components/FormDialogContent'
import { import {
Box, Box,
@@ -581,60 +582,53 @@ export default function ReposPage() {
<Button onClick={() => setEditRepo(null)}>Cancel</Button> <Button onClick={() => setEditRepo(null)}>Cancel</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
<Dialog open={Boolean(deleteRepo)} onClose={closeDelete} maxWidth="sm" fullWidth> <ConfirmDeleteDialog
<DialogTitle>Delete Repository</DialogTitle> open={Boolean(deleteRepo)}
<FormDialogContent> title="Delete Repository"
{deleteError ? <Alert severity="error">{deleteError}</Alert> : null} prompt={deleteLoading ? 'Loading repository stats...' : 'Type the repository name to confirm deletion.'}
{deleteLoading ? ( expectedText={deleteRepo?.name}
confirmLabel="Repository Name"
confirmValue={deleteConfirm}
onConfirmValueChange={setDeleteConfirm}
error={deleteError}
busy={deleting}
confirmDisabled={deleteLoading || !deleteRepo}
onConfirm={handleDelete}
onClose={closeDelete}
>
{deleteLoading ? null : deleteStats ? (
<Box sx={{ display: 'grid', gap: 1 }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Loading repository stats... This will permanently delete the repository and all its data.
</Typography> </Typography>
) : deleteStats ? ( <Typography variant="body2">Branches: {deleteStats.branches}</Typography>
<Box sx={{ display: 'grid', gap: 1 }}> {deleteStats.last_commit ? (
<Typography variant="body2" color="text.secondary"> <Typography variant="body2">Last commit: {deleteStats.last_commit.slice(0, 7)}</Typography>
This will permanently delete the repository and all its data. ) : (
</Typography> <Typography variant="body2">Last commit: none</Typography>
<Typography variant="body2">Branches: {deleteStats.branches}</Typography> )}
{deleteStats.last_commit ? ( {deleteStats.last_message ? <Typography variant="body2">Message: {deleteStats.last_message}</Typography> : null}
<Typography variant="body2">Last commit: {deleteStats.last_commit.slice(0, 7)}</Typography> </Box>
) : ( ) : (
<Typography variant="body2">Last commit: none</Typography> <Typography variant="body2" color="text.secondary">
)} Unable to load repository stats.
{deleteStats.last_message ? ( </Typography>
<Typography variant="body2">Message: {deleteStats.last_message}</Typography> )}
) : null} </ConfirmDeleteDialog>
<Typography variant="body2" color="text.secondary"> <ConfirmDeleteDialog
Type the repository name to confirm deletion. open={bulkDeleteOpen}
</Typography> title="Delete Repositories"
<TextField prompt={`Type DELETE ${selectedRepoIds.length} to confirm.`}
label="Repository Name" expectedText={`DELETE ${selectedRepoIds.length}`}
value={deleteConfirm} confirmLabel="Confirmation"
onChange={(event) => setDeleteConfirm(event.target.value)} confirmValue={bulkDeleteConfirm}
fullWidth onConfirmValueChange={setBulkDeleteConfirm}
/> error={bulkDeleteError}
</Box> busy={bulkDeleting}
) : ( onConfirm={handleBulkDelete}
<Typography variant="body2" color="text.secondary"> onClose={closeBulkDelete}
Unable to load repository stats. >
</Typography> <>
)}
</FormDialogContent>
<DialogActions>
<Button
onClick={handleDelete}
variant="contained"
color="error"
disabled={deleting || deleteLoading || !deleteRepo || deleteConfirm !== deleteRepo.name}
>
{deleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeDelete}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={bulkDeleteOpen} onClose={closeBulkDelete} maxWidth="sm" fullWidth>
<DialogTitle>Delete Repositories</DialogTitle>
<FormDialogContent>
{bulkDeleteError ? <Alert severity="error">{bulkDeleteError}</Alert> : null}
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
You are deleting {selectedRepoIds.length} repositories. This action is permanent. You are deleting {selectedRepoIds.length} repositories. This action is permanent.
</Typography> </Typography>
@@ -647,29 +641,8 @@ export default function ReposPage() {
</Typography> </Typography>
))} ))}
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}> </>
Type DELETE {selectedRepoIds.length} to confirm. </ConfirmDeleteDialog>
</Typography>
<TextField
sx={{ mt: 1 }}
label="Confirmation"
value={bulkDeleteConfirm}
onChange={(event) => setBulkDeleteConfirm(event.target.value)}
fullWidth
/>
</FormDialogContent>
<DialogActions>
<Button
onClick={handleBulkDelete}
variant="contained"
color="error"
disabled={bulkDeleting || bulkDeleteConfirm !== `DELETE ${selectedRepoIds.length}`}
>
{bulkDeleting ? 'Deleting...' : 'Delete'}
</Button>
<Button onClick={closeBulkDelete}>Cancel</Button>
</DialogActions>
</Dialog>
<Dialog open={foreignOpen} onClose={closeForeign} maxWidth="sm" fullWidth> <Dialog open={foreignOpen} onClose={closeForeign} maxWidth="sm" fullWidth>
<DialogTitle>Add Foreign Repository</DialogTitle> <DialogTitle>Add Foreign Repository</DialogTitle>
<FormDialogContent> <FormDialogContent>
+33 -30
View File
@@ -12,6 +12,7 @@ import TextField from '@mui/material/TextField'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
import 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'
@@ -68,12 +69,14 @@ export default function SSHServersPage() {
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 [serverDialogOpen, setServerDialogOpen] = useState(false) const [serverDialogOpen, setServerDialogOpen] = useState(false)
const [editingServerID, setEditingServerID] = useState<string | null>(null) const [editingServerID, setEditingServerID] = useState<string | null>(null)
const [serverForm, setServerForm] = useState<SSHServerFormState>(emptyServerForm()) const [serverForm, setServerForm] = useState<SSHServerFormState>(emptyServerForm())
const [serverDialogError, setServerDialogError] = useState<string | null>(null) const [serverDialogError, setServerDialogError] = useState<string | null>(null)
const [serverSaving, setServerSaving] = useState(false) const [serverSaving, setServerSaving] = useState(false)
const [deleteServerItem, setDeleteServerItem] = useState<SSHServer | null>(null) const [deleteServerItem, setDeleteServerItem] = useState<SSHServer | null>(null)
const [deleteServerConfirm, setDeleteServerConfirm] = useState('')
const [viewServerItem, setViewServerItem] = useState<SSHServer | null>(null) const [viewServerItem, setViewServerItem] = useState<SSHServer | null>(null)
const [viewSharedServerItem, setViewSharedServerItem] = useState<SSHServer | null>(null) const [viewSharedServerItem, setViewSharedServerItem] = useState<SSHServer | null>(null)
const [hostKeyServer, setHostKeyServer] = useState<SSHServer | null>(null) const [hostKeyServer, setHostKeyServer] = useState<SSHServer | null>(null)
@@ -342,10 +345,12 @@ export default function SSHServersPage() {
try { try {
await api.deleteSSHAccessProfileForSelf(deleteItem.id) await api.deleteSSHAccessProfileForSelf(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('')
} }
} }
@@ -395,10 +400,12 @@ export default function SSHServersPage() {
try { try {
await api.deleteSSHServerForSelf(deleteServerItem.id) await api.deleteSSHServerForSelf(deleteServerItem.id)
setDeleteServerItem(null) setDeleteServerItem(null)
setDeleteServerConfirm('')
await load() await load()
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Failed to delete SSH server') setError(err instanceof Error ? err.message : 'Failed to delete SSH server')
setDeleteServerItem(null) setDeleteServerItem(null)
setDeleteServerConfirm('')
} }
} }
@@ -487,7 +494,7 @@ export default function SSHServersPage() {
<ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton> <ListRowActionButton onClick={() => openEdit(item)}>Edit</ListRowActionButton>
) : null} ) : null}
{item.owner_scope === 'user' ? ( {item.owner_scope === 'user' ? (
<ListRowActionButton color="error" onClick={() => setDeleteItem(item)}> <ListRowActionButton color="error" onClick={() => { setDeleteItem(item); setDeleteConfirm('') }}>
Delete Delete
</ListRowActionButton> </ListRowActionButton>
) : null} ) : null}
@@ -547,7 +554,7 @@ export default function SSHServersPage() {
<ListRowActions> <ListRowActions>
<ListRowActionButton onClick={() => setViewServerItem(item)}>View</ListRowActionButton> <ListRowActionButton onClick={() => setViewServerItem(item)}>View</ListRowActionButton>
<ListRowActionButton onClick={() => openServerEdit(item)}>Edit</ListRowActionButton> <ListRowActionButton onClick={() => openServerEdit(item)}>Edit</ListRowActionButton>
<ListRowActionButton color="error" onClick={() => setDeleteServerItem(item)}> <ListRowActionButton color="error" onClick={() => { setDeleteServerItem(item); setDeleteServerConfirm('') }}>
Delete Delete
</ListRowActionButton> </ListRowActionButton>
<ListRowActionButton onClick={() => void openHostKeys(item)}>Host Keys</ListRowActionButton> <ListRowActionButton onClick={() => void openHostKeys(item)}>Host Keys</ListRowActionButton>
@@ -646,20 +653,18 @@ export default function SSHServersPage() {
onSave={handleSave} onSave={handleSave}
/> />
<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 variant="contained" color="error" onClick={handleDelete}> confirmDisabled={!deleteItem}
Delete onConfirm={handleDelete}
</Button> onClose={() => { setDeleteItem(null); setDeleteConfirm('') }}
<Button onClick={() => setDeleteItem(null)}>Cancel</Button> />
</DialogActions>
</Dialog>
<SSHServerFormDialog <SSHServerFormDialog
open={serverDialogOpen} open={serverDialogOpen}
@@ -682,20 +687,18 @@ export default function SSHServersPage() {
onClose={() => setViewSharedServerItem(null)} onClose={() => setViewSharedServerItem(null)}
/> />
<Dialog open={Boolean(deleteServerItem)} onClose={() => setDeleteServerItem(null)} maxWidth="xs" fullWidth> <ConfirmDeleteDialog
<DialogTitle>Delete SSH Server</DialogTitle> open={Boolean(deleteServerItem)}
<FormDialogContent> title="Delete SSH Server"
<Typography variant="body2"> prompt="Type the server name to confirm deletion."
Delete <strong>{deleteServerItem?.name}</strong>? expectedText={deleteServerItem?.name}
</Typography> confirmLabel="Server Name"
</FormDialogContent> confirmValue={deleteServerConfirm}
<DialogActions> onConfirmValueChange={setDeleteServerConfirm}
<Button variant="contained" color="error" onClick={handleServerDelete}> confirmDisabled={!deleteServerItem}
Delete onConfirm={handleServerDelete}
</Button> onClose={() => { setDeleteServerItem(null); setDeleteServerConfirm('') }}
<Button onClick={() => setDeleteServerItem(null)}>Cancel</Button> />
</DialogActions>
</Dialog>
<Dialog open={Boolean(connectPasswordItem)} onClose={closeConnectPasswordPrompt} maxWidth="xs" fullWidth> <Dialog open={Boolean(connectPasswordItem)} onClose={closeConnectPasswordPrompt} maxWidth="xs" fullWidth>
<DialogTitle>Enter SSH Credentials</DialogTitle> <DialogTitle>Enter SSH Credentials</DialogTitle>