diff --git a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx index 2ee25880cedad..4eaaea9bc9e27 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx @@ -17,9 +17,10 @@ */ import React from 'react'; -import { Flex, ButtonText, H2, Indicator, Box } from 'design'; +import { Flex, H2, Indicator, Box, ButtonIcon } from 'design'; import { HoverTooltip } from 'design/Tooltip'; -import { Trash } from 'design/Icon'; + +import { Cross, Trash } from 'design/Icon'; import useTeleport from 'teleport/useTeleport'; import { Role } from 'teleport/services/resources'; @@ -35,6 +36,7 @@ export const EditorHeader = ({ isProcessing, standardEditorId, yamlEditorId, + onClose, }: { role?: Role; onDelete(): void; @@ -43,6 +45,7 @@ export const EditorHeader = ({ isProcessing: boolean; standardEditorId: string; yamlEditorId: string; + onClose(): void; }) => { const ctx = useTeleport(); const isCreating = !role; @@ -51,6 +54,9 @@ export const EditorHeader = ({ return ( + + +

{isCreating @@ -77,14 +83,14 @@ export const EditorHeader = ({ : 'You do not have access to delete a role' } > - - + )} diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx index abd6ed5393f69..f90bf567450aa 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx @@ -16,21 +16,23 @@ * along with this program. If not, see . */ -import React from 'react'; +import React, { useState } from 'react'; import { StoryObj } from '@storybook/react'; import { delay, http, HttpResponse } from 'msw'; import { Info } from 'design/Alert'; import Flex from 'design/Flex'; +import { ButtonPrimary } from 'design/Button'; import { createTeleportContext } from 'teleport/mocks/contexts'; import TeleportContextProvider from 'teleport/TeleportContextProvider'; import cfg from 'teleport/config'; import { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; - import { Access } from 'teleport/services/user'; +import useResources from 'teleport/components/useResources'; import { withDefaults } from './withDefaults'; import { RoleEditor } from './RoleEditor'; +import { RoleEditorDialog } from './RoleEditorDialog'; export default { title: 'Teleport/Roles/Role Editor', @@ -264,6 +266,30 @@ export const noAccess: StoryObj = { }, }; +export const Dialog: StoryObj = { + render() { + const [open, setOpen] = useState(false); + const resources = useResources([], {}); + return ( + <> + setOpen(true)}>Open + setOpen(false)} + onSave={async () => setOpen(false)} + onDelete={async () => setOpen(false)} + /> + + ); + }, + parameters: { + msw: { + handlers: [yamlifyHandler, parseHandler], + }, + }, +}; + const dummyRoleYaml = `kind: role metadata: name: dummy-role diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx index b109c82e8b87d..77945701e8c47 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { Alert, Flex } from 'design'; +import { Alert, Box, Flex } from 'design'; import React, { useId, useState } from 'react'; import { useAsync } from 'shared/hooks/useAsync'; @@ -27,6 +27,8 @@ import { yamlService } from 'teleport/services/yaml'; import { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; import { CaptureEvent, userEventService } from 'teleport/services/userEvent'; +import DeleteRole from '../DeleteRole'; + import { roleEditorModelToRole, newRole, @@ -47,7 +49,7 @@ export type RoleEditorProps = { originalRole?: RoleWithYaml; onCancel?(): void; onSave?(r: Partial): Promise; - onDelete?(): void; + onDelete?(): Promise; }; /** @@ -88,6 +90,8 @@ export const RoleEditor = ({ standardModel.roleModel.requiresReset ? EditorTab.Yaml : EditorTab.Standard ); + const [deleting, setDeleting] = useState(false); + // Converts YAML representation to a standard editor model. const [parseAttempt, parseYaml] = useAsync(async () => { const parsedRole = await yamlService.parse( @@ -181,32 +185,35 @@ export const RoleEditor = ({ {({ validator }) => ( - onTabChange(index, validator)} - isProcessing={isProcessing} - standardEditorId={standardEditorId} - yamlEditorId={yamlEditorId} - /> - {saveAttempt.status === 'error' && ( - - {saveAttempt.statusText} - - )} - {parseAttempt.status === 'error' && ( - - {parseAttempt.statusText} - - )} - {yamlifyAttempt.status === 'error' && ( - - {yamlifyAttempt.statusText} - - )} + + setDeleting(true)} + selectedEditorTab={selectedEditorTab} + onEditorTabChange={index => onTabChange(index, validator)} + isProcessing={isProcessing} + standardEditorId={standardEditorId} + yamlEditorId={yamlEditorId} + onClose={onCancel} + /> + {saveAttempt.status === 'error' && ( + + {saveAttempt.statusText} + + )} + {parseAttempt.status === 'error' && ( + + {parseAttempt.statusText} + + )} + {yamlifyAttempt.status === 'error' && ( + + {yamlifyAttempt.statusText} + + )} + {selectedEditorTab === EditorTab.Standard && ( -
+ handleSave({ object })} @@ -215,7 +222,7 @@ export const RoleEditor = ({ isProcessing={isProcessing} onChange={setStandardModel} /> -
+
)} {selectedEditorTab === EditorTab.Yaml && ( @@ -229,6 +236,13 @@ export const RoleEditor = ({ /> )} + {deleting && ( + setDeleting(false)} + onDelete={onDelete} + /> + )} )}
diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditorAdapter.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditorAdapter.tsx new file mode 100644 index 0000000000000..5951066f0355e --- /dev/null +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditorAdapter.tsx @@ -0,0 +1,229 @@ +/** + * 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 . + */ + +import { Danger } from 'design/Alert'; +import Flex from 'design/Flex'; +import { Indicator } from 'design/Indicator'; +import React, { useEffect } from 'react'; +import { useAsync } from 'shared/hooks/useAsync'; +import { useTheme } from 'styled-components'; +import { H1 } from 'design/Text'; +import Box from 'design/Box'; +import { H3, P, P3 } from 'design/Text/Text'; +import { ButtonSecondary } from 'design/Button'; +import Image from 'design/Image'; + +import { StepComponentProps, StepSlider } from 'design/StepSlider'; + +import { ChevronLeft, ChevronRight } from 'design/Icon'; + +import { State as ResourcesState } from 'teleport/components/useResources'; +import { Role, RoleWithYaml } from 'teleport/services/resources'; +import { yamlService } from 'teleport/services/yaml'; +import { YamlSupportedResourceKind } from 'teleport/services/yaml/types'; +import { ButtonLockedFeature } from 'teleport/components/ButtonLockedFeature'; + +import { RoleEditor } from './RoleEditor'; +import tagpromo from './tagpromo.png'; + +/** + * This component is responsible for converting from the `Resource` + * representation of a role to a more accurate `RoleWithYaml` structure. The + * conversion is asynchronous and it's performed on the server side. + */ +export function RoleEditorAdapter({ + resources, + onSave, + onDelete, + onCancel, +}: { + resources: ResourcesState; + onSave: (role: Partial) => Promise; + onDelete: () => Promise; + onCancel: () => void; +}) { + const theme = useTheme(); + const [convertAttempt, convertToRole] = useAsync( + async (yaml: string): Promise => { + if (resources.status === 'creating' || !resources.item) { + return null; + } + return { + yaml, + object: await yamlService.parse(YamlSupportedResourceKind.Role, { + yaml, + }), + }; + } + ); + + const originalContent = resources.item?.content ?? ''; + useEffect(() => { + convertToRole(originalContent); + }, [originalContent]); + + return ( + + + {convertAttempt.status === 'processing' && ( + + + + )} + {convertAttempt.status === 'error' && ( + {convertAttempt.statusText} + )} + {convertAttempt.status === 'success' && ( + + )} + + + {/* Same width as promo image + border */} + +

Teleport Policy

+ + +

+ Teleport Policy will visualize resource access paths as you + create and edit roles so you can always see what you are + granting before you push a role into production. +

+
+ + + Contact Sales + + + Learn More + + +
+ + + + + + +
+
+
+ ); +} + +const promoImageWidth = 782; + +const promoFlows = { + default: [PromoPanel1, PromoPanel2], +}; + +function PromoPanel1(props: StepComponentProps) { + return ( + + See what you’re granting before pushing to prod. Teleport Policy will + show resource access paths granted by your role before you save + changes. + + } + /> + ); +} + +function PromoPanel2(props: StepComponentProps) { + return ( + + Prevent mistakes. Teleport Policy shows you what access is removed and + what is added as you make edits to a role—all before you save your + changes. + + } + /> + ); +} + +function PromoPanel({ + prev, + next, + refCallback, + stepIndex, + flowLength, + heading, + content, +}: StepComponentProps & { + heading: React.ReactNode; + content: React.ReactNode; +}) { + return ( + + +

{heading}

+ + {content} + +
+ + + + + = flowLength - 1} + > + + + +
+ ); +} diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditorDialog.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditorDialog.tsx new file mode 100644 index 0000000000000..f5c6370820363 --- /dev/null +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditorDialog.tsx @@ -0,0 +1,128 @@ +/** + * 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 . + */ + +import Dialog from 'design/Dialog'; +import { forwardRef, useRef } from 'react'; +import { Transition, TransitionStatus } from 'react-transition-group'; +import { css } from 'styled-components'; + +import { State as ResourcesState } from 'teleport/components/useResources'; +import { RoleWithYaml } from 'teleport/services/resources'; + +import { RoleEditorAdapter } from './RoleEditorAdapter'; + +/** + * Renders a full-screen dialog with a slide-in effect. + * + * TODO(bl-nero): This component has been copied from `ReviewRequests` and + * `NotificationRoutingRulesDialog`. It probably should become reusable. + */ +export function RoleEditorDialog({ + open, + onClose, + resources, + onSave, + onDelete, +}: { + open: boolean; + onClose(): void; + resources: ResourcesState; + onSave(role: Partial): Promise; + onDelete(): Promise; +}) { + const transitionRef = useRef(); + return ( + + {transitionState => ( + + )} + + ); +} + +const DialogInternal = forwardRef< + HTMLDivElement, + { + onClose(): void; + transitionState: TransitionStatus; + resources: ResourcesState; + onSave(role: Partial): Promise; + onDelete(): Promise; + } +>(({ onClose, transitionState, resources, onSave, onDelete }, ref) => { + return ( + fullScreenDialogCss()} + disableEscapeKeyDown={false} + open={true} + ref={ref} + className={transitionState} + > + + + ); +}); + +const fullScreenDialogCss = () => css` + padding: 0; + width: 100%; + height: 100%; + max-height: 100%; + right: 0; + border-radius: 0; + overflow-y: hidden; + flex-direction: row; + background: ${props => props.theme.colors.levels.sunken}; + transition: width 300ms ease-out; + + &.entering { + right: -100%; + } + + &.entered { + right: 0px; + transition: right 300ms ease-out; + } + + &.exiting { + right: -100%; + transition: right 300ms ease-out; + } + + &.exited { + right: -100%; + } +`; diff --git a/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx b/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx index 3652e87a537ca..88899621defb8 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/Shared.tsx @@ -18,6 +18,7 @@ import { Box, ButtonPrimary, ButtonSecondary, Flex } from 'design'; import { HoverTooltip } from 'design/Tooltip'; +import { useTheme } from 'styled-components'; import useTeleport from 'teleport/useTeleport'; @@ -34,6 +35,7 @@ export const EditorSaveCancelButton = ({ }) => { const ctx = useTeleport(); const roleAccess = ctx.storeUser.getRoleAccess(); + const theme = useTheme(); let hoverTooltipContent = ''; if (isEditing && !roleAccess.edit) { @@ -43,7 +45,12 @@ export const EditorSaveCancelButton = ({ } return ( - + {roleModel.requiresReset && ( - + + + )} - + - - handleChange({ ...roleModel, metadata })} - /> - - - - {roleModel.accessSpecs.map((spec, i) => { - const validationResult = validation.accessSpecs[i]; - return ( - setAccessSpec(value)} - onRemove={() => removeAccessSpec(spec.kind)} - /> - ); - })} - - - - Add New Specifications - - } - buttonProps={{ - size: 'medium', - fill: 'filled', - disabled: isProcessing || allowedSpecKinds.length === 0, - }} - > - {allowedSpecKinds.map(kind => ( - addAccessSpec(kind)}> - {specSections[kind].title} - - ))} - - - - - - - - - - + + handleChange({ ...roleModel, metadata })} + /> + + + + {roleModel.accessSpecs.map((spec, i) => { + const validationResult = validation.accessSpecs[i]; + return ( + setAccessSpec(value)} + onRemove={() => removeAccessSpec(spec.kind)} + /> + ); + })} + + + + Add New Specifications + + } + buttonProps={{ + size: 'medium', + fill: 'filled', + disabled: isProcessing || allowedSpecKinds.length === 0, + }} + > + {allowedSpecKinds.map(kind => ( + addAccessSpec(kind)}> + {specSections[kind].title} + + ))} + + + + + + + + + + + handleSave()} @@ -1175,7 +1188,9 @@ const OptionLabel = styled(LabelInput)` ${props => props.theme.typography.body2} `; -export const EditorWrapper = styled(Box)<{ mute?: boolean }>` +export const EditorWrapper = styled(Flex)<{ mute?: boolean }>` + flex-direction: column; + flex: 1; opacity: ${p => (p.mute ? 0.4 : 1)}; pointer-events: ${p => (p.mute ? 'none' : '')}; `; diff --git a/web/packages/teleport/src/Roles/RoleEditor/YamlEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/YamlEditor.tsx index df8be5b3925db..5f58d7826f2bb 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/YamlEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/YamlEditor.tsx @@ -55,7 +55,7 @@ export const YamlEditor = ({ return ( - + r.id !== resources.item.id), })); - } - - function handleRoleEditorDelete() { - const id = resources.item?.id; - if (id) { - resources.remove(id); - } + // The new editor doesn't use `resources` to delete, so we need to close it + // by resetting the state here. + resources.disregard(); } const canCreate = rolesAcl.create; @@ -195,41 +184,43 @@ export function Roles(props: State) { /> - {/* New editor or descriptive text, depending on state. */} - {useNewRoleEditor && - (resources.status === 'creating' || resources.status === 'editing') ? ( - - ) : ( - -

Role-based access control

-

- Teleport Role-based access control (RBAC) provides fine-grained - control over who can access resources and in which contexts. A - Teleport role can be assigned automatically based on user identity - when used with single sign-on (SSO). -

-

- Learn more in{' '} - - the cluster management (RBAC) - {' '} - section of online documentation. -

-
)} + +

Role-based access control

+

+ Teleport Role-based access control (RBAC) provides fine-grained + control over who can access resources and in which contexts. A + Teleport role can be assigned automatically based on user identity + when used with single sign-on (SSO). +

+

+ Learn more in{' '} + + the cluster management (RBAC) + {' '} + section of online documentation. +

+
{/* Old editor. */} @@ -259,73 +250,6 @@ export function Roles(props: State) { ); } -/** - * This component is responsible for converting from the `Resource` - * representation of a role to a more accurate `RoleWithYaml` structure. The - * conversion is asynchronous and it's performed on the server side. - */ -function RoleEditorAdapter({ - resources, - onSave, - onDelete, -}: { - resources: ResourcesState; - onSave: (role: Partial) => Promise; - onDelete: () => void; -}) { - const theme = useTheme(); - const [convertAttempt, convertToRole] = useAsync( - async (yaml: string): Promise => { - if (resources.status === 'creating' || !resources.item) { - return null; - } - return { - yaml, - object: await yamlService.parse(YamlSupportedResourceKind.Role, { - yaml, - }), - }; - } - ); - - const originalContent = resources.item?.content ?? ''; - useEffect(() => { - convertToRole(originalContent); - }, [originalContent]); - - return ( - - {convertAttempt.status === 'processing' && ( - - - - )} - {convertAttempt.status === 'error' && ( - {convertAttempt.statusText} - )} - {convertAttempt.status === 'success' && ( - - )} - - ); -} - function Directions() { return ( <>