decompose structure

This commit is contained in:
Первов Артем
2026-06-20 00:51:18 +03:00
parent dbda8ee613
commit 31add10e56
77 changed files with 4112 additions and 3974 deletions

View File

@@ -0,0 +1,133 @@
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';
type IndicatorsViewProps = {
error: unknown;
getTagLabel: (tag: string) => string;
isError: boolean;
items: CurrentItem[];
search: string;
selectedTags: string[];
onSearchChange: (value: string) => void;
onToggleTag: (tag: string) => void;
};
export function IndicatorsView({
error,
getTagLabel,
isError,
items,
search,
selectedTags,
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 (
<section className="tags-section">
<div className="section-header">
<div>
<span className="page-kicker">
<Search size={14} />
Показатели
</span>
<h2>Текущие значения</h2>
</div>
<div className="indicator-controls">
<div className="view-switch" aria-label="Режим отображения показателей">
<button
type="button"
className={displayMode === 'overview' ? 'view-switch__button--active' : ''}
onClick={() => setDisplayMode('overview')}
>
Картина
</button>
<button
type="button"
className={displayMode === 'cards' ? 'view-switch__button--active' : ''}
onClick={() => setDisplayMode('cards')}
>
Карточки
</button>
</div>
<label className="search-box">
<Search size={16} />
<input value={search} onChange={(event) => onSearchChange(event.target.value)} placeholder="Поиск показателя" />
</label>
</div>
</div>
<div className="indicator-summary">
<div>
<span>Всего</span>
<strong>{items.length}</strong>
</div>
<div>
<span>В норме</span>
<strong>{statusCounts.normal}</strong>
</div>
<div>
<span>Предупреждение</span>
<strong>{statusCounts.warning}</strong>
</div>
<div>
<span>Критично</span>
<strong>{statusCounts.critical}</strong>
</div>
<div>
<span>На графике</span>
<strong>{selectedTags.length}</strong>
</div>
</div>
{isError ? (
<div className="empty-panel">Не удалось загрузить текущие значения: {String(error)}</div>
) : displayMode === 'overview' ? (
<div className="metric-mosaic">
{itemStatuses.map(({ item, statusInfo }) => (
<button
key={`${item.edge}:${item.tag}`}
type="button"
className={`metric-tile metric-tile--${statusInfo.status} ${selectedTags.includes(item.tag) ? 'metric-tile--selected' : ''}`}
title={`${getTagLabel(item.tag)} (${item.tag}): ${formatNumber(item.value)} · ${statusInfo.label}`}
onClick={() => onToggleTag(item.tag)}
>
<span className="metric-tile__status" />
<span className="metric-tile__tag">{getTagLabel(item.tag)}</span>
<strong>{formatNumber(item.value)}</strong>
</button>
))}
</div>
) : (
<div className="metric-grid">
{itemStatuses.map(({ item, statusInfo }) => (
<MetricCard
key={`${item.edge}:${item.tag}`}
item={item}
displayName={getTagLabel(item.tag)}
statusInfo={statusInfo}
selected={selectedTags.includes(item.tag)}
onToggle={onToggleTag}
/>
))}
</div>
)}
</section>
);
}