mirror of
https://github.com/getredash/redash.git
synced 2026-03-21 16:00:09 -04:00
* Initial React Rendering with useDashboard
* Make sure widgets refresh + useCallback
* Rename collectFilters and add refreshRate
* Fix error updates not being rendered
* Only render widget bottom when queryResults exists
* Cleanup
* Add useCallback to refreshDashboard
* Make sure Promise.all have all promises done
* Start migrating Dashoard to React
- initial rendering
- some actions
- temporary updated less file
* Fullscreen handler added
* Separate refreshRateHandler hook
* Add a few tooltips
* Separate DashboardControl and normalize btn width
* Share Button
* Fix serach params not updating
* Enumerate More Options
* Toggle Publish options
* Archive Dashboard
* Parameters + Filters
* Prepare Manage Permissions
* Start to create edit mode
* Add Edit Mode functionalities
* Use previous state when updating dashboard
* Mobile adjustments
* PermissionsEditorDialog + Dashboard page title
* Update Dashboard spec
* Fix other specs
* Break dashboard.less
* Hide publish button on mobile
* Angular Cleaning
* Keep edit state when changing resolution
* Bug fix: Dashboard Level Filters not updating
* Remove prepareWidgetsForDashboard
* Revert "Remove prepareWidgetsForDashboard"
This reverts commit b434f03da1.
* Avoid saving layout changes out of editing mode
* Apply policy for enabled refresh rates
* Disable loadDashboard deps
* Restyled by prettier (#4459)
* Update title when dashboard name updates
Co-authored-by: restyled-io[bot] <32688539+restyled-io[bot]@users.noreply.github.com>
269 lines
8.1 KiB
JavaScript
269 lines
8.1 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
|
|
|
|
export const WidgetTypeEnum = {
|
|
TEXTBOX: "textbox",
|
|
VISUALIZATION: "visualization",
|
|
RESTRICTED: "restricted",
|
|
};
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
get type() {
|
|
if (this.visualization) {
|
|
return WidgetTypeEnum.VISUALIZATION;
|
|
} else if (this.restricted) {
|
|
return WidgetTypeEnum.RESTRICTED;
|
|
}
|
|
return WidgetTypeEnum.TEXTBOX;
|
|
}
|
|
|
|
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 Promise.resolve();
|
|
}
|
|
|
|
// 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;
|
|
return result;
|
|
})
|
|
.catch(error => {
|
|
this.loading = false;
|
|
this.data = error;
|
|
return 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(false) : [];
|
|
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;
|
|
}
|
|
|
|
getLocalParameters() {
|
|
return filter(this.getParametersDefs(), param => !this.isStaticParam(param));
|
|
}
|
|
}
|
|
|
|
return WidgetService;
|
|
}
|
|
|
|
export default function init(ngModule) {
|
|
ngModule.factory("Widget", WidgetFactory);
|
|
|
|
ngModule.run($injector => {
|
|
Widget = $injector.get("Widget");
|
|
});
|
|
}
|
|
|
|
init.init = true;
|