Support modifying the primary allocation for a server
This commit is contained in:
parent
bfb28f949d
commit
fc9054312d
17 changed files with 230 additions and 87 deletions
|
@ -75,12 +75,15 @@ export interface FractalResponseData {
|
|||
object: string;
|
||||
attributes: {
|
||||
[k: string]: any;
|
||||
relationships?: {
|
||||
[k: string]: FractalResponseData;
|
||||
};
|
||||
relationships?: Record<string, FractalResponseData | FractalResponseList>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FractalResponseList {
|
||||
object: 'list';
|
||||
data: FractalResponseData[];
|
||||
}
|
||||
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
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 {
|
||||
ip: string;
|
||||
|
@ -35,7 +36,7 @@ export interface Server {
|
|||
isInstalling: boolean;
|
||||
}
|
||||
|
||||
export const rawDataToServerObject = (data: any): Server => ({
|
||||
export const rawDataToServerObject = ({ attributes: data }: FractalResponseData): Server => ({
|
||||
id: data.identifier,
|
||||
uuid: data.uuid,
|
||||
name: data.name,
|
||||
|
@ -45,23 +46,18 @@ export const rawDataToServerObject = (data: any): Server => ({
|
|||
port: data.sftp_details.port,
|
||||
},
|
||||
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 },
|
||||
featureLimits: { ...data.feature_limits },
|
||||
isSuspended: data.is_suspended,
|
||||
isInstalling: data.is_installing,
|
||||
allocations: ((data.relationships?.allocations as FractalResponseList | undefined)?.data || []).map(rawDataToServerAllocation),
|
||||
});
|
||||
|
||||
export default (uuid: string): Promise<[ Server, string[] ]> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(`/api/client/servers/${uuid}`)
|
||||
.then(({ data }) => resolve([
|
||||
rawDataToServerObject(data.attributes),
|
||||
rawDataToServerObject(data),
|
||||
// eslint-disable-next-line camelcase
|
||||
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) {
|
||||
${tw`border-neutral-500 text-neutral-100`};
|
||||
${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`};
|
||||
}
|
||||
`};
|
||||
|
|
|
@ -2,11 +2,15 @@ import React from 'react';
|
|||
import ContentContainer from '@/components/elements/ContentContainer';
|
||||
import { CSSTransition } from 'react-transition-group';
|
||||
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>
|
||||
<>
|
||||
<ContentContainer css={tw`my-10`} className={className}>
|
||||
{showFlashKey &&
|
||||
<FlashMessageRender byKey={showFlashKey} css={tw`mb-4`}/>
|
||||
}
|
||||
{children}
|
||||
</ContentContainer>
|
||||
<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 Label from '@/components/elements/Label';
|
||||
import { LinkButton } from '@/components/elements/Button';
|
||||
import ServerAllocationsContainer from '@/components/server/settings/ServerAllocationsContainer';
|
||||
|
||||
export default () => {
|
||||
const user = useStoreState<ApplicationStore, UserData>(state => state.user.data!);
|
||||
|
@ -61,6 +60,8 @@ export default () => {
|
|||
</div>
|
||||
</TitledGreyBox>
|
||||
</Can>
|
||||
</div>
|
||||
<div css={tw`w-full mt-6 md:flex-1 md:mt-0`}>
|
||||
<Can action={'settings.rename'}>
|
||||
<div css={tw`mb-6 md:mb-10`}>
|
||||
<RenameServerBox/>
|
||||
|
@ -70,9 +71,6 @@ export default () => {
|
|||
<ReinstallServerBox/>
|
||||
</Can>
|
||||
</div>
|
||||
<div css={tw`w-full mt-6 md:flex-1 md:mt-0`}>
|
||||
<ServerAllocationsContainer/>
|
||||
</div>
|
||||
</div>
|
||||
</PageContentBlock>
|
||||
);
|
||||
|
|
|
@ -24,6 +24,7 @@ import { useStoreState } from 'easy-peasy';
|
|||
import useServer from '@/plugins/useServer';
|
||||
import ScreenBlock from '@/components/screens/ScreenBlock';
|
||||
import SubNavigation from '@/components/elements/SubNavigation';
|
||||
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||
|
||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||
const { rootAdmin } = useStoreState(state => state.user.data!);
|
||||
|
@ -88,6 +89,9 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||
<Can action={'backup.*'}>
|
||||
<NavLink to={`${match.url}/backups`}>Backups</NavLink>
|
||||
</Can>
|
||||
<Can action={'allocations.*'}>
|
||||
<NavLink to={`${match.url}/network`}>Network</NavLink>
|
||||
</Can>
|
||||
<Can action={[ 'settings.*', 'file.sftp' ]} matchAny>
|
||||
<NavLink to={`${match.url}/settings`}>Settings</NavLink>
|
||||
</Can>
|
||||
|
@ -125,6 +129,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
|||
/>
|
||||
<Route path={`${match.path}/users`} component={UsersContainer} 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={'*'} component={NotFound}/>
|
||||
</Switch>
|
||||
|
|
|
@ -5,6 +5,7 @@ export interface FlashStore {
|
|||
items: FlashMessage[];
|
||||
addFlash: Action<FlashStore, FlashMessage>;
|
||||
addError: Action<FlashStore, { message: string; key?: string }>;
|
||||
clearAndAddError: Action<FlashStore, { message: string, key: string }>;
|
||||
clearFlashes: Action<FlashStore, string | void>;
|
||||
}
|
||||
|
||||
|
@ -18,12 +19,19 @@ export interface FlashMessage {
|
|||
|
||||
const flashes: FlashStore = {
|
||||
items: [],
|
||||
|
||||
addFlash: action((state, payload) => {
|
||||
state.items.push(payload);
|
||||
}),
|
||||
|
||||
addError: action((state, payload) => {
|
||||
state.items.push({ type: 'error', title: 'Error', ...payload });
|
||||
}),
|
||||
|
||||
clearAndAddError: action((state, payload) => {
|
||||
state.items = [ { type: 'error', title: 'Error', ...payload } ];
|
||||
}),
|
||||
|
||||
clearFlashes: action((state, payload) => {
|
||||
state.items = payload ? state.items.filter(flashes => flashes.key !== payload) : [];
|
||||
}),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue