forked from Graylog2/graylog2-server
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add source editor component (Graylog2#4419)
* Add react-ace dependency * Add first version of SourceCodeEditor * Add read-only flag to SourceCodeEditor * Make SourceCodeEditor resizable * Do not require onChange prop * Allow to resize editor in all axes This is unfortunately not possible when setting `width` to `Infinity`, but at least can be used when it has other values. * Do not clear editor textarea on undo Reset undo history when the editor is fully loaded to avoid that users clear textare performing an undo command. The problem behind this is that the editor is created empty and ReactAce does a `setValue` to set the given value for the editor. This is registered as a change by ace, so when pressing undo in an apparently not-changed input, the input is reset. Fixes Graylog2/graylog-plugin-pipeline-processor#224 * Add property to disable ClipboardButton * Add toolbar with undo/redo controls * Enable/disable undo and redo buttons * Add copy/paste buttons Copying uses our ClipboardButton component that works without external dependencies. Pasting is currently not possible without the user triggering a system event for it, so we just show a small tooltip. * Add support for annotations * Add button titles to toolbar actions Hovering over the buttons should display a tooltip with the action they perform. * Adapt copy message to macOS too * Improve message when clicking on paste button * Make source editor non-resizable * Add support for multiple modes We only add support for languages that make more sense to write from the web interface. * Disable copy and paste buttons for read-only editors * Avoid some computations when disabled - Do not resize editor when is not resizable - Do not get text selection when copy button cannot be used * Update examples with latest changes I also noticed an error when using the component in an uncontrolled way, so I updated the examples and added a note on that regard. * Upgrade react-ace to 5.9.0 * Remove hack to reset undo history This was fixed upstream in securingsincity/react-ace#345 * Fix copy and paste disabled state
- Loading branch information
Showing
7 changed files
with
384 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
graylog2-web-interface/src/components/common/SourceCodeEditor.css
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
:local(.toolbar) { | ||
background: #f8f8f8; | ||
border: 1px solid #ccc; | ||
border-bottom: 0; | ||
border-radius: 5px 5px 0 0; | ||
} | ||
|
||
:local(.toolbar) .btn-link { | ||
color: #333; | ||
} | ||
|
||
/* Do not add border radius if code editor comes after toolbar */ | ||
:local(.toolbar) + :local(.sourceCodeEditor) .ace_editor { | ||
border-radius: 0 0 5px 5px; | ||
} | ||
|
||
:local(.sourceCodeEditor) .ace_editor { | ||
border: 1px solid #ccc; | ||
border-radius: 5px; | ||
} | ||
|
||
/* Ensure resize handle is over text editor */ | ||
:local(.sourceCodeEditor) .react-resizable-handle { | ||
z-index: 100; | ||
} | ||
|
||
/* Make resize handle visible on a dark background */ | ||
:local(.darkMode) .react-resizable-handle { | ||
filter: invert(100%) brightness(180%); | ||
} | ||
|
||
/* Hide resizable handle if editor is not resizable */ | ||
:local(.static) .react-resizable-handle { | ||
display: none; | ||
} |
205 changes: 205 additions & 0 deletions
205
graylog2-web-interface/src/components/common/SourceCodeEditor.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import React from 'react'; | ||
import { PropTypes } from 'prop-types'; | ||
import { Resizable } from 'react-resizable'; | ||
import 'brace'; | ||
import AceEditor from 'react-ace'; | ||
import { Button, ButtonGroup, ButtonToolbar, OverlayTrigger, Tooltip } from 'react-bootstrap'; | ||
|
||
import { ClipboardButton } from 'components/common'; | ||
|
||
import 'brace/mode/json'; | ||
import 'brace/mode/lua'; | ||
import 'brace/mode/markdown'; | ||
import 'brace/mode/text'; | ||
import 'brace/mode/yaml'; | ||
import 'brace/theme/tomorrow'; | ||
import 'brace/theme/monokai'; | ||
import style from './SourceCodeEditor.css'; | ||
|
||
/** | ||
* Component that renders a source code editor input. This is what powers the pipeline rules and collector | ||
* editors. | ||
* | ||
* **Note:** The component needs to be used in a [controlled way](https://reactjs.org/docs/forms.html#controlled-components). | ||
* Letting the component handle its own internal state may lead to weird errors while typing. | ||
*/ | ||
class SourceCodeEditor extends React.Component { | ||
static propTypes = { | ||
/** | ||
* Annotations to show in the editor's gutter. The format should be: | ||
* `[{ row: 0, column: 2, type: 'error', text: 'Some error.'}]` | ||
* The type value must be one of `error`, `warning`, or `info`. | ||
*/ | ||
annotations: PropTypes.array, | ||
/** Specifies if the source code editor should have the input focus or not. */ | ||
focus: PropTypes.bool, | ||
/** Specifies the font size in pixels to use in the text editor. */ | ||
fontSize: PropTypes.number, | ||
/** Editor height in pixels. */ | ||
height: PropTypes.number, | ||
/** Specifies a unique ID for the source code editor. */ | ||
id: PropTypes.string.isRequired, | ||
/** Specifies the mode to use in the editor. This is used for highlighting and auto-completion. */ | ||
mode: PropTypes.oneOf(['json', 'lua', 'markdown', 'text', 'yaml']), | ||
/** Function called on editor load. The first argument is the instance of the editor. */ | ||
onLoad: PropTypes.func, | ||
/** Function called when the value of the text changes. It receives the the new value and an event as arguments. */ | ||
onChange: PropTypes.func, | ||
/** Specifies if the editor should be in read-only mode. */ | ||
readOnly: PropTypes.bool, | ||
/** Specifies if the editor should be resizable by the user. */ | ||
resizable: PropTypes.bool, | ||
/** Specifies the theme to use for the editor. */ | ||
theme: PropTypes.oneOf(['light', 'dark']), | ||
/** Specifies if the editor should also include a toolbar. */ | ||
toolbar: PropTypes.bool, | ||
/** Text to use in the editor. */ | ||
value: PropTypes.string, | ||
/** Editor width in pixels. Use `Infinity` to indicate the editor should use 100% of its container's width. */ | ||
width: PropTypes.number, | ||
} | ||
|
||
static defaultProps = { | ||
annotations: [], | ||
focus: false, | ||
fontSize: 13, | ||
height: 200, | ||
mode: 'text', | ||
onChange: () => {}, | ||
onLoad: () => {}, | ||
readOnly: false, | ||
resizable: true, | ||
theme: 'light', | ||
toolbar: true, | ||
value: '', | ||
width: Infinity, | ||
} | ||
|
||
constructor(props) { | ||
super(props); | ||
this.state = { | ||
height: props.height, | ||
width: props.width, | ||
selectedText: '', | ||
}; | ||
} | ||
|
||
componentDidUpdate(prevProps) { | ||
if (this.props.height !== prevProps.height || this.props.width !== prevProps.width) { | ||
this.reloadEditor(); | ||
} | ||
} | ||
|
||
handleResize = (event, { size }) => { | ||
const { height, width } = size; | ||
this.setState({ height: height, width: width }, this.reloadEditor); | ||
} | ||
|
||
reloadEditor = () => { | ||
if (this.props.resizable) { | ||
this.reactAce.editor.resize(); | ||
} | ||
} | ||
|
||
isCopyDisabled = () => this.props.readOnly || this.state.selectedText === ''; | ||
|
||
isPasteDisabled = () => this.props.readOnly; | ||
|
||
isRedoDisabled = () => this.props.readOnly || !this.reactAce || !this.reactAce.editor.getSession().getUndoManager().hasRedo(); | ||
|
||
isUndoDisabled = () => this.props.readOnly || !this.reactAce || !this.reactAce.editor.getSession().getUndoManager().hasUndo(); | ||
|
||
handleRedo = () => { | ||
this.reactAce.editor.redo(); | ||
this.focusEditor(); | ||
} | ||
|
||
handleUndo = () => { | ||
this.reactAce.editor.undo(); | ||
this.focusEditor(); | ||
} | ||
|
||
handleSelectionChange = (selection) => { | ||
if (!this.reactAce || !this.props.toolbar || this.props.readOnly) { | ||
return; | ||
} | ||
const selectedText = this.reactAce.editor.getSession().getTextRange(selection.getRange()); | ||
this.setState({ selectedText: selectedText }); | ||
} | ||
|
||
focusEditor = () => { | ||
this.reactAce.editor.focus(); | ||
} | ||
|
||
render() { | ||
const { height, width } = this.state; | ||
const { theme, resizable } = this.props; | ||
const validCssWidth = Number.isNaN(width) ? '100%' : width; | ||
const containerStyle = `${style.sourceCodeEditor} ${theme !== 'light' && style.darkMode} ${!resizable && style.static}`; | ||
const overlay = <Tooltip id={'paste-button-tooltip'}>Press Ctrl+V (⌘V in macOS) or select Edit → Paste to paste from clipboard.</Tooltip>; | ||
return ( | ||
<div> | ||
{this.props.toolbar && | ||
<div className={style.toolbar} style={{ width: validCssWidth }}> | ||
<ButtonToolbar> | ||
<ButtonGroup> | ||
<ClipboardButton title={<i className="fa fa-copy fa-fw" />} | ||
bsStyle="link" | ||
bsSize="sm" | ||
onSuccess={this.focusEditor} | ||
text={this.state.selectedText} | ||
buttonTitle="Copy (Ctrl+C / ⌘C)" | ||
disabled={this.isCopyDisabled()} /> | ||
<OverlayTrigger placement="top" trigger="click" overlay={overlay} rootClose> | ||
<Button bsStyle="link" bsSize="sm" title="Paste (Ctrl+V / ⌘V)" disabled={this.isPasteDisabled()}> | ||
<i className="fa fa-paste fa-fw" /> | ||
</Button> | ||
</OverlayTrigger> | ||
</ButtonGroup> | ||
<ButtonGroup> | ||
<Button bsStyle="link" | ||
bsSize="sm" | ||
onClick={this.handleUndo} | ||
title="Undo (Ctrl+Z / ⌘Z)" | ||
disabled={this.isUndoDisabled()}> | ||
<i className="fa fa-undo fa-fw" /> | ||
</Button> | ||
<Button bsStyle="link" | ||
bsSize="sm" | ||
onClick={this.handleRedo} | ||
title="Redo (Ctrl+Shift+Z / ⌘⇧Z)" | ||
disabled={this.isRedoDisabled()}> | ||
<i className="fa fa-repeat fa-fw" /> | ||
</Button> | ||
</ButtonGroup> | ||
</ButtonToolbar> | ||
</div> | ||
} | ||
<Resizable height={height} | ||
width={width} | ||
minConstraints={[200, 200]} | ||
onResize={this.handleResize}> | ||
<div className={containerStyle} style={{ height: height, width: validCssWidth }}> | ||
<AceEditor ref={(c) => { this.reactAce = c; }} | ||
annotations={this.props.annotations} | ||
editorProps={{ $blockScrolling: 'Infinity' }} | ||
focus={this.props.focus} | ||
fontSize={this.props.fontSize} | ||
mode={this.props.mode} | ||
theme={this.props.theme === 'light' ? 'tomorrow' : 'monokai'} | ||
name={this.props.id} | ||
height="100%" | ||
onLoad={this.props.onLoad} | ||
onChange={this.props.onChange} | ||
onSelectionChange={this.handleSelectionChange} | ||
readOnly={this.props.readOnly} | ||
value={this.props.value} | ||
width="100%" /> | ||
</div> | ||
</Resizable> | ||
</div> | ||
); | ||
} | ||
} | ||
|
||
export default SourceCodeEditor; |
Oops, something went wrong.