Skip to content

Commit

Permalink
Split the StandardEditor (#50350)
Browse files Browse the repository at this point in the history
  • Loading branch information
bl-nero committed Dec 20, 2024
1 parent c01ddc0 commit a191b87
Show file tree
Hide file tree
Showing 10 changed files with 1,688 additions and 1,485 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { render, screen, userEvent } from 'design/utils/testing';
import { act } from '@testing-library/react';
import { Validator } from 'shared/components/Validation';
import selectEvent from 'react-select-event';
import { ResourceKind } from 'teleport/services/resources';

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

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

test('editing', async () => {
const { user, onChange } = setup();
await user.click(screen.getByRole('button', { name: 'Add New' }));
await selectEvent.select(screen.getByLabelText('Resources'), [
'db',
'node',
]);
await selectEvent.select(screen.getByLabelText('Permissions'), [
'list',
'read',
]);
expect(onChange).toHaveBeenLastCalledWith([
{
id: expect.any(String),
resources: [
{ label: ResourceKind.Database, value: 'db' },
{ label: ResourceKind.Node, value: 'node' },
],
verbs: [
{ label: 'list', value: 'list' },
{ label: 'read', value: 'read' },
],
},
] as RuleModel[]);
});

test('validation', async () => {
const { user, validator } = setup();
await user.click(screen.getByRole('button', { name: 'Add New' }));
act(() => validator.validate());
expect(
screen.getByText('At least one resource kind is required')
).toBeInTheDocument();
expect(
screen.getByText('At least one permission is required')
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Flex from 'design/Flex';

import { ButtonSecondary } from 'design/Button';
import { Plus } from 'design/Icon';
import {
FieldSelect,
FieldSelectCreatable,
} from 'shared/components/FieldSelect';
import { precomputed } from 'shared/components/Validation/rules';
import { components, MultiValueProps } from 'react-select';
import { HoverTooltip } from 'design/Tooltip';
import styled from 'styled-components';

import { AccessRuleValidationResult } from './validation';
import {
newRuleModel,
ResourceKindOption,
resourceKindOptions,
resourceKindOptionsMap,
RuleModel,
verbOptions,
} from './standardmodel';
import { Section, SectionProps } from './sections';

export function AccessRules({
value,
isProcessing,
validation,
onChange,
}: SectionProps<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));
}
return (
<Flex flexDirection="column" gap={3}>
{value.map((rule, i) => (
<AccessRule
key={rule.id}
isProcessing={isProcessing}
value={rule}
onChange={setRule}
validation={validation[i]}
onRemove={() => removeRule(rule.id)}
/>
))}
<ButtonSecondary alignSelf="start" onClick={addRule}>
<Plus size="small" mr={2} />
Add New
</ButtonSecondary>
</Flex>
);
}

function AccessRule({
value,
isProcessing,
validation,
onChange,
onRemove,
}: SectionProps<RuleModel, AccessRuleValidationResult> & {
onRemove?(): void;
}) {
const { resources, verbs } = value;
return (
<Section
title="Access Rule"
tooltip="A rule that gives users access to certain kinds of resources"
removable
isProcessing={isProcessing}
validation={validation}
onRemove={onRemove}
>
<ResourceKindSelect
components={{ MultiValue: ResourceKindMultiValue }}
isMulti
label="Resources"
isDisabled={isProcessing}
options={resourceKindOptions}
value={resources}
onChange={r => onChange?.({ ...value, resources: r })}
rule={precomputed(validation.fields.resources)}
/>
<FieldSelect
isMulti
label="Permissions"
isDisabled={isProcessing}
options={verbOptions}
value={verbs}
onChange={v => onChange?.({ ...value, verbs: v })}
rule={precomputed(validation.fields.verbs)}
mb={0}
/>
</Section>
);
}

const ResourceKindSelect = styled(
FieldSelectCreatable<ResourceKindOption, true>
)`
.teleport-resourcekind__value--unknown {
background: ${props => props.theme.colors.interactive.solid.alert.default};
.react-select__multi-value__label,
.react-select__multi-value__remove {
color: ${props => props.theme.colors.text.primaryInverse};
}
}
`;

function ResourceKindMultiValue(props: MultiValueProps<ResourceKindOption>) {
if (resourceKindOptionsMap.has(props.data.value)) {
return <components.MultiValue {...props} />;
}
return (
<HoverTooltip tipContent="Unrecognized resource type">
<components.MultiValue
{...props}
className="teleport-resourcekind__value--unknown"
/>
</HoverTooltip>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import FieldInput from 'shared/components/FieldInput';

import { precomputed } from 'shared/components/Validation/rules';

import { LabelsInput } from 'teleport/components/LabelsInput';

import Text from 'design/Text';

import { Section, SectionProps } from './sections';
import { MetadataModel } from './standardmodel';
import { MetadataValidationResult } from './validation';

export const MetadataSection = ({
value,
isProcessing,
validation,
onChange,
}: SectionProps<MetadataModel, MetadataValidationResult>) => (
<Section
title="Role Metadata"
tooltip="Basic information about the role resource"
isProcessing={isProcessing}
validation={validation}
>
<FieldInput
label="Role Name"
placeholder="Enter Role Name"
value={value.name}
disabled={isProcessing}
rule={precomputed(validation.fields.name)}
onChange={e => onChange({ ...value, name: e.target.value })}
/>
<FieldInput
label="Description"
placeholder="Enter Role Description"
value={value.description || ''}
disabled={isProcessing}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange({ ...value, description: e.target.value })
}
/>
<Text typography="body3" mb={1}>
Labels
</Text>
<LabelsInput
disableBtns={isProcessing}
labels={value.labels}
setLabels={labels => onChange?.({ ...value, labels })}
rule={precomputed(validation.fields.labels)}
/>
</Section>
);
Loading

0 comments on commit a191b87

Please sign in to comment.