Merge pull request #3689 from influxdata/bugfix/annotations-render-on-zoom

Bugfix/annotations render on zoom
pull/3698/head^2
Deniz Kusefoglu 2018-06-15 17:13:31 -07:00 committed by GitHub
commit 09d9cbf8a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 855 additions and 685 deletions

View File

@ -12,7 +12,7 @@ interface State {
@ErrorHandling
class SearchBar extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props)
this.state = {

View File

@ -23,7 +23,7 @@ interface State {
@ErrorHandling
class QueryEditor extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props)
this.state = {
value: this.props.query,

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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)
})
}

View File

@ -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)
})
}

View File

@ -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})
}

View File

@ -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})
}

View File

@ -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<Props> = ({
mode,
dygraph,
dWidth,
xAxisRange,
annotation,
staticLegendHeight,
}) => (
@ -21,6 +30,7 @@ const Annotation = ({
annotation={annotation}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
) : (
<AnnotationSpan
@ -29,19 +39,10 @@ const Annotation = ({
annotation={annotation}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
)}
</div>
)
const {number, shape, string} = PropTypes
Annotation.propTypes = {
mode: string,
dWidth: number,
annotation: schema.annotation.isRequired,
dygraph: shape({}).isRequired,
staticLegendHeight: number,
}
export default Annotation

View File

@ -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<Props, State> {
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 {
</div>
)
}
}
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<HTMLInputElement>) => {
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<HTMLInputElement>) => {
e.target.select()
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
this.props.onChangeInput(e.target.value)
}
}
export default onClickOutside(AnnotationInput)

View File

@ -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<Props, State> {
public static defaultProps: Partial<Props> = {
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 (
<div className={markerClass} style={markerStyles}>
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.startTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
</div>
)
}
private handleMouseEnter = () => {
this.setState({isMouseOver: true})
}
handleMouseLeave = e => {
private handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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 (
<div className={markerClass} style={markerStyles}>
<div
className={clickClass}
draggable={true}
onDrag={this.handleDrag}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
<div className={flagClass} />
<AnnotationTooltip
isEditing={isEditing}
timestamp={annotation.startTime}
annotation={annotation}
onMouseLeave={this.handleMouseLeave}
annotationState={this.state}
/>
</div>
)
}
}
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,

View File

