Consolidating annotation in redux

pull/10616/head
Alex P 2018-01-19 17:46:28 -08:00
parent f473fc0872
commit c953205baf
15 changed files with 160 additions and 84 deletions

View File

@ -27,36 +27,43 @@ const a2 = {
text: 'you have no swoggels',
}
const state = {
mode: null,
annotations: [],
}
describe.only('Shared.Reducers.annotations', () => {
it('can load the annotations', () => {
const state = []
const expected = [{time: '0', duration: ''}]
const actual = reducer(state, loadAnnotations(expected))
expect(actual).to.deep.equal(expected)
expect(actual.annotations).to.deep.equal(expected)
})
it('can update an annotation', () => {
const state = [a1]
const expected = [{...a1, time: ''}]
const actual = reducer(state, updateAnnotation(expected[0]))
const actual = reducer(
{...state, annotations: [a1]},
updateAnnotation(expected[0])
)
expect(actual).to.deep.equal(expected)
expect(actual.annotations).to.deep.equal(expected)
})
it('can delete an annotation', () => {
const state = [a1, a2]
const expected = [a2]
const actual = reducer(state, deleteAnnotation(a1))
const actual = reducer(
{...state, annotations: [a1, a2]},
deleteAnnotation(a1)
)
expect(actual).to.deep.equal(expected)
expect(actual.annotations).to.deep.equal(expected)
})
it('can add an annotation', () => {
const state = []
const expected = [{...a1, id: DEFAULT_ANNOTATION_ID}]
const actual = reducer(state, addAnnotation(a1))
expect(actual).to.deep.equal(expected)
expect(actual.annotations).to.deep.equal(expected)
})
})

View File

