mirror of
https://github.com/pterodactyl/panel.git
synced 2024-11-26 02:52:30 +01:00
Support modifying the primary allocation for a server
This commit is contained in:
parent
bfb28f949d
commit
fc9054312d
@ -3,10 +3,14 @@
|
|||||||
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
|
||||||
|
|
||||||
use Pterodactyl\Models\Server;
|
use Pterodactyl\Models\Server;
|
||||||
|
use Pterodactyl\Exceptions\DisplayException;
|
||||||
|
use Pterodactyl\Repositories\Eloquent\ServerRepository;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
|
use Pterodactyl\Repositories\Eloquent\AllocationRepository;
|
||||||
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
|
use Pterodactyl\Transformers\Api\Client\AllocationTransformer;
|
||||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||||
|
|
||||||
class NetworkController extends ClientApiController
|
class NetworkController extends ClientApiController
|
||||||
{
|
{
|
||||||
@ -15,16 +19,25 @@ class NetworkController extends ClientApiController
|
|||||||
*/
|
*/
|
||||||
private $repository;
|
private $repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Pterodactyl\Repositories\Eloquent\ServerRepository
|
||||||
|
*/
|
||||||
|
private $serverRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NetworkController constructor.
|
* NetworkController constructor.
|
||||||
*
|
*
|
||||||
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository
|
* @param \Pterodactyl\Repositories\Eloquent\AllocationRepository $repository
|
||||||
|
* @param \Pterodactyl\Repositories\Eloquent\ServerRepository $serverRepository
|
||||||
*/
|
*/
|
||||||
public function __construct(AllocationRepository $repository)
|
public function __construct(
|
||||||
{
|
AllocationRepository $repository,
|
||||||
|
ServerRepository $serverRepository
|
||||||
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->serverRepository = $serverRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,11 +50,40 @@ class NetworkController extends ClientApiController
|
|||||||
*/
|
*/
|
||||||
public function index(GetNetworkRequest $request, Server $server): array
|
public function index(GetNetworkRequest $request, Server $server): array
|
||||||
{
|
{
|
||||||
$allocations = $this->repository->findWhere([
|
return $this->fractal->collection($server->allocations)
|
||||||
['server_id', '=', $server->id],
|
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||||
]);
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
return $this->fractal->collection($allocations)
|
/**
|
||||||
|
* Set the primary allocation for a server.
|
||||||
|
*
|
||||||
|
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest $request
|
||||||
|
* @param \Pterodactyl\Models\Server $server
|
||||||
|
* @return array
|
||||||
|
*
|
||||||
|
* @throws \Pterodactyl\Exceptions\DisplayException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||||
|
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||||
|
*/
|
||||||
|
public function storePrimary(SetPrimaryAllocationRequest $request, Server $server): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
/** @var \Pterodactyl\Models\Allocation $allocation */
|
||||||
|
$allocation = $this->repository->findFirstWhere([
|
||||||
|
'server_id' => $server->id,
|
||||||
|
'ip' => $request->input('ip'),
|
||||||
|
'port' => $request->input('port'),
|
||||||
|
]);
|
||||||
|
} catch (ModelNotFoundException $exception) {
|
||||||
|
throw new DisplayException(
|
||||||
|
'The IP and port you selected are not available for this server.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->serverRepository->update($server->id, ['allocation_id' => $allocation->id]);
|
||||||
|
|
||||||
|
return $this->fractal->item($allocation)
|
||||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||||
->toArray();
|
->toArray();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Network;
|
||||||
|
|
||||||
|
use Pterodactyl\Models\Permission;
|
||||||
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
|
|
||||||
|
class SetPrimaryAllocationRequest extends ClientApiRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function permission(): string
|
||||||
|
{
|
||||||
|
return Permission::ACTION_ALLOCIATION_UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'ip' => 'required|string',
|
||||||
|
'port' => 'required|numeric|min:1024|max:65535',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,7 @@
|
|||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
"styled-components": "^5.1.1",
|
"styled-components": "^5.1.1",
|
||||||
"styled-components-breakpoint": "^3.0.0-preview.20",
|
"styled-components-breakpoint": "^3.0.0-preview.20",
|
||||||
|
"swr": "^0.2.3",
|
||||||
"uuid": "^3.3.2",
|
"uuid": "^3.3.2",
|
||||||
"xterm": "^3.14.4",
|
"xterm": "^3.14.4",
|
||||||
"xterm-addon-attach": "^0.1.0",
|
"xterm-addon-attach": "^0.1.0",
|
||||||
|
@ -75,12 +75,15 @@ export interface FractalResponseData {
|
|||||||
object: string;
|
object: string;
|
||||||
attributes: {
|
attributes: {
|
||||||
[k: string]: any;
|
[k: string]: any;
|
||||||
relationships?: {
|
relationships?: Record<string, FractalResponseData | FractalResponseList>;
|
||||||
[k: string]: FractalResponseData;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FractalResponseList {
|
||||||
|
object: 'list';
|
||||||
|
data: FractalResponseData[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface PaginatedResult<T> {
|
export interface PaginatedResult<T> {
|
||||||
items: T[];
|
items: T[];
|
||||||
pagination: PaginationDataSet;
|
pagination: PaginationDataSet;
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import http from '@/api/http';
|
import http, { FractalResponseData, FractalResponseList } from '@/api/http';
|
||||||
|
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||||
|
|
||||||
export interface Allocation {
|
export interface Allocation {
|
||||||
ip: string;
|
ip: string;
|
||||||
@ -35,7 +36,7 @@ export interface Server {
|
|||||||
isInstalling: boolean;
|
isInstalling: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const rawDataToServerObject = (data: any): Server => ({
|
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
||||||
id: data.identifier,
|
id: data.identifier,
|
||||||
uuid: data.uuid,
|
uuid: data.uuid,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@ -45,23 +46,18 @@ export const rawDataToServerObject = (data: any): Server => ({
|
|||||||
port: data.sftp_details.port,
|
port: data.sftp_details.port,
|
||||||
},
|
},
|
||||||
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
description: data.description ? ((data.description.length > 0) ? data.description : null) : null,
|
||||||
allocations: (data.allocations || []).map((datum: any) => ({
|
|
||||||
ip: datum.ip,
|
|
||||||
alias: datum.ip_alias,
|
|
||||||
port: datum.port,
|
|
||||||
isDefault: datum.is_default,
|
|
||||||
})),
|
|
||||||
limits: { ...data.limits },
|
limits: { ...data.limits },
|
||||||
featureLimits: { ...data.feature_limits },
|
featureLimits: { ...data.feature_limits },
|
||||||
isSuspended: data.is_suspended,
|
isSuspended: data.is_suspended,
|
||||||
isInstalling: data.is_installing,
|
isInstalling: data.is_installing,
|
||||||
|
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
http.get(`/api/client/servers/${uuid}`)
|
http.get(`/api/client/servers/${uuid}`)
|
||||||
.then(({ data }) => resolve([
|
.then(({ data }) => resolve([
|
||||||
rawDataToServerObject(data.attributes),
|
rawDataToServerObject(data),
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
data.meta?.is_server_owner ? [ '*' ] : (data.meta?.user_permissions || []),
|
data.meta?.is_server_owner ? [ '*' ] : (data.meta?.user_permissions || []),
|
||||||
]))
|
]))
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
import http from '@/api/http';
|
||||||
|
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
|
||||||
|
export default async (uuid: string): Promise<Allocation[]> => {
|
||||||
|
const { data } = await http.get(`/api/client/servers/${uuid}/network`);
|
||||||
|
|
||||||
|
return (data.data || []).map(rawDataToServerAllocation);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
import http from '@/api/http';
|
||||||
|
import { rawDataToServerAllocation } from '@/api/transformers';
|
||||||
|
|
||||||
|
export default async (uuid: string, ip: string, port: number): Promise<Allocation> => {
|
||||||
|
const { data } = await http.put(`/api/client/servers/${uuid}/network/primary`, { ip, port });
|
||||||
|
|
||||||
|
return rawDataToServerAllocation(data);
|
||||||
|
};
|
9
resources/scripts/api/transformers.ts
Normal file
9
resources/scripts/api/transformers.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
import { FractalResponseData } from '@/api/http';
|
||||||
|
|
||||||
|
export const rawDataToServerAllocation = (data: FractalResponseData): Allocation => ({
|
||||||
|
ip: data.attributes.ip,
|
||||||
|
alias: data.attributes.ip_alias,
|
||||||
|
port: data.attributes.port,
|
||||||
|
isDefault: data.attributes.is_default,
|
||||||
|
});
|
@ -68,6 +68,7 @@ const ButtonStyle = styled.button<Omit<Props, 'isLoading'>>`
|
|||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
${tw`border-neutral-500 text-neutral-100`};
|
${tw`border-neutral-500 text-neutral-100`};
|
||||||
${props => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
${props => props.color === 'red' && tw`bg-red-500 border-red-600 text-red-50`};
|
||||||
|
${props => props.color === 'primary' && tw`bg-primary-500 border-primary-600 text-primary-50`};
|
||||||
${props => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
${props => props.color === 'green' && tw`bg-green-500 border-green-600 text-green-50`};
|
||||||
}
|
}
|
||||||
`};
|
`};
|
||||||
|
@ -2,11 +2,15 @@ import React from 'react';
|
|||||||
import ContentContainer from '@/components/elements/ContentContainer';
|
import ContentContainer from '@/components/elements/ContentContainer';
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
|
||||||
const PageContentBlock: React.FC<{ className?: string }> = ({ children, className }) => (
|
const PageContentBlock: React.FC<{ showFlashKey?: string; className?: string }> = ({ children, showFlashKey, className }) => (
|
||||||
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
||||||
<>
|
<>
|
||||||
<ContentContainer css={tw`my-10`} className={className}>
|
<ContentContainer css={tw`my-10`} className={className}>
|
||||||
|
{showFlashKey &&
|
||||||
|
<FlashMessageRender byKey={showFlashKey} css={tw`mb-4`}/>
|
||||||
|
}
|
||||||
{children}
|
{children}
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
<ContentContainer css={tw`mb-4`}>
|
<ContentContainer css={tw`mb-4`}>
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import tw from 'twin.macro';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||||
|
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||||
|
import Button from '@/components/elements/Button';
|
||||||
|
import Can from '@/components/elements/Can';
|
||||||
|
import useServer from '@/plugins/useServer';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import getServerAllocations from '@/api/server/network/getServerAllocations';
|
||||||
|
import { Allocation } from '@/api/server/getServer';
|
||||||
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation';
|
||||||
|
import useFlash from '@/plugins/useFlash';
|
||||||
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
|
||||||
|
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm block`}`;
|
||||||
|
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||||
|
|
||||||
|
const NetworkContainer = () => {
|
||||||
|
const server = useServer();
|
||||||
|
const { clearFlashes, clearAndAddError } = useFlash();
|
||||||
|
const { data, error, mutate } = useSWR<Allocation[]>(server.uuid, key => getServerAllocations(key), { initialData: server.allocations });
|
||||||
|
|
||||||
|
const setPrimaryAllocation = (ip: string, port: number) => {
|
||||||
|
clearFlashes('server:network');
|
||||||
|
|
||||||
|
mutate(data?.map(a => (a.ip === ip && a.port === port) ? { ...a, isDefault: true } : { ...a, isDefault: false }), false);
|
||||||
|
|
||||||
|
setPrimaryServerAllocation(server.uuid, ip, port)
|
||||||
|
.catch(error => clearAndAddError({ key: 'server:network', message: httpErrorToHuman(error) }));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
clearAndAddError({ key: 'server:network', message: error });
|
||||||
|
}
|
||||||
|
}, [ error ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContentBlock showFlashKey={'server:network'}>
|
||||||
|
{!data ?
|
||||||
|
<Spinner size={'large'} centered/>
|
||||||
|
:
|
||||||
|
data.map(({ ip, port, alias, isDefault }, index) => (
|
||||||
|
<GreyRowBox key={`${ip}:${port}`} css={index > 0 ? tw`mt-2` : undefined}>
|
||||||
|
<div css={tw`pl-4 pr-6 text-neutral-400`}>
|
||||||
|
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||||
|
</div>
|
||||||
|
<div css={tw`mr-4`}>
|
||||||
|
<Code>{alias || ip}</Code>
|
||||||
|
<Label>IP Address</Label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Code>:{port}</Code>
|
||||||
|
<Label>Port</Label>
|
||||||
|
</div>
|
||||||
|
<div css={tw`flex-1 text-right`}>
|
||||||
|
{isDefault ?
|
||||||
|
<span css={tw`bg-green-500 py-1 px-2 rounded text-green-50 text-xs`}>
|
||||||
|
Primary
|
||||||
|
</span>
|
||||||
|
:
|
||||||
|
<Can action={'allocations.update'}>
|
||||||
|
<Button
|
||||||
|
isSecondary
|
||||||
|
size={'xsmall'}
|
||||||
|
color={'primary'}
|
||||||
|
onClick={() => setPrimaryAllocation(ip, port)}
|
||||||
|
>
|
||||||
|
Make Primary
|
||||||
|
</Button>
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</GreyRowBox>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</PageContentBlock>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkContainer;
|
@ -1,63 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import tw from 'twin.macro';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import styled from 'styled-components/macro';
|
|
||||||
|
|
||||||
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm block`}`;
|
|
||||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
|
||||||
|
|
||||||
const Row = styled.div`
|
|
||||||
${tw`flex items-center py-2 pl-4 pr-5 border-l-4 border-transparent transition-colors duration-150`};
|
|
||||||
|
|
||||||
& svg {
|
|
||||||
${tw`transition-colors duration-150`};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
${tw`border-cyan-400`};
|
|
||||||
|
|
||||||
svg {
|
|
||||||
${tw`text-neutral-100`};
|
|
||||||
}
|
|
||||||
|
|
||||||
${Label} {
|
|
||||||
${tw`text-neutral-200`};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const allocations = ServerContext.useStoreState(state => state.server.data!.allocations);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TitledGreyBox title={'Allocated Ports'}>
|
|
||||||
{allocations.map(({ ip, port, alias, isDefault }, index) => (
|
|
||||||
<Row key={`${ip}:${port}`} css={index > 0 ? tw`mt-2` : undefined}>
|
|
||||||
<div css={tw`mr-4 text-neutral-400`}>
|
|
||||||
<FontAwesomeIcon icon={faNetworkWired}/>
|
|
||||||
</div>
|
|
||||||
<div css={tw`mr-4`}>
|
|
||||||
<Code>{alias || ip}</Code>
|
|
||||||
<Label>IP Address</Label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Code>:{port}</Code>
|
|
||||||
<Label>Port</Label>
|
|
||||||
</div>
|
|
||||||
<div css={tw`flex-1 text-right`}>
|
|
||||||
{isDefault ?
|
|
||||||
<span css={tw`bg-green-500 py-1 px-2 rounded text-green-50 text-xs`}>
|
|
||||||
Default
|
|
||||||
</span>
|
|
||||||
:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Row>
|
|
||||||
))}
|
|
||||||
</TitledGreyBox>
|
|
||||||
);
|
|
||||||
};
|
|
@ -13,7 +13,6 @@ import tw from 'twin.macro';
|
|||||||
import Input from '@/components/elements/Input';
|
import Input from '@/components/elements/Input';
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
import { LinkButton } from '@/components/elements/Button';
|
import { LinkButton } from '@/components/elements/Button';
|
||||||
import ServerAllocationsContainer from '@/components/server/settings/ServerAllocationsContainer';
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const user = useStoreState<ApplicationStore, UserData>(state => state.user.data!);
|
const user = useStoreState<ApplicationStore, UserData>(state => state.user.data!);
|
||||||
@ -61,6 +60,8 @@ export default () => {
|
|||||||
</div>
|
</div>
|
||||||
</TitledGreyBox>
|
</TitledGreyBox>
|
||||||
</Can>
|
</Can>
|
||||||
|
</div>
|
||||||
|
<div css={tw`w-full mt-6 md:flex-1 md:mt-0`}>
|
||||||
<Can action={'settings.rename'}>
|
<Can action={'settings.rename'}>
|
||||||
<div css={tw`mb-6 md:mb-10`}>
|
<div css={tw`mb-6 md:mb-10`}>
|
||||||
<RenameServerBox/>
|
<RenameServerBox/>
|
||||||
@ -70,9 +71,6 @@ export default () => {
|
|||||||
<ReinstallServerBox/>
|
<ReinstallServerBox/>
|
||||||
</Can>
|
</Can>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`w-full mt-6 md:flex-1 md:mt-0`}>
|
|
||||||
<ServerAllocationsContainer/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</PageContentBlock>
|
</PageContentBlock>
|
||||||
);
|
);
|
||||||
|
@ -24,6 +24,7 @@ import { useStoreState } from 'easy-peasy';
|
|||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
import ScreenBlock from '@/components/screens/ScreenBlock';
|
import ScreenBlock from '@/components/screens/ScreenBlock';
|
||||||
import SubNavigation from '@/components/elements/SubNavigation';
|
import SubNavigation from '@/components/elements/SubNavigation';
|
||||||
|
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||||
|
|
||||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||||
const { rootAdmin } = useStoreState(state => state.user.data!);
|
const { rootAdmin } = useStoreState(state => state.user.data!);
|
||||||
@ -88,6 +89,9 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||||||
<Can action={'backup.*'}>
|
<Can action={'backup.*'}>
|
||||||
<NavLink to={`${match.url}/backups`}>Backups</NavLink>
|
<NavLink to={`${match.url}/backups`}>Backups</NavLink>
|
||||||
</Can>
|
</Can>
|
||||||
|
<Can action={'allocations.*'}>
|
||||||
|
<NavLink to={`${match.url}/network`}>Network</NavLink>
|
||||||
|
</Can>
|
||||||
<Can action={[ 'settings.*', 'file.sftp' ]} matchAny>
|
<Can action={[ 'settings.*', 'file.sftp' ]} matchAny>
|
||||||
<NavLink to={`${match.url}/settings`}>Settings</NavLink>
|
<NavLink to={`${match.url}/settings`}>Settings</NavLink>
|
||||||
</Can>
|
</Can>
|
||||||
@ -125,6 +129,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||||||
/>
|
/>
|
||||||
<Route path={`${match.path}/users`} component={UsersContainer} exact/>
|
<Route path={`${match.path}/users`} component={UsersContainer} exact/>
|
||||||
<Route path={`${match.path}/backups`} component={BackupContainer} exact/>
|
<Route path={`${match.path}/backups`} component={BackupContainer} exact/>
|
||||||
|
<Route path={`${match.path}/network`} component={NetworkContainer} exact/>
|
||||||
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/>
|
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/>
|
||||||
<Route path={'*'} component={NotFound}/>
|
<Route path={'*'} component={NotFound}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
@ -5,6 +5,7 @@ export interface FlashStore {
|
|||||||
items: FlashMessage[];
|
items: FlashMessage[];
|
||||||
addFlash: Action<FlashStore, FlashMessage>;
|
addFlash: Action<FlashStore, FlashMessage>;
|
||||||
addError: Action<FlashStore, { message: string; key?: string }>;
|
addError: Action<FlashStore, { message: string; key?: string }>;
|
||||||
|
clearAndAddError: Action<FlashStore, { message: string, key: string }>;
|
||||||
clearFlashes: Action<FlashStore, string | void>;
|
clearFlashes: Action<FlashStore, string | void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -18,12 +19,19 @@ export interface FlashMessage {
|
|||||||
|
|
||||||
const flashes: FlashStore = {
|
const flashes: FlashStore = {
|
||||||
items: [],
|
items: [],
|
||||||
|
|
||||||
addFlash: action((state, payload) => {
|
addFlash: action((state, payload) => {
|
||||||
state.items.push(payload);
|
state.items.push(payload);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
addError: action((state, payload) => {
|
addError: action((state, payload) => {
|
||||||
state.items.push({ type: 'error', title: 'Error', ...payload });
|
state.items.push({ type: 'error', title: 'Error', ...payload });
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
clearAndAddError: action((state, payload) => {
|
||||||
|
state.items = [ { type: 'error', title: 'Error', ...payload } ];
|
||||||
|
}),
|
||||||
|
|
||||||
clearFlashes: action((state, payload) => {
|
clearFlashes: action((state, payload) => {
|
||||||
state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : [];
|
state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : [];
|
||||||
}),
|
}),
|
||||||
|
@ -76,6 +76,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
|
|||||||
|
|
||||||
Route::group(['prefix' => '/network'], function () {
|
Route::group(['prefix' => '/network'], function () {
|
||||||
Route::get('/', 'Servers\NetworkController@index');
|
Route::get('/', 'Servers\NetworkController@index');
|
||||||
|
Route::put('/primary', 'Servers\NetworkController@storePrimary');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::group(['prefix' => '/users'], function () {
|
Route::group(['prefix' => '/users'], function () {
|
||||||
|
@ -3113,7 +3113,7 @@ extglob@^2.0.4:
|
|||||||
snapdragon "^0.8.1"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.1"
|
to-regex "^3.0.1"
|
||||||
|
|
||||||
fast-deep-equal@^2.0.1:
|
fast-deep-equal@2.0.1, fast-deep-equal@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49"
|
||||||
|
|
||||||
@ -6534,6 +6534,13 @@ svg-url-loader@^6.0.0:
|
|||||||
file-loader "~6.0.0"
|
file-loader "~6.0.0"
|
||||||
loader-utils "~2.0.0"
|
loader-utils "~2.0.0"
|
||||||
|
|
||||||
|
swr@^0.2.3:
|
||||||
|
version "0.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/swr/-/swr-0.2.3.tgz#e0fb260d27f12fafa2388312083368f45127480d"
|
||||||
|
integrity sha512-JhuuD5ojqgjAQpZAhoPBd8Di0Mr1+ykByVKuRJdtKaxkUX/y8kMACWKkLgLQc8pcDOKEAnbIreNjU7HfqI9nHQ==
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal "2.0.1"
|
||||||
|
|
||||||
symbol-observable@^1.2.0:
|
symbol-observable@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||||
|
Loading…
Reference in New Issue
Block a user