mirror of
https://github.com/apache/superset.git
synced 2026-04-11 04:15:33 +00:00
* Edit Dashboard title and Slice title in place Add EditableTitle component into Dashboard and Explore view to support edit title inline.
361 lines
11 KiB
JavaScript
361 lines
11 KiB
JavaScript
import React from 'react';
|
|
import { render } from 'react-dom';
|
|
import d3 from 'd3';
|
|
import { Alert } from 'react-bootstrap';
|
|
import moment from 'moment';
|
|
|
|
import GridLayout from './components/GridLayout';
|
|
import Header from './components/Header';
|
|
import { appSetup } from '../common';
|
|
import AlertsWrapper from '../components/AlertsWrapper';
|
|
|
|
import '../../stylesheets/dashboard.css';
|
|
|
|
const px = require('../modules/superset');
|
|
const urlLib = require('url');
|
|
const utils = require('../modules/utils');
|
|
|
|
appSetup();
|
|
|
|
export function getInitialState(boostrapData) {
|
|
const dashboard = Object.assign({}, utils.controllerInterface, boostrapData.dashboard_data);
|
|
dashboard.firstLoad = true;
|
|
|
|
dashboard.posDict = {};
|
|
if (dashboard.position_json) {
|
|
dashboard.position_json.forEach((position) => {
|
|
dashboard.posDict[position.slice_id] = position;
|
|
});
|
|
}
|
|
dashboard.refreshTimer = null;
|
|
const state = Object.assign({}, boostrapData, { dashboard });
|
|
return state;
|
|
}
|
|
|
|
function unload() {
|
|
const message = 'You have unsaved changes.';
|
|
window.event.returnValue = message; // Gecko + IE
|
|
return message; // Gecko + Webkit, Safari, Chrome etc.
|
|
}
|
|
|
|
function onBeforeUnload(hasChanged) {
|
|
if (hasChanged) {
|
|
window.addEventListener('beforeunload', unload);
|
|
} else {
|
|
window.removeEventListener('beforeunload', unload);
|
|
}
|
|
}
|
|
|
|
function renderAlert() {
|
|
render(
|
|
<div className="container-fluid">
|
|
<Alert bsStyle="warning">
|
|
<strong>You have unsaved changes.</strong> Click the
|
|
<i className="fa fa-save" />
|
|
button on the top right to save your changes.
|
|
</Alert>
|
|
</div>,
|
|
document.getElementById('alert-container'),
|
|
);
|
|
}
|
|
|
|
function initDashboardView(dashboard) {
|
|
render(
|
|
<div>
|
|
<AlertsWrapper />
|
|
<Header dashboard={dashboard} />
|
|
</div>,
|
|
document.getElementById('dashboard-header'),
|
|
);
|
|
// eslint-disable-next-line no-param-reassign
|
|
dashboard.reactGridLayout = render(
|
|
<GridLayout dashboard={dashboard} />,
|
|
document.getElementById('grid-container'),
|
|
);
|
|
|
|
// 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');
|
|
|
|
$('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();
|
|
}
|
|
});
|
|
px.initFavStars();
|
|
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
|
|
}
|
|
|
|
export function dashboardContainer(dashboard, datasources, userid) {
|
|
return Object.assign({}, dashboard, {
|
|
type: 'dashboard',
|
|
filters: {},
|
|
curUserId: userid,
|
|
init() {
|
|
this.sliceObjects = [];
|
|
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, datasources[data.form_data.datasource], this);
|
|
$(`#slice_${data.slice_id}`).find('a.refresh').click(() => {
|
|
slice.render(true);
|
|
});
|
|
this.sliceObjects.push(slice);
|
|
}
|
|
});
|
|
this.loadPreSelectFilters();
|
|
this.startPeriodicRender(0);
|
|
this.bindResizeToWindowResize();
|
|
},
|
|
onChange() {
|
|
onBeforeUnload(true);
|
|
renderAlert();
|
|
},
|
|
onSave() {
|
|
onBeforeUnload(false);
|
|
$('#alert-container').html('');
|
|
},
|
|
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);
|
|
},
|
|
done(slice) {
|
|
const refresh = slice.getWidgetHeader().find('.refresh');
|
|
const data = slice.data;
|
|
const cachedWhen = moment.utc(data.cached_dttm).fromNow();
|
|
if (data !== undefined && data.is_cached) {
|
|
refresh
|
|
.addClass('danger')
|
|
.attr(
|
|
'title',
|
|
`Served from data cached ${cachedWhen}. ` +
|
|
'Click to force refresh')
|
|
.tooltip('fixTitle');
|
|
} else {
|
|
refresh
|
|
.removeClass('danger')
|
|
.attr('title', 'Click to force refresh')
|
|
.tooltip('fixTitle');
|
|
}
|
|
},
|
|
effectiveExtraFilters(sliceId) {
|
|
const f = [];
|
|
const immuneSlices = this.metadata.filter_immune_slices || [];
|
|
if (sliceId && immuneSlices.includes(sliceId)) {
|
|
// The slice is immune to dashboard fiterls
|
|
return f;
|
|
}
|
|
|
|
// Building a list of fields the slice is immune to filters on
|
|
let immuneToFields = [];
|
|
if (
|
|
sliceId &&
|
|
this.metadata.filter_immune_slice_fields &&
|
|
this.metadata.filter_immune_slice_fields[sliceId]) {
|
|
immuneToFields = this.metadata.filter_immune_slice_fields[sliceId];
|
|
}
|
|
for (const filteringSliceId in this.filters) {
|
|
for (const field in this.filters[filteringSliceId]) {
|
|
if (!immuneToFields.includes(field)) {
|
|
f.push({
|
|
col: field,
|
|
op: 'in',
|
|
val: this.filters[filteringSliceId][field],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return f;
|
|
},
|
|
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;
|
|
|
|
// d3.merge pass in array of arrays while some value form filter components
|
|
// from and to filter box require string to be process and return
|
|
} else if (this.filters[sliceId][col] instanceof Array) {
|
|
this.filters[sliceId][col] = d3.merge([this.filters[sliceId][col], vals]);
|
|
} else {
|
|
this.filters[sliceId][col] = d3.merge([[this.filters[sliceId][col]], vals])[0] || '';
|
|
}
|
|
if (refresh) {
|
|
this.refreshExcept(sliceId);
|
|
}
|
|
this.updateFilterParamsInUrl();
|
|
},
|
|
readFilters() {
|
|
// Returns a list of human readable active filters
|
|
return JSON.stringify(this.filters, null, ' ');
|
|
},
|
|
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.sliceObjects.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.max(interval * 0.2, 5000);
|
|
const refreshAll = () => {
|
|
dash.sliceObjects.forEach((slice) => {
|
|
const force = !dash.firstLoad;
|
|
setTimeout(() => {
|
|
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.sliceObjects.forEach((slice) => {
|
|
if (slice.data.slice_id !== sliceId && immune.indexOf(slice.data.slice_id) === -1) {
|
|
slice.render();
|
|
const sliceSeletor = $(`#${slice.data.slice_id}-cell`);
|
|
sliceSeletor.addClass('slice-cell-highlight');
|
|
setTimeout(function () {
|
|
sliceSeletor.removeClass('slice-cell-highlight');
|
|
}, 1200);
|
|
}
|
|
});
|
|
},
|
|
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.sliceObjects.length) {
|
|
// when the slice is found, assign to slice and break;
|
|
if (this.sliceObjects[i].data.slice_id === id) {
|
|
slice = this.sliceObjects[i];
|
|
break;
|
|
}
|
|
i++;
|
|
}
|
|
return slice;
|
|
},
|
|
getAjaxErrorMsg(error) {
|
|
const respJSON = error.responseJSON;
|
|
return (respJSON && respJSON.message) ? respJSON.message :
|
|
error.responseText;
|
|
},
|
|
addSlicesToDashboard(sliceIds) {
|
|
const getAjaxErrorMsg = this.getAjaxErrorMsg;
|
|
$.ajax({
|
|
type: 'POST',
|
|
url: `/superset/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);
|
|
utils.showModal({
|
|
title: 'Error',
|
|
body: 'Sorry, there was an error adding slices to this dashboard: ' + errorMsg,
|
|
});
|
|
},
|
|
});
|
|
},
|
|
updateDashboardTitle(title) {
|
|
this.dashboard_title = title;
|
|
this.onChange();
|
|
},
|
|
});
|
|
}
|
|
|
|
$(document).ready(() => {
|
|
// Getting bootstrapped data from the DOM
|
|
utils.initJQueryAjax();
|
|
const dashboardData = $('.dashboard').data('bootstrap');
|
|
|
|
const state = getInitialState(dashboardData);
|
|
const dashboard = dashboardContainer(state.dashboard, state.datasources, state.user_id);
|
|
initDashboardView(dashboard);
|
|
dashboard.init();
|
|
});
|