diff --git a/ui/spec/shared/reducers/annotationsSpec.js b/ui/spec/shared/reducers/annotationsSpec.js index 36a64d77b2..f82a7739a2 100644 --- a/ui/spec/shared/reducers/annotationsSpec.js +++ b/ui/spec/shared/reducers/annotationsSpec.js @@ -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) }) }) diff --git a/ui/src/dashboards/components/Dashboard.js b/ui/src/dashboards/components/Dashboard.js index 41b31710d2..f562fd483e 100644 --- a/ui/src/dashboards/components/Dashboard.js +++ b/ui/src/dashboards/components/Dashboard.js @@ -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} /> :
@@ -119,6 +121,7 @@ Dashboard.propTypes = { onSelectTemplate: func.isRequired, showTemplateControlBar: bool, onZoom: func, + onStartAddingAnnotation: func.isRequired, } export default Dashboard diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index c04b8cc725..e4376e5349 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -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)( diff --git a/ui/src/shared/actions/annotations.js b/ui/src/shared/actions/annotations.js index c72511dc9b..833e8d0785 100644 --- a/ui/src/shared/actions/annotations.js +++ b/ui/src/shared/actions/annotations.js @@ -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: { diff --git a/ui/src/shared/annotations/helpers.js b/ui/src/shared/annotations/helpers.js index f132debfb1..1eb0c12083 100644 --- a/ui/src/shared/annotations/helpers.js +++ b/ui/src/shared/annotations/helpers.js @@ -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 [] diff --git a/ui/src/shared/components/Annotations.js b/ui/src/shared/components/Annotations.js index 19b922292e..271e70f3be 100644 --- a/ui/src/shared/components/Annotations.js +++ b/ui/src/shared/components/Annotations.js @@ -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 (
+ {mode === ADDING && + } {annotations.map(a => ({ +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), diff --git a/ui/src/shared/components/Dygraph.js b/ui/src/shared/components/Dygraph.js index f0af965f0d..f776987a64 100644 --- a/ui/src/shared/components/Dygraph.js +++ b/ui/src/shared/components/Dygraph.js @@ -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 (
- + {this.dygraph && - annotationMode !== 'adding' && {cell.isWidget @@ -85,7 +85,7 @@ const Layout = ( autoRefresh={autoRefresh} synchronizer={synchronizer} manualRefresh={manualRefresh} - annotationMode={annotationMode} + onStopAddAnnotation={onStopAddAnnotation} grabDataForDownload={grabDataForDownload} resizeCoords={resizeCoords} queries={buildQueriesForLayouts( diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index 6499deaf71..c049bd3f6f 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -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} /> @@ -91,7 +91,7 @@ LayoutCell.propTypes = { isEditable: bool, onCancelEditCell: func, celldata: arrayOf(shape()), - onStartAddAnnotation: func.isRequired, + onStartAddingAnnotation: func.isRequired, } export default LayoutCell diff --git a/ui/src/shared/components/LayoutCellMenu.js b/ui/src/shared/components/LayoutCellMenu.js index f8deef579b..253b2ef8a5 100644 --- a/ui/src/shared/components/LayoutCellMenu.js +++ b/ui/src/shared/components/LayoutCellMenu.js @@ -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 diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.js index 49e54c32f1..6e59d2fe5c 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.js @@ -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} /> @@ -199,6 +195,7 @@ LayoutRenderer.propTypes = { onCancelEditCell: func, onZoom: func, sources: arrayOf(shape({})), + onStartAddingAnnotation: func.isRequired, } export default LayoutRenderer diff --git a/ui/src/shared/components/LineGraph.js b/ui/src/shared/components/LineGraph.js index 754f0e6c62..a64feff7a4 100644 --- a/ui/src/shared/components/LineGraph.js +++ b/ui/src/shared/components/LineGraph.js @@ -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 diff --git a/ui/src/shared/components/NewAnnotation.js b/ui/src/shared/components/NewAnnotation.js index 67a02c886f..842785c012 100644 --- a/ui/src/shared/components/NewAnnotation.js +++ b/ui/src/shared/components/NewAnnotation.js @@ -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 (
- Click to add Annotation + Click to create Annotation
- Drag to add Range + Drag to create Range
-
+
+ Create Annotation at: +
+ {timestamp} +
) @@ -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 diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 8b02e26850..05b7917de3 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -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, }), diff --git a/ui/src/shared/reducers/annotations.js b/ui/src/shared/reducers/annotations.js index eaf49974d5..e997df621f 100644 --- a/ui/src/shared/reducers/annotations.js +++ b/ui/src/shared/reducers/annotations.js @@ -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} } }