[explore V2] render all control panels and fields dynamically for each vis type (#1493)

* export functions directly rather than object at the bottom

* move viztypes to controlPanelMappings, add fieldset rows and section data

* for each viz type, render a controlPanelsContainer, controlPanelSections, FieldSetRows, and FieldsSets

* add comments, move mappings to store

* organize store and add default sections

* render all the needed sections

* add tooltip to sections

* remove console log

* use only panel panel-default, not panel-body, no need the padding

* render fields for all fields in field set

* add the rest of the control panel sections and field overrides

* fix naming

* add fieldTypes array

* don't use default section

* pass only needed state via mapStateToProps

* fix code climate errors

* linting

* move field components to their own files

* render field sets as lists

* fix field components

* use SFC

* update modal trigger test to be more accurate

* add FieldSetRow test

* add test for controlpanelsContainer

* fix test

* make code climate happy

* add freeform select field
This commit is contained in:
Alanna Scott 2016-11-02 12:57:44 -07:00 committed by GitHub
parent 1b124bfb87
commit 38d3075554
17 changed files with 2070 additions and 82 deletions

View File

@ -0,0 +1,20 @@
import React, { PropTypes } from 'react';
import { Tooltip, OverlayTrigger } from 'react-bootstrap';
const propTypes = {
label: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
};
export default function InfoTooltipWithTrigger({ label, tooltip }) {
return (
<OverlayTrigger
placement="right"
overlay={<Tooltip id={`${label}-tooltip`}>{tooltip}</Tooltip>}
>
<i className="fa fa-question-circle-o" />
</OverlayTrigger>
);
}
InfoTooltipWithTrigger.propTypes = propTypes;

View File

@ -0,0 +1,24 @@
import React, { PropTypes } from 'react';
import { Checkbox } from 'react-bootstrap';
import ControlLabelWithTooltip from './ControlLabelWithTooltip';
const propTypes = {
label: PropTypes.string,
description: PropTypes.string,
};
const defaultProps = {
label: null,
description: null,
};
export default function CheckboxField({ label, description }) {
return (
<Checkbox>
<ControlLabelWithTooltip label={label} description={description} />
</Checkbox>
);
}
CheckboxField.propTypes = propTypes;
CheckboxField.defaultProps = defaultProps;

View File

@ -0,0 +1,27 @@
import React, { PropTypes } from 'react';
import { ControlLabel } from 'react-bootstrap';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
const propTypes = {
label: PropTypes.string,
description: PropTypes.string,
};
const defaultProps = {
label: null,
description: null,
};
export default function ControlLabelWithTooltip({ label, description }) {
return (
<ControlLabel>
{label} &nbsp;
{description &&
<InfoTooltipWithTrigger label={label} tooltip={description} />
}
</ControlLabel>
);
}
ControlLabelWithTooltip.propTypes = propTypes;
ControlLabelWithTooltip.defaultProps = defaultProps;

View File

@ -0,0 +1,43 @@
import React, { PropTypes } from 'react';
import { Panel } from 'react-bootstrap';
import InfoTooltipWithTrigger from '../../components/InfoTooltipWithTrigger';
const propTypes = {
label: PropTypes.string,
description: PropTypes.string,
tooltip: PropTypes.string,
children: PropTypes.node.isRequired,
};
const defaultProps = {
label: null,
description: null,
tooltip: null,
};
export default class ControlPanelSection extends React.Component {
header() {
const { label, tooltip } = this.props;
let header;
if (label) {
header = (
<div className="panel-title">
{label} &nbsp;
{tooltip && <InfoTooltipWithTrigger label={label} tooltip={tooltip} />}
</div>
);
}
return header;
}
render() {
return (
<Panel header={this.header()}>
{this.props.children}
</Panel>
);
}
}
ControlPanelSection.propTypes = propTypes;
ControlPanelSection.defaultProps = defaultProps;

View File

@ -1,27 +1,62 @@
import React from 'react';
import React, { PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import * as actions from '../actions/exploreActions';
import { connect } from 'react-redux';
import { Panel } from 'react-bootstrap';
import { DefaultControls, VIZ_CONTROL_MAPPING } from '../constants';
import { visTypes, commonControlPanelSections } from '../stores/store';
import ControlPanelSection from './ControlPanelSection';
import FieldSetRow from './FieldSetRow';
const propTypes = {
vizType: React.PropTypes.string,
vizType: PropTypes.string,
datasourceId: PropTypes.number.isRequired,
datasourceType: PropTypes.string.isRequired,
actions: PropTypes.object.isRequired,
};
const defaultProps = {
vizType: null,
};
function ControlPanelsContainer(props) {
return (
<Panel>
<div className="scrollbar-container">
<div className="scrollbar-content">
{DefaultControls}
{VIZ_CONTROL_MAPPING[props.vizType]}
function getSectionsToRender(vizSections) {
const { datasourceAndVizType, sqlClause } = commonControlPanelSections;
const sectionsToRender = [datasourceAndVizType].concat(vizSections, sqlClause);
return sectionsToRender;
}
class ControlPanelsContainer extends React.Component {
componentWillMount() {
const { datasourceId, datasourceType } = this.props;
if (datasourceId) {
this.props.actions.setFormOpts(datasourceId, datasourceType);
}
}
render() {
const viz = visTypes[this.props.vizType];
const sectionsToRender = getSectionsToRender(viz.controlPanelSections);
return (
<Panel>
<div className="scrollbar-container">
<div className="scrollbar-content">
{sectionsToRender.map((section) => (
<ControlPanelSection
key={section.label}
label={section.label}
tooltip={section.description}
>
{section.fieldSetRows.map((fieldSets, i) => (
<FieldSetRow key={`${section.label}-fieldSetRow-${i}`} fieldSets={fieldSets} />
))}
</ControlPanelSection>
))}
{/* TODO: add filters section */}
</div>
</div>
</div>
</Panel>
);
</Panel>
);
}
}
ControlPanelsContainer.propTypes = propTypes;
@ -29,12 +64,18 @@ ControlPanelsContainer.defaultProps = defaultProps;
function mapStateToProps(state) {
return {
datasourceId: state.datasourceId,
datasourceType: state.datasourceType,
vizType: state.viz.formData.vizType,
};
}
function mapDispatchToProps() {
return {};
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch),
};
}
export { ControlPanelsContainer };
export default connect(mapStateToProps, mapDispatchToProps)(ControlPanelsContainer);

View File

@ -0,0 +1,63 @@
import React, { PropTypes } from 'react';
import TextField from './TextField';
import CheckboxField from './CheckboxField';
import TextAreaField from './TextAreaField';
import SelectField from './SelectField';
import { fieldTypes } from '../stores/store';
const propTypes = {
type: PropTypes.oneOf(fieldTypes).isRequired,
label: PropTypes.string.isRequired,
choices: PropTypes.arrayOf(PropTypes.array),
description: PropTypes.string,
places: PropTypes.number,
validators: PropTypes.any,
};
const defaultProps = {
choices: null,
description: null,
places: null,
validators: null,
};
export default class FieldSet extends React.Component {
renderCheckBoxField() {
return <CheckboxField label={this.props.label} description={this.props.description} />;
}
renderTextAreaField() {
return <TextAreaField label={this.props.label} description={this.props.description} />;
}
renderSelectField() {
return <SelectField label={this.props.label} description={this.props.description} />;
}
renderTextField() {
return <TextField label={this.props.label} description={this.props.description} />;
}
render() {
const type = this.props.type;
let html;
if (type === 'CheckboxField') {
html = this.renderCheckBoxField();
} else if (type === 'SelectField' ||
type === 'SelectCustomMultiField' ||
type === 'SelectMultipleSortableField' ||
type === 'FreeFormSelectField') {
html = this.renderSelectField();
} else if (type === 'TextField' || type === 'IntegerField') {
html = this.renderTextField();
} else if (type === 'TextAreaField') {
this.renderTextAreaField();
}
return html;
}
}
FieldSet.propTypes = propTypes;
FieldSet.defaultProps = defaultProps;

View File

@ -0,0 +1,17 @@
import React, { PropTypes } from 'react';
import FieldSet from './FieldSet';
import { fields } from '../stores/store';
const propTypes = {
fieldSets: PropTypes.array.isRequired,
};
export default function FieldSetRow({ fieldSets }) {
return (
<ul className="list-unstyled">
{fieldSets.map((fs) => <li key={fs}><FieldSet {...fields[fs]} /></li>)}
</ul>
);
}
FieldSetRow.propTypes = propTypes;

View File

@ -0,0 +1,28 @@
import React, { PropTypes } from 'react';
import { FormGroup, FormControl } from 'react-bootstrap';
import ControlLabelWithTooltip from './ControlLabelWithTooltip';
const propTypes = {
label: PropTypes.string,
description: PropTypes.string,
};
const defaultProps = {
label: null,
description: null,
};
export default function SelectField({ label, description }) {
return (
<FormGroup controlId={`formControlsSelect-${label}`}>
<ControlLabelWithTooltip label={label} description={description} />
<FormControl componentClass="select" placeholder="select">
<option value="select">select</option>
<option value="other">...</option>
</FormControl>
</FormGroup>
);
}
SelectField.propTypes = propTypes;
SelectField.defaultProps = defaultProps;

View File

@ -0,0 +1,25 @@
import React, { PropTypes } from 'react';
import { FormGroup, FormControl } from 'react-bootstrap';
import ControlLabelWithTooltip from './ControlLabelWithTooltip';
const propTypes = {
label: PropTypes.string,
description: PropTypes.string,
};
const defaultProps = {
label: null,
description: null,
};
export default function TextAreaField({ label, description }) {
return (
<FormGroup controlId="formControlsTextarea">
<ControlLabelWithTooltip label={label} description={description} />
<FormControl componentClass="textarea" placeholder="textarea" />
</FormGroup>
);
}
TextAreaField.propTypes = propTypes;
TextAreaField.defaultProps = defaultProps;

View File

@ -0,0 +1,25 @@
import React, { PropTypes } from 'react';
import { FormGroup, FormControl } from 'react-bootstrap';
import ControlLabelWithTooltip from './ControlLabelWithTooltip';
const propTypes = {
label: PropTypes.string,
description: PropTypes.string,
};
const defaultProps = {
label: null,
description: null,
};
export default function TextField({ label, description }) {
return (
<FormGroup controlId="formInlineName">
<ControlLabelWithTooltip label={label} description={description} />
<FormControl type="text" placeholder="" />
</FormGroup>
);
}
TextField.propTypes = propTypes;
TextField.defaultProps = defaultProps;

View File

@ -7,53 +7,25 @@ import Filters from './components/Filters';
import NotGroupBy from './components/NotGroupBy';
import Options from './components/Options';
export const VIZ_TYPES = [
{ value: 'dist_bar', label: 'Distribution - Bar Chart', requiresTime: false },
{ value: 'pie', label: 'Pie Chart', requiresTime: false },
{ value: 'line', label: 'Time Series - Line Chart', requiresTime: true },
{ value: 'bar', label: 'Time Series - Bar Chart', requiresTime: true },
{ value: 'compare', label: 'Time Series - Percent Change', requiresTime: true },
{ value: 'area', label: 'Time Series - Stacked', requiresTime: true },
{ value: 'table', label: 'Table View', requiresTime: false },
{ value: 'markup', label: 'Markup', requiresTime: false },
{ value: 'pivot_table', label: 'Pivot Table', requiresTime: false },
{ value: 'separator', label: 'Separator', requiresTime: false },
{ value: 'word_cloud', label: 'Word Cloud', requiresTime: false },
{ value: 'treemap', label: 'Treemap', requiresTime: false },
{ value: 'cal_heatmap', label: 'Calendar Heatmap', requiresTime: true },
{ value: 'box_plot', label: 'Box Plot', requiresTime: false },
{ value: 'bubble', label: 'Bubble Chart', requiresTime: false },
{ value: 'big_number', label: 'Big Number with Trendline', requiresTime: false },
{ value: 'bubble', label: 'Bubble Chart', requiresTime: false },
{ value: 'histogram', label: 'Histogram', requiresTime: false },
{ value: 'sunburst', label: 'Sunburst', requiresTime: false },
{ value: 'sankey', label: 'Sankey', requiresTime: false },
{ value: 'directed_force', label: 'Directed Force Layout', requiresTime: false },
{ value: 'world_map', label: 'World Map', requiresTime: false },
{ value: 'filter_box', label: 'Filter Box', requiresTime: false },
{ value: 'iframe', label: 'iFrame', requiresTime: false },
{ value: 'para', label: 'Parallel Coordinates', requiresTime: false },
{ value: 'heatmap', label: 'Heatmap', requiresTime: false },
{ value: 'horizon', label: 'Horizon', requiresTime: false },
{ value: 'mapbox', label: 'Mapbox', requiresTime: false },
export const sinceOptions = [
'1 hour ago',
'12 hours ago',
'1 day ago',
'7 days ago',
'28 days ago',
'90 days ago',
'1 year ago',
];
export const sinceOptions = ['1 hour ago', '12 hours ago', '1 day ago',
'7 days ago', '28 days ago', '90 days ago', '1 year ago'];
export const untilOptions = ['now', '1 day ago', '7 days ago',
'28 days ago', '90 days ago', '1 year ago'];
export const timestampOptions = [
['smart_date', 'Adaptative formating'],
['%m/%d/%Y', '"%m/%d/%Y" | 01/14/2019'],
['%Y-%m-%d', '"%Y-%m-%d" | 2019-01-14'],
['%Y-%m-%d %H:%M:%S',
'"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'],
['%H:%M:%S', '"%H:%M:%S" | 01:32:10'],
export const untilOptions = [
'now',
'1 day ago',
'7 days ago',
'28 days ago',
'90 days ago',
'1 year ago',
];
export const rowLimitOptions = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];
export const DefaultControls = (
<div>
<ChartControl />

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import vizMap from '../../visualizations/main.js';
const px = function () {
let slice;
function getParam(name) {
/* eslint no-useless-escape: 0 */
const formattedName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + formattedName + '=([^&#]*)');
const results = regex.exec(location.search);

View File

@ -1,12 +1,13 @@
const d3 = require('d3');
const $ = require('jquery');
/*
Utility function that takes a d3 svg:text selection and a max width, and splits the
text's text across multiple tspan lines such that any given line does not exceed max width
If text does not span multiple lines AND adjustedY is passed,
will set the text to the passed val
*/
/*
Utility function that takes a d3 svg:text selection and a max width, and splits the
text's text across multiple tspan lines such that any given line does not exceed max width
If text does not span multiple lines AND adjustedY is passed,
will set the text to the passed val
*/
export function wrapSvgText(text, width, adjustedY) {
const lineHeight = 1;
// ems
@ -46,6 +47,7 @@ export function wrapSvgText(text, width, adjustedY) {
}
});
}
/**
* Sets the body and title content of a modal, and shows it. Assumes HTML for modal exists and that
* it handles closing (i.e., works with bootstrap)
@ -59,7 +61,7 @@ export function wrapSvgText(text, width, adjustedY) {
* bodySelector: {string, default: '.misc-modal .modal-body' },
* }
*/
function showModal(options) {
export function showModal(options) {
/* eslint no-param-reassign: 0 */
options.modalSelector = options.modalSelector || '.misc-modal';
options.titleSelector = options.titleSelector || '.misc-modal .modal-title';
@ -68,7 +70,9 @@ function showModal(options) {
$(options.bodySelector).html(options.body || '');
$(options.modalSelector).modal('show');
}
const showApiMessage = function (resp) {
function showApiMessage(resp) {
const template =
'<div class="alert"> ' +
'<button type="button" class="close" ' +
@ -77,8 +81,9 @@ const showApiMessage = function (resp) {
$(template).addClass('alert-' + severity)
.append(resp.message)
.appendTo($('#alert-container'));
};
const toggleCheckbox = function (apiUrlPrefix, selector) {
}
export function toggleCheckbox(apiUrlPrefix, selector) {
const apiUrl = apiUrlPrefix + $(selector)[0].checked;
$.get(apiUrl).fail(function (xhr) {
const resp = xhr.responseJSON;
@ -86,16 +91,18 @@ const toggleCheckbox = function (apiUrlPrefix, selector) {
showApiMessage(resp);
}
});
};
}
/**
* Fix the height of the table body of a DataTable with scrollY set
*/
const fixDataTableBodyHeight = function ($tableDom, height) {
export const fixDataTableBodyHeight = function ($tableDom, height) {
const headHeight = $tableDom.find('.dataTables_scrollHead').height();
$tableDom.find('.dataTables_scrollBody').css('max-height', height - headHeight);
};
const formatters = {};
function d3format(format, number) {
export function d3format(format, number) {
const formatters = {};
// Formats a number and memoizes formatters to be reused
format = format || '.3s';
if (!(format in formatters)) {
@ -106,7 +113,7 @@ function d3format(format, number) {
// Slice objects interact with their context through objects that implement
// this controllerInterface (dashboard, explore, standalone)
const controllerInterface = {
export const controllerInterface = {
type: null,
done: () => {},
error: () => {},
@ -119,11 +126,19 @@ const controllerInterface = {
filters: {},
};
module.exports = {
controllerInterface,
d3format,
fixDataTableBodyHeight,
showModal,
toggleCheckbox,
wrapSvgText,
};
export function formatSelectOptionsForRange(start, end) {
// outputs array of arrays
// formatSelectOptionsForRange(1, 5)
// returns [[1,1], [2,2], [3,3], [4,4], [5,5]]
const options = [];
for (let i = start; i <= end; i++) {
options.push([i, i]);
}
return options;
}
export function formatSelectOptions(options) {
return options.map((opt) =>
[opt, opt]
);
}

View File

@ -11,7 +11,7 @@ describe('ModalTrigger', () => {
modalBody: <div>Modal Body</div>,
};
it('renders', () => {
it('is a valid element', () => {
expect(
React.isValidElement(<ModalTrigger {...defaultProps} />)
).to.equal(true);

View File

@ -0,0 +1,32 @@
import React from 'react';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { shallow } from 'enzyme';
import { Panel } from 'react-bootstrap';
import {
ControlPanelsContainer,
} from '../../../../javascripts/explorev2/components/ControlPanelsContainer';
const defaultProps = {
vizType: 'dist_bar',
datasourceId: 1,
datasourceType: 'type',
actions: {
setFormOpts: () => {
// noop
},
},
};
describe('ControlPanelsContainer', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<ControlPanelsContainer {...defaultProps} />);
});
it('renders a Panel', () => {
expect(wrapper.find(Panel)).to.have.lengthOf(1);
});
});

View File

@ -0,0 +1,27 @@
import React from 'react';
import { expect } from 'chai';
import { describe, it, beforeEach } from 'mocha';
import { shallow } from 'enzyme';
import FieldSetRow from '../../../../javascripts/explorev2/components/FieldSetRow';
const defaultProps = {
fieldSets: ['columns', 'metrics'],
};
describe('FieldSetRow', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<FieldSetRow {...defaultProps} />);
});
it('renders a single <ul>', () => {
expect(wrapper.find('ul')).to.have.lengthOf(1);
});
it('renders a <li> for each item in fieldSets array', () => {
const length = defaultProps.fieldSets.length;
expect(wrapper.find('li')).to.have.lengthOf(length);
});
});