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 Table visualization to React Part 2: Editor #4175

Merged
merged 16 commits into from
Oct 24, 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
86 changes: 86 additions & 0 deletions client/app/visualizations/table/Editor/ColumnEditor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { map, keys } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio';
import Checkbox from 'antd/lib/checkbox';
import Select from 'antd/lib/select';
import Icon from 'antd/lib/icon';

import { ColumnTypes } from '../utils';
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved

export default function ColumnEditor({ column, onChange }) {
function handleChange(changes) {
onChange({ ...column, ...changes });
}

const AdditionalOptions = ColumnTypes[column.displayAs].Editor || null;

return (
<div className="table-visualization-editor-column">
<div className="table-visualization-editor-column-header">
<Input
value={column.title}
onChange={event => handleChange({ title: event.target.value })}
addonBefore={(
<Checkbox
checked={column.visible}
onChange={event => handleChange({ visible: event.target.checked })}
/>
)}
/>
</div>
<Radio.Group
className="table-visualization-editor-column-align-content m-b-15"
value={column.alignContent}
onChange={event => handleChange({ alignContent: event.target.value })}
>
<Radio.Button value="left"><Icon type="align-left" /></Radio.Button>
<Radio.Button value="center"><Icon type="align-center" /></Radio.Button>
<Radio.Button value="right"><Icon type="align-right" /></Radio.Button>
</Radio.Group>

<div className="m-b-15">
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<label htmlFor={`table-column-editor-${column.name}-allow-search`}>
<Checkbox
id={`table-column-editor-${column.name}-allow-search`}
checked={column.allowSearch}
onChange={event => handleChange({ allowSearch: event.target.checked })}
/>
<span>Use for search</span>
</label>
</div>

<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-display-as`}>Display as:</label>
<Select
id={`table-column-editor-${column.name}-display-as`}
className="w-100"
value={column.displayAs}
onChange={displayAs => handleChange({ displayAs })}
>
{map(ColumnTypes, ({ friendlyName }, key) => (
<Select.Option key={key}>{friendlyName}</Select.Option>
))}
</Select>
</div>

{AdditionalOptions && <AdditionalOptions column={column} onChange={handleChange} />}
</div>
);
}

ColumnEditor.propTypes = {
column: PropTypes.shape({
name: PropTypes.string.isRequired,
title: PropTypes.string,
visible: PropTypes.bool,
alignContent: PropTypes.oneOf(['left', 'center', 'right']),
displayAs: PropTypes.oneOf(keys(ColumnTypes)),
}).isRequired,
onChange: PropTypes.func,
};

ColumnEditor.defaultProps = {
onChange: () => {},
};
53 changes: 53 additions & 0 deletions client/app/visualizations/table/Editor/ColumnsSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { map, some } from 'lodash';
import React, { useRef } from 'react';
import { sortableContainer, sortableElement } from 'react-sortable-hoc';
import { EditorPropTypes } from '@/visualizations';

import ColumnEditor from './ColumnEditor';

const SortableContainer = sortableContainer(({ children }) => children);
const SortableItem = sortableElement(({ children }) => children);

function shouldCancelDragStart(event) {
const cx = event.target.classList;
const allowDnD = some(
['table-visualization-editor-column', 'table-visualization-editor-column-header'],
c => cx.contains(c),
);
return !allowDnD;
}

export default function ColumnsSettings({ options, onOptionsChange }) {
const containerRef = useRef();

function handleColumnChange(newColumn) {
const columns = map(options.columns, c => (c.name === newColumn.name ? newColumn : c));
onOptionsChange({ columns });
}

function handleColumnsReorder({ oldIndex, newIndex }) {
const columns = [...options.columns];
columns.splice(newIndex, 0, ...columns.splice(oldIndex, 1));
onOptionsChange({ columns });
}

return (
<SortableContainer
axis="x"
lockAxis="x"
helperContainer={() => containerRef.current}
shouldCancelStart={shouldCancelDragStart}
onSortEnd={handleColumnsReorder}
>
<div ref={containerRef} className="table-visualization-editor-columns">
{map(options.columns, (column, index) => (
<SortableItem key={column.name} index={index}>
<ColumnEditor column={column} onChange={handleColumnChange} />
</SortableItem>
))}
</div>
</SortableContainer>
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
);
}

ColumnsSettings.propTypes = EditorPropTypes;
26 changes: 26 additions & 0 deletions client/app/visualizations/table/Editor/GridSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { map } from 'lodash';
import React from 'react';
import Select from 'antd/lib/select';
import { EditorPropTypes } from '@/visualizations';

const ALLOWED_ITEM_PER_PAGE = [5, 10, 15, 20, 25, 50, 100, 150, 200, 250];

export default function GridSettings({ options, onOptionsChange }) {
return (
<div className="m-b-15">
<label htmlFor="table-editor-items-per-page">Items per page</label>
<Select
id="table-editor-items-per-page"
className="w-100"
defaultValue={options.itemsPerPage}
onChange={itemsPerPage => onOptionsChange({ itemsPerPage })}
>
{map(ALLOWED_ITEM_PER_PAGE, value => (
<Select.Option key={`ipp${value}`} value={value}>{value}</Select.Option>
))}
</Select>
</div>
);
}

GridSettings.propTypes = EditorPropTypes;
42 changes: 42 additions & 0 deletions client/app/visualizations/table/Editor/editor.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.table-visualization-editor-columns {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
overflow: auto;
}

.table-visualization-editor-column {
min-width: 200px;
width: 200px;
padding: 0 10px;
border-right: 1px solid #f0f0f0;
background: #ffffff;
cursor: move;

&:last-child {
border-right: none;
}

.table-visualization-editor-column-header {
background: rgba(102, 136, 153, 0.05);
padding: 10px;
margin-left: -10px;
margin-right: -10px;
margin-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}

.table-visualization-editor-column-align-content {
display: flex;
align-items: stretch;
justify-content: stretch;

.ant-radio-button-wrapper {
flex-grow: 1;
text-align: center;
height: 28px;
line-height: 26px;
}
}
}
30 changes: 30 additions & 0 deletions client/app/visualizations/table/Editor/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { merge } from 'lodash';
import React from 'react';
import Tabs from 'antd/lib/tabs';
import { EditorPropTypes } from '@/visualizations';

import ColumnsSettings from './ColumnsSettings';
import GridSettings from './GridSettings';

import './editor.less';

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

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

return (
<Tabs className="table-editor-container" animated={false} tabBarGutter={0}>
<Tabs.TabPane key="columns" tab={<span data-test="Counter.EditorTabs.General">Columns</span>}>
<ColumnsSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
<Tabs.TabPane key="grid" tab={<span data-test="Counter.EditorTabs.Formatting">Grid</span>}>
<GridSettings {...props} onOptionsChange={optionsChanged} />
</Tabs.TabPane>
</Tabs>
);
}

index.propTypes = EditorPropTypes;
53 changes: 51 additions & 2 deletions client/app/visualizations/table/columns/boolean.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,54 @@
/* eslint-disable react/prop-types */
import React from 'react';
import PropTypes from 'prop-types';
import Input from 'antd/lib/input';
import { createBooleanFormatter } from '@/lib/value-format';

function Editor({ column, onChange }) {
function handleChange(index, value) {
const booleanValues = [...column.booleanValues];
booleanValues.splice(index, 1, value);
onChange({ booleanValues });
}

return (
<React.Fragment>
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
<div className="m-b-15">
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-boolean-false`}>
Value for <code>false</code>
</label>
<Input
id={`table-column-editor-${column.name}-boolean-false`}
defaultValue={column.booleanValues[0]}
onChange={event => handleChange(0, event.target.value)}
/>
</div>
</div>

<div className="m-b-15">
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-boolean-true`}>
Value for <code>true</code>
</label>
<Input
id={`table-column-editor-${column.name}-boolean-true`}
defaultValue={column.booleanValues[1]}
onChange={event => handleChange(1, event.target.value)}
/>
</div>
</div>
</React.Fragment>
);
}

Editor.propTypes = {
column: PropTypes.shape({
name: PropTypes.string.isRequired,
booleanValues: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
onChange: PropTypes.func.isRequired,
};

export default function initBooleanColumn(column) {
const format = createBooleanFormatter(column.booleanValues);

Expand All @@ -10,7 +58,7 @@ export default function initBooleanColumn(column) {
};
}

function BooleanColumn({ row }) {
function BooleanColumn({ row }) { // eslint-disable-line react/prop-types
const { text } = prepareData(row);
return text;
}
Expand All @@ -21,3 +69,4 @@ export default function initBooleanColumn(column) {
}

initBooleanColumn.friendlyName = 'Boolean';
initBooleanColumn.Editor = Editor;
44 changes: 42 additions & 2 deletions client/app/visualizations/table/columns/datetime.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
/* eslint-disable react/prop-types */
import React from 'react';
import PropTypes from 'prop-types';
import Input from 'antd/lib/input';
import Popover from 'antd/lib/popover';
import Icon from 'antd/lib/icon';
import { createDateTimeFormatter } from '@/lib/value-format';

function Editor({ column, onChange }) {
return (
<React.Fragment>
<div className="m-b-15">
<label htmlFor={`table-column-editor-${column.name}-datetime-format`}>
Date/Time format
<Popover
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
content={(
<React.Fragment>
Format&nbsp;
<a href="https://momentjs.com/docs/#/displaying/format/" target="_blank" rel="noopener noreferrer">specs.</a>
kravets-levko marked this conversation as resolved.
Show resolved Hide resolved
</React.Fragment>
)}
>
<Icon className="m-l-5" type="question-circle" theme="filled" />
</Popover>
</label>
<Input
id={`table-column-editor-${column.name}-datetime-format`}
defaultValue={column.dateTimeFormat}
onChange={event => onChange({ dateTimeFormat: event.target.value })}
/>
</div>
</React.Fragment>
);
}

Editor.propTypes = {
column: PropTypes.shape({
name: PropTypes.string.isRequired,
dateTimeFormat: PropTypes.string,
}).isRequired,
onChange: PropTypes.func.isRequired,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Couldn't figure out why but if I create a new query select 1556668800000 as date and then edit the column to be "date/time" it won't react to a format input. I enter "MMM-YYYY" but the viz preview stays the number :/

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Cast it to date/time type so redash will convert it to moment instance. it's quite weird, but changing this behavior is not related to this PR

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohhh yeah that's weird 😒

Copy link
Member

Choose a reason for hiding this comment

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

It's definitely confusing and not how the other types work -- let's open a separate issue for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

PR #4389


export default function initDateTimeColumn(column) {
const format = createDateTimeFormatter(column.dateTimeFormat);

Expand All @@ -10,7 +49,7 @@ export default function initDateTimeColumn(column) {
};
}

function DateTimeColumn({ row }) {
function DateTimeColumn({ row }) { // eslint-disable-line react/prop-types
const { text } = prepareData(row);
return text;
}
Expand All @@ -21,3 +60,4 @@ export default function initDateTimeColumn(column) {
}

initDateTimeColumn.friendlyName = 'Date/Time';
initDateTimeColumn.Editor = Editor;
Loading