Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 2m13s
Docker images / frontend-image (push) Successful in 3m14s
Docker images / updater-image (push) Successful in 47s
Docker images / backend-image (push) Successful in 2m44s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 14s
Co-authored-by: Cursor <cursoragent@cursor.com>
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
"use client"
|
|
|
|
import Link from "next/link"
|
|
import {
|
|
Timeline,
|
|
TimelineContent,
|
|
TimelineDate,
|
|
TimelineHeader,
|
|
TimelineIndicator,
|
|
TimelineItem,
|
|
TimelineSeparator,
|
|
TimelineTitle,
|
|
} from "@/components/reui/timeline"
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/reui/alert"
|
|
import { cn } from "@/lib/utils"
|
|
import type { EventItem } from "@mmapp/contracts/events"
|
|
|
|
function formatEventAge(iso: string): string {
|
|
const ts = Date.parse(iso)
|
|
if (!Number.isFinite(ts)) return "—"
|
|
const diffMs = Math.max(0, Date.now() - ts)
|
|
const minutes = Math.floor(diffMs / 60_000)
|
|
if (minutes < 1) return "сейчас"
|
|
if (minutes < 60) return `${minutes}м`
|
|
const hours = Math.floor(minutes / 60)
|
|
if (hours < 24) return `${hours}ч`
|
|
const days = Math.floor(hours / 24)
|
|
return `${days}д`
|
|
}
|
|
|
|
const LEVEL_DOT: Record<EventItem["level"], string> = {
|
|
critical: "border-destructive bg-destructive/20 group-data-completed/timeline-item:border-destructive",
|
|
warning: "border-warning bg-warning/20 group-data-completed/timeline-item:border-warning",
|
|
info: "border-info bg-info/20 group-data-completed/timeline-item:border-info",
|
|
}
|
|
|
|
/**
|
|
* Compact activity timeline.
|
|
* Preview: https://reui.io/preview/base/timeline-3
|
|
* Docs: https://reui.io/docs/components/base/timeline
|
|
*/
|
|
export function DashboardEventsTimeline({
|
|
events,
|
|
loading,
|
|
error,
|
|
}: {
|
|
events: EventItem[]
|
|
loading?: boolean
|
|
error?: string | null
|
|
}) {
|
|
if (loading && events.length === 0) {
|
|
return <p className="text-muted-foreground px-5 py-6 text-sm">Загрузка событий…</p>
|
|
}
|
|
if (error && events.length === 0) {
|
|
return (
|
|
<div className="px-5 py-4">
|
|
<Alert variant="destructive">
|
|
<AlertTitle>События недоступны</AlertTitle>
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
</div>
|
|
)
|
|
}
|
|
if (events.length === 0) {
|
|
return (
|
|
<p className="text-muted-foreground px-5 py-6 text-sm">
|
|
Событий пока нет.{" "}
|
|
<Link href="/alerts" className="underline underline-offset-2">
|
|
Оповещения
|
|
</Link>
|
|
</p>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Timeline defaultValue={events.length} className="px-5 py-4">
|
|
{events.map((event, index) => (
|
|
<TimelineItem key={event.id} step={index + 1}>
|
|
<TimelineHeader>
|
|
<TimelineSeparator />
|
|
<TimelineDate>{formatEventAge(event.createdAt)}</TimelineDate>
|
|
<TimelineTitle className="text-[13px] leading-tight">{event.title}</TimelineTitle>
|
|
<TimelineIndicator className={cn(LEVEL_DOT[event.level])} />
|
|
</TimelineHeader>
|
|
<TimelineContent className="text-xs leading-snug">{event.message}</TimelineContent>
|
|
</TimelineItem>
|
|
))}
|
|
</Timeline>
|
|
)
|
|
}
|