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); +}