Skip to content

Commit cbedd45

Browse files
committed
Performance cleanup; check main box when all children are checked; closes pterodactyl#2379
1 parent 2182a15 commit cbedd45

File tree

2 files changed

+107
-76
lines changed

2 files changed

+107
-76
lines changed

resources/scripts/components/elements/TitledGreyBox.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import React from 'react';
1+
import React, { memo } from 'react';
22
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
33
import { IconProp } from '@fortawesome/fontawesome-svg-core';
44
import tw from 'twin.macro';
5+
import isEqual from 'react-fast-compare';
56

67
interface Props {
78
icon?: IconProp;
@@ -27,4 +28,4 @@ const TitledGreyBox = ({ icon, title, children, className }: Props) => (
2728
</div>
2829
);
2930

30-
export default TitledGreyBox;
31+
export default memo(TitledGreyBox, isEqual);

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

Lines changed: 104 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { forwardRef, useEffect, useRef } from 'react';
1+
import React, { forwardRef, memo, useCallback, useEffect, useRef } from 'react';
22
import { Subuser } from '@/state/server/subusers';
33
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
44
import { array, object, string } from 'yup';
@@ -11,7 +11,6 @@ import Checkbox from '@/components/elements/Checkbox';
1111
import styled from 'styled-components/macro';
1212
import createOrUpdateSubuser from '@/api/server/users/createOrUpdateSubuser';
1313
import { ServerContext } from '@/state/server';
14-
import { httpErrorToHuman } from '@/api/http';
1514
import FlashMessageRender from '@/components/FlashMessageRender';
1615
import Can from '@/components/elements/Can';
1716
import { usePermissions } from '@/plugins/usePermissions';
@@ -20,6 +19,7 @@ import tw from 'twin.macro';
2019
import Button from '@/components/elements/Button';
2120
import Label from '@/components/elements/Label';
2221
import Input from '@/components/elements/Input';
22+
import isEqual from 'react-fast-compare';
2323

2424
type Props = {
2525
subuser?: Subuser;
@@ -31,7 +31,7 @@ interface Values {
3131
}
3232

3333
const PermissionLabel = styled.label`
34-
${tw`flex items-center border border-transparent rounded md:p-2`};
34+
${tw`flex items-center border border-transparent rounded md:p-2 transition-colors duration-75`};
3535
text-transform: none;
3636
3737
&:not(.disabled) {
@@ -41,6 +41,10 @@ const PermissionLabel = styled.label`
4141
${tw`border-neutral-500 bg-neutral-800`};
4242
}
4343
}
44+
45+
&:not(:first-of-type) {
46+
${tw`mt-4 sm:mt-2`};
47+
}
4448
4549
&.disabled {
4650
${tw`opacity-50`};
@@ -51,8 +55,58 @@ const PermissionLabel = styled.label`
5155
}
5256
`;
5357

58+
interface TitleProps {
59+
isEditable: boolean;
60+
permission: string;
61+
permissions: string[];
62+
children: React.ReactNode;
63+
className?: string;
64+
}
65+
66+
const PermissionTitledBox = memo(({ isEditable, permission, permissions, className, children }: TitleProps) => {
67+
const { values, setFieldValue } = useFormikContext<Values>();
68+
69+
const onCheckboxClicked = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
70+
console.log(e.currentTarget.checked, [
71+
...values.permissions,
72+
...permissions.filter(p => !values.permissions.includes(p)),
73+
]);
74+
75+
if (e.currentTarget.checked) {
76+
setFieldValue('permissions', [
77+
...values.permissions,
78+
...permissions.filter(p => !values.permissions.includes(p)),
79+
]);
80+
} else {
81+
setFieldValue('permissions', [
82+
...values.permissions.filter(p => !permissions.includes(p)),
83+
]);
84+
}
85+
}, [ permissions, values.permissions ]);
86+
87+
return (
88+
<TitledGreyBox
89+
title={
90+
<div css={tw`flex items-center`}>
91+
<p css={tw`text-sm uppercase flex-1`}>{permission}</p>
92+
{isEditable &&
93+
<Input
94+
type={'checkbox'}
95+
checked={permissions.every(p => values.permissions.includes(p))}
96+
onChange={onCheckboxClicked}
97+
/>
98+
}
99+
</div>
100+
}
101+
className={className}
102+
>
103+
{children}
104+
</TitledGreyBox>
105+
);
106+
}, isEqual);
107+
54108
const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...props }, ref) => {
55-
const { values, isSubmitting, setFieldValue } = useFormikContext<Values>();
109+
const { isSubmitting } = useFormikContext<Values>();
56110
const [ canEditUser ] = usePermissions(subuser ? [ 'user.update' ] : [ 'user.create' ]);
57111
const permissions = useStoreState(state => state.permissions.data);
58112

@@ -104,73 +158,48 @@ const EditSubuserModal = forwardRef<HTMLHeadingElement, Props>(({ subuser, ...pr
104158
</div>
105159
}
106160
<div css={tw`my-6`}>
107-
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => (
108-
<TitledGreyBox
109-
key={key}
110-
title={
111-
<div css={tw`flex items-center`}>
112-
<p css={tw`text-sm uppercase flex-1`}>{key}</p>
113-
{canEditUser &&
114-
<Input
115-
type={'checkbox'}
116-
onClick={e => {
117-
if (e.currentTarget.checked) {
118-
setFieldValue('permissions', [
119-
...values.permissions,
120-
...Object.keys(permissions[key].keys)
121-
.map(pkey => `${key}.${pkey}`)
122-
.filter(permission => values.permissions.indexOf(permission) === -1),
123-
]);
124-
} else {
125-
setFieldValue('permissions', [
126-
...values.permissions.filter(
127-
permission => Object.keys(permissions[key].keys)
128-
.map(pkey => `${key}.${pkey}`)
129-
.indexOf(permission) < 0,
130-
),
131-
]);
161+
{Object.keys(permissions).filter(key => key !== 'websocket').map((key, index) => {
162+
const group = Object.keys(permissions[key].keys).map(pkey => `${key}.${pkey}`);
163+
164+
return (
165+
<PermissionTitledBox
166+
key={`permission_${key}`}
167+
isEditable={canEditUser}
168+
permission={key}
169+
permissions={group}
170+
css={index > 0 ? tw`mt-4` : undefined}
171+
>
172+
<p css={tw`text-sm text-neutral-400 mb-4`}>
173+
{permissions[key].description}
174+
</p>
175+
{Object.keys(permissions[key].keys).map(pkey => (
176+
<PermissionLabel
177+
key={`permission_${key}_${pkey}`}
178+
htmlFor={`permission_${key}_${pkey}`}
179+
className={(!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0) ? 'disabled' : undefined}
180+
>
181+
<div css={tw`p-2`}>
182+
<Checkbox
183+
id={`permission_${key}_${pkey}`}
184+
name={'permissions'}
185+
value={`${key}.${pkey}`}
186+
css={tw`w-5 h-5 mr-2`}
187+
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
188+
/>
189+
</div>
190+
<div css={tw`flex-1`}>
191+
<Label as={'p'} css={tw`font-medium`}>{pkey}</Label>
192+
{permissions[key].keys[pkey].length > 0 &&
193+
<p css={tw`text-xs text-neutral-400 mt-1`}>
194+
{permissions[key].keys[pkey]}
195+
</p>
132196
}
133-
}}
134-
/>
135-
}
136-
</div>
137-
}
138-
css={index > 0 ? tw`mt-4` : undefined}
139-
>
140-
<p css={tw`text-sm text-neutral-400 mb-4`}>
141-
{permissions[key].description}
142-
</p>
143-
{Object.keys(permissions[key].keys).map((pkey, index) => (
144-
<PermissionLabel
145-
key={`permission_${key}_${pkey}`}
146-
htmlFor={`permission_${key}_${pkey}`}
147-
css={[
148-
tw`transition-colors duration-75`,
149-
index > 0 ? tw`mt-4 sm:mt-2` : undefined,
150-
]}
151-
className={(!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0) ? 'disabled' : undefined}
152-
>
153-
<div css={tw`p-2`}>
154-
<Checkbox
155-
id={`permission_${key}_${pkey}`}
156-
name={'permissions'}
157-
value={`${key}.${pkey}`}
158-
css={tw`w-5 h-5 mr-2`}
159-
disabled={!canEditUser || editablePermissions.indexOf(`${key}.${pkey}`) < 0}
160-
/>
161-
</div>
162-
<div css={tw`flex-1`}>
163-
<Label css={tw`font-medium`}>{pkey}</Label>
164-
{permissions[key].keys[pkey].length > 0 &&
165-
<p css={tw`text-xs text-neutral-400 mt-1`}>
166-
{permissions[key].keys[pkey]}
167-
</p>
168-
}
169-
</div>
170-
</PermissionLabel>
171-
))}
172-
</TitledGreyBox>
173-
))}
197+
</div>
198+
</PermissionLabel>
199+
))}
200+
</PermissionTitledBox>
201+
);
202+
})}
174203
</div>
175204
<Can action={subuser ? 'user.update' : 'user.create'}>
176205
<div css={tw`pb-6 flex justify-end`}>
@@ -187,8 +216,7 @@ export default ({ subuser, ...props }: Props) => {
187216
const ref = useRef<HTMLHeadingElement>(null);
188217
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
189218
const appendSubuser = ServerContext.useStoreActions(actions => actions.subusers.appendSubuser);
190-
191-
const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
219+
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
192220

193221
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
194222
clearFlashes('user:edit');
@@ -200,7 +228,7 @@ export default ({ subuser, ...props }: Props) => {
200228
.catch(error => {
201229
console.error(error);
202230
setSubmitting(false);
203-
addError({ key: 'user:edit', message: httpErrorToHuman(error) });
231+
clearAndAddHttpError({ key: 'user:edit', error });
204232

205233
if (ref.current) {
206234
ref.current.scrollIntoView();
@@ -209,7 +237,9 @@ export default ({ subuser, ...props }: Props) => {
209237
};
210238

211239
useEffect(() => {
212-
clearFlashes('user:edit');
240+
return () => {
241+
clearFlashes('user:edit');
242+
};
213243
}, []);
214244

215245
return (

0 commit comments

Comments
 (0)