import { closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Eye, EyeOff, GripVertical, RotateCcw, Save, Settings as SettingsIcon, X, } from "lucide-react"; import { useEffect, useState } from "react"; import { dashboardPreferencesAPI } from "../utils/api"; // Sortable Card Item Component const SortableCardItem = ({ card, onToggle }) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: card.cardId, }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.5 : 1, }; return (
{card.title} {card.typeLabel ? ( ({card.typeLabel}) ) : null}
); }; const DashboardSettingsModal = ({ isOpen, onClose }) => { const [cards, setCards] = useState([]); const [hasChanges, setHasChanges] = useState(false); const queryClient = useQueryClient(); const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }), ); // Fetch user's dashboard preferences const { data: preferences, isLoading } = useQuery({ queryKey: ["dashboardPreferences"], queryFn: () => dashboardPreferencesAPI.get().then((res) => res.data), enabled: isOpen, }); // Fetch default card configuration const { data: defaultCards } = useQuery({ queryKey: ["dashboardDefaultCards"], queryFn: () => dashboardPreferencesAPI.getDefaults().then((res) => res.data), enabled: isOpen, }); // Update preferences mutation const updatePreferencesMutation = useMutation({ mutationFn: (preferences) => dashboardPreferencesAPI.update(preferences), onSuccess: (response) => { // Optimistically update the query cache with the correct data structure queryClient.setQueryData( ["dashboardPreferences"], response.data.preferences, ); // Also invalidate to ensure fresh data queryClient.invalidateQueries(["dashboardPreferences"]); setHasChanges(false); onClose(); }, onError: (error) => { console.error("Failed to update dashboard preferences:", error); }, }); // Initialize cards when preferences or defaults are loaded useEffect(() => { if (preferences && defaultCards) { // Normalize server preferences (snake_case -> camelCase) const normalizedPreferences = preferences.map((p) => ({ cardId: p.cardId ?? p.card_id, enabled: p.enabled, order: p.order, })); const typeLabelFor = (cardId) => { if ( [ "totalHosts", "hostsNeedingUpdates", "totalOutdatedPackages", "securityUpdates", "upToDateHosts", "totalHostGroups", "totalUsers", "totalRepos", ].includes(cardId) ) return "Top card"; if (cardId === "osDistribution") return "Pie chart"; if (cardId === "osDistributionBar") return "Bar chart"; if (cardId === "updateStatus") return "Pie chart"; if (cardId === "packagePriority") return "Pie chart"; if (cardId === "recentUsers") return "Table"; if (cardId === "recentCollection") return "Table"; if (cardId === "quickStats") return "Wide card"; return undefined; }; // Merge user preferences with default cards const mergedCards = defaultCards .map((defaultCard) => { const userPreference = normalizedPreferences.find( (p) => p.cardId === defaultCard.cardId, ); return { ...defaultCard, enabled: userPreference ? userPreference.enabled : defaultCard.enabled, order: userPreference ? userPreference.order : defaultCard.order, typeLabel: typeLabelFor(defaultCard.cardId), }; }) .sort((a, b) => a.order - b.order); setCards(mergedCards); } }, [preferences, defaultCards]); const handleDragEnd = (event) => { const { active, over } = event; if (active.id !== over.id) { setCards((items) => { const oldIndex = items.findIndex((item) => item.cardId === active.id); const newIndex = items.findIndex((item) => item.cardId === over.id); const newItems = arrayMove(items, oldIndex, newIndex); // Update order values return newItems.map((item, index) => ({ ...item, order: index, })); }); setHasChanges(true); } }; const handleToggle = (cardId) => { setCards((prevCards) => prevCards.map((card) => card.cardId === cardId ? { ...card, enabled: !card.enabled } : card, ), ); setHasChanges(true); }; const handleSave = () => { const preferences = cards.map((card) => ({ cardId: card.cardId, enabled: card.enabled, order: card.order, })); updatePreferencesMutation.mutate(preferences); }; const handleReset = () => { if (defaultCards) { const resetCards = defaultCards.map((card) => ({ ...card, enabled: true, order: card.order, })); setCards(resetCards); setHasChanges(true); } }; if (!isOpen) return null; return (

Customize your dashboard by reordering cards and toggling their visibility. Drag cards to reorder them, and click the visibility toggle to show/hide cards.

{isLoading ? (
) : ( card.cardId)} strategy={verticalListSortingStrategy} >
{cards.map((card) => ( ))}
)}
); }; export default DashboardSettingsModal;