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 67a3a9340..8bda7203f 100644 --- a/ui/src/data_explorer/components/QueryEditor.tsx +++ b/ui/src/data_explorer/components/QueryEditor.tsx @@ -23,7 +23,7 @@ interface State { @ErrorHandling class QueryEditor extends PureComponent { - constructor(props) { + constructor(props: Props) { super(props) this.state = { value: this.props.query, 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..47a8421ff --- /dev/null +++ b/ui/src/shared/actions/annotations.ts @@ -0,0 +1,161 @@ +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' +} +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: number + until: number +} + +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/annotations/helpers.js b/ui/src/shared/annotations/helpers.js deleted file mode 100644 index 739378372..000000000 --- a/ui/src/shared/annotations/helpers.js +++ /dev/null @@ -1,28 +0,0 @@ -export const ANNOTATION_MIN_DELTA = 0.5 - -export const ADDING = 'adding' -export const EDITING = 'editing' - -export const TEMP_ANNOTATION = { - id: 'tempAnnotation', - text: 'Name Me', - type: '', - startTime: '', - endTime: '', -} - -export const visibleAnnotations = (graph, annotations = []) => { - const [xStart, xEnd] = graph.xAxisRange() - - if (xStart === 0 && xEnd === 0) { - return [] - } - - return annotations.filter(a => { - if (a.endTime === a.startTime) { - return xStart <= +a.startTime && +a.startTime <= xEnd - } - - return !(+a.endTime < xStart || xEnd < +a.startTime) - }) -} diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts new file mode 100644 index 000000000..7b44bb178 --- /dev/null +++ b/ui/src/shared/annotations/helpers.ts @@ -0,0 +1,37 @@ +import {AnnotationInterface} from 'src/types' + +export const ANNOTATION_MIN_DELTA = 0.5 + +export const ADDING = 'adding' +export const EDITING = 'editing' + +export const TEMP_ANNOTATION: AnnotationInterface = { + id: 'tempAnnotation', + text: 'Name Me', + type: '', + startTime: null, + endTime: null, + links: {self: ''}, +} + +export const visibleAnnotations = ( + xAxisRange: [number, number], + annotations: AnnotationInterface[] = [] +): AnnotationInterface[] => { + const [xStart, xEnd] = xAxisRange + + if (xStart === 0 && xEnd === 0) { + return [] + } + + 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 !(a.endTime < xStart || xEnd < a.startTime) + }) +} 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/components/Annotation.js b/ui/src/shared/components/Annotation.tsx similarity index 52% rename from ui/src/shared/components/Annotation.js rename to ui/src/shared/components/Annotation.tsx index 26d6fbfb4..d95443386 100644 --- a/ui/src/shared/components/Annotation.js +++ b/ui/src/shared/components/Annotation.tsx @@ -1,15 +1,24 @@ -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, number] + annotation: AnnotationInterface + dygraph: DygraphClass + staticLegendHeight: number +} + +const Annotation: SFC = ({ mode, dygraph, dWidth, + xAxisRange, annotation, staticLegendHeight, }) => ( @@ -21,6 +30,7 @@ const Annotation = ({ annotation={annotation} dWidth={dWidth} staticLegendHeight={staticLegendHeight} + xAxisRange={xAxisRange} /> ) : ( )} ) -const {number, shape, string} = PropTypes - -Annotation.propTypes = { - mode: string, - dWidth: number, - annotation: schema.annotation.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, -} - export default Annotation 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 cdc529d24..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,77 +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 {func, number, shape, string} = PropTypes - AnnotationPoint.defaultProps = { staticLegendHeight: 0, } -AnnotationPoint.propTypes = { - annotation: schema.annotation.isRequired, - mode: string.isRequired, - 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 aed861a63..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,39 +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 {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, } 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.js b/ui/src/shared/components/Annotations.js deleted file mode 100644 index fdb9665e9..000000000 --- a/ui/src/shared/components/Annotations.js +++ /dev/null @@ -1,124 +0,0 @@ -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' - -import { - updateAnnotation, - addingAnnotationSuccess, - dismissAddingAnnotation, - mouseEnterTempAnnotation, - mouseLeaveTempAnnotation, -} from 'src/shared/actions/annotations' -import {visibleAnnotations} from 'src/shared/annotations/helpers' -import {ErrorHandling} from 'src/shared/decorators/errors' - -@ErrorHandling -class Annotations extends Component { - render() { - const { - mode, - dWidth, - dygraph, - isTempHovering, - handleUpdateAnnotation, - handleDismissAddingAnnotation, - handleAddingAnnotationSuccess, - handleMouseEnterTempAnnotation, - handleMouseLeaveTempAnnotation, - staticLegendHeight, - } = this.props - - return ( -
- {mode === ADDING && - this.tempAnnotation && ( - - )} - {this.annotations.map(a => ( - - ))} -
- ) - } - - get annotations() { - return visibleAnnotations( - this.props.dygraph, - this.props.annotations - ).filter(a => a.id !== TEMP_ANNOTATION.id) - } - - get tempAnnotation() { - return this.props.annotations.find(a => a.id === TEMP_ANNOTATION.id) - } -} - -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}, -}) => ({ - 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), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(Annotations) diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx new file mode 100644 index 000000000..6190530f9 --- /dev/null +++ b/ui/src/shared/components/Annotations.tsx @@ -0,0 +1,118 @@ +import React, {Component} from 'react' +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' + +import { + updateAnnotation, + addingAnnotationSuccess, + dismissAddingAnnotation, + mouseEnterTempAnnotation, + mouseLeaveTempAnnotation, +} from 'src/shared/actions/annotations' +import {visibleAnnotations} from 'src/shared/annotations/helpers' +import {ErrorHandling} from 'src/shared/decorators/errors' + +import {AnnotationInterface, DygraphClass, Source} from 'src/types' +import {UpdateAnnotationAction} from 'src/shared/actions/annotations' + +interface Props { + dWidth: number + staticLegendHeight: number + annotations: AnnotationInterface[] + mode: string + xAxisRange: [number, number] + dygraph: DygraphClass + isTempHovering: boolean + handleUpdateAnnotation: ( + annotation: AnnotationInterface + ) => UpdateAnnotationAction + handleDismissAddingAnnotation: () => void + handleAddingAnnotationSuccess: () => void + handleMouseEnterTempAnnotation: () => void + handleMouseLeaveTempAnnotation: () => void +} + +@ErrorHandling +class Annotations extends Component { + public render() { + const { + mode, + dWidth, + dygraph, + xAxisRange, + isTempHovering, + handleUpdateAnnotation, + handleDismissAddingAnnotation, + handleAddingAnnotationSuccess, + handleMouseEnterTempAnnotation, + handleMouseLeaveTempAnnotation, + staticLegendHeight, + } = this.props + return ( +
+ {mode === ADDING && + this.tempAnnotation && ( + + {(source: Source) => ( + + )} + + )} + {this.annotations.map(a => ( + + ))} +
+ ) + } + + get annotations() { + return visibleAnnotations( + this.props.xAxisRange, + this.props.annotations + ).filter(a => a.id !== TEMP_ANNOTATION.id) + } + + get tempAnnotation() { + return this.props.annotations.find(a => a.id === TEMP_ANNOTATION.id) + } +} + +const mstp = ({annotations: {annotations, mode, isTempHovering}}) => ({ + annotations, + mode: mode || 'NORMAL', + isTempHovering, +}) + +const mdtp = { + handleAddingAnnotationSuccess: addingAnnotationSuccess, + handleDismissAddingAnnotation: dismissAddingAnnotation, + handleMouseEnterTempAnnotation: mouseEnterTempAnnotation, + handleMouseLeaveTempAnnotation: mouseLeaveTempAnnotation, + handleUpdateAnnotation: updateAnnotation, +} + +export default connect(mstp, mdtp)(Annotations) diff --git a/ui/src/shared/components/Dygraph.tsx b/ui/src/shared/components/Dygraph.tsx index 852802527..78922f75e 100644 --- a/ui/src/shared/components/Dygraph.tsx +++ b/ui/src/shared/components/Dygraph.tsx @@ -75,6 +75,7 @@ interface Props { interface State { staticLegendHeight: null | number isMounted: boolean + xAxisRange: [number, number] } @ErrorHandling @@ -110,6 +111,7 @@ class Dygraph extends Component { this.state = { staticLegendHeight: null, isMounted: false, + xAxisRange: [0, 0], } this.graphRef = React.createRef() @@ -153,6 +155,7 @@ class Dygraph extends Component { }, zoomCallback: (lower: number, upper: number) => this.handleZoom(lower, upper), + drawCallback: () => this.handleDraw(), highlightCircleSize: 0, } @@ -171,7 +174,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() { @@ -247,7 +250,7 @@ class Dygraph extends Component { } public render() { - const {staticLegendHeight} = this.state + const {staticLegendHeight, xAxisRange} = this.state const {staticLegend, cellID} = this.props return ( @@ -259,6 +262,7 @@ class Dygraph extends Component { dygraph={this.dygraph} dWidth={this.dygraph.width_} staticLegendHeight={staticLegendHeight} + 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 => { diff --git a/ui/src/shared/components/DygraphLegend.tsx b/ui/src/shared/components/DygraphLegend.tsx index e27dea1f7..1eb08ee80 100644 --- a/ui/src/shared/components/DygraphLegend.tsx +++ b/ui/src/shared/components/DygraphLegend.tsx @@ -1,6 +1,5 @@ import React, {PureComponent, ChangeEvent} from 'react' import {connect} from 'react-redux' -import Dygraph from 'dygraphs' import _ from 'lodash' import classnames from 'classnames' @@ -13,22 +12,19 @@ import DygraphLegendSort from 'src/shared/components/DygraphLegendSort' import {makeLegendStyles} from 'src/shared/graphs/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' import {NO_CELL} from 'src/shared/constants' - -interface ExtendedDygraph extends Dygraph { - graphDiv: HTMLElement -} +import {DygraphClass} from 'src/types' interface Props { - dygraph: ExtendedDygraph + dygraph: DygraphClass cellID: string onHide: () => 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 } @@ -48,7 +44,7 @@ interface State { class DygraphLegend extends PureComponent { private legendRef: HTMLElement | null = null - constructor(props) { + constructor(props: Props) { super(props) this.props.dygraph.updateOptions({ @@ -175,7 +171,7 @@ class DygraphLegend extends PureComponent { this.setState({filterText}) } - private handleSortLegend = sortType => () => { + private handleSortLegend = (sortType: string) => () => { this.setState({sortType, isAscending: !this.state.isAscending}) } @@ -185,7 +181,7 @@ class DygraphLegend extends PureComponent { this.props.onShow(e) } - private legendFormatter = legend => { + private legendFormatter = (legend: LegendData) => { if (!legend.x) { return '' } @@ -205,7 +201,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/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 new file mode 100644 index 000000000..e35bb0628 --- /dev/null +++ b/ui/src/types/annotations.ts @@ -0,0 +1,8 @@ +export interface AnnotationInterface { + id: string + startTime: number + endTime: number + text: string + type: string + links: {self: 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, diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 60da481c9..26b0d4d16 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -40,6 +40,7 @@ import { DygraphClass, DygraphData, } from './dygraphs' +import {AnnotationInterface} from './annotations' export { Me, @@ -96,4 +97,5 @@ export { SchemaFilter, RemoteDataState, URLQueryParams, + AnnotationInterface, } 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])