diff --git a/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx b/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
index 025ea59d30..e9c35ea8c1 100644
--- a/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/QueryTable.jsx
@@ -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() {
diff --git a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
index 2b38d76577..15ca8bd1a6 100644
--- a/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
+++ b/caravel/assets/javascripts/SqlLab/components/TabbedSqlEditors.jsx
@@ -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);
diff --git a/caravel/assets/javascripts/components/CopyToClipboard.jsx b/caravel/assets/javascripts/components/CopyToClipboard.jsx
new file mode 100644
index 0000000000..c39d7ff950
--- /dev/null
+++ b/caravel/assets/javascripts/components/CopyToClipboard.jsx
@@ -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: Copy,
+ 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 = (
+
+ {this.tooltipText()}
+
+ );
+
+ return (
+
+ {this.props.shouldShowText &&
+ {this.props.text}
+ }
+
+
+
+
+
+ );
+ }
+}
+
+CopyToClipboard.propTypes = propTypes;
+CopyToClipboard.defaultProps = defaultProps;
diff --git a/caravel/assets/javascripts/components/ModalTrigger.jsx b/caravel/assets/javascripts/components/ModalTrigger.jsx
new file mode 100644
index 0000000000..b8705717e0
--- /dev/null
+++ b/caravel/assets/javascripts/components/ModalTrigger.jsx
@@ -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 (
+
+ {this.props.triggerNode}
+
+
+ {this.props.modalTitle}
+
+
+ {this.props.modalBody}
+
+
+
+ );
+ }
+}
+
+ModalTrigger.propTypes = propTypes;
+ModalTrigger.defaultProps = defaultProps;
diff --git a/caravel/assets/javascripts/explore/components/DisplayQueryButton.jsx b/caravel/assets/javascripts/explore/components/DisplayQueryButton.jsx
new file mode 100644
index 0000000000..6877fe7899
--- /dev/null
+++ b/caravel/assets/javascripts/explore/components/DisplayQueryButton.jsx
@@ -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 = ({this.state.viewSqlQuery}
);
+ return (
+ Query}
+ modalTitle="Query"
+ modalBody={modalBody}
+ beforeOpen={this.beforeOpen}
+ />
+ );
+ }
+}
+
+DisplayQueryButton.propTypes = propTypes;
diff --git a/caravel/assets/javascripts/explore/components/EmbedCodeButton.jsx b/caravel/assets/javascripts/explore/components/EmbedCodeButton.jsx
new file mode 100644
index 0000000000..cddd41e62a
--- /dev/null
+++ b/caravel/assets/javascripts/explore/components/EmbedCodeButton.jsx
@@ -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 =
+ ``;
+ return embedHTML;
+ }
+
+ renderPopover() {
+ const html = this.generateEmbedHTML();
+ return (
+
+
+
+ );
+ }
+ render() {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+EmbedCodeButton.propTypes = propTypes;
diff --git a/caravel/assets/javascripts/explore/components/ExploreActionButtons.jsx b/caravel/assets/javascripts/explore/components/ExploreActionButtons.jsx
new file mode 100644
index 0000000000..95399b0806
--- /dev/null
+++ b/caravel/assets/javascripts/explore/components/ExploreActionButtons.jsx
@@ -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 (
+
+ );
+}
+
+ExploreActionButtons.propTypes = propTypes;
diff --git a/caravel/assets/javascripts/explore/components/URLShortLinkButton.jsx b/caravel/assets/javascripts/explore/components/URLShortLinkButton.jsx
new file mode 100644
index 0000000000..847ff2ebcd
--- /dev/null
+++ b/caravel/assets/javascripts/explore/components/URLShortLinkButton.jsx
@@ -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 (
+
+ }
+ />
+
+ );
+ }
+
+ render() {
+ const shortUrl = this.state.shortUrl;
+ const isDisabled = shortUrl === '';
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+URLShortLinkButton.propTypes = propTypes;
diff --git a/caravel/assets/javascripts/explore/explore.jsx b/caravel/assets/javascripts/explore/explore.jsx
index 76c548aa75..79aff8fe3b 100644
--- a/caravel/assets/javascripts/explore/explore.jsx
+++ b/caravel/assets/javascripts/explore/explore.jsx
@@ -11,6 +11,7 @@ const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
import React from 'react';
import ReactDOM from 'react-dom';
import QueryAndSaveBtns from './components/QueryAndSaveBtns.jsx';
+import ExploreActionButtons from './components/ExploreActionButtons.jsx';
require('jquery-ui');
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
@@ -62,7 +63,6 @@ function query(forceUpdate, pushState) {
force = false;
}
$('.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
$('div.alert').remove();
}
@@ -160,148 +160,6 @@ function initExploreView() {
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 = (
- '' +
- '' +
- ''
- );
- const copy = (
- '' +
- '' +
- ''
- );
- 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:
' + error,
- });
- },
- });
- });
-
- $('#standalone').click(function () {
- const srcLink = window.location.origin + slice.data.standalone_endpoint;
- const close = (
- '' +
- '' +
- ''
- );
- const copy = (
- '' +
- '' +
- ''
- );
- const spaces = ' ';
- const widthInput = '';
- const heightInput = '';
- let popover = "";
- 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 = `';
- $standaloneText.val(dataToCopy);
- }
-
- $standaloneHeight.change(function () {
- generateEmbedHTML();
- });
- $standaloneWidth.change(function () {
- generateEmbedHTML();
- });
- generateEmbedHTML();
- });
-
$('#viz_type').change(function () {
$('#query').submit();
});
@@ -386,15 +244,6 @@ function initExploreView() {
addFilter(undefined, 'having');
});
- const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
- ReactDOM.render(
- query(true)}
- />,
- queryAndSaveBtnsEl
- );
-
function createChoices(term, data) {
const filtered = $(data).filter(function () {
return this.text.localeCompare(term) === 0;
@@ -487,6 +336,26 @@ function initExploreView() {
prepSaveDialog();
}
+function initComponents() {
+ const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
+ ReactDOM.render(
+ query(true)}
+ />,
+ queryAndSaveBtnsEl
+ );
+
+ const exploreActionsEl = document.getElementById('js-explore-actions');
+ ReactDOM.render(
+ ,
+ exploreActionsEl
+ );
+}
+
$(document).ready(function () {
const data = $('.slice').data('slice');
@@ -497,7 +366,10 @@ $(document).ready(function () {
$('.slice').data('slice', slice);
// call vis render method, which issues ajax
+ // calls render on the slice for the first time
query(false, false);
slice.bindResizeToWindowResize();
+
+ initComponents();
});
diff --git a/caravel/assets/javascripts/modules/caravel.js b/caravel/assets/javascripts/modules/caravel.js
index 2e673aef93..c39877e162 100644
--- a/caravel/assets/javascripts/modules/caravel.js
+++ b/caravel/assets/javascripts/modules/caravel.js
@@ -151,19 +151,13 @@ const px = function () {
.tooltip('fixTitle');
}
}
+
if (data !== undefined) {
- $('#query_container').html(data.query);
+ slice.viewSqlQuery = data.query;
}
+
$('#timer').removeClass('label-warning label-danger');
$('#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');
always(data);
},
@@ -195,7 +189,6 @@ const px = function () {
container.show();
$('span.query').removeClass('disabled');
$('#timer').addClass('btn-danger');
- $('.btn-group.results span,a').removeAttr('disabled');
$('.query-and-save button').removeAttr('disabled');
always(data);
},
diff --git a/caravel/assets/spec/javascripts/components/CopyToClipboard_spec.jsx b/caravel/assets/spec/javascripts/components/CopyToClipboard_spec.jsx
new file mode 100644
index 0000000000..a5f0ee3f7f
--- /dev/null
+++ b/caravel/assets/spec/javascripts/components/CopyToClipboard_spec.jsx
@@ -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()
+ ).to.equal(true);
+ });
+});
diff --git a/caravel/assets/spec/javascripts/components/ModalTrigger_spec.jsx b/caravel/assets/spec/javascripts/components/ModalTrigger_spec.jsx
new file mode 100644
index 0000000000..28a35146be
--- /dev/null
+++ b/caravel/assets/spec/javascripts/components/ModalTrigger_spec.jsx
@@ -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: ,
+ modalTitle: 'My Modal Title',
+ modalBody: Modal Body
,
+ };
+
+ it('renders', () => {
+ expect(
+ React.isValidElement()
+ ).to.equal(true);
+ });
+});
diff --git a/caravel/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx b/caravel/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx
new file mode 100644
index 0000000000..a74deb0e78
--- /dev/null
+++ b/caravel/assets/spec/javascripts/explore/components/DisplayQueryButton_spec.jsx
@@ -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()).to.equal(true);
+ });
+});
diff --git a/caravel/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx b/caravel/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx
new file mode 100644
index 0000000000..62f73c6d37
--- /dev/null
+++ b/caravel/assets/spec/javascripts/explore/components/EmbedCodeButton_spec.jsx
@@ -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()).to.equal(true);
+ });
+
+ it('renders overlay trigger', () => {
+ const wrapper = shallow();
+ expect(wrapper.find(OverlayTrigger)).to.have.length(1);
+ });
+
+ it('returns correct embed code', () => {
+ const wrapper = mount();
+ wrapper.setState({
+ height: '1000',
+ width: '2000',
+ srcLink: 'http://localhost/endpoint_url',
+ });
+ const embedHTML = ``;
+ expect(wrapper.instance().generateEmbedHTML()).to.equal(embedHTML);
+ });
+});
diff --git a/caravel/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx b/caravel/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx
new file mode 100644
index 0000000000..f00889a34a
--- /dev/null
+++ b/caravel/assets/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx
@@ -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()
+ ).to.equal(true);
+ });
+
+ it('should render 5 children/buttons', () => {
+ const wrapper = shallow();
+ expect(wrapper.children()).to.have.length(5);
+ });
+});
diff --git a/caravel/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx b/caravel/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx
new file mode 100644
index 0000000000..f2729db116
--- /dev/null
+++ b/caravel/assets/spec/javascripts/explore/components/URLShortLinkButton_spec.jsx
@@ -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()).to.equal(true);
+ });
+});
diff --git a/caravel/templates/caravel/explore.html b/caravel/templates/caravel/explore.html
index 93f2c194fb..1ea84c5de8 100644
--- a/caravel/templates/caravel/explore.html
+++ b/caravel/templates/caravel/explore.html
@@ -205,29 +205,12 @@
data-toggle="tooltip">
{{ _("0 sec") }}
-
-
-
-
-
-
-
-
-
- .json
-
-
- .csv
-
-
-
- {{ _("query") }}
-
-
-
+
+
+
@@ -245,24 +228,7 @@
-
+