feat(health): объединить источники, график и таймлайн в один блок мониторинга
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Failing after 49s
CD / quality (push) Failing after 1m1s
CD / publish (push) Skipped
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 9s
quality / api (push) Skipped
quality / docker-check (push) Skipped
quality / web (push) Failing after 49s
CD / quality (push) Failing after 1m1s
CD / publish (push) Skipped
Переключатель серий в шапке Frame меняет график uptime и смены статуса без отдельного списка плиток. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
export { UptimeChart, type UptimeProbe, type UptimePeriodKey } from './uptime-chart'
|
export { UptimeChart, type UptimeProbe, type UptimePeriodKey, probeUptimePercent, lastProbeLatency } from './uptime-chart'
|
||||||
export { ServiceHealthMonitor } from './service-health-monitor'
|
export { ServiceHealthMonitor } from './service-health-monitor'
|
||||||
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
|
export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils'
|
||||||
export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics'
|
export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics'
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState, type ReactNode } from 'react'
|
||||||
|
import { LayersIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { HealthTimeline } from '@/components/health/health-timeline'
|
import { HealthTimeline } from '@/components/health/health-timeline'
|
||||||
|
import { HealthCheckBadge } from '@/components/health-check-badge'
|
||||||
import {
|
import {
|
||||||
Frame,
|
Frame,
|
||||||
FrameDescription,
|
FrameDescription,
|
||||||
@@ -8,41 +10,145 @@ import {
|
|||||||
FramePanel,
|
FramePanel,
|
||||||
FrameTitle,
|
FrameTitle,
|
||||||
} from '@/components/reui/frame'
|
} from '@/components/reui/frame'
|
||||||
import { UptimeChart, UPTIME_PERIODS, type UptimePeriodKey } from '@/components/reui-kit/uptime-chart'
|
import { IconTile } from '@/components/reui/icon-tile'
|
||||||
|
import { Badge } from '@/components/reui/badge'
|
||||||
|
import {
|
||||||
|
lastProbeLatency,
|
||||||
|
probeUptimePercent,
|
||||||
|
UptimeChart,
|
||||||
|
UPTIME_PERIODS,
|
||||||
|
type UptimePeriodKey,
|
||||||
|
} from '@/components/reui-kit/uptime-chart'
|
||||||
|
import { HEALTH_PROVIDER_ITEMS } from '@/components/reui-kit/health-source-tiles'
|
||||||
import {
|
import {
|
||||||
collapseStatusChanges,
|
collapseStatusChanges,
|
||||||
filterByPeriod,
|
filterByPeriod,
|
||||||
filterByProviders,
|
filterByProviders,
|
||||||
type HealthLogProbe,
|
type HealthLogProbe,
|
||||||
|
type HealthLogStatus,
|
||||||
} from '@/lib/health-log'
|
} from '@/lib/health-log'
|
||||||
|
import { cn } from '@cfdm/ui/lib/utils'
|
||||||
import type { HealthCheckProvider } from '@cfdm/shared'
|
import type { HealthCheckProvider } from '@cfdm/shared'
|
||||||
|
|
||||||
|
type SourceTab = 'all' | HealthCheckProvider
|
||||||
|
|
||||||
|
const ALL_TAB: SourceTab = 'all'
|
||||||
|
|
||||||
|
function formatUptime(value: number | null): string {
|
||||||
|
if (value == null) return '—'
|
||||||
|
return `${value.toFixed(value >= 99.95 ? 2 : 1)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatLatency(ms: number | null): string {
|
||||||
|
if (ms == null) return 'нет проб'
|
||||||
|
return `${ms} мс`
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTileClass(status: HealthLogStatus | undefined): string {
|
||||||
|
if (status === 'down') return 'text-destructive'
|
||||||
|
if (status === 'degraded') return 'text-warning'
|
||||||
|
if (status === 'up') return 'text-success'
|
||||||
|
return 'text-muted-foreground'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combined uptime chart + status-change timeline (solution-analytics-8 DNA).
|
* Единый блок мониторинга: переключатель источников (dashboard-4) + график
|
||||||
* Preview: https://reui.io/preview/base/solution-analytics-8 · https://reui.io/preview/base/chart-17
|
* (chart-17) + таймлайн смен статуса (solution-ai-ops-1 / timeline).
|
||||||
* Docs: https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/timeline
|
*
|
||||||
|
* Preview: https://reui.io/preview/base/dashboard-4
|
||||||
|
* Preview: https://reui.io/preview/base/chart-17
|
||||||
|
* Preview: https://reui.io/preview/base/solution-ai-ops-1
|
||||||
|
* Docs: https://reui.io/docs/components/base/frame
|
||||||
|
* Docs: https://reui.io/docs/components/base/icon-tile
|
||||||
|
* Docs: https://reui.io/docs/components/base/timeline
|
||||||
|
* Docs: https://reui.io/docs/components/base/badge
|
||||||
*/
|
*/
|
||||||
export function ServiceHealthMonitor({
|
export function ServiceHealthMonitor({
|
||||||
items,
|
items,
|
||||||
selectedProviders,
|
enabledProviders,
|
||||||
|
statuses,
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: {
|
}: {
|
||||||
items: HealthLogProbe[]
|
items: HealthLogProbe[]
|
||||||
selectedProviders: readonly HealthCheckProvider[]
|
enabledProviders: readonly HealthCheckProvider[]
|
||||||
|
statuses: Partial<Record<HealthCheckProvider, HealthLogStatus>>
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [period, setPeriod] = useState<UptimePeriodKey>('5D')
|
const [period, setPeriod] = useState<UptimePeriodKey>('5D')
|
||||||
|
const [source, setSource] = useState<SourceTab>(ALL_TAB)
|
||||||
const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5
|
const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5
|
||||||
|
|
||||||
|
const enabledItems = useMemo(
|
||||||
|
() => HEALTH_PROVIDER_ITEMS.filter((item) => enabledProviders.includes(item.id)),
|
||||||
|
[enabledProviders],
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedProviders = useMemo<HealthCheckProvider[]>(() => {
|
||||||
|
if (source === ALL_TAB) return [...enabledProviders]
|
||||||
|
if (enabledProviders.includes(source)) return [source]
|
||||||
|
return [...enabledProviders]
|
||||||
|
}, [enabledProviders, source])
|
||||||
|
|
||||||
|
const periodItems = useMemo(
|
||||||
|
() => filterByPeriod(items, days),
|
||||||
|
[items, days],
|
||||||
|
)
|
||||||
|
|
||||||
const filtered = useMemo(
|
const filtered = useMemo(
|
||||||
() => filterByProviders(filterByPeriod(items, days), selectedProviders),
|
() => filterByProviders(periodItems, selectedProviders),
|
||||||
[items, days, selectedProviders],
|
[periodItems, selectedProviders],
|
||||||
)
|
)
|
||||||
|
|
||||||
const changes = useMemo(() => collapseStatusChanges(filtered), [filtered])
|
const changes = useMemo(() => collapseStatusChanges(filtered), [filtered])
|
||||||
|
|
||||||
|
const showAllTab = enabledItems.length > 1
|
||||||
|
const tabCount = enabledItems.length + (showAllTab ? 1 : 0)
|
||||||
|
const activeSource: SourceTab =
|
||||||
|
source === ALL_TAB || enabledProviders.includes(source)
|
||||||
|
? source
|
||||||
|
: ALL_TAB
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame stacked spacing="sm" className="min-w-0 w-full">
|
<Frame stacked spacing="sm" className="min-w-0 w-full">
|
||||||
|
<FrameHeader className="p-0!">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid',
|
||||||
|
tabCount <= 2 && 'grid-cols-2',
|
||||||
|
tabCount === 3 && 'grid-cols-1 sm:grid-cols-3',
|
||||||
|
tabCount >= 4 && 'grid-cols-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{showAllTab ? (
|
||||||
|
<SourceMetricButton
|
||||||
|
selected={activeSource === ALL_TAB}
|
||||||
|
icon={<LayersIcon />}
|
||||||
|
iconClassName="text-muted-foreground"
|
||||||
|
label="Все источники"
|
||||||
|
value={formatUptime(probeUptimePercent(periodItems))}
|
||||||
|
hint={`${periodItems.length} проб`}
|
||||||
|
onSelect={() => setSource(ALL_TAB)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{enabledItems.map((item) => {
|
||||||
|
const series = filterByProviders(periodItems, [item.id])
|
||||||
|
return (
|
||||||
|
<SourceMetricButton
|
||||||
|
key={item.id}
|
||||||
|
selected={activeSource === item.id}
|
||||||
|
icon={item.icon}
|
||||||
|
iconClassName={item.iconClassName}
|
||||||
|
label={item.title}
|
||||||
|
value={formatUptime(probeUptimePercent(series))}
|
||||||
|
hint={formatLatency(lastProbeLatency(series))}
|
||||||
|
status={statuses[item.id] ?? 'unknown'}
|
||||||
|
onSelect={() => setSource(item.id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FrameHeader>
|
||||||
|
|
||||||
<UptimeChart
|
<UptimeChart
|
||||||
items={filtered}
|
items={filtered}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -50,12 +156,15 @@ export function ServiceHealthMonitor({
|
|||||||
onPeriodChange={setPeriod}
|
onPeriodChange={setPeriod}
|
||||||
skipPeriodFilter
|
skipPeriodFilter
|
||||||
embedded
|
embedded
|
||||||
|
hideHeader
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FramePanel className="flex flex-col gap-3">
|
<FramePanel className="flex flex-col gap-3">
|
||||||
<FrameHeader className="px-0 py-0">
|
<FrameHeader className="px-0 py-0">
|
||||||
<FrameTitle>Смены статуса</FrameTitle>
|
<FrameTitle>Смены статуса</FrameTitle>
|
||||||
<FrameDescription>
|
<FrameDescription>
|
||||||
Только переходы up / degraded / down · Cloudflare = Worker, не Health Checks API
|
Только переходы up / degraded / down · Cloudflare = Worker, не Health
|
||||||
|
Checks API
|
||||||
</FrameDescription>
|
</FrameDescription>
|
||||||
</FrameHeader>
|
</FrameHeader>
|
||||||
<HealthTimeline
|
<HealthTimeline
|
||||||
@@ -77,3 +186,65 @@ export function ServiceHealthMonitor({
|
|||||||
</Frame>
|
</Frame>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SourceMetricButton({
|
||||||
|
selected,
|
||||||
|
icon,
|
||||||
|
iconClassName,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
hint,
|
||||||
|
status,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
selected: boolean
|
||||||
|
icon: ReactNode
|
||||||
|
iconClassName: string
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
hint: string
|
||||||
|
status?: HealthLogStatus
|
||||||
|
onSelect: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-pressed={selected}
|
||||||
|
aria-label={`${label}: ${value}`}
|
||||||
|
onClick={onSelect}
|
||||||
|
className={cn(
|
||||||
|
'focus-visible:ring-ring/50 hover:bg-muted/40 relative flex min-w-0 items-start gap-3 border-e border-b p-4 text-start transition-colors last:border-e-0 focus-visible:ring-2 focus-visible:outline-none sm:border-b-0',
|
||||||
|
selected && 'bg-muted/40',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<IconTile
|
||||||
|
variant="elevated"
|
||||||
|
className={cn(
|
||||||
|
'size-10.5',
|
||||||
|
status ? statusTileClass(status) : iconClassName,
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</IconTile>
|
||||||
|
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
|
<span className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-muted-foreground text-sm font-medium">{label}</span>
|
||||||
|
{status ? (
|
||||||
|
<HealthCheckBadge status={status} size="xs" />
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline" size="sm">
|
||||||
|
{hint}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
{status ? (
|
||||||
|
<span className="text-muted-foreground text-xs">{hint}</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,6 +112,23 @@ function UptimeDelta({ delta }: { delta: number }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function probeUptimePercent(items: UptimeProbe[]): number | null {
|
||||||
|
return uptimePercent(toSeries(items))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lastProbeLatency(items: UptimeProbe[]): number | null {
|
||||||
|
if (items.length === 0) return null
|
||||||
|
const latest = [...items].sort(
|
||||||
|
(a, b) => probeTime(b.checked_at) - probeTime(a.checked_at),
|
||||||
|
)[0]
|
||||||
|
return latest?.latency_ms ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUptime(value: number | null): string {
|
||||||
|
if (value == null) return '—'
|
||||||
|
return `${value.toFixed(value >= 99.95 ? 2 : 1)}%`
|
||||||
|
}
|
||||||
|
|
||||||
interface UptimeChartProps {
|
interface UptimeChartProps {
|
||||||
items: UptimeProbe[]
|
items: UptimeProbe[]
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
@@ -119,6 +136,8 @@ interface UptimeChartProps {
|
|||||||
onPeriodChange?: (period: UptimePeriodKey) => void
|
onPeriodChange?: (period: UptimePeriodKey) => void
|
||||||
skipPeriodFilter?: boolean
|
skipPeriodFilter?: boolean
|
||||||
embedded?: boolean
|
embedded?: boolean
|
||||||
|
/** dashboard-4: chrome живёт в родительском FrameHeader (переключатель серий). */
|
||||||
|
hideHeader?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UptimeChart({
|
export function UptimeChart({
|
||||||
@@ -128,6 +147,7 @@ export function UptimeChart({
|
|||||||
onPeriodChange,
|
onPeriodChange,
|
||||||
skipPeriodFilter = false,
|
skipPeriodFilter = false,
|
||||||
embedded = false,
|
embedded = false,
|
||||||
|
hideHeader = false,
|
||||||
}: UptimeChartProps) {
|
}: UptimeChartProps) {
|
||||||
const gradientId = useId().replace(/:/g, '')
|
const gradientId = useId().replace(/:/g, '')
|
||||||
const [internalPeriod, setInternalPeriod] = useState<UptimePeriodKey>('5D')
|
const [internalPeriod, setInternalPeriod] = useState<UptimePeriodKey>('5D')
|
||||||
@@ -155,43 +175,45 @@ export function UptimeChart({
|
|||||||
|
|
||||||
const panel = (
|
const panel = (
|
||||||
<FramePanel className="flex flex-col gap-6">
|
<FramePanel className="flex flex-col gap-6">
|
||||||
<div className="border-border flex items-center justify-between gap-2 border-b border-dashed pb-4">
|
{hideHeader ? null : (
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="border-border flex items-center justify-between gap-2 border-b border-dashed pb-4">
|
||||||
<IconTile
|
<div className="flex items-center gap-2.5">
|
||||||
variant="elevated"
|
<IconTile
|
||||||
className={`size-10.5 ${tileClass}`}
|
variant="elevated"
|
||||||
aria-hidden="true"
|
className={`size-10.5 ${tileClass}`}
|
||||||
>
|
aria-hidden="true"
|
||||||
<ActivityIcon />
|
|
||||||
</IconTile>
|
|
||||||
<div className="flex flex-col justify-center">
|
|
||||||
<h3 className="text-base font-semibold">Uptime</h3>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
Пробы health-check за период
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<TooltipProvider delay={150}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger
|
|
||||||
render={
|
|
||||||
<Button
|
|
||||||
aria-label="О графике uptime"
|
|
||||||
className="text-muted-foreground/70 -mr-1"
|
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<InfoIcon data-icon="inline-start" aria-hidden="true" />
|
<ActivityIcon />
|
||||||
</TooltipTrigger>
|
</IconTile>
|
||||||
<TooltipContent side="top" sideOffset={8}>
|
<div className="flex flex-col justify-center">
|
||||||
<p>Доля успешных проб и задержка (мс) по журналу health-log.</p>
|
<h3 className="text-base font-semibold">Uptime</h3>
|
||||||
</TooltipContent>
|
<p className="text-muted-foreground text-sm">
|
||||||
</Tooltip>
|
Пробы health-check за период
|
||||||
</TooltipProvider>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<TooltipProvider delay={150}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger
|
||||||
|
render={
|
||||||
|
<Button
|
||||||
|
aria-label="О графике uptime"
|
||||||
|
className="text-muted-foreground/70 -mr-1"
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InfoIcon data-icon="inline-start" aria-hidden="true" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="top" sideOffset={8}>
|
||||||
|
<p>Доля успешных проб и задержка (мс) по журналу health-log.</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="bg-muted h-40 w-full animate-pulse rounded-xl" />
|
<div className="bg-muted h-40 w-full animate-pulse rounded-xl" />
|
||||||
@@ -207,7 +229,7 @@ export function UptimeChart({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="text-foreground text-3xl font-semibold tabular-nums">
|
<div className="text-foreground text-3xl font-semibold tabular-nums">
|
||||||
{uptime == null ? '—' : `${uptime.toFixed(uptime >= 99.95 ? 2 : 1)}%`}
|
{formatUptime(uptime)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm">
|
<div className="flex items-center gap-2 text-sm">
|
||||||
{delta == null ? (
|
{delta == null ? (
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
} from '@/components/services/service-detail-grid'
|
} from '@/components/services/service-detail-grid'
|
||||||
import { LbModeTile } from '@/components/services/service-unit-card'
|
import { LbModeTile } from '@/components/services/service-unit-card'
|
||||||
import {
|
import {
|
||||||
HealthProviderStatusTiles,
|
|
||||||
KpiStatGrid,
|
KpiStatGrid,
|
||||||
ServiceHealthMonitor,
|
ServiceHealthMonitor,
|
||||||
} from '@/components/reui-kit'
|
} from '@/components/reui-kit'
|
||||||
@@ -45,7 +44,6 @@ import {
|
|||||||
providerHealthStatuses,
|
providerHealthStatuses,
|
||||||
} from '@/lib/health-log'
|
} from '@/lib/health-log'
|
||||||
import type { ServiceView, UpdateServiceConfigInput } from '@/lib/schemas'
|
import type { ServiceView, UpdateServiceConfigInput } from '@/lib/schemas'
|
||||||
import type { HealthCheckProvider } from '@cfdm/shared'
|
|
||||||
import {
|
import {
|
||||||
createServiceNode,
|
createServiceNode,
|
||||||
deleteServiceNode,
|
deleteServiceNode,
|
||||||
@@ -112,9 +110,6 @@ function ServiceDetailPage() {
|
|||||||
|
|
||||||
const [editOpen, setEditOpen] = useState(false)
|
const [editOpen, setEditOpen] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [selectedProviders, setSelectedProviders] = useState<
|
|
||||||
HealthCheckProvider[] | null
|
|
||||||
>(null)
|
|
||||||
const [togglingIp, setTogglingIp] = useState<string | null>(null)
|
const [togglingIp, setTogglingIp] = useState<string | null>(null)
|
||||||
const [changeIp, setChangeIp] = useState<{
|
const [changeIp, setChangeIp] = useState<{
|
||||||
bindingId: number
|
bindingId: number
|
||||||
@@ -234,11 +229,6 @@ function ServiceDetailPage() {
|
|||||||
() => enabledHealthProviders(service?.domains ?? []),
|
() => enabledHealthProviders(service?.domains ?? []),
|
||||||
[service],
|
[service],
|
||||||
)
|
)
|
||||||
const activeProviders =
|
|
||||||
selectedProviders?.filter((provider) => enabledProviders.includes(provider)) ??
|
|
||||||
enabledProviders
|
|
||||||
const effectiveProviders =
|
|
||||||
activeProviders.length > 0 ? activeProviders : enabledProviders
|
|
||||||
const providerStatuses = useMemo(
|
const providerStatuses = useMemo(
|
||||||
() => providerHealthStatuses(logItems, enabledProviders),
|
() => providerHealthStatuses(logItems, enabledProviders),
|
||||||
[logItems, enabledProviders],
|
[logItems, enabledProviders],
|
||||||
@@ -333,20 +323,14 @@ function ServiceDetailPage() {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HealthProviderStatusTiles
|
|
||||||
enabled={enabledProviders}
|
|
||||||
selected={effectiveProviders}
|
|
||||||
statuses={providerStatuses}
|
|
||||||
onChange={setSelectedProviders}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
aria-label="Мониторинг"
|
aria-label="Мониторинг"
|
||||||
className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2"
|
className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2"
|
||||||
>
|
>
|
||||||
<ServiceHealthMonitor
|
<ServiceHealthMonitor
|
||||||
items={logItems}
|
items={logItems}
|
||||||
selectedProviders={effectiveProviders}
|
enabledProviders={enabledProviders}
|
||||||
|
statuses={providerStatuses}
|
||||||
isLoading={logQuery.isLoading}
|
isLoading={logQuery.isLoading}
|
||||||
/>
|
/>
|
||||||
<Frame dense spacing="sm" className="min-w-0 w-full">
|
<Frame dense spacing="sm" className="min-w-0 w-full">
|
||||||
|
|||||||
Reference in New Issue
Block a user