Create drag & drop list of columns

Borrowed a lot from the customizable fields component
pull/3815/head
Alex P 2018-06-21 17:38:15 -07:00 committed by Iris Scholten
parent 9b1914e8a0
commit 12cbcd0d68
7 changed files with 332 additions and 30 deletions

View File

@ -1,21 +1,39 @@
import React, {Component} from 'react'
import uuid from 'uuid'
import {DragDropContext} from 'react-dnd'
import HTML5Backend from 'react-dnd-html5-backend'
import DraggableColumn from 'src/logs/components/DraggableColumn'
import {LogsTableColumn} from 'src/types/logs'
interface Props {
columns: string[]
columns: LogsTableColumn[]
onMoveColumn: (dragIndex: number, hoverIndex: number) => void
onUpdateColumn: (column: LogsTableColumn) => void
}
class ColumnsOptions extends Component<Props> {
public render() {
const {columns} = this.props
const {columns, onMoveColumn, onUpdateColumn} = this.props
return (
<>
<label className="form-label">Order Table Columns</label>
{columns.map(c => <p key={uuid.v4()}>{c}</p>)}
<label className="form-label">Table Columns</label>
<div className="logs-options--columns">
{columns.map((c, i) => (
<DraggableColumn
key={c.internalName}
index={i}
id={c.internalName}
internalName={c.internalName}
displayName={c.displayName}
visible={c.visible}
onUpdateColumn={onUpdateColumn}
onMoveColumn={onMoveColumn}
/>
))}
</div>
</>
)
}
}
export default ColumnsOptions
export default DragDropContext(HTML5Backend)(ColumnsOptions)

View File

@ -0,0 +1,214 @@
import React, {Component, ChangeEvent} from 'react'
import {findDOMNode} from 'react-dom'
import {
DragSourceSpec,
DropTargetConnector,
DragSourceMonitor,
DragSource,
DropTarget,
DragSourceConnector,
ConnectDragSource,
ConnectDropTarget,
ConnectDragPreview,
} from 'react-dnd'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {LogsTableColumn} from 'src/types/logs'
const columnType = 'column'
interface Props {
internalName: string
displayName: string
visible: boolean
index: number
id: string
key: string
onUpdateColumn: (column: LogsTableColumn) => void
isDragging?: boolean
connectDragSource?: ConnectDragSource
connectDropTarget?: ConnectDropTarget
connectDragPreview?: ConnectDragPreview
onMoveColumn: (dragIndex: number, hoverIndex: number) => void
}
const columnSource: DragSourceSpec<Props> = {
beginDrag(props) {
return {
id: props.id,
index: props.index,
}
},
}
const columnTarget = {
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index
const hoverIndex = props.index
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return
}
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect()
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2
// Determine mouse position
const clientOffset = monitor.getClientOffset()
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return
}
// Time to actually perform the action
props.onMoveColumn(dragIndex, hoverIndex)
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
monitor.getItem().index = hoverIndex
},
}
function MyDropTarget(dropv1, dropv2, dropfunc1) {
return target => DropTarget(dropv1, dropv2, dropfunc1)(target) as any
}
function MyDragSource(dragv1, dragv2, dragfunc1) {
return target => DragSource(dragv1, dragv2, dragfunc1)(target) as any
}
@ErrorHandling
@MyDropTarget(columnType, columnTarget, (connect: DropTargetConnector) => ({
connectDropTarget: connect.dropTarget(),
}))
@MyDragSource(
columnType,
columnSource,
(connect: DragSourceConnector, monitor: DragSourceMonitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
})
)
export default class DraggableColumn extends Component<Props> {
constructor(props) {
super(props)
this.handleColumnRename = this.handleColumnRename.bind(this)
this.handleToggleVisible = this.handleToggleVisible.bind(this)
}
public render(): JSX.Element | null {
const {
internalName,
displayName,
connectDragPreview,
connectDropTarget,
visible,
} = this.props
return connectDragPreview(
connectDropTarget(
<div className={this.columnClassName}>
<div className={this.labelClassName}>
{this.dragHandle}
{this.visibilityToggle}
<div className="customizable-field--name">{internalName}</div>
</div>
<input
className="form-control input-sm customizable-field--input"
type="text"
spellCheck={false}
id="internalName"
value={displayName}
onChange={this.handleColumnRename}
placeholder={`Rename ${internalName}`}
disabled={!visible}
/>
</div>
)
)
}
private get dragHandle(): JSX.Element {
const {connectDragSource} = this.props
return connectDragSource(
<div className="customizable-field--drag">
<span className="hamburger" />
</div>
)
}
private get visibilityToggle(): JSX.Element {
const {visible, internalName} = this.props
if (visible) {
return (
<div
className="customizable-field--visibility"
onClick={this.handleToggleVisible}
title={`Click to HIDE ${internalName}`}
>
<span className="icon eye-open" />
</div>
)
}
return (
<div
className="customizable-field--visibility"
onClick={this.handleToggleVisible}
title={`Click to SHOW ${internalName}`}
>
<span className="icon eye-closed" />
</div>
)
}
private get labelClassName(): string {
const {visible} = this.props
if (visible) {
return 'customizable-field--label'
}
return 'customizable-field--label__hidden'
}
private get columnClassName(): string {
const {isDragging} = this.props
if (isDragging) {
return 'customizable-field dragging'
}
return 'customizable-field'
}
private handleColumnRename = (e: ChangeEvent<HTMLInputElement>): void => {
const {onUpdateColumn, internalName, visible} = this.props
onUpdateColumn({internalName, displayName: e.target.value, visible})
}
private handleToggleVisible = (): void => {
const {onUpdateColumn, internalName, displayName, visible} = this.props
onUpdateColumn({internalName, displayName, visible: !visible})
}
}

