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

Migrate Counter visualization to React #4106

Merged
merged 12 commits into from
Sep 9, 2019
45 changes: 0 additions & 45 deletions client/app/assets/less/inc/visualizations/counter-render.less

This file was deleted.

1 change: 0 additions & 1 deletion client/app/assets/less/main.less
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@
@import 'inc/schema-browser';
@import 'inc/toast';
@import 'inc/visualizations/box';
@import 'inc/visualizations/counter-render';
@import 'inc/visualizations/sankey';
@import 'inc/visualizations/pivot-table';
@import 'inc/visualizations/map';
Expand Down
4 changes: 2 additions & 2 deletions client/app/pages/dashboards/dashboard.less
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
overflow: hidden;
}

counter {
.counter-visualization-content {
position: absolute;
left: 10px;
top: 15px;
Expand Down Expand Up @@ -145,7 +145,7 @@

.dashboard__control {
margin: 8px 0;

.save-status {
vertical-align: middle;
margin-right: 7px;
Expand Down
236 changes: 236 additions & 0 deletions client/app/visualizations/counter/Editor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import { merge, map } from 'lodash';
import React from 'react';
import Tabs from 'antd/lib/tabs';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
import Select from 'antd/lib/select';
import Switch from 'antd/lib/switch';
import * as Grid from 'antd/lib/grid';
import { EditorPropTypes } from '@/visualizations';

import { isValueNumber } from './utils';

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>

<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>

<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>

<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>

<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>

<label className="d-flex align-items-center" htmlFor="counter-count-rows">
<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>
</React.Fragment>
);
}

GeneralSettings.propTypes = EditorPropTypes;

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>

<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>

<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>

<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>

<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>

<label className="d-flex align-items-center" htmlFor="counter-format-target-value">
<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>
</React.Fragment>
);
}

FormatSettings.propTypes = EditorPropTypes;

export default function Editor(props) {
const { options, onOptionsChange } = props;

const optionsChanged = (newOptions) => {
onOptionsChange(merge({}, options, newOptions));
};

return (
<Tabs animated={false}>
<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;
Copy link
Contributor

Choose a reason for hiding this comment

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

@arikfr wdyt of having a link to the counter video from here?
(Actually, linking to a help doc or blog post with the video in it would be better)

Copy link
Member

Choose a reason for hiding this comment

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

I agree that having a help trigger here makes sense. 👍 But I guess it should be a global thing for all visualizations, which we enable when we have a relevant page?

Copy link
Contributor

Choose a reason for hiding this comment

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

73 changes: 73 additions & 0 deletions client/app/visualizations/counter/Renderer.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { isFinite } from 'lodash';
import React, { useState, useEffect } from 'react';
import cx from 'classnames';
import resizeObserver from '@/services/resizeObserver';
import { RendererPropTypes } from '@/visualizations';

import { getCounterData } from './utils';

import './render.less';

function getCounterStyles(scale) {
return {
msTransform: `scale(${scale})`,
MozTransform: `scale(${scale})`,
WebkitTransform: `scale(${scale})`,
transform: `scale(${scale})`,
};
}

function getCounterScale(container) {
const inner = container.firstChild;
const scale = Math.min(container.offsetWidth / inner.offsetWidth, container.offsetHeight / inner.offsetHeight);
return Number(isFinite(scale) ? scale : 1).toFixed(2); // keep only two decimal places
}

export default function Renderer({ data, options, visualizationName }) {
const [scale, setScale] = useState('1.00');
const [container, setContainer] = useState(null);

useEffect(() => {
if (container) {
const unwatch = resizeObserver(container, () => {
setScale(getCounterScale(container));
});
return unwatch;
}
}, [container]);

useEffect(() => {
if (container) {
// update scaling when options or data change (new formatting, values, etc.
// may change inner container dimensions which will not be tracked by `resizeObserver`);
setScale(getCounterScale(container));
}
}, [data, options, container]);

const {
showTrend, trendPositive,
counterValue, counterValueTooltip,
targetValue, targetValueTooltip,
counterLabel,
} = getCounterData(data.rows, options, visualizationName);
return (
<div
className={cx(
'counter-visualization-container',
{ 'trend-positive': showTrend && trendPositive, 'trend-negative': showTrend && !trendPositive },
)}
>
<div className="counter-visualization-content" ref={setContainer}>
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<div style={getCounterStyles(scale)}>
<div className="counter-visualization-value" title={counterValueTooltip}>{counterValue}</div>
{targetValue && (
<div className="counter-visualization-target" title={targetValueTooltip}>({targetValue})</div>
)}
<div className="counter-visualization-label">{counterLabel}</div>
</div>
</div>
</div>
);
}

Renderer.propTypes = RendererPropTypes;
Loading