mirror of
https://github.com/getredash/redash.git
synced 2026-03-27 11:00:34 -04:00
Set of improvements and refinements to visualizations after React migration (#4382)
This commit is contained in:
@@ -386,4 +386,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// overrides for checkbox
|
||||
@checkbox-prefix-cls: ~'@{ant-prefix}-checkbox';
|
||||
|
||||
.@{checkbox-prefix-cls}-wrapper + span,
|
||||
.@{checkbox-prefix-cls} + span {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { toString } from 'lodash';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Card from 'antd/lib/card';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
|
||||
|
||||
import ColorInput from './Input';
|
||||
import Swatch from './Swatch';
|
||||
@@ -17,6 +17,7 @@ import './index.less';
|
||||
|
||||
export default function ColorPicker({
|
||||
color, placement, presetColors, presetColumns, interactive, children, onChange, triggerProps,
|
||||
addonBefore, addonAfter,
|
||||
}) {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const validatedColor = useMemo(() => validateColor(color), [color]);
|
||||
@@ -61,45 +62,49 @@ export default function ColorPicker({
|
||||
}, [validatedColor, visible]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
arrowPointAtCenter
|
||||
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
|
||||
overlayStyle={{ '--color-picker-selected-color': currentColor }}
|
||||
content={(
|
||||
<Card
|
||||
data-test="ColorPicker"
|
||||
className="color-picker-panel"
|
||||
bordered={false}
|
||||
title={toString(currentColor).toUpperCase()}
|
||||
headStyle={{
|
||||
backgroundColor: currentColor,
|
||||
color: tinycolor(currentColor).isLight() ? '#000000' : '#ffffff',
|
||||
}}
|
||||
actions={actions}
|
||||
>
|
||||
<ColorInput
|
||||
color={currentColor}
|
||||
presetColors={presetColors}
|
||||
presetColumns={presetColumns}
|
||||
onChange={handleInputChange}
|
||||
onPressEnter={handleApply}
|
||||
<React.Fragment>
|
||||
{addonBefore}
|
||||
<Popover
|
||||
arrowPointAtCenter
|
||||
overlayClassName={`color-picker ${interactive ? 'color-picker-interactive' : 'color-picker-with-actions'}`}
|
||||
overlayStyle={{ '--color-picker-selected-color': currentColor }}
|
||||
content={(
|
||||
<Card
|
||||
data-test="ColorPicker"
|
||||
className="color-picker-panel"
|
||||
bordered={false}
|
||||
title={toString(currentColor).toUpperCase()}
|
||||
headStyle={{
|
||||
backgroundColor: currentColor,
|
||||
color: chooseTextColorForBackground(currentColor),
|
||||
}}
|
||||
actions={actions}
|
||||
>
|
||||
<ColorInput
|
||||
color={currentColor}
|
||||
presetColors={presetColors}
|
||||
presetColumns={presetColumns}
|
||||
onChange={handleInputChange}
|
||||
onPressEnter={handleApply}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
trigger="click"
|
||||
placement={placement}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
{children || (
|
||||
<Swatch
|
||||
color={validatedColor}
|
||||
size={30}
|
||||
{...triggerProps}
|
||||
className={cx('color-picker-trigger', triggerProps.className)}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
trigger="click"
|
||||
placement={placement}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
>
|
||||
{children || (
|
||||
<Swatch
|
||||
color={validatedColor}
|
||||
size={30}
|
||||
{...triggerProps}
|
||||
className={cx('color-picker-trigger', triggerProps.className)}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
)}
|
||||
</Popover>
|
||||
{addonAfter}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,6 +123,8 @@ ColorPicker.propTypes = {
|
||||
interactive: PropTypes.bool,
|
||||
triggerProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
children: PropTypes.node,
|
||||
addonBefore: PropTypes.node,
|
||||
addonAfter: PropTypes.node,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -129,6 +136,8 @@ ColorPicker.defaultProps = {
|
||||
interactive: false,
|
||||
triggerProps: {},
|
||||
children: null,
|
||||
addonBefore: null,
|
||||
addonAfter: null,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { startsWith } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
@@ -87,29 +88,30 @@ export const TYPES = {
|
||||
'/user-guide/querying/writing-queries#Managing-Query-Permissions',
|
||||
'Guide: Managing Query Permissions',
|
||||
],
|
||||
NUMBER_FORMAT_SPECS: [
|
||||
'/user-guide/visualizations/formatting-numbers',
|
||||
'Formatting Numbers',
|
||||
],
|
||||
};
|
||||
|
||||
export default class HelpTrigger extends React.Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.oneOf(Object.keys(TYPES)).isRequired,
|
||||
className: PropTypes.string,
|
||||
showTooltip: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
className: null,
|
||||
showTooltip: true,
|
||||
children: <i className="fa fa-question-circle" />,
|
||||
};
|
||||
|
||||
iframeRef = null;
|
||||
iframeRef = React.createRef();
|
||||
|
||||
iframeLoadingTimeout = null;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.iframeRef = React.createRef();
|
||||
}
|
||||
|
||||
state = {
|
||||
visible: false,
|
||||
loading: false,
|
||||
@@ -118,7 +120,7 @@ export default class HelpTrigger extends React.Component {
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.onPostMessageReceived, DOMAIN);
|
||||
window.addEventListener('message', this.onPostMessageReceived, false);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@@ -142,13 +144,17 @@ export default class HelpTrigger extends React.Component {
|
||||
};
|
||||
|
||||
onPostMessageReceived = (event) => {
|
||||
if (!startsWith(event.origin, DOMAIN)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, message: currentUrl } = event.data || {};
|
||||
if (type !== IFRAME_URL_UPDATE_MESSAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ currentUrl });
|
||||
}
|
||||
};
|
||||
|
||||
openDrawer = () => {
|
||||
this.setState({ visible: true });
|
||||
@@ -174,7 +180,7 @@ export default class HelpTrigger extends React.Component {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Tooltip title={tooltip}>
|
||||
<Tooltip title={this.props.showTooltip ? tooltip : null}>
|
||||
<a onClick={this.openDrawer} className={className}>
|
||||
{this.props.children}
|
||||
</a>
|
||||
|
||||
45
client/app/components/TextAlignmentSelect/index.jsx
Normal file
45
client/app/components/TextAlignmentSelect/index.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { pickBy, startsWith } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
|
||||
import './index.less';
|
||||
|
||||
export default function TextAlignmentSelect({ className, ...props }) {
|
||||
return (
|
||||
// Antd RadioGroup does not use any custom attributes
|
||||
<div {...pickBy(props, (v, k) => startsWith(k, 'data-'))}>
|
||||
<Radio.Group
|
||||
className={cx('text-alignment-select', className)}
|
||||
{...props}
|
||||
>
|
||||
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="left" data-test="TextAlignmentSelect.Left">
|
||||
<Icon type="align-left" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="center" data-test="TextAlignmentSelect.Center">
|
||||
<Icon type="align-center" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="right" data-test="TextAlignmentSelect.Right">
|
||||
<Icon type="align-right" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TextAlignmentSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
TextAlignmentSelect.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
13
client/app/components/TextAlignmentSelect/index.less
Normal file
13
client/app/components/TextAlignmentSelect/index.less
Normal file
@@ -0,0 +1,13 @@
|
||||
.ant-radio-group.text-alignment-select {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
// fit <Input> height
|
||||
height: 35px;
|
||||
line-height: 33px;
|
||||
}
|
||||
}
|
||||
54
client/app/components/visualizations/editor/ContextHelp.jsx
Normal file
54
client/app/components/visualizations/editor/ContextHelp.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
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 './context-help.less';
|
||||
|
||||
export default function ContextHelp({ icon, children, ...props }) {
|
||||
return (
|
||||
<Popover {...props} content={children}>{icon || ContextHelp.defaultIcon}</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
ContextHelp.propTypes = {
|
||||
icon: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ContextHelp.defaultProps = {
|
||||
icon: null,
|
||||
children: null,
|
||||
};
|
||||
|
||||
ContextHelp.defaultIcon = (
|
||||
<Icon className="m-l-5 m-r-5" type="question-circle" theme="filled" />
|
||||
);
|
||||
|
||||
function NumberFormatSpecs() {
|
||||
return (
|
||||
<HelpTrigger type="NUMBER_FORMAT_SPECS" className="visualization-editor-context-help">
|
||||
{ContextHelp.defaultIcon}
|
||||
</HelpTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DateTimeFormatSpecs() {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
ContextHelp.NumberFormatSpecs = NumberFormatSpecs;
|
||||
ContextHelp.DateTimeFormatSpecs = DateTimeFormatSpecs;
|
||||
41
client/app/components/visualizations/editor/Section.jsx
Normal file
41
client/app/components/visualizations/editor/Section.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
|
||||
function SectionTitle({ className, children, ...props }) {
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <h4 className={cx('m-t-0', 'm-b-15', className)} {...props}>{children}</h4>;
|
||||
}
|
||||
|
||||
SectionTitle.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
SectionTitle.defaultProps = {
|
||||
className: null,
|
||||
children: null,
|
||||
};
|
||||
|
||||
export default function Section({ className, children, ...props }) {
|
||||
return (
|
||||
<div className={cx('m-b-15', className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Section.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
Section.defaultProps = {
|
||||
className: null,
|
||||
children: null,
|
||||
};
|
||||
|
||||
Section.Title = SectionTitle;
|
||||
34
client/app/components/visualizations/editor/Switch.jsx
Normal file
34
client/app/components/visualizations/editor/Switch.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import AntSwitch from 'antd/lib/switch';
|
||||
import Typography from 'antd/lib/typography';
|
||||
|
||||
export default function Switch({ id, children, disabled, ...props }) {
|
||||
const fallbackId = useMemo(() => `visualization-editor-control-${Math.random().toString(36).substr(2, 10)}`, []);
|
||||
id = id || fallbackId;
|
||||
|
||||
if (children) {
|
||||
return (
|
||||
<label htmlFor={id} className="d-flex align-items-center">
|
||||
<AntSwitch id={id} disabled={disabled} {...props} />
|
||||
<Typography.Text className="m-l-10" disabled={disabled}>{children}</Typography.Text>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AntSwitch {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
Switch.propTypes = {
|
||||
id: PropTypes.string,
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
Switch.defaultProps = {
|
||||
id: null,
|
||||
disabled: false,
|
||||
children: null,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
@import (reference, less) '~@/assets/less/main.less';
|
||||
|
||||
a.visualization-editor-context-help {
|
||||
&, .ant-typography & {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
|
||||
&:hover, &:active {
|
||||
color: @link-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.visualization-editor-control-label {
|
||||
&.visualization-editor-control-label-horizontal {
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { isFunction, map, filter, extend, merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export const UpdateOptionsStrategy = {
|
||||
replace: (existingOptions, newOptions) => merge({}, newOptions),
|
||||
shallowMerge: (existingOptions, newOptions) => extend({}, existingOptions, newOptions),
|
||||
deepMerge: (existingOptions, newOptions) => merge({}, existingOptions, newOptions),
|
||||
};
|
||||
|
||||
export function TabbedEditor({ tabs, options, data, onOptionsChange, ...restProps }) {
|
||||
const optionsChanged = (newOptions, updateStrategy = UpdateOptionsStrategy.deepMerge) => {
|
||||
onOptionsChange(updateStrategy(options, newOptions));
|
||||
};
|
||||
|
||||
tabs = filter(tabs, tab => (isFunction(tab.isAvailable) ? tab.isAvailable(options, data) : true));
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
{map(tabs, ({ key, title, component: Component }) => (
|
||||
<Tabs.TabPane key={key} tab={<span data-test={`VisualizationEditor.Tabs.${key}`}>{title}</span>}>
|
||||
<Component options={options} data={data} onOptionsChange={optionsChanged} {...restProps} />
|
||||
</Tabs.TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
TabbedEditor.propTypes = {
|
||||
...EditorPropTypes,
|
||||
tabs: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
isAvailable: PropTypes.func, // (options) => boolean
|
||||
component: PropTypes.func.isRequired,
|
||||
})),
|
||||
};
|
||||
|
||||
TabbedEditor.defaultProps = {
|
||||
tabs: [],
|
||||
};
|
||||
|
||||
export default function createTabbedEditor(tabs) {
|
||||
return function TabbedEditorWrapper(props) {
|
||||
return (
|
||||
<TabbedEditor {...props} tabs={tabs} />
|
||||
);
|
||||
};
|
||||
}
|
||||
30
client/app/components/visualizations/editor/index.js
Normal file
30
client/app/components/visualizations/editor/index.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import AntSelect from 'antd/lib/select';
|
||||
import AntInput from 'antd/lib/input';
|
||||
import AntInputNumber from 'antd/lib/input-number';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
|
||||
import RedashColorPicker from '@/components/ColorPicker';
|
||||
import RedashTextAlignmentSelect from '@/components/TextAlignmentSelect';
|
||||
|
||||
import withControlLabel, { ControlLabel } from './withControlLabel';
|
||||
import createTabbedEditor from './createTabbedEditor';
|
||||
import Section from './Section';
|
||||
import Switch from './Switch';
|
||||
import ContextHelp from './ContextHelp';
|
||||
|
||||
export {
|
||||
Section,
|
||||
ControlLabel,
|
||||
Checkbox,
|
||||
Switch,
|
||||
ContextHelp,
|
||||
|
||||
withControlLabel,
|
||||
createTabbedEditor,
|
||||
};
|
||||
export const Select = withControlLabel(AntSelect);
|
||||
export const Input = withControlLabel(AntInput);
|
||||
export const TextArea = withControlLabel(AntInput.TextArea);
|
||||
export const InputNumber = withControlLabel(AntInputNumber);
|
||||
export const ColorPicker = withControlLabel(RedashColorPicker);
|
||||
export const TextAlignmentSelect = withControlLabel(RedashTextAlignmentSelect);
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import Typography from 'antd/lib/typography';
|
||||
|
||||
import './control-label.less';
|
||||
|
||||
export function ControlLabel({ layout, label, labelProps, disabled, children }) {
|
||||
if ((layout === 'vertical') && label) {
|
||||
return (
|
||||
<div className="visualization-editor-control-label visualization-editor-control-label-vertical">
|
||||
<label {...labelProps}>
|
||||
<Typography.Text disabled={disabled}>{label}</Typography.Text>
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ((layout === 'horizontal') && label) {
|
||||
return (
|
||||
<Grid.Row
|
||||
className="visualization-editor-control-label visualization-editor-control-label-horizontal"
|
||||
type="flex"
|
||||
align="middle"
|
||||
gutter={15}
|
||||
>
|
||||
<Grid.Col span={12}>
|
||||
<label {...labelProps}>
|
||||
<Typography.Text disabled={disabled}>{label}</Typography.Text>
|
||||
</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
{children}
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
ControlLabel.propTypes = {
|
||||
layout: PropTypes.oneOf(['vertical', 'horizontal']),
|
||||
label: PropTypes.node,
|
||||
labelProps: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
disabled: PropTypes.bool,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
ControlLabel.defaultProps = {
|
||||
layout: 'vertical',
|
||||
label: null,
|
||||
disabled: false,
|
||||
children: null,
|
||||
};
|
||||
|
||||
export default function withControlLabel(WrappedControl) {
|
||||
// eslint-disable-next-line react/prop-types
|
||||
function ControlWrapper({ id, layout, label, labelProps, disabled, ...props }) {
|
||||
const fallbackId = useMemo(() => `visualization-editor-control-${Math.random().toString(36).substr(2, 10)}`, []);
|
||||
labelProps = {
|
||||
...labelProps,
|
||||
htmlFor: id || fallbackId,
|
||||
};
|
||||
|
||||
return (
|
||||
<ControlLabel layout={layout} label={label} labelProps={labelProps} disabled={disabled}>
|
||||
<WrappedControl id={labelProps.htmlFor} disabled={disabled} {...props} />
|
||||
</ControlLabel>
|
||||
);
|
||||
}
|
||||
|
||||
// Copy static methods from `WrappedComponent`
|
||||
hoistNonReactStatics(ControlWrapper, WrappedControl);
|
||||
|
||||
return ControlWrapper;
|
||||
}
|
||||
11
client/app/lib/chooseTextColorForBackground.js
Normal file
11
client/app/lib/chooseTextColorForBackground.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { maxBy } from 'lodash';
|
||||
import chroma from 'chroma-js';
|
||||
|
||||
export default function chooseTextColorForBackground(backgroundColor, textColors = ['#ffffff', '#333333']) {
|
||||
try {
|
||||
backgroundColor = chroma(backgroundColor);
|
||||
return maxBy(textColors, color => chroma.contrast(backgroundColor, color));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export const AdditionalColors = {
|
||||
'Indian Red': '#981717',
|
||||
'Green 2': '#17BF51',
|
||||
'Green 3': '#049235',
|
||||
DarkTurquoise: '#00B6EB',
|
||||
'Dark Turquoise': '#00B6EB',
|
||||
'Dark Violet': '#A58AFF',
|
||||
'Pink 2': '#C63FA9',
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Input from 'antd/lib/input';
|
||||
import { Section, Input } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function Editor({ options, onOptionsChange }) {
|
||||
@@ -14,27 +14,25 @@ export default function Editor({ options, onOptionsChange }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="form-group">
|
||||
<label className="control-label" htmlFor="box-plot-x-axis-label">X Axis Label</label>
|
||||
<React.Fragment>
|
||||
<Section>
|
||||
<Input
|
||||
label="X Axis Label"
|
||||
data-test="BoxPlot.XAxisLabel"
|
||||
id="box-plot-x-axis-label"
|
||||
value={options.xAxisLabel}
|
||||
onChange={event => onXAxisLabelChanged(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="control-label" htmlFor="box-plot-y-axis-label">Y Axis Label</label>
|
||||
<Section>
|
||||
<Input
|
||||
label="Y Axis Label"
|
||||
data-test="BoxPlot.YAxisLabel"
|
||||
id="box-plot-y-axis-label"
|
||||
value={options.yAxisLabel}
|
||||
onChange={event => onYAxisLabelChanged(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,8 @@ import { isString, isObject, isFinite, isNumber, merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import { Section, Select, Input, InputNumber } from '@/components/visualizations/editor';
|
||||
|
||||
function toNumber(value) {
|
||||
value = isNumber(value) ? value : parseFloat(value);
|
||||
@@ -26,10 +24,9 @@ export default function AxisSettings({ id, options, features, onChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`chart-editor-${id}-type`}>Scale</label>
|
||||
<Section>
|
||||
<Select
|
||||
id={`chart-editor-${id}-type`}
|
||||
label="Scale"
|
||||
className="w-100"
|
||||
data-test={`Chart.${id}.Type`}
|
||||
defaultValue={options.type}
|
||||
@@ -41,43 +38,42 @@ export default function AxisSettings({ id, options, features, onChange }) {
|
||||
<Select.Option value="logarithmic" data-test={`Chart.${id}.Type.Logarithmic`}>Logarithmic</Select.Option>
|
||||
<Select.Option value="category" data-test={`Chart.${id}.Type.Category`}>Category</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`chart-editor-${id}-name`}>Name</label>
|
||||
<Section>
|
||||
<Input
|
||||
id={`chart-editor-${id}-name`}
|
||||
label="Name"
|
||||
data-test={`Chart.${id}.Name`}
|
||||
defaultValue={isObject(options.title) ? options.title.text : null}
|
||||
onChange={event => handleNameChange(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{features.range && (
|
||||
<Grid.Row gutter={15} type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor={`chart-editor-${id}-range-min`}>Min Value</label>
|
||||
<InputNumber
|
||||
id={`chart-editor-${id}-range-min`}
|
||||
className="w-100"
|
||||
placeholder="Auto"
|
||||
data-test={`Chart.${id}.RangeMin`}
|
||||
defaultValue={toNumber(options.rangeMin)}
|
||||
onChange={value => handleMinMaxChange({ rangeMin: toNumber(value) })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor={`chart-editor-${id}-range-max`}>Max Value</label>
|
||||
<InputNumber
|
||||
id={`chart-editor-${id}-range-max`}
|
||||
className="w-100"
|
||||
placeholder="Auto"
|
||||
data-test={`Chart.${id}.RangeMax`}
|
||||
defaultValue={toNumber(options.rangeMax)}
|
||||
onChange={value => handleMinMaxChange({ rangeMax: toNumber(value) })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Grid.Row gutter={15} type="flex" align="middle">
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
label="Min Value"
|
||||
className="w-100"
|
||||
placeholder="Auto"
|
||||
data-test={`Chart.${id}.RangeMin`}
|
||||
defaultValue={toNumber(options.rangeMin)}
|
||||
onChange={value => handleMinMaxChange({ rangeMin: toNumber(value) })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
label="Max Value"
|
||||
className="w-100"
|
||||
placeholder="Auto"
|
||||
data-test={`Chart.${id}.RangeMax`}
|
||||
defaultValue={toNumber(options.rangeMax)}
|
||||
onChange={value => handleMinMaxChange({ rangeMax: toNumber(value) })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</Section>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { map } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import { Select } from '@/components/visualizations/editor';
|
||||
import { clientConfig } from '@/services/auth';
|
||||
|
||||
export default function ChartTypeSelect(props) {
|
||||
|
||||
@@ -35,8 +35,10 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
|
||||
columnMapping: { a: 'x', b: 'y' },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Series.v.Color').first().simulate('click');
|
||||
findByTestID(el, 'ColorPicker').first().find('input')
|
||||
findByTestID(el, 'Chart.Series.v.Color')
|
||||
.find('.color-picker-trigger').last()
|
||||
.simulate('click');
|
||||
findByTestID(el, 'ColorPicker').last().find('input')
|
||||
.simulate('change', { target: { value: 'red' } });
|
||||
});
|
||||
});
|
||||
@@ -48,8 +50,8 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
|
||||
columnMapping: { a: 'x', b: 'y' },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme').first().simulate('click');
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme.RdBu').first().simulate('click');
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme').last().simulate('click');
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.ColorScheme.RdBu').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Sets custom color scheme', async (done) => {
|
||||
@@ -59,12 +61,16 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
|
||||
colorScheme: 'Custom...',
|
||||
}, after(2, done)); // we will perform 2 actions, so call `done` after all of them completed
|
||||
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.MinColor').first().simulate('click');
|
||||
findByTestID(el, 'ColorPicker').first().find('input')
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.MinColor')
|
||||
.find('.color-picker-trigger').last()
|
||||
.simulate('click');
|
||||
findByTestID(el, 'ColorPicker').last().find('input')
|
||||
.simulate('change', { target: { value: 'yellow' } });
|
||||
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.MaxColor').first().simulate('click');
|
||||
findByTestID(el, 'ColorPicker').first().find('input')
|
||||
findByTestID(el, 'Chart.Colors.Heatmap.MaxColor')
|
||||
.find('.color-picker-trigger').last()
|
||||
.simulate('click');
|
||||
findByTestID(el, 'ColorPicker').last().find('input')
|
||||
.simulate('change', { target: { value: 'red' } });
|
||||
});
|
||||
});
|
||||
@@ -76,8 +82,11 @@ describe('Visualizations -> Chart -> Editor -> Colors Settings', () => {
|
||||
columnMapping: { a: 'x', b: 'y' },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Series.b.Color').first().simulate('click');
|
||||
findByTestID(el, 'ColorPicker').first().find('input')
|
||||
findByTestID(el, 'Chart.Series.b.Color')
|
||||
.find('.color-picker-trigger').last()
|
||||
.simulate('click');
|
||||
|
||||
findByTestID(el, 'ColorPicker').last().find('input')
|
||||
.simulate('change', { target: { value: 'red' } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isString, map, uniq, flatten, filter, sortBy, keys } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Select from 'antd/lib/select';
|
||||
import { Section, Select } from '@/components/visualizations/editor';
|
||||
|
||||
const MappingTypes = {
|
||||
x: { label: 'X Column' },
|
||||
@@ -20,10 +20,9 @@ export default function ColumnMappingSelect({ value, availableColumns, type, onC
|
||||
const { label, multiple } = MappingTypes[type];
|
||||
|
||||
return (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`chart-editor-column-mapping-${type}`}>{label}</label>
|
||||
<Section>
|
||||
<Select
|
||||
id={`chart-editor-column-mapping-${type}`}
|
||||
label={label}
|
||||
className="w-100"
|
||||
data-test={`Chart.ColumnMapping.${type}`}
|
||||
mode={multiple ? 'multiple' : 'default'}
|
||||
@@ -36,7 +35,7 @@ export default function ColumnMappingSelect({ value, availableColumns, type, onC
|
||||
<Select.Option key={c} value={c} data-test={`Chart.ColumnMapping.${type}.${c}`}>{c}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { isNil, trimStart } from 'lodash';
|
||||
import React from 'react';
|
||||
import Switch from 'antd/lib/switch';
|
||||
import Input from 'antd/lib/input';
|
||||
import { Section, Switch, TextArea } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const defaultCustomCode = trimStart(`
|
||||
// Available variables are x, ys, element, and Plotly
|
||||
// Type console.log(x, ys); for more info about x and ys
|
||||
@@ -16,41 +13,37 @@ const defaultCustomCode = trimStart(`
|
||||
export default function CustomChartSettings({ options, onOptionsChange }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-custom-code">Custom code</label>
|
||||
<Section>
|
||||
<TextArea
|
||||
id="chart-editor-custom-code"
|
||||
label="Custom code"
|
||||
data-test="Chart.Custom.Code"
|
||||
className="form-control v-resizable"
|
||||
rows="10"
|
||||
defaultValue={isNil(options.customCode) ? defaultCustomCode : options.customCode}
|
||||
onChange={event => onOptionsChange({ customCode: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-enable-console-logs">
|
||||
<Switch
|
||||
id="chart-editor-enable-console-logs"
|
||||
data-test="Chart.Custom.EnableConsoleLogs"
|
||||
defaultChecked={options.enableConsoleLogs}
|
||||
onChange={enableConsoleLogs => onOptionsChange({ enableConsoleLogs })}
|
||||
/>
|
||||
<span className="m-l-10">Show errors in the console</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
data-test="Chart.Custom.EnableConsoleLogs"
|
||||
defaultChecked={options.enableConsoleLogs}
|
||||
onChange={enableConsoleLogs => onOptionsChange({ enableConsoleLogs })}
|
||||
>
|
||||
Show errors in the console
|
||||
</Switch>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-auto-update-custom-chart">
|
||||
<Switch
|
||||
id="chart-editor-auto-update-custom-chart"
|
||||
data-test="Chart.Custom.AutoUpdate"
|
||||
defaultChecked={options.autoRedraw}
|
||||
onChange={autoRedraw => onOptionsChange({ autoRedraw })}
|
||||
/>
|
||||
<span className="m-l-10">Auto update graph</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
id="chart-editor-auto-update-custom-chart"
|
||||
data-test="Chart.Custom.AutoUpdate"
|
||||
defaultChecked={options.autoRedraw}
|
||||
onChange={autoRedraw => onOptionsChange({ autoRedraw })}
|
||||
>
|
||||
Auto update graph
|
||||
</Switch>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { includes } from 'lodash';
|
||||
import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Input from 'antd/lib/input';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { Section, Input, Checkbox, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function DataLabelsSettings({ options, onOptionsChange }) {
|
||||
@@ -18,93 +15,50 @@ export default function DataLabelsSettings({ options, onOptionsChange }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{ isShowDataLabelsAvailable && (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-show-data-labels">
|
||||
<Checkbox
|
||||
id="chart-editor-show-data-labels"
|
||||
data-test="Chart.DataLabels.ShowDataLabels"
|
||||
defaultChecked={options.showDataLabels}
|
||||
onChange={event => onOptionsChange({ showDataLabels: event.target.checked })}
|
||||
/>
|
||||
<span>Show Data Labels</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Chart.DataLabels.ShowDataLabels"
|
||||
defaultChecked={options.showDataLabels}
|
||||
onChange={event => onOptionsChange({ showDataLabels: event.target.checked })}
|
||||
>
|
||||
Show Data Labels
|
||||
</Checkbox>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-number-format">
|
||||
Number Values Format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Section>
|
||||
<Input
|
||||
id="chart-editor-number-format"
|
||||
label={<React.Fragment>Number Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>}
|
||||
data-test="Chart.DataLabels.NumberFormat"
|
||||
defaultValue={options.numberFormat}
|
||||
onChange={e => debouncedOnOptionsChange({ numberFormat: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-percent-format">
|
||||
Percent Values Format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Section>
|
||||
<Input
|
||||
id="chart-editor-percent-format"
|
||||
label={<React.Fragment>Percent Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>}
|
||||
data-test="Chart.DataLabels.PercentFormat"
|
||||
defaultValue={options.percentFormat}
|
||||
onChange={e => debouncedOnOptionsChange({ percentFormat: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-datetime-format">
|
||||
Date/Time Values Format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://momentjs.com/docs/#/displaying/format/" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Section>
|
||||
<Input
|
||||
id="chart-editor-datetime-format"
|
||||
label={<React.Fragment>Date/Time Values Format<ContextHelp.DateTimeFormatSpecs /></React.Fragment>}
|
||||
data-test="Chart.DataLabels.DateTimeFormat"
|
||||
defaultValue={options.dateTimeFormat}
|
||||
onChange={e => debouncedOnOptionsChange({ dateTimeFormat: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-text-format">
|
||||
Data Labels
|
||||
<Popover
|
||||
placement="topRight"
|
||||
arrowPointAtCenter
|
||||
content={(
|
||||
<React.Fragment>
|
||||
<Section>
|
||||
<Input
|
||||
label={(
|
||||
<React.Fragment>
|
||||
Data Labels
|
||||
<ContextHelp placement="topRight" arrowPointAtCenter>
|
||||
<div className="p-b-5">Use special names to access additional properties:</div>
|
||||
<div><code>{'{{ @@name }}'}</code> series name;</div>
|
||||
<div><code>{'{{ @@x }}'}</code> x-value;</div>
|
||||
@@ -116,20 +70,15 @@ export default function DataLabelsSettings({ options, onOptionsChange }) {
|
||||
Also, all query result columns can be referenced<br />using
|
||||
<code className="text-nowrap">{'{{ column_name }}'}</code> syntax.
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Input
|
||||
id="chart-editor-text-format"
|
||||
</ContextHelp>
|
||||
</React.Fragment>
|
||||
)}
|
||||
data-test="Chart.DataLabels.TextFormat"
|
||||
placeholder="(auto)"
|
||||
defaultValue={options.textFormat}
|
||||
onChange={e => debouncedOnOptionsChange({ textFormat: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
|
||||
showDataLabels: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.DataLabels.ShowDataLabels').first().find('input')
|
||||
findByTestID(el, 'Chart.DataLabels.ShowDataLabels').last().find('input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
|
||||
numberFormat: '0[.]0000',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.DataLabels.NumberFormat').first()
|
||||
findByTestID(el, 'Chart.DataLabels.NumberFormat').last()
|
||||
.simulate('change', { target: { value: '0.00' } });
|
||||
});
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
|
||||
percentFormat: '0[.]00%',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.DataLabels.PercentFormat').first()
|
||||
findByTestID(el, 'Chart.DataLabels.PercentFormat').last()
|
||||
.simulate('change', { target: { value: '0.0%' } });
|
||||
});
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
|
||||
dateTimeFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.DataLabels.DateTimeFormat').first()
|
||||
findByTestID(el, 'Chart.DataLabels.DateTimeFormat').last()
|
||||
.simulate('change', { target: { value: 'YYYY MMM DD' } });
|
||||
});
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('Visualizations -> Chart -> Editor -> Data Labels Settings', () => {
|
||||
textFormat: null,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.DataLabels.TextFormat').first()
|
||||
findByTestID(el, 'Chart.DataLabels.TextFormat').last()
|
||||
.simulate('change', { target: { value: '{{ @@x }} :: {{ @@y }} / {{ @@yPercent }}' } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,18 +36,17 @@ export default function DefaultColorsSettings({ options, data, onOptionsChange }
|
||||
title: 'Color',
|
||||
dataIndex: 'color',
|
||||
width: '1%',
|
||||
className: 'text-nowrap',
|
||||
render: (unused, item) => (
|
||||
<div className="text-nowrap">
|
||||
<ColorPicker
|
||||
data-test={`Chart.Series.${item.key}.Color`}
|
||||
interactive
|
||||
presetColors={colors}
|
||||
placement="topRight"
|
||||
color={item.color}
|
||||
onChange={value => updateSeriesOption(item.key, 'color', value)}
|
||||
/>
|
||||
<ColorPicker.Label color={item.color} presetColors={colors} />
|
||||
</div>
|
||||
<ColorPicker
|
||||
data-test={`Chart.Series.${item.key}.Color`}
|
||||
interactive
|
||||
presetColors={colors}
|
||||
placement="topRight"
|
||||
color={item.color}
|
||||
onChange={value => updateSeriesOption(item.key, 'color', value)}
|
||||
addonAfter={<ColorPicker.Label color={item.color} presetColors={colors} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { isArray, map, mapValues, includes, some, each, difference } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import { Section, Select, Checkbox } from '@/components/visualizations/editor';
|
||||
import { UpdateOptionsStrategy } from '@/components/visualizations/editor/createTabbedEditor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import ChartTypeSelect from './ChartTypeSelect';
|
||||
@@ -95,21 +95,20 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
...mappedColumns,
|
||||
[type]: column,
|
||||
});
|
||||
onOptionsChange({ columnMapping }, false);
|
||||
onOptionsChange({ columnMapping }, UpdateOptionsStrategy.shallowMerge);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-global-series-type">Chart Type</label>
|
||||
<Section>
|
||||
<ChartTypeSelect
|
||||
id="chart-editor-global-series-type"
|
||||
label="Chart Type"
|
||||
className="w-100"
|
||||
data-test="Chart.GlobalSeriesType"
|
||||
defaultValue={options.globalSeriesType}
|
||||
onChange={handleGlobalSeriesTypeChange}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{map(mappedColumns, (value, type) => (
|
||||
<ColumnMappingSelect
|
||||
@@ -122,10 +121,9 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
))}
|
||||
|
||||
{includes(['pie'], options.globalSeriesType) && (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-pie-direction">Direction</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="chart-editor-pie-direction"
|
||||
label="Direction"
|
||||
className="w-100"
|
||||
data-test="Chart.PieDirection"
|
||||
defaultValue={options.direction.type}
|
||||
@@ -134,43 +132,37 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option value="counterclockwise" data-test="Chart.PieDirection.Counterclockwise">Counterclockwise</Select.Option>
|
||||
<Select.Option value="clockwise" data-test="Chart.PieDirection.Clockwise">Clockwise</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{!includes(['custom', 'heatmap'], options.globalSeriesType) && (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-show-legend">
|
||||
<Checkbox
|
||||
id="chart-editor-show-legend"
|
||||
data-test="Chart.ShowLegend"
|
||||
defaultChecked={options.legend.enabled}
|
||||
onChange={event => onOptionsChange({ legend: { enabled: event.target.checked } })}
|
||||
/>
|
||||
<span>Show Legend</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Chart.ShowLegend"
|
||||
defaultChecked={options.legend.enabled}
|
||||
onChange={event => onOptionsChange({ legend: { enabled: event.target.checked } })}
|
||||
>
|
||||
Show Legend
|
||||
</Checkbox>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{includes(['box'], options.globalSeriesType) && (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-show-points">
|
||||
<Checkbox
|
||||
id="chart-editor-show-points"
|
||||
data-test="Chart.ShowPoints"
|
||||
defaultChecked={options.showpoints}
|
||||
onChange={event => onOptionsChange({ showpoints: event.target.checked })}
|
||||
/>
|
||||
<span>Show All Points</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Chart.ShowPoints"
|
||||
defaultChecked={options.showpoints}
|
||||
onChange={event => onOptionsChange({ showpoints: event.target.checked })}
|
||||
>
|
||||
Show All Points
|
||||
</Checkbox>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{!includes(['custom', 'heatmap'], options.globalSeriesType) && (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-stacking">Stacking</label>
|
||||
|
||||
<Section>
|
||||
<Select
|
||||
id="chart-editor-stacking"
|
||||
label="Stacking"
|
||||
className="w-100"
|
||||
data-test="Chart.Stacking"
|
||||
defaultValue={options.series.stacking}
|
||||
@@ -180,28 +172,25 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option value={null} data-test="Chart.Stacking.Disabled">Disabled</Select.Option>
|
||||
<Select.Option value="stack" data-test="Chart.Stacking.Stack">Stack</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{includes(['line', 'area', 'column'], options.globalSeriesType) && (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-normalize-values">
|
||||
<Checkbox
|
||||
id="chart-editor-normalize-values"
|
||||
data-test="Chart.NormalizeValues"
|
||||
defaultChecked={options.series.percentValues}
|
||||
onChange={event => onOptionsChange({ series: { percentValues: event.target.checked } })}
|
||||
/>
|
||||
<span>Normalize values to percentage</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Chart.NormalizeValues"
|
||||
defaultChecked={options.series.percentValues}
|
||||
onChange={event => onOptionsChange({ series: { percentValues: event.target.checked } })}
|
||||
>
|
||||
Normalize values to percentage
|
||||
</Checkbox>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{!includes(['custom', 'heatmap', 'bubble', 'scatter'], options.globalSeriesType) && (
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-missing-values">Missing and NULL values</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="chart-editor-missing-values"
|
||||
label="Missing and NULL values"
|
||||
className="w-100"
|
||||
data-test="Chart.MissingValues"
|
||||
defaultValue={options.missingValuesAsZero ? 1 : 0}
|
||||
@@ -210,7 +199,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option value={0} data-test="Chart.MissingValues.Keep">Do not display in chart</Select.Option>
|
||||
<Select.Option value={1} data-test="Chart.MissingValues.Zero">Convert to 0 and display in chart</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -38,8 +38,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
},
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.GlobalSeriesType').first().simulate('click');
|
||||
findByTestID(el, 'Chart.ChartType.pie').first().simulate('click');
|
||||
findByTestID(el, 'Chart.GlobalSeriesType').last().simulate('click');
|
||||
findByTestID(el, 'Chart.ChartType.pie').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Pie: changes direction', (done) => {
|
||||
@@ -48,8 +48,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
direction: { type: 'counterclockwise' },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.PieDirection').first().simulate('click');
|
||||
findByTestID(el, 'Chart.PieDirection.Clockwise').first().simulate('click');
|
||||
findByTestID(el, 'Chart.PieDirection').last().simulate('click');
|
||||
findByTestID(el, 'Chart.PieDirection.Clockwise').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Toggles legend', (done) => {
|
||||
@@ -58,7 +58,7 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
legend: { enabled: true },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.ShowLegend').first().find('input')
|
||||
findByTestID(el, 'Chart.ShowLegend').last().find('input')
|
||||
.simulate('change', { target: { checked: false } });
|
||||
});
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
showpoints: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.ShowPoints').first().find('input')
|
||||
findByTestID(el, 'Chart.ShowPoints').last().find('input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
@@ -78,8 +78,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
series: {},
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Stacking').first().simulate('click');
|
||||
findByTestID(el, 'Chart.Stacking.Stack').first().simulate('click');
|
||||
findByTestID(el, 'Chart.Stacking').last().simulate('click');
|
||||
findByTestID(el, 'Chart.Stacking.Stack').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Toggles normalize values to percentage', (done) => {
|
||||
@@ -88,7 +88,7 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
series: {},
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.NormalizeValues').first().find('input')
|
||||
findByTestID(el, 'Chart.NormalizeValues').last().find('input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
@@ -98,8 +98,8 @@ describe('Visualizations -> Chart -> Editor -> General Settings', () => {
|
||||
missingValuesAsZero: true,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.MissingValues').first().simulate('click');
|
||||
findByTestID(el, 'Chart.MissingValues.Keep').first().simulate('click');
|
||||
findByTestID(el, 'Chart.MissingValues').last().simulate('click');
|
||||
findByTestID(el, 'Chart.MissingValues.Keep').last().simulate('click');
|
||||
});
|
||||
|
||||
describe('Column mappings should be available', () => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
import { Section, Select, ColorPicker } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
import ColorPalette from '@/visualizations/ColorPalette';
|
||||
|
||||
@@ -16,10 +14,9 @@ const ColorSchemes = [
|
||||
export default function HeatmapColorsSettings({ options, onOptionsChange }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="chart-editor-colors-heatmap-scheme">Color Scheme</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="chart-editor-colors-heatmap-scheme"
|
||||
label="Color Scheme"
|
||||
className="w-100"
|
||||
data-test="Chart.Colors.Heatmap.ColorScheme"
|
||||
placeholder="Choose Color Scheme..."
|
||||
@@ -33,35 +30,37 @@ export default function HeatmapColorsSettings({ options, onOptionsChange }) {
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{(options.colorScheme === 'Custom...') && (
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label className="m-r-10" htmlFor="chart-editor-colors-heatmap-min-color">Min Color:</label>
|
||||
<React.Fragment>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Min Color:"
|
||||
data-test="Chart.Colors.Heatmap.MinColor"
|
||||
id="chart-editor-colors-heatmap-min-color"
|
||||
interactive
|
||||
placement="topLeft"
|
||||
presetColors={ColorPalette}
|
||||
color={options.heatMinColor}
|
||||
onChange={heatMinColor => onOptionsChange({ heatMinColor })}
|
||||
addonAfter={<ColorPicker.Label color={options.heatMinColor} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<label className="m-r-10" htmlFor="chart-editor-colors-heatmap-max-color">Max Color:</label>
|
||||
</Section>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Max Color:"
|
||||
data-test="Chart.Colors.Heatmap.MaxColor"
|
||||
id="chart-editor-colors-heatmap-max-color"
|
||||
interactive
|
||||
placement="topRight"
|
||||
presetColors={ColorPalette}
|
||||
color={options.heatMaxColor}
|
||||
onChange={heatMaxColor => onOptionsChange({ heatMaxColor })}
|
||||
addonAfter={<ColorPicker.Label color={options.heatMaxColor} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
@@ -46,18 +46,17 @@ export default function PieColorsSettings({ options, data, onOptionsChange }) {
|
||||
title: 'Color',
|
||||
dataIndex: 'color',
|
||||
width: '1%',
|
||||
className: 'text-nowrap',
|
||||
render: (unused, item) => (
|
||||
<div className="text-nowrap">
|
||||
<ColorPicker
|
||||
data-test={`Chart.Series.${item.key}.Color`}
|
||||
interactive
|
||||
presetColors={colors}
|
||||
placement="topRight"
|
||||
color={item.color}
|
||||
onChange={value => updateValuesOption(item.key, 'color', value)}
|
||||
/>
|
||||
<ColorPicker.Label color={item.color} presetColors={colors} />
|
||||
</div>
|
||||
<ColorPicker
|
||||
data-test={`Chart.Series.${item.key}.Color`}
|
||||
interactive
|
||||
presetColors={colors}
|
||||
placement="topRight"
|
||||
color={item.color}
|
||||
onChange={value => updateValuesOption(item.key, 'color', value)}
|
||||
addonAfter={<ColorPicker.Label color={item.color} presetColors={colors} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -33,8 +33,8 @@ describe('Visualizations -> Chart -> Editor -> Series Settings', () => {
|
||||
},
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Series.a.Type').first().simulate('click');
|
||||
findByTestID(el, 'Chart.ChartType.area').first().simulate('click');
|
||||
findByTestID(el, 'Chart.Series.a.Type').last().simulate('click');
|
||||
findByTestID(el, 'Chart.ChartType.area').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Changes series label', (done) => {
|
||||
@@ -46,7 +46,7 @@ describe('Visualizations -> Chart -> Editor -> Series Settings', () => {
|
||||
},
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Series.a.Label').first().simulate('change', { target: { value: 'test' } });
|
||||
findByTestID(el, 'Chart.Series.a.Label').last().simulate('change', { target: { value: 'test' } });
|
||||
});
|
||||
|
||||
test('Changes series axis', (done) => {
|
||||
@@ -58,7 +58,7 @@ describe('Visualizations -> Chart -> Editor -> Series Settings', () => {
|
||||
},
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.Series.a.UseRightAxis').first().find('input')
|
||||
findByTestID(el, 'Chart.Series.a.UseRightAxis').last().find('input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Switch from 'antd/lib/switch';
|
||||
import { Section, Switch } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import AxisSettings from './AxisSettings';
|
||||
@@ -14,41 +14,35 @@ export default function XAxisSettings({ options, onOptionsChange }) {
|
||||
onChange={xAxis => onOptionsChange({ xAxis })}
|
||||
/>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-x-axis-sort">
|
||||
<Switch
|
||||
id="chart-editor-x-axis-sort"
|
||||
data-test="Chart.XAxis.Sort"
|
||||
defaultChecked={options.sortX}
|
||||
onChange={sortX => onOptionsChange({ sortX })}
|
||||
/>
|
||||
<span className="m-l-10">Sort Values</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
data-test="Chart.XAxis.Sort"
|
||||
defaultChecked={options.sortX}
|
||||
onChange={sortX => onOptionsChange({ sortX })}
|
||||
>
|
||||
Sort Values
|
||||
</Switch>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-x-axis-reverse">
|
||||
<Switch
|
||||
id="chart-editor-x-axis-reverse"
|
||||
data-test="Chart.XAxis.Reverse"
|
||||
defaultChecked={options.reverseX}
|
||||
onChange={reverseX => onOptionsChange({ reverseX })}
|
||||
/>
|
||||
<span className="m-l-10">Reverse Order</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
data-test="Chart.XAxis.Reverse"
|
||||
defaultChecked={options.reverseX}
|
||||
onChange={reverseX => onOptionsChange({ reverseX })}
|
||||
>
|
||||
Reverse Order
|
||||
</Switch>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-x-axis-show-labels">
|
||||
<Switch
|
||||
id="chart-editor-x-axis-show-labels"
|
||||
data-test="Chart.XAxis.ShowLabels"
|
||||
defaultChecked={options.xAxis.labels.enabled}
|
||||
onChange={enabled => onOptionsChange({ xAxis: { labels: { enabled } } })}
|
||||
/>
|
||||
<span className="m-l-10">Show Labels</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
data-test="Chart.XAxis.ShowLabels"
|
||||
defaultChecked={options.xAxis.labels.enabled}
|
||||
onChange={enabled => onOptionsChange({ xAxis: { labels: { enabled } } })}
|
||||
>
|
||||
Show Labels
|
||||
</Switch>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
|
||||
xAxis: { type: '-', labels: { enabled: true } },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.XAxis.Type').first().simulate('click');
|
||||
findByTestID(el, 'Chart.XAxis.Type.Linear').first().simulate('click');
|
||||
findByTestID(el, 'Chart.XAxis.Type').last().simulate('click');
|
||||
findByTestID(el, 'Chart.XAxis.Type.Linear').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Changes axis name', (done) => {
|
||||
@@ -40,7 +40,7 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
|
||||
xAxis: { type: '-', labels: { enabled: true } },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.XAxis.Name').first().simulate('change', { target: { value: 'test' } });
|
||||
findByTestID(el, 'Chart.XAxis.Name').last().simulate('change', { target: { value: 'test' } });
|
||||
});
|
||||
|
||||
test('Sets Show Labels option', (done) => {
|
||||
@@ -49,7 +49,7 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
|
||||
xAxis: { type: '-', labels: { enabled: false } },
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.XAxis.ShowLabels').first().simulate('click');
|
||||
findByTestID(el, 'Chart.XAxis.ShowLabels').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Sets Sort X Values option', (done) => {
|
||||
@@ -58,7 +58,7 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
|
||||
sortX: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.XAxis.Sort').first().simulate('click');
|
||||
findByTestID(el, 'Chart.XAxis.Sort').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Sets Reverse X Values option', (done) => {
|
||||
@@ -67,6 +67,6 @@ describe('Visualizations -> Chart -> Editor -> X-Axis Settings', () => {
|
||||
reverseX: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.XAxis.Reverse').first().simulate('click');
|
||||
findByTestID(el, 'Chart.XAxis.Reverse').last().simulate('click');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import Switch from 'antd/lib/switch';
|
||||
import { Section, Switch } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import AxisSettings from './AxisSettings';
|
||||
@@ -9,53 +9,55 @@ export default function YAxisSettings({ options, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<h4>Left Y Axis</h4>
|
||||
<Section.Title>Left Y Axis</Section.Title>
|
||||
|
||||
<Section>
|
||||
<AxisSettings
|
||||
id="LeftYAxis"
|
||||
features={{ range: true }}
|
||||
options={leftYAxis}
|
||||
onChange={axis => onOptionsChange({ yAxis: [axis, rightYAxis] })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{(options.globalSeriesType !== 'heatmap') && (
|
||||
<div className="m-b-15">
|
||||
<h4>Right Y Axis</h4>
|
||||
<AxisSettings
|
||||
id="RightYAxis"
|
||||
features={{ range: true }}
|
||||
options={rightYAxis}
|
||||
onChange={axis => onOptionsChange({ yAxis: [leftYAxis, axis] })}
|
||||
/>
|
||||
</div>
|
||||
<React.Fragment>
|
||||
<Section.Title>Right Y Axis</Section.Title>
|
||||
|
||||
<Section>
|
||||
<AxisSettings
|
||||
id="RightYAxis"
|
||||
features={{ range: true }}
|
||||
options={rightYAxis}
|
||||
onChange={axis => onOptionsChange({ yAxis: [leftYAxis, axis] })}
|
||||
/>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
{(options.globalSeriesType === 'heatmap') && (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-y-axis-sort">
|
||||
<Switch
|
||||
id="chart-editor-y-axis-sort"
|
||||
data-test="Chart.LeftYAxis.Sort"
|
||||
defaultChecked={options.sortY}
|
||||
onChange={sortY => onOptionsChange({ sortY })}
|
||||
/>
|
||||
<span className="m-l-10">Sort Values</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
id="chart-editor-y-axis-sort"
|
||||
data-test="Chart.LeftYAxis.Sort"
|
||||
defaultChecked={options.sortY}
|
||||
onChange={sortY => onOptionsChange({ sortY })}
|
||||
>
|
||||
Sort Values
|
||||
</Switch>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="d-flex align-items-center" htmlFor="chart-editor-y-axis-reverse">
|
||||
<Switch
|
||||
id="chart-editor-y-axis-reverse"
|
||||
data-test="Chart.LeftYAxis.Reverse"
|
||||
defaultChecked={options.reverseY}
|
||||
onChange={reverseY => onOptionsChange({ reverseY })}
|
||||
/>
|
||||
<span className="m-l-10">Reverse Order</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Switch
|
||||
id="chart-editor-y-axis-reverse"
|
||||
data-test="Chart.LeftYAxis.Reverse"
|
||||
defaultChecked={options.reverseY}
|
||||
onChange={reverseY => onOptionsChange({ reverseY })}
|
||||
>
|
||||
Reverse Order
|
||||
</Switch>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
@@ -34,8 +34,8 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
|
||||
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.LeftYAxis.Type').first().simulate('click');
|
||||
findByTestID(el, 'Chart.LeftYAxis.Type.Category').first().simulate('click');
|
||||
findByTestID(el, 'Chart.LeftYAxis.Type').last().simulate('click');
|
||||
findByTestID(el, 'Chart.LeftYAxis.Type.Category').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Changes axis name', (done) => {
|
||||
@@ -44,7 +44,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
|
||||
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.LeftYAxis.Name').first().simulate('change', { target: { value: 'test' } });
|
||||
findByTestID(el, 'Chart.LeftYAxis.Name').last().simulate('change', { target: { value: 'test' } });
|
||||
});
|
||||
|
||||
test('Changes axis min value', (done) => {
|
||||
@@ -53,7 +53,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
|
||||
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.LeftYAxis.RangeMin').find('input').first().simulate('change', { target: { value: '50' } });
|
||||
findByTestID(el, 'Chart.LeftYAxis.RangeMin').find('input').last().simulate('change', { target: { value: '50' } });
|
||||
});
|
||||
|
||||
test('Changes axis max value', (done) => {
|
||||
@@ -62,7 +62,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
|
||||
yAxis: [{ type: 'linear' }, { type: 'linear', opposite: true }],
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.LeftYAxis.RangeMax').find('input').first().simulate('change', { target: { value: '200' } });
|
||||
findByTestID(el, 'Chart.LeftYAxis.RangeMax').find('input').last().simulate('change', { target: { value: '200' } });
|
||||
});
|
||||
|
||||
describe('for non-heatmap', () => {
|
||||
@@ -92,7 +92,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
|
||||
sortY: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.LeftYAxis.Sort').first().simulate('click');
|
||||
findByTestID(el, 'Chart.LeftYAxis.Sort').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Sets Reverse Y Values option', (done) => {
|
||||
@@ -101,7 +101,7 @@ describe('Visualizations -> Chart -> Editor -> Y-Axis Settings', () => {
|
||||
reverseY: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Chart.LeftYAxis.Reverse').first().simulate('click');
|
||||
findByTestID(el, 'Chart.LeftYAxis.Reverse').last().simulate('click');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ Object {
|
||||
|
||||
exports[`Visualizations -> Chart -> Editor -> Colors Settings for heatmap Sets custom color scheme 2`] = `
|
||||
Object {
|
||||
"heatMinColor": "#FF0000",
|
||||
"heatMaxColor": "#FF0000",
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { merge, extend } from 'lodash';
|
||||
/* eslint-disable react/prop-types */
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import XAxisSettings from './XAxisSettings';
|
||||
@@ -13,53 +12,48 @@ import CustomChartSettings from './CustomChartSettings';
|
||||
|
||||
import './editor.less';
|
||||
|
||||
export default function Editor(props) {
|
||||
const { options, onOptionsChange } = props;
|
||||
const isCustomChart = options => options.globalSeriesType === 'custom';
|
||||
const isPieChart = options => options.globalSeriesType === 'pie';
|
||||
|
||||
const optionsChanged = (newOptions, deepUpdate = true) => {
|
||||
if (deepUpdate) {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
} else {
|
||||
onOptionsChange(extend({}, options, newOptions));
|
||||
}
|
||||
};
|
||||
|
||||
const isCustomChart = options.globalSeriesType === 'custom';
|
||||
const isPieChart = options.globalSeriesType === 'pie';
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="general" tab={<span data-test="Chart.EditorTabs.General">General</span>}>
|
||||
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
|
||||
{isCustomChart && <CustomChartSettings {...props} onOptionsChange={optionsChanged} />}
|
||||
</Tabs.TabPane>
|
||||
{!isCustomChart && !isPieChart && (
|
||||
<Tabs.TabPane key="x-axis" tab={<span data-test="Chart.EditorTabs.XAxis">X Axis</span>}>
|
||||
<XAxisSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
{!isCustomChart && !isPieChart && (
|
||||
<Tabs.TabPane key="y-axis" tab={<span data-test="Chart.EditorTabs.YAxis">Y Axis</span>}>
|
||||
<YAxisSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
{!isCustomChart && (
|
||||
<Tabs.TabPane key="series" tab={<span data-test="Chart.EditorTabs.Series">Series</span>}>
|
||||
<SeriesSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
{!isCustomChart && (
|
||||
<Tabs.TabPane key="colors" tab={<span data-test="Chart.EditorTabs.Colors">Colors</span>}>
|
||||
<ColorsSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
{!isCustomChart && (
|
||||
<Tabs.TabPane key="data labels" tab={<span data-test="Chart.EditorTabs.DataLabels">Data Labels</span>}>
|
||||
<DataLabelsSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
)}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = EditorPropTypes;
|
||||
export default createTabbedEditor([
|
||||
{
|
||||
key: 'General',
|
||||
title: 'General',
|
||||
component: props => (
|
||||
<React.Fragment>
|
||||
<GeneralSettings {...props} />
|
||||
{isCustomChart(props.options) && <CustomChartSettings {...props} />}
|
||||
</React.Fragment>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'XAxis',
|
||||
title: 'X Axis',
|
||||
component: XAxisSettings,
|
||||
isAvailable: options => !isCustomChart(options) && !isPieChart(options),
|
||||
},
|
||||
{
|
||||
key: 'YAxis',
|
||||
title: 'Y Axis',
|
||||
component: YAxisSettings,
|
||||
isAvailable: options => !isCustomChart(options) && !isPieChart(options),
|
||||
},
|
||||
{
|
||||
key: 'Series',
|
||||
title: 'Series',
|
||||
component: SeriesSettings,
|
||||
isAvailable: options => !isCustomChart(options),
|
||||
},
|
||||
{
|
||||
key: 'Colors',
|
||||
title: 'Colors',
|
||||
component: ColorsSettings,
|
||||
isAvailable: options => !isCustomChart(options),
|
||||
},
|
||||
{
|
||||
key: 'DataLabels',
|
||||
title: 'Data Labels',
|
||||
component: DataLabelsSettings,
|
||||
isAvailable: options => !isCustomChart(options),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -28,12 +28,12 @@ describe('Visualizations -> Chart -> Editor (wrapper)', () => {
|
||||
test('Renders generic wrapper', () => {
|
||||
const el = mount({ globalSeriesType: 'column' }, { columns: [], rows: [] });
|
||||
|
||||
expect(elementExists(el, 'Chart.EditorTabs.General')).toBeTruthy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.XAxis')).toBeTruthy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.YAxis')).toBeTruthy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.Series')).toBeTruthy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.Colors')).toBeTruthy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.DataLabels')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.General')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.XAxis')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.YAxis')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.Series')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.Colors')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.DataLabels')).toBeTruthy();
|
||||
|
||||
expect(elementExists(el, 'Chart.GlobalSeriesType')).toBeTruthy(); // general settings block exists
|
||||
expect(elementExists(el, 'Chart.Custom.Code')).toBeFalsy(); // custom settings block does not exist
|
||||
@@ -42,12 +42,12 @@ describe('Visualizations -> Chart -> Editor (wrapper)', () => {
|
||||
test('Renders wrapper for custom charts', () => {
|
||||
const el = mount({ globalSeriesType: 'custom' }, { columns: [], rows: [] });
|
||||
|
||||
expect(elementExists(el, 'Chart.EditorTabs.General')).toBeTruthy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.XAxis')).toBeFalsy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.YAxis')).toBeFalsy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.Series')).toBeFalsy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.Colors')).toBeFalsy();
|
||||
expect(elementExists(el, 'Chart.EditorTabs.DataLabels')).toBeFalsy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.General')).toBeTruthy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.XAxis')).toBeFalsy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.YAxis')).toBeFalsy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.Series')).toBeFalsy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.Colors')).toBeFalsy();
|
||||
expect(elementExists(el, 'VisualizationEditor.Tabs.DataLabels')).toBeFalsy();
|
||||
|
||||
expect(elementExists(el, 'Chart.GlobalSeriesType')).toBeTruthy(); // general settings block exists
|
||||
expect(elementExists(el, 'Chart.Custom.Code')).toBeTruthy(); // custom settings block exists
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"textposition": "inside",
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||
"textposition": "inside",
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -73,7 +73,7 @@
|
||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||
"textposition": "inside",
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"textposition": "inside",
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -73,7 +73,7 @@
|
||||
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
|
||||
"textposition": "inside",
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"error_y": { "array": [0, 0, 0, 0], "color": "red" },
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"hoverinfo": "text+x+name",
|
||||
"hover": [],
|
||||
"text": ["10 ± 0: 51", "20 ± 0: 52", "30 ± 0: 53", "40 ± 0: 54"],
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -69,7 +69,7 @@
|
||||
"hover": [],
|
||||
"text": ["", "2 ± 0", "", "4 ± 0"],
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -69,7 +69,7 @@
|
||||
"hover": [],
|
||||
"text": ["0", "2 ± 0", "0", "4 ± 0"],
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"hover": [],
|
||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -71,7 +71,7 @@
|
||||
"hover": [],
|
||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"hover": [],
|
||||
"text": ["20% (10 ± 0)", "40% (20 ± 0)", "60% (30 ± 0)", "80% (40 ± 0)"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -71,7 +71,7 @@
|
||||
"hover": [],
|
||||
"text": ["80% (40 ± 0)", "60% (30 ± 0)", "40% (20 ± 0)", "20% (10 ± 0)"],
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
},
|
||||
{
|
||||
@@ -71,7 +71,7 @@
|
||||
"hover": [],
|
||||
"text": ["1 ± 0", "2 ± 0", "3 ± 0", "4 ± 0"],
|
||||
"marker": { "color": "blue" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"hover": [],
|
||||
"text": ["10 ± 0", "20 ± 0", "30 ± 0", "40 ± 0"],
|
||||
"marker": { "color": "red" },
|
||||
"insidetextfont": { "color": "#333333" },
|
||||
"insidetextfont": { "color": "#ffffff" },
|
||||
"yaxis": "y"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,42 +1,12 @@
|
||||
import { isNil, isString, extend, each, includes, map, sortBy } from 'lodash';
|
||||
import { cleanNumber, normalizeValue, getSeriesAxis } from './utils';
|
||||
import { isNil, extend, each, includes, map, sortBy } from 'lodash';
|
||||
import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
|
||||
import { ColorPaletteArray } from '@/visualizations/ColorPalette';
|
||||
import { cleanNumber, normalizeValue, getSeriesAxis } from './utils';
|
||||
|
||||
function getSeriesColor(seriesOptions, seriesIndex) {
|
||||
return seriesOptions.color || ColorPaletteArray[seriesIndex % ColorPaletteArray.length];
|
||||
}
|
||||
|
||||
function getFontColor(backgroundColor) {
|
||||
let result = '#333333';
|
||||
if (isString(backgroundColor)) {
|
||||
let matches = /#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i.exec(backgroundColor);
|
||||
let r;
|
||||
let g;
|
||||
let b;
|
||||
if (matches) {
|
||||
r = parseInt(matches[1], 16);
|
||||
g = parseInt(matches[2], 16);
|
||||
b = parseInt(matches[3], 16);
|
||||
} else {
|
||||
matches = /#?([0-9a-f])([0-9a-f])([0-9a-f])/i.exec(backgroundColor);
|
||||
if (matches) {
|
||||
r = parseInt(matches[1] + matches[1], 16);
|
||||
g = parseInt(matches[2] + matches[2], 16);
|
||||
b = parseInt(matches[3] + matches[3], 16);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const lightness = r * 0.299 + g * 0.587 + b * 0.114;
|
||||
if (lightness < 170) {
|
||||
result = '#ffffff';
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getHoverInfoPattern(options) {
|
||||
const hasX = /{{\s*@@x\s*}}/.test(options.textFormat);
|
||||
const hasName = /{{\s*@@name\s*}}/.test(options.textFormat);
|
||||
@@ -152,7 +122,7 @@ function prepareSeries(series, options, additionalOptions) {
|
||||
name: seriesOptions.name || series.name,
|
||||
marker: { color: seriesColor },
|
||||
insidetextfont: {
|
||||
color: getFontColor(seriesColor),
|
||||
color: chooseTextColorForBackground(seriesColor),
|
||||
},
|
||||
yaxis: seriesYAxis,
|
||||
sourceData,
|
||||
|
||||
@@ -64,7 +64,11 @@ function prepareSeries(series, options, additionalOptions) {
|
||||
text: [],
|
||||
textinfo: options.showDataLabels ? 'percent' : 'none',
|
||||
textposition: 'inside',
|
||||
textfont: { color: '#ffffff' },
|
||||
textfont: {
|
||||
// In Plotly@1.42.0 and upper this options can be set to array of colors (similar to `marker.colors`):
|
||||
// `colors: map(markerColors, c => chooseTextColorForBackground(c))`
|
||||
color: '#ffffff',
|
||||
},
|
||||
name: series.name,
|
||||
direction: options.direction.type,
|
||||
domain: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isFinite, cloneDeep } from 'lodash';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import { Section, InputNumber, ControlLabel } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function BoundsSettings({ options, onOptionsChange }) {
|
||||
@@ -32,47 +32,47 @@ export default function BoundsSettings({ options, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-bounds-ne">North-East latitude and longitude</label>
|
||||
<Grid.Row gutter={15}>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="choropleth-editor-bounds-ne"
|
||||
className="w-100"
|
||||
value={bounds[1][0]}
|
||||
onChange={value => updateBounds(1, 0, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
className="w-100"
|
||||
value={bounds[1][1]}
|
||||
onChange={value => updateBounds(1, 1, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</div>
|
||||
<Section>
|
||||
<ControlLabel label="North-East latitude and longitude">
|
||||
<Grid.Row gutter={15}>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
className="w-100"
|
||||
value={bounds[1][0]}
|
||||
onChange={value => updateBounds(1, 0, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
className="w-100"
|
||||
value={bounds[1][1]}
|
||||
onChange={value => updateBounds(1, 1, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</ControlLabel>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-bounds-sw">South-West latitude and longitude</label>
|
||||
<Grid.Row gutter={15}>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="choropleth-editor-bounds-sw"
|
||||
className="w-100"
|
||||
value={bounds[0][0]}
|
||||
onChange={value => updateBounds(0, 0, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
className="w-100"
|
||||
value={bounds[0][1]}
|
||||
onChange={value => updateBounds(0, 1, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</div>
|
||||
<Section>
|
||||
<ControlLabel label="South-West latitude and longitude">
|
||||
<Grid.Row gutter={15}>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
className="w-100"
|
||||
value={bounds[0][0]}
|
||||
onChange={value => updateBounds(0, 0, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
className="w-100"
|
||||
value={bounds[0][1]}
|
||||
onChange={value => updateBounds(0, 1, value)}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</ControlLabel>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Select from 'antd/lib/select';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
import { Section, Select, InputNumber, ColorPicker } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
import ColorPalette from '../ColorPalette';
|
||||
|
||||
@@ -12,131 +9,103 @@ export default function ColorsSettings({ options, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-clustering-mode">Clustering mode</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="choropleth-editor-clustering-mode"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ClusteringMode"
|
||||
defaultValue={options.clusteringMode}
|
||||
onChange={clusteringMode => onOptionsChange({ clusteringMode })}
|
||||
>
|
||||
<Select.Option value="q" data-test="Choropleth.Editor.ClusteringMode.q">quantile</Select.Option>
|
||||
<Select.Option value="e" data-test="Choropleth.Editor.ClusteringMode.e">equidistant</Select.Option>
|
||||
<Select.Option value="k" data-test="Choropleth.Editor.ClusteringMode.k">k-means</Select.Option>
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Clustering mode"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ClusteringMode"
|
||||
defaultValue={options.clusteringMode}
|
||||
onChange={clusteringMode => onOptionsChange({ clusteringMode })}
|
||||
>
|
||||
<Select.Option value="q" data-test="Choropleth.Editor.ClusteringMode.q">quantile</Select.Option>
|
||||
<Select.Option value="e" data-test="Choropleth.Editor.ClusteringMode.e">equidistant</Select.Option>
|
||||
<Select.Option value="k" data-test="Choropleth.Editor.ClusteringMode.k">k-means</Select.Option>
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-color-steps">Steps</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="choropleth-editor-color-steps"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ColorSteps"
|
||||
min={3}
|
||||
max={11}
|
||||
defaultValue={options.steps}
|
||||
onChange={steps => onOptionsChangeDebounced({ steps })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Steps"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ColorSteps"
|
||||
min={3}
|
||||
max={11}
|
||||
defaultValue={options.steps}
|
||||
onChange={steps => onOptionsChangeDebounced({ steps })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-color-min">Min Color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="choropleth-editor-color-min"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.min}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Min' }}
|
||||
onChange={min => onOptionsChange({ colors: { min } })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.colors.min} presetColors={ColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Min Color"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.min}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Min' }}
|
||||
onChange={min => onOptionsChange({ colors: { min } })}
|
||||
addonAfter={<ColorPicker.Label color={options.colors.min} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-color-max">Max Color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="choropleth-editor-color-max"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.max}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Max' }}
|
||||
onChange={max => onOptionsChange({ colors: { max } })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.colors.max} presetColors={ColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Max Color"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.max}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Max' }}
|
||||
onChange={max => onOptionsChange({ colors: { max } })}
|
||||
addonAfter={<ColorPicker.Label color={options.colors.max} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-color-no-value">No value color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="choropleth-editor-color-no-value"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.noValue}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.NoValue' }}
|
||||
onChange={noValue => onOptionsChange({ colors: { noValue } })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.colors.noValue} presetColors={ColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="No value color"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.noValue}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.NoValue' }}
|
||||
onChange={noValue => onOptionsChange({ colors: { noValue } })}
|
||||
addonAfter={<ColorPicker.Label color={options.colors.noValue} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-color-background">Background color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="choropleth-editor-color-background"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.background}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Background' }}
|
||||
onChange={background => onOptionsChange({ colors: { background } })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.colors.background} presetColors={ColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Background color"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.background}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Background' }}
|
||||
onChange={background => onOptionsChange({ colors: { background } })}
|
||||
addonAfter={<ColorPicker.Label color={options.colors.background} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-color-borders">Borders color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="choropleth-editor-color-borders"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.borders}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Borders' }}
|
||||
onChange={borders => onOptionsChange({ colors: { borders } })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.colors.borders} presetColors={ColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Borders color"
|
||||
interactive
|
||||
presetColors={ColorPalette}
|
||||
placement="topRight"
|
||||
color={options.colors.borders}
|
||||
triggerProps={{ 'data-test': 'Choropleth.Editor.Colors.Borders' }}
|
||||
onChange={borders => onOptionsChange({ colors: { borders } })}
|
||||
addonAfter={<ColorPicker.Label color={options.colors.borders} presetColors={ColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,47 +1,33 @@
|
||||
import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Select from 'antd/lib/select';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import { Section, Select, Input, Checkbox, TextArea, TextAlignmentSelect, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
function TemplateFormatHint({ mapType }) { // eslint-disable-line react/prop-types
|
||||
return (
|
||||
<Popover
|
||||
placement="topLeft"
|
||||
arrowPointAtCenter
|
||||
content={(
|
||||
<ContextHelp placement="topLeft" arrowPointAtCenter>
|
||||
<div className="p-b-5">All query result columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
|
||||
<div className="p-b-5">Use special names to access additional properties:</div>
|
||||
<div><code>{'{{ @@value }}'}</code> formatted value;</div>
|
||||
{mapType === 'countries' && (
|
||||
<React.Fragment>
|
||||
<div className="p-b-5">All query result columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
|
||||
<div className="p-b-5">Use special names to access additional properties:</div>
|
||||
<div><code>{'{{ @@value }}'}</code> formatted value;</div>
|
||||
{mapType === 'countries' && (
|
||||
<React.Fragment>
|
||||
<div><code>{'{{ @@name }}'}</code> short country name;</div>
|
||||
<div><code>{'{{ @@name_long }}'}</code> full country name;</div>
|
||||
<div><code>{'{{ @@abbrev }}'}</code> abbreviated country name;</div>
|
||||
<div><code>{'{{ @@iso_a2 }}'}</code> two-letter ISO country code;</div>
|
||||
<div><code>{'{{ @@iso_a3 }}'}</code> three-letter ISO country code;</div>
|
||||
<div><code>{'{{ @@iso_n3 }}'}</code> three-digit ISO country code.</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{mapType === 'subdiv_japan' && (
|
||||
<React.Fragment>
|
||||
<div><code>{'{{ @@name }}'}</code> Prefecture name in English;</div>
|
||||
<div><code>{'{{ @@name_local }}'}</code> Prefecture name in Kanji;</div>
|
||||
<div><code>{'{{ @@iso_3166_2 }}'}</code> five-letter ISO subdivision code (JP-xx);</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<div><code>{'{{ @@name }}'}</code> short country name;</div>
|
||||
<div><code>{'{{ @@name_long }}'}</code> full country name;</div>
|
||||
<div><code>{'{{ @@abbrev }}'}</code> abbreviated country name;</div>
|
||||
<div><code>{'{{ @@iso_a2 }}'}</code> two-letter ISO country code;</div>
|
||||
<div><code>{'{{ @@iso_a3 }}'}</code> three-letter ISO country code;</div>
|
||||
<div><code>{'{{ @@iso_n3 }}'}</code> three-digit ISO country code.</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
{mapType === 'subdiv_japan' && (
|
||||
<React.Fragment>
|
||||
<div><code>{'{{ @@name }}'}</code> Prefecture name in English;</div>
|
||||
<div><code>{'{{ @@name_local }}'}</code> Prefecture name in Kanji;</div>
|
||||
<div><code>{'{{ @@iso_3166_2 }}'}</code> five-letter ISO subdivision code (JP-xx);</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ContextHelp>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,139 +38,102 @@ export default function GeneralSettings({ options, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<div className="choropleth-visualization-editor-format-settings">
|
||||
<Grid.Row gutter={15} className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-value-format">
|
||||
Value format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Section>
|
||||
<Grid.Row gutter={15}>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
label={(<React.Fragment>Value format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ValueFormat"
|
||||
defaultValue={options.valueFormat}
|
||||
onChange={event => onOptionsChangeDebounced({ valueFormat: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
label="Value placeholder"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ValuePlaceholder"
|
||||
defaultValue={options.noValuePlaceholder}
|
||||
onChange={event => onOptionsChangeDebounced({ noValuePlaceholder: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Choropleth.Editor.LegendVisibility"
|
||||
checked={options.legend.visible}
|
||||
onChange={event => onOptionsChange({ legend: { visible: event.target.checked } })}
|
||||
>
|
||||
Show legend
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Grid.Row gutter={15}>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
label="Legend position"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.LegendPosition"
|
||||
disabled={!options.legend.visible}
|
||||
defaultValue={options.legend.position}
|
||||
onChange={position => onOptionsChange({ legend: { position } })}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Input
|
||||
id="choropleth-editor-value-format"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ValueFormat"
|
||||
defaultValue={options.valueFormat}
|
||||
onChange={event => onOptionsChangeDebounced({ valueFormat: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-value-placeholder">Value placeholder</label>
|
||||
<Input
|
||||
id="choropleth-editor-value-placeholder"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ValuePlaceholder"
|
||||
defaultValue={options.noValuePlaceholder}
|
||||
onChange={event => onOptionsChangeDebounced({ noValuePlaceholder: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Select.Option value="top-left" data-test="Choropleth.Editor.LegendPosition.TopLeft">top / left</Select.Option>
|
||||
<Select.Option value="top-right" data-test="Choropleth.Editor.LegendPosition.TopRight">top / right</Select.Option>
|
||||
<Select.Option value="bottom-left" data-test="Choropleth.Editor.LegendPosition.BottomLeft">bottom / left</Select.Option>
|
||||
<Select.Option value="bottom-right" data-test="Choropleth.Editor.LegendPosition.BottomRight">bottom / right</Select.Option>
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<TextAlignmentSelect
|
||||
data-test="Choropleth.Editor.LegendTextAlignment"
|
||||
label="Legend text alignment"
|
||||
disabled={!options.legend.visible}
|
||||
defaultValue={options.legend.alignText}
|
||||
onChange={event => onOptionsChange({ legend: { alignText: event.target.value } })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-5">
|
||||
<label htmlFor="choropleth-editor-show-legend">
|
||||
<Checkbox
|
||||
id="choropleth-editor-show-legend"
|
||||
data-test="Choropleth.Editor.LegendVisibility"
|
||||
checked={options.legend.visible}
|
||||
onChange={event => onOptionsChange({ legend: { visible: event.target.checked } })}
|
||||
/>
|
||||
<span>Show legend</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Choropleth.Editor.TooltipEnabled"
|
||||
checked={options.tooltip.enabled}
|
||||
onChange={event => onOptionsChange({ tooltip: { enabled: event.target.checked } })}
|
||||
>
|
||||
Show tooltip
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
<Grid.Row gutter={15} className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-legend-position">Legend position</label>
|
||||
<Select
|
||||
id="choropleth-editor-legend-position"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.LegendPosition"
|
||||
disabled={!options.legend.visible}
|
||||
defaultValue={options.legend.position}
|
||||
onChange={position => onOptionsChange({ legend: { position } })}
|
||||
>
|
||||
<Select.Option value="top-left" data-test="Choropleth.Editor.LegendPosition.TopLeft">top / left</Select.Option>
|
||||
<Select.Option value="top-right" data-test="Choropleth.Editor.LegendPosition.TopRight">top / right</Select.Option>
|
||||
<Select.Option value="bottom-left" data-test="Choropleth.Editor.LegendPosition.BottomLeft">bottom / left</Select.Option>
|
||||
<Select.Option value="bottom-right" data-test="Choropleth.Editor.LegendPosition.BottomRight">bottom / right</Select.Option>
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="choropleth-editor-legend-text-alignment">Legend text alignment</label>
|
||||
<Radio.Group
|
||||
id="choropleth-editor-legend-text-alignment"
|
||||
className="choropleth-visualization-editor-legend-align-text"
|
||||
data-test="Choropleth.Editor.LegendTextAlignment"
|
||||
disabled={!options.legend.visible}
|
||||
defaultValue={options.legend.alignText}
|
||||
onChange={event => onOptionsChange({ legend: { alignText: event.target.value } })}
|
||||
>
|
||||
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="left" data-test="Choropleth.Editor.LegendTextAlignment.Left">
|
||||
<Icon type="align-left" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="center" data-test="Choropleth.Editor.LegendTextAlignment.Center">
|
||||
<Icon type="align-center" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="right" data-test="Choropleth.Editor.LegendTextAlignment.Right">
|
||||
<Icon type="align-right" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
</Radio.Group>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
|
||||
<div className="m-b-5">
|
||||
<label htmlFor="choropleth-editor-show-tooltip">
|
||||
<Checkbox
|
||||
id="choropleth-editor-show-tooltip"
|
||||
data-test="Choropleth.Editor.TooltipEnabled"
|
||||
checked={options.tooltip.enabled}
|
||||
onChange={event => onOptionsChange({ tooltip: { enabled: event.target.checked } })}
|
||||
/>
|
||||
<span>Show tooltip</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-tooltip-template">Tooltip template {templateFormatHint}</label>
|
||||
<Section>
|
||||
<Input
|
||||
id="choropleth-editor-tooltip-template"
|
||||
label={<React.Fragment>Tooltip template {templateFormatHint}</React.Fragment>}
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.TooltipTemplate"
|
||||
disabled={!options.tooltip.enabled}
|
||||
defaultValue={options.tooltip.template}
|
||||
onChange={event => onOptionsChangeDebounced({ tooltip: { template: event.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-5">
|
||||
<label htmlFor="choropleth-editor-show-popup">
|
||||
<Checkbox
|
||||
id="choropleth-editor-show-popup"
|
||||
data-test="Choropleth.Editor.PopupEnabled"
|
||||
checked={options.popup.enabled}
|
||||
onChange={event => onOptionsChange({ popup: { enabled: event.target.checked } })}
|
||||
/>
|
||||
<span>Show popup</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Choropleth.Editor.PopupEnabled"
|
||||
checked={options.popup.enabled}
|
||||
onChange={event => onOptionsChange({ popup: { enabled: event.target.checked } })}
|
||||
>
|
||||
Show popup
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-popup-template">Popup template {templateFormatHint}</label>
|
||||
<Input.TextArea
|
||||
id="choropleth-editor-popup-template"
|
||||
<Section>
|
||||
<TextArea
|
||||
label={<React.Fragment>Popup template {templateFormatHint}</React.Fragment>}
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.PopupTemplate"
|
||||
disabled={!options.popup.enabled}
|
||||
@@ -192,7 +141,7 @@ export default function GeneralSettings({ options, onOptionsChange }) {
|
||||
defaultValue={options.popup.template}
|
||||
onChange={event => onOptionsChangeDebounced({ popup: { template: event.target.value } })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { map } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
import { Section, Select } from '@/components/visualizations/editor';
|
||||
import { inferCountryCodeType } from './utils';
|
||||
|
||||
export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
@@ -38,10 +38,9 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-map-type">Map type</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="choropleth-editor-map-type"
|
||||
label="Map type"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.MapType"
|
||||
defaultValue={options.mapType}
|
||||
@@ -50,12 +49,11 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key="countries" data-test="Choropleth.Editor.MapType.Countries">Countries</Select.Option>
|
||||
<Select.Option key="subdiv_japan" data-test="Choropleth.Editor.MapType.Japan">Japan/Prefectures</Select.Option>
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-key-column">Key column</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="choropleth-editor-key-column"
|
||||
label="Key column"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.KeyColumn"
|
||||
defaultValue={options.countryCodeColumn}
|
||||
@@ -65,12 +63,11 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key={name} data-test={`Choropleth.Editor.KeyColumn.${name}`}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-key-type">Key type</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="choropleth-editor-key-type"
|
||||
label="Key type"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.KeyType"
|
||||
value={options.countryCodeType}
|
||||
@@ -80,12 +77,11 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key={type} data-test={`Choropleth.Editor.KeyType.${type}`}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="choropleth-editor-value-column">Value column</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="choropleth-editor-value-column"
|
||||
label="Value column"
|
||||
className="w-100"
|
||||
data-test="Choropleth.Editor.ValueColumn"
|
||||
defaultValue={options.valueColumn}
|
||||
@@ -95,7 +91,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key={name} data-test={`Choropleth.Editor.ValueColumn.${name}`}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.choropleth-visualization-editor-format-settings {
|
||||
.choropleth-visualization-editor-legend-align-text {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
// fit <Input> height
|
||||
height: 35px;
|
||||
line-height: 33px;
|
||||
}
|
||||
}
|
||||
}
|
||||
13
client/app/visualizations/choropleth/Editor/index.js
Normal file
13
client/app/visualizations/choropleth/Editor/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import ColorsSettings from './ColorsSettings';
|
||||
import FormatSettings from './FormatSettings';
|
||||
import BoundsSettings from './BoundsSettings';
|
||||
|
||||
export default createTabbedEditor([
|
||||
{ key: 'General', title: 'General', component: GeneralSettings },
|
||||
{ key: 'Colors', title: 'Colors', component: ColorsSettings },
|
||||
{ key: 'Format', title: 'Format', component: FormatSettings },
|
||||
{ key: 'Bounds', title: 'Bounds', component: BoundsSettings },
|
||||
]);
|
||||
@@ -1,38 +0,0 @@
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import ColorsSettings from './ColorsSettings';
|
||||
import FormatSettings from './FormatSettings';
|
||||
import BoundsSettings from './BoundsSettings';
|
||||
|
||||
import './editor.less';
|
||||
|
||||
export default function Editor(props) {
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
const optionsChanged = (newOptions) => {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="general" tab={<span data-test="Choropleth.EditorTabs.General">General</span>}>
|
||||
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="colors" tab={<span data-test="Choropleth.EditorTabs.Colors">Colors</span>}>
|
||||
<ColorsSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="format" tab={<span data-test="Choropleth.EditorTabs.Format">Format</span>}>
|
||||
<FormatSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="bounds" tab={<span data-test="Choropleth.EditorTabs.Bounds">Bounds</span>}>
|
||||
<BoundsSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = EditorPropTypes;
|
||||
@@ -10,6 +10,7 @@ import React, { useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { createNumberFormatter, formatSimpleTemplate } from '@/lib/value-format';
|
||||
import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
|
||||
|
||||
import './cornelius.less';
|
||||
|
||||
@@ -68,11 +69,11 @@ function prepareOptions(options) {
|
||||
});
|
||||
}
|
||||
|
||||
function isDarkColor(backgroundColor) {
|
||||
function isLightColor(backgroundColor) {
|
||||
backgroundColor = chroma(backgroundColor);
|
||||
const white = '#ffffff';
|
||||
const black = '#000000';
|
||||
return chroma.contrast(backgroundColor, white) > chroma.contrast(backgroundColor, black);
|
||||
return chroma.contrast(backgroundColor, white) < chroma.contrast(backgroundColor, black);
|
||||
}
|
||||
|
||||
function formatStageTitle(options, index) {
|
||||
@@ -120,8 +121,11 @@ function CorneliusRow({ options, data, index, maxRowLength }) { // eslint-disabl
|
||||
options.formatPercent(percentageValue);
|
||||
|
||||
const backgroundColor = options.getColorForValue(percentageValue);
|
||||
cellProps.style = { backgroundColor };
|
||||
if (isDarkColor(backgroundColor)) {
|
||||
cellProps.style = {
|
||||
backgroundColor,
|
||||
color: chooseTextColorForBackground(backgroundColor),
|
||||
};
|
||||
if (isLightColor(cellProps.style.color)) {
|
||||
cellProps.className += ' cornelius-white-text';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { map, merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import Select from 'antd/lib/select';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
const CohortTimeIntervals = {
|
||||
daily: 'Daily',
|
||||
weekly: 'Weekly',
|
||||
monthly: 'Monthly',
|
||||
};
|
||||
|
||||
const CohortModes = {
|
||||
diagonal: 'Fill gaps with zeros',
|
||||
simple: 'Show data as is',
|
||||
};
|
||||
|
||||
export default function Editor({ options, data, onOptionsChange }) {
|
||||
const optionsChanged = (newOptions) => {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="columns" tab={<span data-test="Cohort.EditorTabs.Columns">Columns</span>}>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="cohort-date-column">Date (Bucket)</label>
|
||||
<Select
|
||||
data-test="Cohort.DateColumn"
|
||||
id="cohort-date-column"
|
||||
className="w-100"
|
||||
value={options.dateColumn}
|
||||
onChange={dateColumn => optionsChanged({ dateColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.DateColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="cohort-stage-column">Stage</label>
|
||||
<Select
|
||||
data-test="Cohort.StageColumn"
|
||||
id="cohort-stage-column"
|
||||
className="w-100"
|
||||
value={options.stageColumn}
|
||||
onChange={stageColumn => optionsChanged({ stageColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.StageColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="cohort-total-column">Bucket Population Size</label>
|
||||
<Select
|
||||
data-test="Cohort.TotalColumn"
|
||||
id="cohort-total-column"
|
||||
className="w-100"
|
||||
value={options.totalColumn}
|
||||
onChange={totalColumn => optionsChanged({ totalColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.TotalColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="cohort-value-column">Stage Value</label>
|
||||
<Select
|
||||
data-test="Cohort.ValueColumn"
|
||||
id="cohort-value-column"
|
||||
className="w-100"
|
||||
value={options.valueColumn}
|
||||
onChange={valueColumn => optionsChanged({ valueColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.ValueColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="options" tab={<span data-test="Cohort.EditorTabs.Options">Options</span>}>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="cohort-time-interval">Time Interval</label>
|
||||
<Select
|
||||
data-test="Cohort.TimeInterval"
|
||||
id="cohort-time-interval"
|
||||
className="w-100"
|
||||
value={options.timeInterval}
|
||||
onChange={timeInterval => optionsChanged({ timeInterval })}
|
||||
>
|
||||
{map(CohortTimeIntervals, (name, value) => (
|
||||
<Select.Option key={value} data-test={'Cohort.TimeInterval.' + value}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="cohort-time-mode">Mode</label>
|
||||
<Select
|
||||
data-test="Cohort.Mode"
|
||||
id="cohort-mode"
|
||||
className="w-100"
|
||||
value={options.mode}
|
||||
onChange={mode => optionsChanged({ mode })}
|
||||
>
|
||||
{map(CohortModes, (name, value) => (
|
||||
<Select.Option key={value} data-test={'Cohort.Mode.' + value}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = EditorPropTypes;
|
||||
68
client/app/visualizations/cohort/Editor/ColumnsSettings.jsx
Normal file
68
client/app/visualizations/cohort/Editor/ColumnsSettings.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Section, Select } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function ColumnsSettings({ options, data, onOptionsChange }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Section>
|
||||
<Select
|
||||
label="Date (Bucket)"
|
||||
data-test="Cohort.DateColumn"
|
||||
className="w-100"
|
||||
value={options.dateColumn}
|
||||
onChange={dateColumn => onOptionsChange({ dateColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.DateColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Select
|
||||
label="Stage"
|
||||
data-test="Cohort.StageColumn"
|
||||
className="w-100"
|
||||
value={options.stageColumn}
|
||||
onChange={stageColumn => onOptionsChange({ stageColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.StageColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Select
|
||||
label="Bucket Population Size"
|
||||
data-test="Cohort.TotalColumn"
|
||||
className="w-100"
|
||||
value={options.totalColumn}
|
||||
onChange={totalColumn => onOptionsChange({ totalColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.TotalColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Select
|
||||
label="Stage Value"
|
||||
data-test="Cohort.ValueColumn"
|
||||
className="w-100"
|
||||
value={options.valueColumn}
|
||||
onChange={valueColumn => onOptionsChange({ valueColumn })}
|
||||
>
|
||||
{map(data.columns, ({ name }) => (
|
||||
<Select.Option key={name} data-test={'Cohort.ValueColumn.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ColumnsSettings.propTypes = EditorPropTypes;
|
||||
51
client/app/visualizations/cohort/Editor/OptionsSettings.jsx
Normal file
51
client/app/visualizations/cohort/Editor/OptionsSettings.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import { Section, Select } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
const CohortTimeIntervals = {
|
||||
daily: 'Daily',
|
||||
weekly: 'Weekly',
|
||||
monthly: 'Monthly',
|
||||
};
|
||||
|
||||
const CohortModes = {
|
||||
diagonal: 'Fill gaps with zeros',
|
||||
simple: 'Show data as is',
|
||||
};
|
||||
|
||||
export default function OptionsSettings({ options, onOptionsChange }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Section>
|
||||
<Select
|
||||
label="Time Interval"
|
||||
data-test="Cohort.TimeInterval"
|
||||
className="w-100"
|
||||
value={options.timeInterval}
|
||||
onChange={timeInterval => onOptionsChange({ timeInterval })}
|
||||
>
|
||||
{map(CohortTimeIntervals, (name, value) => (
|
||||
<Select.Option key={value} data-test={'Cohort.TimeInterval.' + value}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<Select
|
||||
label="Mode"
|
||||
data-test="Cohort.Mode"
|
||||
className="w-100"
|
||||
value={options.mode}
|
||||
onChange={mode => onOptionsChange({ mode })}
|
||||
>
|
||||
{map(CohortModes, (name, value) => (
|
||||
<Select.Option key={value} data-test={'Cohort.Mode.' + value}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
OptionsSettings.propTypes = EditorPropTypes;
|
||||
9
client/app/visualizations/cohort/Editor/index.js
Normal file
9
client/app/visualizations/cohort/Editor/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import ColumnsSettings from './ColumnsSettings';
|
||||
import OptionsSettings from './OptionsSettings';
|
||||
|
||||
export default createTabbedEditor([
|
||||
{ key: 'Columns', title: 'Columns', component: ColumnsSettings },
|
||||
{ key: 'Options', title: 'Options', component: OptionsSettings },
|
||||
]);
|
||||
@@ -1,10 +1,13 @@
|
||||
.cornelius-container {
|
||||
// replace with variable from `ant-variables.less` after Bootstrap removed
|
||||
@table-header-color: #333;
|
||||
|
||||
.cornelius-title {
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 14pt;
|
||||
color: #3A3838;
|
||||
color: @table-header-color;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@@ -18,7 +21,7 @@
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border: 1px solid #E4E4E4;
|
||||
color: #3A3838;
|
||||
color: @table-header-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -37,15 +40,12 @@
|
||||
.cornelius-people,
|
||||
.cornelius-stage {
|
||||
font-weight: bold;
|
||||
color: #3A3838;
|
||||
color: @table-header-color;
|
||||
}
|
||||
|
||||
.cornelius-percentage,
|
||||
.cornelius-absolute {
|
||||
color: #000000;
|
||||
|
||||
&.cornelius-white-text {
|
||||
color: #ffffff;
|
||||
text-shadow: 1px 1px 1px #000000;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import React from 'react';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import Switch from 'antd/lib/switch';
|
||||
import { Section, Input, InputNumber, Switch } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import { isValueNumber } from '../utils';
|
||||
@@ -11,95 +8,75 @@ export default function FormatSettings({ options, data, onOptionsChange }) {
|
||||
const inputsEnabled = isValueNumber(data.rows, options);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-formatting-decimal-place">Formatting Decimal Place</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="counter-formatting-decimal-place"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.DecimalPlace"
|
||||
defaultValue={options.stringDecimal}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={stringDecimal => onOptionsChange({ stringDecimal })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Formatting Decimal Place"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.DecimalPlace"
|
||||
defaultValue={options.stringDecimal}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={stringDecimal => onOptionsChange({ stringDecimal })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-formatting-decimal-character">Formatting Decimal Character</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="counter-formatting-decimal-character"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.DecimalCharacter"
|
||||
defaultValue={options.stringDecChar}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringDecChar: e.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Formatting Decimal Character"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.DecimalCharacter"
|
||||
defaultValue={options.stringDecChar}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringDecChar: e.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-formatting-thousands-separator">Formatting Thousands Separator</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="counter-formatting-thousands-separator"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.ThousandsSeparator"
|
||||
defaultValue={options.stringThouSep}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringThouSep: e.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Formatting Thousands Separator"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.ThousandsSeparator"
|
||||
defaultValue={options.stringThouSep}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringThouSep: e.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-formatting-string-prefix">Formatting String Prefix</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="counter-formatting-string-prefix"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.StringPrefix"
|
||||
defaultValue={options.stringPrefix}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringPrefix: e.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Formatting String Prefix"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.StringPrefix"
|
||||
defaultValue={options.stringPrefix}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringPrefix: e.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-formatting-string-suffix">Formatting String Suffix</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="counter-formatting-string-suffix"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.StringSuffix"
|
||||
defaultValue={options.stringSuffix}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringSuffix: e.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Formatting String Suffix"
|
||||
className="w-100"
|
||||
data-test="Counter.Formatting.StringSuffix"
|
||||
defaultValue={options.stringSuffix}
|
||||
disabled={!inputsEnabled}
|
||||
onChange={e => onOptionsChange({ stringSuffix: e.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<label className="d-flex align-items-center" htmlFor="counter-format-target-value">
|
||||
<Section>
|
||||
<Switch
|
||||
id="counter-format-target-value"
|
||||
data-test="Counter.Formatting.FormatTargetValue"
|
||||
defaultChecked={options.formatTargetValue}
|
||||
onChange={formatTargetValue => onOptionsChange({ formatTargetValue })}
|
||||
/>
|
||||
<span className="m-l-10">Format Target Value</span>
|
||||
</label>
|
||||
>
|
||||
Format Target Value
|
||||
</Switch>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,111 +1,87 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import Switch from 'antd/lib/switch';
|
||||
import { Section, Select, Input, InputNumber, Switch } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function GeneralSettings({ options, data, visualizationName, onOptionsChange }) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-label">Counter Label</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="counter-label"
|
||||
className="w-100"
|
||||
data-test="Counter.General.Label"
|
||||
defaultValue={options.counterLabel}
|
||||
placeholder={visualizationName}
|
||||
onChange={e => onOptionsChange({ counterLabel: e.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Counter Label"
|
||||
className="w-100"
|
||||
data-test="Counter.General.Label"
|
||||
defaultValue={options.counterLabel}
|
||||
placeholder={visualizationName}
|
||||
onChange={e => onOptionsChange({ counterLabel: e.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-value-column">Counter Value Column Name</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="counter-value-column"
|
||||
className="w-100"
|
||||
data-test="Counter.General.ValueColumn"
|
||||
defaultValue={options.counterColName}
|
||||
disabled={options.countRow}
|
||||
onChange={counterColName => onOptionsChange({ counterColName })}
|
||||
>
|
||||
{map(data.columns, col => (
|
||||
<Select.Option key={col.name} data-test={'Counter.General.ValueColumn.' + col.name}>{col.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Counter Value Column Name"
|
||||
className="w-100"
|
||||
data-test="Counter.General.ValueColumn"
|
||||
defaultValue={options.counterColName}
|
||||
disabled={options.countRow}
|
||||
onChange={counterColName => onOptionsChange({ counterColName })}
|
||||
>
|
||||
{map(data.columns, col => (
|
||||
<Select.Option key={col.name} data-test={'Counter.General.ValueColumn.' + col.name}>{col.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-value-row-number">Counter Value Row Number</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="counter-value-row-number"
|
||||
className="w-100"
|
||||
data-test="Counter.General.ValueRowNumber"
|
||||
defaultValue={options.rowNumber}
|
||||
disabled={options.countRow}
|
||||
onChange={rowNumber => onOptionsChange({ rowNumber })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Counter Value Row Number"
|
||||
className="w-100"
|
||||
data-test="Counter.General.ValueRowNumber"
|
||||
defaultValue={options.rowNumber}
|
||||
disabled={options.countRow}
|
||||
onChange={rowNumber => onOptionsChange({ rowNumber })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-target-value-column">Target Value Column Name</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="counter-target-value-column"
|
||||
className="w-100"
|
||||
data-test="Counter.General.TargetValueColumn"
|
||||
defaultValue={options.targetColName}
|
||||
onChange={targetColName => onOptionsChange({ targetColName })}
|
||||
>
|
||||
<Select.Option value="">No target value</Select.Option>
|
||||
{map(data.columns, col => (
|
||||
<Select.Option key={col.name} data-test={'Counter.General.TargetValueColumn.' + col.name}>{col.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Target Value Column Name"
|
||||
className="w-100"
|
||||
data-test="Counter.General.TargetValueColumn"
|
||||
defaultValue={options.targetColName}
|
||||
onChange={targetColName => onOptionsChange({ targetColName })}
|
||||
>
|
||||
<Select.Option value="">No target value</Select.Option>
|
||||
{map(data.columns, col => (
|
||||
<Select.Option key={col.name} data-test={'Counter.General.TargetValueColumn.' + col.name}>{col.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="counter-target-row-number">Target Value Row Number</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="counter-target-row-number"
|
||||
className="w-100"
|
||||
data-test="Counter.General.TargetValueRowNumber"
|
||||
defaultValue={options.targetRowNumber}
|
||||
onChange={targetRowNumber => onOptionsChange({ targetRowNumber })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Target Value Row Number"
|
||||
className="w-100"
|
||||
data-test="Counter.General.TargetValueRowNumber"
|
||||
defaultValue={options.targetRowNumber}
|
||||
onChange={targetRowNumber => onOptionsChange({ targetRowNumber })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<label className="d-flex align-items-center" htmlFor="counter-count-rows">
|
||||
<Section>
|
||||
<Switch
|
||||
id="counter-count-rows"
|
||||
data-test="Counter.General.CountRows"
|
||||
defaultChecked={options.countRow}
|
||||
onChange={countRow => onOptionsChange({ countRow })}
|
||||
/>
|
||||
<span className="m-l-10">Count Rows</span>
|
||||
</label>
|
||||
>
|
||||
Count Rows
|
||||
</Switch>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
9
client/app/visualizations/counter/Editor/index.js
Normal file
9
client/app/visualizations/counter/Editor/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import FormatSettings from './FormatSettings';
|
||||
|
||||
export default createTabbedEditor([
|
||||
{ key: 'General', title: 'General', component: GeneralSettings },
|
||||
{ key: 'Format', title: 'Format', component: FormatSettings },
|
||||
]);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import FormatSettings from './FormatSettings';
|
||||
|
||||
export default function Editor(props) {
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
const optionsChanged = (newOptions) => {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="general" tab={<span data-test="Counter.EditorTabs.General">General</span>}>
|
||||
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="format" tab={<span data-test="Counter.EditorTabs.Formatting">Format</span>}>
|
||||
<FormatSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = EditorPropTypes;
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function DetailsEditor() {
|
||||
return null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { registerVisualization } from '@/visualizations';
|
||||
|
||||
import DetailsRenderer from './DetailsRenderer';
|
||||
import DetailsEditor from './DetailsEditor';
|
||||
|
||||
const DEFAULT_OPTIONS = {};
|
||||
|
||||
@@ -10,8 +10,6 @@ export default function init() {
|
||||
name: 'Details View',
|
||||
getOptions: options => ({ ...DEFAULT_OPTIONS, ...options }),
|
||||
Renderer: DetailsRenderer,
|
||||
Editor: DetailsEditor,
|
||||
|
||||
defaultColumns: 2,
|
||||
defaultRows: 2,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import InputNumber from 'antd/lib/input-number';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import { Section, Input, InputNumber, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function AppearanceSettings({ options, onOptionsChange }) {
|
||||
@@ -12,107 +8,63 @@ export default function AppearanceSettings({ options, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-number-format">
|
||||
Number Values Format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="funnel-editor-step-column-title"
|
||||
className="w-100"
|
||||
data-test="Funnel.NumberFormat"
|
||||
defaultValue={options.numberFormat}
|
||||
onChange={event => onOptionsChangeDebounced({ numberFormat: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label={(<React.Fragment>Number Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
|
||||
className="w-100"
|
||||
data-test="Funnel.NumberFormat"
|
||||
defaultValue={options.numberFormat}
|
||||
onChange={event => onOptionsChangeDebounced({ numberFormat: event.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-number-format">
|
||||
Percent Values Format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="funnel-editor-step-column-title"
|
||||
className="w-100"
|
||||
data-test="Funnel.PercentFormat"
|
||||
defaultValue={options.percentFormat}
|
||||
onChange={event => onOptionsChangeDebounced({ percentFormat: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label={(<React.Fragment>Percent Values Format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
|
||||
className="w-100"
|
||||
data-test="Funnel.PercentFormat"
|
||||
defaultValue={options.percentFormat}
|
||||
onChange={event => onOptionsChangeDebounced({ percentFormat: event.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-items-limit">Items Count Limit</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="funnel-editor-items-limit"
|
||||
className="w-100"
|
||||
data-test="Funnel.ItemsLimit"
|
||||
min={2}
|
||||
defaultValue={options.itemsLimit}
|
||||
onChange={itemsLimit => onOptionsChangeDebounced({ itemsLimit })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Items Count Limit"
|
||||
className="w-100"
|
||||
data-test="Funnel.ItemsLimit"
|
||||
min={2}
|
||||
defaultValue={options.itemsLimit}
|
||||
onChange={itemsLimit => onOptionsChangeDebounced({ itemsLimit })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-percent-values-range-min">Min Percent Value</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="funnel-editor-percent-values-range-min"
|
||||
className="w-100"
|
||||
data-test="Funnel.PercentRangeMin"
|
||||
min={0}
|
||||
defaultValue={options.percentValuesRange.min}
|
||||
onChange={min => onOptionsChangeDebounced({ percentValuesRange: { min } })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Min Percent Value"
|
||||
className="w-100"
|
||||
data-test="Funnel.PercentRangeMin"
|
||||
min={0}
|
||||
defaultValue={options.percentValuesRange.min}
|
||||
onChange={min => onOptionsChangeDebounced({ percentValuesRange: { min } })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-percent-values-range-max">Max Percent Value</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<InputNumber
|
||||
id="funnel-editor-percent-values-range-max"
|
||||
className="w-100"
|
||||
data-test="Funnel.PercentRangeMax"
|
||||
min={0}
|
||||
defaultValue={options.percentValuesRange.max}
|
||||
onChange={max => onOptionsChangeDebounced({ percentValuesRange: { max } })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<InputNumber
|
||||
layout="horizontal"
|
||||
label="Max Percent Value"
|
||||
className="w-100"
|
||||
data-test="Funnel.PercentRangeMax"
|
||||
min={0}
|
||||
defaultValue={options.percentValuesRange.max}
|
||||
onChange={max => onOptionsChangeDebounced({ percentValuesRange: { max } })}
|
||||
/>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { map } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import { Section, Select, Input, Checkbox } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
@@ -14,130 +11,103 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-step-column-name">Step Column</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="funnel-editor-step-column-name"
|
||||
className="w-100"
|
||||
data-test="Funnel.StepColumn"
|
||||
placeholder="Choose column..."
|
||||
defaultValue={options.stepCol.colName || undefined}
|
||||
onChange={colName => onOptionsChange({ stepCol: { colName: colName || null } })}
|
||||
>
|
||||
{map(columnNames, col => (
|
||||
<Select.Option key={col} data-test={`Funnel.StepColumn.${col}`}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Step Column"
|
||||
className="w-100"
|
||||
data-test="Funnel.StepColumn"
|
||||
placeholder="Choose column..."
|
||||
defaultValue={options.stepCol.colName || undefined}
|
||||
onChange={colName => onOptionsChange({ stepCol: { colName: colName || null } })}
|
||||
>
|
||||
{map(columnNames, col => (
|
||||
<Select.Option key={col} data-test={`Funnel.StepColumn.${col}`}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-step-column-title">Step Column Title</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="funnel-editor-step-column-title"
|
||||
className="w-100"
|
||||
data-test="Funnel.StepColumnTitle"
|
||||
defaultValue={options.stepCol.displayAs}
|
||||
onChange={event => onOptionsChangeDebounced({ stepCol: { displayAs: event.target.value } })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Step Column Title"
|
||||
className="w-100"
|
||||
data-test="Funnel.StepColumnTitle"
|
||||
defaultValue={options.stepCol.displayAs}
|
||||
onChange={event => onOptionsChangeDebounced({ stepCol: { displayAs: event.target.value } })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-value-column-name">Value Column</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="funnel-editor-value-column-name"
|
||||
className="w-100"
|
||||
data-test="Funnel.ValueColumn"
|
||||
placeholder="Choose column..."
|
||||
defaultValue={options.valueCol.colName || undefined}
|
||||
onChange={colName => onOptionsChange({ valueCol: { colName: colName || null } })}
|
||||
>
|
||||
{map(columnNames, col => (
|
||||
<Select.Option key={col} data-test={`Funnel.ValueColumn.${col}`}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Value Column"
|
||||
className="w-100"
|
||||
data-test="Funnel.ValueColumn"
|
||||
placeholder="Choose column..."
|
||||
defaultValue={options.valueCol.colName || undefined}
|
||||
onChange={colName => onOptionsChange({ valueCol: { colName: colName || null } })}
|
||||
>
|
||||
{map(columnNames, col => (
|
||||
<Select.Option key={col} data-test={`Funnel.ValueColumn.${col}`}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-value-column-title">Value Column Title</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="funnel-editor-value-column-title"
|
||||
className="w-100"
|
||||
data-test="Funnel.ValueColumnTitle"
|
||||
defaultValue={options.valueCol.displayAs}
|
||||
onChange={event => onOptionsChangeDebounced({ valueCol: { displayAs: event.target.value } })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label="Value Column Title"
|
||||
className="w-100"
|
||||
data-test="Funnel.ValueColumnTitle"
|
||||
defaultValue={options.valueCol.displayAs}
|
||||
onChange={event => onOptionsChangeDebounced({ valueCol: { displayAs: event.target.value } })}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="funnel-editor-custom-sort">
|
||||
<Checkbox
|
||||
id="funnel-editor-custom-sort"
|
||||
data-test="Funnel.CustomSort"
|
||||
checked={!options.autoSort}
|
||||
onChange={event => onOptionsChange({ autoSort: !event.target.checked })}
|
||||
/>
|
||||
<span>Custom Sorting</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Funnel.CustomSort"
|
||||
checked={!options.autoSort}
|
||||
onChange={event => onOptionsChange({ autoSort: !event.target.checked })}
|
||||
>
|
||||
Custom Sorting
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
{!options.autoSort && (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-sort-column-name">Sort Column</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="funnel-editor-sort-column-name"
|
||||
className="w-100"
|
||||
data-test="Funnel.SortColumn"
|
||||
allowClear
|
||||
placeholder="Choose column..."
|
||||
defaultValue={options.sortKeyCol.colName || undefined}
|
||||
onChange={colName => onOptionsChange({ sortKeyCol: { colName: colName || null } })}
|
||||
>
|
||||
{map(columnNames, col => (
|
||||
<Select.Option key={col} data-test={`Funnel.SortColumn.${col}`}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Sort Column"
|
||||
className="w-100"
|
||||
data-test="Funnel.SortColumn"
|
||||
allowClear
|
||||
placeholder="Choose column..."
|
||||
defaultValue={options.sortKeyCol.colName || undefined}
|
||||
onChange={colName => onOptionsChange({ sortKeyCol: { colName: colName || null } })}
|
||||
>
|
||||
{map(columnNames, col => (
|
||||
<Select.Option key={col} data-test={`Funnel.SortColumn.${col}`}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
<Grid.Row type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="funnel-editor-sort-reverse">Sort Order</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="funnel-editor-sort-reverse"
|
||||
className="w-100"
|
||||
data-test="Funnel.SortDirection"
|
||||
disabled={!options.sortKeyCol.colName}
|
||||
defaultValue={options.sortKeyCol.reverse ? 'desc' : 'asc'}
|
||||
onChange={order => onOptionsChange({ sortKeyCol: { reverse: order === 'desc' } })}
|
||||
>
|
||||
<Select.Option value="asc" data-test="Funnel.SortDirection.Ascending">ascending</Select.Option>
|
||||
<Select.Option value="desc" data-test="Funnel.SortDirection.Descending">descending</Select.Option>
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Sort Order"
|
||||
className="w-100"
|
||||
data-test="Funnel.SortDirection"
|
||||
disabled={!options.sortKeyCol.colName}
|
||||
defaultValue={options.sortKeyCol.reverse ? 'desc' : 'asc'}
|
||||
onChange={order => onOptionsChange({ sortKeyCol: { reverse: order === 'desc' } })}
|
||||
>
|
||||
<Select.Option value="asc" data-test="Funnel.SortDirection.Ascending">ascending</Select.Option>
|
||||
<Select.Option value="desc" data-test="Funnel.SortDirection.Descending">descending</Select.Option>
|
||||
</Select>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</React.Fragment>
|
||||
|
||||
9
client/app/visualizations/funnel/Editor/index.js
Normal file
9
client/app/visualizations/funnel/Editor/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import AppearanceSettings from './AppearanceSettings';
|
||||
|
||||
export default createTabbedEditor([
|
||||
{ key: 'General', title: 'General', component: GeneralSettings },
|
||||
{ key: 'Appearance', title: 'Appearance', component: AppearanceSettings },
|
||||
]);
|
||||
@@ -1,28 +0,0 @@
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import AppearanceSettings from './AppearanceSettings';
|
||||
|
||||
export default function Editor(props) {
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
const optionsChanged = (newOptions) => {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="general" tab={<span data-test="Funnel.EditorTabs.General">General</span>}>
|
||||
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="appearance" tab={<span data-test="Funnel.EditorTabs.Appearance">Appearance</span>}>
|
||||
<AppearanceSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = EditorPropTypes;
|
||||
@@ -70,6 +70,7 @@ function validateVisualizationConfig(config) {
|
||||
export function registerVisualization(config) {
|
||||
validateVisualizationConfig(config);
|
||||
config = {
|
||||
Editor: () => null,
|
||||
...config,
|
||||
isDefault: config.isDefault && !config.isDeprecated,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isNil, map, filter, difference } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import { Section, Select } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
function getColumns(column, unusedColumns) {
|
||||
@@ -18,11 +18,10 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="map-editor-latitude-column-name">Latitude Column Name</label>
|
||||
<Section>
|
||||
<Select
|
||||
label="Latitude Column Name"
|
||||
data-test="Map.Editor.LatitudeColumnName"
|
||||
id="map-editor-latitude-column-name"
|
||||
className="w-100"
|
||||
value={options.latColName}
|
||||
onChange={latColName => onOptionsChange({ latColName })}
|
||||
@@ -31,13 +30,12 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key={col} data-test={'Map.Editor.LatitudeColumnName.' + col}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="map-editor-longitude-column-name">Longitude Column Name</label>
|
||||
<Section>
|
||||
<Select
|
||||
label="Longitude Column Name"
|
||||
data-test="Map.Editor.LongitudeColumnName"
|
||||
id="map-editor-longitude-column-name"
|
||||
className="w-100"
|
||||
value={options.lonColName}
|
||||
onChange={lonColName => onOptionsChange({ lonColName })}
|
||||
@@ -46,13 +44,12 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key={col} data-test={'Map.Editor.LongitudeColumnName.' + col}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label className="control-label" htmlFor="map-editor-group-by">Group By</label>
|
||||
<Section>
|
||||
<Select
|
||||
label="Group By"
|
||||
data-test="Map.Editor.GroupBy"
|
||||
id="map-editor-group-by"
|
||||
className="w-100"
|
||||
allowClear
|
||||
placeholder="none"
|
||||
@@ -63,7 +60,7 @@ export default function GeneralSettings({ options, data, onOptionsChange }) {
|
||||
<Select.Option key={col} data-test={'Map.Editor.GroupBy.' + col}>{col}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,18 +37,17 @@ export default function GroupsSettings({ options, data, onOptionsChange }) {
|
||||
title: 'Color',
|
||||
dataIndex: 'color',
|
||||
width: '1%',
|
||||
className: 'text-nowrap',
|
||||
render: (unused, item) => (
|
||||
<div className="text-nowrap">
|
||||
<ColorPicker
|
||||
interactive
|
||||
presetColors={colors}
|
||||
placement="topRight"
|
||||
color={item.color}
|
||||
triggerProps={{ 'data-test': `Map.Editor.Groups.${item.name}.Color` }}
|
||||
onChange={value => updateGroupOption(item.name, 'color', value)}
|
||||
/>
|
||||
<ColorPicker.Label color={item.color} presetColors={colors} />
|
||||
</div>
|
||||
<ColorPicker
|
||||
interactive
|
||||
presetColors={colors}
|
||||
placement="topRight"
|
||||
color={item.color}
|
||||
triggerProps={{ 'data-test': `Map.Editor.Groups.${item.name}.Color` }}
|
||||
onChange={value => updateGroupOption(item.name, 'color', value)}
|
||||
addonAfter={<ColorPicker.Label color={item.color} presetColors={colors} />}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { isNil, map } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Select from 'antd/lib/select';
|
||||
import Input from 'antd/lib/input';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Typography from 'antd/lib/typography';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import ColorPicker from '@/components/ColorPicker';
|
||||
import { Section, Select, Checkbox, Input, ColorPicker, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
import ColorPalette from '@/visualizations/ColorPalette';
|
||||
|
||||
@@ -92,11 +85,10 @@ export default function StyleSettings({ options, onOptionsChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="map-editor-tiles">Map Tiles</label>
|
||||
<Section>
|
||||
<Select
|
||||
label="Map Tiles"
|
||||
data-test="Map.Editor.Tiles"
|
||||
id="map-editor-tiles"
|
||||
className="w-100"
|
||||
value={options.mapTileUrl}
|
||||
onChange={mapTileUrl => onOptionsChange({ mapTileUrl })}
|
||||
@@ -105,168 +97,130 @@ export default function StyleSettings({ options, onOptionsChange }) {
|
||||
<Select.Option key={url} data-test={'Map.Editor.Tiles.' + name}>{name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<h4 className="m-t-15 m-b-15">Markers</h4>
|
||||
<Section.Title>Markers</Section.Title>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="map-editor-cluster-markers">
|
||||
<Checkbox
|
||||
id="map-editor-cluster-markers"
|
||||
data-test="Map.Editor.ClusterMarkers"
|
||||
defaultChecked={options.clusterMarkers}
|
||||
onChange={event => onOptionsChange({ clusterMarkers: event.target.checked })}
|
||||
/>
|
||||
<span>Cluster Markers</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Map.Editor.ClusterMarkers"
|
||||
defaultChecked={options.clusterMarkers}
|
||||
onChange={event => onOptionsChange({ clusterMarkers: event.target.checked })}
|
||||
>
|
||||
Cluster Markers
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="map-editor-customize-markers">
|
||||
<Checkbox
|
||||
id="map-editor-customize-markers"
|
||||
data-test="Map.Editor.CustomizeMarkers"
|
||||
disabled={!isCustomMarkersStyleAllowed}
|
||||
defaultChecked={options.customizeMarkers}
|
||||
onChange={event => onOptionsChange({ customizeMarkers: event.target.checked })}
|
||||
/>
|
||||
<Typography.Text disabled={!isCustomMarkersStyleAllowed}>Override default style</Typography.Text>
|
||||
{!isCustomMarkersStyleAllowed && (
|
||||
<Popover
|
||||
placement="topLeft"
|
||||
arrowPointAtCenter
|
||||
content={(
|
||||
<span>
|
||||
Custom marker styles are not available<br />
|
||||
when <b>Group By</b> column selected.
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Map.Editor.CustomizeMarkers"
|
||||
disabled={!isCustomMarkersStyleAllowed}
|
||||
defaultChecked={options.customizeMarkers}
|
||||
onChange={event => onOptionsChange({ customizeMarkers: event.target.checked })}
|
||||
>
|
||||
Override default style
|
||||
</Checkbox>
|
||||
{!isCustomMarkersStyleAllowed && (
|
||||
<ContextHelp placement="topLeft" arrowPointAtCenter>
|
||||
Custom marker styles are not available<br />
|
||||
when <b>Group By</b> column selected.
|
||||
</ContextHelp>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{isCustomMarkersStyleAllowed && options.customizeMarkers && (
|
||||
<React.Fragment>
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="map-editor-marker-shape">Shape</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Select
|
||||
id="map-editor-marker-shape"
|
||||
className="w-100"
|
||||
data-test="Map.Editor.MarkerShape"
|
||||
value={options.iconShape}
|
||||
onChange={iconShape => onOptionsChange({ iconShape })}
|
||||
>
|
||||
<Select.Option key="marker" data-test="Map.Editor.MarkerShape.marker">Marker + Icon</Select.Option>
|
||||
<Select.Option key="doughnut" data-test="Map.Editor.MarkerShape.doughnut">Circle</Select.Option>
|
||||
<Select.Option key="circle-dot" data-test="Map.Editor.MarkerShape.circle-dot">Circle Dot</Select.Option>
|
||||
<Select.Option key="circle" data-test="Map.Editor.MarkerShape.circle">Circle + Icon</Select.Option>
|
||||
<Select.Option key="rectangle-dot" data-test="Map.Editor.MarkerShape.rectangle-dot">Square Dot</Select.Option>
|
||||
<Select.Option key="rectangle" data-test="Map.Editor.MarkerShape.rectangle">Square + Icon</Select.Option>
|
||||
</Select>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Select
|
||||
layout="horizontal"
|
||||
label="Shape"
|
||||
className="w-100"
|
||||
data-test="Map.Editor.MarkerShape"
|
||||
value={options.iconShape}
|
||||
onChange={iconShape => onOptionsChange({ iconShape })}
|
||||
>
|
||||
<Select.Option key="marker" data-test="Map.Editor.MarkerShape.marker">Marker + Icon</Select.Option>
|
||||
<Select.Option key="doughnut" data-test="Map.Editor.MarkerShape.doughnut">Circle</Select.Option>
|
||||
<Select.Option key="circle-dot" data-test="Map.Editor.MarkerShape.circle-dot">Circle Dot</Select.Option>
|
||||
<Select.Option key="circle" data-test="Map.Editor.MarkerShape.circle">Circle + Icon</Select.Option>
|
||||
<Select.Option key="rectangle-dot" data-test="Map.Editor.MarkerShape.rectangle-dot">Square Dot</Select.Option>
|
||||
<Select.Option key="rectangle" data-test="Map.Editor.MarkerShape.rectangle">Square + Icon</Select.Option>
|
||||
</Select>
|
||||
</Section>
|
||||
|
||||
{showIcon && (
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="map-editor-marker-icon">
|
||||
Icon
|
||||
<Popover
|
||||
placement="topLeft"
|
||||
arrowPointAtCenter
|
||||
content={(
|
||||
<React.Fragment>
|
||||
<div className="m-b-5">
|
||||
Enter an icon name from{' '}
|
||||
<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank" rel="noopener noreferrer">Font-Awesome 4.7</a>
|
||||
</div>
|
||||
<div className="m-b-5">
|
||||
Examples: <code>check</code>, <code>times-circle</code>, <code>flag</code>
|
||||
</div>
|
||||
<div>Leave blank to remove.</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12}>
|
||||
<Input
|
||||
id="map-editor-marker-icon"
|
||||
className="w-100"
|
||||
data-test="Map.Editor.MarkerIcon"
|
||||
defaultValue={options.iconFont}
|
||||
onChange={event => debouncedOnOptionsChange({ iconFont: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
label={(
|
||||
<React.Fragment>
|
||||
Icon
|
||||
<ContextHelp placement="topLeft" arrowPointAtCenter>
|
||||
<div className="m-b-5">
|
||||
Enter an icon name from{' '}
|
||||
<a href="https://fontawesome.com/v4.7.0/icons/" target="_blank" rel="noopener noreferrer">Font-Awesome 4.7</a>
|
||||
</div>
|
||||
<div className="m-b-5">
|
||||
Examples: <code>check</code>, <code>times-circle</code>, <code>flag</code>
|
||||
</div>
|
||||
<div>Leave blank to remove.</div>
|
||||
</ContextHelp>
|
||||
</React.Fragment>
|
||||
)}
|
||||
className="w-100"
|
||||
data-test="Map.Editor.MarkerIcon"
|
||||
defaultValue={options.iconFont}
|
||||
onChange={event => debouncedOnOptionsChange({ iconFont: event.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{showIcon && (
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="map-editor-marker-icon-color">Icon Color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="map-editor-marker-icon-color"
|
||||
interactive
|
||||
presetColors={CustomColorPalette}
|
||||
placement="topRight"
|
||||
color={options.foregroundColor}
|
||||
triggerProps={{ 'data-test': 'Map.Editor.MarkerIconColor' }}
|
||||
onChange={foregroundColor => onOptionsChange({ foregroundColor })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.foregroundColor} presetColors={CustomColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Icon Color"
|
||||
interactive
|
||||
presetColors={CustomColorPalette}
|
||||
placement="topRight"
|
||||
color={options.foregroundColor}
|
||||
triggerProps={{ 'data-test': 'Map.Editor.MarkerIconColor' }}
|
||||
onChange={foregroundColor => onOptionsChange({ foregroundColor })}
|
||||
addonAfter={<ColorPicker.Label color={options.foregroundColor} presetColors={CustomColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{showBackgroundColor && (
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="map-editor-marker-background-color">Background Color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="map-editor-marker-background-color"
|
||||
interactive
|
||||
presetColors={CustomColorPalette}
|
||||
placement="topRight"
|
||||
color={options.backgroundColor}
|
||||
triggerProps={{ 'data-test': 'Map.Editor.MarkerBackgroundColor' }}
|
||||
onChange={backgroundColor => onOptionsChange({ backgroundColor })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.backgroundColor} presetColors={CustomColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Background Color"
|
||||
interactive
|
||||
presetColors={CustomColorPalette}
|
||||
placement="topRight"
|
||||
color={options.backgroundColor}
|
||||
triggerProps={{ 'data-test': 'Map.Editor.MarkerBackgroundColor' }}
|
||||
onChange={backgroundColor => onOptionsChange({ backgroundColor })}
|
||||
addonAfter={<ColorPicker.Label color={options.backgroundColor} presetColors={CustomColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{showBorderColor && (
|
||||
<Grid.Row type="flex" align="middle" className="m-b-10">
|
||||
<Grid.Col span={12}>
|
||||
<label htmlFor="map-editor-marker-border-color">Border Color</label>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={12} className="text-nowrap">
|
||||
<ColorPicker
|
||||
id="map-editor-marker-border-color"
|
||||
interactive
|
||||
presetColors={CustomColorPalette}
|
||||
placement="topRight"
|
||||
color={options.borderColor}
|
||||
triggerProps={{ 'data-test': 'Map.Editor.MarkerBorderColor' }}
|
||||
onChange={borderColor => onOptionsChange({ borderColor })}
|
||||
/>
|
||||
<ColorPicker.Label color={options.borderColor} presetColors={CustomColorPalette} />
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<ColorPicker
|
||||
layout="horizontal"
|
||||
label="Border Color"
|
||||
interactive
|
||||
presetColors={CustomColorPalette}
|
||||
placement="topRight"
|
||||
color={options.borderColor}
|
||||
triggerProps={{ 'data-test': 'Map.Editor.MarkerBorderColor' }}
|
||||
onChange={borderColor => onOptionsChange({ borderColor })}
|
||||
addonAfter={<ColorPicker.Label color={options.borderColor} presetColors={CustomColorPalette} />}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
11
client/app/visualizations/map/Editor/index.js
Normal file
11
client/app/visualizations/map/Editor/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import GroupsSettings from './GroupsSettings';
|
||||
import StyleSettings from './StyleSettings';
|
||||
|
||||
export default createTabbedEditor([
|
||||
{ key: 'General', title: 'General', component: GeneralSettings },
|
||||
{ key: 'Groups', title: 'Groups', component: GroupsSettings },
|
||||
{ key: 'Style', title: 'Style', component: StyleSettings },
|
||||
]);
|
||||
@@ -1,32 +0,0 @@
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
import GeneralSettings from './GeneralSettings';
|
||||
import GroupsSettings from './GroupsSettings';
|
||||
import StyleSettings from './StyleSettings';
|
||||
|
||||
export default function Editor(props) {
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
const optionsChanged = (newOptions) => {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="general" tab={<span data-test="Map.EditorTabs.General">General</span>}>
|
||||
<GeneralSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="groups" tab={<span data-test="Map.EditorTabs.Groups">Groups</span>}>
|
||||
<GroupsSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="style" tab={<span data-test="Map.EditorTabs.Style">Style</span>}>
|
||||
<StyleSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Editor.propTypes = EditorPropTypes;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isFunction, each, map, maxBy, toString } from 'lodash';
|
||||
import { isFunction, each, map, toString } from 'lodash';
|
||||
import chroma from 'chroma-js';
|
||||
import L from 'leaflet';
|
||||
import 'leaflet.markercluster';
|
||||
@@ -13,6 +13,7 @@ import markerShadow from 'leaflet/dist/images/marker-shadow.png';
|
||||
import 'leaflet-fullscreen';
|
||||
import 'leaflet-fullscreen/dist/leaflet.fullscreen.css';
|
||||
import resizeObserver from '@/services/resizeObserver';
|
||||
import chooseTextColorForBackground from '@/lib/chooseTextColorForBackground';
|
||||
|
||||
// This is a workaround for an issue with giving Leaflet load the icon on its own.
|
||||
L.Icon.Default.mergeOptions({
|
||||
@@ -50,7 +51,7 @@ L.MarkerClusterIcon = L.DivIcon.extend({
|
||||
},
|
||||
createIcon(...args) {
|
||||
const color = chroma(this.options.color);
|
||||
const textColor = maxBy(['#ffffff', '#000000'], c => chroma.contrast(color, c));
|
||||
const textColor = chooseTextColorForBackground(color);
|
||||
const borderColor = color.alpha(0.4).css();
|
||||
const backgroundColor = color.alpha(0.8).css();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Switch from 'antd/lib/switch';
|
||||
import { Section, Switch } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
export default function Editor({ options, onOptionsChange }) {
|
||||
@@ -9,33 +9,36 @@ export default function Editor({ options, onOptionsChange }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-group m-t-30">
|
||||
<label className="d-flex align-items-center" htmlFor="pivot-show-controls">
|
||||
<React.Fragment>
|
||||
<Section>
|
||||
<Switch
|
||||
data-test="PivotEditor.HideControls"
|
||||
id="pivot-show-controls"
|
||||
checked={!options.controls.enabled}
|
||||
defaultChecked={!options.controls.enabled}
|
||||
onChange={enabled => updateOptions({ controls: { enabled: !enabled } })}
|
||||
/>
|
||||
<span className="m-l-10">Show Pivot Controls</span>
|
||||
</label>
|
||||
<label className="d-flex align-items-center" htmlFor="pivot-show-row-totals">
|
||||
>
|
||||
Show Pivot Controls
|
||||
</Switch>
|
||||
</Section>
|
||||
<Section>
|
||||
<Switch
|
||||
id="pivot-show-row-totals"
|
||||
checked={options.rendererOptions.table.rowTotals}
|
||||
defaultChecked={options.rendererOptions.table.rowTotals}
|
||||
onChange={rowTotals => updateOptions({ rendererOptions: { table: { rowTotals } } })}
|
||||
/>
|
||||
<span className="m-l-10">Show Row Totals</span>
|
||||
</label>
|
||||
<label className="d-flex align-items-center" htmlFor="pivot-show-col-totals">
|
||||
>
|
||||
Show Row Totals
|
||||
</Switch>
|
||||
</Section>
|
||||
<Section>
|
||||
<Switch
|
||||
id="pivot-show-row-totals"
|
||||
checked={options.rendererOptions.table.colTotals}
|
||||
id="pivot-show-column-totals"
|
||||
defaultChecked={options.rendererOptions.table.colTotals}
|
||||
onChange={colTotals => updateOptions({ rendererOptions: { table: { colTotals } } })}
|
||||
/>
|
||||
<span className="m-l-10">Show Column Totals</span>
|
||||
</label>
|
||||
</div>
|
||||
>
|
||||
Show Column Totals
|
||||
</Switch>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,16 @@ import React from 'react';
|
||||
|
||||
export default function Editor() {
|
||||
return (
|
||||
<div className="form-horizontal">
|
||||
<div>
|
||||
This visualization expects the query result to have rows in the following format:
|
||||
|
||||
<ul>
|
||||
<li><strong>stage1</strong> - stage 1 value</li>
|
||||
<li><strong>stage2</strong> - stage 2 value (or null)</li>
|
||||
<li><strong>stage3</strong> - stage 3 value (or null)</li>
|
||||
<li><strong>stage4</strong> - stage 4 value (or null)</li>
|
||||
<li><strong>stage5</strong> - stage 5 value (or null)</li>
|
||||
<li><strong>value</strong> - number of times this sequence occurred</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<React.Fragment>
|
||||
<p>This visualization expects the query result to have rows in the following format:</p>
|
||||
<ul>
|
||||
<li><strong>stage1</strong> - stage 1 value</li>
|
||||
<li><strong>stage2</strong> - stage 2 value (or null)</li>
|
||||
<li><strong>stage3</strong> - stage 3 value (or null)</li>
|
||||
<li><strong>stage4</strong> - stage 4 value (or null)</li>
|
||||
<li><strong>stage5</strong> - stage 5 value (or null)</li>
|
||||
<li><strong>value</strong> - number of times this sequence occurred</li>
|
||||
</ul>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Section } from '@/components/visualizations/editor';
|
||||
|
||||
export default function Editor() {
|
||||
return (
|
||||
<div className="form-horizontal">
|
||||
<div>
|
||||
This visualization expects the query result to have rows in one of the following formats:
|
||||
|
||||
<div className="m-t-10">
|
||||
<strong>Option 1:</strong>
|
||||
<ul>
|
||||
<li><strong>sequence</strong> - sequence id</li>
|
||||
<li><strong>stage</strong> - what stage in sequence this is (1, 2, ...)</li>
|
||||
<li><strong>node</strong> - stage name</li>
|
||||
<li><strong>value</strong> - number of times this sequence occurred</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="m-t-10">
|
||||
<strong>Option 2:</strong>
|
||||
<ul>
|
||||
<li><strong>stage1</strong> - stage 1 value</li>
|
||||
<li><strong>stage2</strong> - stage 2 value (or null)</li>
|
||||
<li><strong>stage3</strong> - stage 3 value (or null)</li>
|
||||
<li><strong>stage4</strong> - stage 4 value (or null)</li>
|
||||
<li><strong>stage5</strong> - stage 5 value (or null)</li>
|
||||
<li><strong>value</strong> - number of times this sequence occurred</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<React.Fragment>
|
||||
<p>This visualization expects the query result to have rows in one of the following formats:</p>
|
||||
<Section>
|
||||
<p><strong>Option 1:</strong></p>
|
||||
<ul>
|
||||
<li><strong>sequence</strong> - sequence id</li>
|
||||
<li><strong>stage</strong> - what stage in sequence this is (1, 2, ...)</li>
|
||||
<li><strong>node</strong> - stage name</li>
|
||||
<li><strong>value</strong> - number of times this sequence occurred</li>
|
||||
</ul>
|
||||
</Section>
|
||||
<Section>
|
||||
<p><strong>Option 2:</strong></p>
|
||||
<ul>
|
||||
<li><strong>stage1</strong> - stage 1 value</li>
|
||||
<li><strong>stage2</strong> - stage 2 value (or null)</li>
|
||||
<li><strong>stage3</strong> - stage 3 value (or null)</li>
|
||||
<li><strong>stage4</strong> - stage 4 value (or null)</li>
|
||||
<li><strong>stage5</strong> - stage 5 value (or null)</li>
|
||||
<li><strong>value</strong> - number of times this sequence occurred</li>
|
||||
</ul>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,7 @@ import React from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Grid from 'antd/lib/grid';
|
||||
import Input from 'antd/lib/input';
|
||||
import Radio from 'antd/lib/radio';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Select from 'antd/lib/select';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import Tooltip from 'antd/lib/tooltip';
|
||||
import { Section, Select, Input, Checkbox, TextAlignmentSelect } from '@/components/visualizations/editor';
|
||||
|
||||
import ColumnTypes from '../columns';
|
||||
|
||||
@@ -23,55 +18,38 @@ export default function ColumnEditor({ column, onChange }) {
|
||||
|
||||
return (
|
||||
<div className="table-visualization-editor-column">
|
||||
<Grid.Row gutter={15} type="flex" align="middle" className="m-b-15">
|
||||
<Grid.Col span={16}>
|
||||
<Input
|
||||
data-test={`Table.Column.${column.name}.Title`}
|
||||
defaultValue={column.title}
|
||||
onChange={event => handleChangeDebounced({ title: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<Radio.Group
|
||||
className="table-visualization-editor-column-align-content"
|
||||
defaultValue={column.alignContent}
|
||||
onChange={event => handleChange({ alignContent: event.target.value })}
|
||||
>
|
||||
<Tooltip title="Align left" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="left" data-test={`Table.Column.${column.name}.AlignLeft`}>
|
||||
<Icon type="align-left" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Align center" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="center" data-test={`Table.Column.${column.name}.AlignCenter`}>
|
||||
<Icon type="align-center" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
<Tooltip title="Align right" mouseEnterDelay={0} mouseLeaveDelay={0}>
|
||||
<Radio.Button value="right" data-test={`Table.Column.${column.name}.AlignRight`}>
|
||||
<Icon type="align-right" />
|
||||
</Radio.Button>
|
||||
</Tooltip>
|
||||
</Radio.Group>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Section>
|
||||
<Grid.Row gutter={15} type="flex" align="middle">
|
||||
<Grid.Col span={16}>
|
||||
<Input
|
||||
data-test={`Table.Column.${column.name}.Title`}
|
||||
defaultValue={column.title}
|
||||
onChange={event => handleChangeDebounced({ title: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={8}>
|
||||
<TextAlignmentSelect
|
||||
data-test={`Table.Column.${column.name}.TextAlignment`}
|
||||
defaultValue={column.alignContent}
|
||||
onChange={event => handleChange({ alignContent: event.target.value })}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-allow-search`}>
|
||||
<Checkbox
|
||||
id={`table-column-editor-${column.name}-allow-search`}
|
||||
data-test={`Table.Column.${column.name}.UseForSearch`}
|
||||
defaultChecked={column.allowSearch}
|
||||
onChange={event => handleChange({ allowSearch: event.target.checked })}
|
||||
/>
|
||||
<span>Use for search</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test={`Table.Column.${column.name}.UseForSearch`}
|
||||
defaultChecked={column.allowSearch}
|
||||
onChange={event => handleChange({ allowSearch: event.target.checked })}
|
||||
>
|
||||
Use for search
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-display-as`}>Display as:</label>
|
||||
<Section>
|
||||
<Select
|
||||
id={`table-column-editor-${column.name}-display-as`}
|
||||
label="Display as:"
|
||||
data-test={`Table.Column.${column.name}.DisplayAs`}
|
||||
className="w-100"
|
||||
defaultValue={column.displayAs}
|
||||
@@ -81,7 +59,7 @@ export default function ColumnEditor({ column, onChange }) {
|
||||
<Select.Option key={key} data-test={`Table.Column.${column.name}.DisplayAs.${key}`}>{friendlyName}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{AdditionalOptions && <AdditionalOptions column={column} onChange={handleChange} />}
|
||||
</div>
|
||||
|
||||
@@ -31,37 +31,38 @@ describe('Visualizations -> Table -> Editor -> Columns Settings', () => {
|
||||
test('Toggles column visibility', (done) => {
|
||||
const el = mount({}, done);
|
||||
|
||||
findByTestID(el, 'Table.Column.a.Visibility').first().simulate('click');
|
||||
findByTestID(el, 'Table.Column.a.Visibility').last().simulate('click');
|
||||
});
|
||||
|
||||
test('Changes column title', (done) => {
|
||||
const el = mount({}, done);
|
||||
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
|
||||
findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
|
||||
|
||||
findByTestID(el, 'Table.Column.a.Title').first().simulate('change', { target: { value: 'test' } });
|
||||
findByTestID(el, 'Table.Column.a.Title').last().simulate('change', { target: { value: 'test' } });
|
||||
});
|
||||
|
||||
test('Changes column alignment', (done) => {
|
||||
const el = mount({}, done);
|
||||
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
|
||||
findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
|
||||
|
||||
findByTestID(el, 'Table.Column.a.AlignRight').first().find('input')
|
||||
findByTestID(el, 'Table.Column.a.TextAlignment').last()
|
||||
.find('[data-test="TextAlignmentSelect.Right"] input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
test('Enables search by column data', (done) => {
|
||||
const el = mount({}, done);
|
||||
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
|
||||
findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
|
||||
|
||||
findByTestID(el, 'Table.Column.a.UseForSearch').first().find('input')
|
||||
findByTestID(el, 'Table.Column.a.UseForSearch').last().find('input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
test('Changes column display type', (done) => {
|
||||
const el = mount({}, done);
|
||||
findByTestID(el, 'Table.Column.a.Name').first().simulate('click'); // expand settings
|
||||
findByTestID(el, 'Table.Column.a.Name').last().simulate('click'); // expand settings
|
||||
|
||||
findByTestID(el, 'Table.Column.a.DisplayAs').first().simulate('click');
|
||||
findByTestID(el, 'Table.Column.a.DisplayAs.number').first().simulate('click');
|
||||
findByTestID(el, 'Table.Column.a.DisplayAs').last().simulate('click');
|
||||
findByTestID(el, 'Table.Column.a.DisplayAs.number').last().simulate('click');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { map } from 'lodash';
|
||||
import React from 'react';
|
||||
import Select from 'antd/lib/select';
|
||||
import { Section, Select } from '@/components/visualizations/editor';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
|
||||
const ALLOWED_ITEM_PER_PAGE = [5, 10, 15, 20, 25, 50, 100, 150, 200, 250];
|
||||
|
||||
export default function GridSettings({ options, onOptionsChange }) {
|
||||
return (
|
||||
<div className="m-b-15">
|
||||
<label htmlFor="table-editor-items-per-page">Items per page</label>
|
||||
<Section>
|
||||
<Select
|
||||
id="table-editor-items-per-page"
|
||||
label="Items per page"
|
||||
data-test="Table.ItemsPerPage"
|
||||
className="w-100"
|
||||
defaultValue={options.itemsPerPage}
|
||||
@@ -20,7 +19,7 @@ export default function GridSettings({ options, onOptionsChange }) {
|
||||
<Select.Option key={`ipp${value}`} value={value} data-test={`Table.ItemsPerPage.${value}`}>{value}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('Visualizations -> Table -> Editor -> Grid Settings', () => {
|
||||
itemsPerPage: 25,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ItemsPerPage').first().simulate('click');
|
||||
findByTestID(el, 'Table.ItemsPerPage.100').first().simulate('click');
|
||||
findByTestID(el, 'Table.ItemsPerPage').last().simulate('click');
|
||||
findByTestID(el, 'Table.ItemsPerPage.100').last().simulate('click');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,18 +20,4 @@
|
||||
|
||||
.table-visualization-editor-column {
|
||||
padding-left: 6px;
|
||||
|
||||
.table-visualization-editor-column-align-content {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
// fit <Input> height
|
||||
height: 35px;
|
||||
line-height: 33px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
import { merge } from 'lodash';
|
||||
import React from 'react';
|
||||
import Tabs from 'antd/lib/tabs';
|
||||
import { EditorPropTypes } from '@/visualizations';
|
||||
import createTabbedEditor from '@/components/visualizations/editor/createTabbedEditor';
|
||||
|
||||
import ColumnsSettings from './ColumnsSettings';
|
||||
import GridSettings from './GridSettings';
|
||||
|
||||
import './editor.less';
|
||||
|
||||
export default function index(props) {
|
||||
const { options, onOptionsChange } = props;
|
||||
|
||||
const optionsChanged = (newOptions) => {
|
||||
onOptionsChange(merge({}, options, newOptions));
|
||||
};
|
||||
|
||||
return (
|
||||
<Tabs className="table-editor-container" animated={false} tabBarGutter={0}>
|
||||
<Tabs.TabPane key="columns" tab={<span data-test="Counter.EditorTabs.General">Columns</span>}>
|
||||
<ColumnsSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane key="grid" tab={<span data-test="Counter.EditorTabs.Formatting">Grid</span>}>
|
||||
<GridSettings {...props} onOptionsChange={optionsChanged} />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
index.propTypes = EditorPropTypes;
|
||||
export default createTabbedEditor([
|
||||
{ key: 'Columns', title: 'Columns', component: ColumnsSettings },
|
||||
{ key: 'Grid', title: 'Grid', component: GridSettings },
|
||||
]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import { Section, Input } from '@/components/visualizations/editor';
|
||||
import { createBooleanFormatter } from '@/lib/value-format';
|
||||
|
||||
function Editor({ column, onChange }) {
|
||||
@@ -15,33 +15,23 @@ function Editor({ column, onChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-boolean-false`}>
|
||||
Value for <code>false</code>
|
||||
</label>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-boolean-false`}
|
||||
data-test="Table.ColumnEditor.Boolean.False"
|
||||
defaultValue={column.booleanValues[0]}
|
||||
onChange={event => handleChangeDebounced(0, event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Section>
|
||||
<Input
|
||||
label={(<React.Fragment>Value for <code>false</code></React.Fragment>)}
|
||||
data-test="Table.ColumnEditor.Boolean.False"
|
||||
defaultValue={column.booleanValues[0]}
|
||||
onChange={event => handleChangeDebounced(0, event.target.value)}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-boolean-true`}>
|
||||
Value for <code>true</code>
|
||||
</label>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-boolean-true`}
|
||||
data-test="Table.ColumnEditor.Boolean.True"
|
||||
defaultValue={column.booleanValues[1]}
|
||||
onChange={event => handleChangeDebounced(1, event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Section>
|
||||
<Input
|
||||
label={(<React.Fragment>Value for <code>true</code></React.Fragment>)}
|
||||
data-test="Table.ColumnEditor.Boolean.True"
|
||||
defaultValue={column.booleanValues[1]}
|
||||
onChange={event => handleChangeDebounced(1, event.target.value)}
|
||||
/>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Boolean', () => {
|
||||
booleanValues: ['false', 'true'],
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Boolean.False').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Boolean.False').last().find('input')
|
||||
.simulate('change', { target: { value: 'no' } });
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('Visualizations -> Table -> Columns -> Boolean', () => {
|
||||
booleanValues: ['false', 'true'],
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Boolean.True').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Boolean.True').last().find('input')
|
||||
.simulate('change', { target: { value: 'yes' } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { Section, Input, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { createDateTimeFormatter } from '@/lib/value-format';
|
||||
|
||||
function Editor({ column, onChange }) {
|
||||
const [onChangeDebounced] = useDebouncedCallback(onChange, 200);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-datetime-format`}>
|
||||
Date/Time format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://momentjs.com/docs/#/displaying/format/" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-datetime-format`}
|
||||
data-test="Table.ColumnEditor.DateTime.Format"
|
||||
defaultValue={column.dateTimeFormat}
|
||||
onChange={event => onChangeDebounced({ dateTimeFormat: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<Section>
|
||||
<Input
|
||||
label={(<React.Fragment>Date/Time format<ContextHelp.DateTimeFormatSpecs /></React.Fragment>)}
|
||||
data-test="Table.ColumnEditor.DateTime.Format"
|
||||
defaultValue={column.dateTimeFormat}
|
||||
onChange={event => onChangeDebounced({ dateTimeFormat: event.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Date/Time', () => {
|
||||
dateTimeFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.DateTime.Format').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.DateTime.Format').last().find('input')
|
||||
.simulate('change', { target: { value: 'YYYY/MM/DD HH:ss' } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,7 @@ import { extend, trim } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { Section, Input, ControlLabel, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { formatSimpleTemplate } from '@/lib/value-format';
|
||||
|
||||
function Editor({ column, onChange }) {
|
||||
@@ -12,73 +10,69 @@ function Editor({ column, onChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-image-url`}>URL template</label>
|
||||
<Section>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-image-url`}
|
||||
label="URL template"
|
||||
data-test="Table.ColumnEditor.Image.UrlTemplate"
|
||||
defaultValue={column.imageUrlTemplate}
|
||||
onChange={event => onChangeDebounced({ imageUrlTemplate: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-image-width`}>
|
||||
Size
|
||||
<Popover
|
||||
content="Any positive integer value that specifies size in pixels. Leave empty to use default value."
|
||||
placement="topLeft"
|
||||
arrowPointAtCenter
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-image-width`}
|
||||
data-test="Table.ColumnEditor.Image.Width"
|
||||
placeholder="Width"
|
||||
defaultValue={column.imageWidth}
|
||||
onChange={event => onChangeDebounced({ imageWidth: event.target.value })}
|
||||
/>
|
||||
<span className="p-l-5 p-r-5">×</span>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-image-height`}
|
||||
data-test="Table.ColumnEditor.Image.Height"
|
||||
placeholder="Height"
|
||||
defaultValue={column.imageHeight}
|
||||
onChange={event => onChangeDebounced({ imageHeight: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Section>
|
||||
<ControlLabel
|
||||
label={(
|
||||
<React.Fragment>
|
||||
Size
|
||||
<ContextHelp placement="topLeft" arrowPointAtCenter>
|
||||
<div className="m-b-5">Any positive integer value that specifies size in pixels.</div>
|
||||
<div>Leave empty to use default value.</div>
|
||||
</ContextHelp>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<Input
|
||||
data-test="Table.ColumnEditor.Image.Width"
|
||||
placeholder="Width"
|
||||
defaultValue={column.imageWidth}
|
||||
onChange={event => onChangeDebounced({ imageWidth: event.target.value })}
|
||||
/>
|
||||
<span className="p-l-5 p-r-5">×</span>
|
||||
<Input
|
||||
data-test="Table.ColumnEditor.Image.Height"
|
||||
placeholder="Height"
|
||||
defaultValue={column.imageHeight}
|
||||
onChange={event => onChangeDebounced({ imageHeight: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</ControlLabel>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-image-title`}>Title template</label>
|
||||
<Section>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-image-title`}
|
||||
label="Title template"
|
||||
data-test="Table.ColumnEditor.Image.TitleTemplate"
|
||||
defaultValue={column.imageTitleTemplate}
|
||||
onChange={event => onChangeDebounced({ imageTitleTemplate: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
|
||||
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
|
||||
<div>This syntax is applicable to URL, Title and Size options.</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Section>
|
||||
<ContextHelp
|
||||
placement="topLeft"
|
||||
arrowPointAtCenter
|
||||
icon={(
|
||||
<span style={{ cursor: 'default' }}>
|
||||
Format specs {ContextHelp.defaultIcon}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<span style={{ cursor: 'default' }}>
|
||||
Format specs <Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</span>
|
||||
</Popover>
|
||||
</div>
|
||||
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
|
||||
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
|
||||
<div>This syntax is applicable to URL, Title and Size options.</div>
|
||||
</ContextHelp>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
|
||||
imageUrlTemplate: '{{ @ }}',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.UrlTemplate').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.UrlTemplate').last().find('input')
|
||||
.simulate('change', { target: { value: 'http://{{ @ }}.jpeg' } });
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
|
||||
imageWidth: null,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.Width').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.Width').last().find('input')
|
||||
.simulate('change', { target: { value: '400' } });
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
|
||||
imageHeight: null,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.Height').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.Height').last().find('input')
|
||||
.simulate('change', { target: { value: '300' } });
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('Visualizations -> Table -> Columns -> Image', () => {
|
||||
imageUrlTemplate: '{{ @ }}',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.TitleTemplate').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Image.TitleTemplate').last().find('input')
|
||||
.simulate('change', { target: { value: 'Image {{ @ }}' } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,10 +2,7 @@ import { extend, trim } from 'lodash';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import Checkbox from 'antd/lib/checkbox';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { Section, Input, Checkbox, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { formatSimpleTemplate } from '@/lib/value-format';
|
||||
|
||||
function Editor({ column, onChange }) {
|
||||
@@ -13,65 +10,58 @@ function Editor({ column, onChange }) {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-link-url`}>URL template</label>
|
||||
<Section>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-link-url`}
|
||||
label="URL template"
|
||||
data-test="Table.ColumnEditor.Link.UrlTemplate"
|
||||
defaultValue={column.linkUrlTemplate}
|
||||
onChange={event => onChangeDebounced({ linkUrlTemplate: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-link-text`}>Text template</label>
|
||||
<Section>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-link-text`}
|
||||
label="Text template"
|
||||
data-test="Table.ColumnEditor.Link.TextTemplate"
|
||||
defaultValue={column.linkTextTemplate}
|
||||
onChange={event => onChangeDebounced({ linkTextTemplate: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-link-title`}>Title template</label>
|
||||
<Section>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-link-title`}
|
||||
label="Title template"
|
||||
data-test="Table.ColumnEditor.Link.TitleTemplate"
|
||||
defaultValue={column.linkTitleTemplate}
|
||||
onChange={event => onChangeDebounced({ linkTitleTemplate: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-link-open-in-new-tab`}>
|
||||
<Checkbox
|
||||
id={`table-column-editor-${column.name}-link-open-in-new-tab`}
|
||||
data-test="Table.ColumnEditor.Link.OpenInNewTab"
|
||||
checked={column.linkOpenInNewTab}
|
||||
onChange={event => onChange({ linkOpenInNewTab: event.target.checked })}
|
||||
/>
|
||||
<span>Open in new tab</span>
|
||||
</label>
|
||||
</div>
|
||||
<Section>
|
||||
<Checkbox
|
||||
data-test="Table.ColumnEditor.Link.OpenInNewTab"
|
||||
checked={column.linkOpenInNewTab}
|
||||
onChange={event => onChange({ linkOpenInNewTab: event.target.checked })}
|
||||
>
|
||||
Open in new tab
|
||||
</Checkbox>
|
||||
</Section>
|
||||
|
||||
<div className="m-b-15">
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
|
||||
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
|
||||
<div>This syntax is applicable to URL, Text and Title options.</div>
|
||||
</React.Fragment>
|
||||
)}
|
||||
<Section>
|
||||
<ContextHelp
|
||||
placement="topLeft"
|
||||
arrowPointAtCenter
|
||||
icon={(
|
||||
<span style={{ cursor: 'default' }}>
|
||||
Format specs {ContextHelp.defaultIcon}
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<span style={{ cursor: 'default' }}>
|
||||
Format specs <Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</span>
|
||||
</Popover>
|
||||
</div>
|
||||
<div>All columns can be referenced using <code>{'{{ column_name }}'}</code> syntax.</div>
|
||||
<div>Use <code>{'{{ @ }}'}</code> to reference current (this) column.</div>
|
||||
<div>This syntax is applicable to URL, Text and Title options.</div>
|
||||
</ContextHelp>
|
||||
</Section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
|
||||
linkUrlTemplate: '{{ @ }}',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.UrlTemplate').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.UrlTemplate').last().find('input')
|
||||
.simulate('change', { target: { value: 'http://{{ @ }}/index.html' } });
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
|
||||
linkTextTemplate: '{{ @ }}',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.TextTemplate').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.TextTemplate').last().find('input')
|
||||
.simulate('change', { target: { value: 'Text of {{ @ }}' } });
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
|
||||
linkTitleTemplate: '{{ @ }}',
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.TitleTemplate').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.TitleTemplate').last().find('input')
|
||||
.simulate('change', { target: { value: 'Title of {{ @ }}' } });
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('Visualizations -> Table -> Columns -> Link', () => {
|
||||
linkOpenInNewTab: false,
|
||||
}, done);
|
||||
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.OpenInNewTab').first().find('input')
|
||||
findByTestID(el, 'Table.ColumnEditor.Link.OpenInNewTab').last().find('input')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,21 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Input from 'antd/lib/input';
|
||||
import Popover from 'antd/lib/popover';
|
||||
import Icon from 'antd/lib/icon';
|
||||
import { Section, Input, ContextHelp } from '@/components/visualizations/editor';
|
||||
import { createNumberFormatter } from '@/lib/value-format';
|
||||
|
||||
function Editor({ column, onChange }) {
|
||||
const [onChangeDebounced] = useDebouncedCallback(onChange, 200);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="m-b-15">
|
||||
<label htmlFor={`table-column-editor-${column.name}-number-format`}>
|
||||
Number format
|
||||
<Popover
|
||||
content={(
|
||||
<React.Fragment>
|
||||
Format
|
||||
<a href="https://redash.io/help/user-guide/visualizations/formatting-numbers" target="_blank" rel="noopener noreferrer">specs.</a>
|
||||
</React.Fragment>
|
||||
)}
|
||||
>
|
||||
<Icon className="m-l-5" type="question-circle" theme="filled" />
|
||||
</Popover>
|
||||
</label>
|
||||
<Input
|
||||
id={`table-column-editor-${column.name}-number-format`}
|
||||
data-test="Table.ColumnEditor.Number.Format"
|
||||
defaultValue={column.numberFormat}
|
||||
onChange={event => onChangeDebounced({ numberFormat: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
<Section>
|
||||
<Input
|
||||
label={(<React.Fragment>Number format<ContextHelp.NumberFormatSpecs /></React.Fragment>)}
|
||||
data-test="Table.ColumnEditor.Number.Format"
|
||||
defaultValue={column.numberFormat}
|
||||
onChange={event => onChangeDebounced({ numberFormat: event.target.value })}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user