-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
114edf3
Migrate Counter to React: Renderer
kravets-levko da8c54c
Migrate Counter to React: Editor
kravets-levko 3024d41
Cleanup
kravets-levko 9e1fc01
Review and fix rows indexing algorithm
kravets-levko b54b23c
Counter not properly scaled in editor
kravets-levko 6e2d11c
Fix wrong label for/input id pair
kravets-levko e4f2d6e
Tests
kravets-levko 7c5de0e
Tests
kravets-levko 0ff8c77
Fix vendor prefixes
kravets-levko f8e374a
Remove unnecessary useEffect dependencies
kravets-levko 7329ab6
Update tests
kravets-levko ef53826
Fix Percy snapshot names
kravets-levko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}> | ||
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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can embed the video here then https://redash.io/help/user-guide/visualizations/visualization-types#Counter