Fixing explore actions & slice controller interactions (#1292)

* Fixing explore actions & slice controller interactions

* Addressing a comment
This commit is contained in:
Maxime Beauchemin 2016-10-07 14:06:26 -07:00 committed by GitHub
parent 382b8e85da
commit 3384e7598e
9 changed files with 150 additions and 120 deletions

View File

@ -3,7 +3,7 @@ const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
const px = require('../modules/caravel.js'); const px = require('../modules/caravel.js');
const d3 = require('d3'); const d3 = require('d3');
const urlLib = require('url'); const urlLib = require('url');
const showModal = require('../modules/utils.js').showModal; const utils = require('../modules/utils.js');
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
@ -41,7 +41,8 @@ function injectCss(className, css) {
} }
function dashboardContainer(dashboardData) { function dashboardContainer(dashboardData) {
let dashboard = $.extend(dashboardData, { let dashboard = Object.assign({}, utils.controllerInterface, dashboardData, {
type: 'dashboard',
filters: {}, filters: {},
init() { init() {
this.initDashboardView(); this.initDashboardView();
@ -82,6 +83,23 @@ function dashboardContainer(dashboardData) {
setFilter(sliceId, col, vals, refresh) { setFilter(sliceId, col, vals, refresh) {
this.addFilter(sliceId, col, vals, false, refresh); this.addFilter(sliceId, col, vals, false, refresh);
}, },
done(slice) {
const refresh = slice.getWidgetHeader().find('.refresh');
const data = slice.data;
if (data !== undefined && data.is_cached) {
refresh
.addClass('danger')
.attr('title',
'Served from data cached at ' + data.cached_dttm +
'. Click to force refresh')
.tooltip('fixTitle');
} else {
refresh
.removeClass('danger')
.attr('title', 'Click to force refresh')
.tooltip('fixTitle');
}
},
effectiveExtraFilters(sliceId) { effectiveExtraFilters(sliceId) {
// Summarized filter, not defined by sliceId // Summarized filter, not defined by sliceId
// returns k=field, v=array of values // returns k=field, v=array of values
@ -250,7 +268,7 @@ function dashboardContainer(dashboardData) {
}, },
error(error) { error(error) {
const errorMsg = getAjaxErrorMsg(error); const errorMsg = getAjaxErrorMsg(error);
showModal({ utils.showModal({
title: 'Error', title: 'Error',
body: 'Sorry, there was an error adding slices to this dashboard: </ br>' + errorMsg, body: 'Sorry, there was an error adding slices to this dashboard: </ br>' + errorMsg,
}); });
@ -279,14 +297,14 @@ function dashboardContainer(dashboardData) {
data: JSON.stringify(data), data: JSON.stringify(data),
}, },
success() { success() {
showModal({ utils.showModal({
title: 'Success', title: 'Success',
body: 'This dashboard was saved successfully.', body: 'This dashboard was saved successfully.',
}); });
}, },
error(error) { error(error) {
const errorMsg = this.getAjaxErrorMsg(error); const errorMsg = this.getAjaxErrorMsg(error);
showModal({ utils.showModal({
title: 'Error', title: 'Error',
body: 'Sorry, there was an error saving this dashboard: </ br>' + errorMsg, body: 'Sorry, there was an error saving this dashboard: </ br>' + errorMsg,
}); });
@ -344,7 +362,7 @@ function dashboardContainer(dashboardData) {
injectCss('dashboard-template', css); injectCss('dashboard-template', css);
}); });
$('#filters').click(() => { $('#filters').click(() => {
showModal({ utils.showModal({
title: '<span class="fa fa-info-circle"></span> Current Global Filters', title: '<span class="fa fa-info-circle"></span> Current Global Filters',
body: 'The following global filters are currently applied:<br/>' + body: 'The following global filters are currently applied:<br/>' +
dashboard.readFilters(), dashboard.readFilters(),

View File

@ -12,7 +12,6 @@ export default class EmbedCodeButton extends React.Component {
this.state = { this.state = {
height: '400', height: '400',
width: '600', width: '600',
srcLink: window.location.origin + props.slice.data.standalone_endpoint,
}; };
this.handleInputChange = this.handleInputChange.bind(this); this.handleInputChange = this.handleInputChange.bind(this);
} }
@ -26,11 +25,15 @@ export default class EmbedCodeButton extends React.Component {
} }
generateEmbedHTML() { generateEmbedHTML() {
const { width, height, srcLink } = this.state; const srcLink = window.location.origin + this.props.slice.data.standalone_endpoint;
/* eslint max-len: 0 */ /* eslint max-len: 0 */
const embedHTML = return `
`<iframe src="${srcLink}" width="${width}" height="${height}" seamless frameBorder="0" scrolling="no"></iframe>`; <iframe
return embedHTML; src="${srcLink}"
width="${this.state.width}"
height="${this.state.height}"
seamless frameBorder="0" scrolling="no">
</iframe>`;
} }
renderPopover() { renderPopover() {

View File

@ -13,7 +13,6 @@ export default function ExploreActionButtons({ canDownload, slice }) {
const exportToCSVClasses = cx('btn btn-default btn-sm', { const exportToCSVClasses = cx('btn btn-default btn-sm', {
'disabled disabledButton': !canDownload, 'disabled disabledButton': !canDownload,
}); });
return ( return (
<div className="btn-group results" role="group"> <div className="btn-group results" role="group">
<URLShortLinkButton slice={slice} /> <URLShortLinkButton slice={slice} />

View File

@ -13,8 +13,6 @@ export default class URLShortLinkButton extends React.Component {
this.state = { this.state = {
shortUrl: '', shortUrl: '',
}; };
this.getShortUrl();
} }
getShortUrl() { getShortUrl() {
@ -22,7 +20,7 @@ export default class URLShortLinkButton extends React.Component {
type: 'POST', type: 'POST',
url: '/r/shortner/', url: '/r/shortner/',
data: { data: {
data: '/' + window.location.pathname + this.props.slice.querystring(), data: '/' + window.location.pathname + window.location.search,
}, },
success: (data) => { success: (data) => {
this.setState({ this.setState({
@ -51,16 +49,15 @@ export default class URLShortLinkButton extends React.Component {
} }
render() { render() {
const shortUrl = this.state.shortUrl;
const isDisabled = shortUrl === '';
return ( return (
<OverlayTrigger <OverlayTrigger
trigger="click" trigger="click"
rootClose rootClose
placement="left" placement="left"
onEnter={this.getShortUrl.bind(this)}
overlay={this.renderPopover()} overlay={this.renderPopover()}
> >
<span className="btn btn-default btn-sm" disabled={isDisabled}> <span className="btn btn-default btn-sm">
<i className="fa fa-link"></i>&nbsp; <i className="fa fa-link"></i>&nbsp;
</span> </span>
</OverlayTrigger> </OverlayTrigger>

View File

@ -5,7 +5,7 @@
// js // js
const $ = window.$ = require('jquery'); const $ = window.$ = require('jquery');
const px = require('./../modules/caravel.js'); const px = require('./../modules/caravel.js');
const showModal = require('./../modules/utils.js').showModal; const utils = require('./../modules/utils.js');
const jQuery = window.jQuery = require('jquery'); // eslint-disable-line const jQuery = window.jQuery = require('jquery'); // eslint-disable-line
import React from 'react'; import React from 'react';
@ -81,7 +81,7 @@ function saveSlice() {
if (action === 'saveas') { if (action === 'saveas') {
const sliceName = $('input[name=new_slice_name]').val(); const sliceName = $('input[name=new_slice_name]').val();
if (sliceName === '') { if (sliceName === '') {
showModal({ utils.showModal({
title: 'Error', title: 'Error',
body: 'You must pick a name for the new slice', body: 'You must pick a name for the new slice',
}); });
@ -91,13 +91,13 @@ function saveSlice() {
} }
const addToDash = $('input[name=addToDash]:checked').val(); const addToDash = $('input[name=addToDash]:checked').val();
if (addToDash === 'existing' && $('#save_to_dashboard_id').val() === '') { if (addToDash === 'existing' && $('#save_to_dashboard_id').val() === '') {
showModal({ utils.showModal({
title: 'Error', title: 'Error',
body: 'You must pick an existing dashboard', body: 'You must pick an existing dashboard',
}); });
return; return;
} else if (addToDash === 'new' && $('input[name=new_dashboard_name]').val() === '') { } else if (addToDash === 'new' && $('input[name=new_dashboard_name]').val() === '') {
showModal({ utils.showModal({
title: 'Error', title: 'Error',
body: 'Please enter a name for the new dashboard', body: 'Please enter a name for the new dashboard',
}); });
@ -336,16 +336,7 @@ function initExploreView() {
prepSaveDialog(); prepSaveDialog();
} }
function initComponents() { function renderExploreActions() {
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'); const exploreActionsEl = document.getElementById('js-explore-actions');
ReactDOM.render( ReactDOM.render(
<ExploreActionButtons <ExploreActionButtons
@ -356,14 +347,49 @@ function initComponents() {
); );
} }
function initComponents() {
const queryAndSaveBtnsEl = document.getElementById('js-query-and-save-btns');
ReactDOM.render(
<QueryAndSaveBtns
canAdd={queryAndSaveBtnsEl.getAttribute('data-can-add')}
onQuery={() => query(true)}
/>,
queryAndSaveBtnsEl
);
renderExploreActions();
}
let exploreController = {
type: 'slice',
done: (sliceObj) => {
slice = sliceObj;
renderExploreActions();
const cachedSelector = $('#is_cached');
if (slice.data !== undefined && slice.data.is_cached) {
cachedSelector
.attr(
'title',
`Served from data cached at ${slice.data.cached_dttm}. Click [Query] to force refresh`)
.show()
.tooltip('fixTitle');
} else {
cachedSelector.hide();
}
},
error: (sliceObj) => {
slice = sliceObj;
renderExploreActions();
},
};
exploreController = Object.assign({}, utils.controllerInterface, exploreController);
$(document).ready(function () { $(document).ready(function () {
const data = $('.slice').data('slice'); const data = $('.slice').data('slice');
initExploreView(); initExploreView();
slice = px.Slice(data); slice = px.Slice(data, exploreController);
$('.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 // calls render on the slice for the first time

View File

@ -25,40 +25,41 @@ const px = function () {
} }
} }
$('.favstar') $('.favstar')
.attr('title', 'Click to favorite/unfavorite') .attr('title', 'Click to favorite/unfavorite')
.css('cursor', 'pointer') .css('cursor', 'pointer')
.each(show) .each(show)
.each(function () { .each(function () {
let url = baseUrl + $(this).attr('class_name'); let url = baseUrl + $(this).attr('class_name');
const star = this; const star = this;
url += '/' + $(this).attr('obj_id') + '/'; url += '/' + $(this).attr('obj_id') + '/';
$.getJSON(url + 'count/', function (data) { $.getJSON(url + 'count/', function (data) {
if (data.count > 0) { if (data.count > 0) {
$(star).addClass('selected').each(show); $(star).addClass('selected').each(show);
}
});
})
.click(function () {
$(this).toggleClass('selected');
let url = baseUrl + $(this).attr('class_name');
url += '/' + $(this).attr('obj_id') + '/';
if ($(this).hasClass('selected')) {
url += 'select/';
} else {
url += 'unselect/';
} }
$.get(url); });
$(this).each(show); })
}) .click(function () {
$(this).toggleClass('selected');
let url = baseUrl + $(this).attr('class_name');
url += '/' + $(this).attr('obj_id') + '/';
if ($(this).hasClass('selected')) {
url += 'select/';
} else {
url += 'unselect/';
}
$.get(url);
$(this).each(show);
})
.tooltip(); .tooltip();
} }
const Slice = function (data, dashboard) { const Slice = function (data, controller) {
let timer; let timer;
const token = $('#' + data.token); const token = $('#' + data.token);
const containerId = data.token + '_con'; const containerId = data.token + '_con';
const selector = '#' + containerId; const selector = '#' + containerId;
const container = $(selector); const container = $(selector);
const sliceId = data.slice_id; const sliceId = data.slice_id;
const origJsonEndpoint = data.json_endpoint;
let dttm = 0; let dttm = 0;
const stopwatch = function () { const stopwatch = function () {
dttm += 10; dttm += 10;
@ -76,16 +77,13 @@ const px = function () {
container, container,
containerId, containerId,
selector, selector,
querystring(params) { querystring() {
const newParams = params || {};
const parser = document.createElement('a'); const parser = document.createElement('a');
parser.href = data.json_endpoint; parser.href = data.json_endpoint;
if (dashboard !== undefined) { if (controller.type === 'dashboard') {
let flts = ''; parser.href = origJsonEndpoint;
if (newParams.extraFilters !== false) { let flts = controller.effectiveExtraFilters(sliceId);
flts = dashboard.effectiveExtraFilters(sliceId); flts = encodeURIComponent(JSON.stringify(flts));
flts = encodeURIComponent(JSON.stringify(flts));
}
qrystr = parser.search + '&extra_filters=' + flts; qrystr = parser.search + '&extra_filters=' + flts;
} else if ($('#query').length === 0) { } else if ($('#query').length === 0) {
qrystr = parser.search; qrystr = parser.search;
@ -104,11 +102,10 @@ const px = function () {
}; };
return Mustache.render(s, context); return Mustache.render(s, context);
}, },
jsonEndpoint(params) { jsonEndpoint() {
const newParams = params || {};
const parser = document.createElement('a'); const parser = document.createElement('a');
parser.href = data.json_endpoint; parser.href = data.json_endpoint;
let endpoint = parser.pathname + this.querystring({ extraFilters: newParams.extraFilters }); let endpoint = parser.pathname + this.querystring();
endpoint += '&json=true'; endpoint += '&json=true';
endpoint += '&force=' + this.force; endpoint += '&force=' + this.force;
return endpoint; return endpoint;
@ -116,43 +113,16 @@ const px = function () {
d3format(col, number) { d3format(col, number) {
// uses the utils memoized d3format function and formats based on // uses the utils memoized d3format function and formats based on
// column level defined preferences // column level defined preferences
const format = this.data.column_formats[col]; const format = data.column_formats[col];
return utils.d3format(format, number); return utils.d3format(format, number);
}, },
/* eslint no-shadow: 0 */ /* eslint no-shadow: 0 */
done(data) { done(payload) {
Object.assign(data, payload);
clearInterval(timer); clearInterval(timer);
token.find('img.loading').hide(); token.find('img.loading').hide();
container.show(); container.show();
let cachedSelector = null;
if (dashboard === undefined) {
cachedSelector = $('#is_cached');
if (data !== undefined && data.is_cached) {
cachedSelector
.attr(
'title',
`Served from data cached at ${data.cached_dttm}. Click [Query] to force-refresh`)
.show()
.tooltip('fixTitle');
} else {
cachedSelector.hide();
}
} else {
const refresh = this.getWidgetHeader().find('.refresh');
if (data !== undefined && data.is_cached) {
refresh
.addClass('danger')
.attr('title',
'Served from data cached at ' + data.cached_dttm +
'. Click to force-refresh')
.tooltip('fixTitle');
} else {
refresh
.removeClass('danger')
.attr('title', 'Click to force-refresh')
.tooltip('fixTitle');
}
}
if (data !== undefined) { if (data !== undefined) {
slice.viewSqlQuery = data.query; slice.viewSqlQuery = data.query;
@ -162,6 +132,7 @@ const px = function () {
$('#timer').addClass('label-success'); $('#timer').addClass('label-success');
$('.query-and-save button').removeAttr('disabled'); $('.query-and-save button').removeAttr('disabled');
always(data); always(data);
controller.done(this);
}, },
getErrorMsg(xhr) { getErrorMsg(xhr) {
if (xhr.statusText === 'timeout') { if (xhr.statusText === 'timeout') {
@ -193,6 +164,7 @@ const px = function () {
$('#timer').addClass('btn-danger'); $('#timer').addClass('btn-danger');
$('.query-and-save button').removeAttr('disabled'); $('.query-and-save button').removeAttr('disabled');
always(data); always(data);
controller.error(this);
}, },
width() { width() {
return token.width(); return token.width();
@ -238,30 +210,19 @@ const px = function () {
this.viz.resize(); this.viz.resize();
}, },
addFilter(col, vals) { addFilter(col, vals) {
if (dashboard !== undefined) { controller.addFilter(sliceId, col, vals);
dashboard.addFilter(sliceId, col, vals);
}
}, },
setFilter(col, vals) { setFilter(col, vals) {
if (dashboard !== undefined) { controller.setFilter(sliceId, col, vals);
dashboard.setFilter(sliceId, col, vals);
}
}, },
getFilters() { getFilters() {
if (dashboard !== undefined) { return controller.filters[sliceId];
return dashboard.filters[sliceId];
}
return false;
}, },
clearFilter() { clearFilter() {
if (dashboard !== undefined) { controller.clearFilter(sliceId);
dashboard.clearFilter(sliceId);
}
}, },
removeFilter(col, vals) { removeFilter(col, vals) {
if (dashboard !== undefined) { controller.removeFilter(sliceId, col, vals);
dashboard.removeFilter(sliceId, col, vals);
}
}, },
}; };
slice.viz = vizMap[data.form_data.viz_type](slice); slice.viz = vizMap[data.form_data.viz_type](slice);

View File

@ -103,7 +103,23 @@ function d3format(format, number) {
} }
return formatters[format](number); return formatters[format](number);
} }
// Slice objects interact with their context through objects that implement
// this controllerInterface (dashboard, explore, standalone)
const controllerInterface = {
type: null,
done: () => {},
error: () => {},
always: () => {},
addFiler: () => {},
setFilter: () => {},
getFilters: () => false,
clearFilter: () => {},
removeFilter: () => {},
};
module.exports = { module.exports = {
controllerInterface,
d3format, d3format,
fixDataTableBodyHeight, fixDataTableBodyHeight,
showModal, showModal,

View File

@ -2,12 +2,16 @@ const $ = window.$ = require('jquery');
/* eslint no-unused-vars: 0 */ /* eslint no-unused-vars: 0 */
const jQuery = window.jQuery = $; const jQuery = window.jQuery = $;
const px = require('./modules/caravel.js'); const px = require('./modules/caravel.js');
const utils = require('./modules/utils.js');
require('bootstrap'); require('bootstrap');
const standaloneController = Object.assign(
{}, utils.controllerInterface, { type: 'standalone' });
$(document).ready(function () { $(document).ready(function () {
const data = $('.slice').data('slice'); const data = $('.slice').data('slice');
const slice = px.Slice(data); const slice = px.Slice(data, standaloneController);
slice.render(); slice.render();
slice.bindResizeToWindowResize(); slice.bindResizeToWindowResize();
}); });

View File

@ -31,7 +31,13 @@ describe('EmbedCodeButton', () => {
width: '2000', width: '2000',
srcLink: 'http://localhost/endpoint_url', srcLink: 'http://localhost/endpoint_url',
}); });
const embedHTML = `<iframe src="http://localhost/endpoint_url" width="2000" height="1000" seamless frameBorder="0" scrolling="no"></iframe>`; const embedHTML = `
<iframe
src="nullendpoint_url"
width="2000"
height="1000"
seamless frameBorder="0" scrolling="no">
</iframe>`;
expect(wrapper.instance().generateEmbedHTML()).to.equal(embedHTML); expect(wrapper.instance().generateEmbedHTML()).to.equal(embedHTML);
}); });
}); });