forked from getredash/redash
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Migrate Counter visualization to React (getredash#4106)
* Migrate Counter to React: Renderer * Migrate Counter to React: Editor * Cleanup * Review and fix rows indexing algorithm * Counter not properly scaled in editor * Fix wrong label for/input id pair * Tests * Tests * Fix vendor prefixes * Remove unnecessary useEffect dependencies * Update tests * Fix Percy snapshot names
- Loading branch information
1 parent
3ada213
commit 47bc5df
Showing
11 changed files
with
716 additions
and
364 deletions.
There are no files selected for viewing
45 changes: 0 additions & 45 deletions
45
client/app/assets/less/inc/visualizations/counter-render.less
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}> | ||
<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; |
Oops, something went wrong.