superset/caravel/assets/javascripts/dashboard/Dashboard.jsx

367 lines
11 KiB
JavaScript

const $ = window.$ = require('jquery');
const jQuery = window.jQuery = $;
const px = require('../modules/caravel.js');
const d3 = require('d3');
const urlLib = require('url');
const showModal = require('../modules/utils.js').showModal;
import React from 'react';
import { render } from 'react-dom';
import SliceAdder from './components/SliceAdder.jsx';
import GridLayout from './components/GridLayout.jsx';
const ace = require('brace');
require('bootstrap');
require('brace/mode/css');
require('brace/theme/crimson_editor');
require('./main.css');
require('../caravel-select2.js');
// Injects the passed css string into a style sheet with the specified className
// If a stylesheet doesn't exist with the passed className, one will be injected into <head>
function injectCss(className, css) {
const head = document.head || document.getElementsByTagName('head')[0];
let style = document.querySelector('.' + className);
if (!style) {
if (className.split(' ').length > 1) {
throw new Error('This method only supports selections with a single class name.');
}
style = document.createElement('style');
style.className = className;
style.type = 'text/css';
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.innerHTML = css;
}
}
function dashboardContainer(dashboardData) {
let dashboard = $.extend(dashboardData, {
filters: {},
init() {
this.initDashboardView();
this.firstLoad = true;
px.initFavStars();
const sliceObjects = [];
const dash = this;
dashboard.slices.forEach((data) => {
if (data.error) {
const html = '<div class="alert alert-danger">' + data.error + '</div>';
$('#slice_' + data.slice_id).find('.token').html(html);
} else {
const slice = px.Slice(data, dash);
$('#slice_' + data.slice_id).find('a.refresh').click(() => {
slice.render(true);
});
sliceObjects.push(slice);
}
});
this.slices = sliceObjects;
this.refreshTimer = null;
this.loadPreSelectFilters();
this.startPeriodicRender(0);
this.bindResizeToWindowResize();
},
loadPreSelectFilters() {
try {
const filters = JSON.parse(px.getParam('preselect_filters') || '{}');
for (const sliceId in filters) {
for (const col in filters[sliceId]) {
this.setFilter(sliceId, col, filters[sliceId][col], false, false);
}
}
} catch (e) {
// console.error(e);
}
},
setFilter(sliceId, col, vals, refresh) {
this.addFilter(sliceId, col, vals, false, refresh);
},
addFilter(sliceId, col, vals, merge = true, refresh = true) {
if (!(sliceId in this.filters)) {
this.filters[sliceId] = {};
}
if (!(col in this.filters[sliceId]) || !merge) {
this.filters[sliceId][col] = vals;
} else {
this.filters[sliceId][col] = d3.merge([this.filters[sliceId][col], vals]);
}
if (refresh) {
this.refreshExcept(sliceId);
}
this.updateFilterParamsInUrl();
},
readFilters() {
// Returns a list of human readable active filters
return JSON.stringify(this.filters, null, 4);
},
updateFilterParamsInUrl() {
const urlObj = urlLib.parse(location.href, true);
urlObj.query = urlObj.query || {};
urlObj.query.preselect_filters = this.readFilters();
urlObj.search = null;
history.pushState(urlObj.query, window.title, urlLib.format(urlObj));
},
bindResizeToWindowResize() {
let resizeTimer;
const dash = this;
$(window).on('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
dash.slices.forEach((slice) => {
slice.resize();
});
}, 500);
});
},
stopPeriodicRender() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
},
startPeriodicRender(interval) {
this.stopPeriodicRender();
const dash = this;
const maxRandomDelay = Math.min(interval * 0.2, 5000);
const refreshAll = function () {
dash.slices.forEach(function (slice) {
const force = !dash.firstLoad;
setTimeout(function () {
slice.render(force);
},
// Randomize to prevent all widgets refreshing at the same time
maxRandomDelay * Math.random());
});
dash.firstLoad = false;
};
const fetchAndRender = function () {
refreshAll();
if (interval > 0) {
dash.refreshTimer = setTimeout(function () {
fetchAndRender();
}, interval);
}
};
fetchAndRender();
},
refreshExcept(sliceId) {
const immune = this.metadata.filter_immune_slices || [];
this.slices.forEach(function (slice) {
if (slice.data.sliceId !== sliceId && immune.indexOf(slice.data.sliceId) === -1) {
slice.render();
}
});
},
clearFilters(sliceId) {
delete this.filters[sliceId];
this.refreshExcept(sliceId);
this.updateFilterParamsInUrl();
},
removeFilter(sliceId, col, vals) {
if (sliceId in this.filters) {
if (col in this.filters[sliceId]) {
const a = [];
this.filters[sliceId][col].forEach(function (v) {
if (vals.indexOf(v) < 0) {
a.push(v);
}
});
this.filters[sliceId][col] = a;
}
}
this.refreshExcept(sliceId);
this.updateFilterParamsInUrl();
},
getSlice(sliceId) {
const id = parseInt(sliceId, 10);
let i = 0;
let slice = null;
while (i < this.slices.length) {
// when the slice is found, assign to slice and break;
if (this.slices[i].data.slice_id === id) {
slice = this.slices[i];
break;
}
i++;
}
return slice;
},
showAddSlice() {
const slicesOnDashMap = {};
const layoutPositions = this.reactGridLayout.serialize();
layoutPositions.forEach((position) => {
slicesOnDashMap[position.slice_id] = true;
});
render(
<SliceAdder dashboard={dashboard} slicesOnDashMap={slicesOnDashMap} caravel={px} />,
document.getElementById('add-slice-container')
);
},
getAjaxErrorMsg(error) {
const respJSON = error.responseJSON;
return (respJSON && respJSON.message) ? respJSON.message :
error.responseText;
},
addSlicesToDashboard(sliceIds) {
const getAjaxErrorMsg = this.getAjaxErrorMsg;
$.ajax({
type: 'POST',
url: '/caravel/add_slices/' + dashboard.id + '/',
data: {
data: JSON.stringify({ slice_ids: sliceIds }),
},
success() {
// Refresh page to allow for slices to re-render
window.location.reload();
},
error(error) {
const errorMsg = getAjaxErrorMsg(error);
showModal({
title: 'Error',
body: 'Sorry, there was an error adding slices to this dashboard: </ br>' + errorMsg,
});
},
});
},
saveDashboard() {
const expandedSlices = {};
$.each($('.slice_info'), function () {
const widget = $(this).parents('.widget');
const sliceDescription = widget.find('.slice_description');
if (sliceDescription.is(':visible')) {
expandedSlices[$(widget).attr('data-slice-id')] = true;
}
});
const positions = this.reactGridLayout.serialize();
const data = {
positions,
css: this.editor.getValue(),
expanded_slices: expandedSlices,
};
$.ajax({
type: 'POST',
url: '/caravel/save_dash/' + dashboard.id + '/',
data: {
data: JSON.stringify(data),
},
success() {
showModal({
title: 'Success',
body: 'This dashboard was saved successfully.',
});
},
error(error) {
const errorMsg = this.getAjaxErrorMsg(error);
showModal({
title: 'Error',
body: 'Sorry, there was an error saving this dashboard: </ br>' + errorMsg,
});
},
});
},
initDashboardView() {
this.posDict = {};
this.position_json.forEach(function (position) {
this.posDict[position.slice_id] = position;
}, this);
this.reactGridLayout = render(
<GridLayout slices={this.slices} posDict={this.posDict} dashboard={dashboard} />,
document.getElementById('grid-container')
);
this.curUserId = $('.dashboard').data('user');
dashboard = this;
// Displaying widget controls on hover
$('.react-grid-item').hover(
function () {
$(this).find('.chart-controls').fadeIn(300);
},
function () {
$(this).find('.chart-controls').fadeOut(300);
}
);
$('div.grid-container').css('visibility', 'visible');
$('#savedash').click(this.saveDashboard.bind(this));
$('#add-slice').click(this.showAddSlice.bind(this));
const editor = ace.edit('dash_css');
this.editor = editor;
editor.$blockScrolling = Infinity;
editor.setTheme('ace/theme/crimson_editor');
editor.setOptions({
minLines: 16,
maxLines: Infinity,
useWorker: false,
});
editor.getSession().setMode('ace/mode/css');
$('.select2').select2({
dropdownAutoWidth: true,
});
$('#css_template').on('change', function () {
const css = $(this).find('option:selected').data('css');
editor.setValue(css);
$('#dash_css').val(css);
injectCss('dashboard-template', css);
});
$('#filters').click(() => {
showModal({
title: '<span class="fa fa-info-circle"></span> Current Global Filters',
body: 'The following global filters are currently applied:<br/>' +
dashboard.readFilters(),
});
});
$('#refresh_dash_interval').on('change', function () {
const interval = $(this).find('option:selected').val() * 1000;
dashboard.startPeriodicRender(interval);
});
$('#refresh_dash').click(() => {
dashboard.slices.forEach((slice) => {
slice.render(true);
});
});
$('div.widget').click(function (e) {
const $this = $(this);
const $target = $(e.target);
if ($target.hasClass('slice_info')) {
$this.find('.slice_description').slideToggle(0, function () {
$this.find('.refresh').click();
});
} else if ($target.hasClass('controls-toggle')) {
$this.find('.chart-controls').toggle();
}
});
editor.on('change', function () {
const css = editor.getValue();
$('#dash_css').val(css);
injectCss('dashboard-template', css);
});
const css = $('.dashboard').data('css');
injectCss('dashboard-template', css);
},
});
dashboard.init();
return dashboard;
}
$(document).ready(() => {
dashboardContainer($('.dashboard').data('dashboard'));
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
});