Skip to content

Commit

Permalink
[explore] refactor slice action button group (#1074)
Browse files Browse the repository at this point in the history
* pull explore actions button group into component

* use button component

* make sure we render all action buttons

* test that embed code is correct

* don't need before each

* generalize modal trigger for use with plain links or icons
  • Loading branch information
Alanna Scott committed Sep 20, 2016
1 parent 32980a6 commit 0e7af8d
Show file tree
Hide file tree
Showing 17 changed files with 592 additions and 203 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class QueryTable extends React.Component {
this.props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
}
notImplemented() {
/* eslint no-alert: 0 */
alert('Not implemented yet!');
}
render() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let queryCount = 1;

class QueryEditors extends React.Component {
renameTab(qe) {
/* eslint no-alert: 0 */
const newTitle = prompt('Enter a new title for the tab');
if (newTitle) {
this.props.actions.queryEditorSetTitle(qe, newTitle);
Expand Down
102 changes: 102 additions & 0 deletions caravel/assets/javascripts/components/CopyToClipboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { PropTypes } from 'react';
import { Button, Tooltip, OverlayTrigger } from 'react-bootstrap';

const propTypes = {
copyNode: PropTypes.node,
onCopyEnd: PropTypes.func,
shouldShowText: PropTypes.bool,
text: PropTypes.string.isRequired,
};

const defaultProps = {
copyNode: <span>Copy</span>,
onCopyEnd: () => {},
shouldShowText: true,
};

export default class CopyToClipboard extends React.Component {
constructor(props) {
super(props);
this.state = {
hasCopied: false,
};

// bindings
this.copyToClipboard = this.copyToClipboard.bind(this);
this.resetTooltipText = this.resetTooltipText.bind(this);
this.onMouseOut = this.onMouseOut.bind(this);
}

onMouseOut() {
// delay to avoid flash of text change on tooltip
setTimeout(this.resetTooltipText, 200);
}

resetTooltipText() {
this.setState({ hasCopied: false });
}

copyToClipboard() {
const textToCopy = this.props.text;
const textArea = document.createElement('textarea');

textArea.style.position = 'fixed';
textArea.style.left = '-1000px';
textArea.value = textToCopy;

document.body.appendChild(textArea);
textArea.select();

try {
if (!document.execCommand('copy')) {
throw new Error('Not successful');
}
} catch (err) {
window.alert('Sorry, your browser does not support copying. Use Ctrl / Cmd + C!'); // eslint-disable-line
}

document.body.removeChild(textArea);

this.setState({ hasCopied: true });
this.props.onCopyEnd();
}

tooltipText() {
let tooltipText;
if (this.state.hasCopied) {
tooltipText = 'Copied!';
} else {
tooltipText = 'Copy text';
}
return tooltipText;
}

render() {
const tooltip = (
<Tooltip id="copy-to-clipboard-tooltip">
{this.tooltipText()}
</Tooltip>
);

return (
<div>
{this.props.shouldShowText &&
<span>{this.props.text}</span>
}
&nbsp;&nbsp;&nbsp;&nbsp;
<OverlayTrigger placement="top" overlay={tooltip} trigger={['hover']}>
<Button
bsStyle="link"
onClick={this.copyToClipboard}
onMouseOut={this.onMouseOut}
>
{this.props.copyNode}
</Button>
</OverlayTrigger>
</div>
);
}
}

CopyToClipboard.propTypes = propTypes;
CopyToClipboard.defaultProps = defaultProps;
59 changes: 59 additions & 0 deletions caravel/assets/javascripts/components/ModalTrigger.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { PropTypes } from 'react';
import { Modal } from 'react-bootstrap';
import cx from 'classnames';

const propTypes = {
triggerNode: PropTypes.node.isRequired,
modalTitle: PropTypes.string.isRequired,
modalBody: PropTypes.node.isRequired,
beforeOpen: PropTypes.func,
isButton: PropTypes.bool,
};

const defaultProps = {
beforeOpen: () => {},
isButton: false,
};

export default class ModalTrigger extends React.Component {
constructor(props) {
super(props);
this.state = {
showModal: false,
};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}

close() {
this.setState({ showModal: false });
}

open(e) {
e.preventDefault();
this.props.beforeOpen();
this.setState({ showModal: true });
}

render() {
const classNames = cx({
'btn btn-default btn-sm': this.props.isButton,
});
return (
<a href="#" className={classNames} onClick={this.open}>
{this.props.triggerNode}
<Modal show={this.state.showModal} onHide={this.close}>
<Modal.Header closeButton>
<Modal.Title>{this.props.modalTitle}</Modal.Title>
</Modal.Header>
<Modal.Body>
{this.props.modalBody}
</Modal.Body>
</Modal>
</a>
);
}
}

ModalTrigger.propTypes = propTypes;
ModalTrigger.defaultProps = defaultProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { PropTypes } from 'react';
import ModalTrigger from './../../components/ModalTrigger';

const propTypes = {
slice: PropTypes.object.isRequired,
};

export default class DisplayQueryButton extends React.Component {
constructor(props) {
super(props);
this.state = {
viewSqlQuery: '',
};
this.beforeOpen = this.beforeOpen.bind(this);
}

beforeOpen() {
this.setState({
viewSqlQuery: this.props.slice.viewSqlQuery,
});
}

render() {
const modalBody = (<pre>{this.state.viewSqlQuery}</pre>);
return (
<ModalTrigger
isButton
triggerNode={<span>Query</span>}
modalTitle="Query"
modalBody={modalBody}
beforeOpen={this.beforeOpen}
/>
);
}
}

DisplayQueryButton.propTypes = propTypes;
105 changes: 105 additions & 0 deletions caravel/assets/javascripts/explore/components/EmbedCodeButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { PropTypes } from 'react';
import CopyToClipboard from './../../components/CopyToClipboard';
import { Popover, OverlayTrigger } from 'react-bootstrap';

const propTypes = {
slice: PropTypes.object.isRequired,
};

export default class EmbedCodeButton extends React.Component {
constructor(props) {
super(props);
this.state = {
height: '400',
width: '600',
srcLink: window.location.origin + props.slice.data.standalone_endpoint,
};
this.handleInputChange = this.handleInputChange.bind(this);
}

handleInputChange(e) {
const value = e.currentTarget.value;
const name = e.currentTarget.name;
const data = {};
data[name] = value;
this.setState(data);
}

generateEmbedHTML() {
const { width, height, srcLink } = this.state;
/* eslint max-len: 0 */
const embedHTML =
`<iframe src="${srcLink}" width="${width}" height="${height}" seamless frameBorder="0" scrolling="no"></iframe>`;
return embedHTML;
}

renderPopover() {
const html = this.generateEmbedHTML();
return (
<Popover id="embed-code-popover">
<div>
<div className="row">
<div className="col-sm-10">
<textarea name="embedCode" value={html} rows="4" readOnly className="form-control input-sm"></textarea>
</div>
<div className="col-sm-2">
<CopyToClipboard
shouldShowText={false}
text={html}
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard"></i>}
/>
</div>
</div>
<br />
<div className="row">
<div className="col-md-6 col-sm-12">
<div className="form-group">
<small>
<label className="control-label" htmlFor="embed-height">Height</label>
</small>
<input
className="form-control input-sm"
type="text"
defaultValue={this.state.height}
name="height"
onChange={this.handleInputChange}
/>
</div>
</div>
<div className="col-md-6 col-sm-12">
<div className="form-group">
<small>
<label className="control-label" htmlFor="embed-width">Width</label>
</small>
<input
className="form-control input-sm"
type="text"
defaultValue={this.state.width}
name="width"
onChange={this.handleInputChange}
id="embed-width"
/>
</div>
</div>
</div>
</div>
</Popover>
);
}
render() {
return (
<OverlayTrigger
trigger="click"
rootClose
placement="left"
overlay={this.renderPopover()}
>
<span className="btn btn-default btn-sm">
<i className="fa fa-code"></i>&nbsp;
</span>
</OverlayTrigger>
);
}
}

EmbedCodeButton.propTypes = propTypes;
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import React, { PropTypes } from 'react';
import cx from 'classnames';
import URLShortLinkButton from './URLShortLinkButton';
import EmbedCodeButton from './EmbedCodeButton';
import DisplayQueryButton from './DisplayQueryButton';

const propTypes = {
canDownload: PropTypes.string.isRequired,
slice: PropTypes.object.isRequired,
};

export default function ExploreActionButtons({ canDownload, slice }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload,
});

return (
<div className="btn-group results" role="group">
<URLShortLinkButton slice={slice} />

<EmbedCodeButton slice={slice} />

<a
href={slice.data.json_endpoint}
className="btn btn-default btn-sm"
title="Export to .json"
target="_blank"
>
<i className="fa fa-file-code-o"></i> .json
</a>

<a
href={slice.data.csv_endpoint}
className={exportToCSVClasses}
title="Export to .csv format"
target="_blank"
>
<i className="fa fa-file-text-o"></i> .csv
</a>

<DisplayQueryButton slice={slice} />
</div>
);
}

ExploreActionButtons.propTypes = propTypes;
Loading

0 comments on commit 0e7af8d

Please sign in to comment.