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 Pivot Table visualization to React #4133

Merged
merged 14 commits into from
Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
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
27 changes: 6 additions & 21 deletions client/app/components/Filters.jsx
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';
Copy link
Collaborator

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?

Copy link
Member Author

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.

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###';
Expand Down Expand Up @@ -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;
Expand All @@ -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 (
Expand All @@ -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"
Copy link
Member Author

Choose a reason for hiding this comment

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

Took the opportunity to simplify this

showSearch
onChange={values => onChange(filter, values)}
>
Expand Down
16 changes: 16 additions & 0 deletions client/app/filters/index.js
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',
Expand Down Expand Up @@ -168,3 +169,18 @@ export function join(arr) {

return arr.join(' / ');
}

export function formatColumnValue(value, columnType = null) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Just moved this from Filters.jsx. This seemed the best place to put it, LMK if you have somewhere else in mind.

if (moment.isMoment(value)) {
if (columnType === 'date') {
return formatDate(value);
}
return formatDateTime(value);
}

if (typeof value === 'boolean') {
return value.toString();
}

return value;
}
63 changes: 63 additions & 0 deletions client/app/visualizations/pivot/Renderer.jsx
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: () => {} };
6 changes: 3 additions & 3 deletions client/app/visualizations/pivot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import angular from 'angular';
import $ from 'jquery';
import 'pivottable';
import 'pivottable/dist/pivot.css';
import { angular2react } from 'angular2react';
import { registerVisualization } from '@/visualizations';

import './pivot.less';

import Renderer from './Renderer';
import Editor from './Editor';

const DEFAULT_OPTIONS = {
Expand Down Expand Up @@ -67,12 +67,12 @@ const PivotTableRenderer = {
export default function init(ngModule) {
ngModule.component('pivotTableRenderer', PivotTableRenderer);

ngModule.run(($injector) => {
ngModule.run(() => {
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
registerVisualization({
type: 'PIVOT',
name: 'Pivot Table',
getOptions: options => merge({}, DEFAULT_OPTIONS, options),
Renderer: angular2react('pivotTableRenderer', PivotTableRenderer, $injector),
Renderer,
Editor,

defaultRows: 10,
Expand Down
99 changes: 99 additions & 0 deletions client/app/visualizations/pivot/renderer.less
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 {
Copy link
Member Author

Choose a reason for hiding this comment

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

the :not(:empty) here avoids us to hide this cell (that I actually have no idea why it now has .pvtTotalLabel):

image

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;
}
}
Loading