Merge pull request #12821 from influxdata/variables/drag-variables-on-dashboard

feat(dashboard): add ability to change order of variable dropdowns
pull/12886/head
Alirie Gray 2019-03-25 12:58:38 -07:00 committed by GitHub
commit dcd65ee4ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 241 additions and 22 deletions

View File

@ -11,6 +11,7 @@
1. [12843](https://github.com/influxdata/influxdb/pull/12843): Add copy to clipboard button to export overlays
1. [12826](https://github.com/influxdata/influxdb/pull/12826): Enable copying error messages to the clipboard from dashboard cells
1. [12876](https://github.com/influxdata/influxdb/pull/12876): Add the ability to update token's status in Token list
1. [12821](https://github.com/influxdata/influxdb/pull/12821): Allow variables to be re-ordered within control bar on a dashboard.
### Bug Fixes
@ -20,6 +21,7 @@
1. [12790](https://github.com/influxdata/influxdb/pull/12790): Fix bucket creation error when changing rentention rules types.
1. [12793](https://github.com/influxdata/influxdb/pull/12793): Fix task creation error when switching schedule types.
1. [12805](https://github.com/influxdata/influxdb/pull/12805): Fix hidden horizonal scrollbars in flux raw data view
1. [12827](https://github.com/influxdata/influxdb/pull/12827): Fix screen tearing bug in Raw Data View
### UI Improvements

View File

@ -0,0 +1,118 @@
// Libraries
import * as React from 'react'
import {
DragSource,
DropTarget,
ConnectDropTarget,
ConnectDragSource,
DropTargetConnector,
DragSourceConnector,
DragSourceMonitor,
} from 'react-dnd'
import classnames from 'classnames'
// Components
import VariableDropdown from './VariableDropdown'
// Constants
const dropdownType = 'dropdown'
const dropdownSource = {
beginDrag(props: Props) {
return {
id: props.id,
index: props.index,
}
},
}
interface Props {
id: string
index: number
name: string
moveDropdown: (dragIndex: number, hoverIndex: number) => void
dashboardID: string
}
interface DropdownSourceCollectedProps {
isDragging: boolean
connectDragSource: ConnectDragSource
}
interface DropdownTargetCollectedProps {
connectDropTarget?: ConnectDropTarget
}
const dropdownTarget = {
hover(props, monitor, component) {
if (!component) {
return null
}
const dragIndex = monitor.getItem().index
const hoverIndex = props.index
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return
}
// Time to actually perform the action
props.moveDropdown(dragIndex, hoverIndex)
monitor.getItem().index = hoverIndex
},
}
class Dropdown extends React.Component<
Props & DropdownSourceCollectedProps & DropdownTargetCollectedProps
> {
public render() {
const {
name,
id,
dashboardID,
isDragging,
connectDragSource,
connectDropTarget,
} = this.props
const className = classnames('variable-dropdown', {
'variable-dropdown__dragging': isDragging,
})
return connectDragSource(
connectDropTarget(
<div style={{display: 'inline-block'}}>
<div className={className}>
{/* TODO: Add variable description to title attribute when it is ready */}
<div className="variable-dropdown--label">
<div className="customizable-field--drag">
<span className="hamburger" />
</div>
<span>{name}</span>
</div>
<div className="variable-dropdown--placeholder" />
<VariableDropdown variableID={id} dashboardID={dashboardID} />
</div>
</div>
)
)
}
}
export default DropTarget<Props & DropdownTargetCollectedProps>(
dropdownType,
dropdownTarget,
(connect: DropTargetConnector) => ({
connectDropTarget: connect.dropTarget(),
})
)(
DragSource<Props & DropdownSourceCollectedProps>(
dropdownType,
dropdownSource,
(connect: DragSourceConnector, monitor: DragSourceMonitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
})
)(Dropdown)
)

View File

@ -4,6 +4,8 @@
display: flex;
flex-direction: row;
height: $form-sm-height;
position: relative;
border-radius: $radius;
}
.variable-dropdown--dropdown {
@ -21,10 +23,11 @@
color: $c-comet;
white-space: nowrap;
overflow: hidden;
padding: 0 $form-sm-padding;
padding: 0 $form-sm-padding 0 0;
border-radius: $radius 0 0 $radius;
background-color: $g3-castle;
background-attachment: fixed;
display: flex;
> span {
@include gradient-h($c-comet, $c-laser);
@ -32,3 +35,21 @@
-webkit-text-fill-color: transparent;
}
}
.variable-dropdown--placeholder {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: $radius;
@include gradient-h($c-comet, $c-pool);
opacity: 0;
pointer-events: none;
z-index: 2;
transition: opacity 0.25s ease;
.variable-dropdown__dragging & {
opacity: 1;
}
}

View File

@ -29,7 +29,6 @@ interface DispatchProps {
}
interface OwnProps {
name: string
variableID: string
dashboardID: string
}
@ -38,15 +37,12 @@ type Props = StateProps & DispatchProps & OwnProps
class VariableDropdown extends PureComponent<Props> {
render() {
const {name, selectedValue} = this.props
const {selectedValue} = this.props
const dropdownValues = this.props.values || []
return (
<div className="variable-dropdown">
{/* TODO: Add variable description to title attribute when it is ready */}
<div className="variable-dropdown--label">
<span>{name}</span>
</div>
<Dropdown
selectedID={selectedValue}
onChange={this.handleSelect}

View File

@ -1,10 +1,11 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import {isEmpty} from 'lodash'
import {DragDropContext} from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'
// Components
import VariableDropdown from 'src/dashboards/components/variablesControlBar/VariableDropdown'
import {EmptyState, ComponentSize} from 'src/clockface'
import {TechnoSpinner} from '@influxdata/clockface'
@ -14,6 +15,12 @@ import {
getDashboardValuesStatus,
} from 'src/variables/selectors'
// Styles
import 'src/dashboards/components/variablesControlBar/VariablesControlBar.scss'
// Actions
import {moveVariable} from 'src/variables/actions'
// Types
import {AppState} from 'src/types/v2'
import {Variable} from '@influxdata/influx'
@ -21,6 +28,7 @@ import {Variable} from '@influxdata/influx'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
import {RemoteDataState} from 'src/types'
import DraggableDropdown from 'src/dashboards/components/variablesControlBar/DraggableDropdown'
interface OwnProps {
dashboardID: string
@ -31,14 +39,18 @@ interface StateProps {
valuesStatus: RemoteDataState
}
type Props = StateProps & OwnProps
interface DispatchProps {
moveVariable: typeof moveVariable
}
type Props = StateProps & DispatchProps & OwnProps
@ErrorHandling
class VariablesControlBar extends PureComponent<Props> {
render() {
const {dashboardID, variables, valuesStatus} = this.props
if (_.isEmpty(variables)) {
if (isEmpty(variables)) {
return (
<div className="variables-control-bar">
<EmptyState
@ -53,12 +65,14 @@ class VariablesControlBar extends PureComponent<Props> {
return (
<div className="variables-control-bar">
{variables.map(v => (
<VariableDropdown
{variables.map((v, i) => (
<DraggableDropdown
key={v.id}
name={v.name}
variableID={v.id}
id={v.id}
index={i}
dashboardID={dashboardID}
moveDropdown={this.handleMoveDropdown}
/>
))}
{valuesStatus === RemoteDataState.Loading && (
@ -67,6 +81,18 @@ class VariablesControlBar extends PureComponent<Props> {
</div>
)
}
private handleMoveDropdown = (
originalIndex: number,
newIndex: number
): void => {
const {dashboardID, moveVariable} = this.props
moveVariable(originalIndex, newIndex, dashboardID)
}
}
const mdtp = {
moveVariable,
}
const mstp = (state: AppState, props: OwnProps): StateProps => {
@ -76,4 +102,9 @@ const mstp = (state: AppState, props: OwnProps): StateProps => {
return {variables, valuesStatus}
}
export default connect<StateProps, {}, OwnProps>(mstp)(VariablesControlBar)
export default DragDropContext(HTML5Backend)(
connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(VariablesControlBar)
)

View File

@ -30,6 +30,7 @@ export const loadLocalStorage = (): LocalStorage => {
export const saveToLocalStorage = ({
app: {persisted},
ranges,
variables,
}: LocalStorage): void => {
try {
const appPersisted = {app: {persisted}}
@ -39,6 +40,7 @@ export const saveToLocalStorage = ({
...appPersisted,
VERSION,
ranges: normalizer(ranges),
variables,
})
)
} catch (err) {

View File

@ -3,6 +3,7 @@ import {Provider} from 'react-redux'
import {Router, createMemoryHistory} from 'react-router'
import {render} from 'react-testing-library'
import {initialState} from 'src/variables/reducers'
import configureStore from 'src/store/configureStore'
const localState = {
@ -24,6 +25,7 @@ const localState = {
duration: '15m',
},
],
variables: initialState(),
}
const history = createMemoryHistory({entries: ['/']})

View File

@ -1,7 +1,9 @@
import {AppState} from 'src/shared/reducers/app'
import {VariablesState} from 'src/variables/reducers'
export interface LocalStorage {
VERSION: string
app: AppState
ranges: any[]
variables: VariablesState
}

View File

@ -28,6 +28,7 @@ export type Action =
| SetVariables
| SetVariable
| RemoveVariable
| MoveVariable
| SetValues
| SelectValue
@ -75,6 +76,20 @@ const removeVariable = (id: string): RemoveVariable => ({
payload: {id},
})
interface MoveVariable {
type: 'MOVE_VARIABLE'
payload: {originalIndex: number; newIndex: number; contextID: string}
}
export const moveVariable = (
originalIndex: number,
newIndex: number,
contextID: string
): MoveVariable => ({
type: 'MOVE_VARIABLE',
payload: {originalIndex, newIndex, contextID},
})
interface SetValues {
type: 'SET_VARIABLE_VALUES'
payload: {

View File

@ -28,6 +28,7 @@ export interface VariablesState {
// the Data Explorer
[contextID: string]: {
status: RemoteDataState
order: string[] // IDs of variables
values: VariableValuesByID
}
}
@ -81,13 +82,22 @@ export const variablesReducer = (
case 'SET_VARIABLE_VALUES': {
const {contextID, status, values} = action.payload
const prevOrder = get(draftState, `values.${contextID}.order`, [])
if (values) {
draftState.values[contextID] = {status, values}
const order = Object.keys(values).sort(
(a, b) => prevOrder.indexOf(a) - prevOrder.indexOf(b)
)
draftState.values[contextID] = {
status,
values,
order,
}
} else if (draftState.values[contextID]) {
draftState.values[contextID].status = status
} else {
draftState.values[contextID] = {status, values: null}
draftState.values[contextID] = {status, values: null, order: []}
}
return
@ -111,5 +121,24 @@ export const variablesReducer = (
return
}
case 'MOVE_VARIABLE': {
const {originalIndex, newIndex, contextID} = action.payload
const variableIDToMove = get(
draftState,
`values.${contextID}.order[${originalIndex}]`
)
const variableIDToSwap = get(
draftState,
`values.${contextID}.order[${newIndex}]`
)
draftState.values[contextID].order[originalIndex] = variableIDToSwap
draftState.values[contextID].order[newIndex] = variableIDToMove
return
}
}
})

View File

@ -37,12 +37,10 @@ export const getVariablesForOrg = (
}
const getVariablesForDashboardMemoized = memoizeOne(
(variables: VariablesState, values: VariableValuesByID): Variable[] => {
(variables: VariablesState, variableIDs: string[]): Variable[] => {
let variablesForDash = []
const variablesIDs = Object.keys(values)
variablesIDs.forEach(variableID => {
variableIDs.forEach(variableID => {
const variable = get(variables, `${variableID}.variable`)
if (variable) {
@ -58,9 +56,12 @@ export const getVariablesForDashboard = (
state: AppState,
dashboardID: string
): Variable[] => {
const values = get(state, `variables.values.${dashboardID}.values`, {})
const variableIDs = get(state, `variables.values.${dashboardID}.order`, [])
return getVariablesForDashboardMemoized(state.variables.variables, values)
return getVariablesForDashboardMemoized(
state.variables.variables,
variableIDs
)
}
export const getValuesForVariable = (