Skip to content

Commit 0bf4201

Browse files
committed
Enhance UpdatesPage and AutoUpdateHistory components
- Added critical updates banner to the UpdatesPage, displaying the number of critical updates requiring attention. - Introduced logic to calculate and display the age of the oldest critical update. - Improved the layout and animations for better user experience, including motion effects based on user preferences. - Updated AutoUpdateHistory to group updates by date and enhance visual representation with connecting lines. - Refactored UpdateCard to include policy badges and improved version comparison display.
1 parent 866c659 commit 0bf4201

File tree

5 files changed

+505
-287
lines changed

5 files changed

+505
-287
lines changed

app/api/updates/refresh/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export async function POST(request: NextRequest) {
7474
}
7575

7676
// Reuse the live Intune matching route, then sync results into update_check_results.
77+
// The live route calls the Graph API list endpoint which returns largeIcon data inline.
7778
const forwardHeaders = new Headers({
7879
Authorization: authHeader,
7980
});

app/dashboard/updates/page.tsx

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useState, useCallback, useRef, useEffect } from 'react';
44
import { useRouter } from 'next/navigation';
5-
import { motion } from 'framer-motion';
5+
import { motion, useReducedMotion } from 'framer-motion';
66
import {
77
RefreshCw,
88
AlertTriangle,
@@ -25,6 +25,7 @@ import {
2525
useUpdatePolicy,
2626
} from '@/hooks/use-updates';
2727
import { useMspOptional } from '@/hooks/useMspOptional';
28+
import { fadeIn } from '@/lib/animations/variants';
2829
import { cn } from '@/lib/utils';
2930
import type { AvailableUpdate, UpdatePolicyType } from '@/types/update-policies';
3031

@@ -34,6 +35,7 @@ export default function UpdatesPage() {
3435
const [showCriticalOnly, setShowCriticalOnly] = useState(false);
3536
const [activeTab, setActiveTab] = useState<'available' | 'history'>('available');
3637
const [updatingIds, setUpdatingIds] = useState<Set<string>>(new Set());
38+
const shouldReduceMotion = useReducedMotion();
3739
const { isMspUser, selectedTenantId, managedTenants } = useMspOptional();
3840
const tenantId = isMspUser ? selectedTenantId || undefined : undefined;
3941
const hasGrantedManagedTenants = managedTenants.some(
@@ -90,6 +92,21 @@ export default function UpdatesPage() {
9092
(h) => h.status === 'failed' && isWithinDays(h.triggered_at, 7)
9193
).length;
9294

95+
// Oldest critical update age
96+
const oldestCriticalAge = (() => {
97+
const criticals = updates.filter((u) => u.is_critical);
98+
if (criticals.length === 0) return null;
99+
const oldest = criticals.reduce((prev, curr) =>
100+
new Date(prev.detected_at) < new Date(curr.detected_at) ? prev : curr
101+
);
102+
const days = Math.floor(
103+
(Date.now() - new Date(oldest.detected_at).getTime()) / (1000 * 60 * 60 * 24)
104+
);
105+
if (days === 0) return 'detected today';
106+
if (days === 1) return 'oldest: 1 day ago';
107+
return `oldest: ${days} days ago`;
108+
})();
109+
93110
// Handlers
94111
const handleTriggerUpdate = useCallback(async (update: AvailableUpdate) => {
95112
setUpdatingIds((prev) => new Set(prev).add(update.id));
@@ -202,6 +219,23 @@ export default function UpdatesPage() {
202219
}
203220
/>
204221

222+
{/* Critical Updates Banner */}
223+
{criticalCount > 0 && (
224+
<motion.div
225+
variants={shouldReduceMotion ? undefined : fadeIn}
226+
initial={shouldReduceMotion ? { opacity: 1 } : 'hidden'}
227+
animate={shouldReduceMotion ? { opacity: 1 } : 'visible'}
228+
className="glass-light rounded-xl p-4 border-l-4 border-l-status-warning border-t border-r border-b border-black/[0.08] bg-gradient-to-r from-status-warning/5 to-transparent"
229+
>
230+
<div className="flex items-center gap-3">
231+
<AlertTriangle className="w-5 h-5 text-status-warning flex-shrink-0" />
232+
<p className="text-sm font-medium text-text-primary">
233+
{criticalCount} critical update{criticalCount !== 1 ? 's' : ''} require{criticalCount === 1 ? 's' : ''} attention
234+
</p>
235+
</div>
236+
</motion.div>
237+
)}
238+
205239
{/* Stats Cards */}
206240
<StatCardGrid columns={4}>
207241
<AnimatedStatCard
@@ -219,6 +253,7 @@ export default function UpdatesPage() {
219253
color={criticalCount > 0 ? 'warning' : 'neutral'}
220254
delay={0.1}
221255
loading={isLoadingUpdates}
256+
description={oldestCriticalAge || undefined}
222257
/>
223258
<AnimatedStatCard
224259
title="Auto-Update Enabled"
@@ -227,6 +262,7 @@ export default function UpdatesPage() {
227262
color="success"
228263
delay={0.2}
229264
loading={isLoadingUpdates}
265+
description={availableCount > 0 ? `${autoUpdateCount} of ${availableCount}` : undefined}
230266
/>
231267
<AnimatedStatCard
232268
title="Updated (7 days)"
@@ -255,11 +291,14 @@ export default function UpdatesPage() {
255291
Update History
256292
</TabsTrigger>
257293
</TabsList>
294+
</div>
258295

259-
{activeTab === 'available' && (
260-
<div className="flex items-center gap-3 w-full sm:w-auto">
261-
{/* Search */}
262-
<div className="relative flex-1 sm:w-64">
296+
{/* Available Updates Tab */}
297+
<TabsContent value="available" className="mt-0">
298+
{/* Search & Filter Bar */}
299+
{activeTab === 'available' && !mspTenantSelectionRequired && (
300+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 mb-4">
301+
<div className="relative flex-1 w-full">
263302
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted" />
264303
<Input
265304
placeholder="Search updates..."
@@ -269,26 +308,30 @@ export default function UpdatesPage() {
269308
/>
270309
</div>
271310

272-
{/* Critical Filter */}
273-
<Button
274-
variant={showCriticalOnly ? 'default' : 'ghost'}
275-
size="sm"
276-
onClick={() => setShowCriticalOnly(!showCriticalOnly)}
277-
className={cn(
278-
showCriticalOnly
279-
? 'bg-status-warning/20 text-status-warning hover:bg-status-warning/30'
280-
: 'text-text-secondary hover:text-text-primary'
311+
<div className="flex items-center gap-3">
312+
<Button
313+
variant={showCriticalOnly ? 'default' : 'ghost'}
314+
size="sm"
315+
onClick={() => setShowCriticalOnly(!showCriticalOnly)}
316+
className={cn(
317+
showCriticalOnly
318+
? 'bg-status-warning/20 text-status-warning hover:bg-status-warning/30'
319+
: 'text-text-secondary hover:text-text-primary'
320+
)}
321+
>
322+
<AlertTriangle className="w-4 h-4 mr-1.5" />
323+
Critical
324+
</Button>
325+
326+
{!isLoadingUpdates && updates.length > 0 && (
327+
<span className="text-xs text-text-muted whitespace-nowrap">
328+
Showing {filteredUpdates.length} of {updates.length}
329+
</span>
281330
)}
282-
>
283-
<AlertTriangle className="w-4 h-4 mr-1.5" />
284-
Critical
285-
</Button>
331+
</div>
286332
</div>
287333
)}
288-
</div>
289334

290-
{/* Available Updates Tab */}
291-
<TabsContent value="available" className="mt-0">
292335
{mspTenantSelectionRequired ? (
293336
<AnimatedEmptyState
294337
icon={Package}
@@ -298,7 +341,7 @@ export default function UpdatesPage() {
298341
showOrbs={false}
299342
/>
300343
) : isLoadingUpdates ? (
301-
<UpdateCardSkeleton count={5} />
344+
<UpdateCardSkeleton count={6} />
302345
) : updatesError ? (
303346
<AnimatedEmptyState
304347
icon={XCircle}
@@ -315,15 +358,16 @@ export default function UpdatesPage() {
315358
<motion.div
316359
initial={{ opacity: 0 }}
317360
animate={{ opacity: 1 }}
318-
className="space-y-4"
361+
className="grid grid-cols-1 md:grid-cols-2 gap-4"
319362
>
320-
{filteredUpdates.map((update) => (
363+
{filteredUpdates.map((update, index) => (
321364
<UpdateCard
322365
key={update.id}
323366
update={update}
324367
onTriggerUpdate={handleTriggerUpdate}
325368
onPolicyChange={handlePolicyChange}
326369
isUpdating={updatingIds.has(update.id)}
370+
index={index}
327371
/>
328372
))}
329373
</motion.div>

0 commit comments

Comments
 (0)