22
33import { useState , useCallback , useRef , useEffect } from 'react' ;
44import { useRouter } from 'next/navigation' ;
5- import { motion } from 'framer-motion' ;
5+ import { motion , useReducedMotion } from 'framer-motion' ;
66import {
77 RefreshCw ,
88 AlertTriangle ,
@@ -25,6 +25,7 @@ import {
2525 useUpdatePolicy ,
2626} from '@/hooks/use-updates' ;
2727import { useMspOptional } from '@/hooks/useMspOptional' ;
28+ import { fadeIn } from '@/lib/animations/variants' ;
2829import { cn } from '@/lib/utils' ;
2930import 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