mirror of
https://github.com/ConvoyPanel/panel.git
synced 2026-06-20 10:32:40 +08:00
Add server state fetching
This commit is contained in:
@@ -4,17 +4,24 @@ namespace Convoy\Http\Controllers\Client\Servers;
|
||||
|
||||
use Convoy\Http\Controllers\ApplicationApiController;
|
||||
use Convoy\Models\Server;
|
||||
use Convoy\Repositories\Proxmox\Server\ProxmoxServerRepository;
|
||||
use Convoy\Services\Servers\ServerDetailService;
|
||||
use Convoy\Transformers\Client\ServerStatusTransformer;
|
||||
use Convoy\Transformers\Client\ServerTransformer;
|
||||
|
||||
class ServerController extends ApplicationApiController
|
||||
{
|
||||
public function __construct(private ServerDetailService $service)
|
||||
public function __construct(private ServerDetailService $detailService, private ProxmoxServerRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
public function index(Server $server)
|
||||
{
|
||||
return fractal($this->service->getByEloquent($server), new ServerTransformer)->respond();
|
||||
return fractal($this->detailService->getByEloquent($server), new ServerTransformer)->respond();
|
||||
}
|
||||
|
||||
public function status(Server $server)
|
||||
{
|
||||
return fractal()->item($this->repository->setServer($server)->getStatus(), new ServerStatusTransformer)->respond();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,43 +38,12 @@ class AuthenticateServerAccess
|
||||
|
||||
try {
|
||||
$server->validateCurrentState();
|
||||
|
||||
if ($request->routeIs(['servers.show.building', 'servers.show.suspended'])) {
|
||||
return redirect()->route('servers.show', $server);
|
||||
}
|
||||
} catch (ServerStateConflictException $exception) {
|
||||
if ($server->isInstalling() && !$request->routeIs('servers.show.building')) {
|
||||
//throw $exception; // for v3
|
||||
return redirect()->route('servers.show.building', $server->id);
|
||||
}
|
||||
|
||||
if ($server->isSuspended() && !$request->routeIs('servers.show.suspended')) {
|
||||
//throw $exception; // for v3
|
||||
return redirect()->route('servers.show.suspended', $server->id);
|
||||
}
|
||||
|
||||
if ($request->routeIs(['servers.show.building', 'servers.show.suspended'])) {
|
||||
if ($request->routeIs('api:client:servers.show')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
throw $exception;
|
||||
|
||||
/* if (!$request->routeIs(['servers.show.building'])) {
|
||||
if ($server->isSuspended() && !$request->routeIs('servers.show.suspended')) {
|
||||
//throw $exception; // for v3
|
||||
return redirect()->route('servers.show.suspended', $server->id);
|
||||
}
|
||||
|
||||
if ($server->isInstalling()) {
|
||||
//throw $exception; // for v3
|
||||
return redirect()->route('servers.show.building', $server->id);
|
||||
}
|
||||
|
||||
// TODO: users can still view building screen on accident if the server is suspended and vice versa
|
||||
if (!$user->root_admin || !$request->routeIs($this->except) && !$request->routeIs(['servers.show.suspended', 'servers.show.building'])) {
|
||||
throw $exception;
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
||||
23
app/Transformers/Client/ServerStatusTransformer.php
Normal file
23
app/Transformers/Client/ServerStatusTransformer.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Convoy\Transformers\Client;
|
||||
|
||||
use League\Fractal\TransformerAbstract;
|
||||
|
||||
class ServerStatusTransformer extends TransformerAbstract
|
||||
{
|
||||
/**
|
||||
* A Fractal transformer.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function transform(array $data)
|
||||
{
|
||||
return [
|
||||
'state' => array_get($data, 'status'),
|
||||
'uptime' => array_get($data, 'uptime'),
|
||||
'cpu' => array_get($data, 'cpu'),
|
||||
'memory' => array_get($data, 'mem'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@
|
||||
"webmozart/assert": "^1.11"
|
||||
},
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-debugbar": "^3.7",
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
"laravel/breeze": "^1.10",
|
||||
"laravel/pint": "^1.2",
|
||||
|
||||
152
composer.lock
generated
152
composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "05cf11cf918a00051cc054e8ffc75170",
|
||||
"content-hash": "cb91f4b0758f1e0941967192c7c84c2c",
|
||||
"packages": [
|
||||
{
|
||||
"name": "bacon/bacon-qr-code",
|
||||
@@ -6778,90 +6778,6 @@
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "barryvdh/laravel-debugbar",
|
||||
"version": "v3.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/barryvdh/laravel-debugbar.git",
|
||||
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3372ed65e6d2039d663ed19aa699956f9d346271",
|
||||
"reference": "3372ed65e6d2039d663ed19aa699956f9d346271",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"illuminate/routing": "^7|^8|^9",
|
||||
"illuminate/session": "^7|^8|^9",
|
||||
"illuminate/support": "^7|^8|^9",
|
||||
"maximebf/debugbar": "^1.17.2",
|
||||
"php": ">=7.2.5",
|
||||
"symfony/finder": "^5|^6"
|
||||
},
|
||||
"require-dev": {
|
||||
"mockery/mockery": "^1.3.3",
|
||||
"orchestra/testbench-dusk": "^5|^6|^7",
|
||||
"phpunit/phpunit": "^8.5|^9.0",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.6-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Barryvdh\\Debugbar\\ServiceProvider"
|
||||
],
|
||||
"aliases": {
|
||||
"Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Barryvdh\\Debugbar\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "PHP Debugbar integration for Laravel",
|
||||
"keywords": [
|
||||
"debug",
|
||||
"debugbar",
|
||||
"laravel",
|
||||
"profiler",
|
||||
"webprofiler"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
|
||||
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.7.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://fruitcake.nl",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/barryvdh",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2022-07-11T09:26:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/class-map-generator",
|
||||
"version": "1.0.0",
|
||||
@@ -7452,72 +7368,6 @@
|
||||
},
|
||||
"time": "2022-09-28T13:13:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maximebf/debugbar",
|
||||
"version": "v1.18.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/maximebf/php-debugbar.git",
|
||||
"reference": "ba0af68dd4316834701ecb30a00ce9604ced3ee9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/ba0af68dd4316834701ecb30a00ce9604ced3ee9",
|
||||
"reference": "ba0af68dd4316834701ecb30a00ce9604ced3ee9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.1|^8",
|
||||
"psr/log": "^1|^2|^3",
|
||||
"symfony/var-dumper": "^2.6|^3|^4|^5|^6"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7.5.20 || ^9.4.2",
|
||||
"twig/twig": "^1.38|^2.7|^3.0"
|
||||
},
|
||||
"suggest": {
|
||||
"kriswallsmith/assetic": "The best way to manage assets",
|
||||
"monolog/monolog": "Log using Monolog",
|
||||
"predis/predis": "Redis storage"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.18-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"DebugBar\\": "src/DebugBar/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Maxime Bouroumeau-Fuseau",
|
||||
"email": "maxime.bouroumeau@gmail.com",
|
||||
"homepage": "http://maximebf.com"
|
||||
},
|
||||
{
|
||||
"name": "Barry vd. Heuvel",
|
||||
"email": "barryvdh@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Debug bar in the browser for php application",
|
||||
"homepage": "https://github.com/maximebf/php-debugbar",
|
||||
"keywords": [
|
||||
"debug",
|
||||
"debugbar"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maximebf/php-debugbar/issues",
|
||||
"source": "https://github.com/maximebf/php-debugbar/tree/v1.18.1"
|
||||
},
|
||||
"time": "2022-03-31T14:55:54+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mockery/mockery",
|
||||
"version": "1.5.1",
|
||||
|
||||
@@ -37,7 +37,7 @@ const http: AxiosInstance = axios.create({
|
||||
})
|
||||
|
||||
http.interceptors.request.use((req) => {
|
||||
if (!req.url?.endsWith('/resources')) {
|
||||
if (!req.url?.endsWith('/status')) {
|
||||
store.getActions().progress.startContinuous()
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ http.interceptors.request.use((req) => {
|
||||
|
||||
http.interceptors.response.use(
|
||||
(resp) => {
|
||||
if (!resp.request?.url?.endsWith('/resources')) {
|
||||
if (!resp.request?.url?.endsWith('/status')) {
|
||||
store.getActions().progress.setComplete()
|
||||
}
|
||||
|
||||
|
||||
18
resources/scripts/api/server/getStatus.ts
Normal file
18
resources/scripts/api/server/getStatus.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import http from '@/api/http';
|
||||
|
||||
export type ServerState = 'running' | 'stopped'
|
||||
|
||||
export interface ServerStatus {
|
||||
state: ServerState
|
||||
uptime: number
|
||||
cpu: number
|
||||
memory: number
|
||||
}
|
||||
|
||||
export default (uuid: string): Promise<ServerStatus> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}/status`)
|
||||
.then(({ data: { data } }) => resolve(data))
|
||||
.catch(reject)
|
||||
})
|
||||
};
|
||||
@@ -15,7 +15,7 @@ ${tw`dark:text-stone-400 text-stone-500 text-xs`}
|
||||
`
|
||||
|
||||
const Dd = styled.dt`
|
||||
${tw`text-sm font-semibold`}
|
||||
${tw`text-sm font-medium`}
|
||||
`
|
||||
|
||||
const ServerCard = ({ server }: Props) => {
|
||||
|
||||
@@ -98,7 +98,7 @@ const NavigationBar = ({ routes, breadcrumb }: Props) => {
|
||||
className='w-7 h-7 dark:invert'
|
||||
alt='Convoy logo'
|
||||
/>
|
||||
<h1 className='font-bold text-lg dark:text-white'>Convoy</h1>
|
||||
<h1 className='font-semibold text-lg dark:text-white'>Convoy</h1>
|
||||
</Link>
|
||||
{breadcrumb && (
|
||||
<>
|
||||
|
||||
18
resources/scripts/components/servers/ServerContentBlock.tsx
Normal file
18
resources/scripts/components/servers/ServerContentBlock.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Props extends PageContentBlockProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
|
||||
const name = ServerContext.useStoreState((state) => state.server.data!.name);
|
||||
|
||||
return (
|
||||
<PageContentBlock title={`${title} | ${name}`} {...props}>
|
||||
{children}
|
||||
</PageContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerContentBlock;
|
||||
@@ -0,0 +1,135 @@
|
||||
import { ServerContext } from '@/state/server'
|
||||
import { capitalize, convertTimeToSmallest, formatBytes } from '@/util/helpers'
|
||||
import styled from '@emotion/styled'
|
||||
import { Skeleton } from '@mantine/core'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import tw from 'twin.macro'
|
||||
|
||||
export const StatRow = styled.div`
|
||||
${tw`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 border border-[#eaeaea] dark:border-[#333333] shadow dark:shadow-none rounded-lg bg-white dark:bg-black`}
|
||||
|
||||
&>div {
|
||||
${tw`border-[#eaeaea] dark:border-[#333333] p-6`}
|
||||
}
|
||||
|
||||
& > div > p {
|
||||
${tw`font-semibold text-sm text-black dark:text-stone-400`}
|
||||
}
|
||||
|
||||
& > div:not(:last-child) {
|
||||
${tw`md:border-r border-[#eaeaea] dark:border-[#333333]`}
|
||||
}
|
||||
|
||||
& > div:nth-of-type(-n + 2) {
|
||||
${tw`border-b lg:border-b-0`}
|
||||
}
|
||||
|
||||
& > div:nth-of-type(2) {
|
||||
${tw`md:border-r-0 lg:border-r`}
|
||||
}
|
||||
|
||||
& > div:nth-of-type(3) {
|
||||
${tw`border-b md:border-b-0`}
|
||||
}
|
||||
|
||||
& > div > p:nth-of-type(2) {
|
||||
${tw`text-2xl font-semibold mt-1 dark:text-white`}
|
||||
}
|
||||
`
|
||||
|
||||
const ServerDetailsBlock = () => {
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid)
|
||||
const memoryLimit = ServerContext.useStoreState(
|
||||
(state) => state.server.data!.limits.memory
|
||||
)
|
||||
const status = ServerContext.useStoreState((state) => state.status.data)
|
||||
const getStatus = ServerContext.useStoreActions(
|
||||
(actions) => actions.status.getStatus
|
||||
)
|
||||
const isUpdating = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
isUpdating.current = true
|
||||
|
||||
const update = async () => {
|
||||
if (!isUpdating.current) return
|
||||
|
||||
await getStatus(uuid)
|
||||
|
||||
setTimeout(update, 1000)
|
||||
}
|
||||
|
||||
update()
|
||||
|
||||
return () => {
|
||||
isUpdating.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const memory = useMemo(
|
||||
() => ({
|
||||
used: formatBytes(status?.memory || 0),
|
||||
total: formatBytes(memoryLimit),
|
||||
}),
|
||||
[status]
|
||||
)
|
||||
|
||||
const uptime = useMemo(
|
||||
() => convertTimeToSmallest(status?.uptime || 0),
|
||||
[status]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!status ? (
|
||||
<Skeleton className='w-full !h-[421px] md:!h-[211px] lg:!h-[106px]' />
|
||||
) : (
|
||||
<StatRow>
|
||||
<div>
|
||||
<p>Server State</p>
|
||||
<div className='flex space-x-2 items-center mt-1'>
|
||||
<div className='grid place-items-center h-full'>
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full ${
|
||||
status.state === 'running' ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
<p className='text-2xl font-semibold dark:text-white'>
|
||||
{capitalize(status.state)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>CPU Usage</p>
|
||||
<p>{Math.floor(status.cpu * 100)}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Memory Usage</p>
|
||||
<div className='flex space-x-2 items-end mt-1'>
|
||||
<p className='text-2xl font-semibold dark:text-white'>
|
||||
{memory.used.size} {memory.used.unit}
|
||||
</p>
|
||||
<p className='text-sm font-semibold description mb-[0.3rem]'>
|
||||
/ {memory.total.size} {memory.total.unit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>Uptime</p>
|
||||
<div className='flex space-x-2 items-end mt-1'>
|
||||
<p className='text-2xl font-semibold dark:text-white'>
|
||||
{Math.floor(uptime.time)}
|
||||
</p>
|
||||
<p className='text-sm font-semibold description mb-[0.3rem]'>
|
||||
{uptime.unit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</StatRow>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerDetailsBlock
|
||||
@@ -1,9 +1,12 @@
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock'
|
||||
import ServerDetailsBlock from '@/components/servers/overview/ServerDetailsBlock'
|
||||
import ServerContentBlock from '@/components/servers/ServerContentBlock'
|
||||
|
||||
const ServerOverviewContainer = () => {
|
||||
return <PageContentBlock title='Overview'>
|
||||
<div>Overview</div>
|
||||
</PageContentBlock>
|
||||
return (
|
||||
<ServerContentBlock title='Overview'>
|
||||
<ServerDetailsBlock />
|
||||
</ServerContentBlock>
|
||||
)
|
||||
}
|
||||
|
||||
export default ServerOverviewContainer
|
||||
export default ServerOverviewContainer
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { httpErrorToHuman } from '@/api/http'
|
||||
import NavigationBar from '@/components/elements/navigation/NavigationBar'
|
||||
import { NotFound, ServerError } from '@/components/elements/ScreenBlock'
|
||||
import ScreenBlock, {
|
||||
NotFound,
|
||||
ServerError,
|
||||
} from '@/components/elements/ScreenBlock'
|
||||
import Spinner from '@/components/elements/Spinner'
|
||||
import routes from '@/routers/routes'
|
||||
import { ServerContext } from '@/state/server'
|
||||
import { useEffect, useState } from 'react'
|
||||
import {
|
||||
matchPath,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
useMatch,
|
||||
} from 'react-router-dom'
|
||||
import { ArrowPathIcon, NoSymbolIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
const navRoutes = [
|
||||
{
|
||||
@@ -21,7 +24,6 @@ const navRoutes = [
|
||||
]
|
||||
|
||||
const ServerRouter = () => {
|
||||
const location = useLocation()
|
||||
const match = useMatch('/servers/:id')
|
||||
const [error, setError] = useState<string>()
|
||||
const server = ServerContext.useStoreState((state) => state.server.data)
|
||||
@@ -55,17 +57,27 @@ const ServerRouter = () => {
|
||||
<Spinner />
|
||||
)
|
||||
) : (
|
||||
<Routes>
|
||||
{routes.server.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={<route.component />}
|
||||
/>
|
||||
))}
|
||||
<>
|
||||
{server.status === 'suspended' && (
|
||||
<ScreenBlock center icon={NoSymbolIcon} message='This server is suspended. Contact your provider or system administrator for help.' title='Suspended' />
|
||||
)}
|
||||
{server.status === 'installing' && (
|
||||
<ScreenBlock center icon={ArrowPathIcon} message='Your server is being reinstalled. This can take from 1-15 minutes.' title='Installing' />
|
||||
)}
|
||||
{server.status === null || server.status === undefined ? (
|
||||
<Routes>
|
||||
{routes.server.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={<route.component />}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route path={'*'} element={<NotFound full />} />
|
||||
</Routes>
|
||||
<Route path={'*'} element={<NotFound full />} />
|
||||
</Routes>
|
||||
) : ''}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import getServer, { Server } from '@/api/server/getServer'
|
||||
import getStatus, { ServerStatus } from '@/api/server/getStatus'
|
||||
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy'
|
||||
import isEqual from 'react-fast-compare'
|
||||
|
||||
export interface ServerDataStore {
|
||||
data?: Server
|
||||
@@ -10,7 +12,9 @@ export interface ServerDataStore {
|
||||
const server: ServerDataStore = {
|
||||
data: undefined,
|
||||
setServer: action((state, payload) => {
|
||||
state.data = payload
|
||||
if (!isEqual(payload, state.data)) {
|
||||
state.data = payload
|
||||
}
|
||||
}),
|
||||
getServer: thunk(async (actions, uuid) => {
|
||||
const server = await getServer(uuid)
|
||||
@@ -19,14 +23,38 @@ const server: ServerDataStore = {
|
||||
}),
|
||||
}
|
||||
|
||||
export interface ServerStatusStore {
|
||||
data?: ServerStatus
|
||||
getStatus: Thunk<ServerStatusStore, string>
|
||||
setStatus: Action<ServerStatusStore, ServerStatus>
|
||||
}
|
||||
|
||||
const status: ServerStatusStore = {
|
||||
data: undefined,
|
||||
getStatus: thunk(async (actions, uuid) => {
|
||||
const status = await getStatus(uuid)
|
||||
|
||||
actions.setStatus(status)
|
||||
}),
|
||||
|
||||
setStatus: action((state, payload) => {
|
||||
if (!isEqual(payload, state.data)) {
|
||||
state.data = payload
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
interface ServerStore {
|
||||
server: ServerDataStore
|
||||
status: ServerStatusStore
|
||||
clearServerState: Action<ServerStore>
|
||||
}
|
||||
|
||||
export const ServerContext = createContextStore<ServerStore>({
|
||||
server: server,
|
||||
server,
|
||||
status,
|
||||
clearServerState: action((state) => {
|
||||
state.server.data = undefined
|
||||
state.status.data = undefined
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -49,4 +49,28 @@ export function formatBytes(
|
||||
size,
|
||||
unit: sizes[i],
|
||||
}
|
||||
}
|
||||
|
||||
export const capitalize = (word: string) => {
|
||||
return word.charAt(0).toUpperCase() + word.slice(1)
|
||||
}
|
||||
|
||||
export const convertTimeToSmallest = (seconds: number) => {
|
||||
const units = [
|
||||
[1, 'seconds'],
|
||||
[60, 'minutes'],
|
||||
[60 * 60, 'hours'],
|
||||
[60 * 60 * 24, 'days'],
|
||||
]
|
||||
let bestUnit = units[0]
|
||||
for (const unit of units) {
|
||||
if (seconds >= unit[0]) {
|
||||
bestUnit = unit
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
time: seconds / (bestUnit[0] as number),
|
||||
unit: bestUnit[1] as string,
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
<link rel="icon" href="favicon.svg" sizes="any" type="image/svg+xml">
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
|
||||
<!-- Inject Data -->
|
||||
@if(!is_null(Auth::user()))
|
||||
|
||||
@@ -14,5 +14,7 @@ Route::group([
|
||||
AuthenticateServerAccess::class,
|
||||
],
|
||||
], function () {
|
||||
Route::get('/', [Client\Servers\ServerController::class, 'index']);
|
||||
Route::get('/', [Client\Servers\ServerController::class, 'index'])->name('api:client:servers.show');
|
||||
|
||||
Route::get('/status', [Client\Servers\ServerController::class, 'status']);
|
||||
});
|
||||
@@ -18,7 +18,7 @@ module.exports = {
|
||||
primary: '#111111'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Nunito', ...defaultTheme.fontFamily.sans],
|
||||
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user