Path: blob/1.0-develop/resources/scripts/components/dashboard/ServerRow.tsx
7461 views
import React, { memo, useEffect, useRef, useState } from 'react';1import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';2import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';3import { Link } from 'react-router-dom';4import { Server } from '@/api/server/getServer';5import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage';6import { bytesToString, ip, mbToBytes } from '@/lib/formatters';7import tw from 'twin.macro';8import GreyRowBox from '@/components/elements/GreyRowBox';9import Spinner from '@/components/elements/Spinner';10import styled from 'styled-components/macro';11import isEqual from 'react-fast-compare';1213// Determines if the current value is in an alarm threshold so we can show it in red rather14// than the more faded default style.15const isAlarmState = (current: number, limit: number): boolean => limit > 0 && current / (limit * 1024 * 1024) >= 0.9;1617const Icon = memo(18styled(FontAwesomeIcon)<{ $alarm: boolean }>`19${(props) => (props.$alarm ? tw`text-red-400` : tw`text-neutral-500`)};20`,21isEqual22);2324const IconDescription = styled.p<{ $alarm: boolean }>`25${tw`text-sm ml-2`};26${(props) => (props.$alarm ? tw`text-white` : tw`text-neutral-400`)};27`;2829const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`30${tw`grid grid-cols-12 gap-4 relative`};3132& .status-bar {33${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};34height: calc(100% - 0.5rem);3536${({ $status }) =>37!$status || $status === 'offline'38? tw`bg-red-500`39: $status === 'running'40? tw`bg-green-500`41: tw`bg-yellow-500`};42}4344&:hover .status-bar {45${tw`opacity-75`};46}47`;4849type Timer = ReturnType<typeof setInterval>;5051export default ({ server, className }: { server: Server; className?: string }) => {52const interval = useRef<Timer>(null) as React.MutableRefObject<Timer>;53const [isSuspended, setIsSuspended] = useState(server.status === 'suspended');54const [stats, setStats] = useState<ServerStats | null>(null);5556const getStats = () =>57getServerResourceUsage(server.uuid)58.then((data) => setStats(data))59.catch((error) => console.error(error));6061useEffect(() => {62setIsSuspended(stats?.isSuspended || server.status === 'suspended');63}, [stats?.isSuspended, server.status]);6465useEffect(() => {66// Don't waste a HTTP request if there is nothing important to show to the user because67// the server is suspended.68if (isSuspended) return;6970getStats().then(() => {71interval.current = setInterval(() => getStats(), 30000);72});7374return () => {75interval.current && clearInterval(interval.current);76};77}, [isSuspended]);7879const alarms = { cpu: false, memory: false, disk: false };80if (stats) {81alarms.cpu = server.limits.cpu === 0 ? false : stats.cpuUsagePercent >= server.limits.cpu * 0.9;82alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);83alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);84}8586const diskLimit = server.limits.disk !== 0 ? bytesToString(mbToBytes(server.limits.disk)) : 'Unlimited';87const memoryLimit = server.limits.memory !== 0 ? bytesToString(mbToBytes(server.limits.memory)) : 'Unlimited';88const cpuLimit = server.limits.cpu !== 0 ? server.limits.cpu + ' %' : 'Unlimited';8990return (91<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>92<div css={tw`flex items-center col-span-12 sm:col-span-5 lg:col-span-6`}>93<div className={'icon mr-4'}>94<FontAwesomeIcon icon={faServer} />95</div>96<div>97<p css={tw`text-lg break-words`}>{server.name}</p>98{!!server.description && (99<p css={tw`text-sm text-neutral-300 break-words line-clamp-2`}>{server.description}</p>100)}101</div>102</div>103<div css={tw`flex-1 ml-4 lg:block lg:col-span-2 hidden`}>104<div css={tw`flex justify-center`}>105<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`} />106<p css={tw`text-sm text-neutral-400 ml-2`}>107{server.allocations108.filter((alloc) => alloc.isDefault)109.map((allocation) => (110<React.Fragment key={allocation.ip + allocation.port.toString()}>111{allocation.alias || ip(allocation.ip)}:{allocation.port}112</React.Fragment>113))}114</p>115</div>116</div>117<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>118{!stats || isSuspended ? (119isSuspended ? (120<div css={tw`flex-1 text-center`}>121<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>122{server.status === 'suspended' ? 'Suspended' : 'Connection Error'}123</span>124</div>125) : server.isTransferring || server.status ? (126<div css={tw`flex-1 text-center`}>127<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>128{server.isTransferring129? 'Transferring'130: server.status === 'installing'131? 'Installing'132: server.status === 'restoring_backup'133? 'Restoring Backup'134: 'Unavailable'}135</span>136</div>137) : (138<Spinner size={'small'} />139)140) : (141<React.Fragment>142<div css={tw`flex-1 ml-4 sm:block hidden`}>143<div css={tw`flex justify-center`}>144<Icon icon={faMicrochip} $alarm={alarms.cpu} />145<IconDescription $alarm={alarms.cpu}>146{stats.cpuUsagePercent.toFixed(2)} %147</IconDescription>148</div>149<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {cpuLimit}</p>150</div>151<div css={tw`flex-1 ml-4 sm:block hidden`}>152<div css={tw`flex justify-center`}>153<Icon icon={faMemory} $alarm={alarms.memory} />154<IconDescription $alarm={alarms.memory}>155{bytesToString(stats.memoryUsageInBytes)}156</IconDescription>157</div>158<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memoryLimit}</p>159</div>160<div css={tw`flex-1 ml-4 sm:block hidden`}>161<div css={tw`flex justify-center`}>162<Icon icon={faHdd} $alarm={alarms.disk} />163<IconDescription $alarm={alarms.disk}>164{bytesToString(stats.diskUsageInBytes)}165</IconDescription>166</div>167<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {diskLimit}</p>168</div>169</React.Fragment>170)}171</div>172<div className={'status-bar'} />173</StatusIndicatorBox>174);175};176177178