Phase 3: UI redesign (Gitea-issues aesthetic)
Top-nav Layout replaces side-nav: brand, primary nav, global search (debounced /search), notifications bell (Popover + unread badge), user avatar DropdownMenu. Mobile hamburger collapse. New pages: - /dashboard: analytics home (open-by-severity, age buckets, queue load, median resolution) - /tickets: Gitea-style list with status tabs, severity/assignee/CTI filters, server pagination (25/page), multi-select bulk bar (reassign/close/severity), saved views CRUD - /notifications: full list with mark-all-read - /settings: profile, notification prefs grid, API key (SERVICE role) - /admin/webhooks: CRUD + rotate-secret + active toggle, reveal-once secret dialog TicketDetail: inline Popover editing for Status/Severity/Assignee (replaces modal chain), AlertDialog delete confirmation, comment draft autosave to localStorage per ticket. Admin Users: window.confirm swapped for AlertDialog on delete + rotate API key with toast feedback. React Query hooks added for paged tickets, bulk actions, notifications, webhooks, saved views, analytics, notification prefs. ThemeProvider wired (v1.0 ships dark-only; toggle deferred). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Plus, Pencil, Trash2, RefreshCw, Copy, Check } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import Layout from '../../components/Layout';
|
||||
import Modal from '../../components/Modal';
|
||||
import { User, Role } from '../../types';
|
||||
@@ -10,6 +11,16 @@ import {
|
||||
useUpdateUser,
|
||||
useDeleteUser,
|
||||
} from '../../api/queries';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface UserForm {
|
||||
username: string;
|
||||
@@ -61,6 +72,8 @@ export default function AdminUsers() {
|
||||
const [error, setError] = useState('');
|
||||
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
||||
const [newApiKey, setNewApiKey] = useState<string | null>(null);
|
||||
const [deleting, setDeleting] = useState<User | null>(null);
|
||||
const [rotating, setRotating] = useState<User | null>(null);
|
||||
|
||||
const submitting = createUser.isPending || updateUser.isPending;
|
||||
|
||||
@@ -130,25 +143,31 @@ export default function AdminUsers() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (u: User) => {
|
||||
if (!confirm(`Delete user "${u.displayName}"?`)) return;
|
||||
await deleteUser.mutateAsync(u.id);
|
||||
const confirmDelete = async () => {
|
||||
if (!deleting) return;
|
||||
try {
|
||||
await deleteUser.mutateAsync(deleting.id);
|
||||
toast.success(`Deleted ${deleting.displayName}`);
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message || 'Failed to delete user');
|
||||
}
|
||||
setDeleting(null);
|
||||
};
|
||||
|
||||
const handleRegenerateKey = async (u: User) => {
|
||||
if (
|
||||
!confirm(
|
||||
`Regenerate API key for "${u.displayName}"? The old key will stop working immediately.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
const updated = await updateUser.mutateAsync({
|
||||
id: u.id,
|
||||
data: { regenerateApiKey: true },
|
||||
});
|
||||
setNewApiKey(updated.apiKey ?? null);
|
||||
setSelected(u);
|
||||
setModal('edit');
|
||||
const confirmRegenerate = async () => {
|
||||
if (!rotating) return;
|
||||
try {
|
||||
const updated = await updateUser.mutateAsync({
|
||||
id: rotating.id,
|
||||
data: { regenerateApiKey: true },
|
||||
});
|
||||
setNewApiKey(updated.apiKey ?? null);
|
||||
setSelected(rotating);
|
||||
setModal('edit');
|
||||
} catch (e) {
|
||||
toast.error((e as Error).message || 'Failed to rotate key');
|
||||
}
|
||||
setRotating(null);
|
||||
};
|
||||
|
||||
const copyToClipboard = (key: string) => {
|
||||
@@ -210,7 +229,7 @@ export default function AdminUsers() {
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{u.role === 'SERVICE' && (
|
||||
<button
|
||||
onClick={() => handleRegenerateKey(u)}
|
||||
onClick={() => setRotating(u)}
|
||||
className="text-gray-600 hover:text-gray-300 transition-colors"
|
||||
title="Regenerate API key"
|
||||
>
|
||||
@@ -225,7 +244,7 @@ export default function AdminUsers() {
|
||||
</button>
|
||||
{u.id !== authUser?.id && (
|
||||
<button
|
||||
onClick={() => handleDelete(u)}
|
||||
onClick={() => setDeleting(u)}
|
||||
className="text-gray-600 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
@@ -365,6 +384,42 @@ export default function AdminUsers() {
|
||||
)}
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<AlertDialog open={!!deleting} onOpenChange={(o) => !o && setDeleting(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete {deleting?.displayName}?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This user will be permanently removed. Their tickets and comments are preserved.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={!!rotating} onOpenChange={(o) => !o && setRotating(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Regenerate API key for {rotating?.displayName}?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
The old key will stop working immediately. You'll see the new key once — make
|
||||
sure whatever uses it can be updated.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={confirmRegenerate}>Rotate key</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user