Phase 4: Add Unraid management pages with sidebar layout
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
Some checks failed
Master CI / yarn_install_and_build (push) Has been cancelled
- Add UnraidLayout component with full sidebar navigation - Add Array management page with disk tables and parity check controls - Add Docker management page with container cards and filtering - Add VMs management page with power controls (start/stop/pause/resume/reboot) - Add Shares page with security levels and storage usage - Add Users page with admin/user roles display - Add Settings index with links to all settings pages - Add Identification settings page with system info - Add Notifications settings page with notification history - Add Tools index with links to all tools - Add Syslog page with live log viewing and filtering - Add Diagnostics page with system health checks - Update dashboard to use UnraidLayout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
383
src/pages/unraid/tools/diagnostics.tsx
Normal file
383
src/pages/unraid/tools/diagnostics.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Diagnostics Page
|
||||
* Generate and download Unraid diagnostic reports
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Group,
|
||||
List,
|
||||
Loader,
|
||||
Paper,
|
||||
Progress,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconBug,
|
||||
IconAlertCircle,
|
||||
IconCheck,
|
||||
IconDownload,
|
||||
IconRefresh,
|
||||
IconCpu,
|
||||
IconDatabase,
|
||||
IconServer,
|
||||
IconNetwork,
|
||||
IconBrandDocker,
|
||||
} from '@tabler/icons-react';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
|
||||
import { UnraidLayout } from '~/components/Unraid/Layout';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
interface DiagnosticCheck {
|
||||
name: string;
|
||||
icon: React.ElementType;
|
||||
status: 'pending' | 'running' | 'success' | 'warning' | 'error';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export default function DiagnosticsPage() {
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [checks, setChecks] = useState<DiagnosticCheck[]>([
|
||||
{ name: 'System Information', icon: IconServer, status: 'pending' },
|
||||
{ name: 'CPU & Memory', icon: IconCpu, status: 'pending' },
|
||||
{ name: 'Array Status', icon: IconDatabase, status: 'pending' },
|
||||
{ name: 'Network Configuration', icon: IconNetwork, status: 'pending' },
|
||||
{ name: 'Docker Containers', icon: IconBrandDocker, status: 'pending' },
|
||||
]);
|
||||
|
||||
const {
|
||||
data: info,
|
||||
isLoading,
|
||||
error,
|
||||
} = api.unraid.info.useQuery();
|
||||
|
||||
const {
|
||||
data: array,
|
||||
} = api.unraid.array.useQuery();
|
||||
|
||||
const {
|
||||
data: docker,
|
||||
} = api.unraid.docker.useQuery();
|
||||
|
||||
const runDiagnostics = async () => {
|
||||
setIsGenerating(true);
|
||||
setProgress(0);
|
||||
|
||||
// Simulate running diagnostics
|
||||
const steps = checks.length;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
setChecks((prev) =>
|
||||
prev.map((check, idx) =>
|
||||
idx === i ? { ...check, status: 'running' } : check
|
||||
)
|
||||
);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
// Simulate random results
|
||||
const statuses: Array<'success' | 'warning' | 'error'> = ['success', 'success', 'success', 'warning'];
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)];
|
||||
|
||||
setChecks((prev) =>
|
||||
prev.map((check, idx) =>
|
||||
idx === i
|
||||
? {
|
||||
...check,
|
||||
status,
|
||||
message:
|
||||
status === 'warning'
|
||||
? 'Minor issues detected'
|
||||
: status === 'error'
|
||||
? 'Problems found'
|
||||
: 'All checks passed',
|
||||
}
|
||||
: check
|
||||
)
|
||||
);
|
||||
|
||||
setProgress(((i + 1) / steps) * 100);
|
||||
}
|
||||
|
||||
setIsGenerating(false);
|
||||
notifications.show({
|
||||
title: 'Diagnostics Complete',
|
||||
message: 'Diagnostic report has been generated',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
};
|
||||
|
||||
const resetDiagnostics = () => {
|
||||
setProgress(0);
|
||||
setChecks((prev) =>
|
||||
prev.map((check) => ({
|
||||
...check,
|
||||
status: 'pending',
|
||||
message: undefined,
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusColor = (status: DiagnosticCheck['status']) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return 'green';
|
||||
case 'warning':
|
||||
return 'yellow';
|
||||
case 'error':
|
||||
return 'red';
|
||||
case 'running':
|
||||
return 'blue';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<UnraidLayout>
|
||||
<Container size="lg" py="xl">
|
||||
<Stack align="center" spacing="md">
|
||||
<Loader size="xl" />
|
||||
<Text color="dimmed">Loading system information...</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
</UnraidLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<UnraidLayout>
|
||||
<Container size="lg" py="xl">
|
||||
<Alert icon={<IconAlertCircle size={16} />} title="Error" color="red">
|
||||
{error.message}
|
||||
</Alert>
|
||||
</Container>
|
||||
</UnraidLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<UnraidLayout>
|
||||
<Container size="lg" py="xl">
|
||||
<Stack spacing="xl">
|
||||
{/* Header */}
|
||||
<Group position="apart">
|
||||
<Group>
|
||||
<ThemeIcon size={48} radius="md" variant="light" color="red">
|
||||
<IconBug size={28} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Title order={2}>Diagnostics</Title>
|
||||
<Text color="dimmed" size="sm">
|
||||
System health checks and diagnostic reports
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Group>
|
||||
<Button
|
||||
variant="light"
|
||||
leftIcon={<IconRefresh size={16} />}
|
||||
onClick={resetDiagnostics}
|
||||
disabled={isGenerating}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<IconBug size={16} />}
|
||||
onClick={runDiagnostics}
|
||||
loading={isGenerating}
|
||||
>
|
||||
Run Diagnostics
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Progress */}
|
||||
{progress > 0 && (
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Group position="apart" mb="xs">
|
||||
<Text weight={500}>Diagnostic Progress</Text>
|
||||
<Text size="sm" color="dimmed">
|
||||
{Math.round(progress)}%
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={progress}
|
||||
color={progress === 100 ? 'green' : 'blue'}
|
||||
size="lg"
|
||||
radius="xl"
|
||||
animate={isGenerating}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Diagnostic Checks */}
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Title order={4} mb="md">
|
||||
System Checks
|
||||
</Title>
|
||||
|
||||
<Stack spacing="md">
|
||||
{checks.map((check, idx) => (
|
||||
<div key={check.name}>
|
||||
<Group position="apart">
|
||||
<Group spacing="sm">
|
||||
<ThemeIcon
|
||||
size="md"
|
||||
variant="light"
|
||||
color={getStatusColor(check.status)}
|
||||
>
|
||||
{check.status === 'running' ? (
|
||||
<Loader size={14} color="blue" />
|
||||
) : (
|
||||
<check.icon size={16} />
|
||||
)}
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text weight={500}>{check.name}</Text>
|
||||
{check.message && (
|
||||
<Text size="xs" color="dimmed">
|
||||
{check.message}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
<Text
|
||||
size="sm"
|
||||
weight={500}
|
||||
color={getStatusColor(check.status)}
|
||||
transform="uppercase"
|
||||
>
|
||||
{check.status}
|
||||
</Text>
|
||||
</Group>
|
||||
{idx < checks.length - 1 && <Divider mt="md" />}
|
||||
</div>
|
||||
))}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* System Summary */}
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Title order={4} mb="md">
|
||||
System Summary
|
||||
</Title>
|
||||
|
||||
<Stack spacing="md">
|
||||
<Group position="apart">
|
||||
<Text color="dimmed">Unraid Version</Text>
|
||||
<Text weight={500}>{info?.versions.unraid || 'Unknown'}</Text>
|
||||
</Group>
|
||||
<Divider />
|
||||
<Group position="apart">
|
||||
<Text color="dimmed">Linux Kernel</Text>
|
||||
<Text weight={500}>{info?.versions.linux || 'Unknown'}</Text>
|
||||
</Group>
|
||||
<Divider />
|
||||
<Group position="apart">
|
||||
<Text color="dimmed">Array Status</Text>
|
||||
<Text weight={500} color={array?.state === 'STARTED' ? 'green' : 'red'}>
|
||||
{array?.state || 'Unknown'}
|
||||
</Text>
|
||||
</Group>
|
||||
<Divider />
|
||||
<Group position="apart">
|
||||
<Text color="dimmed">Docker Containers</Text>
|
||||
<Text weight={500}>
|
||||
{docker?.containers.filter((c) => c.state === 'RUNNING').length || 0} running /{' '}
|
||||
{docker?.containers.length || 0} total
|
||||
</Text>
|
||||
</Group>
|
||||
<Divider />
|
||||
<Group position="apart">
|
||||
<Text color="dimmed">CPU</Text>
|
||||
<Text weight={500}>{info?.cpu.model || 'Unknown'}</Text>
|
||||
</Group>
|
||||
<Divider />
|
||||
<Group position="apart">
|
||||
<Text color="dimmed">Total RAM</Text>
|
||||
<Text weight={500}>
|
||||
{info?.memory?.total
|
||||
? `${Math.round(info.memory.total / 1024 / 1024 / 1024)} GB`
|
||||
: 'Unknown'}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
{/* Download Report */}
|
||||
<Card shadow="sm" radius="md" withBorder>
|
||||
<Title order={4} mb="md">
|
||||
Diagnostic Report
|
||||
</Title>
|
||||
|
||||
<Text size="sm" color="dimmed" mb="md">
|
||||
Generate a comprehensive diagnostic report for troubleshooting. This includes system
|
||||
logs, configuration files, and hardware information.
|
||||
</Text>
|
||||
|
||||
<List size="sm" spacing="xs" mb="md">
|
||||
<List.Item>System configuration and settings</List.Item>
|
||||
<List.Item>Hardware information (CPU, RAM, disks)</List.Item>
|
||||
<List.Item>Network configuration</List.Item>
|
||||
<List.Item>Docker container status</List.Item>
|
||||
<List.Item>Recent system logs</List.Item>
|
||||
<List.Item>Plugin information</List.Item>
|
||||
</List>
|
||||
|
||||
<Button
|
||||
leftIcon={<IconDownload size={16} />}
|
||||
variant="light"
|
||||
disabled
|
||||
>
|
||||
Download Diagnostic Report
|
||||
</Button>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Container>
|
||||
</UnraidLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const session = await getServerAuthSession(context);
|
||||
const translations = await getServerSideTranslations(
|
||||
['common'],
|
||||
context.locale,
|
||||
context.req,
|
||||
context.res
|
||||
);
|
||||
|
||||
const result = checkForSessionOrAskForLogin(
|
||||
context,
|
||||
session,
|
||||
() => session?.user != undefined
|
||||
);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user