Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Set of improvements and refinements to visualizations after React migration #4382

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 48 additions & 39 deletions client/app/components/ColorPicker/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -61,46 +62,50 @@ export default function ColorPicker({
}, [validatedColor, visible]);

return (
<Popover
arrowPointAtCenter
destroyTooltipOnHide
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}
<React.Fragment>
{addonBefore}
<Popover
arrowPointAtCenter
destroyTooltipOnHide
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>
);
}

Expand All @@ -119,6 +124,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,
};

Expand All @@ -130,6 +137,8 @@ ColorPicker.defaultProps = {
interactive: false,
triggerProps: {},
children: null,
addonBefore: null,
addonAfter: null,
onChange: () => {},
};

Expand Down
41 changes: 41 additions & 0 deletions client/app/components/TextAlignmentSelect/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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 (
<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>
);
}

TextAlignmentSelect.propTypes = {
className: PropTypes.string,
};

TextAlignmentSelect.defaultProps = {
className: null,
};
13 changes: 13 additions & 0 deletions client/app/components/TextAlignmentSelect/index.less
Original file line number Diff line number Diff line change
@@ -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;
}
}
28 changes: 26 additions & 2 deletions client/app/components/visualizations/editor/Section.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,40 @@ import React from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';

export default function Section({ className, ...props }) {
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} />
<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;
31 changes: 31 additions & 0 deletions client/app/components/visualizations/editor/Switch.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import AntSwitch from 'antd/lib/switch';

export default function Switch({ id, children, ...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} {...props} />
<span className="m-l-10 m-r-10">{children}</span>
</label>
);
}

return (
<AntSwitch {...props} />
);
}

Switch.propTypes = {
id: PropTypes.string,
children: PropTypes.node,
};

Switch.defaultProps = {
id: null,
children: null,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.visualization-editor-control-label {
&.visualization-editor-control-label-horizontal {
label {
margin-bottom: 0;
}
}
}
30 changes: 30 additions & 0 deletions client/app/components/visualizations/editor/index.js
Original file line number Diff line number Diff line change
@@ -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);
75 changes: 75 additions & 0 deletions client/app/components/visualizations/editor/withControlLabel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 './control-label.less';

// TODO: Control label should reflect disabled state
// <Typography.Text disabled={disabled}>label text</Typography.Text>

export function ControlLabel({ layout, label, labelProps, children }) {
if ((layout === 'vertical') && label) {
return (
<div className="visualization-editor-control-label visualization-editor-control-label-vertical">
<label {...labelProps}>{label}</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}>{label}</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
children: PropTypes.node,
};

ControlLabel.defaultProps = {
layout: 'vertical',
label: null,
children: null,
};

export default function withControlLabel(WrappedControl) {
// eslint-disable-next-line react/prop-types
function ControlWrapper({ id, layout, label, labelProps, ...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}>
<WrappedControl id={labelProps.htmlFor} {...props} />
</ControlLabel>
);
}

// Copy static methods from `WrappedComponent`
hoistNonReactStatics(ControlWrapper, WrappedControl);

return ControlWrapper;
}
9 changes: 3 additions & 6 deletions client/app/visualizations/box-plot/Editor.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import Input from 'antd/lib/input';
import Section from '@/components/visualizations/editor/Section';
import { Section, Input } from '@/components/visualizations/editor';
import { EditorPropTypes } from '@/visualizations';

export default function Editor({ options, onOptionsChange }) {
Expand All @@ -17,20 +16,18 @@ export default function Editor({ options, onOptionsChange }) {
return (
<React.Fragment>
<Section>
<label className="control-label" htmlFor="box-plot-x-axis-label">X Axis Label</label>
<Input
label="X Axis Label"
data-test="BoxPlot.XAxisLabel"
id="box-plot-x-axis-label"
value={options.xAxisLabel}
onChange={event => onXAxisLabelChanged(event.target.value)}
/>
</Section>

<Section>
<label className="control-label" htmlFor="box-plot-y-axis-label">Y Axis Label</label>
<Input
label="Y Axis Label"
data-test="BoxPlot.YAxisLabel"
id="box-plot-y-axis-label"
value={options.yAxisLabel}
onChange={event => onYAxisLabelChanged(event.target.value)}
/>
Expand Down
Loading