-
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 Pivot Table visualization to React #4133
Changes from 4 commits
324ea07
489c8d9
09cf7a4
bace703
c82a4c6
4032c1c
9a3c74b
dd43500
26090dd
b7b325e
88096e2
d8a9a89
c5bb351
842e7a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
import { isArray, indexOf, get, map, includes, every, some, toNumber, toLower } from 'lodash'; | ||
import { isArray, indexOf, get, map, includes, every, some, toNumber } from 'lodash'; | ||
import moment from 'moment'; | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { react2angular } from 'react2angular'; | ||
import Select from 'antd/lib/select'; | ||
import { formatDateTime, formatDate } from '@/filters/datetime'; | ||
import { formatColumnValue } from '@/filters'; | ||
|
||
const ALL_VALUES = '###Redash::Filters::SelectAll###'; | ||
const NONE_VALUES = '###Redash::Filters::Clear###'; | ||
|
@@ -71,21 +71,6 @@ export function filterData(rows, filters = []) { | |
return result; | ||
} | ||
|
||
function formatValue(value, columnType) { | ||
if (moment.isMoment(value)) { | ||
if (columnType === 'date') { | ||
return formatDate(value); | ||
} | ||
return formatDateTime(value); | ||
} | ||
|
||
if (typeof value === 'boolean') { | ||
return value.toString(); | ||
} | ||
|
||
return value; | ||
} | ||
|
||
export function Filters({ filters, onChange }) { | ||
if (filters.length === 0) { | ||
return null; | ||
|
@@ -99,7 +84,7 @@ export function Filters({ filters, onChange }) { | |
<div className="row"> | ||
{map(filters, (filter) => { | ||
const options = map(filter.values, (value, index) => ( | ||
<Select.Option key={index}>{formatValue(value, get(filter, 'column.type'))}</Select.Option> | ||
<Select.Option key={index}>{formatColumnValue(value, get(filter, 'column.type'))}</Select.Option> | ||
)); | ||
|
||
return ( | ||
|
@@ -115,10 +100,10 @@ export function Filters({ filters, onChange }) { | |
mode={filter.multiple ? 'multiple' : 'default'} | ||
value={isArray(filter.current) ? | ||
map(filter.current, | ||
value => ({ key: `${indexOf(filter.values, value)}`, label: formatValue(value) })) : | ||
({ key: `${indexOf(filter.values, filter.current)}`, label: formatValue(filter.current) })} | ||
value => ({ key: `${indexOf(filter.values, value)}`, label: formatColumnValue(value) })) : | ||
({ key: `${indexOf(filter.values, filter.current)}`, label: formatColumnValue(filter.current) })} | ||
allowClear={filter.multiple} | ||
filterOption={(searchText, option) => includes(toLower(option.props.children), toLower(searchText))} | ||
optionFilterProp="children" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Took the opportunity to simplify this |
||
showSearch | ||
onChange={values => onChange(filter, values)} | ||
> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import moment from 'moment'; | ||
import { capitalize as _capitalize, isEmpty } from 'lodash'; | ||
import { formatDate, formatDateTime } from './datetime'; | ||
|
||
export const IntervalEnum = { | ||
NEVER: 'Never', | ||
|
@@ -168,3 +169,18 @@ export function join(arr) { | |
|
||
return arr.join(' / '); | ||
} | ||
|
||
export function formatColumnValue(value, columnType = null) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just moved this from |
||
if (moment.isMoment(value)) { | ||
if (columnType === 'date') { | ||
return formatDate(value); | ||
} | ||
return formatDateTime(value); | ||
} | ||
|
||
if (typeof value === 'boolean') { | ||
return value.toString(); | ||
} | ||
|
||
return value; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import React, { useState, useEffect } from 'react'; | ||
import { get, find, pick, map, mapValues } from 'lodash'; | ||
import PivotTableUI from 'react-pivottable/PivotTableUI'; | ||
import { RendererPropTypes } from '@/visualizations'; | ||
import { formatColumnValue } from '@/filters'; | ||
|
||
import 'react-pivottable/pivottable.css'; | ||
import './renderer.less'; | ||
|
||
const VALID_OPTIONS = [ | ||
'data', | ||
'rows', | ||
'cols', | ||
'vals', | ||
'aggregatorName', | ||
'valueFilter', | ||
'sorters', | ||
'rowOrder', | ||
'colOrder', | ||
'derivedAttributes', | ||
'rendererName', | ||
'hiddenAttributes', | ||
'hiddenFromAggregators', | ||
'hiddenFromDragDrop', | ||
'menuLimit', | ||
'unusedOrientationCutoff', | ||
'rendererOptions', | ||
]; | ||
|
||
function formatRows({ rows, columns }) { | ||
return map(rows, row => mapValues(row, (value, key) => formatColumnValue(value, find(columns, { name: key }).type))); | ||
} | ||
|
||
export default function Renderer({ data, options, onOptionsChange }) { | ||
const [config, setConfig] = useState({ data: formatRows(data), ...options }); | ||
|
||
useEffect(() => { | ||
setConfig({ data: formatRows(data), ...options }); | ||
}, [data]); | ||
|
||
const onChange = (updatedOptions) => { | ||
const validOptions = pick(updatedOptions, VALID_OPTIONS); | ||
setConfig(validOptions); | ||
onOptionsChange(validOptions); | ||
}; | ||
|
||
const hideControls = get(options, 'controls.enabled'); | ||
const hideRowTotals = !get(options, 'rendererOptions.table.rowTotals'); | ||
const hideColumnTotals = !get(options, 'rendererOptions.table.colTotals'); | ||
return ( | ||
<div | ||
className="pivot-table-renderer" | ||
data-hide-controls={hideControls || null} | ||
data-hide-row-totals={hideRowTotals || null} | ||
data-hide-column-totals={hideColumnTotals || null} | ||
> | ||
<PivotTableUI {...pick(config, VALID_OPTIONS)} onChange={onChange} /> | ||
</div> | ||
); | ||
} | ||
|
||
Renderer.propTypes = RendererPropTypes; | ||
Renderer.defaultProps = { onOptionsChange: () => {} }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
@redash-gray: rgba(102, 136, 153, 1); | ||
|
||
// TODO: Change .pivot-table-renderer to .pivot-table-visualization-container | ||
.pivot-table-renderer { | ||
&[data-hide-controls] { | ||
.pvtAxisContainer, .pvtRenderers, .pvtVals { | ||
display: none; | ||
} | ||
} | ||
|
||
&[data-hide-row-totals] { | ||
td:last-child, th:last-child { | ||
&.pvtTotalLabel:not(:empty), &.pvtTotal, &.pvtGrandTotal { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
display: none; | ||
} | ||
} | ||
} | ||
|
||
&[data-hide-column-totals] { | ||
tbody > tr:last-child { | ||
& > .pvtTotalLabel, & > .pvtTotal, & > .pvtGrandTotal { | ||
display: none; | ||
} | ||
} | ||
} | ||
} | ||
|
||
.pvtAxisContainer, .pvtVals { | ||
border: 1px solid fade(@redash-gray, 15%); | ||
background: #fff; | ||
} | ||
|
||
.pvtUi { | ||
td, th { | ||
padding: 5px; | ||
} | ||
|
||
li.ui-sortable-handle { | ||
padding: 5px 5px 5px 0; | ||
} | ||
} | ||
|
||
.pvtAxisContainer li span.pvtAttr { | ||
background: fade(@redash-gray, 10%); | ||
border: 1px solid fade(@redash-gray, 15%); | ||
border-radius: 3px; | ||
} | ||
|
||
.pvtCheckContainer { | ||
border-top: 1px solid fade(@redash-gray, 15%); | ||
border-bottom: 1px solid fade(@redash-gray, 15%); | ||
} | ||
|
||
.pvtCheckContainer p { | ||
margin: 2px; | ||
line-height: 1; | ||
} | ||
|
||
.pvtTriangle { | ||
color: fade(@redash-gray, 90%); | ||
} | ||
|
||
table.pvtTable thead tr th, table.pvtTable tbody tr th { | ||
background-color: fade(@redash-gray, 10%); | ||
border: 1px solid #ced8dc; | ||
} | ||
|
||
table.pvtTable tbody tr td { | ||
border: 1px solid #ced8dc; | ||
} | ||
|
||
.pvtFilterBox { | ||
border: 1px solid fade(@redash-gray, 15%); | ||
border-radius: 3px; | ||
box-shadow: fade(@redash-gray, 15%) 0px 4px 9px -3px; | ||
|
||
button { | ||
background-color: rgba(102, 136, 153, 0.15); | ||
margin-right: 5px; | ||
border: 1px solid transparent; | ||
padding: 3px 6px; | ||
font-size: 13px; | ||
line-height: 1.42857143; | ||
border-radius: 3px; | ||
|
||
&:hover { | ||
background-color: rgba(102, 136, 153, 0.25); | ||
} | ||
} | ||
|
||
input[type='text'] { | ||
width: 90%; | ||
margin: 0 auto 10px; | ||
height: 35px; | ||
padding: 6px 12px; | ||
border: 1px solid #e8e8e8; | ||
border-radius: 3px; | ||
} | ||
} |
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.
Is this related to this PR?
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.
Yes, I faced an issue with the react version, it doesn't handle Moment values as the jQuery version does. The jQuery tries to stringfy it, while the react version just breaks the whole thing.
So I was thinking about moving the logic we have to normalize row values for Filters somewhere shared and use it here as well.