Add Unraid API integration and Orchis theme
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
Phase 1: Foundation Setup - Create Unraid GraphQL client with type-safe queries/mutations - Add comprehensive TypeScript types for all Unraid data models - Implement tRPC router with 30+ endpoints for Unraid management - Add environment variables for Unraid connection Phase 2: Core Dashboard - Create SystemInfoCard component (CPU, RAM, OS, motherboard) - Create ArrayCard component (disks, parity, cache pools) - Create DockerCard component with start/stop controls - Create VmsCard component with power management - Add main Unraid dashboard page with real-time updates Phase 3: Orchis Theme Integration - Create Mantine theme override with Orchis design tokens - Add CSS custom properties for light/dark modes - Configure shadows, spacing, radius from Orchis specs - Style all Mantine components with Orchis patterns Files added: - src/lib/unraid/* (GraphQL client, types, queries) - src/server/api/routers/unraid/* (tRPC router) - src/components/Unraid/* (Dashboard components) - src/pages/unraid/* (Dashboard page) - src/styles/orchis/* (Theme configuration) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
346
src/pages/unraid/index.tsx
Normal file
346
src/pages/unraid/index.tsx
Normal file
@@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Unraid Dashboard Page
|
||||
* Main overview page for Unraid server management
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Container,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Title,
|
||||
useMantineTheme,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconServer, IconAlertCircle, IconCheck } from '@tabler/icons-react';
|
||||
import { GetServerSidePropsContext } from 'next';
|
||||
|
||||
import { SystemInfoCard, ArrayCard, DockerCard, VmsCard } from '~/components/Unraid/Dashboard';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
import { api } from '~/utils/api';
|
||||
|
||||
export default function UnraidDashboardPage() {
|
||||
const theme = useMantineTheme();
|
||||
const [loadingContainers, setLoadingContainers] = useState<string[]>([]);
|
||||
const [loadingVms, setLoadingVms] = useState<string[]>([]);
|
||||
const [arrayLoading, setArrayLoading] = useState(false);
|
||||
|
||||
// Fetch dashboard data
|
||||
const {
|
||||
data: dashboard,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = api.unraid.dashboard.useQuery(undefined, {
|
||||
refetchInterval: 10000, // Refresh every 10 seconds
|
||||
});
|
||||
|
||||
// Mutations
|
||||
const startContainer = api.unraid.startContainer.useMutation({
|
||||
onMutate: ({ id }) => setLoadingContainers((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) =>
|
||||
setLoadingContainers((prev) => prev.filter((cid) => cid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'Container Started',
|
||||
message: 'Container started successfully',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle size={16} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const stopContainer = api.unraid.stopContainer.useMutation({
|
||||
onMutate: ({ id }) => setLoadingContainers((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) =>
|
||||
setLoadingContainers((prev) => prev.filter((cid) => cid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'Container Stopped',
|
||||
message: 'Container stopped successfully',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle size={16} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const startVm = api.unraid.startVm.useMutation({
|
||||
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'VM Started',
|
||||
message: 'Virtual machine started successfully',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle size={16} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const stopVm = api.unraid.stopVm.useMutation({
|
||||
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'VM Stopped',
|
||||
message: 'Virtual machine stopped successfully',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle size={16} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const pauseVm = api.unraid.pauseVm.useMutation({
|
||||
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'VM Paused',
|
||||
message: 'Virtual machine paused successfully',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const resumeVm = api.unraid.resumeVm.useMutation({
|
||||
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'VM Resumed',
|
||||
message: 'Virtual machine resumed successfully',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const rebootVm = api.unraid.rebootVm.useMutation({
|
||||
onMutate: ({ id }) => setLoadingVms((prev) => [...prev, id]),
|
||||
onSettled: (_, __, { id }) => setLoadingVms((prev) => prev.filter((vid) => vid !== id)),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'VM Rebooting',
|
||||
message: 'Virtual machine is rebooting',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
});
|
||||
|
||||
const startArray = api.unraid.startArray.useMutation({
|
||||
onMutate: () => setArrayLoading(true),
|
||||
onSettled: () => setArrayLoading(false),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'Array Starting',
|
||||
message: 'Array is starting...',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle size={16} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const stopArray = api.unraid.stopArray.useMutation({
|
||||
onMutate: () => setArrayLoading(true),
|
||||
onSettled: () => setArrayLoading(false),
|
||||
onSuccess: () => {
|
||||
notifications.show({
|
||||
title: 'Array Stopping',
|
||||
message: 'Array is stopping...',
|
||||
color: 'green',
|
||||
icon: <IconCheck size={16} />,
|
||||
});
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
notifications.show({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
color: 'red',
|
||||
icon: <IconAlertCircle size={16} />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Stack align="center" spacing="md">
|
||||
<Loader size="xl" />
|
||||
<Text color="dimmed">Connecting to Unraid server...</Text>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error) {
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="Connection Error"
|
||||
color="red"
|
||||
variant="filled"
|
||||
>
|
||||
{error.message}
|
||||
</Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
// No data
|
||||
if (!dashboard) {
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Alert
|
||||
icon={<IconAlertCircle size={16} />}
|
||||
title="No Data"
|
||||
color="yellow"
|
||||
>
|
||||
No data received from Unraid server. Please check your configuration.
|
||||
</Alert>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Container size="xl" py="xl">
|
||||
<Stack spacing="xl">
|
||||
{/* Header */}
|
||||
<Group position="apart">
|
||||
<Group>
|
||||
<ThemeIcon size={48} radius="md" variant="gradient" gradient={{ from: 'blue', to: 'cyan' }}>
|
||||
<IconServer size={28} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Title order={1}>Unraid Dashboard</Title>
|
||||
<Text color="dimmed" size="sm">
|
||||
{dashboard.vars.name} - Unraid {dashboard.info.versions.unraid}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{/* Dashboard Grid */}
|
||||
<Grid>
|
||||
{/* System Info */}
|
||||
<Grid.Col md={6} lg={4}>
|
||||
<SystemInfoCard
|
||||
info={dashboard.info}
|
||||
vars={dashboard.vars}
|
||||
registration={dashboard.registration}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Array */}
|
||||
<Grid.Col md={6} lg={8}>
|
||||
<ArrayCard
|
||||
array={dashboard.array}
|
||||
onStartArray={() => startArray.mutate()}
|
||||
onStopArray={() => stopArray.mutate()}
|
||||
isLoading={arrayLoading}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
{/* Docker */}
|
||||
<Grid.Col md={6}>
|
||||
<DockerCard
|
||||
docker={dashboard.docker}
|
||||
onStartContainer={(id) => startContainer.mutate({ id })}
|
||||
onStopContainer={(id) => stopContainer.mutate({ id })}
|
||||
loadingContainers={loadingContainers}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
{/* VMs */}
|
||||
<Grid.Col md={6}>
|
||||
<VmsCard
|
||||
vms={dashboard.vms}
|
||||
onStartVm={(id) => startVm.mutate({ id })}
|
||||
onStopVm={(id) => stopVm.mutate({ id })}
|
||||
onPauseVm={(id) => pauseVm.mutate({ id })}
|
||||
onResumeVm={(id) => resumeVm.mutate({ id })}
|
||||
onRebootVm={(id) => rebootVm.mutate({ id })}
|
||||
loadingVms={loadingVms}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
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