diff --git a/client/app/components/HelpTrigger.jsx b/client/app/components/HelpTrigger.jsx
index 8ad487f34e..ee987d76f7 100644
--- a/client/app/components/HelpTrigger.jsx
+++ b/client/app/components/HelpTrigger.jsx
@@ -1,4 +1,4 @@
-import { startsWith } from "lodash";
+import { startsWith, get } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import cx from "classnames";
@@ -42,13 +42,18 @@ export const TYPES = {
export default class HelpTrigger extends React.Component {
static propTypes = {
- type: PropTypes.oneOf(Object.keys(TYPES)).isRequired,
+ type: PropTypes.oneOf(Object.keys(TYPES)),
+ href: PropTypes.string,
+ title: PropTypes.node,
className: PropTypes.string,
showTooltip: PropTypes.bool,
children: PropTypes.node,
};
static defaultProps = {
+ type: null,
+ href: null,
+ title: null,
className: null,
showTooltip: true,
children: ,
@@ -102,13 +107,15 @@ export default class HelpTrigger extends React.Component {
this.setState({ currentUrl });
};
+ getUrl = () => {
+ const [pagePath] = get(TYPES, this.props.type, []);
+ return pagePath ? DOMAIN + HELP_PATH + pagePath : this.props.href;
+ };
+
openDrawer = () => {
this.setState({ visible: true });
- const [pagePath] = TYPES[this.props.type];
- const url = DOMAIN + HELP_PATH + pagePath;
-
// wait for drawer animation to complete so there's no animation jank
- setTimeout(() => this.loadIframe(url), 300);
+ setTimeout(() => this.loadIframe(this.getUrl()), 300);
};
closeDrawer = event => {
@@ -120,16 +127,32 @@ export default class HelpTrigger extends React.Component {
};
render() {
- const [, tooltip] = TYPES[this.props.type];
+ const tooltip = get(TYPES, `${this.props.type}[1]`, this.props.title);
const className = cx("help-trigger", this.props.className);
const url = this.state.currentUrl;
+ const isAllowedDomain = startsWith(url || this.getUrl(), DOMAIN);
+
return (
-
-
- {this.props.children}
-
+
+ {tooltip}
+ {!isAllowedDomain && }
+ >
+ ) : null
+ }>
+ {isAllowedDomain ? (
+
+ {this.props.children}
+
+ ) : (
+
+ {this.props.children}
+
+ )}
-
+
@@ -205,17 +212,13 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
- Error while rendering visualization.}>
-
-
+
diff --git a/client/app/visualizations/components/EditVisualizationDialog.less b/client/app/components/visualizations/EditVisualizationDialog.less
similarity index 100%
rename from client/app/visualizations/components/EditVisualizationDialog.less
rename to client/app/components/visualizations/EditVisualizationDialog.less
diff --git a/client/app/visualizations/components/VisualizationName.jsx b/client/app/components/visualizations/VisualizationName.jsx
similarity index 86%
rename from client/app/visualizations/components/VisualizationName.jsx
rename to client/app/components/visualizations/VisualizationName.jsx
index 4c908a2906..71ba14b835 100644
--- a/client/app/visualizations/components/VisualizationName.jsx
+++ b/client/app/components/visualizations/VisualizationName.jsx
@@ -1,6 +1,6 @@
import React from "react";
import { VisualizationType } from "@/visualizations/prop-types";
-import registeredVisualizations from "@/visualizations";
+import registeredVisualizations from "@/visualizations/registeredVisualizations";
import "./VisualizationName.less";
diff --git a/client/app/visualizations/components/VisualizationName.less b/client/app/components/visualizations/VisualizationName.less
similarity index 100%
rename from client/app/visualizations/components/VisualizationName.less
rename to client/app/components/visualizations/VisualizationName.less
diff --git a/client/app/visualizations/components/VisualizationRenderer.jsx b/client/app/components/visualizations/VisualizationRenderer.jsx
similarity index 59%
rename from client/app/visualizations/components/VisualizationRenderer.jsx
rename to client/app/components/visualizations/VisualizationRenderer.jsx
index 10aa3a973c..0a0b812f49 100644
--- a/client/app/visualizations/components/VisualizationRenderer.jsx
+++ b/client/app/components/visualizations/VisualizationRenderer.jsx
@@ -1,11 +1,11 @@
-import { isEqual, map, find } from "lodash";
+import { map, find } from "lodash";
import React, { useState, useMemo, useEffect, useRef } from "react";
import PropTypes from "prop-types";
import getQueryResultData from "@/lib/getQueryResultData";
-import ErrorBoundary, { ErrorMessage } from "@/components/ErrorBoundary";
+import { getColumnCleanName } from "@/services/query-result";
import Filters, { FiltersType, filterData } from "@/components/Filters";
import { VisualizationType } from "@/visualizations/prop-types";
-import registeredVisualizations from "@/visualizations";
+import { Renderer } from "@/components/visualizations/visualizationComponents";
function combineFilters(localFilters, globalFilters) {
// tiny optimization - to avoid unnecessary updates
@@ -31,9 +31,6 @@ export default function VisualizationRenderer(props) {
const filtersRef = useRef();
filtersRef.current = filters;
- const lastOptions = useRef();
- const errorHandlerRef = useRef();
-
// Reset local filters when query results updated
useEffect(() => {
setFilters(combineFilters(data.filters, props.filters));
@@ -46,55 +43,37 @@ export default function VisualizationRenderer(props) {
setFilters(combineFilters(filtersRef.current, props.filters));
}, [props.filters]);
+ const cleanColumnNames = useMemo(
+ () => map(data.columns, col => ({ ...col, name: getColumnCleanName(col.friendly_name) })),
+ [data.columns]
+ );
+
const filteredData = useMemo(
() => ({
- columns: data.columns,
+ columns: cleanColumnNames,
rows: filterData(data.rows, filters),
}),
- [data, filters]
+ [cleanColumnNames, data.rows, filters]
);
const { showFilters, visualization } = props;
- const { Renderer, getOptions } = registeredVisualizations[visualization.type];
- let options = getOptions(visualization.options, data);
+ let options = { ...visualization.options };
// define pagination size based on context for Table visualization
if (visualization.type === "TABLE") {
options.paginationSize = props.context === "widget" ? "small" : "default";
}
- // Avoid unnecessary updates (which may be expensive or cause issues with
- // internal state of some visualizations like Table) - compare options deeply
- // and use saved reference if nothing changed
- // More details: https://github.com/getredash/redash/pull/3963#discussion_r306935810
- if (isEqual(lastOptions.current, options)) {
- options = lastOptions.current;
- }
- lastOptions.current = options;
-
- useEffect(() => {
- if (errorHandlerRef.current) {
- errorHandlerRef.current.reset();
- }
- }, [props.visualization.options, data]);
-
return (
-
-
Error while rendering visualization.}>
- {showFilters && }
-
-
-
-
-
+ }
+ />
);
}
diff --git a/client/app/components/visualizations/editor/ContextHelp.jsx b/client/app/components/visualizations/editor/ContextHelp.jsx
index 32196cddef..704ed39f32 100644
--- a/client/app/components/visualizations/editor/ContextHelp.jsx
+++ b/client/app/components/visualizations/editor/ContextHelp.jsx
@@ -1,9 +1,8 @@
import React from "react";
import PropTypes from "prop-types";
import Popover from "antd/lib/popover";
-import Tooltip from "antd/lib/tooltip";
import Icon from "antd/lib/icon";
-import HelpTrigger from "@/components/HelpTrigger";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
import "./context-help.less";
@@ -28,30 +27,26 @@ ContextHelp.defaultProps = {
ContextHelp.defaultIcon = ;
function NumberFormatSpecs() {
+ const { HelpTriggerComponent } = visualizationsSettings;
return (
-
+
{ContextHelp.defaultIcon}
-
+
);
}
function DateTimeFormatSpecs() {
+ const { HelpTriggerComponent } = visualizationsSettings;
return (
-
- Formatting Dates and Times
-
-
- }>
-
- {ContextHelp.defaultIcon}
-
-
+
+ {ContextHelp.defaultIcon}
+
);
}
diff --git a/client/app/components/visualizations/visualizationComponents.jsx b/client/app/components/visualizations/visualizationComponents.jsx
new file mode 100644
index 0000000000..f6c8330781
--- /dev/null
+++ b/client/app/components/visualizations/visualizationComponents.jsx
@@ -0,0 +1,42 @@
+import React from "react";
+import { pick } from "lodash";
+import HelpTrigger from "@/components/HelpTrigger";
+import { Renderer as VisRenderer, Editor as VisEditor } from "@/visualizations";
+import { updateVisualizationsSettings } from "@/visualizations/visualizationsSettings";
+import { clientConfig } from "@/services/auth";
+
+import countriesDataUrl from "@/visualizations/choropleth/maps/countries.geo.json";
+import subdivJapanDataUrl from "@/visualizations/choropleth/maps/japan.prefectures.geo.json";
+
+function wrapComponentWithSettings(WrappedComponent) {
+ return function VisualizationComponent(props) {
+ updateVisualizationsSettings({
+ HelpTriggerComponent: HelpTrigger,
+ choroplethAvailableMaps: {
+ countries: {
+ name: "Countries",
+ url: countriesDataUrl,
+ },
+ subdiv_japan: {
+ name: "Japan/Prefectures",
+ url: subdivJapanDataUrl,
+ },
+ },
+ ...pick(clientConfig, [
+ "dateFormat",
+ "dateTimeFormat",
+ "integerFormat",
+ "floatFormat",
+ "booleanValues",
+ "tableCellMaxJSONSize",
+ "allowCustomJSVisualization",
+ "hidePlotlyModeBar",
+ ]),
+ });
+
+ return ;
+ };
+}
+
+export const Renderer = wrapComponentWithSettings(VisRenderer);
+export const Editor = wrapComponentWithSettings(VisEditor);
diff --git a/client/app/pages/queries/VisualizationEmbed.jsx b/client/app/pages/queries/VisualizationEmbed.jsx
index 9c62471424..cb0e0911e8 100644
--- a/client/app/pages/queries/VisualizationEmbed.jsx
+++ b/client/app/pages/queries/VisualizationEmbed.jsx
@@ -18,8 +18,8 @@ import { Moment } from "@/components/proptypes";
import TimeAgo from "@/components/TimeAgo";
import Timer from "@/components/Timer";
import QueryResultsLink from "@/components/EditVisualizationButton/QueryResultsLink";
-import VisualizationName from "@/visualizations/components/VisualizationName";
-import VisualizationRenderer from "@/visualizations/components/VisualizationRenderer";
+import VisualizationName from "@/components/visualizations/VisualizationName";
+import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import { VisualizationType } from "@/visualizations/prop-types";
import logoUrl from "@/assets/images/redash_icon_small.png";
diff --git a/client/app/pages/queries/components/QueryVisualizationTabs.jsx b/client/app/pages/queries/components/QueryVisualizationTabs.jsx
index aa93407f13..6e6243b2ed 100644
--- a/client/app/pages/queries/components/QueryVisualizationTabs.jsx
+++ b/client/app/pages/queries/components/QueryVisualizationTabs.jsx
@@ -4,7 +4,7 @@ import cx from "classnames";
import { find, orderBy } from "lodash";
import useMedia from "use-media";
import Tabs from "antd/lib/tabs";
-import VisualizationRenderer from "@/visualizations/components/VisualizationRenderer";
+import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
diff --git a/client/app/pages/queries/hooks/useEditVisualizationDialog.js b/client/app/pages/queries/hooks/useEditVisualizationDialog.js
index 5ee3cc3682..69bf9b489e 100644
--- a/client/app/pages/queries/hooks/useEditVisualizationDialog.js
+++ b/client/app/pages/queries/hooks/useEditVisualizationDialog.js
@@ -1,6 +1,6 @@
import { isFunction, extend, filter, find } from "lodash";
import { useCallback, useRef } from "react";
-import EditVisualizationDialog from "@/visualizations/components/EditVisualizationDialog";
+import EditVisualizationDialog from "@/components/visualizations/EditVisualizationDialog";
export default function useEditVisualizationDialog(query, queryResult, onChange) {
const onChangeRef = useRef();
diff --git a/client/app/services/widget.js b/client/app/services/widget.js
index 9d89be99c2..dfc9ed2efe 100644
--- a/client/app/services/widget.js
+++ b/client/app/services/widget.js
@@ -4,7 +4,7 @@ import { each, pick, extend, isObject, truncate, keys, difference, filter, map,
import location from "@/services/location";
import { cloneParameter } from "@/services/parameters";
import dashboardGridOptions from "@/config/dashboard-grid-options";
-import registeredVisualizations from "@/visualizations";
+import registeredVisualizations from "@/visualizations/registeredVisualizations";
import { Query } from "./query";
export const WidgetTypeEnum = {
diff --git a/client/app/visualizations/Editor.jsx b/client/app/visualizations/Editor.jsx
new file mode 100644
index 0000000000..01fe1cd167
--- /dev/null
+++ b/client/app/visualizations/Editor.jsx
@@ -0,0 +1,14 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { EditorPropTypes } from "@/visualizations/prop-types";
+import registeredVisualizations from "@/visualizations/registeredVisualizations";
+
+export default function Editor({ type, ...otherProps }) {
+ const { Editor } = registeredVisualizations[type];
+ return ;
+}
+
+Editor.propTypes = {
+ type: PropTypes.string.isRequired,
+ ...EditorPropTypes,
+};
diff --git a/client/app/visualizations/Renderer.jsx b/client/app/visualizations/Renderer.jsx
new file mode 100644
index 0000000000..3bd005f9f6
--- /dev/null
+++ b/client/app/visualizations/Renderer.jsx
@@ -0,0 +1,58 @@
+import { isEqual } from "lodash";
+import React, { useEffect, useRef } from "react";
+import PropTypes from "prop-types";
+import ErrorBoundary, { ErrorMessage } from "@/components/ErrorBoundary";
+import { RendererPropTypes } from "@/visualizations/prop-types";
+import registeredVisualizations from "@/visualizations/registeredVisualizations";
+
+export default function Renderer({
+ type,
+ data,
+ options: optionsProp,
+ visualizationName,
+ addonBefore,
+ addonAfter,
+ ...otherProps
+}) {
+ const lastOptions = useRef();
+ const errorHandlerRef = useRef();
+
+ const { Renderer, getOptions } = registeredVisualizations[type];
+
+ // Avoid unnecessary updates (which may be expensive or cause issues with
+ // internal state of some visualizations like Table) - compare options deeply
+ // and use saved reference if nothing changed
+ // More details: https://github.com/getredash/redash/pull/3963#discussion_r306935810
+ let options = getOptions(optionsProp, data);
+ if (isEqual(lastOptions.current, options)) {
+ options = lastOptions.current;
+ }
+ lastOptions.current = options;
+
+ useEffect(() => {
+ if (errorHandlerRef.current) {
+ errorHandlerRef.current.reset();
+ }
+ }, [optionsProp, data]);
+
+ return (
+
+ {addonBefore}
+
Error while rendering visualization.}>
+
+
+
+
+ {addonAfter}
+
+ );
+}
+
+Renderer.propTypes = {
+ type: PropTypes.string.isRequired,
+ addonBefore: PropTypes.node,
+ addonAfter: PropTypes.node,
+ ...RendererPropTypes,
+};
diff --git a/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx b/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx
index b4dc589643..ac0e03aff9 100644
--- a/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx
+++ b/client/app/visualizations/chart/Editor/ChartTypeSelect.jsx
@@ -1,7 +1,7 @@
import { map } from "lodash";
import React, { useMemo } from "react";
import { Select } from "@/components/visualizations/editor";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
export default function ChartTypeSelect(props) {
const chartTypes = useMemo(() => {
@@ -16,7 +16,7 @@ export default function ChartTypeSelect(props) {
{ type: "box", name: "Box", icon: "square-o" },
];
- if (clientConfig.allowCustomJSVisualizations) {
+ if (visualizationsSettings.allowCustomJSVisualizations) {
result.push({ type: "custom", name: "Custom", icon: "code" });
}
diff --git a/client/app/visualizations/chart/Renderer/PlotlyChart.jsx b/client/app/visualizations/chart/Renderer/PlotlyChart.jsx
index 54cef84738..6d1cc9e7ef 100644
--- a/client/app/visualizations/chart/Renderer/PlotlyChart.jsx
+++ b/client/app/visualizations/chart/Renderer/PlotlyChart.jsx
@@ -4,7 +4,7 @@ import useMedia from "use-media";
import { ErrorBoundaryContext } from "@/components/ErrorBoundary";
import { RendererPropTypes } from "@/visualizations/prop-types";
import resizeObserver from "@/services/resizeObserver";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
import getChartData from "../getChartData";
import { Plotly, prepareData, prepareLayout, updateData, applyLayoutFixes } from "../plotly";
@@ -29,7 +29,7 @@ export default function PlotlyChart({ options, data }) {
const plotlyOptions = {
showLink: false,
displaylogo: false,
- displayModeBar: !clientConfig.hidePlotlyModeBar
+ displayModeBar: !visualizationsSettings.hidePlotlyModeBar,
};
const chartData = getChartData(data.rows, options);
diff --git a/client/app/visualizations/chart/Renderer/index.jsx b/client/app/visualizations/chart/Renderer/index.jsx
index 69e63ee26b..338a0f149c 100644
--- a/client/app/visualizations/chart/Renderer/index.jsx
+++ b/client/app/visualizations/chart/Renderer/index.jsx
@@ -3,12 +3,12 @@ import { RendererPropTypes } from "@/visualizations/prop-types";
import PlotlyChart from "./PlotlyChart";
import CustomPlotlyChart from "./CustomPlotlyChart";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
import "./renderer.less";
export default function Renderer({ options, ...props }) {
- if (options.globalSeriesType === "custom" && clientConfig.allowCustomJSVisualizations) {
+ if (options.globalSeriesType === "custom" && visualizationsSettings.allowCustomJSVisualizations) {
return ;
}
return ;
diff --git a/client/app/visualizations/chart/getOptions.js b/client/app/visualizations/chart/getOptions.js
index 56cfb28ef1..cbd5cee406 100644
--- a/client/app/visualizations/chart/getOptions.js
+++ b/client/app/visualizations/chart/getOptions.js
@@ -1,5 +1,5 @@
import { merge } from "lodash";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
const DEFAULT_OPTIONS = {
globalSeriesType: "column",
@@ -19,7 +19,7 @@ const DEFAULT_OPTIONS = {
// showDataLabels: false, // depends on chart type
numberFormat: "0,0[.]00000",
percentFormat: "0[.]00%",
- // dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from clientConfig
+ // dateTimeFormat: 'DD/MM/YYYY HH:mm', // will be set from visualizationsSettings
textFormat: "", // default: combination of {{ @@yPercent }} ({{ @@y }} ± {{ @@yError }})
missingValuesAsZero: true,
@@ -31,7 +31,7 @@ export default function getOptions(options) {
DEFAULT_OPTIONS,
{
showDataLabels: options.globalSeriesType === "pie",
- dateTimeFormat: clientConfig.dateTimeFormat,
+ dateTimeFormat: visualizationsSettings.dateTimeFormat,
},
options
);
diff --git a/client/app/visualizations/choropleth/Renderer/index.jsx b/client/app/visualizations/choropleth/Renderer/index.jsx
index 09d34869b4..96cada33b9 100644
--- a/client/app/visualizations/choropleth/Renderer/index.jsx
+++ b/client/app/visualizations/choropleth/Renderer/index.jsx
@@ -1,25 +1,16 @@
-import { omit, merge } from "lodash";
+import { omit, merge, get } from "lodash";
+import axios from "axios";
import React, { useState, useEffect } from "react";
-import { axios } from "@/services/axios";
import { RendererPropTypes } from "@/visualizations/prop-types";
import useMemoWithDeepCompare from "@/lib/hooks/useMemoWithDeepCompare";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
import initChoropleth from "./initChoropleth";
import { prepareData } from "./utils";
import "./renderer.less";
-import countriesDataUrl from "../maps/countries.geo.json";
-import subdivJapanDataUrl from "../maps/japan.prefectures.geo.json";
-
function getDataUrl(type) {
- switch (type) {
- case "countries":
- return countriesDataUrl;
- case "subdiv_japan":
- return subdivJapanDataUrl;
- default:
- return null;
- }
+ return get(visualizationsSettings, `choroplethAvailableMaps.${type}.url`, undefined);
}
export default function Renderer({ data, options, onOptionsChange }) {
@@ -33,7 +24,7 @@ export default function Renderer({ data, options, onOptionsChange }) {
useEffect(() => {
let cancelled = false;
- axios.get(getDataUrl(options.mapType)).then(data => {
+ axios.get(getDataUrl(options.mapType)).then(({ data }) => {
if (!cancelled) {
setGeoJson(data);
}
diff --git a/client/app/visualizations/details/DetailsRenderer.jsx b/client/app/visualizations/details/DetailsRenderer.jsx
index f605124632..3faa211905 100644
--- a/client/app/visualizations/details/DetailsRenderer.jsx
+++ b/client/app/visualizations/details/DetailsRenderer.jsx
@@ -2,15 +2,15 @@ import React, { useState } from "react";
import { map, mapValues, keyBy } from "lodash";
import moment from "moment";
import { RendererPropTypes } from "@/visualizations/prop-types";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
import Pagination from "antd/lib/pagination";
import "./details.less";
function renderValue(value, type) {
const formats = {
- date: clientConfig.dateFormat,
- datetime: clientConfig.dateTimeFormat,
+ date: visualizationsSettings.dateFormat,
+ datetime: visualizationsSettings.dateTimeFormat,
};
if (type === "date" || type === "datetime") {
diff --git a/client/app/visualizations/funnel/Renderer/prepareData.js b/client/app/visualizations/funnel/Renderer/prepareData.js
index 425e75cef6..2872366cdb 100644
--- a/client/app/visualizations/funnel/Renderer/prepareData.js
+++ b/client/app/visualizations/funnel/Renderer/prepareData.js
@@ -1,10 +1,10 @@
import { map, maxBy, sortBy, toString } from "lodash";
import moment from "moment";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
function stepValueToString(value) {
if (moment.isMoment(value)) {
- const format = clientConfig.dateTimeFormat || "DD/MM/YYYY HH:mm";
+ const format = visualizationsSettings.dateTimeFormat || "DD/MM/YYYY HH:mm";
return value.format(format);
}
return toString(value);
diff --git a/client/app/visualizations/index.js b/client/app/visualizations/index.js
index 3d09afc813..b166d62be5 100644
--- a/client/app/visualizations/index.js
+++ b/client/app/visualizations/index.js
@@ -1,97 +1,4 @@
-import { find, flatten, each } from "lodash";
-import PropTypes from "prop-types";
+import Renderer from "./Renderer";
+import Editor from "./Editor";
-import boxPlotVisualization from "./box-plot";
-import chartVisualization from "./chart";
-import choroplethVisualization from "./choropleth";
-import cohortVisualization from "./cohort";
-import counterVisualization from "./counter";
-import detailsVisualization from "./details";
-import funnelVisualization from "./funnel";
-import mapVisualization from "./map";
-import pivotVisualization from "./pivot";
-import sankeyVisualization from "./sankey";
-import sunburstVisualization from "./sunburst";
-import tableVisualization from "./table";
-import wordCloudVisualization from "./word-cloud";
-
-const VisualizationConfig = PropTypes.shape({
- type: PropTypes.string.isRequired,
- name: PropTypes.string.isRequired,
- getOptions: PropTypes.func.isRequired, // (existingOptions: object, data: { columns[], rows[] }) => object
- isDefault: PropTypes.bool,
- isDeprecated: PropTypes.bool,
- Renderer: PropTypes.func.isRequired,
- Editor: PropTypes.func,
-
- // other config options
- autoHeight: PropTypes.bool,
- defaultRows: PropTypes.number,
- defaultColumns: PropTypes.number,
- minRows: PropTypes.number,
- maxRows: PropTypes.number,
- minColumns: PropTypes.number,
- maxColumns: PropTypes.number,
-});
-
-const registeredVisualizations = {};
-
-function validateVisualizationConfig(config) {
- const typeSpecs = { config: VisualizationConfig };
- const values = { config };
- PropTypes.checkPropTypes(typeSpecs, values, "prop", "registerVisualization");
-}
-
-function registerVisualization(config) {
- validateVisualizationConfig(config);
- config = {
- Editor: () => null,
- ...config,
- isDefault: config.isDefault && !config.isDeprecated,
- };
-
- if (registeredVisualizations[config.type]) {
- throw new Error(`Visualization ${config.type} already registered.`);
- }
-
- registeredVisualizations[config.type] = config;
-}
-
-each(
- flatten([
- boxPlotVisualization,
- chartVisualization,
- choroplethVisualization,
- cohortVisualization,
- counterVisualization,
- detailsVisualization,
- funnelVisualization,
- mapVisualization,
- pivotVisualization,
- sankeyVisualization,
- sunburstVisualization,
- tableVisualization,
- wordCloudVisualization,
- ]),
- registerVisualization
-);
-
-export default registeredVisualizations;
-
-export function getDefaultVisualization() {
- // return any visualization explicitly marked as default, or any non-deprecated otherwise
- return (
- find(registeredVisualizations, visualization => visualization.isDefault) ||
- find(registeredVisualizations, visualization => !visualization.isDeprecated)
- );
-}
-
-export function newVisualization(type = null, options = {}) {
- const visualization = type ? registeredVisualizations[type] : getDefaultVisualization();
- return {
- type: visualization.type,
- name: visualization.name,
- description: "",
- options,
- };
-}
+export { Renderer, Editor };
diff --git a/client/app/visualizations/registeredVisualizations.js b/client/app/visualizations/registeredVisualizations.js
new file mode 100644
index 0000000000..3d09afc813
--- /dev/null
+++ b/client/app/visualizations/registeredVisualizations.js
@@ -0,0 +1,97 @@
+import { find, flatten, each } from "lodash";
+import PropTypes from "prop-types";
+
+import boxPlotVisualization from "./box-plot";
+import chartVisualization from "./chart";
+import choroplethVisualization from "./choropleth";
+import cohortVisualization from "./cohort";
+import counterVisualization from "./counter";
+import detailsVisualization from "./details";
+import funnelVisualization from "./funnel";
+import mapVisualization from "./map";
+import pivotVisualization from "./pivot";
+import sankeyVisualization from "./sankey";
+import sunburstVisualization from "./sunburst";
+import tableVisualization from "./table";
+import wordCloudVisualization from "./word-cloud";
+
+const VisualizationConfig = PropTypes.shape({
+ type: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ getOptions: PropTypes.func.isRequired, // (existingOptions: object, data: { columns[], rows[] }) => object
+ isDefault: PropTypes.bool,
+ isDeprecated: PropTypes.bool,
+ Renderer: PropTypes.func.isRequired,
+ Editor: PropTypes.func,
+
+ // other config options
+ autoHeight: PropTypes.bool,
+ defaultRows: PropTypes.number,
+ defaultColumns: PropTypes.number,
+ minRows: PropTypes.number,
+ maxRows: PropTypes.number,
+ minColumns: PropTypes.number,
+ maxColumns: PropTypes.number,
+});
+
+const registeredVisualizations = {};
+
+function validateVisualizationConfig(config) {
+ const typeSpecs = { config: VisualizationConfig };
+ const values = { config };
+ PropTypes.checkPropTypes(typeSpecs, values, "prop", "registerVisualization");
+}
+
+function registerVisualization(config) {
+ validateVisualizationConfig(config);
+ config = {
+ Editor: () => null,
+ ...config,
+ isDefault: config.isDefault && !config.isDeprecated,
+ };
+
+ if (registeredVisualizations[config.type]) {
+ throw new Error(`Visualization ${config.type} already registered.`);
+ }
+
+ registeredVisualizations[config.type] = config;
+}
+
+each(
+ flatten([
+ boxPlotVisualization,
+ chartVisualization,
+ choroplethVisualization,
+ cohortVisualization,
+ counterVisualization,
+ detailsVisualization,
+ funnelVisualization,
+ mapVisualization,
+ pivotVisualization,
+ sankeyVisualization,
+ sunburstVisualization,
+ tableVisualization,
+ wordCloudVisualization,
+ ]),
+ registerVisualization
+);
+
+export default registeredVisualizations;
+
+export function getDefaultVisualization() {
+ // return any visualization explicitly marked as default, or any non-deprecated otherwise
+ return (
+ find(registeredVisualizations, visualization => visualization.isDefault) ||
+ find(registeredVisualizations, visualization => !visualization.isDeprecated)
+ );
+}
+
+export function newVisualization(type = null, options = {}) {
+ const visualization = type ? registeredVisualizations[type] : getDefaultVisualization();
+ return {
+ type: visualization.type,
+ name: visualization.name,
+ description: "",
+ options,
+ };
+}
diff --git a/client/app/visualizations/table/columns/json.jsx b/client/app/visualizations/table/columns/json.jsx
index 2f932e78c9..19ba9b4e48 100644
--- a/client/app/visualizations/table/columns/json.jsx
+++ b/client/app/visualizations/table/columns/json.jsx
@@ -1,12 +1,12 @@
import { isString, isUndefined } from "lodash";
import React from "react";
import JsonViewInteractive from "@/components/json-view-interactive/JsonViewInteractive";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
export default function initJsonColumn(column) {
function prepareData(row) {
const text = row[column.name];
- if (isString(text) && text.length <= clientConfig.tableCellMaxJSONSize) {
+ if (isString(text) && text.length <= visualizationsSettings.tableCellMaxJSONSize) {
try {
return { text, value: JSON.parse(text) };
} catch (e) {
diff --git a/client/app/visualizations/table/getOptions.js b/client/app/visualizations/table/getOptions.js
index e6953b2718..0cf1cd30c6 100644
--- a/client/app/visualizations/table/getOptions.js
+++ b/client/app/visualizations/table/getOptions.js
@@ -1,6 +1,5 @@
import _ from "lodash";
-import { getColumnCleanName } from "@/services/query-result";
-import { clientConfig } from "@/services/auth";
+import { visualizationsSettings } from "@/visualizations/visualizationsSettings";
const DEFAULT_OPTIONS = {
itemsPerPage: 25,
@@ -26,7 +25,7 @@ function getDefaultColumnsOptions(columns) {
displayAs: displayAs[col.type] || "string",
visible: true,
order: 100000 + index,
- title: getColumnCleanName(col.name),
+ title: col.name,
allowSearch: false,
alignContent: getColumnContentAlignment(col.type),
// `string` cell options
@@ -37,17 +36,17 @@ function getDefaultColumnsOptions(columns) {
function getDefaultFormatOptions(column) {
const dateTimeFormat = {
- date: clientConfig.dateFormat || "DD/MM/YYYY",
- datetime: clientConfig.dateTimeFormat || "DD/MM/YYYY HH:mm",
+ date: visualizationsSettings.dateFormat || "DD/MM/YYYY",
+ datetime: visualizationsSettings.dateTimeFormat || "DD/MM/YYYY HH:mm",
};
const numberFormat = {
- integer: clientConfig.integerFormat || "0,0",
- float: clientConfig.floatFormat || "0,0.00",
+ integer: visualizationsSettings.integerFormat || "0,0",
+ float: visualizationsSettings.floatFormat || "0,0.00",
};
return {
dateTimeFormat: dateTimeFormat[column.type],
numberFormat: numberFormat[column.type],
- booleanValues: clientConfig.booleanValues || ["false", "true"],
+ booleanValues: visualizationsSettings.booleanValues || ["false", "true"],
// `image` cell options
imageUrlTemplate: "{{ @ }}",
imageTitleTemplate: "{{ @ }}",
diff --git a/client/app/visualizations/visualizationsSettings.js b/client/app/visualizations/visualizationsSettings.js
new file mode 100644
index 0000000000..521e16e9fe
--- /dev/null
+++ b/client/app/visualizations/visualizationsSettings.js
@@ -0,0 +1,50 @@
+import React from "react";
+import { extend } from "lodash";
+import PropTypes from "prop-types";
+import Tooltip from "antd/lib/tooltip";
+
+function HelpTrigger({ title, href, className, children }) {
+ return (
+
+ {title}
+
+
+ }>
+
+ {children}
+
+
+ );
+}
+
+HelpTrigger.propTypes = {
+ title: PropTypes.node,
+ href: PropTypes.string.isRequired,
+ className: PropTypes.string,
+ children: PropTypes.node,
+};
+
+HelpTrigger.defaultValues = {
+ title: null,
+ className: null,
+ children: null,
+};
+
+export const visualizationsSettings = {
+ HelpTriggerComponent: HelpTrigger,
+ dateFormat: "DD/MM/YYYY",
+ dateTimeFormat: "DD/MM/YYYY HH:mm",
+ integetFormat: "0,0",
+ floatFormat: "0,0.00",
+ booleanValues: ["false", "true"],
+ tableCellMaxJSONSize: 50000,
+ allowCustomJSVisualization: false,
+ hidePlotlyModeBar: false,
+ choroplethAvailableMaps: {},
+};
+
+export function updateVisualizationsSettings(options) {
+ extend(visualizationsSettings, options);
+}