Skip to content

Commit

Permalink
Add buttons to download and copy to clipboard content to TextEditor (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
mcbattirola authored and michellescripts committed Feb 13, 2024
1 parent 35d44b5 commit af3bf46
Show file tree
Hide file tree
Showing 4 changed files with 247 additions and 2 deletions.
51 changes: 49 additions & 2 deletions web/packages/shared/components/TextEditor/TextEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,19 @@
*/

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');
Expand Down Expand Up @@ -104,20 +112,59 @@ class TextEditor extends React.Component {

render() {
const { bg = 'levels.sunken' } = this.props;
const hasButton = this.props.copyButton || this.props.downloadButton;

return (
<StyledTextEditor bg={bg}>
<div ref={e => (this.ace_viewer = e)} />
{hasButton && (
<ButtonSection>
{this.props.copyButton && (
<EditorButton
title="Copy to clipboard"
onClick={() => copyToClipboard(this.editor.session.getValue())}
>
<Copy size="medium" />
</EditorButton>
)}
{this.props.downloadButton && (
<EditorButton
title="Download"
onClick={() =>
downloadObject(
this.props.downloadFileName,
this.editor.session.getValue()
)
}
>
<Download size="medium" />
</EditorButton>
)}
</ButtonSection>
)}
</StyledTextEditor>
);
}
}

export default TextEditor;

function getMode(docType) {
if (docType === 'json') {
return 'ace/mode/json';
}

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;
95 changes: 95 additions & 0 deletions web/packages/shared/components/TextEditor/TextEditor.story.tsx
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import React from 'react';

import Flex from 'design/Flex';

import TextEditor from './TextEditor';

export default {
title: 'Shared/TextEditor',
};

export const Editor = () => {
return (
<Flex height="600px" width="600px" py={3} pr={3} bg="levels.deep">
<TextEditor
bg="levels.deep"
data={[
{
content,
type: 'yaml',
},
]}
/>
</Flex>
);
};

export const ReadOnly = () => {
return (
<Flex height="600px" width="600px" py={3} pr={3} bg="levels.deep">
<TextEditor
bg="levels.deep"
readOnly
data={[
{
content,
type: 'yaml',
},
]}
/>
</Flex>
);
};

export const WithButtons = () => {
return (
<Flex height="600px" width="600px" py={3} pr={3} bg="levels.deep">
<TextEditor
bg="levels.deep"
data={[
{
content,
type: 'yaml',
},
]}
copyButton
downloadButton
downloadFileName="content.yaml"
/>
</Flex>
);
};

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"]
`;
68 changes: 68 additions & 0 deletions web/packages/shared/components/TextEditor/TextEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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(
<TextEditor
copyButton
data={[
{
content: 'my-content',
type: 'yaml',
},
]}
/>
);
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(
<TextEditor
downloadButton
downloadFileName="test.yaml"
data={[
{
content: 'my-content',
type: 'yaml',
},
]}
/>
);
const mockedDownload = jest.spyOn(downloadsModule, 'downloadObject');

await userEvent.click(screen.getByTitle('Download'));
expect(mockedDownload).toHaveBeenCalledWith('test.yaml', 'my-content');
});
});
35 changes: 35 additions & 0 deletions web/packages/shared/utils/download.ts
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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);
};

0 comments on commit af3bf46

Please sign in to comment.