@ -23,6 +23,7 @@ const Dashboard = ({
onSummonOverlayTechnologies,
onSelectTemplate,
showTemplateControlBar,
onStartAddingAnnotation,
}) => {
const cells = dashboard.cells.map(cell => {
const dashboardCell = {...cell}
@ -63,6 +64,7 @@ const Dashboard = ({
onDeleteCell={onDeleteCell}
onPositionChange={onPositionChange}
templates={templatesIncludingDashTime}
onStartAddingAnnotation={onStartAddingAnnotation}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
/>
: <div className="dashboard__empty">
@ -119,6 +121,7 @@ Dashboard.propTypes = {
onSelectTemplate: func.isRequired,
showTemplateControlBar: bool,
onZoom: func,
onStartAddingAnnotation: func.isRequired,
}
export default Dashboard

View File

@ -25,6 +25,7 @@ import {
setAutoRefresh,
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
} from 'shared/actions/app'
import {addingAnnotation} from 'shared/actions/annotations'
import {presentationButtonDispatcher} from 'shared/dispatchers'
const FORMAT_INFLUXQL = 'influxql'
@ -248,6 +249,7 @@ class DashboardPage extends Component {
handleChooseAutoRefresh,
handleClickPresentationButton,
params: {sourceID, dashboardID},
handleStartAddingAnnotation,
} = this.props
const low = zoomedLower ? zoomedLower : lower
@ -387,6 +389,7 @@ class DashboardPage extends Component {
showTemplateControlBar={showTemplateControlBar}
onOpenTemplateManager={this.handleOpenTemplateManager}
templatesIncludingDashTime={templatesIncludingDashTime}
onStartAddingAnnotation={handleStartAddingAnnotation}
onSummonOverlayTechnologies={this.handleSummonOverlayTechnologies}
/>
: null}
@ -467,6 +470,7 @@ DashboardPage.propTypes = {
isUsingAuth: bool.isRequired,
router: shape().isRequired,
notify: func.isRequired,
handleStartAddingAnnotation: func.isRequired,
}
const mapStateToProps = (state, {params: {dashboardID}}) => {
@ -516,6 +520,7 @@ const mapDispatchToProps = dispatch => ({
dashboardActions: bindActionCreators(dashboardActionCreators, dispatch),
errorThrown: bindActionCreators(errorThrownAction, dispatch),
notify: bindActionCreators(publishNotification, dispatch),
handleStartAddingAnnotation: bindActionCreators(addingAnnotation, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(

View File

@ -1,3 +1,11 @@
export const addingAnnotation = () => ({
type: 'ADDING_ANNOTATION',
})
export const addingAnnotationSuccess = () => ({
type: 'ADDING_ANNOTATION_SUCCESS',
})
export const loadAnnotations = annotations => ({
type: 'LOAD_ANNOTATIONS',
payload: {

View File

@ -1,3 +1,15 @@
export const ADDING = 'adding'
export const EDITING = 'editing'
export const TEMP_ANNOTATION = {
id: 'tempAnnotation',
group: '',
name: 'New Annotation',
time: '',
duration: '',
text: '',
}
export const getAnnotations = (graph, annotations = []) => {
if (!graph) {
return []

View File

@ -4,11 +4,15 @@ import {bindActionCreators} from 'redux'
import Annotation from 'src/shared/components/Annotation'
import AnnotationWindow from 'src/shared/components/AnnotationWindow'
import NewAnnotation from 'src/shared/components/NewAnnotation'
import {ADDING} from 'src/shared/annotations/helpers'
import {
addAnnotation,
updateAnnotation,
deleteAnnotation,
addingAnnotationSuccess,
} from 'src/shared/actions/annotations'
import {getAnnotations} from 'src/shared/annotations/helpers'
@ -23,7 +27,13 @@ class Annotations extends Component {
render() {
const {dygraph} = this.state
const {mode, handleUpdateAnnotation, handleDeleteAnnotation} = this.props
const {
mode,
handleUpdateAnnotation,
handleDeleteAnnotation,
handleAddAnnotation,
handleAddingAnnotationSuccess,
} = this.props
if (!dygraph) {
return null
@ -33,6 +43,12 @@ class Annotations extends Component {
return (
<div className="annotations-container">
{mode === ADDING &&
<NewAnnotation
dygraph={dygraph}
onAddAnnotation={handleAddAnnotation}
onAddingAnnotationSuccess={handleAddingAnnotationSuccess}
/>}
{annotations.map(a =>
<Annotation
key={a.id}
@ -63,13 +79,19 @@ Annotations.propTypes = {
handleDeleteAnnotation: func.isRequired,
handleUpdateAnnotation: func.isRequired,
handleAddAnnotation: func.isRequired,
handleAddingAnnotationSuccess: func.isRequired,
}
const mapStateToProps = ({annotations}) => ({
const mapStateToProps = ({annotations: {annotations, mode}}) => ({
annotations,
mode,
})
const mapDispatchToProps = dispatch => ({
handleAddingAnnotationSuccess: bindActionCreators(
addingAnnotationSuccess,
dispatch
),
handleAddAnnotation: bindActionCreators(addAnnotation, dispatch),
handleUpdateAnnotation: bindActionCreators(updateAnnotation, dispatch),
handleDeleteAnnotation: bindActionCreators(deleteAnnotation, dispatch),

View File

@ -295,17 +295,12 @@ export default class Dygraph extends Component {
handleAnnotationsRef = ref => (this.annotationsRef = ref)
render() {
const {annotationMode} = this.props
const {isHidden} = this.state
return (
<div className="dygraph-child" onMouseLeave={this.deselectCrosshair}>
<Annotations
mode={annotationMode}
annotationsRef={this.handleAnnotationsRef}
/>
<Annotations annotationsRef={this.handleAnnotationsRef} />
{this.dygraph &&
annotationMode !== 'adding' &&
<DygraphLegend
isHidden={isHidden}
dygraph={this.dygraph}
@ -379,5 +374,4 @@ Dygraph.propTypes = {
setResolution: func,
dygraphRef: func,
onZoom: func,
annotationMode: string,
}

View File

@ -53,9 +53,9 @@ const Layout = (
onDeleteCell,
synchronizer,
resizeCoords,
annotationMode,
onCancelEditCell,
onStartAddAnnotation,
onStopAddAnnotation,
onStartAddingAnnotation,
onSummonOverlayTechnologies,
grabDataForDownload,
},
@ -68,7 +68,7 @@ const Layout = (
onEditCell={onEditCell}
onDeleteCell={onDeleteCell}
onCancelEditCell={onCancelEditCell}
onStartAddAnnotation={onStartAddAnnotation}
onStartAddingAnnotation={onStartAddingAnnotation}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
>
{cell.isWidget
@ -85,7 +85,7 @@ const Layout = (
autoRefresh={autoRefresh}
synchronizer={synchronizer}
manualRefresh={manualRefresh}
annotationMode={annotationMode}
onStopAddAnnotation={onStopAddAnnotation}
grabDataForDownload={grabDataForDownload}
resizeCoords={resizeCoords}
queries={buildQueriesForLayouts(

View File

@ -35,7 +35,7 @@ class LayoutCell extends Component {
children,
isEditable,
celldata,
onStartAddAnnotation,
onStartAddingAnnotation,
} = this.props
const queries = _.get(cell, ['queries'], [])
@ -52,7 +52,7 @@ class LayoutCell extends Component {
onEdit={this.handleSummonOverlay}
handleClickOutside={this.closeMenu}
onCSVDownload={this.handleCSVDownload}
onStartAddAnnotation={onStartAddAnnotation}
onStartAddingAnnotation={onStartAddingAnnotation}
/>
</Authorized>
<LayoutCellHeader cellName={cell.name} isEditable={isEditable} />
@ -91,7 +91,7 @@ LayoutCell.propTypes = {
isEditable: bool,
onCancelEditCell: func,
celldata: arrayOf(shape()),
onStartAddAnnotation: func.isRequired,
onStartAddingAnnotation: func.isRequired,
}
export default LayoutCell

View File

@ -23,7 +23,7 @@ class LayoutCellMenu extends Component {
isEditable,
dataExists,
onCSVDownload,
onStartAddAnnotation,
onStartAddingAnnotation,
} = this.props
return (
@ -45,7 +45,7 @@ class LayoutCellMenu extends Component {
icon="pencil"
menuOptions={[
{text: 'Queries', action: onEdit(cell)},
{text: 'Add Annotation', action: onStartAddAnnotation},
{text: 'Add Annotation', action: onStartAddingAnnotation},
]}
informParent={this.handleToggleSubMenu}
/>
@ -79,7 +79,7 @@ LayoutCellMenu.propTypes = {
dataExists: bool,
onCSVDownload: func,
queries: arrayOf(shape()),
onStartAddAnnotation: func.isRequired,
onStartAddingAnnotation: func.isRequired,
}
export default LayoutCellMenu

View File

@ -26,14 +26,9 @@ class LayoutRenderer extends Component {
this.state = {
rowHeight: this.calculateRowHeight(),
resizeCoords: null,
annotationMode: null,
}
}
handleStartAddAnnotation = () => {
this.setState({annotationMode: 'adding'})
}
handleLayoutChange = layout => {
if (!this.props.onPositionChange) {
return
@ -86,10 +81,11 @@ class LayoutRenderer extends Component {
onDeleteCell,
synchronizer,
onCancelEditCell,
onStartAddingAnnotation,
onSummonOverlayTechnologies,
} = this.props
const {rowHeight, resizeCoords, annotationMode} = this.state
const {rowHeight, resizeCoords} = this.state
const isDashboard = !!this.props.onPositionChange
return (
@ -139,9 +135,9 @@ class LayoutRenderer extends Component {
onDeleteCell={onDeleteCell}
synchronizer={synchronizer}
manualRefresh={manualRefresh}
annotationMode={annotationMode}
onCancelEditCell={onCancelEditCell}
onStartAddAnnotation={this.handleStartAddAnnotation}
onStopAddAnnotation={this.handleStopAddAnnotation}
onStartAddingAnnotation={onStartAddingAnnotation}
onSummonOverlayTechnologies={onSummonOverlayTechnologies}
/>
</Authorized>
@ -199,6 +195,7 @@ LayoutRenderer.propTypes = {
onCancelEditCell: func,
onZoom: func,
sources: arrayOf(shape({})),
onStartAddingAnnotation: func.isRequired,
}
export default LayoutRenderer

View File

@ -51,7 +51,6 @@ class LineGraph extends Component {
isRefreshing,
setResolution,
isGraphFilled,
annotationMode,
showSingleStat,
displayOptions,
underlayCallback,
@ -112,7 +111,6 @@ class LineGraph extends Component {
setResolution={setResolution}
overrideLineColors={lineColors}
containerStyle={containerStyle}
annotationMode={annotationMode}
isGraphFilled={showSingleStat ? false : isGraphFilled}
/>
{showSingleStat
@ -180,7 +178,6 @@ LineGraph.propTypes = {
resizeCoords: shape(),
queries: arrayOf(shape({}).isRequired).isRequired,
data: arrayOf(shape({}).isRequired).isRequired,
annotationMode: string,
}
export default LineGraph

View File

@ -38,12 +38,13 @@ const prompterContainerStyle = isMouseHovering => {
const prompterStyle = isMouseHovering => {
return {
padding: '16px',
padding: '16px 32px',
textAlign: 'center',
backgroundColor: 'rgba(255,0,0,0.2)',
backgroundColor: 'rgba(255,0,0,0.7)',
color: '#fff',
borderRadius: '4px',
fontSize: '16px',
borderRadius: '5px',
fontSize: '17px',
lineHeight: '30px',
fontWeight: '400',
opacity: isMouseHovering ? '0' : '1',
transition: 'opacity 0.25s ease',
@ -53,7 +54,7 @@ const prompterStyle = isMouseHovering => {
class NewAnnotation extends Component {
state = {
xPos: null,
isMouseHovering: false,
isMouseHovering: true,
}
handleMouseEnter = () => {
@ -67,38 +68,39 @@ class NewAnnotation extends Component {
const wrapperRect = this.wrapper.getBoundingClientRect()
const trueGraphX = e.pageX - wrapperRect.left
console.log('mouseMove')
this.setState({xPos: trueGraphX})
}
handleMouseLeave = () => {
console.log('mouseLeave')
this.setState({xPos: null, isMouseHovering: false})
}
handleClick = () => {
const {onAddAnnotation, dygraph} = this.props
const {onAddAnnotation, onAddingAnnotationSuccess, dygraph} = this.props
const {xPos} = this.state
const time = dygraph.toDataXCoord(xPos)
const time = `${dygraph.toDataXCoord(xPos)}`
const annotation = {
id: 'newannotationid',
id: 'newannotationid', // TODO generate real ID
group: '',
name: 'New Annotation',
time,
duration: '',
text: '',
}
console.log(annotation)
onAddAnnotation(annotation)
this.setState({xPos: null, isMouseHovering: false})
onAddingAnnotationSuccess()
onAddAnnotation(annotation)
}
render() {
// const {dygraph} = this.props
const {dygraph} = this.props
const {xPos, isMouseHovering} = this.state
const timestamp = `${new Date(dygraph.toDataXCoord(xPos))}`
return (
<div
className="new-annotation"
@ -114,13 +116,17 @@ class NewAnnotation extends Component {
style={prompterContainerStyle(isMouseHovering)}
>
<div style={prompterStyle(isMouseHovering)}>
<strong>Click</strong> to add Annotation
<strong>Click</strong> to create Annotation
<br />
<strong>Drag</strong> to add Range
<strong>Drag</strong> to create Range
</div>
</div>
<div className="new-annotation--crosshair" style={newLineStyle(xPos)}>
<div className="new-annotation--tooltip" />
<div className="new-annotation--tooltip">
Create Annotation at:
<br />
{timestamp}
</div>
</div>
</div>
)
@ -132,7 +138,7 @@ const {func, shape} = PropTypes
NewAnnotation.propTypes = {
dygraph: shape({}).isRequired,
onAddAnnotation: func.isRequired,
onCancelAddAnnotation: func.isRequired,
onAddingAnnotationSuccess: func.isRequired,
}
export default NewAnnotation

View File

@ -22,7 +22,6 @@ const RefreshingGraph = ({
cellHeight,
autoRefresh,
resizerTopHeight,
annotationMode,
manualRefresh, // when changed, re-mounts the component
synchronizer,
resizeCoords,
@ -86,7 +85,6 @@ const RefreshingGraph = ({
isBarGraph={type === 'bar'}
synchronizer={synchronizer}
resizeCoords={resizeCoords}
annotationMode={annotationMode}
displayOptions={displayOptions}
editQueryStatus={editQueryStatus}
grabDataForDownload={grabDataForDownload}
@ -98,7 +96,6 @@ const RefreshingGraph = ({
const {arrayOf, func, number, shape, string} = PropTypes
RefreshingGraph.propTypes = {
annotationMode: string,
timeRange: shape({
lower: string.isRequired,
}),

View File

@ -1,48 +1,76 @@
import {DEFAULT_ANNOTATION_ID} from 'src/shared/constants/annotations'
import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers'
const initialState = [
{
id: '0',
group: '',
name: 'anno1',
time: '1515716169000',
duration: '33600000', // 1 hour
text: 'you have no swoggels',
},
{
id: '1',
group: '',
name: 'anno2',
time: '1515772377000',
duration: '',
text: 'another annotation',
},
]
const initialState = {
mode: null,
annotations: [
{
id: '0',
group: '',
name: 'anno1',
time: '1515716169000',
duration: '33600000', // 1 hour
text: 'you have no swoggels',
},
{
id: '1',
group: '',
name: 'anno2',
time: '1515772377000',
duration: '',
text: 'another annotation',
},
],
}
const annotationsReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADDING_ANNOTATION': {
return {
...state,
mode: ADDING,
annotations: [...state.annotations, TEMP_ANNOTATION],
}
}
case 'ADDING_ANNOTATION_SUCCESS': {
const annotations = state.annotations.filter(
a => a.id !== TEMP_ANNOTATION.id
)
return {...state, mode: null, annotations}
}
case 'LOAD_ANNOTATIONS': {
return action.payload.annotations
const {annotations} = action.payload
return {...state, annotations}
}
case 'UPDATE_ANNOTATION': {
const {annotation} = action.payload
const newState = state.map(a => (a.id === annotation.id ? annotation : a))
const annotations = state.annotations.map(
a => (a.id === annotation.id ? annotation : a)
)
return newState
return {...state, annotations}
}
case 'DELETE_ANNOTATION': {
const {annotation} = action.payload
const newState = state.filter(a => a.id !== annotation.id)
const annotations = state.annotations.filter(a => a.id !== annotation.id)
return newState
return {...state, annotations}
}
case 'ADD_ANNOTATION': {
const {annotation} = action.payload
const annotations = [
...state.annotations,
{...annotation, id: DEFAULT_ANNOTATION_ID},
]
return [...state, {...annotation, id: DEFAULT_ANNOTATION_ID}]
return {...state, annotations}
}
}