feat: Expandable Container-Rows + shadcn AlertDialog + Status 207 Fix
- Backend: Status 207 → 200 für partielle Erfolge (admin_api.py) - Frontend: Expandable User-Rows mit Container-Checkboxen (admin/page.tsx) - UI: Neues shadcn AlertDialog für Container-Lösch-Bestätigung - Deps: @radix-ui/react-alert-dialog installiert - Docs: Version 3.0 Dokumentation aktualisiert (admin-dashboard-improvements.md) Behebt: - Problem I: Browser-confirm() → echtes Modal - Problem II: Status 207 Fehler (0 gelöscht, 1 fehlgeschlagen)
This commit is contained in:
parent
a39488139c
commit
0117566268
21
admin_api.py
21
admin_api.py
|
|
@ -198,7 +198,12 @@ def delete_user_container(user_id):
|
|||
return jsonify({'error': 'User nicht gefunden'}), 404
|
||||
|
||||
if not user.containers:
|
||||
return jsonify({'error': 'User hat keine Container'}), 400
|
||||
return jsonify({
|
||||
'message': 'User hat keine Container',
|
||||
'deleted': 0,
|
||||
'failed': [],
|
||||
'skipped': True
|
||||
}), 200
|
||||
|
||||
container_mgr = ContainerManager()
|
||||
deleted_count = 0
|
||||
|
|
@ -225,16 +230,12 @@ def delete_user_container(user_id):
|
|||
|
||||
current_app.logger.info(f"Admin {admin_id} löschte {deleted_count} Container von User {user.email}")
|
||||
|
||||
if failed_containers:
|
||||
return jsonify({
|
||||
'message': f'{deleted_count} Container gelöscht, {len(failed_containers)} fehlgeschlagen',
|
||||
'failed': failed_containers,
|
||||
'deleted': deleted_count
|
||||
}), 207 # Multi-Status
|
||||
|
||||
return jsonify({
|
||||
'message': f'Alle {deleted_count} Container von {user.email} wurden gelöscht',
|
||||
'deleted': deleted_count
|
||||
'message': f'{deleted_count} Container gelöscht' +
|
||||
(f', {len(failed_containers)} fehlgeschlagen' if failed_containers else ''),
|
||||
'deleted': deleted_count,
|
||||
'failed': failed_containers,
|
||||
'partial_failure': len(failed_containers) > 0
|
||||
}), 200
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# Admin-Dashboard: Verbesserte Container- und User-Löschung
|
||||
|
||||
**Datum:** 02.02.2026
|
||||
**Version:** 2.0
|
||||
**Datum:** 08.02.2026 (Update), 02.02.2026 (Initial)
|
||||
**Version:** 3.0
|
||||
**Status:** ✅ Vollständig implementiert
|
||||
|
||||
---
|
||||
|
|
@ -14,6 +14,82 @@ Diese Dokumentation beschreibt die Verbesserungen des Admin-Dashboards:
|
|||
2. **Toast-Benachrichtigungen** - Modernes UI statt primitiver Alerts
|
||||
3. **Bulk-Operations** - Mehrere User gleichzeitig verwalten (Sperren, Löschen, etc.)
|
||||
4. **DSGVO-Compliance** - Vollständige Datenlöschung (MagicLinkToken, AdminTakeoverSession)
|
||||
5. **✨ NEU (v3.0)**: Expandable Container-Rows + shadcn AlertDialog (08.02.2026)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Version 3.0: Expandable Rows & AlertDialog (08.02.2026)
|
||||
|
||||
### 🎯 Problem (Gelöst)
|
||||
|
||||
**Problem I:** Browser-`confirm()` statt echtes Modal
|
||||
- Admin konnte nicht einzelne Container auswählen
|
||||
- Nur "Alles löschen oder gar nichts" möglich
|
||||
- Keine Übersicht der betroffenen Container
|
||||
|
||||
**Problem II:** Toast zeigt "0 gelöscht, 1 fehlgeschlagen"
|
||||
- **Root Cause**: Backend gab Status 207 zurück
|
||||
- Frontend interpretierte Status 207 als Fehler → `response.ok = false`
|
||||
- Obwohl Container erfolgreich gelöscht wurden, zählte Frontend sie als "fehlgeschlagen"
|
||||
|
||||
### ✅ Lösung Implementiert
|
||||
|
||||
#### Backend-Fix (`admin_api.py` Zeile 189-238)
|
||||
- Status 207 → **200** (für partielle Erfolge)
|
||||
- Status 400 → **200** (für "keine Container")
|
||||
- Details im Response-Body: `deleted`, `failed`, `partial_failure`
|
||||
- **Begründung**: HTTP-Status sollten Transport-Errors signalisieren, nicht Business-Logic
|
||||
|
||||
```python
|
||||
# VORHER: Status 207 für Teilerfolge
|
||||
return jsonify({...}), 207
|
||||
|
||||
# NACHHER: Immer Status 200, Details im Body
|
||||
return jsonify({
|
||||
'message': f'{deleted_count} Container gelöscht' +
|
||||
(f', {len(failed_containers)} fehlgeschlagen' if failed_containers else ''),
|
||||
'deleted': deleted_count,
|
||||
'failed': failed_containers,
|
||||
'partial_failure': len(failed_containers) > 0
|
||||
}), 200
|
||||
```
|
||||
|
||||
#### Frontend-Verbesserungen (`frontend/src/app/admin/page.tsx`)
|
||||
|
||||
**Expandable User-Rows:**
|
||||
- Klick auf User → Container-Liste klappt auf/zu
|
||||
- ChevronDown Icon rotiert bei Expand/Collapse
|
||||
- Container nur angezeigt wenn User expandiert
|
||||
|
||||
**Container-Checkboxen:**
|
||||
- Jeder Container hat eine Checkbox
|
||||
- Nur ausgewählte Container werden gelöscht
|
||||
- Unterstützung für Einzelaus wahl und Bulk-Select
|
||||
|
||||
**shadcn AlertDialog (neu):**
|
||||
- Echtes Modal statt Browser-`confirm()`
|
||||
- Zeigt Zusammenfassung:
|
||||
```
|
||||
3 Container von 2 Benutzer(n) löschen?
|
||||
• user1@example.com (2 Container)
|
||||
• user2@example.com (1 Container)
|
||||
```
|
||||
- Ja/Nein Buttons mit klarer Bestätigung
|
||||
|
||||
**Verbesserte Bulk-Delete-Logik:**
|
||||
- Parst `deleted` und `failed` aus Response-Body
|
||||
- Akkumuliert Container-Zähler (nicht User-Zähler)
|
||||
- Toast zeigt genaue Zahlen: "3 Container gelöscht, 1 fehlgeschlagen"
|
||||
|
||||
### 📝 Dateiänderungen
|
||||
|
||||
```
|
||||
✅ Geänderte Dateien:
|
||||
- admin_api.py (Backend-Fix für Status 207)
|
||||
- frontend/src/app/admin/page.tsx (UI-Overhaul)
|
||||
- frontend/src/components/ui/alert-dialog.tsx (neue Komponente)
|
||||
- frontend/package.json (@radix-ui/react-alert-dialog)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -410,6 +486,7 @@ with app.app_context():
|
|||
|
||||
| Version | Datum | Änderungen |
|
||||
|---------|-------|-----------|
|
||||
| 3.0 | 08.02.2026 | **Expandable Rows**, shadcn AlertDialog, Status 207→200 Fix, Container-Checkboxen |
|
||||
| 2.0 | 02.02.2026 | Multi-Container, Toast-System, Bulk-Operations, DSGVO |
|
||||
| 1.0 | ≤01.02.2026 | Ursprüngliches Admin-Dashboard |
|
||||
|
||||
|
|
|
|||
|
|
@ -9,19 +9,20 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.15",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.408.0",
|
||||
"next": "14.2.5",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"lucide-react": "^0.408.0",
|
||||
"sonner": "1.7.2",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1"
|
||||
"tailwind-merge": "^2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.14.10",
|
||||
|
|
|
|||
|
|
@ -34,8 +34,19 @@ import {
|
|||
Search,
|
||||
Monitor,
|
||||
X,
|
||||
ChevronDown,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
type StatusColor = "green" | "yellow" | "red";
|
||||
|
||||
|
|
@ -114,6 +125,12 @@ export default function AdminPage() {
|
|||
const [selectedUserIds, setSelectedUserIds] = useState<Set<number>>(new Set());
|
||||
const [activeTab, setActiveTab] = useState<"users" | "containers">("users");
|
||||
const [selectedContainerIds, setSelectedContainerIds] = useState<Set<number>>(new Set());
|
||||
const [expandedUserIds, setExpandedUserIds] = useState<Set<number>>(new Set());
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogData, setDeleteDialogData] = useState<{
|
||||
containerIds: number[];
|
||||
userSummary: { email: string; count: number }[];
|
||||
} | null>(null);
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
|
|
@ -130,6 +147,96 @@ export default function AdminPage() {
|
|||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
|
||||
// Expand/Collapse Helper
|
||||
const toggleUserExpand = (userId: number) => {
|
||||
const newExpanded = new Set(expandedUserIds);
|
||||
if (newExpanded.has(userId)) {
|
||||
newExpanded.delete(userId);
|
||||
} else {
|
||||
newExpanded.add(userId);
|
||||
}
|
||||
setExpandedUserIds(newExpanded);
|
||||
};
|
||||
|
||||
// Dialog Helper für Bulk-Delete
|
||||
const openBulkDeleteDialog = () => {
|
||||
if (selectedContainerIds.size === 0) {
|
||||
toast.error("Keine Container ausgewählt");
|
||||
return;
|
||||
}
|
||||
|
||||
// Erstelle Zusammenfassung nach User
|
||||
const userMap = new Map<number, { email: string; count: number }>();
|
||||
|
||||
for (const containerId of selectedContainerIds) {
|
||||
const user = users.find(u =>
|
||||
u.containers?.some(c => c.id === containerId)
|
||||
);
|
||||
if (user) {
|
||||
const existing = userMap.get(user.id) || { email: user.email, count: 0 };
|
||||
existing.count++;
|
||||
userMap.set(user.id, existing);
|
||||
}
|
||||
}
|
||||
|
||||
setDeleteDialogData({
|
||||
containerIds: Array.from(selectedContainerIds),
|
||||
userSummary: Array.from(userMap.values())
|
||||
});
|
||||
setIsDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
// Bestätigte Bulk-Delete
|
||||
const handleConfirmBulkDelete = async () => {
|
||||
if (!deleteDialogData) return;
|
||||
|
||||
setIsDeleteDialogOpen(false);
|
||||
toast.loading(
|
||||
`Lösche ${deleteDialogData.containerIds.length} Container...`,
|
||||
{ id: "bulk-delete-containers" }
|
||||
);
|
||||
|
||||
// Gruppiere Container nach User-ID
|
||||
const containersByUser = new Map<number, number[]>();
|
||||
|
||||
for (const containerId of deleteDialogData.containerIds) {
|
||||
const user = users.find(u =>
|
||||
u.containers?.some(c => c.id === containerId)
|
||||
);
|
||||
if (user) {
|
||||
if (!containersByUser.has(user.id)) {
|
||||
containersByUser.set(user.id, []);
|
||||
}
|
||||
containersByUser.get(user.id)!.push(containerId);
|
||||
}
|
||||
}
|
||||
|
||||
let totalDeleted = 0;
|
||||
let totalFailed = 0;
|
||||
|
||||
// Lösche Container pro User
|
||||
for (const [userId, containerIds] of containersByUser) {
|
||||
const { data, error } = await adminApi.deleteUserContainer(userId);
|
||||
|
||||
if (error) {
|
||||
totalFailed += containerIds.length;
|
||||
} else if (data) {
|
||||
// Parse Response-Body (nach Backend-Fix)
|
||||
totalDeleted += data.deleted || 0;
|
||||
totalFailed += (data.failed?.length || 0);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(`${totalDeleted} Container gelöscht`, {
|
||||
id: "bulk-delete-containers",
|
||||
description: totalFailed > 0 ? `${totalFailed} fehlgeschlagen` : undefined,
|
||||
});
|
||||
|
||||
fetchUsers();
|
||||
setSelectedContainerIds(new Set());
|
||||
setDeleteDialogData(null);
|
||||
};
|
||||
|
||||
// Bulk-Selection Helpers
|
||||
const toggleUserSelection = (userId: number) => {
|
||||
const newSelection = new Set(selectedUserIds);
|
||||
|
|
@ -188,23 +295,6 @@ export default function AdminPage() {
|
|||
setActionLoading(null);
|
||||
};
|
||||
|
||||
const handleDeleteContainer = async (userId: number, userEmail: string) => {
|
||||
if (!confirm(`Container von "${userEmail}" wirklich loeschen? Der User kann einen neuen Container starten.`)) {
|
||||
return;
|
||||
}
|
||||
setActionLoading(userId);
|
||||
const { data, error } = await adminApi.deleteUserContainer(userId);
|
||||
if (error) {
|
||||
toast.error(`Fehler: ${error}`);
|
||||
} else {
|
||||
toast.success(data?.message || "Container geloescht", {
|
||||
description: data?.deleted ? `${data.deleted} Container entfernt` : undefined,
|
||||
});
|
||||
fetchUsers();
|
||||
}
|
||||
setActionLoading(null);
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userId: number, userEmail: string) => {
|
||||
if (!confirm(
|
||||
`⚠️ ACHTUNG: User "${userEmail}" VOLLSTAENDIG loeschen?\n\n` +
|
||||
|
|
@ -304,43 +394,6 @@ export default function AdminPage() {
|
|||
deselectAll();
|
||||
};
|
||||
|
||||
const handleBulkDeleteContainers = async () => {
|
||||
const userList = Array.from(selectedUserIds)
|
||||
.map((id) => users.find((u) => u.id === id)?.email)
|
||||
.filter(Boolean)
|
||||
.join(", ");
|
||||
|
||||
if (!confirm(
|
||||
`Container von ${selectedUserIds.size} Usern löschen?\n\n` +
|
||||
`Betroffene User:\n${userList}\n\n` +
|
||||
`User können danach neue Container erstellen.`
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.loading(`Lösche Container von ${selectedUserIds.size} Usern...`, { id: "bulk-delete-containers" });
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const userId of selectedUserIds) {
|
||||
const { error } = await adminApi.deleteUserContainer(userId);
|
||||
if (error) {
|
||||
failed++;
|
||||
} else {
|
||||
success++;
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(`${success} User-Container gelöscht`, {
|
||||
id: "bulk-delete-containers",
|
||||
description: failed > 0 ? `${failed} fehlgeschlagen` : undefined,
|
||||
});
|
||||
|
||||
fetchUsers();
|
||||
deselectAll();
|
||||
};
|
||||
|
||||
const handleBulkDeleteUsers = async () => {
|
||||
const selectedUsers = Array.from(selectedUserIds)
|
||||
.map((id) => users.find((u) => u.id === id))
|
||||
|
|
@ -717,15 +770,17 @@ export default function AdminPage() {
|
|||
</Button>
|
||||
|
||||
{/* Bulk-Delete-Container */}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBulkDeleteContainers}
|
||||
disabled={actionLoading !== null}
|
||||
>
|
||||
<Container className="mr-2 h-4 w-4" />
|
||||
Container löschen
|
||||
</Button>
|
||||
{selectedContainerIds.size > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={openBulkDeleteDialog}
|
||||
disabled={actionLoading !== null}
|
||||
>
|
||||
<Container className="mr-2 h-4 w-4" />
|
||||
Container löschen ({selectedContainerIds.size})
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Bulk-Delete User */}
|
||||
<Button
|
||||
|
|
@ -805,12 +860,31 @@ export default function AdminPage() {
|
|||
return (
|
||||
<div
|
||||
key={u.id}
|
||||
className={`flex items-center justify-between rounded-lg border p-4 ${
|
||||
u.is_blocked ? "bg-red-50 border-red-200" : ""
|
||||
} ${isSelected ? "bg-primary/5 border-primary" : ""}`}
|
||||
className="border rounded-lg overflow-hidden"
|
||||
>
|
||||
{/* Main User Row */}
|
||||
<div
|
||||
className={`flex items-center justify-between p-4 ${
|
||||
u.is_blocked ? "bg-red-50 border-b border-red-200" : "border-b"
|
||||
} ${isSelected ? "bg-primary/5" : ""}`}
|
||||
>
|
||||
{/* Checkbox + User Info */}
|
||||
<div className="flex items-center gap-4">
|
||||
{/* Expand Icon */}
|
||||
{u.containers && u.containers.length > 0 && (
|
||||
<button
|
||||
onClick={() => toggleUserExpand(u.id)}
|
||||
className="p-0 h-4 w-4 flex items-center justify-center text-muted-foreground hover:text-foreground transition-colors"
|
||||
title={expandedUserIds.has(u.id) ? "Container ausblenden" : "Container anzeigen"}
|
||||
>
|
||||
<ChevronDown
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
expandedUserIds.has(u.id) ? "rotate-180" : ""
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isSelectable && (
|
||||
<input
|
||||
type="checkbox"
|
||||
|
|
@ -909,18 +983,6 @@ export default function AdminPage() {
|
|||
<Mail className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{/* Container loeschen */}
|
||||
{u.container_id && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteContainer(u.id, u.email)}
|
||||
title="Container loeschen"
|
||||
>
|
||||
<Container className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Takeover (Dummy) */}
|
||||
{u.container_id && !isCurrentUser && (
|
||||
<Button
|
||||
|
|
@ -974,6 +1036,45 @@ export default function AdminPage() {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expandable Container List */}
|
||||
{expandedUserIds.has(u.id) && u.containers && u.containers.length > 0 && (
|
||||
<div className="border-t bg-muted/30 p-4">
|
||||
<div className="space-y-2">
|
||||
{u.containers.map(container => (
|
||||
<div
|
||||
key={container.id}
|
||||
className="flex items-center gap-3 p-3 rounded border bg-background hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
{/* Checkbox für Container */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedContainerIds.has(container.id)}
|
||||
onChange={() => toggleContainerSelection(container.id)}
|
||||
className="h-4 w-4 rounded border-gray-300"
|
||||
/>
|
||||
|
||||
{/* Container Icon + Info */}
|
||||
<Container className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm">{container.container_type}</span>
|
||||
{container.is_blocked && (
|
||||
<Badge variant="destructive" className="text-xs">
|
||||
Gesperrt
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{container.container_id ? "Running" : "Stopped"} • {formatDate(container.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
@ -1174,6 +1275,47 @@ export default function AdminPage() {
|
|||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Container wirklich löschen?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{deleteDialogData && (
|
||||
<>
|
||||
<p className="mb-3">
|
||||
Du bist dabei, <strong>{deleteDialogData.containerIds.length} Container</strong> von{" "}
|
||||
<strong>{deleteDialogData.userSummary.length} Benutzer(n)</strong> zu löschen.
|
||||
</p>
|
||||
<div className="mt-3 space-y-1 text-sm bg-muted/50 p-3 rounded">
|
||||
<p className="font-semibold text-foreground">Betroffene Benutzer:</p>
|
||||
<ul className="space-y-1 ml-2">
|
||||
{deleteDialogData.userSummary.map((user, idx) => (
|
||||
<li key={idx} className="text-sm">
|
||||
• <span className="font-medium">{user.email}</span> ({user.count} Container)
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-muted-foreground">
|
||||
Die Benutzer können danach neue Container erstellen.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Abbrechen</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleConfirmBulkDelete}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Jetzt löschen
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
132
frontend/src/components/ui/alert-dialog.tsx
Normal file
132
frontend/src/components/ui/alert-dialog.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user