client/app/services/dashboard.js (200 lines of code) (raw):

import _ from "lodash"; import { axios } from "@/services/axios"; import dashboardGridOptions from "@/config/dashboard-grid-options"; import Widget from "./widget"; import { currentUser } from "@/services/auth"; import location from "@/services/location"; import { cloneParameter } from "@/services/parameters"; export function collectDashboardFilters(dashboard, queryResults, urlParams) { const filters = {}; _.each(queryResults, queryResult => { const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : []; _.each(queryFilters, queryFilter => { const hasQueryStringValue = _.has(urlParams, queryFilter.name); if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) { // If dashboard filters not enabled, or no query string value given, // skip filters linking. return; } if (hasQueryStringValue) { queryFilter.current = urlParams[queryFilter.name]; } const filter = { ...queryFilter }; if (!_.has(filters, queryFilter.name)) { filters[filter.name] = filter; } else { filters[filter.name].values = _.union(filters[filter.name].values, filter.values); } }); }); return _.values(filters); } function prepareWidgetsForDashboard(widgets) { // Default height for auto-height widgets. // Compute biggest widget size and choose between it and some magic number. // This value should be big enough so auto-height widgets will not overlap other ones. const defaultWidgetSizeY = Math.max( _.chain(widgets) .map(w => w.options.position.sizeY) .max() .value(), 20 ) + 5; // Fix layout: // 1. sort and group widgets by row // 2. update position of widgets in each row - place it right below // biggest widget from previous row _.chain(widgets) .sortBy(widget => widget.options.position.row) .groupBy(widget => widget.options.position.row) .reduce((row, widgetsAtRow) => { let height = 1; _.each(widgetsAtRow, widget => { height = Math.max( height, widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY ); widget.options.position.row = row; if (widget.options.position.sizeY < 1) { widget.options.position.sizeY = defaultWidgetSizeY; } }); return row + height; }, 0) .value(); // Sort widgets by updated column and row value widgets = _.sortBy(widgets, widget => widget.options.position.col); widgets = _.sortBy(widgets, widget => widget.options.position.row); return widgets; } function calculateNewWidgetPosition(existingWidgets, newWidget) { const width = _.extend({ sizeX: dashboardGridOptions.defaultSizeX }, _.extend({}, newWidget.options).position).sizeX; // Find first free row for each column const bottomLine = _.chain(existingWidgets) .map(w => { const options = _.extend({}, w.options); const position = _.extend({ row: 0, sizeY: 0 }, options.position); return { left: position.col, top: position.row, right: position.col + position.sizeX, bottom: position.row + position.sizeY, width: position.sizeX, height: position.sizeY, }; }) .reduce((result, item) => { const from = Math.max(item.left, 0); const to = Math.min(item.right, result.length + 1); for (let i = from; i < to; i += 1) { result[i] = Math.max(result[i], item.bottom); } return result; }, _.map(new Array(dashboardGridOptions.columns), _.constant(0))) .value(); // Go through columns, pick them by count necessary to hold new block, // and calculate bottom-most free row per group. // Choose group with the top-most free row (comparing to other groups) return _.chain(_.range(0, dashboardGridOptions.columns - width + 1)) .map(col => ({ col, row: _.chain(bottomLine) .slice(col, col + width) .max() .value(), })) .sortBy("row") .first() .value(); } export function Dashboard(dashboard) { _.extend(this, dashboard); } function prepareDashboardWidgets(widgets) { return prepareWidgetsForDashboard(_.map(widgets, widget => new Widget(widget))); } function transformSingle(dashboard) { dashboard = new Dashboard(dashboard); if (dashboard.widgets) { dashboard.widgets = prepareDashboardWidgets(dashboard.widgets); } dashboard.publicAccessEnabled = dashboard.public_url !== undefined; return dashboard; } function transformResponse(data) { if (data.results) { data = { ...data, results: _.map(data.results, transformSingle) }; } else { data = transformSingle(data); } return data; } const saveOrCreateUrl = data => (data.slug ? `api/dashboards/${data.slug}` : "api/dashboards"); const DashboardService = { get: ({ slug }) => axios.get(`api/dashboards/${slug}`).then(transformResponse), getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse), save: data => axios.post(saveOrCreateUrl(data), data).then(transformResponse), delete: ({ slug }) => axios.delete(`api/dashboards/${slug}`).then(transformResponse), query: params => axios.get("api/dashboards", { params }).then(transformResponse), recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse), favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse), favorite: ({ slug }) => axios.post(`api/dashboards/${slug}/favorite`), unfavorite: ({ slug }) => axios.delete(`api/dashboards/${slug}/favorite`), }; _.extend(Dashboard, DashboardService); Dashboard.prepareDashboardWidgets = prepareDashboardWidgets; Dashboard.prepareWidgetsForDashboard = prepareWidgetsForDashboard; Dashboard.prototype.canEdit = function canEdit() { return currentUser.canEdit(this) || this.can_edit; }; Dashboard.prototype.getParametersDefs = function getParametersDefs() { const globalParams = {}; const queryParams = location.search; _.each(this.widgets, widget => { if (widget.getQuery()) { const mappings = widget.getParameterMappings(); widget .getQuery() .getParametersDefs(false) .forEach(param => { const mapping = mappings[param.name]; if (mapping.type === Widget.MappingType.DashboardLevel) { // create global param if (!globalParams[mapping.mapTo]) { globalParams[mapping.mapTo] = cloneParameter(param); globalParams[mapping.mapTo].name = mapping.mapTo; globalParams[mapping.mapTo].title = mapping.title || param.title; globalParams[mapping.mapTo].locals = []; } // add to locals list globalParams[mapping.mapTo].locals.push(param); } }); } }); return _.values( _.each(globalParams, param => { param.setValue(param.value); // apply global param value to all locals param.fromUrlParams(queryParams); // try to initialize from url (may do nothing) }) ); }; Dashboard.prototype.addWidget = function addWidget(textOrVisualization, options = {}) { const props = { dashboard_id: this.id, options: { ...options, isHidden: false, position: {}, }, text: "", visualization_id: null, visualization: null, }; if (_.isString(textOrVisualization)) { props.text = textOrVisualization; } else if (_.isObject(textOrVisualization)) { props.visualization_id = textOrVisualization.id; props.visualization = textOrVisualization; } else { // TODO: Throw an error? } const widget = new Widget(props); const position = calculateNewWidgetPosition(this.widgets, widget); widget.options.position.col = position.col; widget.options.position.row = position.row; return widget.save().then(() => { this.widgets = [...this.widgets, widget]; return widget; }); }; Dashboard.prototype.favorite = function favorite() { return Dashboard.favorite(this); }; Dashboard.prototype.unfavorite = function unfavorite() { return Dashboard.unfavorite(this); };