Skip to content

Commit c441864

Browse files
committed
Start cleaning up the mess of useServer; make startup page update in real time
1 parent 179885b commit c441864

File tree

16 files changed

+175
-61
lines changed

16 files changed

+175
-61
lines changed

resources/scripts/api/server/updateStartupVariable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import http from '@/api/http';
22
import { ServerEggVariable } from '@/api/server/types';
33
import { rawDataToServerEggVariable } from '@/api/transformers';
44

5-
export default async (uuid: string, key: string, value: string): Promise<ServerEggVariable> => {
5+
export default async (uuid: string, key: string, value: string): Promise<[ ServerEggVariable, string ]> => {
66
const { data } = await http.put(`/api/client/servers/${uuid}/startup/variable`, { key, value });
77

8-
return rawDataToServerEggVariable(data);
8+
return [ rawDataToServerEggVariable(data), data.meta.startup_command ];
99
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import useSWR from 'swr';
2+
import http, { FractalResponseList } from '@/api/http';
3+
import { rawDataToServerEggVariable } from '@/api/transformers';
4+
import { ServerEggVariable } from '@/api/server/types';
5+
6+
interface Response {
7+
invocation: string;
8+
variables: ServerEggVariable[];
9+
}
10+
11+
export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
12+
console.log('firing getServerStartup');
13+
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
14+
15+
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
16+
17+
return { invocation: data.meta.startup_command, variables };
18+
}, { initialData, errorRetryCount: 3 });

resources/scripts/components/elements/Can.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import React from 'react';
1+
import React, { memo } from 'react';
22
import { usePermissions } from '@/plugins/usePermissions';
3+
import isEqual from 'react-fast-compare';
34

45
interface Props {
56
action: string | string[];
@@ -23,4 +24,4 @@ const Can = ({ action, matchAny = false, renderOnError, children }: Props) => {
2324
);
2425
};
2526

26-
export default Can;
27+
export default memo(Can, isEqual);

resources/scripts/components/elements/ServerContentBlock.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import useServer from '@/plugins/useServer';
21
import PageContentBlock, { PageContentBlockProps } from '@/components/elements/PageContentBlock';
32
import React from 'react';
3+
import { ServerContext } from '@/state/server';
44

55
interface Props extends PageContentBlockProps {
66
title: string;
77
}
88

