Fix TVM UX flow issue with a React lifecycle antipattern; make TVM editing mode consistent
parent
822cf4ac4b
commit
14d4a1327e
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue