Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor the standard role editor to optimize its performance #50871

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ import { EditorHeader } from './EditorHeader';
import { EditorTab } from './EditorTabs';
import { StandardEditor } from './StandardEditor/StandardEditor';
import {
newRole,
roleEditorModelToRole,
roleToRoleEditorModel,
StandardEditorModel,
} from './StandardEditor/standardmodel';
import { useStandardModel } from './StandardEditor/useStandardModel';
import { YamlEditor } from './YamlEditor';
import { YamlEditorModel } from './yamlmodel';

Expand Down Expand Up @@ -65,16 +64,7 @@ export const RoleEditor = ({
const standardEditorId = `${idPrefix}-standard`;
const yamlEditorId = `${idPrefix}-yaml`;

const [standardModel, setStandardModel] = useState<StandardEditorModel>(
() => {
const role = originalRole?.object ?? newRole();
const roleModel = roleToRoleEditorModel(role, role);
return {
roleModel,
isDirty: !originalRole, // New role is dirty by default.
};
}
);
const [standardModel, dispatch] = useStandardModel(originalRole?.object);

const [yamlModel, setYamlModel] = useState<YamlEditorModel>({
content: originalRole?.yaml ?? '',
Expand Down Expand Up @@ -141,9 +131,9 @@ export const RoleEditor = ({
// Abort if there's an error. Don't switch the tab or set the model.
if (err) return;

setStandardModel({
roleModel,
isDirty: yamlModel.isDirty,
dispatch({
type: 'set-role-model',
payload: roleModel,
});
break;
}
Expand Down Expand Up @@ -213,7 +203,7 @@ export const RoleEditor = ({
onCancel={handleCancel}
standardEditorModel={standardModel}
isProcessing={isProcessing}
onChange={setStandardModel}
dispatch={dispatch}
/>
</Flex>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,29 +26,29 @@ import { ResourceKind } from 'teleport/services/resources';

import { AccessRules } from './AccessRules';
import { RuleModel } from './standardmodel';
import { StatefulSection } from './StatefulSection';
import { AccessRuleValidationResult, validateAccessRule } from './validation';
import { StatefulSectionWithDispatch } from './StatefulSection';
import { AccessRuleValidationResult } from './validation';

describe('AccessRules', () => {
const setup = () => {
const onChange = jest.fn();
const modelRef = jest.fn();
let validator: Validator;
render(
<StatefulSection<RuleModel[], AccessRuleValidationResult[]>
<StatefulSectionWithDispatch<RuleModel[], AccessRuleValidationResult[]>
selector={m => m.roleModel.rules}
validationSelector={m => m.validationResult.rules}
component={AccessRules}
defaultValue={[]}
onChange={onChange}
validatorRef={v => {
validator = v;
}}
validate={rules => rules.map(validateAccessRule)}
modelRef={modelRef}
/>
);
return { user: userEvent.setup(), onChange, validator };
return { user: userEvent.setup(), modelRef, validator };
};

test('editing', async () => {
const { user, onChange } = setup();
const { user, modelRef } = setup();
await user.click(screen.getByRole('button', { name: 'Add New' }));
await selectEvent.select(screen.getByLabelText('Resources'), [
'db',
Expand All @@ -58,7 +58,7 @@ describe('AccessRules', () => {
'list',
'read',
]);
expect(onChange).toHaveBeenLastCalledWith([
expect(modelRef).toHaveBeenLastCalledWith([
{
id: expect.any(String),
resources: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { memo } from 'react';
import { components, MultiValueProps } from 'react-select';
import styled from 'styled-components';

Expand All @@ -29,9 +30,8 @@ import {
} from 'shared/components/FieldSelect';
import { precomputed } from 'shared/components/Validation/rules';

import { SectionBox, SectionProps } from './sections';
import { SectionBox, SectionPropsWithDispatch } from './sections';
import {
newRuleModel,
ResourceKindOption,
resourceKindOptions,
resourceKindOptionsMap,
Expand All @@ -40,20 +40,18 @@ import {
} from './standardmodel';
import { AccessRuleValidationResult } from './validation';

export function AccessRules({
/**
* Access rules tab. This component is memoized to optimize performance; make
* sure that the properties don't change unless necessary.
*/
export const AccessRules = memo(function AccessRules({
value,
isProcessing,
validation,
onChange,
}: SectionProps<RuleModel[], AccessRuleValidationResult[]>) {
dispatch,
}: SectionPropsWithDispatch<RuleModel[], AccessRuleValidationResult[]>) {
function addRule() {
onChange?.([...value, newRuleModel()]);
}
function setRule(rule: RuleModel) {
onChange?.(value.map(r => (r.id === rule.id ? rule : r)));
}
function removeRule(id: string) {
onChange?.(value.filter(r => r.id !== id));
dispatch({ type: 'add-access-rule' });
}
return (
<Flex flexDirection="column" gap={3}>
Expand All @@ -62,9 +60,8 @@ export function AccessRules({
key={rule.id}
isProcessing={isProcessing}
value={rule}
onChange={setRule}
validation={validation[i]}
onRemove={() => removeRule(rule.id)}
dispatch={dispatch}
/>
))}
<ButtonSecondary alignSelf="start" onClick={addRule}>
Expand All @@ -73,26 +70,29 @@ export function AccessRules({
</ButtonSecondary>
</Flex>
);
}
});

function AccessRule({
const AccessRule = memo(function AccessRule({
value,
isProcessing,
validation,
onChange,
onRemove,
}: SectionProps<RuleModel, AccessRuleValidationResult> & {
onRemove?(): void;
}) {
const { resources, verbs } = value;
dispatch,
}: SectionPropsWithDispatch<RuleModel, AccessRuleValidationResult>) {
const { id, resources, verbs } = value;
function setRule(rule: RuleModel) {
dispatch({ type: 'set-access-rule', payload: rule });
}
function removeRule() {
dispatch({ type: 'remove-access-rule', payload: { id } });
}
return (
<SectionBox
title="Access Rule"
tooltip="A rule that gives users access to certain kinds of resources"
removable
isProcessing={isProcessing}
validation={validation}
onRemove={onRemove}
onRemove={removeRule}
>
<ResourceKindSelect
components={{ MultiValue: ResourceKindMultiValue }}
Expand All @@ -101,7 +101,7 @@ function AccessRule({
isDisabled={isProcessing}
options={resourceKindOptions}
value={resources}
onChange={r => onChange?.({ ...value, resources: r })}
onChange={r => setRule?.({ ...value, resources: r })}
rule={precomputed(validation.fields.resources)}
/>
<FieldSelect
Expand All @@ -110,13 +110,13 @@ function AccessRule({
isDisabled={isProcessing}
options={verbOptions}
value={verbs}
onChange={v => onChange?.({ ...value, verbs: v })}
onChange={v => setRule?.({ ...value, verbs: v })}
rule={precomputed(validation.fields.verbs)}
mb={0}
/>
</SectionBox>
);
}
});

const ResourceKindSelect = styled(
FieldSelectCreatable<ResourceKindOption, true>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { useId } from 'react';
import { memo, useId } from 'react';
import styled, { useTheme } from 'styled-components';

import Box from 'design/Box';
Expand All @@ -35,7 +35,11 @@ import {
sessionRecordingModeOptions,
} from './standardmodel';

export function Options({
/**
* Options tab. This component is memoized to optimize performance; make sure
* that the properties don't change unless necessary.
*/
export const Options = memo(function Options({
value,
isProcessing,
onChange,
Expand Down Expand Up @@ -189,7 +193,7 @@ export function Options({
/>
</OptionsGridContainer>
);
}
});

const OptionsGridContainer = styled(Box)`
display: grid;
Expand Down
Loading
Loading