99
const ServerContentBlock: React.FC<Props> = ({ title, children, ...props }) => {
10-
const { name } = useServer();
10+
const name = ServerContext.useStoreState(state => state.server.data!.name);
1111

1212
return (
1313
<PageContentBlock title={`${name} | ${title}`} {...props}>

resources/scripts/components/elements/Spinner.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,10 @@ const Spinner = ({ centered, ...props }: Props) => (
4545
);
4646
Spinner.DisplayName = 'Spinner';
4747

48+
Spinner.Size = {
49+
SMALL: 'small' as SpinnerSize,
50+
BASE: 'base' as SpinnerSize,
51+
LARGE: 'large' as SpinnerSize,
52+
};
53+
4854
export default Spinner;

resources/scripts/components/server/startup/StartupContainer.tsx

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,64 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import TitledGreyBox from '@/components/elements/TitledGreyBox';
3-
import useServer from '@/plugins/useServer';
43
import tw from 'twin.macro';
54
import VariableBox from '@/components/server/startup/VariableBox';
65
import ServerContentBlock from '@/components/elements/ServerContentBlock';
6+
import getServerStartup from '@/api/swr/getServerStartup';
7+
import Spinner from '@/components/elements/Spinner';
8+
import ServerError from '@/components/screens/ServerError';
9+
import { httpErrorToHuman } from '@/api/http';
10+
import { ServerContext } from '@/state/server';
11+
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
712

813
const StartupContainer = () => {
9-
const { invocation, variables } = useServer();
14+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
15+
const invocation = ServerContext.useStoreState(state => state.server.data!.invocation);
16+
const variables = ServerContext.useStoreState(state => state.server.data!.variables);
17+
18+
const { data, error, isValidating, mutate } = getServerStartup(uuid, { invocation, variables });
19+
20+
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
21+
22+
useEffect(() => {
23+
// Since we're passing in initial data this will not trigger on mount automatically. We
24+
// want to always fetch fresh information from the API however when we're loading the startup
25+
// information.
26+
mutate();
27+
}, []);
28+
29+
useDeepCompareEffect(() => {
30+
if (!data) return;
31+
32+
setServerFromState(s => ({
33+
...s,
34+
invocation: data.invocation,
35+
variables: data.variables,
36+
}));
37+
}, [ data ]);
1038

1139
return (
12-
<ServerContentBlock title={'Startup Settings'}>
13-
<TitledGreyBox title={'Startup Command'}>
14-
<div css={tw`px-1 py-2`}>
15-
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
16-
{invocation}
17-
</p>
40+
!data ?
41+
(!error || (error && isValidating)) ?
42+
<Spinner centered size={Spinner.Size.LARGE}/>
43+
:
44+
<ServerError
45+
title={'Oops!'}
46+
message={httpErrorToHuman(error)}
47+
onRetry={() => mutate()}
48+
/>
49+
:
50+
<ServerContentBlock title={'Startup Settings'}>
51+
<TitledGreyBox title={'Startup Command'}>
52+
<div css={tw`px-1 py-2`}>
53+
<p css={tw`font-mono bg-neutral-900 rounded py-2 px-4`}>
54+
{data.invocation}
55+
</p>
56+
</div>
57+
</TitledGreyBox>
58+
<div css={tw`grid gap-8 grid-cols-2 mt-10`}>
59+
{data.variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
1860
</div>
19-
</TitledGreyBox>
20-
<div css={tw`grid gap-8 grid-cols-2 mt-10`}>
21-
{variables.map(variable => <VariableBox key={variable.envVariable} variable={variable}/>)}
22-
</div>
23-
</ServerContentBlock>
61+
</ServerContentBlock>
2462
);
2563
};
2664

resources/scripts/components/server/startup/VariableBox.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { memo, useState } from 'react';
22
import { ServerEggVariable } from '@/api/server/types';
33
import TitledGreyBox from '@/components/elements/TitledGreyBox';
44
import { usePermissions } from '@/plugins/usePermissions';
@@ -8,9 +8,11 @@ import tw from 'twin.macro';
88
import { debounce } from 'debounce';
99
import updateStartupVariable from '@/api/server/updateStartupVariable';
1010
import useServer from '@/plugins/useServer';
11-
import { ServerContext } from '@/state/server';
1211
import useFlash from '@/plugins/useFlash';
1312
import FlashMessageRender from '@/components/FlashMessageRender';
13+
import getServerStartup from '@/api/swr/getServerStartup';
14+
import isEqual from 'react-fast-compare';
15+
import { ServerContext } from '@/state/server';
1416

1517
interface Props {
1618
variable: ServerEggVariable;
@@ -19,22 +21,21 @@ interface Props {
1921
const VariableBox = ({ variable }: Props) => {
2022
const FLASH_KEY = `server:startup:${variable.envVariable}`;
2123

22-
const server = useServer();
24+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
2325
const [ loading, setLoading ] = useState(false);
2426
const [ canEdit ] = usePermissions([ 'startup.update' ]);
2527
const { clearFlashes, clearAndAddHttpError } = useFlash();
26-
27-
const setServer = ServerContext.useStoreActions(actions => actions.server.setServer);
28+
const { mutate } = getServerStartup(uuid);
2829

2930
const setVariableValue = debounce((value: string) => {
3031
setLoading(true);
3132
clearFlashes(FLASH_KEY);
3233

33-
updateStartupVariable(server.uuid, variable.envVariable, value)
34-
.then(response => setServer({
35-
...server,
36-
variables: server.variables.map(v => v.envVariable === response.envVariable ? response : v),
37-
}))
34+
updateStartupVariable(uuid, variable.envVariable, value)
35+
.then(([ response, invocation ]) => mutate(data => ({
36+
invocation,
37+
variables: data.variables.map(v => v.envVariable === response.envVariable ? response : v),
38+
}), false))
3839
.catch(error => {
3940
console.error(error);
4041
clearAndAddHttpError({ error, key: FLASH_KEY });
@@ -74,4 +75,4 @@ const VariableBox = ({ variable }: Props) => {
7475
);
7576
};
7677

77-
export default VariableBox;
78+
export default memo(VariableBox, isEqual);

resources/scripts/components/server/users/EditSubuserModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { httpErrorToHuman } from '@/api/http';
1515
import FlashMessageRender from '@/components/FlashMessageRender';
1616
import Can from '@/components/elements/Can';
1717
import { usePermissions } from '@/plugins/usePermissions';
18-
import { useDeepMemo } from '@/plugins/useDeepMemo';
18+
import { useDeepCompareMemo } from '@/plugins/useDeepCompareMemo';
1919
import tw from 'twin.macro';
2020
import Button from '@/components/elements/Button';
2121
import Label from '@/components/elements/Label';
@@ -63,7 +63,7 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
6363
const loggedInPermissions = ServerContext.useStoreState(state => state.server.permissions);
6464

6565
// The permissions that can be modified by this user.
66-
const editablePermissions = useDeepMemo(() => {
66+
const editablePermissions = useDeepCompareMemo(() => {
6767
const cleaned = Object.keys(permissions)
6868
.map(key => Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`));
6969

resources/scripts/hoc/requireServerPermission.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import React from 'react';
22
import Can from '@/components/elements/Can';
33
import ScreenBlock from '@/components/screens/ScreenBlock';
4+
import isEqual from 'react-fast-compare';
45

56
const requireServerPermission = (Component: React.ComponentType<any>, permissions: string | string[]) => {
67
return class extends React.Component<any, any> {
8+
shouldComponentUpdate (nextProps: Readonly<any>) {
9+
return !isEqual(nextProps, this.props);
10+
}
11+
712
render () {
813
return (
914
<Can
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { DependencyList, EffectCallback, useEffect } from 'react';
2+
import { useDeepMemoize } from './useDeepMemoize';
3+
4+
export const useDeepCompareEffect = (callback: EffectCallback, dependencies: DependencyList) =>
5+
useEffect(callback, useDeepMemoize(dependencies));

0 commit comments

Comments
 (0)