@ -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<Props, State> {
public static defaultProps: Partial<Props> = {
staticLegendHeight: 0,
}
public state: State = {
isDragging: null,
isMouseOver: null,
}
handleMouseEnter = direction => () => {
public render() {
const {annotation, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
return (
<div>
<AnnotationWindow
annotation={annotation}
dygraph={dygraph}
active={!!isDragging}
staticLegendHeight={staticLegendHeight}
/>
{this.renderLeftMarker(annotation.startTime, dygraph)}
{this.renderRightMarker(annotation.endTime, dygraph)}
</div>
)
}
private handleMouseEnter = (direction: string) => () => {
this.setState({isMouseOver: direction})
}
handleMouseLeave = e => {
private handleMouseLeave = (e: MouseEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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 {
</div>
)
}
render() {
const {annotation, dygraph, staticLegendHeight} = this.props
const {isDragging} = this.state
return (
<div>
<AnnotationWindow
annotation={annotation}
dygraph={dygraph}
active={!!isDragging}
staticLegendHeight={staticLegendHeight}
/>
{this.renderLeftMarker(annotation.startTime, dygraph)}
{this.renderRightMarker(annotation.endTime, dygraph)}
</div>
)
}
}
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 = {

View File

@ -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 => (
<div className="annotation-tooltip--timestamp">
{`${moment(+time).format('YYYY/MM/DD HH:mm:ss.SS')}`}
</div>
)
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<HTMLDivElement>) => {}
annotationState: AnnotationState
deleteAnnotationAsync: (a: AnnotationInterface) => void
updateAnnotationAsync: (a: AnnotationInterface) => void
span: Span
}
@ErrorHandling
class AnnotationTooltip extends Component {
state = {
class AnnotationTooltip extends Component<Props, State> {
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 {
</div>
)
}
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)

View File

@ -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 => (
<div
className={`annotation-window${active ? ' active' : ''}`}
style={windowDimensions(annotation, dygraph, staticLegendHeight)}
/>
)
const {bool, number, shape} = PropTypes
AnnotationWindow.propTypes = {
annotation: schema.annotation.isRequired,
dygraph: shape({}).isRequired,
staticLegendHeight: number,
active: bool,
}
export default AnnotationWindow

View File

@ -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 (
<div className="annotations-container">
{mode === ADDING &&
this.tempAnnotation && (
<NewAnnotation
dygraph={dygraph}
isTempHovering={isTempHovering}
tempAnnotation={this.tempAnnotation}
staticLegendHeight={staticLegendHeight}
onUpdateAnnotation={handleUpdateAnnotation}
onDismissAddingAnnotation={handleDismissAddingAnnotation}
onAddingAnnotationSuccess={handleAddingAnnotationSuccess}
onMouseEnterTempAnnotation={handleMouseEnterTempAnnotation}
onMouseLeaveTempAnnotation={handleMouseLeaveTempAnnotation}
/>
)}
{this.annotations.map(a => (
<Annotation
key={a.id}
mode={mode}
annotation={a}
dygraph={dygraph}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
/>
))}
</div>
)
}
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)

View File

@ -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<Props> {
public render() {
const {
mode,
dWidth,
dygraph,
xAxisRange,
isTempHovering,
handleUpdateAnnotation,
handleDismissAddingAnnotation,
handleAddingAnnotationSuccess,
handleMouseEnterTempAnnotation,
handleMouseLeaveTempAnnotation,
staticLegendHeight,
} = this.props
return (
<div className="annotations-container">
{mode === ADDING &&
this.tempAnnotation && (
<SourceContext.Consumer>
{(source: Source) => (
<NewAnnotation
dygraph={dygraph}
source={source}
isTempHovering={isTempHovering}
tempAnnotation={this.tempAnnotation}
staticLegendHeight={staticLegendHeight}
onUpdateAnnotation={handleUpdateAnnotation}
onDismissAddingAnnotation={handleDismissAddingAnnotation}
onAddingAnnotationSuccess={handleAddingAnnotationSuccess}
onMouseEnterTempAnnotation={handleMouseEnterTempAnnotation}
onMouseLeaveTempAnnotation={handleMouseLeaveTempAnnotation}
/>
)}
</SourceContext.Consumer>
)}
{this.annotations.map(a => (
<Annotation
key={a.id}
mode={mode}
xAxisRange={xAxisRange}
annotation={a}
dygraph={dygraph}
dWidth={dWidth}
staticLegendHeight={staticLegendHeight}
/>
))}
</div>
)
}
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)

View File

@ -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<Props, State> {
this.state = {
staticLegendHeight: null,
isMounted: false,
xAxisRange: [0, 0],
}
this.graphRef = React.createRef<HTMLDivElement>()
@ -153,6 +155,7 @@ class Dygraph extends Component<Props, State> {
},
zoomCallback: (lower: number, upper: number) =>
this.handleZoom(lower, upper),
drawCallback: () => this.handleDraw(),
highlightCircleSize: 0,
}
@ -171,7 +174,7 @@ class Dygraph extends Component<Props, State> {
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<Props, State> {
}
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<Props, State> {
dygraph={this.dygraph}
dWidth={this.dygraph.width_}
staticLegendHeight={staticLegendHeight}
xAxisRange={xAxisRange}
/>
)}
<DygraphLegend
@ -368,6 +372,12 @@ class Dygraph extends Component<Props, State> {
onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper))
}
private handleDraw = () => {
if (this.dygraph) {
this.setState({xAxisRange: this.dygraph.xAxisRange()})
}
}
private eventToTimestamp = ({
pageX: pxBetweenMouseAndPage,
}: MouseEvent<HTMLDivElement>): string => {

View File

@ -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<Props, State> {
private legendRef: HTMLElement | null = null
constructor(props) {
constructor(props: Props) {
super(props)
this.props.dygraph.updateOptions({
@ -175,7 +171,7 @@ class DygraphLegend extends PureComponent<Props, State> {
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<Props, State> {
this.props.onShow(e)
}
private legendFormatter = legend => {
private legendFormatter = (legend: LegendData) => {
if (!legend.x) {
return ''
}
@ -205,7 +201,7 @@ class DygraphLegend extends PureComponent<Props, State> {
return ''
}
private unhighlightCallback = e => {
private unhighlightCallback = (e: MouseEvent) => {
const {top, bottom, left, right} = this.legendRef.getBoundingClientRect()
const mouseY = e.clientY

View File

@ -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<Props, State> {
public wrapperRef: React.RefObject<HTMLDivElement>
constructor(props: Props) {
super(props)
this.wrapperRef = React.createRef<HTMLDivElement>()
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 (
<div className="new-annotation-tooltip">
<span className="new-annotation-helper">Click or Drag to Annotate</span>
<span className="new-annotation-timestamp">{timestamp}</span>
</div>
)
}
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 {
</div>
)
}
}
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<HTMLDivElement>): 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<HTMLDivElement>) => {
const startTime = this.eventToTimestamp(e)
this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime})
this.setState({gatherMode: 'endTime'})
}
private handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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 (
<div className="new-annotation-tooltip">
<span className="new-annotation-helper">Click or Drag to Annotate</span>
<span className="new-annotation-timestamp">{timestamp}</span>
</div>
)
}
}
const mdtp = {

View File

@ -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 {

View File

@ -0,0 +1,8 @@
export interface AnnotationInterface {
id: string
startTime: number
endTime: number
text: string
type: string
links: {self: string}
}

View File

@ -495,6 +495,7 @@ export declare class DygraphClass {
// tslint:disable-next-line:variable-name
public width_: number
public graphDiv: HTMLElement
constructor(
container: HTMLElement | string,

View File

@ -40,6 +40,7 @@ import {
DygraphClass,
DygraphData,
} from './dygraphs'
import {AnnotationInterface} from './annotations'
export {
Me,
@ -96,4 +97,5 @@ export {
SchemaFilter,
RemoteDataState,
URLQueryParams,
AnnotationInterface,
}

View File

@ -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])