/
sweetebin
/
hackaton_26
Обзор
Документация
Войти
/
sweetebin
/
hackaton_26
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
web-form-project/src/components/ProjectList.jsx
160 строк
5 KB
sweetebin
serve-first: экран сборки пула в дашборде (файлы/папка, живой СберТрек через фасад), старт анализа кнопкой; fbminer sbertrack в CLI; техдолг
28 июл 2026, 15:32
28 июл 2026, 15:32
8dbd2f7
Код
Авторство
О чём код?
import { useEffect, useState } from 'react'; import { Alert, Box, Button, Card, CardActionArea, CardContent, Chip, CircularProgress, Stack, Typography, } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import FolderIcon from '@mui/icons-material/Folder'; import HourglassTopIcon from '@mui/icons-material/HourglassTop'; import { listProjects } from '../api/client'; import NewProjectDialog from './NewProjectDialog'; const POLL_INTERVAL_MS = 3000; function formatDate(iso) { if (!iso) return '—'; return new Date(iso).toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', hour: '2-digit', minute: '2-digit', }); } export default function ProjectList({ onOpenProject }) { const [projects, setProjects] = useState(null); const [error, setError] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); // Первичная загрузка + фоновое обновление, пока какой-то проект анализируется useEffect(() => { let timer = null; let alive = true; const load = () => { listProjects() .then((list) => { if (!alive) return; setProjects(list); setError(null); if (list.some((p) => p.status === 'processing')) { timer = setTimeout(load, POLL_INTERVAL_MS); } }) .catch((e) => alive && setError(e.message)); }; load(); return () => { alive = false; clearTimeout(timer); }; }, []); if (error) { return ( <Alert severity="error" sx={{ maxWidth: 720, mx: 'auto' }}> Не удалось загрузить проекты: {error} </Alert> ); } if (!projects) { return ( <Box sx={{ textAlign: 'center', py: 8 }}> <CircularProgress /> <Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}> Загружаем проекты с сервера… </Typography> </Box> ); } return ( <Box> <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mb: 2 }}> <Typography variant="h6">Проекты анализа</Typography> <Button variant="contained" startIcon={<AddIcon />} onClick={() => setDialogOpen(true)}> Новый проект </Button> </Stack> {projects.length === 0 && ( <Alert severity="info">Проектов пока нет — создайте первый.</Alert> )} <Box sx={{ display: 'grid', gap: 2, gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr', lg: 'repeat(3, 1fr)' }, }} > {projects.map((p) => ( <Card key={p.id} variant="outlined"> <CardActionArea onClick={() => onOpenProject(p.id)} sx={{ height: '100%' }}> <CardContent> <Stack direction="row" alignItems="center" spacing={1} sx={{ mb: 1 }}> <FolderIcon color="primary" /> <Typography variant="subtitle1" fontWeight={600} sx={{ flexGrow: 1 }}> {p.name} </Typography> <Chip size="small" variant="outlined" label={p.jiraProjectKey} /> </Stack> {p.status === 'processing' ? ( <Chip size="small" color="info" icon={<HourglassTopIcon />} label="Анализируется…" sx={{ mb: 1 }} /> ) : p.status === 'new' ? ( <Chip size="small" color="warning" variant="outlined" label="Новый — соберите пул данных" sx={{ mb: 1 }} /> ) : ( p.stats && ( <Stack direction="row" spacing={1} useFlexGap flexWrap="wrap" sx={{ mb: 1 }}> <Chip size="small" label={`Кластеров: ${p.stats.clusters}`} /> <Chip size="small" label={`Сигналов: ${p.stats.signals}`} /> <Chip size="small" color={p.stats.pendingTasks > 0 ? 'warning' : 'default'} label={`Задач ждут решения: ${p.stats.pendingTasks}`} /> </Stack> ) )} <Typography variant="caption" color="text.secondary" display="block"> Источников данных: {p.sourcesCount} · Обновлён: {formatDate(p.updatedAt)} </Typography> </CardContent> </CardActionArea> </Card> ))} </Box> <NewProjectDialog open={dialogOpen} onClose={() => setDialogOpen(false)} onCreated={(projectId) => { setDialogOpen(false); onOpenProject(projectId); }} /> </Box> ); }