Панель расхода с фильтрами
Сделай панель расхода по проектам. Сверху чипы-фильтры по статусу, которые можно снимать. Под ними на каждый проект строка с названием, круговым индикатором заполненности квоты в процентах и меткой статуса. Список постраничный, по пять строк. Если после фильтрации ничего не осталось, вместо списка показывается пустое состояние с кнопкой сброса фильтров.
usage-dashboard__docs__claude-code__claude-opus-5__1 · 23.09.2026, 20:20:27 мск
Первый вызов модели получил 47 387 токенов входа, включая системный промпт Claude Code. Контекст до задачи — оценка по длине текста, для русского текста она занижает число токенов в полтора-два раза.
Компилятор
Ошибок нет: все компоненты и пропсы существуют.
Линтер и покрытие
- inline-styleСтроки 72, 73, 103, 117, 127, 128Инлайновые стили вместо классов и токенов библиотеки
JxBadge, JxButton, JxChip, JxEmptyState, JxPagination, JxProgressCircle
Ожидались JxChip, JxProgressCircle, JxBadge, JxPagination, JxEmptyState · покрытие 100%
Доки (llms-full.txt, ~22548)
Вход 2 · выход 3 346 · из кэша 8 233 · в кэш 39 152 · 38 с
Рендер
Код компонента
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
import { useMemo, useState } from 'react';
import {
JxBadge,
JxButton,
JxChip,
JxEmptyState,
JxPagination,
JxProgressCircle,
} from '@jinx-ui/react';
type ProjectStatus = 'active' | 'paused' | 'over';
interface Project {
id: string;
name: string;
used: number;
quota: number;
status: ProjectStatus;
}
const STATUS_META: Record<ProjectStatus, { label: string; tone: 'success' | 'warning' | 'danger' }> = {
active: { label: 'Активен', tone: 'success' },
paused: { label: 'На паузе', tone: 'warning' },
over: { label: 'Превышен', tone: 'danger' },
};
const ALL_STATUSES: ProjectStatus[] = ['active', 'paused', 'over'];
const PROJECTS: Project[] = [
{ id: 'p1', name: 'Витрина каталога', used: 420, quota: 1000, status: 'active' },
{ id: 'p2', name: 'Платёжный шлюз', used: 980, quota: 1000, status: 'over' },
{ id: 'p3', name: 'Мобильный клиент', used: 310, quota: 800, status: 'active' },
{ id: 'p4', name: 'Аналитика событий', used: 640, quota: 900, status: 'paused' },
{ id: 'p5', name: 'Поиск по документам', used: 120, quota: 500, status: 'active' },
{ id: 'p6', name: 'Импорт из CRM', used: 700, quota: 700, status: 'over' },
{ id: 'p7', name: 'Уведомления', used: 210, quota: 600, status: 'paused' },
{ id: 'p8', name: 'Личный кабинет', used: 455, quota: 1000, status: 'active' },
{ id: 'p9', name: 'Отчёты для бухгалтерии', used: 890, quota: 1200, status: 'active' },
{ id: 'p10', name: 'Онбординг', used: 150, quota: 400, status: 'paused' },
{ id: 'p11', name: 'Партнёрский API', used: 1100, quota: 1000, status: 'over' },
{ id: 'p12', name: 'Внутренние инструменты', used: 260, quota: 900, status: 'active' },
];
const PAGE_SIZE = 5;
export default function ProjectQuotaPanel() {
const [filters, setFilters] = useState<ProjectStatus[]>(ALL_STATUSES);
const [page, setPage] = useState(1);
const visible = useMemo(
() => PROJECTS.filter((project) => filters.includes(project.status)),
[filters],
);
const pageCount = Math.max(1, Math.ceil(visible.length / PAGE_SIZE));
const currentPage = Math.min(page, pageCount);
const rows = visible.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
const toggleStatus = (status: ProjectStatus) => {
setFilters((prev) =>
prev.includes(status) ? prev.filter((item) => item !== status) : [...prev, status],
);
setPage(1);
};
const resetFilters = () => {
setFilters(ALL_STATUSES);
setPage(1);
};
return (
<section style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{ALL_STATUSES.map((status) => {
const selected = filters.includes(status);
return (
<JxChip
key={status}
active={selected}
removable={selected}
onClick={() => toggleStatus(status)}
onRemove={() => toggleStatus(status)}
>
{STATUS_META[status].label}
</JxChip>
);
})}
</div>
{rows.length === 0 ? (
<JxEmptyState
title="Проектов не найдено"
message="Ни один проект не подходит под выбранные фильтры статуса."
action={
<JxButton variant="primary" onClick={resetFilters}>
Сбросить фильтры
</JxButton>
}
/>
) : (
<>
<ul
style={{
listStyle: 'none',
margin: 0,
padding: 0,
display: 'flex',
flexDirection: 'column',
gap: '8px',
}}
>
{rows.map((project) => {
const meta = STATUS_META[project.status];
return (
<li
key={project.id}
style={{
display: 'flex',
alignItems: 'center',
gap: '16px',
padding: '12px 16px',
background: 'var(--jx-surface)',
border: '1px solid var(--jx-border)',
borderRadius: 'var(--jx-r)',
}}
>
<span style={{ flex: 1, minWidth: 0, color: 'var(--jx-text)' }}>{project.name}</span>
<span style={{ color: 'var(--jx-text-3)', fontFamily: 'var(--jx-font-mono)' }}>
{project.used} / {project.quota}
</span>
<JxProgressCircle value={project.used} max={project.quota} showValue />
<JxBadge tone={meta.tone} dot>
{meta.label}
</JxBadge>
</li>
);
})}
</ul>
<JxPagination total={pageCount} page={currentPage} onPageChange={setPage} />
</>
)}
</section>
);
}