From eb75595a0403d40df7f7230fa06e4c38f732e09a Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Wed, 13 Jun 2018 15:35:49 -0700 Subject: [PATCH 1/5] Convert Annotations component to TS and pass xAxisRange explicitly to cause rerender of annotations on zoom in --- .../annotations/{helpers.js => helpers.ts} | 11 ++- ui/src/shared/components/Annotation.js | 6 +- ui/src/shared/components/AnnotationPoint.js | 3 +- ui/src/shared/components/AnnotationSpan.js | 3 +- .../{Annotations.js => Annotations.tsx} | 78 ++++++++----------- ui/src/shared/components/Dygraph.tsx | 1 + ui/src/types/annotations.ts | 7 ++ ui/src/types/index.ts | 2 + 8 files changed, 59 insertions(+), 52 deletions(-) rename ui/src/shared/annotations/{helpers.js => helpers.ts} (62%) rename ui/src/shared/components/{Annotations.js => Annotations.tsx} (61%) create mode 100644 ui/src/types/annotations.ts diff --git a/ui/src/shared/annotations/helpers.js b/ui/src/shared/annotations/helpers.ts similarity index 62% rename from ui/src/shared/annotations/helpers.js rename to ui/src/shared/annotations/helpers.ts index 739378372..e90e15a89 100644 --- a/ui/src/shared/annotations/helpers.js +++ b/ui/src/shared/annotations/helpers.ts @@ -1,9 +1,11 @@ +import {AnnotationInterface} from 'src/types' + export const ANNOTATION_MIN_DELTA = 0.5 export const ADDING = 'adding' export const EDITING = 'editing' -export const TEMP_ANNOTATION = { +export const TEMP_ANNOTATION: AnnotationInterface = { id: 'tempAnnotation', text: 'Name Me', type: '', @@ -11,8 +13,11 @@ export const TEMP_ANNOTATION = { endTime: '', } -export const visibleAnnotations = (graph, annotations = []) => { - const [xStart, xEnd] = graph.xAxisRange() +export const visibleAnnotations = ( + xAxisRange: number[], + annotations: AnnotationInterface[] = [] +): AnnotationInterface[] => { + const [xStart, xEnd] = xAxisRange if (xStart === 0 && xEnd === 0) { return [] diff --git a/ui/src/shared/components/Annotation.js b/ui/src/shared/components/Annotation.js index 26d6fbfb4..00919b1d0 100644 --- a/ui/src/shared/components/Annotation.js +++ b/ui/src/shared/components/Annotation.js @@ -10,6 +10,7 @@ const Annotation = ({ mode, dygraph, dWidth, + xAxisRange, annotation, staticLegendHeight, }) => ( @@ -21,6 +22,7 @@ const Annotation = ({ annotation={annotation} dWidth={dWidth} staticLegendHeight={staticLegendHeight} + xAxisRange={xAxisRange} /> ) : ( )} ) -const {number, shape, string} = PropTypes +const {arrayOf, number, shape, string} = PropTypes Annotation.propTypes = { mode: string, dWidth: number, + xAxisRange: arrayOf(number), annotation: schema.annotation.isRequired, dygraph: shape({}).isRequired, staticLegendHeight: number, diff --git a/ui/src/shared/components/AnnotationPoint.js b/ui/src/shared/components/AnnotationPoint.js index cdc529d24..5d704d4de 100644 --- a/ui/src/shared/components/AnnotationPoint.js +++ b/ui/src/shared/components/AnnotationPoint.js @@ -139,7 +139,7 @@ class AnnotationPoint extends React.Component { } } -const {func, number, shape, string} = PropTypes +const {arrayOf, func, number, shape, string} = PropTypes AnnotationPoint.defaultProps = { staticLegendHeight: 0, @@ -148,6 +148,7 @@ AnnotationPoint.defaultProps = { AnnotationPoint.propTypes = { annotation: schema.annotation.isRequired, mode: string.isRequired, + xAxisRange: arrayOf(number), dygraph: shape({}).isRequired, updateAnnotation: func.isRequired, updateAnnotationAsync: func.isRequired, diff --git a/ui/src/shared/components/AnnotationSpan.js b/ui/src/shared/components/AnnotationSpan.js index aed861a63..3f4bdfbd0 100644 --- a/ui/src/shared/components/AnnotationSpan.js +++ b/ui/src/shared/components/AnnotationSpan.js @@ -217,7 +217,7 @@ class AnnotationSpan extends React.Component { } } -const {func, number, shape, string} = PropTypes +const {arrayOf, func, number, shape, string} = PropTypes AnnotationSpan.defaultProps = { staticLegendHeight: 0, @@ -230,6 +230,7 @@ AnnotationSpan.propTypes = { staticLegendHeight: number, updateAnnotationAsync: func.isRequired, updateAnnotation: func.isRequired, + xAxisRange: arrayOf(number), } const mapDispatchToProps = { diff --git a/ui/src/shared/components/Annotations.js b/ui/src/shared/components/Annotations.tsx similarity index 61% rename from ui/src/shared/components/Annotations.js rename to ui/src/shared/components/Annotations.tsx index fdb9665e9..a8f595959 100644 --- a/ui/src/shared/components/Annotations.js +++ b/ui/src/shared/components/Annotations.tsx @@ -1,11 +1,8 @@ import React, {Component} from 'react' -import PropTypes from 'prop-types' import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' import Annotation from 'src/shared/components/Annotation' import NewAnnotation from 'src/shared/components/NewAnnotation' -import * as schema from 'src/shared/schemas' import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' @@ -19,13 +16,31 @@ import { import {visibleAnnotations} from 'src/shared/annotations/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass} from 'src/types' + +interface Props { + dygraph: DygraphClass + dWidth: number + xAxisRange: number[] + staticLegendHeight: number + annotations: AnnotationInterface[] + mode: string + isTempHovering: boolean + handleUpdateAnnotation: () => void + handleDismissAddingAnnotation: () => void + handleAddingAnnotationSuccess: () => void + handleMouseEnterTempAnnotation: () => void + handleMouseLeaveTempAnnotation: () => void +} + @ErrorHandling -class Annotations extends Component { - render() { +class Annotations extends Component { + public render() { const { mode, dWidth, dygraph, + xAxisRange, isTempHovering, handleUpdateAnnotation, handleDismissAddingAnnotation, @@ -34,7 +49,7 @@ class Annotations extends Component { handleMouseLeaveTempAnnotation, staticLegendHeight, } = this.props - + console.log('rendering annotations') return (
{mode === ADDING && @@ -58,6 +73,7 @@ class Annotations extends Component { annotation={a} dygraph={dygraph} dWidth={dWidth} + xAxisRange={xAxisRange} staticLegendHeight={staticLegendHeight} /> ))} @@ -67,7 +83,7 @@ class Annotations extends Component { get annotations() { return visibleAnnotations( - this.props.dygraph, + this.props.xAxisRange, this.props.annotations ).filter(a => a.id !== TEMP_ANNOTATION.id) } @@ -77,48 +93,18 @@ class Annotations extends Component { } } -const {arrayOf, bool, func, number, shape, string} = PropTypes - -Annotations.propTypes = { - annotations: arrayOf(schema.annotation), - dygraph: shape({}).isRequired, - dWidth: number.isRequired, - mode: string, - isTempHovering: bool, - handleUpdateAnnotation: func.isRequired, - handleDismissAddingAnnotation: func.isRequired, - handleAddingAnnotationSuccess: func.isRequired, - handleMouseEnterTempAnnotation: func.isRequired, - handleMouseLeaveTempAnnotation: func.isRequired, - staticLegendHeight: number, -} - -const mapStateToProps = ({ - annotations: {annotations, mode, isTempHovering}, -}) => ({ +const mstp = ({annotations: {annotations, mode, isTempHovering}}) => ({ annotations, mode: mode || 'NORMAL', isTempHovering, }) -const mapDispatchToProps = dispatch => ({ - handleAddingAnnotationSuccess: bindActionCreators( - addingAnnotationSuccess, - dispatch - ), - handleDismissAddingAnnotation: bindActionCreators( - dismissAddingAnnotation, - dispatch - ), - handleMouseEnterTempAnnotation: bindActionCreators( - mouseEnterTempAnnotation, - dispatch - ), - handleMouseLeaveTempAnnotation: bindActionCreators( - mouseLeaveTempAnnotation, - dispatch - ), - handleUpdateAnnotation: bindActionCreators(updateAnnotation, dispatch), -}) +const mdtp = { + handleAddingAnnotationSuccess: addingAnnotationSuccess, + handleDismissAddingAnnotation: dismissAddingAnnotation, + handleMouseEnterTempAnnotation: mouseEnterTempAnnotation, + handleMouseLeaveTempAnnotation: mouseLeaveTempAnnotation, + handleUpdateAnnotation: updateAnnotation, +} -export default connect(mapStateToProps, mapDispatchToProps)(Annotations) +export default connect(mstp, mdtp)(Annotations) diff --git a/ui/src/shared/components/Dygraph.tsx b/ui/src/shared/components/Dygraph.tsx index 6e0f1c1dc..e0c79d6fe 100644 --- a/ui/src/shared/components/Dygraph.tsx +++ b/ui/src/shared/components/Dygraph.tsx @@ -258,6 +258,7 @@ class Dygraph extends Component { dygraph={this.dygraph} dWidth={this.dygraph.width_} staticLegendHeight={staticLegendHeight} + xAxisRange={this.dygraph.xAxisRange()} /> )} Date: Wed, 13 Jun 2018 18:44:16 -0700 Subject: [PATCH 2/5] Convert Annotation component to TS --- ui/src/shared/actions/annotations.js | 82 ---------- ui/src/shared/actions/annotations.ts | 149 ++++++++++++++++++ .../{Annotation.js => Annotation.tsx} | 31 ++-- ui/src/shared/components/Annotations.tsx | 5 +- 4 files changed, 167 insertions(+), 100 deletions(-) delete mode 100644 ui/src/shared/actions/annotations.js create mode 100644 ui/src/shared/actions/annotations.ts rename ui/src/shared/components/{Annotation.js => Annotation.tsx} (56%) diff --git a/ui/src/shared/actions/annotations.js b/ui/src/shared/actions/annotations.js deleted file mode 100644 index 328cdb1a1..000000000 --- a/ui/src/shared/actions/annotations.js +++ /dev/null @@ -1,82 +0,0 @@ -import * as api from 'shared/apis/annotation' - -export const editingAnnotation = () => ({ - type: 'EDITING_ANNOTATION', -}) - -export const dismissEditingAnnotation = () => ({ - type: 'DISMISS_EDITING_ANNOTATION', -}) - -export const addingAnnotation = () => ({ - type: 'ADDING_ANNOTATION', -}) - -export const addingAnnotationSuccess = () => ({ - type: 'ADDING_ANNOTATION_SUCCESS', -}) - -export const dismissAddingAnnotation = () => ({ - type: 'DISMISS_ADDING_ANNOTATION', -}) - -export const mouseEnterTempAnnotation = () => ({ - type: 'MOUSEENTER_TEMP_ANNOTATION', -}) - -export const mouseLeaveTempAnnotation = () => ({ - type: 'MOUSELEAVE_TEMP_ANNOTATION', -}) - -export const loadAnnotations = annotations => ({ - type: 'LOAD_ANNOTATIONS', - payload: { - annotations, - }, -}) - -export const updateAnnotation = annotation => ({ - type: 'UPDATE_ANNOTATION', - payload: { - annotation, - }, -}) - -export const deleteAnnotation = annotation => ({ - type: 'DELETE_ANNOTATION', - payload: { - annotation, - }, -}) - -export const addAnnotation = annotation => ({ - type: 'ADD_ANNOTATION', - payload: { - annotation, - }, -}) - -export const addAnnotationAsync = (createUrl, annotation) => async dispatch => { - dispatch(addAnnotation(annotation)) - const savedAnnotation = await api.createAnnotation(createUrl, annotation) - dispatch(addAnnotation(savedAnnotation)) - dispatch(deleteAnnotation(annotation)) -} - -export const getAnnotationsAsync = ( - indexUrl, - {since, until} -) => async dispatch => { - const annotations = await api.getAnnotations(indexUrl, since, until) - dispatch(loadAnnotations(annotations)) -} - -export const deleteAnnotationAsync = annotation => async dispatch => { - await api.deleteAnnotation(annotation) - dispatch(deleteAnnotation(annotation)) -} - -export const updateAnnotationAsync = annotation => async dispatch => { - await api.updateAnnotation(annotation) - dispatch(updateAnnotation(annotation)) -} diff --git a/ui/src/shared/actions/annotations.ts b/ui/src/shared/actions/annotations.ts new file mode 100644 index 000000000..2f62285fb --- /dev/null +++ b/ui/src/shared/actions/annotations.ts @@ -0,0 +1,149 @@ +import * as api from 'src/shared/apis/annotation' + +import {AnnotationInterface} from 'src/types' + +export interface EditingAnnotationAction { + type: 'EDITING_ANNOTATION' +} +export const editingAnnotation = (): EditingAnnotationAction => ({ + type: 'EDITING_ANNOTATION', +}) + +export interface DismissEditingAnnotationAction { + type: 'DISMISS_EDITING_ANNOTATION' +} +export const dismissEditingAnnotation = (): DismissEditingAnnotationAction => ({ + type: 'DISMISS_EDITING_ANNOTATION', +}) + +export interface AddingAnnotationAction { + type: 'ADDING_ANNOTATION' +} +export const addingAnnotation = (): AddingAnnotationAction => ({ + type: 'ADDING_ANNOTATION', +}) + +export interface AddingAnnotationSuccessAction { + type: 'ADDING_ANNOTATION_SUCCESS' +} +export const addingAnnotationSuccess = (): AddingAnnotationSuccessAction => ({ + type: 'ADDING_ANNOTATION_SUCCESS', +}) + +export interface DismissAddingAnnotationAction { + type: 'DISMISS_ADDING_ANNOTATION' +} +export const dismissAddingAnnotation = (): DismissAddingAnnotationAction => ({ + type: 'DISMISS_ADDING_ANNOTATION', +}) + +export interface MouseEnterTempAnnotationAction { + type: 'MOUSEENTER_TEMP_ANNOTATION' +} +export const mouseEnterTempAnnotation = (): MouseEnterTempAnnotationAction => ({ + type: 'MOUSEENTER_TEMP_ANNOTATION', +}) + +export interface MouseLeaveTempAnnotationAction { + type: 'MOUSELEAVE_TEMP_ANNOTATION' +} +export const mouseLeaveTempAnnotation = (): MouseLeaveTempAnnotationAction => ({ + type: 'MOUSELEAVE_TEMP_ANNOTATION', +}) + +export interface LoadAnnotationsAction { + type: 'LOAD_ANNOTATIONS' + payload: { + annotations: AnnotationInterface[] + } +} +export const loadAnnotations = ( + annotations: AnnotationInterface[] +): LoadAnnotationsAction => ({ + type: 'LOAD_ANNOTATIONS', + payload: { + annotations, + }, +}) + +export interface UpdateAnnotationAction { + type: 'UPDATE_ANNOTATION' + payload: { + annotation: AnnotationInterface + } +} +export const updateAnnotation = ( + annotation: AnnotationInterface +): UpdateAnnotationAction => ({ + type: 'UPDATE_ANNOTATION', + payload: { + annotation, + }, +}) + +export interface DeleteAnnotationAction { + type: 'DELETE_ANNOTATION' + payload: { + annotation: AnnotationInterface + } +} +export const deleteAnnotation = ( + annotation: AnnotationInterface +): DeleteAnnotationAction => ({ + type: 'DELETE_ANNOTATION', + payload: { + annotation, + }, +}) + +export interface AddAnnotationAction { + type: 'ADD_ANNOTATION' + payload: { + annotation: AnnotationInterface + } +} +export const addAnnotation = ( + annotation: AnnotationInterface +): AddAnnotationAction => ({ + type: 'ADD_ANNOTATION', + payload: { + annotation, + }, +}) + +export const addAnnotationAsync = ( + createUrl: string, + annotation: AnnotationInterface +) => async dispatch => { + dispatch(addAnnotation(annotation)) + const savedAnnotation = await api.createAnnotation(createUrl, annotation) + dispatch(addAnnotation(savedAnnotation)) + dispatch(deleteAnnotation(annotation)) +} + +export interface AnnotationRange { + since: string + until: string +} + +export const getAnnotationsAsync = ( + indexUrl: string, + {since, until}: AnnotationRange +) => async dispatch => { + const annotations = await api.getAnnotations(indexUrl, since, until) + dispatch(loadAnnotations(annotations)) +} + +export const deleteAnnotationAsync = ( + annotation: AnnotationInterface +) => async dispatch => { + await api.deleteAnnotation(annotation) + dispatch(deleteAnnotation(annotation)) +} + +export const updateAnnotationAsync = ( + annotation: AnnotationInterface +) => async dispatch => { + await api.updateAnnotation(annotation) + dispatch(updateAnnotation(annotation)) +} diff --git a/ui/src/shared/components/Annotation.js b/ui/src/shared/components/Annotation.tsx similarity index 56% rename from ui/src/shared/components/Annotation.js rename to ui/src/shared/components/Annotation.tsx index 00919b1d0..91aa2c7f3 100644 --- a/ui/src/shared/components/Annotation.js +++ b/ui/src/shared/components/Annotation.tsx @@ -1,12 +1,20 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {SFC} from 'react' -import AnnotationPoint from 'shared/components/AnnotationPoint' -import AnnotationSpan from 'shared/components/AnnotationSpan' +import AnnotationPoint from 'src/shared/components/AnnotationPoint' +import AnnotationSpan from 'src/shared/components/AnnotationSpan' -import * as schema from 'shared/schemas' +import {AnnotationInterface, DygraphClass} from 'src/types' -const Annotation = ({ +interface Props { + mode: string + dWidth: number + xAxisRange: number[] + annotation: AnnotationInterface + dygraph: DygraphClass + staticLegendHeight: number +} + +const Annotation: SFC = ({ mode, dygraph, dWidth, @@ -37,15 +45,4 @@ const Annotation = ({
) -const {arrayOf, number, shape, string} = PropTypes - -Annotation.propTypes = { - mode: string, - dWidth: number, - xAxisRange: arrayOf(number), - annotation: schema.annotation.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, -} - export default Annotation diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx index a8f595959..a2cd5df1d 100644 --- a/ui/src/shared/components/Annotations.tsx +++ b/ui/src/shared/components/Annotations.tsx @@ -17,6 +17,7 @@ import {visibleAnnotations} from 'src/shared/annotations/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' import {AnnotationInterface, DygraphClass} from 'src/types' +import {UpdateAnnotationAction} from 'src/shared/actions/annotations' interface Props { dygraph: DygraphClass @@ -26,7 +27,9 @@ interface Props { annotations: AnnotationInterface[] mode: string isTempHovering: boolean - handleUpdateAnnotation: () => void + handleUpdateAnnotation: ( + annotation: AnnotationInterface + ) => UpdateAnnotationAction handleDismissAddingAnnotation: () => void handleAddingAnnotationSuccess: () => void handleMouseEnterTempAnnotation: () => void From 2db9b0a1a6e4f15d85b29142e81e52a3805e4dea Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Thu, 14 Jun 2018 17:52:26 -0700 Subject: [PATCH 3/5] Rerender annotations on drawCallback --- ui/src/shared/annotations/helpers.ts | 2 +- ui/src/shared/components/Annotation.tsx | 2 +- ui/src/shared/components/Annotations.tsx | 7 +++---- ui/src/shared/components/Dygraph.tsx | 15 ++++++++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts index e90e15a89..8e975caa0 100644 --- a/ui/src/shared/annotations/helpers.ts +++ b/ui/src/shared/annotations/helpers.ts @@ -14,7 +14,7 @@ export const TEMP_ANNOTATION: AnnotationInterface = { } export const visibleAnnotations = ( - xAxisRange: number[], + xAxisRange: [number, number], annotations: AnnotationInterface[] = [] ): AnnotationInterface[] => { const [xStart, xEnd] = xAxisRange diff --git a/ui/src/shared/components/Annotation.tsx b/ui/src/shared/components/Annotation.tsx index 91aa2c7f3..d95443386 100644 --- a/ui/src/shared/components/Annotation.tsx +++ b/ui/src/shared/components/Annotation.tsx @@ -8,7 +8,7 @@ import {AnnotationInterface, DygraphClass} from 'src/types' interface Props { mode: string dWidth: number - xAxisRange: number[] + xAxisRange: [number, number] annotation: AnnotationInterface dygraph: DygraphClass staticLegendHeight: number diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx index a2cd5df1d..139f3e32d 100644 --- a/ui/src/shared/components/Annotations.tsx +++ b/ui/src/shared/components/Annotations.tsx @@ -20,12 +20,12 @@ import {AnnotationInterface, DygraphClass} from 'src/types' import {UpdateAnnotationAction} from 'src/shared/actions/annotations' interface Props { - dygraph: DygraphClass dWidth: number - xAxisRange: number[] staticLegendHeight: number annotations: AnnotationInterface[] mode: string + xAxisRange: [number, number] + dygraph: DygraphClass isTempHovering: boolean handleUpdateAnnotation: ( annotation: AnnotationInterface @@ -52,7 +52,6 @@ class Annotations extends Component { handleMouseLeaveTempAnnotation, staticLegendHeight, } = this.props - console.log('rendering annotations') return (
{mode === ADDING && @@ -73,10 +72,10 @@ class Annotations extends Component { ))} diff --git a/ui/src/shared/components/Dygraph.tsx b/ui/src/shared/components/Dygraph.tsx index e0c79d6fe..cba2e7b14 100644 --- a/ui/src/shared/components/Dygraph.tsx +++ b/ui/src/shared/components/Dygraph.tsx @@ -74,6 +74,7 @@ interface Props { interface State { staticLegendHeight: null | number isMounted: boolean + xAxisRange: [number, number] } @ErrorHandling @@ -109,6 +110,7 @@ class Dygraph extends Component { this.state = { staticLegendHeight: null, isMounted: false, + xAxisRange: [0, 0], } this.graphRef = React.createRef() @@ -152,6 +154,7 @@ class Dygraph extends Component { }, zoomCallback: (lower: number, upper: number) => this.handleZoom(lower, upper), + drawCallback: () => this.handleDraw(), highlightCircleSize: 0, } @@ -170,7 +173,7 @@ class Dygraph extends Component { const {w} = this.dygraph.getArea() this.props.setResolution(w) - this.setState({isMounted: true}) + this.setState({isMounted: true, xAxisRange: this.dygraph.xAxisRange()}) } public componentWillUnmount() { @@ -246,7 +249,7 @@ class Dygraph extends Component { } public render() { - const {staticLegendHeight} = this.state + const {staticLegendHeight, xAxisRange} = this.state const {staticLegend, cellID} = this.props return ( @@ -258,7 +261,7 @@ class Dygraph extends Component { dygraph={this.dygraph} dWidth={this.dygraph.width_} staticLegendHeight={staticLegendHeight} - xAxisRange={this.dygraph.xAxisRange()} + xAxisRange={xAxisRange} /> )} { onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper)) } + private handleDraw = () => { + if (this.dygraph) { + this.setState({xAxisRange: this.dygraph.xAxisRange()}) + } + } + private eventToTimestamp = ({ pageX: pxBetweenMouseAndPage, }: MouseEvent): string => { From 35214a2ad391a772ee9ca12afaf58187a14ae603 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 15 Jun 2018 13:49:53 -0700 Subject: [PATCH 4/5] Convert Annotation sub-components to TS --- ui/src/alerts/components/SearchBar.tsx | 2 +- .../data_explorer/components/QueryEditor.tsx | 2 +- ui/src/shared/annotations/helpers.ts | 11 +- ...AnnotationInput.js => AnnotationInput.tsx} | 80 +++--- ...AnnotationPoint.js => AnnotationPoint.tsx} | 164 ++++++------ .../{AnnotationSpan.js => AnnotationSpan.tsx} | 119 +++++---- ...tationTooltip.js => AnnotationTooltip.tsx} | 112 ++++---- ...notationWindow.js => AnnotationWindow.tsx} | 35 ++- ui/src/shared/components/Annotations.tsx | 30 ++- ui/src/shared/components/DygraphLegend.tsx | 20 +- .../{NewAnnotation.js => NewAnnotation.tsx} | 247 +++++++++--------- ui/src/types/annotations.ts | 4 +- ui/src/types/dygraphs.ts | 1 + 13 files changed, 432 insertions(+), 395 deletions(-) rename ui/src/shared/components/{AnnotationInput.js => AnnotationInput.tsx} (69%) rename ui/src/shared/components/{AnnotationPoint.js => AnnotationPoint.tsx} (68%) rename ui/src/shared/components/{AnnotationSpan.js => AnnotationSpan.tsx} (77%) rename ui/src/shared/components/{AnnotationTooltip.js => AnnotationTooltip.tsx} (66%) rename ui/src/shared/components/{AnnotationWindow.js => AnnotationWindow.tsx} (70%) rename ui/src/shared/components/{NewAnnotation.js => NewAnnotation.tsx} (69%) diff --git a/ui/src/alerts/components/SearchBar.tsx b/ui/src/alerts/components/SearchBar.tsx index ce27e9364..f00e43394 100644 --- a/ui/src/alerts/components/SearchBar.tsx +++ b/ui/src/alerts/components/SearchBar.tsx @@ -12,7 +12,7 @@ interface State { @ErrorHandling class SearchBar extends PureComponent { - constructor(props) { + constructor(props: Props) { super(props) this.state = { diff --git a/ui/src/data_explorer/components/QueryEditor.tsx b/ui/src/data_explorer/components/QueryEditor.tsx index 0f64bdec8..07868e596 100644 --- a/ui/src/data_explorer/components/QueryEditor.tsx +++ b/ui/src/data_explorer/components/QueryEditor.tsx @@ -20,7 +20,7 @@ interface State { class QueryEditor extends PureComponent { private editor: React.RefObject - constructor(props) { + constructor(props: Props) { super(props) this.state = { value: this.props.query, diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts index 8e975caa0..503517114 100644 --- a/ui/src/shared/annotations/helpers.ts +++ b/ui/src/shared/annotations/helpers.ts @@ -9,8 +9,8 @@ export const TEMP_ANNOTATION: AnnotationInterface = { id: 'tempAnnotation', text: 'Name Me', type: '', - startTime: '', - endTime: '', + startTime: null, + endTime: null, } export const visibleAnnotations = ( @@ -24,10 +24,13 @@ export const visibleAnnotations = ( } return annotations.filter(a => { + if (a.startTime === null || a.endTime === null) { + return false + } if (a.endTime === a.startTime) { - return xStart <= +a.startTime && +a.startTime <= xEnd + return xStart <= a.startTime && a.startTime <= xEnd } - return !(+a.endTime < xStart || xEnd < +a.startTime) + return !(a.endTime < xStart || xEnd < a.startTime) }) } diff --git a/ui/src/shared/components/AnnotationInput.js b/ui/src/shared/components/AnnotationInput.tsx similarity index 69% rename from ui/src/shared/components/AnnotationInput.js rename to ui/src/shared/components/AnnotationInput.tsx index 0338bdb7c..8d8129048 100644 --- a/ui/src/shared/components/AnnotationInput.js +++ b/ui/src/shared/components/AnnotationInput.tsx @@ -1,45 +1,25 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, ChangeEvent, FocusEvent, KeyboardEvent} from 'react' import onClickOutside from 'react-onclickoutside' import {ErrorHandling} from 'src/shared/decorators/errors' +interface State { + isEditing: boolean +} + +interface Props { + value: string + onChangeInput: (i: string) => void + onConfirmUpdate: () => void + onRejectUpdate: () => void +} + @ErrorHandling -class AnnotationInput extends Component { - state = { +class AnnotationInput extends Component { + public state = { isEditing: false, } - handleInputClick = () => { - this.setState({isEditing: true}) - } - - handleKeyDown = e => { - const {onConfirmUpdate, onRejectUpdate} = this.props - - if (e.key === 'Enter') { - onConfirmUpdate() - this.setState({isEditing: false}) - } - if (e.key === 'Escape') { - onRejectUpdate() - this.setState({isEditing: false}) - } - } - - handleFocus = e => { - e.target.select() - } - - handleChange = e => { - this.props.onChangeInput(e.target.value) - } - - handleClickOutside = () => { - this.props.onConfirmUpdate() - this.setState({isEditing: false}) - } - - render() { + public render() { const {isEditing} = this.state const {value} = this.props @@ -65,15 +45,31 @@ class AnnotationInput extends Component {
) } -} -const {func, string} = PropTypes + private handleInputClick = () => { + this.setState({isEditing: true}) + } -AnnotationInput.propTypes = { - value: string, - onChangeInput: func.isRequired, - onConfirmUpdate: func.isRequired, - onRejectUpdate: func.isRequired, + private handleKeyDown = (e: KeyboardEvent) => { + const {onConfirmUpdate, onRejectUpdate} = this.props + + if (e.key === 'Enter') { + onConfirmUpdate() + this.setState({isEditing: false}) + } + if (e.key === 'Escape') { + onRejectUpdate() + this.setState({isEditing: false}) + } + } + + private handleFocus = (e: FocusEvent) => { + e.target.select() + } + + private handleChange = (e: ChangeEvent) => { + this.props.onChangeInput(e.target.value) + } } export default onClickOutside(AnnotationInput) diff --git a/ui/src/shared/components/AnnotationPoint.js b/ui/src/shared/components/AnnotationPoint.tsx similarity index 68% rename from ui/src/shared/components/AnnotationPoint.js rename to ui/src/shared/components/AnnotationPoint.tsx index 5d704d4de..831d046a3 100644 --- a/ui/src/shared/components/AnnotationPoint.js +++ b/ui/src/shared/components/AnnotationPoint.tsx @@ -1,49 +1,116 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent, DragEvent} from 'react' import {connect} from 'react-redux' import { DYGRAPH_CONTAINER_H_MARGIN, DYGRAPH_CONTAINER_V_MARGIN, DYGRAPH_CONTAINER_XLABEL_MARGIN, -} from 'shared/constants' -import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' -import AnnotationTooltip from 'shared/components/AnnotationTooltip' +} from 'src/shared/constants' +import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers' +import * as actions from 'src/shared/actions/annotations' +import AnnotationTooltip from 'src/shared/components/AnnotationTooltip' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass} from 'src/types' + +interface State { + isMouseOver: boolean + isDragging: boolean +} + +interface Props { + annotation: AnnotationInterface + mode: string + xAxisRange: [number, number] + dygraph: DygraphClass + updateAnnotation: (a: AnnotationInterface) => void + updateAnnotationAsync: (a: AnnotationInterface) => void + staticLegendHeight: number +} + @ErrorHandling -class AnnotationPoint extends React.Component { - state = { +class AnnotationPoint extends Component { + public static defaultProps: Partial = { + staticLegendHeight: 0, + } + + public state = { isMouseOver: false, isDragging: false, } - handleMouseEnter = () => { + public render() { + const {annotation, mode, dygraph, staticLegendHeight} = this.props + const {isDragging} = this.state + + const isEditing = mode === EDITING + + const flagClass = isDragging + ? 'annotation-point--flag__dragging' + : 'annotation-point--flag' + + const markerClass = isDragging ? 'annotation dragging' : 'annotation' + + const clickClass = isEditing + ? 'annotation--click-area editing' + : 'annotation--click-area' + + const markerStyles = { + left: `${dygraph.toDomXCoord(Number(annotation.startTime)) + + DYGRAPH_CONTAINER_H_MARGIN}px`, + height: `calc(100% - ${staticLegendHeight + + DYGRAPH_CONTAINER_XLABEL_MARGIN + + DYGRAPH_CONTAINER_V_MARGIN * 2}px)`, + } + + return ( +
+
+
+ +
+ ) + } + + private handleMouseEnter = () => { this.setState({isMouseOver: true}) } - handleMouseLeave = e => { + private handleMouseLeave = (e: MouseEvent) => { const {annotation} = this.props - - if (e.relatedTarget.id === `tooltip-${annotation.id}`) { - return this.setState({isDragging: false}) + if (e.relatedTarget instanceof Element) { + if (e.relatedTarget.id === `tooltip-${annotation.id}`) { + return this.setState({isDragging: false}) + } } this.setState({isMouseOver: false}) } - handleDragStart = () => { + private handleDragStart = () => { this.setState({isDragging: true}) } - handleDragEnd = () => { + private handleDragEnd = () => { const {annotation, updateAnnotationAsync} = this.props updateAnnotationAsync(annotation) this.setState({isDragging: false}) } - handleDrag = e => { + private handleDrag = (e: DragEvent) => { if (this.props.mode !== EDITING) { return } @@ -83,78 +150,19 @@ class AnnotationPoint extends React.Component { updateAnnotation({ ...annotation, - startTime: `${newTime}`, - endTime: `${newTime}`, + startTime: newTime, + endTime: newTime, }) e.preventDefault() e.stopPropagation() } - - render() { - const {annotation, mode, dygraph, staticLegendHeight} = this.props - const {isDragging} = this.state - - const isEditing = mode === EDITING - - const flagClass = isDragging - ? 'annotation-point--flag__dragging' - : 'annotation-point--flag' - - const markerClass = isDragging ? 'annotation dragging' : 'annotation' - - const clickClass = isEditing - ? 'annotation--click-area editing' - : 'annotation--click-area' - - const markerStyles = { - left: `${dygraph.toDomXCoord(annotation.startTime) + - DYGRAPH_CONTAINER_H_MARGIN}px`, - height: `calc(100% - ${staticLegendHeight + - DYGRAPH_CONTAINER_XLABEL_MARGIN + - DYGRAPH_CONTAINER_V_MARGIN * 2}px)`, - } - - return ( -
-
-
- -
- ) - } } -const {arrayOf, func, number, shape, string} = PropTypes - AnnotationPoint.defaultProps = { staticLegendHeight: 0, } -AnnotationPoint.propTypes = { - annotation: schema.annotation.isRequired, - mode: string.isRequired, - xAxisRange: arrayOf(number), - dygraph: shape({}).isRequired, - updateAnnotation: func.isRequired, - updateAnnotationAsync: func.isRequired, - staticLegendHeight: number, -} - const mdtp = { updateAnnotationAsync: actions.updateAnnotationAsync, updateAnnotation: actions.updateAnnotation, diff --git a/ui/src/shared/components/AnnotationSpan.js b/ui/src/shared/components/AnnotationSpan.tsx similarity index 77% rename from ui/src/shared/components/AnnotationSpan.js rename to ui/src/shared/components/AnnotationSpan.tsx index 3f4bdfbd0..f3f00b2f6 100644 --- a/ui/src/shared/components/AnnotationSpan.js +++ b/ui/src/shared/components/AnnotationSpan.tsx @@ -1,44 +1,83 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent, DragEvent} from 'react' import {connect} from 'react-redux' import { DYGRAPH_CONTAINER_H_MARGIN, DYGRAPH_CONTAINER_V_MARGIN, DYGRAPH_CONTAINER_XLABEL_MARGIN, -} from 'shared/constants' -import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' -import AnnotationTooltip from 'shared/components/AnnotationTooltip' -import AnnotationWindow from 'shared/components/AnnotationWindow' +} from 'src/shared/constants' + +import * as actions from 'src/shared/actions/annotations' +import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers' +import AnnotationTooltip from 'src/shared/components/AnnotationTooltip' +import AnnotationWindow from 'src/shared/components/AnnotationWindow' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass} from 'src/types' + +interface State { + isMouseOver: string + isDragging: string +} + +interface Props { + annotation: AnnotationInterface + mode: string + dygraph: DygraphClass + staticLegendHeight: number + updateAnnotation: (a: AnnotationInterface) => void + updateAnnotationAsync: (a: AnnotationInterface) => void + xAxisRange: [number, number] +} + @ErrorHandling -class AnnotationSpan extends React.Component { - state = { +class AnnotationSpan extends Component { + public static defaultProps: Partial = { + staticLegendHeight: 0, + } + + public state: State = { isDragging: null, isMouseOver: null, } - handleMouseEnter = direction => () => { + public render() { + const {annotation, dygraph, staticLegendHeight} = this.props + const {isDragging} = this.state + + return ( +
+ + {this.renderLeftMarker(annotation.startTime, dygraph)} + {this.renderRightMarker(annotation.endTime, dygraph)} +
+ ) + } + + private handleMouseEnter = (direction: string) => () => { this.setState({isMouseOver: direction}) } - handleMouseLeave = e => { + private handleMouseLeave = (e: MouseEvent) => { const {annotation} = this.props - - if (e.relatedTarget.id === `tooltip-${annotation.id}`) { - return this.setState({isDragging: null}) + if (e.relatedTarget instanceof Element) { + if (e.relatedTarget.id === `tooltip-${annotation.id}`) { + return this.setState({isDragging: null}) + } } this.setState({isMouseOver: null}) } - handleDragStart = direction => () => { + private handleDragStart = (direction: string) => () => { this.setState({isDragging: direction}) } - handleDragEnd = () => { + private handleDragEnd = () => { const {annotation, updateAnnotationAsync} = this.props const [startTime, endTime] = [ annotation.startTime, @@ -54,7 +93,7 @@ class AnnotationSpan extends React.Component { this.setState({isDragging: null}) } - handleDrag = timeProp => e => { + private handleDrag = (timeProp: string) => (e: DragEvent) => { if (this.props.mode !== EDITING) { return } @@ -96,7 +135,10 @@ class AnnotationSpan extends React.Component { e.stopPropagation() } - renderLeftMarker(startTime, dygraph) { + private renderLeftMarker( + startTime: number, + dygraph: DygraphClass + ): JSX.Element { const isEditing = this.props.mode === EDITING const {isDragging, isMouseOver} = this.state const {annotation, staticLegendHeight} = this.props @@ -147,7 +189,10 @@ class AnnotationSpan extends React.Component { ) } - renderRightMarker(endTime, dygraph) { + private renderRightMarker( + endTime: number, + dygraph: DygraphClass + ): JSX.Element { const isEditing = this.props.mode === EDITING const {isDragging, isMouseOver} = this.state const {annotation, staticLegendHeight} = this.props @@ -197,40 +242,6 @@ class AnnotationSpan extends React.Component {
) } - - render() { - const {annotation, dygraph, staticLegendHeight} = this.props - const {isDragging} = this.state - - return ( -
- - {this.renderLeftMarker(annotation.startTime, dygraph)} - {this.renderRightMarker(annotation.endTime, dygraph)} -
- ) - } -} - -const {arrayOf, func, number, shape, string} = PropTypes - -AnnotationSpan.defaultProps = { - staticLegendHeight: 0, -} - -AnnotationSpan.propTypes = { - annotation: schema.annotation.isRequired, - mode: string.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, - updateAnnotationAsync: func.isRequired, - updateAnnotation: func.isRequired, - xAxisRange: arrayOf(number), } const mapDispatchToProps = { diff --git a/ui/src/shared/components/AnnotationTooltip.js b/ui/src/shared/components/AnnotationTooltip.tsx similarity index 66% rename from ui/src/shared/components/AnnotationTooltip.js rename to ui/src/shared/components/AnnotationTooltip.tsx index e347cdec2..8121710df 100644 --- a/ui/src/shared/components/AnnotationTooltip.js +++ b/ui/src/shared/components/AnnotationTooltip.tsx @@ -1,50 +1,62 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent} from 'react' import {connect} from 'react-redux' import moment from 'moment' import classnames from 'classnames' import AnnotationInput from 'src/shared/components/AnnotationInput' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' +import * as actions from 'src/shared/actions/annotations' import {ErrorHandling} from 'src/shared/decorators/errors' -const TimeStamp = ({time}) => ( +import {AnnotationInterface} from 'src/types' + +interface TimeStampProps { + time: string +} + +const TimeStamp = ({time}: TimeStampProps): JSX.Element => (
{`${moment(+time).format('YYYY/MM/DD HH:mm:ss.SS')}`}
) +interface AnnotationState { + isDragging: boolean + isMouseOver: boolean +} + +interface Span { + spanCenter: number + tooltipLeft: number + spanWidth: number +} + +interface State { + annotation: AnnotationInterface +} + +interface Props { + isEditing: boolean + annotation: AnnotationInterface + timestamp: string + onMouseLeave: (e: MouseEvent) => {} + annotationState: AnnotationState + deleteAnnotationAsync: (a: AnnotationInterface) => void + updateAnnotationAsync: (a: AnnotationInterface) => void + span: Span +} + @ErrorHandling -class AnnotationTooltip extends Component { - state = { +class AnnotationTooltip extends Component { + public state = { annotation: this.props.annotation, } - componentWillReceiveProps = ({annotation}) => { + public componentWillReceiveProps(nextProps: Props) { + const {annotation} = nextProps this.setState({annotation}) } - handleChangeInput = key => value => { - const {annotation} = this.state - const newAnnotation = {...annotation, [key]: value} - - this.setState({annotation: newAnnotation}) - } - - handleConfirmUpdate = () => { - this.props.updateAnnotationAsync(this.state.annotation) - } - - handleRejectUpdate = () => { - this.setState({annotation: this.props.annotation}) - } - - handleDelete = () => { - this.props.deleteAnnotationAsync(this.props.annotation) - } - - render() { + public render() { const {annotation} = this.state const { onMouseLeave, @@ -99,28 +111,30 @@ class AnnotationTooltip extends Component {
) } + + private handleChangeInput = (key: string) => (value: string) => { + const {annotation} = this.state + const newAnnotation = {...annotation, [key]: value} + + this.setState({annotation: newAnnotation}) + } + + private handleConfirmUpdate = () => { + this.props.updateAnnotationAsync(this.state.annotation) + } + + private handleRejectUpdate = () => { + this.setState({annotation: this.props.annotation}) + } + + private handleDelete = () => { + this.props.deleteAnnotationAsync(this.props.annotation) + } } -const {bool, func, number, shape, string} = PropTypes - -TimeStamp.propTypes = { - time: string.isRequired, -} -AnnotationTooltip.propTypes = { - isEditing: bool, - annotation: schema.annotation.isRequired, - timestamp: string, - onMouseLeave: func.isRequired, - annotationState: shape({}), - deleteAnnotationAsync: func.isRequired, - updateAnnotationAsync: func.isRequired, - span: shape({ - spanCenter: number.isRequired, - spanWidth: number.isRequired, - }), -} - -export default connect(null, { +const mdtp = { deleteAnnotationAsync: actions.deleteAnnotationAsync, updateAnnotationAsync: actions.updateAnnotationAsync, -})(AnnotationTooltip) +} + +export default connect(null, mdtp)(AnnotationTooltip) diff --git a/ui/src/shared/components/AnnotationWindow.js b/ui/src/shared/components/AnnotationWindow.tsx similarity index 70% rename from ui/src/shared/components/AnnotationWindow.js rename to ui/src/shared/components/AnnotationWindow.tsx index 08c8c0540..78212decf 100644 --- a/ui/src/shared/components/AnnotationWindow.js +++ b/ui/src/shared/components/AnnotationWindow.tsx @@ -1,14 +1,23 @@ import React from 'react' -import PropTypes from 'prop-types' import { DYGRAPH_CONTAINER_H_MARGIN, DYGRAPH_CONTAINER_V_MARGIN, DYGRAPH_CONTAINER_XLABEL_MARGIN, -} from 'shared/constants' -import * as schema from 'shared/schemas' +} from 'src/shared/constants' +import {AnnotationInterface, DygraphClass} from 'src/types' -const windowDimensions = (anno, dygraph, staticLegendHeight) => { +interface WindowDimensionsReturn { + left: string + width: string + height: string +} + +const windowDimensions = ( + anno: AnnotationInterface, + dygraph: DygraphClass, + staticLegendHeight: number +): WindowDimensionsReturn => { // TODO: export and test this function const [startX, endX] = dygraph.xAxisRange() const startTime = Math.max(+anno.startTime, startX) @@ -34,25 +43,23 @@ const windowDimensions = (anno, dygraph, staticLegendHeight) => { } } +interface AnnotationWindowProps { + annotation: AnnotationInterface + dygraph: DygraphClass + active: boolean + staticLegendHeight: number +} + const AnnotationWindow = ({ annotation, dygraph, active, staticLegendHeight, -}) => ( +}: AnnotationWindowProps): JSX.Element => (
) -const {bool, number, shape} = PropTypes - -AnnotationWindow.propTypes = { - annotation: schema.annotation.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, - active: bool, -} - export default AnnotationWindow diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx index 139f3e32d..6190530f9 100644 --- a/ui/src/shared/components/Annotations.tsx +++ b/ui/src/shared/components/Annotations.tsx @@ -3,6 +3,7 @@ import {connect} from 'react-redux' import Annotation from 'src/shared/components/Annotation' import NewAnnotation from 'src/shared/components/NewAnnotation' +import {SourceContext} from 'src/CheckSources' import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' @@ -16,7 +17,7 @@ import { import {visibleAnnotations} from 'src/shared/annotations/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' -import {AnnotationInterface, DygraphClass} from 'src/types' +import {AnnotationInterface, DygraphClass, Source} from 'src/types' import {UpdateAnnotationAction} from 'src/shared/actions/annotations' interface Props { @@ -56,17 +57,22 @@ class Annotations extends Component {
{mode === ADDING && this.tempAnnotation && ( - + + {(source: Source) => ( + + )} + )} {this.annotations.map(a => ( void - onShow: (MouseEvent) => void + onShow: (e: MouseEvent) => void activeCellID: string setActiveCell: (cellID: string) => void } interface LegendData { - x: string | null + x: number series: SeriesLegendData[] xHTML: string } @@ -49,7 +45,7 @@ interface State { class DygraphLegend extends PureComponent { private legendRef: HTMLElement | null = null - constructor(props) { + constructor(props: Props) { super(props) this.props.dygraph.updateOptions({ @@ -199,7 +195,7 @@ class DygraphLegend extends PureComponent { this.setState({filterText}) } - private handleSortLegend = sortType => () => { + private handleSortLegend = (sortType: string) => () => { this.setState({sortType, isAscending: !this.state.isAscending}) } @@ -209,7 +205,7 @@ class DygraphLegend extends PureComponent { this.props.onShow(e) } - private legendFormatter = legend => { + private legendFormatter = (legend: LegendData) => { if (!legend.x) { return '' } @@ -229,7 +225,7 @@ class DygraphLegend extends PureComponent { return '' } - private unhighlightCallback = e => { + private unhighlightCallback = (e: MouseEvent) => { const {top, bottom, left, right} = this.legendRef.getBoundingClientRect() const mouseY = e.clientY diff --git a/ui/src/shared/components/NewAnnotation.js b/ui/src/shared/components/NewAnnotation.tsx similarity index 69% rename from ui/src/shared/components/NewAnnotation.js rename to ui/src/shared/components/NewAnnotation.tsx index 98e2b6ebe..a06e4dda6 100644 --- a/ui/src/shared/components/NewAnnotation.js +++ b/ui/src/shared/components/NewAnnotation.tsx @@ -1,120 +1,47 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent} from 'react' import classnames from 'classnames' import {connect} from 'react-redux' import uuid from 'uuid' -import OnClickOutside from 'shared/components/OnClickOutside' -import AnnotationWindow from 'shared/components/AnnotationWindow' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' +import OnClickOutside from 'src/shared/components/OnClickOutside' +import AnnotationWindow from 'src/shared/components/AnnotationWindow' +import * as actions from 'src/shared/actions/annotations' -import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'shared/constants' +import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'src/shared/constants' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass, Source} from 'src/types' + +interface Props { + dygraph: DygraphClass + source: Source + isTempHovering: boolean + tempAnnotation: AnnotationInterface + addAnnotationAsync: (url: string, a: AnnotationInterface) => void + onDismissAddingAnnotation: () => void + onAddingAnnotationSuccess: () => void + onUpdateAnnotation: (a: AnnotationInterface) => void + onMouseEnterTempAnnotation: () => void + onMouseLeaveTempAnnotation: () => void + staticLegendHeight: number +} +interface State { + isMouseOver: boolean + gatherMode: string +} @ErrorHandling -class NewAnnotation extends Component { - state = { - isMouseOver: false, - gatherMode: 'startTime', - } - - clampWithinGraphTimerange = timestamp => { - const [xRangeStart] = this.props.dygraph.xAxisRange() - return Math.max(xRangeStart, timestamp) - } - - eventToTimestamp = ({pageX: pxBetweenMouseAndPage}) => { - const {left: pxBetweenGraphAndPage} = this.wrapper.getBoundingClientRect() - const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage - const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate) - const clamped = this.clampWithinGraphTimerange(timestamp) - return `${clamped}` - } - - handleMouseDown = e => { - const startTime = this.eventToTimestamp(e) - this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime}) - this.setState({gatherMode: 'endTime'}) - } - - handleMouseMove = e => { - if (this.props.isTempHovering === false) { - return - } - - const {tempAnnotation, onUpdateAnnotation} = this.props - const newTime = this.eventToTimestamp(e) - - if (this.state.gatherMode === 'startTime') { - onUpdateAnnotation({ - ...tempAnnotation, - startTime: newTime, - endTime: newTime, - }) - } else { - onUpdateAnnotation({...tempAnnotation, endTime: newTime}) - } - } - - handleMouseUp = e => { - const { - tempAnnotation, - onUpdateAnnotation, - addAnnotationAsync, - onAddingAnnotationSuccess, - onMouseLeaveTempAnnotation, - } = this.props - const createUrl = this.context.source.links.annotations - - const upTime = this.eventToTimestamp(e) - const downTime = tempAnnotation.startTime - const [startTime, endTime] = [downTime, upTime].sort() - const newAnnotation = {...tempAnnotation, startTime, endTime} - - onUpdateAnnotation(newAnnotation) - addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()}) - - onAddingAnnotationSuccess() - onMouseLeaveTempAnnotation() - - this.setState({ +class NewAnnotation extends Component { + public wrapperRef: React.RefObject + constructor(props: Props) { + super(props) + this.wrapperRef = React.createRef() + this.state = { isMouseOver: false, gatherMode: 'startTime', - }) - } - - handleMouseOver = e => { - this.setState({isMouseOver: true}) - this.handleMouseMove(e) - this.props.onMouseEnterTempAnnotation() - } - - handleMouseLeave = () => { - this.setState({isMouseOver: false}) - this.props.onMouseLeaveTempAnnotation() - } - - handleClickOutside = () => { - const {onDismissAddingAnnotation, isTempHovering} = this.props - - if (!isTempHovering) { - onDismissAddingAnnotation() } } - renderTimestamp(time) { - const timestamp = `${new Date(+time)}` - - return ( -
- Click or Drag to Annotate - {timestamp} -
- ) - } - - render() { + public render() { const { dygraph, isTempHovering, @@ -123,7 +50,6 @@ class NewAnnotation extends Component { staticLegendHeight, } = this.props const {isMouseOver} = this.state - const crosshairOne = Math.max(-1000, dygraph.toDomXCoord(startTime)) const crosshairTwo = dygraph.toDomXCoord(endTime) const crosshairHeight = `calc(100% - ${staticLegendHeight + @@ -154,7 +80,7 @@ class NewAnnotation extends Component { className={classnames('new-annotation', { hover: isTempHovering, })} - ref={el => (this.wrapper = el)} + ref={this.wrapperRef} onMouseMove={this.handleMouseMove} onMouseOver={this.handleMouseOver} onMouseLeave={this.handleMouseLeave} @@ -185,29 +111,98 @@ class NewAnnotation extends Component {
) } -} -const {bool, func, number, shape, string} = PropTypes + private clampWithinGraphTimerange = (timestamp: number): number => { + const [xRangeStart] = this.props.dygraph.xAxisRange() + return Math.max(xRangeStart, timestamp) + } -NewAnnotation.contextTypes = { - source: shape({ - links: shape({ - annotations: string, - }), - }), -} + private eventToTimestamp = ({ + pageX: pxBetweenMouseAndPage, + }: MouseEvent): number => { + const { + left: pxBetweenGraphAndPage, + } = this.wrapperRef.current.getBoundingClientRect() + const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage + const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate) + const clamped = this.clampWithinGraphTimerange(timestamp) + return clamped + } -NewAnnotation.propTypes = { - dygraph: shape({}).isRequired, - isTempHovering: bool, - tempAnnotation: schema.annotation.isRequired, - addAnnotationAsync: func.isRequired, - onDismissAddingAnnotation: func.isRequired, - onAddingAnnotationSuccess: func.isRequired, - onUpdateAnnotation: func.isRequired, - onMouseEnterTempAnnotation: func.isRequired, - onMouseLeaveTempAnnotation: func.isRequired, - staticLegendHeight: number, + private handleMouseDown = (e: MouseEvent) => { + const startTime = this.eventToTimestamp(e) + this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime}) + this.setState({gatherMode: 'endTime'}) + } + + private handleMouseMove = (e: MouseEvent) => { + if (this.props.isTempHovering === false) { + return + } + + const {tempAnnotation, onUpdateAnnotation} = this.props + const newTime = this.eventToTimestamp(e) + + if (this.state.gatherMode === 'startTime') { + onUpdateAnnotation({ + ...tempAnnotation, + startTime: newTime, + endTime: newTime, + }) + } else { + onUpdateAnnotation({...tempAnnotation, endTime: newTime}) + } + } + + private handleMouseUp = (e: MouseEvent) => { + const { + tempAnnotation, + onUpdateAnnotation, + addAnnotationAsync, + onAddingAnnotationSuccess, + onMouseLeaveTempAnnotation, + source, + } = this.props + const createUrl = source.links.annotations + + const upTime = this.eventToTimestamp(e) + const downTime = tempAnnotation.startTime + const [startTime, endTime] = [downTime, upTime].sort() + const newAnnotation = {...tempAnnotation, startTime, endTime} + + onUpdateAnnotation(newAnnotation) + addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()}) + + onAddingAnnotationSuccess() + onMouseLeaveTempAnnotation() + + this.setState({ + isMouseOver: false, + gatherMode: 'startTime', + }) + } + + private handleMouseOver = (e: MouseEvent) => { + this.setState({isMouseOver: true}) + this.handleMouseMove(e) + this.props.onMouseEnterTempAnnotation() + } + + private handleMouseLeave = () => { + this.setState({isMouseOver: false}) + this.props.onMouseLeaveTempAnnotation() + } + + private renderTimestamp(time: number): JSX.Element { + const timestamp = `${new Date(time)}` + + return ( +
+ Click or Drag to Annotate + {timestamp} +
+ ) + } } const mdtp = { diff --git a/ui/src/types/annotations.ts b/ui/src/types/annotations.ts index b338a2501..690e51386 100644 --- a/ui/src/types/annotations.ts +++ b/ui/src/types/annotations.ts @@ -1,7 +1,7 @@ export interface AnnotationInterface { id: string - startTime: string - endTime: string + startTime: number + endTime: number text: string type: string } diff --git a/ui/src/types/dygraphs.ts b/ui/src/types/dygraphs.ts index 2aef990b6..5c9f8bbea 100644 --- a/ui/src/types/dygraphs.ts +++ b/ui/src/types/dygraphs.ts @@ -495,6 +495,7 @@ export declare class DygraphClass { // tslint:disable-next-line:variable-name public width_: number + public graphDiv: HTMLElement constructor( container: HTMLElement | string, From e90ab6433e2bc57336c98c6b5bc4f28948cfb623 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 15 Jun 2018 15:20:24 -0700 Subject: [PATCH 5/5] Convert Annotation action and reducer to TS --- ui/src/shared/actions/annotations.ts | 18 +++++- ui/src/shared/annotations/helpers.ts | 1 + ui/src/shared/apis/annotation.js | 40 ------------ ui/src/shared/apis/annotation.ts | 63 +++++++++++++++++++ .../{annotations.js => annotations.ts} | 14 ++++- ui/src/types/annotations.ts | 1 + ...nnotations.test.js => annotations.test.ts} | 36 ++++++----- 7 files changed, 112 insertions(+), 61 deletions(-) delete mode 100644 ui/src/shared/apis/annotation.js create mode 100644 ui/src/shared/apis/annotation.ts rename ui/src/shared/reducers/{annotations.js => annotations.ts} (88%) rename ui/test/annotations/reducers/{annotations.test.js => annotations.test.ts} (59%) diff --git a/ui/src/shared/actions/annotations.ts b/ui/src/shared/actions/annotations.ts index 2f62285fb..47a8421ff 100644 --- a/ui/src/shared/actions/annotations.ts +++ b/ui/src/shared/actions/annotations.ts @@ -1,7 +1,19 @@ import * as api from 'src/shared/apis/annotation' - import {AnnotationInterface} from 'src/types' +export type Action = + | EditingAnnotationAction + | DismissEditingAnnotationAction + | AddingAnnotationAction + | AddingAnnotationSuccessAction + | DismissAddingAnnotationAction + | MouseEnterTempAnnotationAction + | MouseLeaveTempAnnotationAction + | LoadAnnotationsAction + | UpdateAnnotationAction + | DeleteAnnotationAction + | AddAnnotationAction + export interface EditingAnnotationAction { type: 'EDITING_ANNOTATION' } @@ -122,8 +134,8 @@ export const addAnnotationAsync = ( } export interface AnnotationRange { - since: string - until: string + since: number + until: number } export const getAnnotationsAsync = ( diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts index 503517114..7b44bb178 100644 --- a/ui/src/shared/annotations/helpers.ts +++ b/ui/src/shared/annotations/helpers.ts @@ -11,6 +11,7 @@ export const TEMP_ANNOTATION: AnnotationInterface = { type: '', startTime: null, endTime: null, + links: {self: ''}, } export const visibleAnnotations = ( diff --git a/ui/src/shared/apis/annotation.js b/ui/src/shared/apis/annotation.js deleted file mode 100644 index e36c24859..000000000 --- a/ui/src/shared/apis/annotation.js +++ /dev/null @@ -1,40 +0,0 @@ -import AJAX from 'src/utils/ajax' - -const msToRFC = ms => ms && new Date(parseInt(ms, 10)).toISOString() -const rfcToMS = rfc3339 => rfc3339 && JSON.stringify(Date.parse(rfc3339)) -const annoToMillisecond = anno => ({ - ...anno, - startTime: rfcToMS(anno.startTime), - endTime: rfcToMS(anno.endTime), -}) -const annoToRFC = anno => ({ - ...anno, - startTime: msToRFC(anno.startTime), - endTime: msToRFC(anno.endTime), -}) - -export const createAnnotation = async (url, annotation) => { - const data = annoToRFC(annotation) - const response = await AJAX({method: 'POST', url, data}) - return annoToMillisecond(response.data) -} - -export const getAnnotations = async (url, since, until) => { - const {data} = await AJAX({ - method: 'GET', - url, - params: {since: msToRFC(since), until: msToRFC(until)}, - }) - return data.annotations.map(annoToMillisecond) -} - -export const deleteAnnotation = async annotation => { - const url = annotation.links.self - await AJAX({method: 'DELETE', url}) -} - -export const updateAnnotation = async annotation => { - const url = annotation.links.self - const data = annoToRFC(annotation) - await AJAX({method: 'PATCH', url, data}) -} diff --git a/ui/src/shared/apis/annotation.ts b/ui/src/shared/apis/annotation.ts new file mode 100644 index 000000000..d5d82e985 --- /dev/null +++ b/ui/src/shared/apis/annotation.ts @@ -0,0 +1,63 @@ +import AJAX from 'src/utils/ajax' +import {AnnotationInterface} from 'src/types' + +const msToRFCString = (ms: number) => + ms && new Date(Math.round(ms)).toISOString() + +const rfcStringToMS = (rfc3339: string) => rfc3339 && Date.parse(rfc3339) + +interface ServerAnnotation { + id: string + startTime: string + endTime: string + text: string + type: string + links: {self: string} +} + +const annoToMillisecond = ( + annotation: ServerAnnotation +): AnnotationInterface => ({ + ...annotation, + startTime: rfcStringToMS(annotation.startTime), + endTime: rfcStringToMS(annotation.endTime), +}) + +const annoToRFC = (annotation: AnnotationInterface): ServerAnnotation => ({ + ...annotation, + startTime: msToRFCString(annotation.startTime), + endTime: msToRFCString(annotation.endTime), +}) + +export const createAnnotation = async ( + url: string, + annotation: AnnotationInterface +) => { + const data = annoToRFC(annotation) + const response = await AJAX({method: 'POST', url, data}) + return annoToMillisecond(response.data) +} + +export const getAnnotations = async ( + url: string, + since: number, + until: number +) => { + const {data} = await AJAX({ + method: 'GET', + url, + params: {since: msToRFCString(since), until: msToRFCString(until)}, + }) + return data.annotations.map(annoToMillisecond) +} + +export const deleteAnnotation = async (annotation: AnnotationInterface) => { + const url = annotation.links.self + await AJAX({method: 'DELETE', url}) +} + +export const updateAnnotation = async (annotation: AnnotationInterface) => { + const url = annotation.links.self + const data = annoToRFC(annotation) + await AJAX({method: 'PATCH', url, data}) +} diff --git a/ui/src/shared/reducers/annotations.js b/ui/src/shared/reducers/annotations.ts similarity index 88% rename from ui/src/shared/reducers/annotations.js rename to ui/src/shared/reducers/annotations.ts index 658f2803e..f1c3ebb65 100644 --- a/ui/src/shared/reducers/annotations.js +++ b/ui/src/shared/reducers/annotations.ts @@ -1,12 +1,24 @@ import {ADDING, EDITING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' +import {Action} from 'src/shared/actions/annotations' +import {AnnotationInterface} from 'src/types' + +export interface AnnotationState { + mode: string + isTempHovering: boolean + annotations: AnnotationInterface[] +} + const initialState = { mode: null, isTempHovering: false, annotations: [], } -const annotationsReducer = (state = initialState, action) => { +const annotationsReducer = ( + state: AnnotationState = initialState, + action: Action +) => { switch (action.type) { case 'EDITING_ANNOTATION': { return { diff --git a/ui/src/types/annotations.ts b/ui/src/types/annotations.ts index 690e51386..e35bb0628 100644 --- a/ui/src/types/annotations.ts +++ b/ui/src/types/annotations.ts @@ -4,4 +4,5 @@ export interface AnnotationInterface { endTime: number text: string type: string + links: {self: string} } diff --git a/ui/test/annotations/reducers/annotations.test.js b/ui/test/annotations/reducers/annotations.test.ts similarity index 59% rename from ui/test/annotations/reducers/annotations.test.js rename to ui/test/annotations/reducers/annotations.test.ts index fb7cddffd..8e402e236 100644 --- a/ui/test/annotations/reducers/annotations.test.js +++ b/ui/test/annotations/reducers/annotations.test.ts @@ -1,45 +1,47 @@ -import reducer from 'shared/reducers/annotations' +import reducer from 'src/shared/reducers/annotations' +import {AnnotationInterface} from 'src/types' +import {AnnotationState} from 'src/shared/reducers/annotations' import { addAnnotation, deleteAnnotation, loadAnnotations, updateAnnotation, -} from 'shared/actions/annotations' +} from 'src/shared/actions/annotations' -const a1 = { +const a1: AnnotationInterface = { id: '1', - group: '', - name: 'anno1', - time: '1515716169000', - duration: '', + startTime: 1515716169000, + endTime: 1515716169000, + type: '', text: 'you have no swoggels', + links: {self: 'to/thine/own/self/be/true'}, } -const a2 = { +const a2: AnnotationInterface = { id: '2', - group: '', - name: 'anno1', - time: '1515716169000', - duration: '', - text: 'you have no swoggels', + startTime: 1515716169000, + endTime: 1515716169002, + type: '', + text: 'you have so many swoggels', + links: {self: 'self/in/eye/of/beholder'}, } -const state = { +const state: AnnotationState = { + isTempHovering: false, mode: null, annotations: [], } describe('Shared.Reducers.annotations', () => { it('can load the annotations', () => { - const expected = [{time: '0', duration: ''}] + const expected = [a1] const actual = reducer(state, loadAnnotations(expected)) expect(actual.annotations).toEqual(expected) }) - it('can update an annotation', () => { - const expected = [{...a1, time: ''}] + const expected = [{...a1, startTime: 6666666666666}] const actual = reducer( {...state, annotations: [a1]}, updateAnnotation(expected[0])