Skip to content

Commit

Permalink
Migrate Parameters component to React (getredash#4006)
Browse files Browse the repository at this point in the history
* Start Parameters Migration

* Add dirtyCount

* Use workaround with setState

* Apply Changes

* Add EditSettingsDialog

* Add Cmd/Ctrl + Enter behavior

* Remove isApplying

* Delete Angular version of parameters

* Update tests

* Remove angular stuff

* Update jest

* Drag placeholder

* Update events

* Use old button styling and move css

* Reviewing code

* Add parameter rearrange test

* Add Parameter Settings title change test

* Update Parameter Settings button styling

* Move parameter url logic back to Parameters

* Disable url update when query is new

* Styling changes (getredash#4019)

* Ran's title width styling

* Update drag test

* Improve sizing for Number inputs

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Fix issue with dragged parameter wrapping

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Don't reevaluate dirtyParamCount

* Allow multiple values :)

* Fix parameter alignments

* Fix Select width on search

* Update client/app/components/Parameters.less

Co-Authored-By: Ran Byron <ranbena@gmail.com>

* Humanize param.name

* Make sure angular updates Execute disabled status
  • Loading branch information
gabrieldutra authored and harveyrendell committed Nov 14, 2019
1 parent 192bc18 commit bc21f8c
Show file tree
Hide file tree
Showing 18 changed files with 519 additions and 279 deletions.
1 change: 1 addition & 0 deletions client/app/components/EditParameterSettingsDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function EditParameterSettingsDialog(props) {
<Input
value={isNull(param.title) ? getDefaultTitle(param.name) : param.title}
onChange={e => setParam({ ...param, title: e.target.value })}
data-test="ParameterTitleInput"
/>
</Form.Item>
<Form.Item label="Type" {...formItemProps}>
Expand Down
14 changes: 4 additions & 10 deletions client/app/components/ParameterApplyButton.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Button from 'antd/lib/button';
import Badge from 'antd/lib/badge';
import Tooltip from 'antd/lib/tooltip';
import { KeyboardShortcuts } from '@/services/keyboard-shortcuts';

function ParameterApplyButton({ paramCount, onClick, isApplying }) {
// show spinner when applying (also when count is empty so the fade out is consistent)
const icon = isApplying || !paramCount ? 'spinner fa-pulse' : 'check';
function ParameterApplyButton({ paramCount, onClick }) {
// show spinner when count is empty so the fade out is consistent
const icon = !paramCount ? 'spinner fa-pulse' : 'check';

return (
<div className="parameter-apply-button" data-show={!!paramCount} data-test="ParameterApplyButton">
Expand All @@ -28,11 +27,6 @@ function ParameterApplyButton({ paramCount, onClick, isApplying }) {
ParameterApplyButton.propTypes = {
onClick: PropTypes.func.isRequired,
paramCount: PropTypes.number.isRequired,
isApplying: PropTypes.bool.isRequired,
};

export default function init(ngModule) {
ngModule.component('parameterApplyButton', react2angular(ParameterApplyButton));
}

init.init = true;
export default ParameterApplyButton;
2 changes: 1 addition & 1 deletion client/app/components/ParameterMappingInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Input from 'antd/lib/input';
import Radio from 'antd/lib/radio';
import Form from 'antd/lib/form';
import Tooltip from 'antd/lib/tooltip';
import { ParameterValueInput } from '@/components/ParameterValueInput';
import ParameterValueInput from '@/components/ParameterValueInput';
import { ParameterMappingType } from '@/services/widget';
import { Parameter } from '@/services/query';
import { HelpTrigger } from '@/components/HelpTrigger';
Expand Down
35 changes: 2 additions & 33 deletions client/app/components/ParameterValueInput.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { react2angular } from 'react2angular';
import Select from 'antd/lib/select';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
Expand All @@ -19,7 +18,7 @@ const multipleValuesProps = {
maxTagPlaceholder: num => `+${num.length} more`,
};

export class ParameterValueInput extends React.Component {
class ParameterValueInput extends React.Component {
static propTypes = {
type: PropTypes.string,
value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
Expand Down Expand Up @@ -194,34 +193,4 @@ export class ParameterValueInput extends React.Component {
}
}

export default function init(ngModule) {
ngModule.component('parameterValueInput', {
template: `
<parameter-value-input-impl
type="$ctrl.param.type"
value="$ctrl.param.normalizedValue"
parameter="$ctrl.param"
enum-options="$ctrl.param.enumOptions"
query-id="$ctrl.param.queryId"
allow-multiple-values="!!$ctrl.param.multiValuesOptions"
on-select="$ctrl.setValue"
></parameter-value-input-impl>
`,
bindings: {
param: '<',
},
controller($scope) {
this.setValue = (value, isDirty) => {
if (isDirty) {
this.param.setPendingValue(value);
} else {
this.param.clearPendingValue();
}
$scope.$apply();
};
},
});
ngModule.component('parameterValueInputImpl', react2angular(ParameterValueInput));
}

init.init = true;
export default ParameterValueInput;
72 changes: 8 additions & 64 deletions client/app/components/ParameterValueInput.less
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
.parameter-input {
display: inline-block;
position: relative;
width: 100%;

.@{ant-prefix}-input[type="text"] {
width: 195px;
.@{ant-prefix}-input,
.@{ant-prefix}-input-number {
min-width: 100% !important;
}

.@{ant-prefix}-select {
width: 100%;
}

&[data-dirty] {
Expand All @@ -18,65 +24,3 @@
}
}
}

.parameter-container {
position: relative;

.parameter-apply-button {
display: none; // default for mobile

// "floating" on desktop
@media (min-width: 768px) {
position: absolute;
bottom: -42px;
left: -15px;
border-radius: 2px;
z-index: 1;
transition: opacity 150ms ease-out;
box-shadow: 0 4px 9px -3px rgba(102, 136, 153, 0.15);
background-color: #ffffff;
padding: 4px;
padding-left: 16px;
opacity: 0;
display: block;
pointer-events: none; // so tooltip doesn't remain after button hides
}

&[data-show="true"] {
opacity: 1;
display: block;
pointer-events: auto;
}

button {
padding: 0 8px 0 6px;
color: #2096f3;
border-color: #50acf6;

// smaller on desktop
@media (min-width: 768px) {
font-size: 12px;
height: 27px;
}

&:hover, &:focus, &:active {
background-color: #eef7fe;
}

i {
margin-right: 3px;
}
}

.ant-badge-count {
min-width: 15px;
height: 15px;
padding: 0 5px;
font-size: 10px;
line-height: 15px;
background: #f77b74;
border-radius: 7px;
box-shadow: 0px 0px 0 1px white, -1px 1px 0 1px #5d6f7d85;
}
}
}
207 changes: 207 additions & 0 deletions client/app/components/Parameters.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import React from 'react';
import PropTypes from 'prop-types';
import { size, filter, forEach, extend } from 'lodash';
import { react2angular } from 'react2angular';
import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc';
import { $location } from '@/services/ng';
import { Parameter } from '@/services/query';
import ParameterApplyButton from '@/components/ParameterApplyButton';
import ParameterValueInput from '@/components/ParameterValueInput';
import EditParameterSettingsDialog from './EditParameterSettingsDialog';
import { toHuman } from '@/filters';

import './Parameters.less';

const DragHandle = sortableHandle(({ parameterName }) => (
<div className="drag-handle" data-test={`DragHandle-${parameterName}`} />
));

const SortableItem = sortableElement(({ className, parameterName, disabled, children }) => (
<div className={className} data-editable={!disabled || null}>
{!disabled && <DragHandle parameterName={parameterName} />}
{children}
</div>
));
const SortableContainer = sortableContainer(({ children }) => children);

function updateUrl(parameters) {
const params = extend({}, $location.search());
parameters.forEach((param) => {
extend(params, param.toUrlParams());
});
Object.keys(params).forEach(key => params[key] == null && delete params[key]);
$location.search(params);
}

export class Parameters extends React.Component {
static propTypes = {
parameters: PropTypes.arrayOf(PropTypes.instanceOf(Parameter)),
editable: PropTypes.bool,
disableUrlUpdate: PropTypes.bool,
onValuesChange: PropTypes.func,
onPendingValuesChange: PropTypes.func,
onParametersEdit: PropTypes.func,
};

static defaultProps = {
parameters: [],
editable: false,
disableUrlUpdate: false,
onValuesChange: () => {},
onPendingValuesChange: () => {},
onParametersEdit: () => {},
}

constructor(props) {
super(props);
const { parameters } = props;
this.state = { parameters, dragging: false };
if (!props.disableUrlUpdate) {
updateUrl(parameters);
}
}

componentDidUpdate = (prevProps) => {
const { parameters, disableUrlUpdate } = this.props;
if (prevProps.parameters !== parameters) {
this.setState({ parameters });
if (!disableUrlUpdate) {
updateUrl(parameters);
}
}
};

handleKeyDown = (e) => {
// Cmd/Ctrl/Alt + Enter
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey || e.altKey)) {
e.stopPropagation();
this.applyChanges();
}
};

setPendingValue = (param, value, isDirty) => {
const { onPendingValuesChange } = this.props;
this.setState(({ parameters }) => {
if (isDirty) {
param.setPendingValue(value);
} else {
param.clearPendingValue();
}
onPendingValuesChange();
return { parameters };
});
};

moveParameter = ({ oldIndex, newIndex }) => {
const { onParametersEdit } = this.props;
if (oldIndex !== newIndex) {
this.setState(({ parameters }) => {
parameters.splice(newIndex, 0, parameters.splice(oldIndex, 1)[0]);
onParametersEdit();
return { parameters };
});
}
this.setState({ dragging: false });
};

onBeforeSortStart = () => {
this.setState({ dragging: true });
};

applyChanges = () => {
const { onValuesChange, disableUrlUpdate } = this.props;
this.setState(({ parameters }) => {
forEach(parameters, p => p.applyPendingValue());
onValuesChange();
if (!disableUrlUpdate) {
updateUrl(parameters);
}
return { parameters };
});
};

showParameterSettings = (parameter, index) => {
const { onParametersEdit } = this.props;
EditParameterSettingsDialog
.showModal({ parameter })
.result.then((updated) => {
this.setState(({ parameters }) => {
const updatedParameter = extend(parameter, updated);
parameters[index] = new Parameter(updatedParameter, updatedParameter.parentQueryId);
onParametersEdit();
return { parameters };
});
});
};

renderParameter(param, index) {
const { editable } = this.props;
return (
<div
key={param.name}
className="di-block"
data-test={`ParameterName-${param.name}`}
>
<div className="parameter-heading">
<label>{param.title || toHuman(param.name)}</label>
{editable && (
<button
className="btn btn-default btn-xs m-l-5"
onClick={() => this.showParameterSettings(param, index)}
data-test={`ParameterSettings-${param.name}`}
type="button"
>
<i className="fa fa-cog" />
</button>
)}
</div>
<ParameterValueInput
type={param.type}
value={param.normalizedValue}
parameter={param}
enumOptions={param.enumOptions}
queryId={param.queryId}
allowMultipleValues={!!param.multiValuesOptions}
onSelect={(value, isDirty) => this.setPendingValue(param, value, isDirty)}
/>
</div>
);
}

render() {
const { parameters, dragging } = this.state;
const { editable } = this.props;
const dirtyParamCount = size(filter(parameters, 'hasPendingValue'));
return (
<SortableContainer
axis="xy"
useDragHandle
lockToContainerEdges
helperClass="parameter-dragged"
updateBeforeSortStart={this.onBeforeSortStart}
onSortEnd={this.moveParameter}
>
<div
className="parameter-container"
onKeyDown={dirtyParamCount ? this.handleKeyDown : null}
data-draggable={editable || null}
data-dragging={dragging || null}
>
{parameters.map((param, index) => (
<SortableItem className="parameter-block" key={param.name} index={index} parameterName={param.name} disabled={!editable}>
{this.renderParameter(param, index)}
</SortableItem>
))}

<ParameterApplyButton onClick={this.applyChanges} paramCount={dirtyParamCount} />
</div>
</SortableContainer>
);
}
}

export default function init(ngModule) {
ngModule.component('parameters', react2angular(Parameters));
}

init.init = true;
Loading

0 comments on commit bc21f8c

Please sign in to comment.