Merge pull request #3689 from influxdata/bugfix/annotations-render-on-zoom
Bugfix/annotations render on zoompull/3698/head^2
commit
09d9cbf8a5
|
@ -12,7 +12,7 @@ interface State {
|
|||
|
||||
@ErrorHandling
|
||||
class SearchBar extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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})
|
||||
}
|
|
@ -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})
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
|
@ -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 = {
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
|
@ -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 {
|
|
@ -0,0 +1,8 @@
|
|||
export interface AnnotationInterface {
|
||||
id: string
|
||||
startTime: number
|
||||
endTime: number
|
||||
text: string
|
||||
type: string
|
||||
links: {self: string}
|
||||
}
|
|
@ -495,6 +495,7 @@ export declare class DygraphClass {
|
|||
|
||||
// tslint:disable-next-line:variable-name
|
||||
public width_: number
|
||||
public graphDiv: HTMLElement
|
||||
|
||||
constructor(
|
||||
container: HTMLElement | string,
|
||||
|
|
|
@ -40,6 +40,7 @@ import {
|
|||
DygraphClass,
|
||||
DygraphData,
|
||||
} from './dygraphs'
|
||||
import {AnnotationInterface} from './annotations'
|
||||
|
||||
export {
|
||||
Me,
|
||||
|
@ -96,4 +97,5 @@ export {
|
|||
SchemaFilter,
|
||||
RemoteDataState,
|
||||
URLQueryParams,
|
||||
AnnotationInterface,
|
||||
}
|
||||
|
|
|
@ -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])
|
Loading…
Reference in New Issue