Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6008cd763a |
@@ -19,8 +19,6 @@ import {
|
||||
import { Switch } from '@cfdm/ui/components/switch'
|
||||
import { FieldGroup } from '@cfdm/ui/components/field'
|
||||
import { ToggleGroup, ToggleGroupItem } from '@cfdm/ui/components/toggle-group'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/reui/alert'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import {
|
||||
HealthAggregateTiles,
|
||||
@@ -123,9 +121,6 @@ export function HealthCheckConfigFields({
|
||||
const aggregate = value.aggregate ?? 'majority'
|
||||
const isHttp = value.type === 'http'
|
||||
const rowClass = 'gap-3 px-0 py-3'
|
||||
const hasCloudflare = providers.includes('cloudflare')
|
||||
const hasGlobalping = providers.includes('globalping')
|
||||
const hasLocal = providers.includes('local')
|
||||
|
||||
return (
|
||||
<FieldGroup className={cn('gap-0', className)}>
|
||||
@@ -163,6 +158,7 @@ export function HealthCheckConfigFields({
|
||||
compact
|
||||
stacked
|
||||
className={rowClass}
|
||||
contentClassName="min-w-0"
|
||||
>
|
||||
<HealthSourceTiles
|
||||
value={providers}
|
||||
@@ -176,46 +172,6 @@ export function HealthCheckConfigFields({
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{hasCloudflare ? (
|
||||
<Alert>
|
||||
<AlertTitle>Cloudflare Worker</AlertTitle>
|
||||
<AlertDescription>
|
||||
Проба с edge Cloudflare, не продукт Health Checks API (на Free его нет).
|
||||
Worker создаётся автоматически и сам опрашивает IP (KV mailbox).
|
||||
Статус деплоя — в{' '}
|
||||
<Link to="/settings/health" className="text-foreground underline">
|
||||
Настройках → Health-check
|
||||
</Link>
|
||||
. Если Worker не создан, этот источник не пробируется как Local.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{hasGlobalping ? (
|
||||
<Alert>
|
||||
<AlertTitle>Globalping</AlertTitle>
|
||||
<AlertDescription>
|
||||
Пробы из сети globalping.io (TCP ping / HTTP). Токен, локации и лимит —
|
||||
в{' '}
|
||||
<Link to="/settings/health" className="text-foreground underline">
|
||||
Настройках → Health-check
|
||||
</Link>
|
||||
. Без токена или при 429 этот источник = fail, без fallback на Local.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
{hasLocal ? (
|
||||
<Alert>
|
||||
<AlertTitle>Local health-check</AlertTitle>
|
||||
<AlertDescription>
|
||||
Проба TCP/HTTP с сервера API. Cron и пороги Slow/Down — в{' '}
|
||||
<Link to="/settings/health" className="text-foreground underline">
|
||||
Настройках → Health-check
|
||||
</Link>
|
||||
. Интервал в карточке не используется.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{providers.length > 1 ? (
|
||||
<SettingRow
|
||||
title="Агрегация"
|
||||
@@ -223,6 +179,7 @@ export function HealthCheckConfigFields({
|
||||
compact
|
||||
stacked
|
||||
className={rowClass}
|
||||
contentClassName="min-w-0"
|
||||
>
|
||||
<HealthAggregateTiles
|
||||
value={aggregate}
|
||||
@@ -261,7 +218,10 @@ export function HealthCheckConfigFields({
|
||||
<ToggleGroup
|
||||
id={`${idPrefix}-type`}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
spacing={0}
|
||||
multiple={false}
|
||||
className="w-full min-w-0"
|
||||
value={[value.type]}
|
||||
onValueChange={(next) => {
|
||||
const picked = next[0]
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
import type { KeyboardEvent, ReactNode } from 'react'
|
||||
import { CheckIcon, GlobeIcon, ServerIcon, CloudIcon, LayersIcon, ShieldAlertIcon, ScaleIcon } from 'lucide-react'
|
||||
import { CheckIcon, ServerIcon, LayersIcon, ShieldAlertIcon, ScaleIcon } from 'lucide-react'
|
||||
|
||||
import { Frame, FramePanel } from '@/components/reui/frame'
|
||||
import { IconTile } from '@/components/reui/icon-tile'
|
||||
import { cn } from '@cfdm/ui/lib/utils'
|
||||
import type { HealthCheckAggregate, HealthCheckProvider } from '@cfdm/shared'
|
||||
import { kpiCols } from './kpi-cols'
|
||||
|
||||
export type HealthProvider = HealthCheckProvider
|
||||
export type HealthAggregate = HealthCheckAggregate
|
||||
|
||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||
|
||||
/** Official Cloudflare mark (Simple Icons). */
|
||||
function CloudflareMark() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.5088 16.8447c.1475-.5068.0908-.9707-.1553-1.3154-.2246-.3164-.6045-.499-1.0615-.5205l-8.6592-.1123a.1559.1559 0 0 1-.1333-.0713c-.0283-.042-.0351-.0986-.021-.1553.0278-.084.1123-.1484.2036-.1562l8.7359-.1123c1.0351-.0489 2.1601-.8868 2.5537-1.9136l.499-1.3013c.0215-.0561.0293-.1128.0147-.168-.5625-2.5463-2.835-4.4453-5.5499-4.4453-2.5039 0-4.6284 1.6177-5.3876 3.8614-.4927-.3658-1.1187-.5625-1.794-.499-1.2026.119-2.1665 1.083-2.2861 2.2856-.0283.31-.0069.6128.0635.894C1.5683 13.171 0 14.7754 0 16.752c0 .1748.0142.3515.0352.5273.0141.083.0844.1475.1689.1475h15.9814c.0909 0 .1758-.0645.2032-.1553l.12-.4268zm2.7568-5.5634c-.0771 0-.1611 0-.2383.0112-.0566 0-.1054.0415-.127.0976l-.3378 1.1744c-.1475.5068-.0918.9707.1543 1.3164.2256.3164.6055.498 1.0625.5195l1.8437.1133c.0557 0 .1055.0263.1329.0703.0283.043.0351.1074.0214.1562-.0283.084-.1132.1485-.204.1553l-1.921.1123c-1.041.0488-2.1582.8867-2.5527 1.914l-.1406.3585c-.0283.0713.0215.1416.0986.1416h6.5977c.0771 0 .1474-.0489.169-.126.1122-.4082.1757-.837.1757-1.2803 0-2.6025-2.125-4.727-4.7344-4.727"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
/** Official Globalping mark (globalping.io favicon). */
|
||||
function GlobalpingMark() {
|
||||
return (
|
||||
<svg viewBox="0 0 26 26" fill="none" aria-hidden="true">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10.354 6.081a2.636 2.636 0 0 1 4.32.257 31.5 31.5 0 0 1 8.55-1.349A12.97 12.97 0 0 0 3.247 4.42a33.6 33.6 0 0 1 7.107 1.661m13.647.003-.764.02a30.6 30.6 0 0 0-8.235 1.3q.005.08 0 .16c0 .6-.222 1.178-.624 1.624a42 42 0 0 1 2.382 3.695q.478.835.884 1.645h.134c.348-.003.689.102.975.302q3.286-2.798 5.83-7.517l.052-.094m-11.16 2.616a2.65 2.65 0 0 1-2.997-.644c-3.276 1.658-6.702 4.284-9.022 8.45 1.579.6 3.232.982 4.914 1.135a1.566 1.566 0 0 1 2.811.026h.14a16 16 0 0 0 6.935-2.188 1.5 1.5 0 0 1-.097-.52 1.53 1.53 0 0 1 .475-1.105 39 39 0 0 0-3.16-5.154Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.866 24.463c.28-2.216-.22-4.582-1.06-6.825h-.019a1.66 1.66 0 0 1-.812-.211 17 17 0 0 1-7.622 2.467h-.14a1.563 1.563 0 0 1-2.902-.026 19 19 0 0 1-4.94-1.086 13 13 0 0 0 17.479 5.801zm6.324-15.97a26.5 26.5 0 0 1-5.82 7.218 1.4 1.4 0 0 1 .049.374 1.55 1.55 0 0 1-.56 1.18c.907 2.417 1.3 4.644 1.174 6.659a12.98 12.98 0 0 0 5.158-15.431Zm-15.258-.28a2.35 2.35 0 0 1-.045-1.108 33 33 0 0 0-7.46-1.658 12.97 12.97 0 0 0-1.829 11.45c2.106-3.682 5.268-6.627 9.334-8.684"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const PROVIDER_ITEMS: Array<{
|
||||
id: HealthProvider
|
||||
title: string
|
||||
@@ -29,14 +58,14 @@ const PROVIDER_ITEMS: Array<{
|
||||
id: 'cloudflare',
|
||||
title: 'Cloudflare',
|
||||
description: 'Worker на edge, KV mailbox',
|
||||
icon: <CloudIcon />,
|
||||
icon: <CloudflareMark />,
|
||||
iconClassName: 'text-warning [&_svg]:text-current',
|
||||
},
|
||||
{
|
||||
id: 'globalping',
|
||||
title: 'Globalping',
|
||||
description: 'Пробы из сети globalping.io',
|
||||
icon: <GlobeIcon />,
|
||||
icon: <GlobalpingMark />,
|
||||
iconClassName: 'text-success [&_svg]:text-current',
|
||||
},
|
||||
]
|
||||
@@ -98,38 +127,48 @@ function TilePanel({
|
||||
aria-pressed={selected}
|
||||
tabIndex={0}
|
||||
className={cn(
|
||||
'relative isolate flex h-full cursor-pointer flex-col p-3 transition-colors',
|
||||
'relative isolate flex min-w-0 cursor-pointer flex-col p-3 transition-colors',
|
||||
'hover:bg-muted/40 focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none',
|
||||
selected && 'ring-ring ring-1',
|
||||
)}
|
||||
onClick={onActivate}
|
||||
onKeyDown={(event) => handleTileKeyDown(onActivate, event)}
|
||||
>
|
||||
<div className="relative z-10 flex h-full items-start gap-3">
|
||||
<div className="relative z-10 flex min-w-0 items-start gap-3">
|
||||
<IconTile
|
||||
variant="elevated"
|
||||
aria-hidden="true"
|
||||
className={cn('size-10.5', iconClassName ?? DEFAULT_ICON_CLASS)}
|
||||
className={cn('size-10.5 shrink-0', iconClassName ?? DEFAULT_ICON_CLASS)}
|
||||
>
|
||||
{icon}
|
||||
</IconTile>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="text-foreground text-sm font-medium">{title}</span>
|
||||
<div className="flex min-w-0 items-start justify-between gap-2">
|
||||
<span className="text-foreground truncate text-sm font-medium">{title}</span>
|
||||
{selected ? (
|
||||
<CheckIcon className="text-foreground size-4 shrink-0" aria-hidden />
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs leading-relaxed">{description}</p>
|
||||
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FramePanel>
|
||||
)
|
||||
}
|
||||
|
||||
function TileGrid({ children, count }: { children: ReactNode; count: number }) {
|
||||
return (
|
||||
<Frame dense spacing="sm" className="@container w-full min-w-0 overflow-hidden">
|
||||
<div className={cn('grid min-w-0 gap-2', kpiCols(count))}>{children}</div>
|
||||
</Frame>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Мультивыбор источников проб (Local / Cloudflare / Globalping).
|
||||
* Preview: https://reui.io/preview/base/card-12
|
||||
* Preview: https://reui.io/preview/base/card-12 · https://reui.io/preview/base/stats-12
|
||||
* Docs: https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/icon-tile
|
||||
*/
|
||||
export function HealthSourceTiles({
|
||||
@@ -151,22 +190,20 @@ export function HealthSourceTiles({
|
||||
}
|
||||
|
||||
return (
|
||||
<Frame dense spacing="sm" className="@container w-full">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
{PROVIDER_ITEMS.map((item) => (
|
||||
<TilePanel
|
||||
key={item.id}
|
||||
selected={selected.includes(item.id)}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
iconClassName={item.iconClassName}
|
||||
role="checkbox"
|
||||
onActivate={() => toggle(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Frame>
|
||||
<TileGrid count={PROVIDER_ITEMS.length}>
|
||||
{PROVIDER_ITEMS.map((item) => (
|
||||
<TilePanel
|
||||
key={item.id}
|
||||
selected={selected.includes(item.id)}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
iconClassName={item.iconClassName}
|
||||
role="checkbox"
|
||||
onActivate={() => toggle(item.id)}
|
||||
/>
|
||||
))}
|
||||
</TileGrid>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,20 +220,18 @@ export function HealthAggregateTiles({
|
||||
}) {
|
||||
const selected = value || 'majority'
|
||||
return (
|
||||
<Frame dense spacing="sm" className="@container w-full">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
||||
{AGGREGATE_ITEMS.map((item) => (
|
||||
<TilePanel
|
||||
key={item.id}
|
||||
selected={selected === item.id}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
role="radio"
|
||||
onActivate={() => onChange(item.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Frame>
|
||||
<TileGrid count={AGGREGATE_ITEMS.length}>
|
||||
{AGGREGATE_ITEMS.map((item) => (
|
||||
<TilePanel
|
||||
key={item.id}
|
||||
selected={selected === item.id}
|
||||
title={item.title}
|
||||
description={item.description}
|
||||
icon={item.icon}
|
||||
role="radio"
|
||||
onActivate={() => onChange(item.id)}
|
||||
/>
|
||||
))}
|
||||
</TileGrid>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ export function SettingRow({
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex w-full justify-start',
|
||||
'flex w-full min-w-0 justify-start',
|
||||
stacked ? 'justify-start' : '@md/field-group:justify-end',
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user