Start cleaning up the mess of useServer; make startup page update in real time
This commit is contained in:
parent
179885b546
commit
c4418640eb
16 changed files with 175 additions and 61 deletions
|
@ -2,8 +2,8 @@ import http from '@/api/http';
|
||||||
import { ServerEggVariable } from '@/api/server/types';
|
import { ServerEggVariable } from '@/api/server/types';
|
||||||
import { rawDataToServerEggVariable } from '@/api/transformers';
|
import { rawDataToServerEggVariable } from '@/api/transformers';
|
||||||
|
|
||||||
export default async (uuid: string, key: string, value: string): Promise<ServerEggVariable> => {
|
export default async (uuid: string, key: string, value: string): Promise<[ ServerEggVariable, string ]> => {
|
||||||
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
|
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
|
||||||
|
|
||||||
return rawDataToServerEggVariable(data);
|
return [ rawDataToServerEggVariable(data), data.meta.startup_command ];
|
||||||
};
|
};
|
||||||
|
|
18
resources/scripts/api/swr/getServerStartup.ts
Normal file
18
resources/scripts/api/swr/getServerStartup.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import useSWR from 'swr';
|
||||||
|
import http, { FractalResponseList } from '@/api/http';
|
||||||
|
import { rawDataToServerEggVariable } from '@/api/transformers';
|
||||||
|
import { ServerEggVariable } from '@/api/server/types';
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
invocation: string;
|
||||||
|
variables: ServerEggVariable[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
|
||||||
|
console.log('firing getServerStartup');
|
||||||
|
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
|
||||||
|
|
||||||
|
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
|
||||||
|
|
||||||
|
return { invocation: data.meta.startup_command, variables };
|
||||||
|
}, { initialData, errorRetryCount: 3 });
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React, { memo } from 'react';
|
||||||
import { usePermissions } from '@/plugins/usePermissions';
|
import { usePermissions } from '@/plugins/usePermissions';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
action: string | string[];
|
action: string | string[];
|
||||||
|
@ -23,4 +24,4 @@ const Can = ({ action, matchAny = false, renderOnError, children }: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Can;
|
export default memo(Can, isEqual);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import useServer from '@/plugins/useServer';
|
|
||||||
import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock';
|
import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
|
||||||
interface Props extends PageContentBlockProps {
|
interface Props extends PageContentBlockProps {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
|
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
|
||||||
const { name } = useServer();
|
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentBlock title={`${name} | ${title}`} {...props}>
|
<PageContentBlock title={`${name} | ${title}`} {...props}>
|
||||||
|
|
|
@ -45,4 +45,10 @@ const Spinner = ({ centered, ...props }: Props) => (
|
||||||
);
|
);
|
||||||
Spinner.DisplayName = 'Spinner';
|
Spinner.DisplayName = 'Spinner';
|
||||||
|
|
||||||
|
Spinner.Size = {
|
||||||
|
SMALL: 'small' as SpinnerSize,
|
||||||
|
BASE: 'base' as SpinnerSize,
|
||||||
|
LARGE: 'large' as SpinnerSize,
|
||||||
|
};
|
||||||
|
|
||||||
export default Spinner;
|
export default Spinner;
|
||||||
|
|
|
@ -1,26 +1,64 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||||
import useServer from '@/plugins/useServer';
|
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import VariableBox from '@/components/server/startup/VariableBox';
|
import VariableBox from '@/components/server/startup/VariableBox';
|
||||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||||
|
import getServerStartup from '@/api/swr/getServerStartup';
|
||||||
|
import Spinner from '@/components/elements/Spinner';
|
||||||
|
import ServerError from '@/components/screens/ServerError';
|
||||||
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
||||||
|
|
||||||
const StartupContainer = () => {
|
const StartupContainer = () => {
|
||||||
const { invocation, variables } = useServer();
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
|
const invocation = ServerContext.useStoreState(state => state.server.data!.invocation);
|
||||||
|
const variables = ServerContext.useStoreState(state => state.server.data!.variables);
|
||||||
|
|
||||||
|
const { data, error, isValidating, mutate } = getServerStartup(uuid, { invocation, variables });
|
||||||
|
|
||||||
|
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Since we're passing in initial data this will not trigger on mount automatically. We
|
||||||
|
// want to always fetch fresh information from the API however when we're loading the startup
|
||||||
|
// information.
|
||||||
|
mutate();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useDeepCompareEffect(() => {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
setServerFromState(s => ({
|
||||||
|
...s,
|
||||||
|
invocation: data.invocation,
|
||||||
|
variables: data.variables,
|
||||||
|
}));
|
||||||
|
}, [ data ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ServerContentBlock title={'Startup Settings'}>
|
!data ?
|
||||||
<TitledGreyBox title={'Startup Command'}>
|
(!error || (error && isValidating)) ?
|
||||||
<div css={tw`px-1 py-2`}>
|
<Spinner centered size={Spinner.Size.LARGE}/>
|
||||||
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
|
:
|
||||||
{invocation}
|
<ServerError
|
||||||
</p>
|
title={'Oops!'}
|
||||||
|
message={httpErrorToHuman(error)}
|
||||||
|
onRetry={() => mutate()}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<ServerContentBlock title={'Startup Settings'}>
|
||||||
|
<TitledGreyBox title={'Startup Command'}>
|
||||||
|
<div css={tw`px-1 py-2`}>
|
||||||
|
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
|
||||||
|
{data.invocation}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TitledGreyBox>
|
||||||
|
<div css={tw`grid gap-8 grid-cols-2 mt-10`}>
|
||||||
|
{data.variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
|
||||||
</div>
|
</div>
|
||||||
</TitledGreyBox>
|
</ServerContentBlock>
|
||||||
<div css={tw`grid gap-8 grid-cols-2 mt-10`}>
|
|
||||||
{variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
|
|
||||||
</div>
|
|
||||||
</ServerContentBlock>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { memo, useState } from 'react';
|
||||||
import { ServerEggVariable } from '@/api/server/types';
|
import { ServerEggVariable } from '@/api/server/types';
|
||||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||||
import { usePermissions } from '@/plugins/usePermissions';
|
import { usePermissions } from '@/plugins/usePermissions';
|
||||||
|
@ -8,9 +8,11 @@ import tw from 'twin.macro';
|
||||||
import { debounce } from 'debounce';
|
import { debounce } from 'debounce';
|
||||||
import updateStartupVariable from '@/api/server/updateStartupVariable';
|
import updateStartupVariable from '@/api/server/updateStartupVariable';
|
||||||
import useServer from '@/plugins/useServer';
|
import useServer from '@/plugins/useServer';
|
||||||
import { ServerContext } from '@/state/server';
|
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
|
import getServerStartup from '@/api/swr/getServerStartup';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
variable: ServerEggVariable;
|
variable: ServerEggVariable;
|
||||||
|
@ -19,22 +21,21 @@ interface Props {
|
||||||
const VariableBox = ({ variable }: Props) => {
|
const VariableBox = ({ variable }: Props) => {
|
||||||
const FLASH_KEY = `server:startup:${variable.envVariable}`;
|
const FLASH_KEY = `server:startup:${variable.envVariable}`;
|
||||||
|
|
||||||
const server = useServer();
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
const [ loading, setLoading ] = useState(false);
|
const [ loading, setLoading ] = useState(false);
|
||||||
const [ canEdit ] = usePermissions([ 'startup.update' ]);
|
const [ canEdit ] = usePermissions([ 'startup.update' ]);
|
||||||
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
const { clearFlashes, clearAndAddHttpError } = useFlash();
|
||||||
|
const { mutate } = getServerStartup(uuid);
|
||||||
const setServer = ServerContext.useStoreActions(actions => actions.server.setServer);
|
|
||||||
|
|
||||||
const setVariableValue = debounce((value: string) => {
|
const setVariableValue = debounce((value: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
clearFlashes(FLASH_KEY);
|
clearFlashes(FLASH_KEY);
|
||||||
|
|
||||||
updateStartupVariable(server.uuid, variable.envVariable, value)
|
updateStartupVariable(uuid, variable.envVariable, value)
|
||||||
.then(response => setServer({
|
.then(([ response, invocation ]) => mutate(data => ({
|
||||||
...server,
|
invocation,
|
||||||
variables: server.variables.map(v => v.envVariable === response.envVariable ? response : v),
|
variables: data.variables.map(v => v.envVariable === response.envVariable ? response : v),
|
||||||
}))
|
}), false))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
clearAndAddHttpError({ error, key: FLASH_KEY });
|
clearAndAddHttpError({ error, key: FLASH_KEY });
|
||||||
|
@ -74,4 +75,4 @@ const VariableBox = ({ variable }: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default VariableBox;
|
export default memo(VariableBox, isEqual);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { httpErrorToHuman } from '@/api/http';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
import { usePermissions } from '@/plugins/usePermissions';
|
import { usePermissions } from '@/plugins/usePermissions';
|
||||||
import { useDeepMemo } from '@/plugins/useDeepMemo';
|
import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import Label from '@/components/elements/Label';
|
import Label from '@/components/elements/Label';
|
||||||
|
@ -63,7 +63,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
|
||||||
const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions);
|
const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions);
|
||||||
|
|
||||||
// The permissions that can be modified by this user.
|
// The permissions that can be modified by this user.
|
||||||
const editablePermissions = useDeepMemo(() => {
|
const editablePermissions = useDeepCompareMemo(() => {
|
||||||
const cleaned = Object.keys(permissions)
|
const cleaned = Object.keys(permissions)
|
||||||
.map(key => Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`));
|
.map(key => Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`));
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Can from '@/components/elements/Can';
|
import Can from '@/components/elements/Can';
|
||||||
import ScreenBlock from '@/components/screens/ScreenBlock';
|
import ScreenBlock from '@/components/screens/ScreenBlock';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
const requireServerPermission = (Component: React.ComponentType<any>, permissions: string | string[]) => {
|
const requireServerPermission = (Component: React.ComponentType<any>, permissions: string | string[]) => {
|
||||||
return class extends React.Component<any, any> {
|
return class extends React.Component<any, any> {
|
||||||
|
shouldComponentUpdate (nextProps: Readonly<any>) {
|
||||||
|
return !isEqual(nextProps, this.props);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
<Can
|
<Can
|
||||||
|
|
5
resources/scripts/plugins/useDeepCompareEffect.ts
Normal file
5
resources/scripts/plugins/useDeepCompareEffect.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { DependencyList, EffectCallback, useEffect } from 'react';
|
||||||
|
import { useDeepMemoize } from './useDeepMemoize';
|
||||||
|
|
||||||
|
export const useDeepCompareEffect = (callback: EffectCallback, dependencies: DependencyList) =>
|
||||||
|
useEffect(callback, useDeepMemoize(dependencies));
|
5
resources/scripts/plugins/useDeepCompareMemo.ts
Normal file
5
resources/scripts/plugins/useDeepCompareMemo.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { DependencyList, useMemo } from 'react';
|
||||||
|
import { useDeepMemoize } from '@/plugins/useDeepMemoize';
|
||||||
|
|
||||||
|
export const useDeepCompareMemo = <T> (callback: () => T, dependencies: DependencyList) =>
|
||||||
|
useMemo(callback, useDeepMemoize(dependencies));
|
|
@ -1,12 +0,0 @@
|
||||||
import { useRef } from 'react';
|
|
||||||
import isEqual from 'react-fast-compare';
|
|
||||||
|
|
||||||
export const useDeepMemo = <T, K> (fn: () => T, key: K): T => {
|
|
||||||
const ref = useRef<{ key: K, value: T }>();
|
|
||||||
|
|
||||||
if (!ref.current || !isEqual(key, ref.current.key)) {
|
|
||||||
ref.current = { key, value: fn() };
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref.current.value;
|
|
||||||
};
|
|
12
resources/scripts/plugins/useDeepMemoize.ts
Normal file
12
resources/scripts/plugins/useDeepMemoize.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { DependencyList, MutableRefObject, useRef } from 'react';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
|
export const useDeepMemoize = <T = DependencyList> (value: T): T => {
|
||||||
|
const ref: MutableRefObject<T | undefined> = useRef();
|
||||||
|
|
||||||
|
if (!isEqual(value, ref.current)) {
|
||||||
|
ref.current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current as T;
|
||||||
|
};
|
|
@ -1,10 +1,10 @@
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import { useDeepMemo } from '@/plugins/useDeepMemo';
|
import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo';
|
||||||
|
|
||||||
export const usePermissions = (action: string | string[]): boolean[] => {
|
export const usePermissions = (action: string | string[]): boolean[] => {
|
||||||
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
|
const userPermissions = ServerContext.useStoreState(state => state.server.permissions);
|
||||||
|
|
||||||
return useDeepMemo(() => {
|
return useDeepCompareMemo(() => {
|
||||||
if (userPermissions[0] === '*') {
|
if (userPermissions[0] === '*') {
|
||||||
return Array(Array.isArray(action) ? action.length : 1).fill(true);
|
return Array(Array.isArray(action) ? action.length : 1).fill(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ import ServerError from '@/components/screens/ServerError';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import NotFound from '@/components/screens/NotFound';
|
import NotFound from '@/components/screens/NotFound';
|
||||||
import { useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
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';
|
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||||
|
@ -34,7 +33,10 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
const { rootAdmin } = useStoreState(state => state.user.data!);
|
const { rootAdmin } = useStoreState(state => state.user.data!);
|
||||||
const [ error, setError ] = useState('');
|
const [ error, setError ] = useState('');
|
||||||
const [ installing, setInstalling ] = useState(false);
|
const [ installing, setInstalling ] = useState(false);
|
||||||
const server = useServer();
|
|
||||||
|
const id = ServerContext.useStoreState(state => state.server.data?.id);
|
||||||
|
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
||||||
|
const isInstalling = ServerContext.useStoreState(state => state.server.data?.isInstalling);
|
||||||
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
|
||||||
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
|
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
|
||||||
|
|
||||||
|
@ -43,8 +45,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInstalling(server?.isInstalling !== false);
|
setInstalling(!!isInstalling);
|
||||||
}, [ server?.isInstalling ]);
|
}, [ isInstalling ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setError('');
|
setError('');
|
||||||
|
@ -71,7 +73,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={'server-router'}>
|
<React.Fragment key={'server-router'}>
|
||||||
<NavigationBar/>
|
<NavigationBar/>
|
||||||
{!server ?
|
{(!uuid || !id) ?
|
||||||
error ?
|
error ?
|
||||||
<ServerError message={error}/>
|
<ServerError message={error}/>
|
||||||
:
|
:
|
||||||
|
@ -111,7 +113,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
<InstallListener/>
|
<InstallListener/>
|
||||||
<WebsocketHandler/>
|
<WebsocketHandler/>
|
||||||
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${server.id}`)))) ?
|
{(installing && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
|
||||||
<ScreenBlock
|
<ScreenBlock
|
||||||
title={'Your server is installing.'}
|
title={'Your server is installing.'}
|
||||||
image={'/assets/svgs/server_installing.svg'}
|
image={'/assets/svgs/server_installing.svg'}
|
||||||
|
@ -136,17 +138,37 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
|
||||||
)}
|
)}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Route path={`${match.path}/databases`} component={requireServerPermission(DatabasesContainer, 'database.*')} exact/>
|
<Route
|
||||||
<Route path={`${match.path}/schedules`} component={requireServerPermission(ScheduleContainer, 'schedule.*')} exact/>
|
path={`${match.path}/databases`}
|
||||||
|
component={requireServerPermission(DatabasesContainer, 'database.*')}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}/schedules`}
|
||||||
|
component={requireServerPermission(ScheduleContainer, 'schedule.*')}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={`${match.path}/schedules/:id`}
|
path={`${match.path}/schedules/:id`}
|
||||||
component={ScheduleEditContainer}
|
component={ScheduleEditContainer}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
<Route path={`${match.path}/users`} component={requireServerPermission(UsersContainer, 'user.*')} exact/>
|
<Route
|
||||||
<Route path={`${match.path}/backups`} component={requireServerPermission(BackupContainer, 'backup.*')} exact/>
|
path={`${match.path}/users`}
|
||||||
<Route path={`${match.path}/network`} component={requireServerPermission(NetworkContainer, 'allocation.*')} exact/>
|
component={requireServerPermission(UsersContainer, 'user.*')}
|
||||||
<Route path={`${match.path}/startup`} component={requireServerPermission(StartupContainer, 'startup.*')} exact/>
|
exact
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}/backups`}
|
||||||
|
component={requireServerPermission(BackupContainer, 'backup.*')}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={`${match.path}/network`}
|
||||||
|
component={requireServerPermission(NetworkContainer, 'allocation.*')}
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
<Route path={`${match.path}/startup`} component={StartupContainer} 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>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import subusers, { ServerSubuserStore } from '@/state/server/subusers';
|
||||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||||
import schedules, { ServerScheduleStore } from '@/state/server/schedules';
|
import schedules, { ServerScheduleStore } from '@/state/server/schedules';
|
||||||
import databases, { ServerDatabaseStore } from '@/state/server/databases';
|
import databases, { ServerDatabaseStore } from '@/state/server/databases';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
|
export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running';
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ interface ServerDataStore {
|
||||||
|
|
||||||
getServer: Thunk<ServerDataStore, string, Record<string, unknown>, ServerStore, Promise<void>>;
|
getServer: Thunk<ServerDataStore, string, Record<string, unknown>, ServerStore, Promise<void>>;
|
||||||
setServer: Action<ServerDataStore, Server>;
|
setServer: Action<ServerDataStore, Server>;
|
||||||
|
setServerFromState: Action<ServerDataStore, (s: Server) => Server>;
|
||||||
setPermissions: Action<ServerDataStore, string[]>;
|
setPermissions: Action<ServerDataStore, string[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +31,22 @@ const server: ServerDataStore = {
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setServer: action((state, payload) => {
|
setServer: action((state, payload) => {
|
||||||
state.data = payload;
|
if (!isEqual(payload, state.data)) {
|
||||||
|
state.data = payload;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
setServerFromState: action((state, payload) => {
|
||||||
|
const output = payload(state.data!);
|
||||||
|
if (!isEqual(output, state.data)) {
|
||||||
|
state.data = output;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
setPermissions: action((state, payload) => {
|
setPermissions: action((state, payload) => {
|
||||||
state.permissions = payload;
|
if (!isEqual(payload, state.permissions)) {
|
||||||
|
state.permissions = payload;
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue