Skip to content

Commit 561eaed

Browse files
committed
Add Role Diff Visual to Role editor
This will enable a role diff viewer from Teleport Policy if the client has Teleport policy enabled
1 parent f926efb commit 561eaed

File tree

9 files changed

+125
-37
lines changed

9 files changed

+125
-37
lines changed

web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19-
import { useId, useState } from 'react';
19+
import { useCallback, useEffect, useId, useState } from 'react';
2020

2121
import { Alert, Box, Flex } from 'design';
2222
import Validation, { Validator } from 'shared/components/Validation';
2323
import { useAsync } from 'shared/hooks/useAsync';
2424

25+
import cfg from 'teleport/config';
2526
import { Role, RoleWithYaml } from 'teleport/services/resources';
27+
import { storageService } from 'teleport/services/storageService';
2628
import { CaptureEvent, userEventService } from 'teleport/services/userEvent';
2729
import { yamlService } from 'teleport/services/yaml';
2830
import { YamlSupportedResourceKind } from 'teleport/services/yaml/types';
@@ -46,6 +48,7 @@ export type RoleEditorProps = {
4648
originalRole?: RoleWithYaml;
4749
onCancel?(): void;
4850
onSave?(r: Partial<RoleWithYaml>): Promise<void>;
51+
onRoleUpdate?(r: Role): void;
4952
};
5053

5154
/**
@@ -57,7 +60,10 @@ export const RoleEditor = ({
5760
originalRole,
5861
onCancel,
5962
onSave,
63+
onRoleUpdate,
6064
}: RoleEditorProps) => {
65+
const roleTesterEnabled =
66+
cfg.isPolicyEnabled && storageService.getAccessGraphRoleTesterEnabled();
6167
const idPrefix = useId();
6268
// These IDs are needed to connect accessibility attributes between the
6369
// standard/YAML tab switcher and the switched panels.
@@ -66,6 +72,12 @@ export const RoleEditor = ({
6672

6773
const [standardModel, dispatch] = useStandardModel(originalRole?.object);
6874

75+
useEffect(() => {
76+
if (standardModel.validationResult.isValid) {
77+
onRoleUpdate?.(roleEditorModelToRole(standardModel.roleModel));
78+
}
79+
}, [standardModel, onRoleUpdate]);
80+
6981
const [yamlModel, setYamlModel] = useState<YamlEditorModel>({
7082
content: originalRole?.yaml ?? '',
7183
isDirty: !originalRole, // New role is dirty by default.
@@ -87,6 +99,20 @@ export const RoleEditor = ({
8799
return roleToRoleEditorModel(parsedRole, originalRole?.object);
88100
});
89101

102+
// The standard editor will automatically preview the changes based on state updates
103+
// but the yaml editor needs to be told when to update (the preview button)
104+
const handleYamlPreview = useCallback(async () => {
105+
if (!onRoleUpdate) {
106+
return;
107+
}
108+
// error will be handled by the parseYaml attempt. we only continue if parsed returns a value (success)
109+
const [parsed] = await parseYaml();
110+
if (!parsed) {
111+
return;
112+
}
113+
onRoleUpdate(roleEditorModelToRole(parsed));
114+
}, [onRoleUpdate, parseYaml]);
115+
90116
// Converts standard editor model to a YAML representation.
91117
const [yamlifyAttempt, yamlifyRole] = useAsync(
92118
async () =>
@@ -216,6 +242,7 @@ export const RoleEditor = ({
216242
isProcessing={isProcessing}
217243
onCancel={handleCancel}
218244
originalRole={originalRole}
245+
onPreview={roleTesterEnabled ? handleYamlPreview : undefined}
219246
/>
220247
</Flex>
221248
)}

web/packages/teleport/src/Roles/RoleEditor/RoleEditorAdapter.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19-
import { useEffect } from 'react';
19+
import { useCallback, useEffect } from 'react';
2020
import { useTheme } from 'styled-components';
2121

2222
import { Danger } from 'design/Alert';
2323
import Flex from 'design/Flex';
2424
import { Indicator } from 'design/Indicator';
2525
import { useAsync } from 'shared/hooks/useAsync';
26+
import { debounce } from 'shared/utils/highbar';
2627

2728
import { State as ResourcesState } from 'teleport/components/useResources';
2829
import { Role, RoleWithYaml } from 'teleport/services/resources';
@@ -68,6 +69,11 @@ export function RoleEditorAdapter({
6869
convertToRole(originalContent);
6970
}, [originalContent]);
7071

72+
const onRoleUpdate = useCallback(
73+
debounce(roleDiffProps.updateRoleDiff, 500),
74+
[]
75+
);
76+
7177
return (
7278
<Flex flex="1">
7379
<Flex
@@ -90,26 +96,29 @@ export function RoleEditorAdapter({
9096
{convertAttempt.status === 'error' && (
9197
<Danger>{convertAttempt.statusText}</Danger>
9298
)}
99+
{roleDiffProps?.errorMessage && (
100+
<Danger>{roleDiffProps.errorMessage}</Danger>
101+
)}
93102
{convertAttempt.status === 'success' && (
94103
<RoleEditor
95104
originalRole={convertAttempt.data}
96105
onCancel={onCancel}
97106
onSave={onSave}
107+
onRoleUpdate={onRoleUpdate}
98108
/>
99109
)}
100110
</Flex>
101-
<Flex flex="1" alignItems="center" justifyContent="center" m={3}>
102-
{/* TODO (avatus) this component will not be rendered until the Access Diff feature is implemented */}
103-
{roleDiffProps ? (
104-
<roleDiffProps.RoleDiffComponent />
105-
) : (
111+
{roleDiffProps ? (
112+
roleDiffProps.roleDiffElement
113+
) : (
114+
<Flex flex="1" alignItems="center" justifyContent="center" m={3}>
106115
<PolicyPlaceholder
107116
currentFlow={
108117
resources.status === 'creating' ? 'creating' : 'updating'
109118
}
110119
/>
111-
)}
112-
</Flex>
120+
</Flex>
121+
)}
113122
</Flex>
114123
);
115124
}

web/packages/teleport/src/Roles/RoleEditor/Shared.tsx

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ import useTeleport from 'teleport/useTeleport';
2525

2626
export const EditorSaveCancelButton = ({
2727
onSave,
28+
onPreview,
2829
onCancel,
29-
disabled,
30+
saveDisabled,
31+
previewDisabled = true,
3032
isEditing,
3133
}: {
3234
onSave?(): void;
35+
onPreview?(): void;
3336
onCancel?(): void;
34-
disabled: boolean;
37+
saveDisabled: boolean;
38+
previewDisabled?: boolean;
3539
isEditing?: boolean;
3640
}) => {
3741
const ctx = useTeleport();
@@ -45,32 +49,45 @@ export const EditorSaveCancelButton = ({
4549
hoverTooltipContent = 'You do not have access to create roles';
4650
}
4751

52+
const saveButton = (
53+
<Box width="50%">
54+
<HoverTooltip tipContent={hoverTooltipContent}>
55+
<ButtonPrimary
56+
width="100%"
57+
size="large"
58+
onClick={onSave}
59+
disabled={
60+
saveDisabled ||
61+
(isEditing && !roleAccess.edit) ||
62+
(!isEditing && !roleAccess.create)
63+
}
64+
>
65+
{isEditing ? 'Save Changes' : 'Create Role'}
66+
</ButtonPrimary>
67+
</HoverTooltip>
68+
</Box>
69+
);
70+
const cancelButton = (
71+
<ButtonSecondary width="50%" onClick={onCancel}>
72+
Cancel
73+
</ButtonSecondary>
74+
);
75+
76+
const previewButton = (
77+
<ButtonPrimary width="50%" onClick={onPreview} disabled={previewDisabled}>
78+
Preview
79+
</ButtonPrimary>
80+
);
81+
4882
return (
4983
<Flex
5084
gap={2}
5185
p={3}
5286
borderTop={1}
5387
borderColor={theme.colors.interactive.tonal.neutral[0]}
5488
>
55-
<Box width="50%">
56-
<HoverTooltip tipContent={hoverTooltipContent}>
57-
<ButtonPrimary
58-
width="100%"
59-
size="large"
60-
onClick={onSave}
61-
disabled={
62-
disabled ||
63-
(isEditing && !roleAccess.edit) ||
64-
(!isEditing && !roleAccess.create)
65-
}
66-
>
67-
{isEditing ? 'Save Changes' : 'Create Role'}
68-
</ButtonPrimary>
69-
</HoverTooltip>
70-
</Box>
71-
<ButtonSecondary width="50%" onClick={onCancel}>
72-
Cancel
73-
</ButtonSecondary>
89+
{saveButton}
90+
{onPreview ? previewButton : cancelButton}
7491
</Flex>
7592
);
7693
};

web/packages/teleport/src/Roles/RoleEditor/StandardEditor/StandardEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export const StandardEditor = ({
214214
<EditorSaveCancelButton
215215
onSave={() => handleSave()}
216216
onCancel={onCancel}
217-
disabled={
217+
saveDisabled={
218218
isProcessing ||
219219
standardEditorModel.roleModel.requiresReset ||
220220
!standardEditorModel.isDirty

web/packages/teleport/src/Roles/RoleEditor/YamlEditor.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19+
import { useState } from 'react';
20+
1921
import { Flex } from 'design';
2022
import TextEditor from 'shared/components/TextEditor';
2123

@@ -30,6 +32,7 @@ type YamlEditorProps = {
3032
isProcessing: boolean;
3133
onChange?(y: YamlEditorModel): void;
3234
onSave?(content: string): void;
35+
onPreview?(): void;
3336
onCancel?(): void;
3437
};
3538

@@ -39,13 +42,25 @@ export const YamlEditor = ({
3942
yamlEditorModel,
4043
onChange,
4144
onSave,
45+
onPreview,
4246
onCancel,
4347
}: YamlEditorProps) => {
4448
const isEditing = !!originalRole;
49+
const [wasPreviewed, setHasPreviewed] = useState(!onPreview);
4550

4651
const handleSave = () => onSave?.(yamlEditorModel.content);
4752

53+
const handlePreview = () => {
54+
// handlePreview should only be called if `onPreview` exists, but adding
55+
// the extra safety here to protect against potential misuse
56+
onPreview?.();
57+
setHasPreviewed(true);
58+
};
59+
4860
function handleSetYaml(newContent: string) {
61+
if (onPreview) {
62+
setHasPreviewed(false);
63+
}
4964
onChange?.({
5065
isDirty: originalRole?.yaml !== newContent,
5166
content: newContent,
@@ -63,8 +78,12 @@ export const YamlEditor = ({
6378
</Flex>
6479
<EditorSaveCancelButton
6580
onSave={handleSave}
81+
onPreview={onPreview ? handlePreview : undefined}
6682
onCancel={onCancel}
67-
disabled={isProcessing || !yamlEditorModel.isDirty}
83+
saveDisabled={isProcessing || !yamlEditorModel.isDirty || !wasPreviewed}
84+
previewDisabled={
85+
isProcessing || wasPreviewed || !yamlEditorModel.isDirty
86+
}
6887
isEditing={isEditing}
6988
/>
7089
</Flex>

web/packages/teleport/src/Roles/Roles.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,19 +271,25 @@ test('renders the role diff component', async () => {
271271
list: true,
272272
},
273273
});
274-
const RoleDiffComponent = () => <div>i am rendered</div>;
274+
const roleDiffElement = <div>i am rendered</div>;
275+
275276
render(
276277
<MemoryRouter>
277278
<ContextProvider ctx={ctx}>
278279
<Roles
279280
{...defaultState()}
280-
roleDiffProps={{ RoleDiffComponent, updateRoleDiff: () => null }}
281+
roleDiffProps={{
282+
roleDiffElement,
283+
updateRoleDiff: () => null,
284+
errorMessage: 'there is an error here',
285+
}}
281286
/>
282287
</ContextProvider>
283288
</MemoryRouter>
284289
);
285290
await openEditor();
286291
expect(screen.getByText('i am rendered')).toBeInTheDocument();
292+
expect(screen.getByText('there is an error here')).toBeInTheDocument();
287293
});
288294

289295
async function openEditor() {

web/packages/teleport/src/Roles/Roles.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
*/
1818

19-
import { ComponentType, useEffect, useState } from 'react';
19+
import { useEffect, useState } from 'react';
2020
import styled from 'styled-components';
2121

2222
import { Alert, Box, Button, Flex, H3, Link } from 'design';
@@ -50,8 +50,9 @@ import { State, useRoles } from './useRoles';
5050

5151
// RoleDiffProps are an optional set of props to render the role diff visualizer.
5252
type RoleDiffProps = {
53-
RoleDiffComponent: ComponentType;
54-
updateRoleDiff: (role: Role) => Promise<void>;
53+
roleDiffElement: React.ReactNode;
54+
updateRoleDiff: (role: Role) => void;
55+
errorMessage: string;
5556
};
5657

5758
export type RolesProps = {

web/packages/teleport/src/services/storageService/storageService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,13 @@ export const storageService = {
265265
return this.getParsedJSONValue(KeysEnum.ACCESS_GRAPH_SQL_ENABLED, false);
266266
},
267267

268+
getAccessGraphRoleTesterEnabled(): boolean {
269+
return this.getParsedJSONValue(
270+
KeysEnum.ACCESS_GRAPH_ROLE_TESTER_ENABLED,
271+
false
272+
);
273+
},
274+
268275
getExternalAuditStorageCtaDisabled(): boolean {
269276
return this.getParsedJSONValue(
270277
KeysEnum.EXTERNAL_AUDIT_STORAGE_CTA_DISABLED,

web/packages/teleport/src/services/storageService/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const KeysEnum = {
3030
ACCESS_GRAPH_QUERY: 'grv_teleport_access_graph_query',
3131
ACCESS_GRAPH_ENABLED: 'grv_teleport_access_graph_enabled',
3232
ACCESS_GRAPH_SQL_ENABLED: 'grv_teleport_access_graph_sql_enabled',
33+
ACCESS_GRAPH_ROLE_TESTER_ENABLED:
34+
'grv_teleport_access_graph_role_tester_enabled',
3335
ACCESS_LIST_PREFERENCES: 'grv_teleport_access_list_preferences',
3436
EXTERNAL_AUDIT_STORAGE_CTA_DISABLED:
3537
'grv_teleport_external_audit_storage_disabled',

0 commit comments

Comments
 (0)