Fix TVM UX flow issue with a React lifecycle antipattern; make TVM editing mode consistent

pull/1326/head
Hunter Trujillo 2017-04-21 19:52:01 -06:00
parent 822cf4ac4b
commit 14d4a1327e
12 changed files with 192 additions and 75 deletions

View File

@ -194,7 +194,6 @@ type TemplateQuery struct {
RP string `json:"rp,omitempty"` // RP is a retention policy and optional; if empty will not be used.
Measurement string `json:"measurement"` // Measurement is the optinally selected measurement for the query
TagKey string `json:"tagKey"` // TagKey is the optionally selected tag key for the query
FieldKey string `json:"fieldKey"` // FieldKey is the optionally selected field key for the query
}
// Response is the result of a query against a TimeSeries

View File

@ -10,8 +10,8 @@ import {
editDashboardCell,
renameDashboardCell,
syncDashboardCell,
editTemplate,
templateSelected,
templateVariableEdited,
templateVariableSelected,
} from 'src/dashboards/actions'
let state
@ -174,17 +174,23 @@ describe('DataExplorer.Reducers.UI', () => {
}
const expected = {...tempVar, ...updates}
const actual = reducer(state, editTemplate(dash.id, tempVar.id, updates))
const actual = reducer(
state,
templateVariableEdited(dash.id, tempVar.id, updates)
)
expect(actual.dashboards[0].templates[0]).to.deep.equal(expected)
})
it('can select a different template variable', () => {
const dash = _.cloneDeep(d1)
state = {
dashboards: [dash]
dashboards: [dash],
}
const value = dash.templates[0].values[2].value
const actual = reducer({dashboards}, templateSelected(dash.id, dash.templates[0].id, [{value}]))
const actual = reducer(
{dashboards},
templateVariableSelected(dash.id, dash.templates[0].id, [{value}])
)
expect(actual.dashboards[0].templates[0].values[0].selected).to.equal(false)
expect(actual.dashboards[0].templates[0].values[1].selected).to.equal(false)

View File

@ -113,7 +113,7 @@ export const editCellQueryStatus = (queryID, status) => ({
},
})
export const templateSelected = (dashboardID, templateID, values) => ({
export const templateVariableSelected = (dashboardID, templateID, values) => ({
type: TEMPLATE_VARIABLE_SELECTED,
payload: {
dashboardID,
@ -122,8 +122,8 @@ export const templateSelected = (dashboardID, templateID, values) => ({
},
})
export const editTemplate = (dashboardID, templateID, updates) => ({
type: 'EDIT_TEMPLATE',
export const templateVariableEdited = (dashboardID, templateID, updates) => ({
type: 'TEMPLATE_VARIABLE_EDITED',
payload: {
dashboardID,
templateID,

View File

@ -28,13 +28,19 @@ class DatabaseDropdown extends Component {
render() {
const {databases} = this.state
const {database, onSelectDatabase} = this.props
const {database, onSelectDatabase, onStartEdit} = this.props
// :(
if (!database) {
this.componentDidMount()
}
return (
<Dropdown
items={databases.map(text => ({text}))}
selected={database}
selected={database || 'Loading...'}
onChoose={onSelectDatabase}
onClick={() => onStartEdit(null)}
/>
)
}
@ -53,6 +59,7 @@ DatabaseDropdown.contextTypes = {
DatabaseDropdown.propTypes = {
database: string,
onSelectDatabase: func.isRequired,
onStartEdit: func.isRequired,
}
export default DatabaseDropdown

View File

@ -29,12 +29,13 @@ class MeasurementDropdown extends Component {
render() {
const {measurements} = this.state
const {measurement, onSelectMeasurement} = this.props
const {measurement, onSelectMeasurement, onStartEdit} = this.props
return (
<Dropdown
items={measurements.map(text => ({text}))}
selected={measurement || 'Select Measurement'}
onChoose={onSelectMeasurement}
onClick={() => onStartEdit(null)}
/>
)
}
@ -71,6 +72,7 @@ MeasurementDropdown.propTypes = {
database: string.isRequired,
measurement: string,
onSelectMeasurement: func.isRequired,
onStartEdit: func.isRequired,
}
export default MeasurementDropdown

View File

@ -31,12 +31,13 @@ class TagKeyDropdown extends Component {
render() {
const {tagKeys} = this.state
const {tagKey, onSelectTagKey} = this.props
const {tagKey, onSelectTagKey, onStartEdit} = this.props
return (
<Dropdown
items={tagKeys.map(text => ({text}))}
selected={tagKey || 'Select Tag Key'}
onChoose={onSelectTagKey}
onClick={() => onStartEdit(null)}
/>
)
}
@ -69,6 +70,7 @@ TagKeyDropdown.propTypes = {
measurement: string.isRequired,
tagKey: string,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
}
export default TagKeyDropdown

View File

@ -11,6 +11,7 @@ const TemplateQueryBuilder = ({
onSelectDatabase,
onSelectMeasurement,
onSelectTagKey,
onStartEdit,
}) => {
switch (selectedType) {
case 'csv':
@ -24,17 +25,19 @@ const TemplateQueryBuilder = ({
<DatabaseDropdown
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
/>
</div>
)
case 'fields':
case 'fieldKeys':
case 'tagKeys':
return (
<div>
SHOW {selectedType === 'fields' ? 'FIELD' : 'TAG'} KEYS ON
SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON
<DatabaseDropdown
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
/>
FROM
{selectedDatabase
@ -42,8 +45,9 @@ const TemplateQueryBuilder = ({
database={selectedDatabase}
measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement}
onStartEdit={onStartEdit}
/>
: 'Pick a DB'}
: <div>No database selected</div>}
</div>
)
case 'tagValues':
@ -53,6 +57,7 @@ const TemplateQueryBuilder = ({
<DatabaseDropdown
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
/>
FROM
{selectedDatabase
@ -60,6 +65,7 @@ const TemplateQueryBuilder = ({
database={selectedDatabase}
measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement}
onStartEdit={onStartEdit}
/>
: 'Pick a DB'}
WITH KEY =
@ -69,6 +75,7 @@ const TemplateQueryBuilder = ({
measurement={selectedMeasurement}
tagKey={selectedTagKey}
onSelectTagKey={onSelectTagKey}
onStartEdit={onStartEdit}
/>
: 'Pick a Tag Key'}
</div>
@ -85,6 +92,7 @@ TemplateQueryBuilder.propTypes = {
onSelectDatabase: func.isRequired,
onSelectMeasurement: func.isRequired,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
selectedMeasurement: string,
selectedDatabase: string,
selectedTagKey: string,

View File

@ -10,8 +10,7 @@ const TemplateVariableManager = ({onClose, templates}) => (
Template Variables
</div>
<div className="page-header__right">
<button className="btn btn-primary btn-sm">New Variable</button>
<button className="btn btn-success btn-sm">Save Changes</button>
<button className="btn btn-primary btn-sm">Add Variable</button>
<span
className="icon remove"
onClick={onClose}

View File

@ -4,10 +4,10 @@ import Dropdown from 'shared/components/Dropdown'
import TemplateQueryBuilder
from 'src/dashboards/components/TemplateQueryBuilder'
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
import {TEMPLATE_TYPES} from 'src/dashboards/constants'
const TemplateVariableRow = ({
template: {label, tempVar, values},
template: {id, label, tempVar, values},
isEditing,
selectedType,
selectedDatabase,
@ -18,11 +18,22 @@ const TemplateVariableRow = ({
selectedTagKey,
onSelectTagKey,
onStartEdit,
onEndEdit,
onCancelEdit,
autoFocusTarget,
onSubmit,
}) => (
<form className="tr" onSubmit={onSubmit}>
<form
className="tr"
onSubmit={onSubmit(
{
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
},
id
)}
>
<TableInput
name="label"
defaultValue={label}
@ -39,11 +50,10 @@ const TemplateVariableRow = ({
/>
<div className="td">
<Dropdown
items={TEMPLATE_VARIABLE_TYPES}
items={TEMPLATE_TYPES}
onChoose={onSelectType}
selected={
TEMPLATE_VARIABLE_TYPES.find(t => t.type === selectedType).text
}
onClick={() => onStartEdit(null)}
selected={TEMPLATE_TYPES.find(t => t.type === selectedType).text}
className={'template-variable--dropdown'}
/>
</div>
@ -56,6 +66,7 @@ const TemplateVariableRow = ({
selectedMeasurement={selectedMeasurement}
selectedTagKey={selectedTagKey}
onSelectTagKey={onSelectTagKey}
onStartEdit={onStartEdit}
/>
</div>
<div className="td">
@ -70,7 +81,7 @@ const TemplateVariableRow = ({
<button
className="btn btn-sm btn-primary"
type="button"
onClick={onEndEdit}
onClick={onCancelEdit}
>
Cancel
</button>
@ -122,31 +133,65 @@ class RowWrapper extends Component {
this.handleSelectMeasurement = ::this.handleSelectMeasurement
this.handleSelectTagKey = ::this.handleSelectTagKey
this.handleStartEdit = ::this.handleStartEdit
this.handleEndEdit = ::this.handleEndEdit
this.handleCancelEdit = ::this.handleCancelEdit
}
handleSubmit(e) {
e.preventDefault()
// const tempVar = e.target.tempVar.value
// const label = e.target.label.value
handleSubmit(
{
selectedDatabase: database,
selectedMeasurement: measurement,
selectedTagKey: tagKey,
selectedType: type,
},
id
) {
return e => {
e.preventDefault()
// updateTempVarsAsync({tempVar, label})
const label = e.target.label.value
const tempVar = e.target.tempVar.value
console.log({
id,
type,
label,
tempVar,
query: {
db: database,
measurement,
tagKey,
},
})
// updateTempVarsAsync({tempVar, label})
}
}
handleClickOutside() {
this.setState({isEditing: false})
}
handleEndEdit() {
this.setState({isEditing: false})
}
handleStartEdit(name) {
this.setState({isEditing: true, autoFocusTarget: name})
}
handleCancelEdit() {
const {template: {type, query: {db, measurement, tagKey}}} = this.props
this.setState({
selectedType: type,
selectedDatabase: db,
selectedMeasurement: measurement,
selectedKey: tagKey,
isEditing: false,
})
}
handleSelectType(item) {
this.setState({selectedType: item.type})
this.setState({
selectedType: item.type,
selectedDatabase: null,
selectedMeasurement: null,
selectedKey: null,
})
}
handleSelectDatabase(item) {
@ -184,7 +229,7 @@ class RowWrapper extends Component {
onSelectMeasurement={this.handleSelectMeasurement}
onSelectTagKey={this.handleSelectTagKey}
onStartEdit={this.handleStartEdit}
onEndEdit={this.handleEndEdit}
onCancelEdit={this.handleCancelEdit}
autoFocusTarget={autoFocusTarget}
onSubmit={this.handleSubmit}
/>
@ -224,7 +269,7 @@ TemplateVariableRow.propTypes = {
onSelectDatabase: func.isRequired,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
onEndEdit: func.isRequired,
onCancelEdit: func.isRequired,
}
TableInput.propTypes = {

View File

@ -27,7 +27,7 @@ export const NEW_DASHBOARD = {
cells: [NEW_DEFAULT_DASHBOARD_CELL],
}
export const TEMPLATE_VARIABLE_TYPES = [
export const TEMPLATE_TYPES = [
{
text: 'CSV',
type: 'csv',
@ -41,8 +41,8 @@ export const TEMPLATE_VARIABLE_TYPES = [
type: 'measurements',
},
{
text: 'Fields',
type: 'fields',
text: 'Field Keys',
type: 'fieldKeys',
},
{
text: 'Tag Keys',
@ -53,3 +53,30 @@ export const TEMPLATE_VARIABLE_TYPES = [
type: 'tagValues',
},
]
export const TEMPLATE_VARIABLE_TYPES = [
{
text: 'CSV',
type: 'csv',
},
{
text: 'Database',
type: 'database',
},
{
text: 'Measurement',
type: 'measurement',
},
{
text: 'Field Key',
type: 'fieldKey',
},
{
text: 'Tag Key',
type: 'tagKey',
},
{
text: 'Tag Value',
type: 'tagValue',
},
]

View File

@ -146,13 +146,22 @@ class DashboardPage extends Component {
handleSelectTemplate(templateID, values) {
const {params: {dashboardID}} = this.props
this.props.dashboardActions.templateSelected(
this.props.dashboardActions.templateVariableSelected(
+dashboardID,
templateID,
values
)
}
handleEditTemplate(templateVariableID, updates) {
const {params: {dashboardID}} = this.props
this.props.dashboardActions.templateVariableEdited(
+dashboardID,
templateVariableID,
updates
)
}
getActiveDashboard() {
const {params: {dashboardID}, dashboards} = this.props
return dashboards.find(d => d.id === +dashboardID)
@ -248,6 +257,7 @@ class DashboardPage extends Component {
onOpenTemplateManager={this.handleOpenTemplateManager}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
onSelectTemplate={this.handleSelectTemplate}
onEditTemplate={this.handleEditTemplate}
/>
: null}
</div>

View File

@ -2,19 +2,17 @@ import React, {PropTypes} from 'react'
import classnames from 'classnames'
import OnClickOutside from 'shared/components/OnClickOutside'
const {
arrayOf,
shape,
string,
func,
} = PropTypes
const {arrayOf, shape, string, func} = PropTypes
const Dropdown = React.createClass({
propTypes: {
items: arrayOf(shape({
text: string.isRequired,
})).isRequired,
items: arrayOf(
shape({
text: string.isRequired,
})
).isRequired,
onChoose: func.isRequired,
onClick: func,
selected: string.isRequired,
iconName: string,
className: string,
@ -40,6 +38,9 @@ const Dropdown = React.createClass({
if (e) {
e.stopPropagation()
}
if (this.props.onClick) {
this.props.onClick(e)
}
this.setState({isOpen: !this.state.isOpen})
},
handleAction(e, action, item) {
@ -53,31 +54,42 @@ const Dropdown = React.createClass({
return (
<div onClick={this.toggleMenu} className={`dropdown ${className}`}>
<div className="btn btn-sm btn-info dropdown-toggle">
{iconName ? <span className={classnames("icon", {[iconName]: true})}></span> : null}
{iconName
? <span className={classnames('icon', {[iconName]: true})} />
: null}
<span className="dropdown-selected">{selected}</span>
<span className="caret" />
</div>
{self.state.isOpen ?
<ul className="dropdown-menu show">
{items.map((item, i) => {
return (
<li className="dropdown-item" key={i}>
<a href="#" onClick={() => self.handleSelection(item)}>
{item.text}
</a>
<div className="dropdown-item__actions">
{actions.map((action) => {
return (
<button key={action.text} data-target={action.target} data-toggle="modal" className="dropdown-item__action" onClick={(e) => self.handleAction(e, action, item)}>
<span title={action.text} className={`icon ${action.icon}`}></span>
</button>
)
})}
</div>
</li>
)
})}
</ul>
{self.state.isOpen
? <ul className="dropdown-menu show">
{items.map((item, i) => {
return (
<li className="dropdown-item" key={i}>
<a href="#" onClick={() => self.handleSelection(item)}>
{item.text}
</a>
<div className="dropdown-item__actions">
{actions.map(action => {
return (
<button
key={action.text}
data-target={action.target}
data-toggle="modal"
className="dropdown-item__action"
onClick={e => self.handleAction(e, action, item)}
>
<span
title={action.text}
className={`icon ${action.icon}`}
/>
</button>
)
})}
</div>
</li>
)
})}
</ul>
: null}
</div>
)