Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visualizations customizable settings #4793

Merged
merged 14 commits into from
Apr 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 34 additions & 11 deletions client/app/components/HelpTrigger.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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: <i className="fa fa-question-circle" />,
Expand Down Expand Up @@ -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 => {
Expand All @@ -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 (
<React.Fragment>
<Tooltip title={this.props.showTooltip ? tooltip : null}>
<a onClick={this.openDrawer} className={className}>
{this.props.children}
</a>
<Tooltip
title={
this.props.showTooltip ? (
<>
{tooltip}
{!isAllowedDomain && <i className="fa fa-external-link" style={{ marginLeft: 5 }} />}
</>
) : null
}>
{isAllowedDomain ? (
<a onClick={this.openDrawer} className={className}>
{this.props.children}
</a>
) : (
<a href={url || this.getUrl()} className={className} rel="noopener noreferrer" target="_blank">
{this.props.children}
</a>
)}
</Tooltip>
<Drawer
placement="right"
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/QueryLink.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import PropTypes from "prop-types";
import { VisualizationType } from "@/visualizations/prop-types";
import VisualizationName from "@/visualizations/components/VisualizationName";
import VisualizationName from "@/components/visualizations/VisualizationName";

import "./QueryLink.less";

Expand Down
4 changes: 2 additions & 2 deletions client/app/components/dashboards/ExpandedWidgetDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import PropTypes from "prop-types";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import VisualizationRenderer from "@/visualizations/components/VisualizationRenderer";
import VisualizationName from "@/visualizations/components/VisualizationName";
import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import VisualizationName from "@/components/visualizations/VisualizationName";

function ExpandedWidgetDialog({ dialog, widget }) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import QueryLink from "@/components/QueryLink";
import { FiltersType } from "@/components/Filters";
import ExpandedWidgetDialog from "@/components/dashboards/ExpandedWidgetDialog";
import EditParameterMappingsDialog from "@/components/dashboards/EditParameterMappingsDialog";
import VisualizationRenderer from "@/visualizations/components/VisualizationRenderer";
import VisualizationRenderer from "@/components/visualizations/VisualizationRenderer";
import Widget from "./Widget";

function visualizationWidgetMenuOptions({ widget, canEditDashboard, onParametersEdit }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import Modal from "antd/lib/modal";
import Select from "antd/lib/select";
import Input from "antd/lib/input";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import ErrorBoundary, { ErrorMessage } from "@/components/ErrorBoundary";
import Filters, { filterData } from "@/components/Filters";
import notification from "@/services/notification";
import Visualization from "@/services/visualization";
import recordEvent from "@/services/recordEvent";
import getQueryResultData from "@/lib/getQueryResultData";
import { VisualizationType } from "@/visualizations/prop-types";
import registeredVisualizations, { getDefaultVisualization, newVisualization } from "@/visualizations";
import { Renderer, Editor } from "@/components/visualizations/visualizationComponents";
import registeredVisualizations, {
getDefaultVisualization,
newVisualization,
} from "@/visualizations/registeredVisualizations";

import "./EditVisualizationDialog.less";

Expand Down Expand Up @@ -146,8 +149,6 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
confirmDialogClose(nameChanged || optionsChanged).then(dialog.dismiss);
}

const { Renderer, Editor } = registeredVisualizations[type];

// When editing existing visualization chart type selector is disabled, so add only existing visualization's
// descriptor there (to properly render the component). For new visualizations show all types except of deprecated
const availableVisualizations = isNew
Expand Down Expand Up @@ -196,7 +197,13 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
/>
</div>
<div data-test="VisualizationEditor">
<Editor data={data} options={options} visualizationName={name} onOptionsChange={onOptionsChanged} />
<Editor
type={type}
data={data}
options={options}
visualizationName={name}
onOptionsChange={onOptionsChanged}
/>
</div>
</div>
<div className="visualization-preview">
Expand All @@ -205,17 +212,13 @@ function EditVisualizationDialog({ dialog, visualization, query, queryResult })
</label>
<Filters filters={filters} onChange={setFilters} />
<div className="scrollbox" data-test="VisualizationPreview">
<ErrorBoundary
ref={errorHandlerRef}
renderError={() => <ErrorMessage>Error while rendering visualization.</ErrorMessage>}>
<Renderer
data={filteredData}
options={options}
visualizationName={name}
onOptionsChange={onOptionsChanged}
context="query"
/>
</ErrorBoundary>
<Renderer
type={type}
data={filteredData}
options={options}
visualizationName={name}
onOptionsChange={onOptionsChanged}
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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));
Expand All @@ -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) })),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kravets-levko it seemed to make sense to move getColumnCleanName out of Table vis and rather apply it to all visualizations, do you see any problems may happen with this change? (that method is used to remove ::filter from column names for example)

