Refactor project structure and integrate Redux for state management. Update configuration files for new project name and add Webpack plugins for environment variables. Implement user authentication with Keycloak and create a context for challenge management. Add various components for user interaction, including dashboards and task workspaces. Enhance API integration and add error handling utilities. Introduce analytics and polling mechanisms for improved user experience.
platform/bro-js/challenge-pl/pipeline/head There was a failure building this commit

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-11-03 12:55:34 +03:00
parent 3a65307fd0
commit 624280ab5e
47 changed files with 3465 additions and 67 deletions
+252
View File
@@ -0,0 +1,252 @@
import React, { useMemo, useState } from 'react'
import {
Badge,
Box,
HStack,
Heading,
SimpleGrid,
Table,
TableBody,
TableCell,
TableColumnHeader,
TableContainer,
TableHeader,
TableRow,
Text,
VStack,
Select,
} from '@chakra-ui/react'
import {
useGetAllSubmissionsQuery,
useGetChainsQuery,
useGetSystemStatsQuery,
} from '../../__data__/api/api'
import type { ChallengeChain } from '../../__data__/types'
import { StatCard } from '../../components/personal'
import { ABTestPanel } from '../../components/admin/ABTestPanel'
import { mapTaskMetrics, detectIssues, msToMinutes } from '../../utils/analytics'
import { keycloak } from '../../__data__/kc'
const formatNumber = (value: number | undefined) => {
if (!value && value !== 0) return '—'
return Intl.NumberFormat('ru-RU').format(value)
}
const hasTeacherRole = () => {
try {
return keycloak?.hasResourceRole?.('teacher', 'journal') ?? false
} catch (error) {
return false
}
}
export const AdminDashboard = () => {
const isTeacher = hasTeacherRole()
const { data: systemStats, isLoading } = useGetSystemStatsQuery()
const { data: chains = [] } = useGetChainsQuery(undefined, { skip: !isTeacher })
const { data: submissions = [] } = useGetAllSubmissionsQuery(undefined, { skip: !isTeacher })
const issues = useMemo(() => (systemStats ? detectIssues(systemStats) : []), [systemStats])
const taskMetrics = useMemo(() => mapTaskMetrics(submissions), [submissions])
const [difficultyFilter, setDifficultyFilter] = useState<'all' | 'easy' | 'medium' | 'hard'>('all')
const filteredTaskMetrics = useMemo(() => {
if (difficultyFilter === 'all') return taskMetrics
return taskMetrics.filter((metric) => metric.difficulty === difficultyFilter)
}, [difficultyFilter, taskMetrics])
if (!isTeacher) {
return (
<Box bg="gray.50" minH="100vh" display="flex" alignItems="center" justifyContent="center">
<Box borderWidth="1px" borderRadius="lg" borderColor="gray.200" bg="white" p={8} maxW="480px" textAlign="center">
<Heading size="md" mb={4}>
Требуется роль преподавателя
</Heading>
<Text color="gray.600">
У вас нет доступа к панели администратора. Обратитесь к администратору Keycloak для назначения роли
<Badge ml={2} colorScheme="purple">
teacher
</Badge>
.
</Text>
</Box>
</Box>
)
}
if (isLoading || !systemStats) {
return (
<Box bg="gray.50" minH="100vh" display="flex" alignItems="center" justifyContent="center">
<Text color="gray.500">Загружаем системные метрики...</Text>
</Box>
)
}
return (
<Box bg="gray.50" minH="100vh" py={8} px={{ base: 4, md: 8 }}>
<VStack align="stretch" spacing={8} maxW="1200px" mx="auto">
<Heading size="lg">Панель преподавателя</Heading>
<SimpleGrid minChildWidth="200px" spacing={4}>
<StatCard title="Пользователей" value={formatNumber(systemStats.users)} icon="👥" />
<StatCard title="Заданий" value={formatNumber(systemStats.tasks)} icon="🧩" />
<StatCard title="Цепочек" value={formatNumber(systemStats.chains)} icon="🔗" />
<StatCard title="Всего проверок" value={formatNumber(systemStats.submissions.total)} icon="✅" />
<StatCard
title="В ожидании"
value={formatNumber(systemStats.queue.waiting)}
icon="⏳"
/>
<StatCard
title="Среднее время проверки"
value={`${msToMinutes(systemStats.averageCheckTimeMs)} мин`}
icon="⏱️"
/>
</SimpleGrid>
<Box borderWidth="1px" borderRadius="lg" borderColor="gray.200" bg="white" p={4}>
<Heading size="sm" mb={3}>
Статус очереди
</Heading>
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4}>
<Box>
<Text fontSize="sm" color="gray.500">
Всего в очереди
</Text>
<Text fontSize="lg" fontWeight="semibold">
{formatNumber(systemStats.queue.queueLength)}
</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.500">
В ожидании
</Text>
<Text fontSize="lg" fontWeight="semibold">
{formatNumber(systemStats.queue.waiting)}
</Text>
</Box>
<Box>
<Text fontSize="sm" color="gray.500">
В обработке
</Text>
<Text fontSize="lg" fontWeight="semibold">
{formatNumber(systemStats.queue.currentlyProcessing)} / {formatNumber(systemStats.queue.maxConcurrency)}
</Text>
</Box>
</SimpleGrid>
</Box>
<Box borderWidth="1px" borderRadius="lg" borderColor="gray.200" bg="white" p={4}>
<Heading size="sm" mb={3}>
Проблемные области
</Heading>
{issues.length === 0 ? (
<Text color="green.500">Критических проблем не обнаружено.</Text>
) : (
<VStack align="stretch" spacing={3}>
{issues.map((issue) => (
<Box
key={`${issue.type}-${issue.message}`}
borderWidth="1px"
borderRadius="md"
borderColor={issue.severity === 'high' ? 'red.200' : 'yellow.200'}
bg={issue.severity === 'high' ? 'red.50' : 'yellow.50'}
p={3}
>
<Text fontWeight="medium">{issue.message}</Text>
<Text fontSize="sm" color="gray.600">
Сущность: {issue.affectedEntity} · Важность: {issue.severity}
</Text>
</Box>
))}
</VStack>
)}
</Box>
<Box borderWidth="1px" borderRadius="lg" borderColor="gray.200" bg="white" p={4}>
<HStack justify="space-between" mb={4} align="center">
<Heading size="sm">Метрики по заданиям</Heading>
<Select
size="sm"
width="200px"
value={difficultyFilter}
onChange={(event) => setDifficultyFilter(event.target.value as typeof difficultyFilter)}
>
<option value="all">Все сложности</option>
<option value="easy">Лёгкие</option>
<option value="medium">Средние</option>
<option value="hard">Сложные</option>
</Select>
</HStack>
{filteredTaskMetrics.length === 0 ? (
<Text color="gray.500">Недостаточно данных о проверках для построения аналитики.</Text>
) : (
<Box overflowX="auto">
<Table size="sm" variant="simple">
<TableHeader>
<TableRow>
<TableColumnHeader>Задание</TableColumnHeader>
<TableColumnHeader textAlign="right">Попыток</TableColumnHeader>
<TableColumnHeader textAlign="right">Успешность</TableColumnHeader>
<TableColumnHeader textAlign="right">Сред. попыток</TableColumnHeader>
<TableColumnHeader textAlign="right">Сред. время (мин)</TableColumnHeader>
<TableColumnHeader>Сложность</TableColumnHeader>
</TableRow>
</TableHeader>
<TableBody>
{filteredTaskMetrics.map((metric) => (
<TableRow key={metric.taskId}>
<TableCell>
<Text fontWeight="medium">{metric.title}</Text>
<Text fontSize="xs" color="gray.500">
ID: {metric.taskId}
</Text>
</TableCell>
<TableCell textAlign="right">{formatNumber(metric.attemptsCount)}</TableCell>
<TableCell textAlign="right">{formatNumber(Math.round(metric.successRate))}%</TableCell>
<TableCell textAlign="right">{formatNumber(Math.round(metric.avgAttempts * 10) / 10)}</TableCell>
<TableCell textAlign="right">{formatNumber(Math.round(msToMinutes(metric.avgTimeToComplete * 1000)))}</TableCell>
<TableCell>
<Badge colorScheme={metric.difficulty === 'hard' ? 'red' : metric.difficulty === 'medium' ? 'yellow' : 'green'}>
{metric.difficulty}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
)}
</Box>
<Box borderWidth="1px" borderRadius="lg" borderColor="gray.200" bg="white" p={4}>
<Heading size="sm" mb={3}>
Цепочки заданий
</Heading>
{chains.length === 0 ? (
<Text color="gray.500">Цепочки не найдены. Создайте первое задание, чтобы начать.</Text>
) : (
<VStack align="stretch" spacing={3}>
{chains.map((chain: ChallengeChain) => (
<Box key={chain.id} borderWidth="1px" borderRadius="md" borderColor="gray.200" p={3}>
<Text fontWeight="medium">{chain.name}</Text>
<Text fontSize="sm" color="gray.600">
Заданий: {chain.tasks.length} · Последнее обновление:{' '}
{new Date(chain.updatedAt).toLocaleString()}
</Text>
</Box>
))}
</VStack>
)}
</Box>
<ABTestPanel />
</VStack>
</Box>
)
}