View File

@ -6,24 +6,32 @@ import Heading from 'src/shared/components/overlay/OverlayHeading'
import Body from 'src/shared/components/overlay/OverlayBody'
import SeverityOptions from 'src/logs/components/SeverityOptions'
import ColumnsOptions from 'src/logs/components/ColumnsOptions'
import {SeverityLevel, SeverityColor, SeverityFormat} from 'src/types/logs'
import {
SeverityLevel,
SeverityColor,
SeverityFormat,
LogsTableColumn,
} from 'src/types/logs'
import {DEFAULT_SEVERITY_LEVELS} from 'src/logs/constants'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
severityLevels: SeverityLevel[]
onUpdateSeverityLevels: (severityLevels: SeverityLevel[]) => void
onDismissOverlay: () => void
columns: string[]
columns: LogsTableColumn[]
onUpdateColumns: (columns: LogsTableColumn[]) => void
severityFormat: SeverityFormat
onUpdateSeverityFormat: (format: SeverityFormat) => void
}
interface State {
workingSeverityLevels: SeverityLevel[]
workingColumns: string[]
workingColumns: LogsTableColumn[]
workingFormat: SeverityFormat
}
@ErrorHandling
class OptionsOverlay extends Component<Props, State> {
constructor(props: Props) {
super(props)
@ -39,13 +47,13 @@ class OptionsOverlay extends Component<Props, State> {
const {workingSeverityLevels, workingColumns, workingFormat} = this.state
return (
<Container maxWidth={700}>
<Container maxWidth={800}>
<Heading title="Configure Log Viewer">
{this.overlayActionButtons}
</Heading>
<Body>
<div className="row">
<div className="col-sm-6">
<div className="col-sm-5">
<SeverityOptions
severityLevels={workingSeverityLevels}
onReset={this.handleResetSeverityLevels}
@ -54,8 +62,12 @@ class OptionsOverlay extends Component<Props, State> {
onChangeSeverityFormat={this.handleChangeSeverityFormat}
/>
</div>
<div className="col-sm-6">
<ColumnsOptions columns={workingColumns} />
<div className="col-sm-7">
<ColumnsOptions
columns={workingColumns}
onMoveColumn={this.handleMoveColumn}
onUpdateColumn={this.handleUpdateColumn}
/>
</div>
</div>
</Body>
@ -102,11 +114,13 @@ class OptionsOverlay extends Component<Props, State> {
onUpdateSeverityLevels,
onDismissOverlay,
onUpdateSeverityFormat,
onUpdateColumns,
} = this.props
const {workingSeverityLevels, workingFormat} = this.state
const {workingSeverityLevels, workingFormat, workingColumns} = this.state
onUpdateSeverityFormat(workingFormat)
onUpdateSeverityLevels(workingSeverityLevels)
onUpdateColumns(workingColumns)
onDismissOverlay()
}
@ -133,6 +147,37 @@ class OptionsOverlay extends Component<Props, State> {
private handleChangeSeverityFormat = (format: SeverityFormat) => () => {
this.setState({workingFormat: format})
}
private handleMoveColumn = (dragIndex, hoverIndex) => {
const {workingColumns} = this.state
const draggedField = workingColumns[dragIndex]
const columnsRemoved = _.concat(
_.slice(workingColumns, 0, dragIndex),
_.slice(workingColumns, dragIndex + 1)
)
const columnsAdded = _.concat(
_.slice(columnsRemoved, 0, hoverIndex),
[draggedField],
_.slice(columnsRemoved, hoverIndex)
)
this.setState({workingColumns: columnsAdded})
}
private handleUpdateColumn = (column: LogsTableColumn) => {
const workingColumns = this.state.workingColumns.map(wc => {
if (wc.internalName === column.internalName) {
return column
}
return wc
})
this.setState({workingColumns})
}
}
export default OptionsOverlay

View File

@ -20,12 +20,12 @@ const SeverityConfig: SFC<Props> = ({
onChangeSeverityFormat,
}) => (
<>
<label className="form-label">Customize Severity Colors</label>
<label className="form-label">Severity Colors</label>
<div className="logs-options--color-list">
{severityLevels.map(config => (
<div key={uuid.v4()} className="logs-options--color-row">
<div className="logs-options--color-column">
<div className="logs-options--label">{config.severity}</div>
<div className="logs-options--color-label">{config.severity}</div>
</div>
<div className="logs-options--color-column">
<ColorDropdown

View File

@ -34,12 +34,14 @@ import {colorForSeverity} from 'src/logs/utils/colors'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Source, Namespace, TimeRange} from 'src/types'
<<<<<<< HEAD
import {Filter, SeverityLevel} from 'src/types/logs'
import {HistogramData, TimePeriod} from 'src/types/histogram'
=======
import {Filter, SeverityLevel, SeverityFormat} from 'src/types/logs'
>>>>>>> Add UI for toggling severity format
import {
Filter,
SeverityLevel,
SeverityFormat,
LogsTableColumn,
} from 'src/types/logs'
// Mock
import {DEFAULT_SEVERITY_LEVELS} from 'src/logs/constants'
@ -300,10 +302,7 @@ class LogsPage extends PureComponent<Props, State> {
}
private handleShowOptionsOverlay = (): void => {
const {
showOverlay,
tableData: {columns},
} = this.props
const {showOverlay} = this.props
const options = {
dismissOnClickOutside: false,
dismissOnEscape: false,
@ -316,7 +315,8 @@ class LogsPage extends PureComponent<Props, State> {
severityLevels={DEFAULT_SEVERITY_LEVELS} // Todo: replace with real
onUpdateSeverityLevels={this.handleUpdateSeverityLevels}
onDismissOverlay={onDismissOverlay}
columns={columns}
columns={this.fakeColumns}
onUpdateColumns={this.handleUpdateColumns}
onUpdateSeverityFormat={this.handleUpdateSeverityFormat}
severityFormat="dotText" // Todo: repleace with real value
/>
@ -328,12 +328,25 @@ class LogsPage extends PureComponent<Props, State> {
private handleUpdateSeverityLevels = (levels: SeverityLevel[]) => {
console.log(levels)
// Save these new configs here
// Todo: Handle saving of these new severity colors here
}
private handleUpdateSeverityFormat = (format: SeverityFormat) => {
console.log(format)
// Save these new configs here
// Todo: Handle saving of the new format here
}
private get fakeColumns(): LogsTableColumn[] {
const {
tableData: {columns},
} = this.props
return columns.map(c => ({internalName: c, displayName: '', visible: true}))
}
private handleUpdateColumns = (columns: LogsTableColumn[]) => {
console.log(columns)
// Todo: Handle saving of column names, ordering, and visibility
}
}

View File

@ -20,7 +20,7 @@
width: 50%;
}
.logs-options--label {
.logs-options--color-label {
height: 30px;
border-radius: $radius;
background-color: $g3-castle;
@ -35,3 +35,9 @@
margin-right: 4px;
@include no-user-select();
}
// Not very clean way of slightly darkening the disabled state
// of draggable columns in the overlay
.logs-options--columns .customizable-field--label__hidden {
background-color: $g3-castle;
}

View File

@ -38,3 +38,9 @@ export interface SeverityColor {
}
export type SeverityFormat = 'dot' | 'dotText' | 'text'
export interface LogsTableColumn {
internalName: string
displayName: string
visible: boolean
}