mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
[explore] refactor slice action button group (#1074)
* 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
This commit is contained in:
parent
32980a653c
commit
0e7af8d8a6
@ -33,6 +33,7 @@ class QueryTable extends React.Component {
|
|||||||
this.props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
|
this.props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
|
||||||
}
|
}
|
||||||
notImplemented() {
|
notImplemented() {
|
||||||
|
/* eslint no-alert: 0 */
|
||||||
alert('Not implemented yet!');
|
alert('Not implemented yet!');
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
|
@ -10,6 +10,7 @@ let queryCount = 1;
|
|||||||
|
|
||||||
class QueryEditors extends React.Component {
|
class QueryEditors extends React.Component {
|
||||||
renameTab(qe) {
|
renameTab(qe) {
|
||||||
|
/* eslint no-alert: 0 */
|
||||||
const newTitle = prompt('Enter a new title for the tab');
|
const newTitle = prompt('Enter a new title for the tab');
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
this.props.actions.queryEditorSetTitle(qe, newTitle);
|
||||||
|
102
caravel/assets/javascripts/components/CopyToClipboard.jsx
Normal file
102
caravel/assets/javascripts/components/CopyToClipboard.jsx
Normal file
@ -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>
|
||||||
|
}
|
||||||
|
|
||||||
|
<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
caravel/assets/javascripts/components/ModalTrigger.jsx
Normal file
59
caravel/assets/javascripts/components/ModalTrigger.jsx
Normal file
@ -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;
|
@ -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;
|
@ -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>
|
||||||
|
</span>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedCodeButton.propTypes = propTypes;
|
@ -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;
|
@ -0,0 +1,71 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Popover, OverlayTrigger } from 'react-bootstrap';
|
||||||
|
import CopyToClipboard from './../../components/CopyToClipboard';
|
||||||
|
import $ from 'jquery';
|
||||||
|
|
||||||
|
const propTypes = {
|
||||||
|
slice: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class URLShortLinkButton extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
shortUrl: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
this.getShortUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
getShortUrl() {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: '/r/shortner/',
|
||||||
|
data: {
|
||||||
|
data: '/' + window.location.pathname + this.props.slice.querystring(),
|
||||||
|
},
|
||||||
|
success: (data) => {
|
||||||
|
this.setState({
|
||||||
|
shortUrl: data,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
/* eslint no-console: 0 */
|
||||||
|
if (console && console.warn) {
|
||||||
|
console.warn('Something went wrong...');
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPopover() {
|
||||||
|
return (
|
||||||
|
<Popover id="shorturl-popover">
|
||||||
|
<CopyToClipboard
|
||||||
|
text={this.state.shortUrl}
|
||||||
|
copyNode={<i className="fa fa-clipboard" title="Copy to clipboard"></i>}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const shortUrl = this.state.shortUrl;
|
||||||
|
const isDisabled = shortUrl === '';
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
trigger="click"
|
||||||
|
rootClose
|
||||||
|
placement="left"
|
||||||
|
overlay={this.renderPopover()}
|
||||||
|
>
|
||||||
|
<span className="btn btn-default btn-sm" disabled={isDisabled}>
|
||||||
|
<i className="fa fa-link"></i>
|
||||||
|
</span>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
URLShortLinkButton.propTypes = propTypes;
|
@ -11,6 +11,7 @@ const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
|
import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
|
||||||
|
import ExploreActionButtons from './components/ExploreActionButtons.jsx';
|
||||||
|
|
||||||
require('jquery-ui');
|
require('jquery-ui');
|
||||||
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
|
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
|
||||||
@ -62,7 +63,6 @@ function query(forceUpdate, pushState) {
|
|||||||
force = false;
|
force = false;
|
||||||
}
|
}
|
||||||
$('.query-and-save button').attr('disabled', 'disabled');
|
$('.query-and-save button').attr('disabled', 'disabled');
|
||||||
$('.btn-group.results span,a').attr('disabled', 'disabled');
|
|
||||||
if (force) { // Don't hide the alert message when the page is just loaded
|
if (force) { // Don't hide the alert message when the page is just loaded
|
||||||
$('div.alert').remove();
|
$('div.alert').remove();
|
||||||
}
|
}
|
||||||
@ -160,148 +160,6 @@ function initExploreView() {
|
|||||||
|
|
||||||
px.initFavStars();
|
px.initFavStars();
|
||||||
|
|
||||||
function copyURLToClipboard(url) {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.style.position = 'fixed';
|
|
||||||
textArea.style.left = '-1000px';
|
|
||||||
textArea.value = url;
|
|
||||||
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
let successful;
|
|
||||||
try {
|
|
||||||
successful = document.execCommand('copy');
|
|
||||||
if (!successful) {
|
|
||||||
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);
|
|
||||||
return successful;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#shortner').click(function () {
|
|
||||||
$.ajax({
|
|
||||||
type: 'POST',
|
|
||||||
url: '/r/shortner/',
|
|
||||||
data: {
|
|
||||||
data: '/' + window.location.pathname + slice.querystring(),
|
|
||||||
},
|
|
||||||
success(data) {
|
|
||||||
const close = (
|
|
||||||
'<a style="cursor: pointer;">' +
|
|
||||||
'<i class="fa fa-close" id="close_shortner"></i>' +
|
|
||||||
'</a>'
|
|
||||||
);
|
|
||||||
const copy = (
|
|
||||||
'<a style="cursor: pointer;">' +
|
|
||||||
'<i class="fa fa-clipboard" title="Copy to clipboard" id="copy_url"></i>' +
|
|
||||||
'</a>'
|
|
||||||
);
|
|
||||||
const spaces = ' ';
|
|
||||||
const popover = data + spaces + copy + spaces + close;
|
|
||||||
|
|
||||||
const $shortner = $('#shortner')
|
|
||||||
.popover({
|
|
||||||
content: popover,
|
|
||||||
placement: 'left',
|
|
||||||
html: true,
|
|
||||||
trigger: 'manual',
|
|
||||||
})
|
|
||||||
.popover('show');
|
|
||||||
function destroyPopover() {
|
|
||||||
$shortner.popover('destroy');
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#copy_url')
|
|
||||||
.tooltip()
|
|
||||||
.click(function () {
|
|
||||||
const success = copyURLToClipboard(data);
|
|
||||||
if (success) {
|
|
||||||
$(this).attr('data-original-title', 'Copied!')
|
|
||||||
.tooltip('fixTitle')
|
|
||||||
.tooltip('show');
|
|
||||||
window.setTimeout(destroyPopover, 1200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$('#close_shortner').click(destroyPopover);
|
|
||||||
},
|
|
||||||
error(error) {
|
|
||||||
showModal({
|
|
||||||
title: 'Error',
|
|
||||||
body: 'Sorry, an error occurred during this operation:<br/>' + error,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#standalone').click(function () {
|
|
||||||
const srcLink = window.location.origin + slice.data.standalone_endpoint;
|
|
||||||
const close = (
|
|
||||||
'<a style="cursor: pointer;">' +
|
|
||||||
'<i class="fa fa-close" id="close_standalone"></i>' +
|
|
||||||
'</a>'
|
|
||||||
);
|
|
||||||
const copy = (
|
|
||||||
'<a style="cursor: pointer;">' +
|
|
||||||
'<i class="fa fa-clipboard" title="Copy to clipboard" id="copy_embed"></i>' +
|
|
||||||
'</a>'
|
|
||||||
);
|
|
||||||
const spaces = ' ';
|
|
||||||
const widthInput = '<input type="number" id="standalone_width" placeholder="width">';
|
|
||||||
const heightInput = '<input type="number" id="standalone_height" placeholder="height">';
|
|
||||||
let popover = "<input id='standalone_text' value='' disabled></input>";
|
|
||||||
popover = popover + spaces + copy + spaces + close + spaces + widthInput + spaces + heightInput;
|
|
||||||
let dataToCopy = '';
|
|
||||||
|
|
||||||
const $standalone = $(this);
|
|
||||||
|
|
||||||
function destroyPopover() {
|
|
||||||
$standalone.popover('destroy');
|
|
||||||
}
|
|
||||||
|
|
||||||
$standalone.popover({
|
|
||||||
content: popover,
|
|
||||||
title: 'embed html',
|
|
||||||
placement: 'left',
|
|
||||||
html: true,
|
|
||||||
trigger: 'manual',
|
|
||||||
})
|
|
||||||
.popover('show');
|
|
||||||
$('#copy_embed').tooltip().click(function () {
|
|
||||||
const success = copyURLToClipboard(dataToCopy);
|
|
||||||
if (success) {
|
|
||||||
$(this).attr('data-original-title', 'Copied!')
|
|
||||||
.tooltip('fixTitle')
|
|
||||||
.tooltip('show');
|
|
||||||
window.setTimeout(destroyPopover, 1200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#close_standalone').click(destroyPopover);
|
|
||||||
|
|
||||||
const $standaloneWidth = $('#standalone_width');
|
|
||||||
const $standaloneHeight = $('#standalone_height');
|
|
||||||
const $standaloneText = $('#standalone_text');
|
|
||||||
|
|
||||||
function generateEmbedHTML() {
|
|
||||||
const width = $standaloneWidth.val();
|
|
||||||
const height = $standaloneHeight.val();
|
|
||||||
dataToCopy = `<iframe src="${srcLink}" width="${width}" height="${height}"`;
|
|
||||||
dataToCopy = dataToCopy + ' seamless frameBorder="0" scrolling="no"></iframe>';
|
|
||||||
$standaloneText.val(dataToCopy);
|
|
||||||
}
|
|
||||||
|
|
||||||
$standaloneHeight.change(function () {
|
|
||||||
generateEmbedHTML();
|
|
||||||
});
|
|
||||||
$standaloneWidth.change(function () {
|
|
||||||
generateEmbedHTML();
|
|
||||||
});
|
|
||||||
generateEmbedHTML();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#viz_type').change(function () {
|
$('#viz_type').change(function () {
|
||||||
$('#query').submit();
|
$('#query').submit();
|
||||||
});
|
});
|
||||||
@ -386,15 +244,6 @@ function initExploreView() {
|
|||||||
addFilter(undefined, 'having');
|
addFilter(undefined, 'having');
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
|
|
||||||
ReactDOM.render(
|
|
||||||
<QueryAndSaveBtns
|
|
||||||
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
|
|
||||||
onQuery={() => query(true)}
|
|
||||||
/>,
|
|
||||||
queryAndSaveBtnsEl
|
|
||||||
);
|
|
||||||
|
|
||||||
function createChoices(term, data) {
|
function createChoices(term, data) {
|
||||||
const filtered = $(data).filter(function () {
|
const filtered = $(data).filter(function () {
|
||||||
return this.text.localeCompare(term) === 0;
|
return this.text.localeCompare(term) === 0;
|
||||||
@ -487,6 +336,26 @@ function initExploreView() {
|
|||||||
prepSaveDialog();
|
prepSaveDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initComponents() {
|
||||||
|
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
|
||||||
|
ReactDOM.render(
|
||||||
|
<QueryAndSaveBtns
|
||||||
|
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
|
||||||
|
onQuery={() => query(true)}
|
||||||
|
/>,
|
||||||
|
queryAndSaveBtnsEl
|
||||||
|
);
|
||||||
|
|
||||||
|
const exploreActionsEl = document.getElementById('js-explore-actions');
|
||||||
|
ReactDOM.render(
|
||||||
|
<ExploreActionButtons
|
||||||
|
canDownload={exploreActionsEl.getAttribute('data-can-download')}
|
||||||
|
slice={slice}
|
||||||
|
/>,
|
||||||
|
exploreActionsEl
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
const data = $('.slice').data('slice');
|
const data = $('.slice').data('slice');
|
||||||
|
|
||||||
@ -497,7 +366,10 @@ $(document).ready(function () {
|
|||||||
$('.slice').data('slice', slice);
|
$('.slice').data('slice', slice);
|
||||||
|
|
||||||
// call vis render method, which issues ajax
|
// call vis render method, which issues ajax
|
||||||
|
// calls render on the slice for the first time
|
||||||
query(false, false);
|
query(false, false);
|
||||||
|
|
||||||
slice.bindResizeToWindowResize();
|
slice.bindResizeToWindowResize();
|
||||||
|
|
||||||
|
initComponents();
|
||||||
});
|
});
|
||||||
|
@ -151,19 +151,13 @@ const px = function () {
|
|||||||
.tooltip('fixTitle');
|
.tooltip('fixTitle');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data !== undefined) {
|
if (data !== undefined) {
|
||||||
$('#query_container').html(data.query);
|
slice.viewSqlQuery = data.query;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#timer').removeClass('label-warning label-danger');
|
$('#timer').removeClass('label-warning label-danger');
|
||||||
$('#timer').addClass('label-success');
|
$('#timer').addClass('label-success');
|
||||||
$('span.view_query').removeClass('disabled');
|
|
||||||
$('#json').click(function () {
|
|
||||||
window.location = data.json_endpoint;
|
|
||||||
});
|
|
||||||
$('#csv').click(function () {
|
|
||||||
window.location = data.csv_endpoint;
|
|
||||||
});
|
|
||||||
$('.btn-group.results span,a').removeAttr('disabled');
|
|
||||||
$('.query-and-save button').removeAttr('disabled');
|
$('.query-and-save button').removeAttr('disabled');
|
||||||
always(data);
|
always(data);
|
||||||
},
|
},
|
||||||
@ -195,7 +189,6 @@ const px = function () {
|
|||||||
container.show();
|
container.show();
|
||||||
$('span.query').removeClass('disabled');
|
$('span.query').removeClass('disabled');
|
||||||
$('#timer').addClass('btn-danger');
|
$('#timer').addClass('btn-danger');
|
||||||
$('.btn-group.results span,a').removeAttr('disabled');
|
|
||||||
$('.query-and-save button').removeAttr('disabled');
|
$('.query-and-save button').removeAttr('disabled');
|
||||||
always(data);
|
always(data);
|
||||||
},
|
},
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'mocha';
|
||||||
|
|
||||||
|
import CopyToClipboard from '../../../javascripts/components/CopyToClipboard';
|
||||||
|
|
||||||
|
describe('CopyToClipboard', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
text: 'some text to copy',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(
|
||||||
|
React.isValidElement(<CopyToClipboard {...defaultProps} />)
|
||||||
|
).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'mocha';
|
||||||
|
|
||||||
|
import ModalTrigger from '../../../javascripts/components/ModalTrigger';
|
||||||
|
|
||||||
|
describe('ModalTrigger', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
triggerNode: <i className="fa fa-link" />,
|
||||||
|
modalTitle: 'My Modal Title',
|
||||||
|
modalBody: <div>Modal Body</div>,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(
|
||||||
|
React.isValidElement(<ModalTrigger {...defaultProps} />)
|
||||||
|
).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'mocha';
|
||||||
|
|
||||||
|
import DisplayQueryButton from '../../../../javascripts/explore/components/DisplayQueryButton';
|
||||||
|
|
||||||
|
describe('DisplayQueryButton', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
slice: {
|
||||||
|
viewSqlQuery: 'sql query string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(React.isValidElement(<DisplayQueryButton {...defaultProps} />)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'mocha';
|
||||||
|
import { shallow, mount } from 'enzyme';
|
||||||
|
import { OverlayTrigger } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import EmbedCodeButton from '../../../../javascripts/explore/components/EmbedCodeButton';
|
||||||
|
|
||||||
|
describe('EmbedCodeButton', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
slice: {
|
||||||
|
data: {
|
||||||
|
standalone_endpoint: 'endpoint_url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(React.isValidElement(<EmbedCodeButton {...defaultProps} />)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders overlay trigger', () => {
|
||||||
|
const wrapper = shallow(<EmbedCodeButton {...defaultProps} />);
|
||||||
|
expect(wrapper.find(OverlayTrigger)).to.have.length(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct embed code', () => {
|
||||||
|
const wrapper = mount(<EmbedCodeButton {...defaultProps} />);
|
||||||
|
wrapper.setState({
|
||||||
|
height: '1000',
|
||||||
|
width: '2000',
|
||||||
|
srcLink: 'http://localhost/endpoint_url',
|
||||||
|
});
|
||||||
|
const embedHTML = `<iframe src="http://localhost/endpoint_url" width="2000" height="1000" seamless frameBorder="0" scrolling="no"></iframe>`;
|
||||||
|
expect(wrapper.instance().generateEmbedHTML()).to.equal(embedHTML);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'mocha';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
import ExploreActionButtons from '../../../../javascripts/explore/components/ExploreActionButtons';
|
||||||
|
|
||||||
|
describe('ExploreActionButtons', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
canDownload: 'True',
|
||||||
|
slice: {
|
||||||
|
data: {
|
||||||
|
csv_endpoint: '',
|
||||||
|
json_endpoint: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(
|
||||||
|
React.isValidElement(<ExploreActionButtons {...defaultProps} />)
|
||||||
|
).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render 5 children/buttons', () => {
|
||||||
|
const wrapper = shallow(<ExploreActionButtons {...defaultProps} />);
|
||||||
|
expect(wrapper.children()).to.have.length(5);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { describe, it } from 'mocha';
|
||||||
|
|
||||||
|
import URLShortLinkButton from '../../../../javascripts/explore/components/URLShortLinkButton';
|
||||||
|
|
||||||
|
describe('URLShortLinkButton', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
slice: {
|
||||||
|
querystring: () => 'query string',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
expect(React.isValidElement(<URLShortLinkButton {...defaultProps} />)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
@ -205,29 +205,12 @@
|
|||||||
data-toggle="tooltip">
|
data-toggle="tooltip">
|
||||||
{{ _("0 sec") }}
|
{{ _("0 sec") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="btn-group results" role="group">
|
|
||||||
<a role="button" tabindex="0" class="btn btn-default btn-sm" id="shortner" data-toggle="popover" data-trigger="focus">
|
<span
|
||||||
<i class="fa fa-link" data-toggle="tooltip" title="{{ _("Short URL") }}"></i>
|
id="js-explore-actions"
|
||||||
</a>
|
data-can-download="{{can_download}}"
|
||||||
<span class="btn btn-default btn-sm" id="standalone" title="{{ _("Generate an embeddable iframe") }}" data-toggle="tooltip">
|
>
|
||||||
<i class="fa fa-code"></i>
|
</span>
|
||||||
</span>
|
|
||||||
<span class="btn btn-default btn-sm" id="json" title="{{ _("Export to .json")}}" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-file-code-o"></i>
|
|
||||||
.json
|
|
||||||
</span>
|
|
||||||
<span class="btn btn-default btn-sm {{ "disabled disabledButton" if not can_download }}" id="csv" title="{{ _("Export to .csv format") }}" data-toggle="tooltip">
|
|
||||||
<i class="fa fa-file-text-o"></i>.csv
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="btn btn-default btn-sm disabled view_query"
|
|
||||||
data-toggle="modal"
|
|
||||||
data-target="#query_modal">
|
|
||||||
<span data-toggle="tooltip" title="{{ _("View database query") }}">
|
|
||||||
{{ _("query") }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -245,24 +228,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade" id="query_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
<h4 class="modal-title">{{ _("Query") }}</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<pre id="query_container"></pre>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ _("Close") }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="sourceinfo_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
<div class="modal fade" id="sourceinfo_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
Loading…
Reference in New Issue
Block a user