mirror of
				https://github.com/9technologygroup/patchmon.net.git
				synced 2025-11-04 05:53:27 +00:00 
			
		
		
		
	first commit
This commit is contained in:
		
							
								
								
									
										460
									
								
								frontend/src/pages/Dashboard.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								frontend/src/pages/Dashboard.jsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,460 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react'
 | 
			
		||||
import { useQuery } from '@tanstack/react-query'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { 
 | 
			
		||||
  Server, 
 | 
			
		||||
  Package, 
 | 
			
		||||
  AlertTriangle, 
 | 
			
		||||
  Shield, 
 | 
			
		||||
  TrendingUp,
 | 
			
		||||
  RefreshCw,
 | 
			
		||||
  Clock
 | 
			
		||||
} from 'lucide-react'
 | 
			
		||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title } from 'chart.js'
 | 
			
		||||
import { Pie, Bar } from 'react-chartjs-2'
 | 
			
		||||
import { dashboardAPI, dashboardPreferencesAPI, formatRelativeTime } from '../utils/api'
 | 
			
		||||
import DashboardSettingsModal from '../components/DashboardSettingsModal'
 | 
			
		||||
import { useTheme } from '../contexts/ThemeContext'
 | 
			
		||||
 | 
			
		||||
// Register Chart.js components
 | 
			
		||||
ChartJS.register(ArcElement, Tooltip, Legend, CategoryScale, LinearScale, BarElement, Title)
 | 
			
		||||
 | 
			
		||||