[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 (
<div className="visualization-renderer">
<ErrorBoundary
ref={errorHandlerRef}
renderError={() => <ErrorMessage>Error while rendering visualization.</ErrorMessage>}>
{showFilters && <Filters filters={filters} onChange={setFilters} />}
<div className="visualization-renderer-wrapper">
<Renderer
key={`visualization${visualization.id}`}
options={options}
data={filteredData}
visualizationName={visualization.name}
/>
</div>
</ErrorBoundary>
</div>
<Renderer
key={`visualization${visualization.id}`}
type={visualization.type}
options={options}
data={filteredData}
visualizationName={visualization.name}
addonBefore={showFilters && <Filters filters={filters} onChange={setFilters} />}
/>
);
}

Expand Down
33 changes: 14 additions & 19 deletions client/app/components/visualizations/editor/ContextHelp.jsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -28,30 +27,26 @@ ContextHelp.defaultProps = {
ContextHelp.defaultIcon = <Icon className="m-l-5 m-r-5" type="question-circle" theme="filled" />;

function NumberFormatSpecs() {
const { HelpTriggerComponent } = visualizationsSettings;
return (
<HelpTrigger type="NUMBER_FORMAT_SPECS" className="visualization-editor-context-help">
<HelpTriggerComponent
title="Formatting Numbers"
href="https://redash.io/help/user-guide/visualizations/formatting-numbers"
className="visualization-editor-context-help">
{ContextHelp.defaultIcon}
</HelpTrigger>
</HelpTriggerComponent>
);
}

function DateTimeFormatSpecs() {
const { HelpTriggerComponent } = visualizationsSettings;
return (
<Tooltip
title={
<React.Fragment>
Formatting Dates and Times
<i className="fa fa-external-link m-l-5" />
</React.Fragment>
}>
<a
className="visualization-editor-context-help"
href="https://momentjs.com/docs/#/displaying/format/"
target="_blank"
rel="noopener noreferrer">
{ContextHelp.defaultIcon}
</a>
</Tooltip>
<HelpTriggerComponent
title="Formatting Dates and Times"
href="https://momentjs.com/docs/#/displaying/format/"
className="visualization-editor-context-help">
{ContextHelp.defaultIcon}
</HelpTriggerComponent>
);
}

Expand Down
42 changes: 42 additions & 0 deletions client/app/components/visualizations/visualizationComponents.jsx
Original file line number Diff line number Diff line change
@@ -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,
},
},
Comment on lines +15 to +24
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to go in the direction of #4599 as it has a complementary code to this.

...pick(clientConfig, [
"dateFormat",
"dateTimeFormat",
"integerFormat",
"floatFormat",
"booleanValues",
"tableCellMaxJSONSize",
"allowCustomJSVisualization",
"hidePlotlyModeBar",
]),
});

return <WrappedComponent {...props} />;
};
}

export const Renderer = wrapComponentWithSettings(VisRenderer);
export const Editor = wrapComponentWithSettings(VisEditor);
4 changes: 2 additions & 2 deletions client/app/pages/queries/VisualizationEmbed.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
Loading