Merge pull request #12821 from influxdata/variables/drag-variables-on-dashboard
feat(dashboard): add ability to change order of variable dropdownspull/12886/head
commit
dcd65ee4ae
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
)
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: ['/']})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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 = (
|
||||
|
|
Loading…
Reference in New Issue