const Dashboard = () => {
 | 
			
		||||
  const [showSettingsModal, setShowSettingsModal] = useState(false)
 | 
			
		||||
  const [cardPreferences, setCardPreferences] = useState([])
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const { isDark } = useTheme()
 | 
			
		||||
 | 
			
		||||
  // Navigation handlers
 | 
			
		||||
  const handleTotalHostsClick = () => {
 | 
			
		||||
    navigate('/hosts')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleHostsNeedingUpdatesClick = () => {
 | 
			
		||||
    navigate('/hosts?filter=needsUpdates')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOutdatedPackagesClick = () => {
 | 
			
		||||
    navigate('/packages?filter=outdated')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSecurityUpdatesClick = () => {
 | 
			
		||||
    navigate('/packages?filter=security')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const { data: stats, isLoading, error, refetch } = useQuery({
 | 
			
		||||
    queryKey: ['dashboardStats'],
 | 
			
		||||
    queryFn: () => dashboardAPI.getStats().then(res => res.data),
 | 
			
		||||
      refetchInterval: 60000, // Refresh every minute
 | 
			
		||||
      staleTime: 30000, // Consider data stale after 30 seconds
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Fetch user's dashboard preferences
 | 
			
		||||
  const { data: preferences, refetch: refetchPreferences } = useQuery({
 | 
			
		||||
    queryKey: ['dashboardPreferences'],
 | 
			
		||||
    queryFn: () => dashboardPreferencesAPI.get().then(res => res.data),
 | 
			
		||||
    staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Fetch default card configuration
 | 
			
		||||
  const { data: defaultCards } = useQuery({
 | 
			
		||||
    queryKey: ['dashboardDefaultCards'],
 | 
			
		||||
    queryFn: () => dashboardPreferencesAPI.getDefaults().then(res => res.data),
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Merge preferences with default cards
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (preferences && defaultCards) {
 | 
			
		||||
      const mergedCards = defaultCards.map(defaultCard => {
 | 
			
		||||
        const userPreference = preferences.find(p => p.cardId === defaultCard.cardId);
 | 
			
		||||
        return {
 | 
			
		||||
          ...defaultCard,
 | 
			
		||||
          enabled: userPreference ? userPreference.enabled : defaultCard.enabled,
 | 
			
		||||
          order: userPreference ? userPreference.order : defaultCard.order
 | 
			
		||||
        };
 | 
			
		||||
      }).sort((a, b) => a.order - b.order);
 | 
			
		||||
      
 | 
			
		||||
      setCardPreferences(mergedCards);
 | 
			
		||||
    } else if (defaultCards) {
 | 
			
		||||
      // If no preferences exist, use defaults
 | 
			
		||||
      setCardPreferences(defaultCards.sort((a, b) => a.order - b.order));
 | 
			
		||||
    }
 | 
			
		||||
  }, [preferences, defaultCards])
 | 
			
		||||
 | 
			
		||||
  // Listen for custom event from Layout component
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleOpenSettings = () => {
 | 
			
		||||
      setShowSettingsModal(true);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    window.addEventListener('openDashboardSettings', handleOpenSettings);
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('openDashboardSettings', handleOpenSettings);
 | 
			
		||||
    };
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  // Helper function to check if a card should be displayed
 | 
			
		||||
  const isCardEnabled = (cardId) => {
 | 
			
		||||
    const card = cardPreferences.find(c => c.cardId === cardId);
 | 
			
		||||
    return card ? card.enabled : true; // Default to enabled if not found
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper function to get card type for layout grouping
 | 
			
		||||
  const getCardType = (cardId) => {
 | 
			
		||||
    if (['totalHosts', 'hostsNeedingUpdates', 'totalOutdatedPackages', 'securityUpdates'].includes(cardId)) {
 | 
			
		||||
      return 'stats';
 | 
			
		||||
    } else if (['osDistribution', 'updateStatus', 'packagePriority'].includes(cardId)) {
 | 
			
		||||
      return 'charts';
 | 
			
		||||
    } else if (['erroredHosts', 'quickStats'].includes(cardId)) {
 | 
			
		||||
      return 'fullwidth';
 | 
			
		||||
    }
 | 
			
		||||
    return 'fullwidth'; // Default to full width
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper function to get CSS class for card group
 | 
			
		||||
  const getGroupClassName = (cardType) => {
 | 
			
		||||
    switch (cardType) {
 | 
			
		||||
      case 'stats':
 | 
			
		||||
        return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4';
 | 
			
		||||
      case 'charts':
 | 
			
		||||
        return 'grid grid-cols-1 lg:grid-cols-3 gap-6';
 | 
			
		||||
      case 'fullwidth':
 | 
			
		||||
        return 'space-y-6';
 | 
			
		||||
      default:
 | 
			
		||||
        return 'space-y-6';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper function to render a card by ID
 | 
			
		||||
  const renderCard = (cardId) => {
 | 
			
		||||
    switch (cardId) {
 | 
			
		||||
      case 'totalHosts':
 | 
			
		||||
        return (
 | 
			
		||||
          <div 
 | 
			
		||||
            className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200"
 | 
			
		||||
            onClick={handleTotalHostsClick}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex items-center">
 | 
			
		||||
              <div className="flex-shrink-0">
 | 
			
		||||
                <Server className="h-5 w-5 text-primary-600 mr-2" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="w-0 flex-1">
 | 
			
		||||
                <p className="text-sm text-secondary-500 dark:text-white">Total Hosts</p>
 | 
			
		||||
                <p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
                  {stats.cards.totalHosts}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'hostsNeedingUpdates':
 | 
			
		||||
        return (
 | 
			
		||||
          <div 
 | 
			
		||||
            className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200"
 | 
			
		||||
            onClick={handleHostsNeedingUpdatesClick}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex items-center">
 | 
			
		||||
              <div className="flex-shrink-0">
 | 
			
		||||
                <AlertTriangle className="h-5 w-5 text-warning-600 mr-2" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="w-0 flex-1">
 | 
			
		||||
                <p className="text-sm text-secondary-500 dark:text-white">Needs Updating</p>
 | 
			
		||||
                <p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
                  {stats.cards.hostsNeedingUpdates}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'totalOutdatedPackages':
 | 
			
		||||
        return (
 | 
			
		||||
          <div 
 | 
			
		||||
            className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200"
 | 
			
		||||
            onClick={handleOutdatedPackagesClick}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex items-center">
 | 
			
		||||
              <div className="flex-shrink-0">
 | 
			
		||||
                <Package className="h-5 w-5 text-secondary-600 mr-2" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="w-0 flex-1">
 | 
			
		||||
                <p className="text-sm text-secondary-500 dark:text-white">Outdated Packages</p>
 | 
			
		||||
                <p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
                  {stats.cards.totalOutdatedPackages}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'securityUpdates':
 | 
			
		||||
        return (
 | 
			
		||||
          <div 
 | 
			
		||||
            className="card p-4 cursor-pointer hover:shadow-card-hover dark:hover:shadow-card-hover-dark transition-shadow duration-200"
 | 
			
		||||
            onClick={handleSecurityUpdatesClick}
 | 
			
		||||
          >
 | 
			
		||||
            <div className="flex items-center">
 | 
			
		||||
              <div className="flex-shrink-0">
 | 
			
		||||
                <Shield className="h-5 w-5 text-danger-600 mr-2" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="w-0 flex-1">
 | 
			
		||||
                <p className="text-sm text-secondary-500 dark:text-white">Security Updates</p>
 | 
			
		||||
                <p className="text-xl font-semibold text-secondary-900 dark:text-white">
 | 
			
		||||
                  {stats.cards.securityUpdates}
 | 
			
		||||
                </p>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'erroredHosts':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className={`border rounded-lg p-4 ${
 | 
			
		||||
            stats.cards.erroredHosts > 0 
 | 
			
		||||
              ? 'bg-danger-50 border-danger-200' 
 | 
			
		||||
              : 'bg-success-50 border-success-200'
 | 
			
		||||
          }`}>
 | 
			
		||||
            <div className="flex">
 | 
			
		||||
              <AlertTriangle className={`h-5 w-5 ${
 | 
			
		||||
                stats.cards.erroredHosts > 0 ? 'text-danger-400' : 'text-success-400'
 | 
			
		||||
              }`} />
 | 
			
		||||
              <div className="ml-3">
 | 
			
		||||
                {stats.cards.erroredHosts > 0 ? (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <h3 className="text-sm font-medium text-danger-800">
 | 
			
		||||
                      {stats.cards.erroredHosts} host{stats.cards.erroredHosts > 1 ? 's' : ''} haven't reported in 24+ hours
 | 
			
		||||
                    </h3>
 | 
			
		||||
                    <p className="text-sm text-danger-700 mt-1">
 | 
			
		||||
                      These hosts may be offline or experiencing connectivity issues.
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <h3 className="text-sm font-medium text-success-800">
 | 
			
		||||
                      All hosts are reporting normally
 | 
			
		||||
                    </h3>
 | 
			
		||||
                    <p className="text-sm text-success-700 mt-1">
 | 
			
		||||
                      No hosts have failed to report in the last 24 hours.
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'osDistribution':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="card p-6">
 | 
			
		||||
            <h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">OS Distribution</h3>
 | 
			
		||||
            <div className="h-64">
 | 
			
		||||
              <Pie data={osChartData} options={chartOptions} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'updateStatus':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="card p-6">
 | 
			
		||||
            <h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">Update Status</h3>
 | 
			
		||||
            <div className="h-64">
 | 
			
		||||
              <Pie data={updateStatusChartData} options={chartOptions} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'packagePriority':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="card p-6">
 | 
			
		||||
            <h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-4">Package Priority</h3>
 | 
			
		||||
            <div className="h-64">
 | 
			
		||||
              <Pie data={packagePriorityChartData} options={chartOptions} />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      case 'quickStats':
 | 
			
		||||
        return (
 | 
			
		||||
          <div className="card p-6">
 | 
			
		||||
            <div className="flex items-center justify-between mb-4">
 | 
			
		||||
              <h3 className="text-lg font-medium text-secondary-900 dark:text-white">Quick Stats</h3>
 | 
			
		||||
              <TrendingUp className="h-5 w-5 text-success-500" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
 | 
			
		||||
              <div className="text-center">
 | 
			
		||||
                <div className="text-2xl font-bold text-primary-600">
 | 
			
		||||
                  {((stats.cards.hostsNeedingUpdates / stats.cards.totalHosts) * 100).toFixed(1)}%
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="text-sm text-secondary-500 dark:text-secondary-300">Hosts need updates</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="text-center">
 | 
			
		||||
                <div className="text-2xl font-bold text-danger-600">
 | 
			
		||||
                  {stats.cards.securityUpdates}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="text-sm text-secondary-500 dark:text-secondary-300">Security updates pending</div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="text-center">
 | 
			
		||||
                <div className="text-2xl font-bold text-success-600">
 | 
			
		||||
                  {stats.cards.totalHosts - stats.cards.erroredHosts}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="text-sm text-secondary-500 dark:text-secondary-300">Hosts online</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      
 | 
			
		||||
      default:
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (isLoading) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="flex items-center justify-center h-64">
 | 
			
		||||
        <RefreshCw className="h-8 w-8 animate-spin text-primary-600" />
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (error) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="bg-danger-50 border border-danger-200 rounded-md p-4">
 | 
			
		||||
        <div className="flex">
 | 
			
		||||
          <AlertTriangle className="h-5 w-5 text-danger-400" />
 | 
			
		||||
          <div className="ml-3">
 | 
			
		||||
            <h3 className="text-sm font-medium text-danger-800">Error loading dashboard</h3>
 | 
			
		||||
            <p className="text-sm text-danger-700 mt-1">
 | 
			
		||||
              {error.message || 'Failed to load dashboard statistics'}
 | 
			
		||||
            </p>
 | 
			
		||||
            <button
 | 
			
		||||
              onClick={() => refetch()}
 | 
			
		||||
              className="mt-2 btn-danger text-xs"
 | 
			
		||||
            >
 | 
			
		||||
              Try again
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const chartOptions = {
 | 
			
		||||
    responsive: true,
 | 
			
		||||
    plugins: {
 | 
			
		||||
      legend: {
 | 
			
		||||
        position: 'bottom',
 | 
			
		||||
        labels: {
 | 
			
		||||
          color: isDark ? '#ffffff' : '#374151',
 | 
			
		||||
          font: {
 | 
			
		||||
            size: 12
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const osChartData = {
 | 
			
		||||
    labels: stats.charts.osDistribution.map(item => item.name),
 | 
			
		||||
    datasets: [
 | 
			
		||||
      {
 | 
			
		||||
        data: stats.charts.osDistribution.map(item => item.count),
 | 
			
		||||
        backgroundColor: [
 | 
			
		||||
          '#3B82F6', // Blue
 | 
			
		||||
          '#10B981', // Green
 | 
			
		||||
          '#F59E0B', // Yellow
 | 
			
		||||
          '#EF4444', // Red
 | 
			
		||||
          '#8B5CF6', // Purple
 | 
			
		||||
          '#06B6D4', // Cyan
 | 
			
		||||
        ],
 | 
			
		||||
        borderWidth: 2,
 | 
			
		||||
        borderColor: '#ffffff',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const updateStatusChartData = {
 | 
			
		||||
    labels: stats.charts.updateStatusDistribution.map(item => item.name),
 | 
			
		||||
    datasets: [
 | 
			
		||||
      {
 | 
			
		||||
        data: stats.charts.updateStatusDistribution.map(item => item.count),
 | 
			
		||||
        backgroundColor: [
 | 
			
		||||
          '#10B981', // Green - Up to date
 | 
			
		||||
          '#F59E0B', // Yellow - Needs updates
 | 
			
		||||
          '#EF4444', // Red - Errored
 | 
			
		||||
        ],
 | 
			
		||||
        borderWidth: 2,
 | 
			
		||||
        borderColor: '#ffffff',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const packagePriorityChartData = {
 | 
			
		||||
    labels: stats.charts.packageUpdateDistribution.map(item => item.name),
 | 
			
		||||
    datasets: [
 | 
			
		||||
      {
 | 
			
		||||
        data: stats.charts.packageUpdateDistribution.map(item => item.count),
 | 
			
		||||
        backgroundColor: [
 | 
			
		||||
          '#EF4444', // Red - Security
 | 
			
		||||
          '#3B82F6', // Blue - Regular
 | 
			
		||||
        ],
 | 
			
		||||
        borderWidth: 2,
 | 
			
		||||
        borderColor: '#ffffff',
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="space-y-6">
 | 
			
		||||
 | 
			
		||||
      {/* Dynamically Rendered Cards - Unified Order */}
 | 
			
		||||
      {(() => {
 | 
			
		||||
        const enabledCards = cardPreferences
 | 
			
		||||
          .filter(card => isCardEnabled(card.cardId))
 | 
			
		||||
          .sort((a, b) => a.order - b.order);
 | 
			
		||||
        
 | 
			
		||||
        // Group consecutive cards of the same type for proper layout
 | 
			
		||||
        const cardGroups = [];
 | 
			
		||||
        let currentGroup = null;
 | 
			
		||||
        
 | 
			
		||||
        enabledCards.forEach(card => {
 | 
			
		||||
          const cardType = getCardType(card.cardId);
 | 
			
		||||
          
 | 
			
		||||
          if (!currentGroup || currentGroup.type !== cardType) {
 | 
			
		||||
            // Start a new group
 | 
			
		||||
            currentGroup = {
 | 
			
		||||
              type: cardType,
 | 
			
		||||
              cards: [card]
 | 
			
		||||
            };
 | 
			
		||||
            cardGroups.push(currentGroup);
 | 
			
		||||
          } else {
 | 
			
		||||
            // Add to existing group
 | 
			
		||||
            currentGroup.cards.push(card);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        return (
 | 
			
		||||
          <>
 | 
			
		||||
            {cardGroups.map((group, groupIndex) => (
 | 
			
		||||
              <div key={groupIndex} className={getGroupClassName(group.type)}>
 | 
			
		||||
                {group.cards.map(card => (
 | 
			
		||||
                  <div key={card.cardId}>
 | 
			
		||||
                    {renderCard(card.cardId)}
 | 
			
		||||
            </div>
 | 
			
		||||
                ))}
 | 
			
		||||
          </div>
 | 
			
		||||
            ))}
 | 
			
		||||
          </>
 | 
			
		||||
        );
 | 
			
		||||
      })()}
 | 
			
		||||
 | 
			
		||||
      {/* Dashboard Settings Modal */}
 | 
			
		||||
      <DashboardSettingsModal 
 | 
			
		||||
        isOpen={showSettingsModal} 
 | 
			
		||||
        onClose={() => setShowSettingsModal(false)} 
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Dashboard 
 | 
			
		||||
		Reference in New Issue
	
	Block a user