mirror of
https://github.com/getredash/redash.git
synced 2026-05-11 00:00:57 -04:00
* Dashboard grid React migration * Updated tests * Fixes comments * One col layout * Tests unskipped * Test fixes * Test fix * AutoHeight feature * Kebab-cased * Get rid of lazyInjector * Replace react-grid-layout with patched fork to fix performance issues * Fix issue with initial layout when page has a scrollbar * Decrease polling interval (500ms is too slow) * Rename file to match it's contents * Added some notes and very minor fixes * Fix Remove widget button (should be visible only in editing mode); fix widget actions menu * Fixed missing grid markings * Enhanced resize handle * Updated placeholder color * Render DashboardGrid only when dashboard is loaded
256 lines
7.7 KiB
JavaScript
256 lines
7.7 KiB
JavaScript
import moment from 'moment';
|
|
import { each, pick, extend, isObject, truncate, keys, difference, filter, map, merge } from 'lodash';
|
|
import dashboardGridOptions from '@/config/dashboard-grid-options';
|
|
import { registeredVisualizations } from '@/visualizations';
|
|
|
|
export let Widget = null; // eslint-disable-line import/no-mutable-exports
|
|
|
|
function calculatePositionOptions(widget) {
|
|
widget.width = 1; // Backward compatibility, user on back-end
|
|
|
|
const visualizationOptions = {
|
|
autoHeight: false,
|
|
sizeX: Math.round(dashboardGridOptions.columns / 2),
|
|
sizeY: dashboardGridOptions.defaultSizeY,
|
|
minSizeX: dashboardGridOptions.minSizeX,
|
|
maxSizeX: dashboardGridOptions.maxSizeX,
|
|
minSizeY: dashboardGridOptions.minSizeY,
|
|
maxSizeY: dashboardGridOptions.maxSizeY,
|
|
};
|
|
|
|
const config = widget.visualization ? registeredVisualizations[widget.visualization.type] : null;
|
|
if (isObject(config)) {
|
|
if (Object.prototype.hasOwnProperty.call(config, 'autoHeight')) {
|
|
visualizationOptions.autoHeight = config.autoHeight;
|
|
}
|
|
|
|
// Width constraints
|
|
const minColumns = parseInt(config.minColumns, 10);
|
|
if (isFinite(minColumns) && minColumns >= 0) {
|
|
visualizationOptions.minSizeX = minColumns;
|
|
}
|
|
const maxColumns = parseInt(config.maxColumns, 10);
|
|
if (isFinite(maxColumns) && maxColumns >= 0) {
|
|
visualizationOptions.maxSizeX = Math.min(maxColumns, dashboardGridOptions.columns);
|
|
}
|
|
|
|
// Height constraints
|
|
// `minRows` is preferred, but it should be kept for backward compatibility
|
|
const height = parseInt(config.height, 10);
|
|
if (isFinite(height)) {
|
|
visualizationOptions.minSizeY = Math.ceil(height / dashboardGridOptions.rowHeight);
|
|
}
|
|
const minRows = parseInt(config.minRows, 10);
|
|
if (isFinite(minRows)) {
|
|
visualizationOptions.minSizeY = minRows;
|
|
}
|
|
const maxRows = parseInt(config.maxRows, 10);
|
|
if (isFinite(maxRows) && maxRows >= 0) {
|
|
visualizationOptions.maxSizeY = maxRows;
|
|
}
|
|
|
|
// Default dimensions
|
|
const defaultWidth = parseInt(config.defaultColumns, 10);
|
|
if (isFinite(defaultWidth) && defaultWidth > 0) {
|
|
visualizationOptions.sizeX = defaultWidth;
|
|
}
|
|
const defaultHeight = parseInt(config.defaultRows, 10);
|
|
if (isFinite(defaultHeight) && defaultHeight > 0) {
|
|
visualizationOptions.sizeY = defaultHeight;
|
|
}
|
|
}
|
|
|
|
return visualizationOptions;
|
|
}
|
|
|
|
export const ParameterMappingType = {
|
|
DashboardLevel: 'dashboard-level',
|
|
WidgetLevel: 'widget-level',
|
|
StaticValue: 'static-value',
|
|
};
|
|
|
|
function WidgetFactory($http, $location, Query) {
|
|
class WidgetService {
|
|
static MappingType = ParameterMappingType;
|
|
|
|
constructor(data) {
|
|
// Copy properties
|
|
each(data, (v, k) => {
|
|
this[k] = v;
|
|
});
|
|
|
|
const visualizationOptions = calculatePositionOptions(this);
|
|
|
|
this.options = this.options || {};
|
|
this.options.position = extend(
|
|
{},
|
|
visualizationOptions,
|
|
pick(this.options.position, ['col', 'row', 'sizeX', 'sizeY', 'autoHeight']),
|
|
);
|
|
|
|
if (this.options.position.sizeY < 0) {
|
|
this.options.position.autoHeight = true;
|
|
}
|
|
}
|
|
|
|
getQuery() {
|
|
if (!this.query && this.visualization) {
|
|
this.query = new Query(this.visualization.query);
|
|
}
|
|
|
|
return this.query;
|
|
}
|
|
|
|
getQueryResult() {
|
|
return this.data;
|
|
}
|
|
|
|
getName() {
|
|
if (this.visualization) {
|
|
return `${this.visualization.query.name} (${this.visualization.name})`;
|
|
}
|
|
return truncate(this.text, 20);
|
|
}
|
|
|
|
load(force, maxAge) {
|
|
if (!this.visualization) {
|
|
return undefined;
|
|
}
|
|
|
|
// Both `this.data` and `this.queryResult` are query result objects;
|
|
// `this.data` is last loaded query result;
|
|
// `this.queryResult` is currently loading query result;
|
|
// while widget is refreshing, `this.data` !== `this.queryResult`
|
|
|
|
if (force || (this.queryResult === undefined)) {
|
|
this.loading = true;
|
|
this.refreshStartedAt = moment();
|
|
|
|
if (maxAge === undefined || force) {
|
|
maxAge = force ? 0 : undefined;
|
|
}
|
|
this.queryResult = this.getQuery().getQueryResult(maxAge);
|
|
|
|
this.queryResult.toPromise()
|
|
.then((result) => {
|
|
this.loading = false;
|
|
this.data = result;
|
|
})
|
|
.catch((error) => {
|
|
this.loading = false;
|
|
this.data = error;
|
|
});
|
|
}
|
|
|
|
return this.queryResult.toPromise();
|
|
}
|
|
|
|
save(key, value) {
|
|
const data = pick(this, 'options', 'text', 'id', 'width', 'dashboard_id', 'visualization_id');
|
|
if (key && value) {
|
|
data[key] = merge({}, data[key], value); // done like this so `this.options` doesn't get updated by side-effect
|
|
}
|
|
|
|
let url = 'api/widgets';
|
|
if (this.id) {
|
|
url = `${url}/${this.id}`;
|
|
}
|
|
|
|
return $http.post(url, data).then((response) => {
|
|
each(response.data, (v, k) => {
|
|
this[k] = v;
|
|
});
|
|
|
|
return this;
|
|
});
|
|
}
|
|
|
|
delete() {
|
|
const url = `api/widgets/${this.id}`;
|
|
return $http.delete(url);
|
|
}
|
|
|
|
isStaticParam(param) {
|
|
const mappings = this.getParameterMappings();
|
|
const mappingType = mappings[param.name].type;
|
|
return mappingType === WidgetService.MappingType.StaticValue;
|
|
}
|
|
|
|
getParametersDefs() {
|
|
const mappings = this.getParameterMappings();
|
|
// textboxes does not have query
|
|
const params = this.getQuery() ? this.getQuery().getParametersDefs() : [];
|
|
|
|
const queryParams = $location.search();
|
|
|
|
const localTypes = [
|
|
WidgetService.MappingType.WidgetLevel,
|
|
WidgetService.MappingType.StaticValue,
|
|
];
|
|
return map(
|
|
filter(params, param => localTypes.indexOf(mappings[param.name].type) >= 0),
|
|
(param) => {
|
|
const mapping = mappings[param.name];
|
|
const result = param.clone();
|
|
result.title = mapping.title || param.title;
|
|
result.locals = [param];
|
|
result.urlPrefix = `p_w${this.id}_`;
|
|
if (mapping.type === WidgetService.MappingType.StaticValue) {
|
|
result.setValue(mapping.value);
|
|
} else {
|
|
result.fromUrlParams(queryParams);
|
|
}
|
|
return result;
|
|
},
|
|
);
|
|
}
|
|
|
|
getParameterMappings() {
|
|
if (!isObject(this.options.parameterMappings)) {
|
|
this.options.parameterMappings = {};
|
|
}
|
|
|
|
const existingParams = {};
|
|
// textboxes does not have query
|
|
const params = this.getQuery() ? this.getQuery().getParametersDefs() : [];
|
|
each(params, (param) => {
|
|
existingParams[param.name] = true;
|
|
if (!isObject(this.options.parameterMappings[param.name])) {
|
|
// "migration" for old dashboards: parameters with `global` flag
|
|
// should be mapped to a dashboard-level parameter with the same name
|
|
this.options.parameterMappings[param.name] = {
|
|
name: param.name,
|
|
type: param.global ? WidgetService.MappingType.DashboardLevel : WidgetService.MappingType.WidgetLevel,
|
|
mapTo: param.name, // map to param with the same name
|
|
value: null, // for StaticValue
|
|
title: '', // Use parameter's title
|
|
};
|
|
}
|
|
});
|
|
|
|
// Remove mappings for parameters that do not exists anymore
|
|
const removedParams = difference(
|
|
keys(this.options.parameterMappings),
|
|
keys(existingParams),
|
|
);
|
|
each(removedParams, (name) => {
|
|
delete this.options.parameterMappings[name];
|
|
});
|
|
|
|
return this.options.parameterMappings;
|
|
}
|
|
}
|
|
|
|
return WidgetService;
|
|
}
|
|
|
|
export default function init(ngModule) {
|
|
ngModule.factory('Widget', WidgetFactory);
|
|
|
|
ngModule.run(($injector) => {
|
|
Widget = $injector.get('Widget');
|
|
});
|
|
}
|
|
|
|
init.init = true;
|