From 23ea85f6477ff2000267943692d882529a9d5c10 Mon Sep 17 00:00:00 2001 From: matheus Date: Thu, 1 Feb 2024 17:12:23 -0300 Subject: [PATCH] Add buttons to download and copy to clipboard content to `TextEditor` (#37333) * Add editor buttons * Set icon size * Move downloadObject to OSS * remove unused test-id * Use values from theme * Fix buttons positioning * Convert jsx files to tsx * Use const instead of var * Add z-index to buttons * Add license --- .../components/TextEditor/TextEditor.jsx | 51 +++++++++- .../TextEditor/TextEditor.story.tsx | 95 +++++++++++++++++++ .../components/TextEditor/TextEditor.test.tsx | 68 +++++++++++++ web/packages/shared/utils/download.ts | 35 +++++++ 4 files changed, 247 insertions(+), 2 deletions(-) create mode 100644 web/packages/shared/components/TextEditor/TextEditor.story.tsx create mode 100644 web/packages/shared/components/TextEditor/TextEditor.test.tsx create mode 100644 web/packages/shared/utils/download.ts diff --git a/web/packages/shared/components/TextEditor/TextEditor.jsx b/web/packages/shared/components/TextEditor/TextEditor.jsx index a119768d7a852..ef368ac91bfc9 100644 --- a/web/packages/shared/components/TextEditor/TextEditor.jsx +++ b/web/packages/shared/components/TextEditor/TextEditor.jsx @@ -15,11 +15,19 @@ limitations under the License. */ import React from 'react'; +import styled from 'styled-components'; import ace from 'ace-builds/src-min-noconflict/ace'; import 'ace-builds/src-noconflict/mode-json'; import 'ace-builds/src-noconflict/mode-yaml'; import 'ace-builds/src-noconflict/ext-searchbox'; +import { ButtonSecondary } from 'design/Button'; +import Flex from 'design/Flex'; +import { Copy, Download } from 'design/Icon'; +import { copyToClipboard } from 'design/utils/copyToClipboard'; + +import { downloadObject } from 'shared/utils/download'; + import StyledTextEditor from './StyledTextEditor'; const { UndoManager } = ace.require('ace/undomanager'); @@ -102,16 +110,41 @@ class TextEditor extends React.Component { render() { const { bg = 'levels.sunken' } = this.props; + const hasButton = this.props.copyButton || this.props.downloadButton; + return (
(this.ace_viewer = e)} /> + {hasButton && ( + + {this.props.copyButton && ( + copyToClipboard(this.editor.session.getValue())} + > + + + )} + {this.props.downloadButton && ( + + downloadObject( + this.props.downloadFileName, + this.editor.session.getValue() + ) + } + > + + + )} + + )} ); } } -export default TextEditor; - function getMode(docType) { if (docType === 'json') { return 'ace/mode/json'; @@ -119,3 +152,17 @@ function getMode(docType) { return 'ace/mode/yaml'; } + +const EditorButton = styled(ButtonSecondary)` + padding: ${({ theme }) => theme.space[2]}px; + background-color: transparent; +`; + +const ButtonSection = styled(Flex)` + position: absolute; + right: 0; + top: 0; + z-index: 10; +`; + +export default TextEditor; diff --git a/web/packages/shared/components/TextEditor/TextEditor.story.tsx b/web/packages/shared/components/TextEditor/TextEditor.story.tsx new file mode 100644 index 0000000000000..9d51d3d77ce7e --- /dev/null +++ b/web/packages/shared/components/TextEditor/TextEditor.story.tsx @@ -0,0 +1,95 @@ +/* + * 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 React from 'react'; + +import Flex from 'design/Flex'; + +import TextEditor from './TextEditor'; + +export default { + title: 'Shared/TextEditor', +}; + +export const Editor = () => { + return ( + + + + ); +}; + +export const ReadOnly = () => { + return ( + + + + ); +}; + +export const WithButtons = () => { + return ( + + + + ); +}; + +const content = `# example +kind: github +version: v3 +metadata: + name: github +spec: + client_id: client-id + client_secret: client-secret + display: GitHub + redirect_url: https://tele.example.com:443/v1/webapi/github/callback + teams_to_roles: + - organization: octocats + team: admin + roles: ["access"] +`; diff --git a/web/packages/shared/components/TextEditor/TextEditor.test.tsx b/web/packages/shared/components/TextEditor/TextEditor.test.tsx new file mode 100644 index 0000000000000..d8868b64e49f7 --- /dev/null +++ b/web/packages/shared/components/TextEditor/TextEditor.test.tsx @@ -0,0 +1,68 @@ +/** + * 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 React from 'react'; +import { render, userEvent, screen } from 'design/utils/testing'; +import * as copyModule from 'design/utils/copyToClipboard'; + +import * as downloadsModule from 'shared/utils/download'; + +import TextEditor from '.'; + +describe('textEditor tests', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('copy content button', async () => { + render( + + ); + const mockedCopyToClipboard = jest.spyOn(copyModule, 'copyToClipboard'); + + await userEvent.click(screen.getByTitle('Copy to clipboard')); + expect(mockedCopyToClipboard).toHaveBeenCalledWith('my-content'); + }); + + test('download content button', async () => { + render( + + ); + const mockedDownload = jest.spyOn(downloadsModule, 'downloadObject'); + + await userEvent.click(screen.getByTitle('Download')); + expect(mockedDownload).toHaveBeenCalledWith('test.yaml', 'my-content'); + }); +}); diff --git a/web/packages/shared/utils/download.ts b/web/packages/shared/utils/download.ts new file mode 100644 index 0000000000000..a91a51df02553 --- /dev/null +++ b/web/packages/shared/utils/download.ts @@ -0,0 +1,35 @@ +/** + * 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 . + */ + +export const downloadObject = (filename: string, text: string) => { + /* + * http://stackoverflow.com/questions/3665115/create-a-file-in-memory-for-user-to-download-not-through-server + */ + const element = document.createElement('a'); + element.setAttribute( + 'href', + 'data:text/plain;charset=utf-8,' + encodeURIComponent(text) + ); + element.setAttribute('download', filename); + element.style.display = 'none'; + + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); +};