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

Move the new role editor to a full-screen dialog #49881

Merged
merged 16 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
5 changes: 5 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import (
)

const (
// The `Kind*` constants in this const block identify resource kinds used for
// storage an/or and access control. Please keep these in sync with the
// `ResourceKind` enum in
// `web/packages/teleport/src/services/resources/types.ts`.

// DefaultAPIGroup is a default group of permissions API,
// lets us to add different permission types
DefaultAPIGroup = "gravitational.io/teleport"
Expand Down
2 changes: 1 addition & 1 deletion web/packages/design/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import CardSuccess, { CardSuccessLogin } from './CardSuccess';
import { Indicator } from './Indicator';
import Input from './Input';
import Label from './Label';
import LabelInput from './LabelInput';
import { LabelInput } from './LabelInput';
import LabelState from './LabelState';
import Link from './Link';
import { Mark } from './Mark';
Expand Down
14 changes: 10 additions & 4 deletions web/packages/teleport/src/Roles/RoleEditor/EditorHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +36,7 @@ export const EditorHeader = ({
isProcessing,
standardEditorId,
yamlEditorId,
onClose,
}: {
role?: Role;
onDelete(): void;
Expand All @@ -43,6 +45,7 @@ export const EditorHeader = ({
isProcessing: boolean;
standardEditorId: string;
yamlEditorId: string;
onClose(): void;
}) => {
const ctx = useTeleport();
const isCreating = !role;
Expand All @@ -51,6 +54,9 @@ export const EditorHeader = ({

return (
<Flex alignItems="center" mb={3} gap={2}>
<ButtonIcon aria-label="Close" onClick={onClose}>
<Cross size="small" />
</ButtonIcon>
<Box flex="1">
<H2>
{isCreating
Expand All @@ -77,14 +83,14 @@ export const EditorHeader = ({
: 'You do not have access to delete a role'
}
>
<ButtonText
<ButtonIcon
onClick={onDelete}
disabled={!hasDeleteAccess}
data-testid="delete"
p={1}
>
<Trash size="medium" />
</ButtonText>
</ButtonIcon>
</HoverTooltip>
)}
</Flex>
Expand Down
32 changes: 29 additions & 3 deletions web/packages/teleport/src/Roles/RoleEditor/RoleEditor.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

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',
Expand All @@ -42,7 +44,7 @@ export default {
}
return (
<TeleportContextProvider ctx={ctx}>
<Flex flexDirection="column" width="500px" height="800px">
<Flex flexDirection="column" width="700px" height="800px">
<Story />
</Flex>
</TeleportContextProvider>
Expand Down Expand Up @@ -264,6 +266,30 @@ export const noAccess: StoryObj = {
},
};

export const Dialog: StoryObj = {
render() {
const [open, setOpen] = useState(false);
const resources = useResources([], {});
return (
<>
<ButtonPrimary onClick={() => setOpen(true)}>Open</ButtonPrimary>
<RoleEditorDialog
resources={resources}
open={open}
onClose={() => setOpen(false)}
onSave={async () => setOpen(false)}
onDelete={async () => setOpen(false)}
/>
</>
);
},
parameters: {
msw: {
handlers: [yamlifyHandler, parseHandler],
},
},
};

const dummyRoleYaml = `kind: role
metadata:
name: dummy-role
Expand Down
129 changes: 78 additions & 51 deletions web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { Alert, Flex } from 'design';
import { Alert, Box, Flex } from 'design';
import React, { useId, useState } from 'react';
import { useAsync } from 'shared/hooks/useAsync';

import Validation, { Validator } from 'shared/components/Validation';

import { Role, RoleWithYaml } from 'teleport/services/resources';
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,
Expand All @@ -45,7 +49,7 @@ export type RoleEditorProps = {
originalRole?: RoleWithYaml;
onCancel?(): void;
onSave?(r: Partial<RoleWithYaml>): Promise<void>;
onDelete?(): void;
onDelete?(): Promise<void>;
};

/**
Expand Down Expand Up @@ -86,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<Role>(
Expand Down Expand Up @@ -119,11 +125,18 @@ export const RoleEditor = ({
yamlifyAttempt.status === 'processing' ||
saveAttempt.status === 'processing';

async function onTabChange(activeIndex: EditorTab) {
async function onTabChange(activeIndex: EditorTab, validator: Validator) {
// The code below is not idempotent, so we need to protect ourselves from
// an accidental model replacement.
if (activeIndex === selectedEditorTab) return;

// Validate the model on tab switch, because the server-side yamlification
// requires model to be valid. However, if it's OK, we reset the validator.
// We don't want it to be validating at this point, since the user didn't
// attempt to submit the form.
if (!validator.validate()) return;
validator.reset();

switch (activeIndex) {
case EditorTab.Standard: {
if (!yamlModel.content) {
Expand Down Expand Up @@ -169,55 +182,69 @@ export const RoleEditor = ({
}

return (
<Flex flexDirection="column" flex="1">
<EditorHeader
role={originalRole?.object}
onDelete={onDelete}
selectedEditorTab={selectedEditorTab}
onEditorTabChange={onTabChange}
isProcessing={isProcessing}
standardEditorId={standardEditorId}
yamlEditorId={yamlEditorId}
/>
{saveAttempt.status === 'error' && (
<Alert mt={3} dismissible>
{saveAttempt.statusText}
</Alert>
)}
{parseAttempt.status === 'error' && (
<Alert mt={3} dismissible>
{parseAttempt.statusText}
</Alert>
)}
{yamlifyAttempt.status === 'error' && (
<Alert mt={3} dismissible>
{yamlifyAttempt.statusText}
</Alert>
)}
{selectedEditorTab === EditorTab.Standard && (
<div id={standardEditorId}>
<StandardEditor
originalRole={originalRole}
onSave={object => handleSave({ object })}
onCancel={handleCancel}
standardEditorModel={standardModel}
isProcessing={isProcessing}
onChange={setStandardModel}
/>
</div>
)}
{selectedEditorTab === EditorTab.Yaml && (
<Flex flexDirection="column" flex="1" id={yamlEditorId}>
<YamlEditor
yamlEditorModel={yamlModel}
onChange={setYamlModel}
onSave={async yaml => void (await handleSave({ yaml }))}
isProcessing={isProcessing}
onCancel={handleCancel}
originalRole={originalRole}
/>
<Validation>
{({ validator }) => (
<Flex flexDirection="column" flex="1">
<Box mt={3} mx={3}>
<EditorHeader
role={originalRole?.object}
onDelete={() => setDeleting(true)}
selectedEditorTab={selectedEditorTab}
onEditorTabChange={index => onTabChange(index, validator)}
isProcessing={isProcessing}
standardEditorId={standardEditorId}
yamlEditorId={yamlEditorId}
onClose={onCancel}
/>
{saveAttempt.status === 'error' && (
<Alert mt={3} dismissible>
{saveAttempt.statusText}
</Alert>
)}
{parseAttempt.status === 'error' && (
<Alert mt={3} dismissible>
{parseAttempt.statusText}
</Alert>
)}
{yamlifyAttempt.status === 'error' && (
<Alert mt={3} dismissible>
{yamlifyAttempt.statusText}
</Alert>
)}
</Box>
{selectedEditorTab === EditorTab.Standard && (
<Flex flexDirection="column" flex="1" id={standardEditorId}>
<StandardEditor
originalRole={originalRole}
onSave={object => handleSave({ object })}
onCancel={handleCancel}
standardEditorModel={standardModel}
isProcessing={isProcessing}
onChange={setStandardModel}
/>
</Flex>
)}
{selectedEditorTab === EditorTab.Yaml && (
<Flex flexDirection="column" flex="1" id={yamlEditorId}>
<YamlEditor
yamlEditorModel={yamlModel}
onChange={setYamlModel}
onSave={async yaml => void (await handleSave({ yaml }))}
isProcessing={isProcessing}
onCancel={handleCancel}
originalRole={originalRole}
/>
</Flex>
)}
{deleting && (
<DeleteRole
name={originalRole.object.metadata.name}
onClose={() => setDeleting(false)}
onDelete={onDelete}
/>
)}
</Flex>
)}
</Flex>
</Validation>
);
};
Loading
Loading