mirror of
https://github.com/pyrohost/pyrodactyl.git
synced 2026-04-06 04:01:58 +02:00
feat: added uptime and improved buttons on console
This commit is contained in:
@@ -2,7 +2,7 @@ import { forwardRef } from 'react';
|
||||
|
||||
interface ActionButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
size?: 'start' | 'sm' | 'md' | 'lg';
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const ActionButton = forwardRef<HTMLButtonElement, ActionButtonProps>(
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
start: 'h-8 px-2 py-1.5 text-xs',
|
||||
sm: 'h-8 px-3 py-1.5 text-xs',
|
||||
md: 'h-10 px-4 py-2 text-sm',
|
||||
lg: 'h-12 px-6 py-3 text-base',
|
||||
|
||||
19
resources/scripts/components/server/UptimeDuration.ts
Normal file
19
resources/scripts/components/server/UptimeDuration.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export function formatUptime(uptime: number): string {
|
||||
if (uptime <= 0) {
|
||||
return 'Offline';
|
||||
}
|
||||
|
||||
const secondsTotal = Math.floor(uptime / 1000);
|
||||
const days = Math.floor(secondsTotal / 86400);
|
||||
const hours = Math.floor((secondsTotal % 86400) / 3600);
|
||||
const minutes = Math.floor((secondsTotal % 3600) / 60);
|
||||
const seconds = secondsTotal % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
return `${hours}h ${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
export default formatUptime;
|
||||
@@ -1,24 +0,0 @@
|
||||
const UptimeDuration = ({ uptime }: { uptime: number }) => {
|
||||
const uptimeDiv = uptime / 1000;
|
||||
const days = Math.floor(uptimeDiv / (24 * 60 * 60));
|
||||
const hours = Math.floor((Math.floor(uptimeDiv) / 60 / 60) % 24);
|
||||
const remainder = Math.floor(uptimeDiv - hours * 60 * 60);
|
||||
const minutes = Math.floor((remainder / 60) % 60);
|
||||
const seconds = remainder % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return (
|
||||
<>
|
||||
{days}d {hours}h {minutes}m
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hours}h {minutes}m {seconds}s
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UptimeDuration;
|
||||
@@ -3,6 +3,7 @@ import { HugeiconsIcon } from '@hugeicons/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import ActionButton from '@/components/elements/ActionButton';
|
||||
import Can from '@/components/elements/Can';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import { PowerAction } from '@/components/server/console/ServerConsoleContainer';
|
||||
@@ -71,9 +72,9 @@ const PowerButtons = ({ className }: PowerButtonProps) => {
|
||||
Forcibly stopping a server can lead to data corruption.
|
||||
</Dialog.Confirm>
|
||||
<Can action={'control.start'}>
|
||||
<Button
|
||||
<ActionButton
|
||||
variant={'secondary'}
|
||||
size={'sm'}
|
||||
size={'start'}
|
||||
className='px-3 gap-1 rounded-full'
|
||||
disabled={status !== 'offline'}
|
||||
onClick={onButtonClick.bind(this, 'start')}
|
||||
@@ -83,31 +84,31 @@ const PowerButtons = ({ className }: PowerButtonProps) => {
|
||||
<HugeiconsIcon size={16} strokeWidth={2} icon={PlayIcon} className='size-4' />
|
||||
Start
|
||||
</div>
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Can>
|
||||
<Can action={'control.restart'}>
|
||||
<Button
|
||||
<ActionButton
|
||||
variant={'secondary'}
|
||||
size={'sm'}
|
||||
size={'start'}
|
||||
className='p-1 gap-1 rounded-full size-8'
|
||||
disabled={!status}
|
||||
onClick={onButtonClick.bind(this, 'restart')}
|
||||
aria-label='Restart server'
|
||||
>
|
||||
<HugeiconsIcon size={16} icon={Rotate01FreeIcons} />
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Can>
|
||||
<Can action={'control.stop'}>
|
||||
<Button
|
||||
<ActionButton
|
||||
variant={'secondary'}
|
||||
size={'sm'}
|
||||
size={'start'}
|
||||
className='p-1 gap-1 rounded-full size-8'
|
||||
disabled={status === 'offline'}
|
||||
onClick={onButtonClick.bind(this, killable ? 'kill' : 'stop')}
|
||||
aria-label={`${killable ? 'Kill' : 'Stop'} server`}
|
||||
>
|
||||
<HugeiconsIcon size={16} icon={StopIcon} />
|
||||
</Button>
|
||||
</ActionButton>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ const ServerConsoleContainer = () => {
|
||||
<StatusPillHeader />
|
||||
<span className='xl:max-w-[20vw] min-w-0 truncate'>{name}</span>
|
||||
</div>
|
||||
|
||||
<div className='border-l border-gray-200 h-6' />
|
||||
<ServerDetailsHeader />
|
||||
</HeaderCentered>
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import StatBlock from '@/components/server/console/StatBlock';
|
||||
import { SocketEvent, SocketRequest } from '@/components/server/events';
|
||||
|
||||
import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
|
||||
type Stats = Record<'memory' | 'cpu' | 'disk' | 'uptime' | 'rx' | 'tx', number>;
|
||||
|
||||
// const getBackgroundColor = (value: number, max: number | null): string | undefined => {
|
||||
// const delta = !max ? 0 : value / max;
|
||||
|
||||
// if (delta > 0.8) {
|
||||
// if (delta > 0.9) {
|
||||
// return 'bg-red-500';
|
||||
// }
|
||||
// return 'bg-yellow-500';
|
||||
// }
|
||||
|
||||
// return undefined;
|
||||
// };
|
||||
|
||||
// @ts-expect-error - Unused parameter in component definition
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const Limit = ({ limit, children }: { limit: string | null; children: React.ReactNode }) => <>{children}</>;
|
||||
|
||||
const ServerDetailsBlock = ({ className }: { className?: string }) => {
|
||||
const [stats, setStats] = useState<Stats>({ memory: 0, cpu: 0, disk: 0, uptime: 0, tx: 0, rx: 0 });
|
||||
|
||||
const status = ServerContext.useStoreState((state) => state.status.value);
|
||||
const connected = ServerContext.useStoreState((state) => state.socket.connected);
|
||||
const instance = ServerContext.useStoreState((state) => state.socket.instance);
|
||||
const limits = ServerContext.useStoreState((state) => state.server.data!.limits);
|
||||
|
||||
const textLimits = useMemo(
|
||||
() => ({
|
||||
cpu: limits?.cpu ? `${limits.cpu}%` : null,
|
||||
memory: limits?.memory ? bytesToString(mbToBytes(limits.memory)) : null,
|
||||
disk: limits?.disk ? bytesToString(mbToBytes(limits.disk)) : null,
|
||||
}),
|
||||
[limits],
|
||||
);
|
||||
|
||||
const allocation = ServerContext.useStoreState((state) => {
|
||||
const match = state.server.data!.allocations.find((allocation) => allocation.isDefault);
|
||||
|
||||
return !match ? 'n/a' : `${match.alias || ip(match.ip)}:${match.port}`;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.send(SocketRequest.SEND_STATS);
|
||||
}, [instance, connected]);
|
||||
|
||||
useWebsocketEvent(SocketEvent.STATS, (data) => {
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStats({
|
||||
memory: stats.memory_bytes,
|
||||
cpu: stats.cpu_absolute,
|
||||
disk: stats.disk_bytes,
|
||||
tx: stats.network.tx_bytes,
|
||||
rx: stats.network.rx_bytes,
|
||||
uptime: stats.uptime || 0,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cn('flex md:flex-row gap-4 flex-col', className)}>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `150ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<StatBlock title={'IP Address'} copyOnClick={allocation}>
|
||||
{allocation}
|
||||
</StatBlock>
|
||||
</div>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `175ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<StatBlock title={'CPU'}>
|
||||
{status === 'offline' ? (
|
||||
<span className={'text-zinc-400'}>Offline</span>
|
||||
) : (
|
||||
<Limit limit={textLimits.cpu}>{stats.cpu.toFixed(2)}%</Limit>
|
||||
)}
|
||||
</StatBlock>
|
||||
</div>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `200ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<StatBlock title={'RAM'}>
|
||||
{status === 'offline' ? (
|
||||
<span className={'text-zinc-400'}>Offline</span>
|
||||
) : (
|
||||
<Limit limit={textLimits.memory}>{bytesToString(stats.memory)}</Limit>
|
||||
)}
|
||||
</StatBlock>
|
||||
</div>
|
||||
<div
|
||||
className='transform-gpu skeleton-anim-2'
|
||||
style={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
animationDelay: `225ms`,
|
||||
animationTimingFunction:
|
||||
'linear(0,0.01,0.04 1.6%,0.161 3.3%,0.816 9.4%,1.046,1.189 14.4%,1.231,1.254 17%,1.259,1.257 18.6%,1.236,1.194 22.3%,1.057 27%,0.999 29.4%,0.955 32.1%,0.942,0.935 34.9%,0.933,0.939 38.4%,1 47.3%,1.011,1.017 52.6%,1.016 56.4%,1 65.2%,0.996 70.2%,1.001 87.2%,1)',
|
||||
}}
|
||||
>
|
||||
<StatBlock title={'Storage'}>
|
||||
<Limit limit={textLimits.disk}>{bytesToString(stats.disk)}</Limit>
|
||||
</StatBlock>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDetailsBlock;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CloudDownload, CloudUpload } from '@carbon/icons-react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
|
||||
import ChartBlock from '@/components/server/console/ChartBlock';
|
||||
@@ -14,9 +14,12 @@ import { ServerContext } from '@/state/server';
|
||||
|
||||
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
|
||||
|
||||
import formatUptime from '../UptimeDuration';
|
||||
|
||||
interface StatsData {
|
||||
cpu_absolute: number;
|
||||
memory_bytes: number;
|
||||
uptime: number;
|
||||
network: {
|
||||
tx_bytes: number;
|
||||
rx_bytes: number;
|
||||
@@ -29,6 +32,7 @@ const StatGraphs = () => {
|
||||
const previous = useRef<Record<'tx' | 'rx', number>>({ tx: -1, rx: -1 });
|
||||
|
||||
const cpu = useChartTickLabel('CPU', limits.cpu, '%', 2);
|
||||
const [uptime, setUptime] = useState(0);
|
||||
const memory = useChartTickLabel('Memory', limits.memory, 'MiB');
|
||||
const network = useChart('Network', {
|
||||
sets: 2,
|
||||
@@ -58,6 +62,7 @@ const StatGraphs = () => {
|
||||
cpu.clear();
|
||||
memory.clear();
|
||||
network.clear();
|
||||
setUptime(0);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [status]);
|
||||
@@ -69,8 +74,10 @@ const StatGraphs = () => {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
setUptime(values.uptime);
|
||||
cpu.push(values.cpu_absolute);
|
||||
memory.push(Math.floor(values.memory_bytes / 1024 / 1024));
|
||||
|
||||
network.push([
|
||||
previous.current.tx < 0 ? 0 : Math.max(0, values.network.tx_bytes - previous.current.tx),
|
||||
previous.current.rx < 0 ? 0 : Math.max(0, values.network.rx_bytes - previous.current.rx),
|
||||
@@ -96,6 +103,12 @@ const StatGraphs = () => {
|
||||
<div className='font-medium'>{allocation}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='group p-4 justify-between relative rounded-xl border-[1px] border-[#ffffff11] bg-[#110f0d] flex gap-4 text-sm'>
|
||||
<h3 className='font-extrabold'>Uptime</h3>
|
||||
<div className='font-medium'>{formatUptime(uptime)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='group p-4 justify-between relative rounded-xl border-[1px] border-[#ffffff11] flex-col bg-[#110f0d] flex gap-4 text-sm'>
|
||||
<h3 className='font-extrabold'>Description</h3>
|
||||
|
||||
Reference in New Issue
Block a user