diff --git a/src/components/MetricWidget.tsx b/src/components/MetricWidget.tsx new file mode 100644 index 0000000..1718bd9 --- /dev/null +++ b/src/components/MetricWidget.tsx @@ -0,0 +1,17 @@ +import type { CurrentItem } from '../entities/current/types'; +import { formatNumber } from '../utils/format'; + +type MetricWidgetProps = { + item: CurrentItem; +}; + +export function MetricWidget({ item }: MetricWidgetProps) { + const title = item.name?.trim() || item.tag; + + return ( +
+ {title} + {formatNumber(item.value)} +
+ ); +} diff --git a/src/components/MetricWidgetsContainer.tsx b/src/components/MetricWidgetsContainer.tsx new file mode 100644 index 0000000..adfbe1a --- /dev/null +++ b/src/components/MetricWidgetsContainer.tsx @@ -0,0 +1,7 @@ +import type { PropsWithChildren } from 'react'; + +type MetricWidgetsContainerProps = PropsWithChildren; + +export function MetricWidgetsContainer({ children }: MetricWidgetsContainerProps) { + return
{children}
; +} diff --git a/src/entities/current/api.ts b/src/entities/current/api.ts index 6f14ac2..884b56a 100644 --- a/src/entities/current/api.ts +++ b/src/entities/current/api.ts @@ -1,4 +1,4 @@ -import { getJson } from '../../shared/api/http'; +import { buildApiUrl, getJson } from '../../shared/api/http'; import { cloudApiUrl } from '../../shared/config/env'; import type { CurrentResponse } from './types'; @@ -9,8 +9,5 @@ export function getCurrent(edge: string, tags?: string[]): Promise url.searchParams.append('tags', tag)); - return url.toString(); + return buildApiUrl(cloudApiUrl, '/current/events', { edge, tags }); } diff --git a/src/features/edge-detail/components/IndicatorsView.tsx b/src/features/edge-detail/components/IndicatorsView.tsx index 282002c..62a9b12 100644 --- a/src/features/edge-detail/components/IndicatorsView.tsx +++ b/src/features/edge-detail/components/IndicatorsView.tsx @@ -1,9 +1,7 @@ -import { useMemo, useState } from 'react'; import { Search } from 'lucide-react'; import type { CurrentItem } from '../../../entities/current/types'; -import { MetricCard } from '../../../components/MetricCard'; -import { formatNumber } from '../../../utils/format'; -import { getMetricStatus } from '../../../utils/metricStatus'; +import { MetricWidget } from '../../../components/MetricWidget'; +import { MetricWidgetsContainer } from '../../../components/MetricWidgetsContainer'; type IndicatorsViewProps = { error: unknown; @@ -26,19 +24,6 @@ export function IndicatorsView({ onSearchChange, onToggleTag, }: IndicatorsViewProps) { - const [displayMode, setDisplayMode] = useState<'overview' | 'cards'>('overview'); - const itemStatuses = useMemo(() => { - const now = Date.now(); - return items.map((item) => ({ item, statusInfo: getMetricStatus(item, now) })); - }, [items]); - const statusCounts = itemStatuses.reduce( - (counts, { statusInfo }) => { - counts[statusInfo.status] += 1; - return counts; - }, - { normal: 0, warning: 0, critical: 0 }, - ); - return (
@@ -50,22 +35,6 @@ export function IndicatorsView({

Текущие значения

-
- - -
-
- В норме - {statusCounts.normal} -
-
- Предупреждение - {statusCounts.warning} -
-
- Критично - {statusCounts.critical} -
На графике {selectedTags.length} @@ -98,35 +55,44 @@ export function IndicatorsView({ {isError ? (
Не удалось загрузить текущие значения: {String(error)}
- ) : displayMode === 'overview' ? ( -
- {itemStatuses.map(({ item, statusInfo }) => ( - - ))} -
) : ( -
- {itemStatuses.map(({ item, statusInfo }) => ( - - ))} -
+ <> + {/* displayMode === 'overview' ? ( +
+ {itemStatuses.map(({ item, statusInfo }) => ( + + ))} +
+ ) : ( +
+ {itemStatuses.map(({ item, statusInfo }) => ( + + ))} +
+ ) */} + + {items.map((item) => ( + + ))} + + )}
); diff --git a/src/shared/api/http.ts b/src/shared/api/http.ts index a2a1776..0140511 100644 --- a/src/shared/api/http.ts +++ b/src/shared/api/http.ts @@ -1,5 +1,38 @@ export type QueryParamValue = string | number | string[] | undefined; +function joinUrl(baseUrl: string, path: string): string { + const base = baseUrl.replace(/\/$/, ''); + const endpoint = path.startsWith('/') ? path : `/${path}`; + return `${base}${endpoint}`; +} + +function appendQueryParams(url: string, params?: Record): string { + const searchParams = new URLSearchParams(); + + Object.entries(params ?? {}).forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach((item) => searchParams.append(key, item)); + return; + } + + if (value !== undefined && value !== '') { + searchParams.set(key, String(value)); + } + }); + + const query = searchParams.toString(); + return query ? `${url}?${query}` : url; +} + +/** Собирает URL API из base, пути и query-параметров. */ +export function buildApiUrl( + baseUrl: string, + path: string, + params?: Record, +): string { + return appendQueryParams(joinUrl(baseUrl, path), params); +} + /** Выполняет GET-запрос и добавляет query-параметры в едином формате для всех API. */ export async function getJson( baseUrl: string, @@ -7,18 +40,7 @@ export async function getJson( params?: Record, init?: RequestInit, ): Promise { - const url = new URL(path, baseUrl); - - Object.entries(params ?? {}).forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach((item) => url.searchParams.append(key, item)); - return; - } - - if (value !== undefined && value !== '') { - url.searchParams.set(key, String(value)); - } - }); + const url = buildApiUrl(baseUrl, path, params); const response = await fetch(url, init); if (!response.ok) { diff --git a/src/styles/components/metrics.css b/src/styles/components/metrics.css index 6a0db96..1cd3504 100644 --- a/src/styles/components/metrics.css +++ b/src/styles/components/metrics.css @@ -4,6 +4,53 @@ gap: 12px; } +.metric-widgets-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 10px; +} + +.metric-widget { + min-width: 0; + height: 100%; + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid rgba(88, 103, 121, 0.42); + border-radius: 8px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 28%), + linear-gradient(145deg, rgba(6, 9, 14, 0.98), rgba(2, 4, 8, 0.96)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.05), + 0 6px 14px rgba(0, 0, 0, 0.28); +} + +.metric-widget__tag { + flex: 0 0 auto; + min-width: 0; + color: #cbd5e1; + font-size: 0.74rem; + font-weight: 600; + line-height: 1.35; + white-space: normal; + overflow-wrap: anywhere; + word-break: break-word; +} + +.metric-widget__value { + margin-top: auto; + align-self: flex-end; + min-width: 0; + color: #f8fafc; + font-size: 1.1rem; + font-weight: 700; + font-variant-numeric: tabular-nums; + line-height: 1.2; + text-align: right; +} + .metric-mosaic { display: grid; grid-template-columns: repeat(auto-fill, minmax(82px, 1fr)); diff --git a/vite.config.ts b/vite.config.ts index 92806ff..bfacc8e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,8 +8,12 @@ export default defineConfig({ }, server: { port: 5173, - }, - preview: { - port: 4173, + proxy: { + '/api': { + target: 'https://beta.backend.drill.greact.ru', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + }, + }, }, });