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