From ee34456f7dbc5d21ac35ff81197cb62f8f502729 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Wed, 13 Jun 2018 15:35:49 -0700 Subject: [PATCH 01/18] Convert Annotations component to TS and pass xAxisRange explicitly to cause rerender of annotations on zoom in --- .../annotations/{helpers.js => helpers.ts} | 11 ++- ui/src/shared/components/Annotation.js | 6 +- ui/src/shared/components/AnnotationPoint.js | 3 +- ui/src/shared/components/AnnotationSpan.js | 3 +- .../{Annotations.js => Annotations.tsx} | 78 ++++++++----------- ui/src/shared/components/Dygraph.tsx | 1 + ui/src/types/annotations.ts | 7 ++ ui/src/types/index.ts | 2 + 8 files changed, 59 insertions(+), 52 deletions(-) rename ui/src/shared/annotations/{helpers.js => helpers.ts} (62%) rename ui/src/shared/components/{Annotations.js => Annotations.tsx} (61%) create mode 100644 ui/src/types/annotations.ts diff --git a/ui/src/shared/annotations/helpers.js b/ui/src/shared/annotations/helpers.ts similarity index 62% rename from ui/src/shared/annotations/helpers.js rename to ui/src/shared/annotations/helpers.ts index 7393783720..e90e15a898 100644 --- a/ui/src/shared/annotations/helpers.js +++ b/ui/src/shared/annotations/helpers.ts @@ -1,9 +1,11 @@ +import {AnnotationInterface} from 'src/types' + export const ANNOTATION_MIN_DELTA = 0.5 export const ADDING = 'adding' export const EDITING = 'editing' -export const TEMP_ANNOTATION = { +export const TEMP_ANNOTATION: AnnotationInterface = { id: 'tempAnnotation', text: 'Name Me', type: '', @@ -11,8 +13,11 @@ export const TEMP_ANNOTATION = { endTime: '', } -export const visibleAnnotations = (graph, annotations = []) => { - const [xStart, xEnd] = graph.xAxisRange() +export const visibleAnnotations = ( + xAxisRange: number[], + annotations: AnnotationInterface[] = [] +): AnnotationInterface[] => { + const [xStart, xEnd] = xAxisRange if (xStart === 0 && xEnd === 0) { return [] diff --git a/ui/src/shared/components/Annotation.js b/ui/src/shared/components/Annotation.js index 26d6fbfb4a..00919b1d0a 100644 --- a/ui/src/shared/components/Annotation.js +++ b/ui/src/shared/components/Annotation.js @@ -10,6 +10,7 @@ const Annotation = ({ mode, dygraph, dWidth, + xAxisRange, annotation, staticLegendHeight, }) => ( @@ -21,6 +22,7 @@ const Annotation = ({ annotation={annotation} dWidth={dWidth} staticLegendHeight={staticLegendHeight} + xAxisRange={xAxisRange} /> ) : ( )} ) -const {number, shape, string} = PropTypes +const {arrayOf, number, shape, string} = PropTypes Annotation.propTypes = { mode: string, dWidth: number, + xAxisRange: arrayOf(number), annotation: schema.annotation.isRequired, dygraph: shape({}).isRequired, staticLegendHeight: number, diff --git a/ui/src/shared/components/AnnotationPoint.js b/ui/src/shared/components/AnnotationPoint.js index cdc529d246..5d704d4de1 100644 --- a/ui/src/shared/components/AnnotationPoint.js +++ b/ui/src/shared/components/AnnotationPoint.js @@ -139,7 +139,7 @@ class AnnotationPoint extends React.Component { } } -const {func, number, shape, string} = PropTypes +const {arrayOf, func, number, shape, string} = PropTypes AnnotationPoint.defaultProps = { staticLegendHeight: 0, @@ -148,6 +148,7 @@ AnnotationPoint.defaultProps = { AnnotationPoint.propTypes = { annotation: schema.annotation.isRequired, mode: string.isRequired, + xAxisRange: arrayOf(number), dygraph: shape({}).isRequired, updateAnnotation: func.isRequired, updateAnnotationAsync: func.isRequired, diff --git a/ui/src/shared/components/AnnotationSpan.js b/ui/src/shared/components/AnnotationSpan.js index aed861a63b..3f4bdfbd0a 100644 --- a/ui/src/shared/components/AnnotationSpan.js +++ b/ui/src/shared/components/AnnotationSpan.js @@ -217,7 +217,7 @@ class AnnotationSpan extends React.Component { } } -const {func, number, shape, string} = PropTypes +const {arrayOf, func, number, shape, string} = PropTypes AnnotationSpan.defaultProps = { staticLegendHeight: 0, @@ -230,6 +230,7 @@ AnnotationSpan.propTypes = { staticLegendHeight: number, updateAnnotationAsync: func.isRequired, updateAnnotation: func.isRequired, + xAxisRange: arrayOf(number), } const mapDispatchToProps = { diff --git a/ui/src/shared/components/Annotations.js b/ui/src/shared/components/Annotations.tsx similarity index 61% rename from ui/src/shared/components/Annotations.js rename to ui/src/shared/components/Annotations.tsx index fdb9665e94..a8f595959c 100644 --- a/ui/src/shared/components/Annotations.js +++ b/ui/src/shared/components/Annotations.tsx @@ -1,11 +1,8 @@ import React, {Component} from 'react' -import PropTypes from 'prop-types' import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' import Annotation from 'src/shared/components/Annotation' import NewAnnotation from 'src/shared/components/NewAnnotation' -import * as schema from 'src/shared/schemas' import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' @@ -19,13 +16,31 @@ import { import {visibleAnnotations} from 'src/shared/annotations/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass} from 'src/types' + +interface Props { + dygraph: DygraphClass + dWidth: number + xAxisRange: number[] + staticLegendHeight: number + annotations: AnnotationInterface[] + mode: string + isTempHovering: boolean + handleUpdateAnnotation: () => void + handleDismissAddingAnnotation: () => void + handleAddingAnnotationSuccess: () => void + handleMouseEnterTempAnnotation: () => void + handleMouseLeaveTempAnnotation: () => void +} + @ErrorHandling -class Annotations extends Component { - render() { +class Annotations extends Component { + public render() { const { mode, dWidth, dygraph, + xAxisRange, isTempHovering, handleUpdateAnnotation, handleDismissAddingAnnotation, @@ -34,7 +49,7 @@ class Annotations extends Component { handleMouseLeaveTempAnnotation, staticLegendHeight, } = this.props - + console.log('rendering annotations') return (
{mode === ADDING && @@ -58,6 +73,7 @@ class Annotations extends Component { annotation={a} dygraph={dygraph} dWidth={dWidth} + xAxisRange={xAxisRange} staticLegendHeight={staticLegendHeight} /> ))} @@ -67,7 +83,7 @@ class Annotations extends Component { get annotations() { return visibleAnnotations( - this.props.dygraph, + this.props.xAxisRange, this.props.annotations ).filter(a => a.id !== TEMP_ANNOTATION.id) } @@ -77,48 +93,18 @@ class Annotations extends Component { } } -const {arrayOf, bool, func, number, shape, string} = PropTypes - -Annotations.propTypes = { - annotations: arrayOf(schema.annotation), - dygraph: shape({}).isRequired, - dWidth: number.isRequired, - mode: string, - isTempHovering: bool, - handleUpdateAnnotation: func.isRequired, - handleDismissAddingAnnotation: func.isRequired, - handleAddingAnnotationSuccess: func.isRequired, - handleMouseEnterTempAnnotation: func.isRequired, - handleMouseLeaveTempAnnotation: func.isRequired, - staticLegendHeight: number, -} - -const mapStateToProps = ({ - annotations: {annotations, mode, isTempHovering}, -}) => ({ +const mstp = ({annotations: {annotations, mode, isTempHovering}}) => ({ annotations, mode: mode || 'NORMAL', isTempHovering, }) -const mapDispatchToProps = dispatch => ({ - handleAddingAnnotationSuccess: bindActionCreators( - addingAnnotationSuccess, - dispatch - ), - handleDismissAddingAnnotation: bindActionCreators( - dismissAddingAnnotation, - dispatch - ), - handleMouseEnterTempAnnotation: bindActionCreators( - mouseEnterTempAnnotation, - dispatch - ), - handleMouseLeaveTempAnnotation: bindActionCreators( - mouseLeaveTempAnnotation, - dispatch - ), - handleUpdateAnnotation: bindActionCreators(updateAnnotation, dispatch), -}) +const mdtp = { + handleAddingAnnotationSuccess: addingAnnotationSuccess, + handleDismissAddingAnnotation: dismissAddingAnnotation, + handleMouseEnterTempAnnotation: mouseEnterTempAnnotation, + handleMouseLeaveTempAnnotation: mouseLeaveTempAnnotation, + handleUpdateAnnotation: updateAnnotation, +} -export default connect(mapStateToProps, mapDispatchToProps)(Annotations) +export default connect(mstp, mdtp)(Annotations) diff --git a/ui/src/shared/components/Dygraph.tsx b/ui/src/shared/components/Dygraph.tsx index 6e0f1c1dca..e0c79d6fec 100644 --- a/ui/src/shared/components/Dygraph.tsx +++ b/ui/src/shared/components/Dygraph.tsx @@ -258,6 +258,7 @@ class Dygraph extends Component { dygraph={this.dygraph} dWidth={this.dygraph.width_} staticLegendHeight={staticLegendHeight} + xAxisRange={this.dygraph.xAxisRange()} /> )} Date: Wed, 13 Jun 2018 18:44:16 -0700 Subject: [PATCH 02/18] Convert Annotation component to TS --- ui/src/shared/actions/annotations.js | 82 ---------- ui/src/shared/actions/annotations.ts | 149 ++++++++++++++++++ .../{Annotation.js => Annotation.tsx} | 31 ++-- ui/src/shared/components/Annotations.tsx | 5 +- 4 files changed, 167 insertions(+), 100 deletions(-) delete mode 100644 ui/src/shared/actions/annotations.js create mode 100644 ui/src/shared/actions/annotations.ts rename ui/src/shared/components/{Annotation.js => Annotation.tsx} (56%) diff --git a/ui/src/shared/actions/annotations.js b/ui/src/shared/actions/annotations.js deleted file mode 100644 index 328cdb1a10..0000000000 --- a/ui/src/shared/actions/annotations.js +++ /dev/null @@ -1,82 +0,0 @@ -import * as api from 'shared/apis/annotation' - -export const editingAnnotation = () => ({ - type: 'EDITING_ANNOTATION', -}) - -export const dismissEditingAnnotation = () => ({ - type: 'DISMISS_EDITING_ANNOTATION', -}) - -export const addingAnnotation = () => ({ - type: 'ADDING_ANNOTATION', -}) - -export const addingAnnotationSuccess = () => ({ - type: 'ADDING_ANNOTATION_SUCCESS', -}) - -export const dismissAddingAnnotation = () => ({ - type: 'DISMISS_ADDING_ANNOTATION', -}) - -export const mouseEnterTempAnnotation = () => ({ - type: 'MOUSEENTER_TEMP_ANNOTATION', -}) - -export const mouseLeaveTempAnnotation = () => ({ - type: 'MOUSELEAVE_TEMP_ANNOTATION', -}) - -export const loadAnnotations = annotations => ({ - type: 'LOAD_ANNOTATIONS', - payload: { - annotations, - }, -}) - -export const updateAnnotation = annotation => ({ - type: 'UPDATE_ANNOTATION', - payload: { - annotation, - }, -}) - -export const deleteAnnotation = annotation => ({ - type: 'DELETE_ANNOTATION', - payload: { - annotation, - }, -}) - -export const addAnnotation = annotation => ({ - type: 'ADD_ANNOTATION', - payload: { - annotation, - }, -}) - -export const addAnnotationAsync = (createUrl, annotation) => async dispatch => { - dispatch(addAnnotation(annotation)) - const savedAnnotation = await api.createAnnotation(createUrl, annotation) - dispatch(addAnnotation(savedAnnotation)) - dispatch(deleteAnnotation(annotation)) -} - -export const getAnnotationsAsync = ( - indexUrl, - {since, until} -) => async dispatch => { - const annotations = await api.getAnnotations(indexUrl, since, until) - dispatch(loadAnnotations(annotations)) -} - -export const deleteAnnotationAsync = annotation => async dispatch => { - await api.deleteAnnotation(annotation) - dispatch(deleteAnnotation(annotation)) -} - -export const updateAnnotationAsync = annotation => async dispatch => { - await api.updateAnnotation(annotation) - dispatch(updateAnnotation(annotation)) -} diff --git a/ui/src/shared/actions/annotations.ts b/ui/src/shared/actions/annotations.ts new file mode 100644 index 0000000000..2f62285fbb --- /dev/null +++ b/ui/src/shared/actions/annotations.ts @@ -0,0 +1,149 @@ +import * as api from 'src/shared/apis/annotation' + +import {AnnotationInterface} from 'src/types' + +export interface EditingAnnotationAction { + type: 'EDITING_ANNOTATION' +} +export const editingAnnotation = (): EditingAnnotationAction => ({ + type: 'EDITING_ANNOTATION', +}) + +export interface DismissEditingAnnotationAction { + type: 'DISMISS_EDITING_ANNOTATION' +} +export const dismissEditingAnnotation = (): DismissEditingAnnotationAction => ({ + type: 'DISMISS_EDITING_ANNOTATION', +}) + +export interface AddingAnnotationAction { + type: 'ADDING_ANNOTATION' +} +export const addingAnnotation = (): AddingAnnotationAction => ({ + type: 'ADDING_ANNOTATION', +}) + +export interface AddingAnnotationSuccessAction { + type: 'ADDING_ANNOTATION_SUCCESS' +} +export const addingAnnotationSuccess = (): AddingAnnotationSuccessAction => ({ + type: 'ADDING_ANNOTATION_SUCCESS', +}) + +export interface DismissAddingAnnotationAction { + type: 'DISMISS_ADDING_ANNOTATION' +} +export const dismissAddingAnnotation = (): DismissAddingAnnotationAction => ({ + type: 'DISMISS_ADDING_ANNOTATION', +}) + +export interface MouseEnterTempAnnotationAction { + type: 'MOUSEENTER_TEMP_ANNOTATION' +} +export const mouseEnterTempAnnotation = (): MouseEnterTempAnnotationAction => ({ + type: 'MOUSEENTER_TEMP_ANNOTATION', +}) + +export interface MouseLeaveTempAnnotationAction { + type: 'MOUSELEAVE_TEMP_ANNOTATION' +} +export const mouseLeaveTempAnnotation = (): MouseLeaveTempAnnotationAction => ({ + type: 'MOUSELEAVE_TEMP_ANNOTATION', +}) + +export interface LoadAnnotationsAction { + type: 'LOAD_ANNOTATIONS' + payload: { + annotations: AnnotationInterface[] + } +} +export const loadAnnotations = ( + annotations: AnnotationInterface[] +): LoadAnnotationsAction => ({ + type: 'LOAD_ANNOTATIONS', + payload: { + annotations, + }, +}) + +export interface UpdateAnnotationAction { + type: 'UPDATE_ANNOTATION' + payload: { + annotation: AnnotationInterface + } +} +export const updateAnnotation = ( + annotation: AnnotationInterface +): UpdateAnnotationAction => ({ + type: 'UPDATE_ANNOTATION', + payload: { + annotation, + }, +}) + +export interface DeleteAnnotationAction { + type: 'DELETE_ANNOTATION' + payload: { + annotation: AnnotationInterface + } +} +export const deleteAnnotation = ( + annotation: AnnotationInterface +): DeleteAnnotationAction => ({ + type: 'DELETE_ANNOTATION', + payload: { + annotation, + }, +}) + +export interface AddAnnotationAction { + type: 'ADD_ANNOTATION' + payload: { + annotation: AnnotationInterface + } +} +export const addAnnotation = ( + annotation: AnnotationInterface +): AddAnnotationAction => ({ + type: 'ADD_ANNOTATION', + payload: { + annotation, + }, +}) + +export const addAnnotationAsync = ( + createUrl: string, + annotation: AnnotationInterface +) => async dispatch => { + dispatch(addAnnotation(annotation)) + const savedAnnotation = await api.createAnnotation(createUrl, annotation) + dispatch(addAnnotation(savedAnnotation)) + dispatch(deleteAnnotation(annotation)) +} + +export interface AnnotationRange { + since: string + until: string +} + +export const getAnnotationsAsync = ( + indexUrl: string, + {since, until}: AnnotationRange +) => async dispatch => { + const annotations = await api.getAnnotations(indexUrl, since, until) + dispatch(loadAnnotations(annotations)) +} + +export const deleteAnnotationAsync = ( + annotation: AnnotationInterface +) => async dispatch => { + await api.deleteAnnotation(annotation) + dispatch(deleteAnnotation(annotation)) +} + +export const updateAnnotationAsync = ( + annotation: AnnotationInterface +) => async dispatch => { + await api.updateAnnotation(annotation) + dispatch(updateAnnotation(annotation)) +} diff --git a/ui/src/shared/components/Annotation.js b/ui/src/shared/components/Annotation.tsx similarity index 56% rename from ui/src/shared/components/Annotation.js rename to ui/src/shared/components/Annotation.tsx index 00919b1d0a..91aa2c7f3a 100644 --- a/ui/src/shared/components/Annotation.js +++ b/ui/src/shared/components/Annotation.tsx @@ -1,12 +1,20 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {SFC} from 'react' -import AnnotationPoint from 'shared/components/AnnotationPoint' -import AnnotationSpan from 'shared/components/AnnotationSpan' +import AnnotationPoint from 'src/shared/components/AnnotationPoint' +import AnnotationSpan from 'src/shared/components/AnnotationSpan' -import * as schema from 'shared/schemas' +import {AnnotationInterface, DygraphClass} from 'src/types' -const Annotation = ({ +interface Props { + mode: string + dWidth: number + xAxisRange: number[] + annotation: AnnotationInterface + dygraph: DygraphClass + staticLegendHeight: number +} + +const Annotation: SFC = ({ mode, dygraph, dWidth, @@ -37,15 +45,4 @@ const Annotation = ({
) -const {arrayOf, number, shape, string} = PropTypes - -Annotation.propTypes = { - mode: string, - dWidth: number, - xAxisRange: arrayOf(number), - annotation: schema.annotation.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, -} - export default Annotation diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx index a8f595959c..a2cd5df1d6 100644 --- a/ui/src/shared/components/Annotations.tsx +++ b/ui/src/shared/components/Annotations.tsx @@ -17,6 +17,7 @@ import {visibleAnnotations} from 'src/shared/annotations/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' import {AnnotationInterface, DygraphClass} from 'src/types' +import {UpdateAnnotationAction} from 'src/shared/actions/annotations' interface Props { dygraph: DygraphClass @@ -26,7 +27,9 @@ interface Props { annotations: AnnotationInterface[] mode: string isTempHovering: boolean - handleUpdateAnnotation: () => void + handleUpdateAnnotation: ( + annotation: AnnotationInterface + ) => UpdateAnnotationAction handleDismissAddingAnnotation: () => void handleAddingAnnotationSuccess: () => void handleMouseEnterTempAnnotation: () => void From bb091cb94ea26bbac99f0acdecff630bc8f2beb2 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Thu, 14 Jun 2018 17:52:26 -0700 Subject: [PATCH 03/18] Rerender annotations on drawCallback --- ui/src/shared/annotations/helpers.ts | 2 +- ui/src/shared/components/Annotation.tsx | 2 +- ui/src/shared/components/Annotations.tsx | 7 +++---- ui/src/shared/components/Dygraph.tsx | 15 ++++++++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts index e90e15a898..8e975caa07 100644 --- a/ui/src/shared/annotations/helpers.ts +++ b/ui/src/shared/annotations/helpers.ts @@ -14,7 +14,7 @@ export const TEMP_ANNOTATION: AnnotationInterface = { } export const visibleAnnotations = ( - xAxisRange: number[], + xAxisRange: [number, number], annotations: AnnotationInterface[] = [] ): AnnotationInterface[] => { const [xStart, xEnd] = xAxisRange diff --git a/ui/src/shared/components/Annotation.tsx b/ui/src/shared/components/Annotation.tsx index 91aa2c7f3a..d95443386b 100644 --- a/ui/src/shared/components/Annotation.tsx +++ b/ui/src/shared/components/Annotation.tsx @@ -8,7 +8,7 @@ import {AnnotationInterface, DygraphClass} from 'src/types' interface Props { mode: string dWidth: number - xAxisRange: number[] + xAxisRange: [number, number] annotation: AnnotationInterface dygraph: DygraphClass staticLegendHeight: number diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx index a2cd5df1d6..139f3e32dd 100644 --- a/ui/src/shared/components/Annotations.tsx +++ b/ui/src/shared/components/Annotations.tsx @@ -20,12 +20,12 @@ import {AnnotationInterface, DygraphClass} from 'src/types' import {UpdateAnnotationAction} from 'src/shared/actions/annotations' interface Props { - dygraph: DygraphClass dWidth: number - xAxisRange: number[] staticLegendHeight: number annotations: AnnotationInterface[] mode: string + xAxisRange: [number, number] + dygraph: DygraphClass isTempHovering: boolean handleUpdateAnnotation: ( annotation: AnnotationInterface @@ -52,7 +52,6 @@ class Annotations extends Component { handleMouseLeaveTempAnnotation, staticLegendHeight, } = this.props - console.log('rendering annotations') return (
{mode === ADDING && @@ -73,10 +72,10 @@ class Annotations extends Component { ))} diff --git a/ui/src/shared/components/Dygraph.tsx b/ui/src/shared/components/Dygraph.tsx index e0c79d6fec..cba2e7b14c 100644 --- a/ui/src/shared/components/Dygraph.tsx +++ b/ui/src/shared/components/Dygraph.tsx @@ -74,6 +74,7 @@ interface Props { interface State { staticLegendHeight: null | number isMounted: boolean + xAxisRange: [number, number] } @ErrorHandling @@ -109,6 +110,7 @@ class Dygraph extends Component { this.state = { staticLegendHeight: null, isMounted: false, + xAxisRange: [0, 0], } this.graphRef = React.createRef() @@ -152,6 +154,7 @@ class Dygraph extends Component { }, zoomCallback: (lower: number, upper: number) => this.handleZoom(lower, upper), + drawCallback: () => this.handleDraw(), highlightCircleSize: 0, } @@ -170,7 +173,7 @@ class Dygraph extends Component { const {w} = this.dygraph.getArea() this.props.setResolution(w) - this.setState({isMounted: true}) + this.setState({isMounted: true, xAxisRange: this.dygraph.xAxisRange()}) } public componentWillUnmount() { @@ -246,7 +249,7 @@ class Dygraph extends Component { } public render() { - const {staticLegendHeight} = this.state + const {staticLegendHeight, xAxisRange} = this.state const {staticLegend, cellID} = this.props return ( @@ -258,7 +261,7 @@ class Dygraph extends Component { dygraph={this.dygraph} dWidth={this.dygraph.width_} staticLegendHeight={staticLegendHeight} - xAxisRange={this.dygraph.xAxisRange()} + xAxisRange={xAxisRange} /> )} { onZoom(this.formatTimeRange(lower), this.formatTimeRange(upper)) } + private handleDraw = () => { + if (this.dygraph) { + this.setState({xAxisRange: this.dygraph.xAxisRange()}) + } + } + private eventToTimestamp = ({ pageX: pxBetweenMouseAndPage, }: MouseEvent): string => { From 1d3dda66671f8a9e6cbaa805c042fca527d0852e Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 15 Jun 2018 13:49:53 -0700 Subject: [PATCH 04/18] Convert Annotation sub-components to TS --- ui/src/alerts/components/SearchBar.tsx | 2 +- .../data_explorer/components/QueryEditor.tsx | 2 +- ui/src/shared/annotations/helpers.ts | 11 +- ...AnnotationInput.js => AnnotationInput.tsx} | 80 +++--- ...AnnotationPoint.js => AnnotationPoint.tsx} | 164 ++++++------ .../{AnnotationSpan.js => AnnotationSpan.tsx} | 119 +++++---- ...tationTooltip.js => AnnotationTooltip.tsx} | 112 ++++---- ...notationWindow.js => AnnotationWindow.tsx} | 35 ++- ui/src/shared/components/Annotations.tsx | 30 ++- ui/src/shared/components/DygraphLegend.tsx | 20 +- .../{NewAnnotation.js => NewAnnotation.tsx} | 247 +++++++++--------- ui/src/types/annotations.ts | 4 +- ui/src/types/dygraphs.ts | 1 + 13 files changed, 432 insertions(+), 395 deletions(-) rename ui/src/shared/components/{AnnotationInput.js => AnnotationInput.tsx} (69%) rename ui/src/shared/components/{AnnotationPoint.js => AnnotationPoint.tsx} (68%) rename ui/src/shared/components/{AnnotationSpan.js => AnnotationSpan.tsx} (77%) rename ui/src/shared/components/{AnnotationTooltip.js => AnnotationTooltip.tsx} (66%) rename ui/src/shared/components/{AnnotationWindow.js => AnnotationWindow.tsx} (70%) rename ui/src/shared/components/{NewAnnotation.js => NewAnnotation.tsx} (69%) diff --git a/ui/src/alerts/components/SearchBar.tsx b/ui/src/alerts/components/SearchBar.tsx index ce27e9364f..f00e433941 100644 --- a/ui/src/alerts/components/SearchBar.tsx +++ b/ui/src/alerts/components/SearchBar.tsx @@ -12,7 +12,7 @@ interface State { @ErrorHandling class SearchBar extends PureComponent { - constructor(props) { + constructor(props: Props) { super(props) this.state = { diff --git a/ui/src/data_explorer/components/QueryEditor.tsx b/ui/src/data_explorer/components/QueryEditor.tsx index 0f64bdec84..07868e5961 100644 --- a/ui/src/data_explorer/components/QueryEditor.tsx +++ b/ui/src/data_explorer/components/QueryEditor.tsx @@ -20,7 +20,7 @@ interface State { class QueryEditor extends PureComponent { private editor: React.RefObject - constructor(props) { + constructor(props: Props) { super(props) this.state = { value: this.props.query, diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts index 8e975caa07..5035171144 100644 --- a/ui/src/shared/annotations/helpers.ts +++ b/ui/src/shared/annotations/helpers.ts @@ -9,8 +9,8 @@ export const TEMP_ANNOTATION: AnnotationInterface = { id: 'tempAnnotation', text: 'Name Me', type: '', - startTime: '', - endTime: '', + startTime: null, + endTime: null, } export const visibleAnnotations = ( @@ -24,10 +24,13 @@ export const visibleAnnotations = ( } return annotations.filter(a => { + if (a.startTime === null || a.endTime === null) { + return false + } if (a.endTime === a.startTime) { - return xStart <= +a.startTime && +a.startTime <= xEnd + return xStart <= a.startTime && a.startTime <= xEnd } - return !(+a.endTime < xStart || xEnd < +a.startTime) + return !(a.endTime < xStart || xEnd < a.startTime) }) } diff --git a/ui/src/shared/components/AnnotationInput.js b/ui/src/shared/components/AnnotationInput.tsx similarity index 69% rename from ui/src/shared/components/AnnotationInput.js rename to ui/src/shared/components/AnnotationInput.tsx index 0338bdb7c4..8d8129048e 100644 --- a/ui/src/shared/components/AnnotationInput.js +++ b/ui/src/shared/components/AnnotationInput.tsx @@ -1,45 +1,25 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, ChangeEvent, FocusEvent, KeyboardEvent} from 'react' import onClickOutside from 'react-onclickoutside' import {ErrorHandling} from 'src/shared/decorators/errors' +interface State { + isEditing: boolean +} + +interface Props { + value: string + onChangeInput: (i: string) => void + onConfirmUpdate: () => void + onRejectUpdate: () => void +} + @ErrorHandling -class AnnotationInput extends Component { - state = { +class AnnotationInput extends Component { + public state = { isEditing: false, } - handleInputClick = () => { - this.setState({isEditing: true}) - } - - handleKeyDown = e => { - const {onConfirmUpdate, onRejectUpdate} = this.props - - if (e.key === 'Enter') { - onConfirmUpdate() - this.setState({isEditing: false}) - } - if (e.key === 'Escape') { - onRejectUpdate() - this.setState({isEditing: false}) - } - } - - handleFocus = e => { - e.target.select() - } - - handleChange = e => { - this.props.onChangeInput(e.target.value) - } - - handleClickOutside = () => { - this.props.onConfirmUpdate() - this.setState({isEditing: false}) - } - - render() { + public render() { const {isEditing} = this.state const {value} = this.props @@ -65,15 +45,31 @@ class AnnotationInput extends Component {
) } -} -const {func, string} = PropTypes + private handleInputClick = () => { + this.setState({isEditing: true}) + } -AnnotationInput.propTypes = { - value: string, - onChangeInput: func.isRequired, - onConfirmUpdate: func.isRequired, - onRejectUpdate: func.isRequired, + private handleKeyDown = (e: KeyboardEvent) => { + const {onConfirmUpdate, onRejectUpdate} = this.props + + if (e.key === 'Enter') { + onConfirmUpdate() + this.setState({isEditing: false}) + } + if (e.key === 'Escape') { + onRejectUpdate() + this.setState({isEditing: false}) + } + } + + private handleFocus = (e: FocusEvent) => { + e.target.select() + } + + private handleChange = (e: ChangeEvent) => { + this.props.onChangeInput(e.target.value) + } } export default onClickOutside(AnnotationInput) diff --git a/ui/src/shared/components/AnnotationPoint.js b/ui/src/shared/components/AnnotationPoint.tsx similarity index 68% rename from ui/src/shared/components/AnnotationPoint.js rename to ui/src/shared/components/AnnotationPoint.tsx index 5d704d4de1..831d046a37 100644 --- a/ui/src/shared/components/AnnotationPoint.js +++ b/ui/src/shared/components/AnnotationPoint.tsx @@ -1,49 +1,116 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent, DragEvent} from 'react' import {connect} from 'react-redux' import { DYGRAPH_CONTAINER_H_MARGIN, DYGRAPH_CONTAINER_V_MARGIN, DYGRAPH_CONTAINER_XLABEL_MARGIN, -} from 'shared/constants' -import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' -import AnnotationTooltip from 'shared/components/AnnotationTooltip' +} from 'src/shared/constants' +import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers' +import * as actions from 'src/shared/actions/annotations' +import AnnotationTooltip from 'src/shared/components/AnnotationTooltip' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass} from 'src/types' + +interface State { + isMouseOver: boolean + isDragging: boolean +} + +interface Props { + annotation: AnnotationInterface + mode: string + xAxisRange: [number, number] + dygraph: DygraphClass + updateAnnotation: (a: AnnotationInterface) => void + updateAnnotationAsync: (a: AnnotationInterface) => void + staticLegendHeight: number +} + @ErrorHandling -class AnnotationPoint extends React.Component { - state = { +class AnnotationPoint extends Component { + public static defaultProps: Partial = { + staticLegendHeight: 0, + } + + public state = { isMouseOver: false, isDragging: false, } - handleMouseEnter = () => { + public render() { + const {annotation, mode, dygraph, staticLegendHeight} = this.props + const {isDragging} = this.state + + const isEditing = mode === EDITING + + const flagClass = isDragging + ? 'annotation-point--flag__dragging' + : 'annotation-point--flag' + + const markerClass = isDragging ? 'annotation dragging' : 'annotation' + + const clickClass = isEditing + ? 'annotation--click-area editing' + : 'annotation--click-area' + + const markerStyles = { + left: `${dygraph.toDomXCoord(Number(annotation.startTime)) + + DYGRAPH_CONTAINER_H_MARGIN}px`, + height: `calc(100% - ${staticLegendHeight + + DYGRAPH_CONTAINER_XLABEL_MARGIN + + DYGRAPH_CONTAINER_V_MARGIN * 2}px)`, + } + + return ( +
+
+
+ +
+ ) + } + + private handleMouseEnter = () => { this.setState({isMouseOver: true}) } - handleMouseLeave = e => { + private handleMouseLeave = (e: MouseEvent) => { const {annotation} = this.props - - if (e.relatedTarget.id === `tooltip-${annotation.id}`) { - return this.setState({isDragging: false}) + if (e.relatedTarget instanceof Element) { + if (e.relatedTarget.id === `tooltip-${annotation.id}`) { + return this.setState({isDragging: false}) + } } this.setState({isMouseOver: false}) } - handleDragStart = () => { + private handleDragStart = () => { this.setState({isDragging: true}) } - handleDragEnd = () => { + private handleDragEnd = () => { const {annotation, updateAnnotationAsync} = this.props updateAnnotationAsync(annotation) this.setState({isDragging: false}) } - handleDrag = e => { + private handleDrag = (e: DragEvent) => { if (this.props.mode !== EDITING) { return } @@ -83,78 +150,19 @@ class AnnotationPoint extends React.Component { updateAnnotation({ ...annotation, - startTime: `${newTime}`, - endTime: `${newTime}`, + startTime: newTime, + endTime: newTime, }) e.preventDefault() e.stopPropagation() } - - render() { - const {annotation, mode, dygraph, staticLegendHeight} = this.props - const {isDragging} = this.state - - const isEditing = mode === EDITING - - const flagClass = isDragging - ? 'annotation-point--flag__dragging' - : 'annotation-point--flag' - - const markerClass = isDragging ? 'annotation dragging' : 'annotation' - - const clickClass = isEditing - ? 'annotation--click-area editing' - : 'annotation--click-area' - - const markerStyles = { - left: `${dygraph.toDomXCoord(annotation.startTime) + - DYGRAPH_CONTAINER_H_MARGIN}px`, - height: `calc(100% - ${staticLegendHeight + - DYGRAPH_CONTAINER_XLABEL_MARGIN + - DYGRAPH_CONTAINER_V_MARGIN * 2}px)`, - } - - return ( -
-
-
- -
- ) - } } -const {arrayOf, func, number, shape, string} = PropTypes - AnnotationPoint.defaultProps = { staticLegendHeight: 0, } -AnnotationPoint.propTypes = { - annotation: schema.annotation.isRequired, - mode: string.isRequired, - xAxisRange: arrayOf(number), - dygraph: shape({}).isRequired, - updateAnnotation: func.isRequired, - updateAnnotationAsync: func.isRequired, - staticLegendHeight: number, -} - const mdtp = { updateAnnotationAsync: actions.updateAnnotationAsync, updateAnnotation: actions.updateAnnotation, diff --git a/ui/src/shared/components/AnnotationSpan.js b/ui/src/shared/components/AnnotationSpan.tsx similarity index 77% rename from ui/src/shared/components/AnnotationSpan.js rename to ui/src/shared/components/AnnotationSpan.tsx index 3f4bdfbd0a..f3f00b2f68 100644 --- a/ui/src/shared/components/AnnotationSpan.js +++ b/ui/src/shared/components/AnnotationSpan.tsx @@ -1,44 +1,83 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent, DragEvent} from 'react' import {connect} from 'react-redux' import { DYGRAPH_CONTAINER_H_MARGIN, DYGRAPH_CONTAINER_V_MARGIN, DYGRAPH_CONTAINER_XLABEL_MARGIN, -} from 'shared/constants' -import {ANNOTATION_MIN_DELTA, EDITING} from 'shared/annotations/helpers' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' -import AnnotationTooltip from 'shared/components/AnnotationTooltip' -import AnnotationWindow from 'shared/components/AnnotationWindow' +} from 'src/shared/constants' + +import * as actions from 'src/shared/actions/annotations' +import {ANNOTATION_MIN_DELTA, EDITING} from 'src/shared/annotations/helpers' +import AnnotationTooltip from 'src/shared/components/AnnotationTooltip' +import AnnotationWindow from 'src/shared/components/AnnotationWindow' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass} from 'src/types' + +interface State { + isMouseOver: string + isDragging: string +} + +interface Props { + annotation: AnnotationInterface + mode: string + dygraph: DygraphClass + staticLegendHeight: number + updateAnnotation: (a: AnnotationInterface) => void + updateAnnotationAsync: (a: AnnotationInterface) => void + xAxisRange: [number, number] +} + @ErrorHandling -class AnnotationSpan extends React.Component { - state = { +class AnnotationSpan extends Component { + public static defaultProps: Partial = { + staticLegendHeight: 0, + } + + public state: State = { isDragging: null, isMouseOver: null, } - handleMouseEnter = direction => () => { + public render() { + const {annotation, dygraph, staticLegendHeight} = this.props + const {isDragging} = this.state + + return ( +
+ + {this.renderLeftMarker(annotation.startTime, dygraph)} + {this.renderRightMarker(annotation.endTime, dygraph)} +
+ ) + } + + private handleMouseEnter = (direction: string) => () => { this.setState({isMouseOver: direction}) } - handleMouseLeave = e => { + private handleMouseLeave = (e: MouseEvent) => { const {annotation} = this.props - - if (e.relatedTarget.id === `tooltip-${annotation.id}`) { - return this.setState({isDragging: null}) + if (e.relatedTarget instanceof Element) { + if (e.relatedTarget.id === `tooltip-${annotation.id}`) { + return this.setState({isDragging: null}) + } } this.setState({isMouseOver: null}) } - handleDragStart = direction => () => { + private handleDragStart = (direction: string) => () => { this.setState({isDragging: direction}) } - handleDragEnd = () => { + private handleDragEnd = () => { const {annotation, updateAnnotationAsync} = this.props const [startTime, endTime] = [ annotation.startTime, @@ -54,7 +93,7 @@ class AnnotationSpan extends React.Component { this.setState({isDragging: null}) } - handleDrag = timeProp => e => { + private handleDrag = (timeProp: string) => (e: DragEvent) => { if (this.props.mode !== EDITING) { return } @@ -96,7 +135,10 @@ class AnnotationSpan extends React.Component { e.stopPropagation() } - renderLeftMarker(startTime, dygraph) { + private renderLeftMarker( + startTime: number, + dygraph: DygraphClass + ): JSX.Element { const isEditing = this.props.mode === EDITING const {isDragging, isMouseOver} = this.state const {annotation, staticLegendHeight} = this.props @@ -147,7 +189,10 @@ class AnnotationSpan extends React.Component { ) } - renderRightMarker(endTime, dygraph) { + private renderRightMarker( + endTime: number, + dygraph: DygraphClass + ): JSX.Element { const isEditing = this.props.mode === EDITING const {isDragging, isMouseOver} = this.state const {annotation, staticLegendHeight} = this.props @@ -197,40 +242,6 @@ class AnnotationSpan extends React.Component {
) } - - render() { - const {annotation, dygraph, staticLegendHeight} = this.props - const {isDragging} = this.state - - return ( -
- - {this.renderLeftMarker(annotation.startTime, dygraph)} - {this.renderRightMarker(annotation.endTime, dygraph)} -
- ) - } -} - -const {arrayOf, func, number, shape, string} = PropTypes - -AnnotationSpan.defaultProps = { - staticLegendHeight: 0, -} - -AnnotationSpan.propTypes = { - annotation: schema.annotation.isRequired, - mode: string.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, - updateAnnotationAsync: func.isRequired, - updateAnnotation: func.isRequired, - xAxisRange: arrayOf(number), } const mapDispatchToProps = { diff --git a/ui/src/shared/components/AnnotationTooltip.js b/ui/src/shared/components/AnnotationTooltip.tsx similarity index 66% rename from ui/src/shared/components/AnnotationTooltip.js rename to ui/src/shared/components/AnnotationTooltip.tsx index e347cdec2e..8121710df4 100644 --- a/ui/src/shared/components/AnnotationTooltip.js +++ b/ui/src/shared/components/AnnotationTooltip.tsx @@ -1,50 +1,62 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent} from 'react' import {connect} from 'react-redux' import moment from 'moment' import classnames from 'classnames' import AnnotationInput from 'src/shared/components/AnnotationInput' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' +import * as actions from 'src/shared/actions/annotations' import {ErrorHandling} from 'src/shared/decorators/errors' -const TimeStamp = ({time}) => ( +import {AnnotationInterface} from 'src/types' + +interface TimeStampProps { + time: string +} + +const TimeStamp = ({time}: TimeStampProps): JSX.Element => (
{`${moment(+time).format('YYYY/MM/DD HH:mm:ss.SS')}`}
) +interface AnnotationState { + isDragging: boolean + isMouseOver: boolean +} + +interface Span { + spanCenter: number + tooltipLeft: number + spanWidth: number +} + +interface State { + annotation: AnnotationInterface +} + +interface Props { + isEditing: boolean + annotation: AnnotationInterface + timestamp: string + onMouseLeave: (e: MouseEvent) => {} + annotationState: AnnotationState + deleteAnnotationAsync: (a: AnnotationInterface) => void + updateAnnotationAsync: (a: AnnotationInterface) => void + span: Span +} + @ErrorHandling -class AnnotationTooltip extends Component { - state = { +class AnnotationTooltip extends Component { + public state = { annotation: this.props.annotation, } - componentWillReceiveProps = ({annotation}) => { + public componentWillReceiveProps(nextProps: Props) { + const {annotation} = nextProps this.setState({annotation}) } - handleChangeInput = key => value => { - const {annotation} = this.state - const newAnnotation = {...annotation, [key]: value} - - this.setState({annotation: newAnnotation}) - } - - handleConfirmUpdate = () => { - this.props.updateAnnotationAsync(this.state.annotation) - } - - handleRejectUpdate = () => { - this.setState({annotation: this.props.annotation}) - } - - handleDelete = () => { - this.props.deleteAnnotationAsync(this.props.annotation) - } - - render() { + public render() { const {annotation} = this.state const { onMouseLeave, @@ -99,28 +111,30 @@ class AnnotationTooltip extends Component {
) } + + private handleChangeInput = (key: string) => (value: string) => { + const {annotation} = this.state + const newAnnotation = {...annotation, [key]: value} + + this.setState({annotation: newAnnotation}) + } + + private handleConfirmUpdate = () => { + this.props.updateAnnotationAsync(this.state.annotation) + } + + private handleRejectUpdate = () => { + this.setState({annotation: this.props.annotation}) + } + + private handleDelete = () => { + this.props.deleteAnnotationAsync(this.props.annotation) + } } -const {bool, func, number, shape, string} = PropTypes - -TimeStamp.propTypes = { - time: string.isRequired, -} -AnnotationTooltip.propTypes = { - isEditing: bool, - annotation: schema.annotation.isRequired, - timestamp: string, - onMouseLeave: func.isRequired, - annotationState: shape({}), - deleteAnnotationAsync: func.isRequired, - updateAnnotationAsync: func.isRequired, - span: shape({ - spanCenter: number.isRequired, - spanWidth: number.isRequired, - }), -} - -export default connect(null, { +const mdtp = { deleteAnnotationAsync: actions.deleteAnnotationAsync, updateAnnotationAsync: actions.updateAnnotationAsync, -})(AnnotationTooltip) +} + +export default connect(null, mdtp)(AnnotationTooltip) diff --git a/ui/src/shared/components/AnnotationWindow.js b/ui/src/shared/components/AnnotationWindow.tsx similarity index 70% rename from ui/src/shared/components/AnnotationWindow.js rename to ui/src/shared/components/AnnotationWindow.tsx index 08c8c05401..78212decfe 100644 --- a/ui/src/shared/components/AnnotationWindow.js +++ b/ui/src/shared/components/AnnotationWindow.tsx @@ -1,14 +1,23 @@ import React from 'react' -import PropTypes from 'prop-types' import { DYGRAPH_CONTAINER_H_MARGIN, DYGRAPH_CONTAINER_V_MARGIN, DYGRAPH_CONTAINER_XLABEL_MARGIN, -} from 'shared/constants' -import * as schema from 'shared/schemas' +} from 'src/shared/constants' +import {AnnotationInterface, DygraphClass} from 'src/types' -const windowDimensions = (anno, dygraph, staticLegendHeight) => { +interface WindowDimensionsReturn { + left: string + width: string + height: string +} + +const windowDimensions = ( + anno: AnnotationInterface, + dygraph: DygraphClass, + staticLegendHeight: number +): WindowDimensionsReturn => { // TODO: export and test this function const [startX, endX] = dygraph.xAxisRange() const startTime = Math.max(+anno.startTime, startX) @@ -34,25 +43,23 @@ const windowDimensions = (anno, dygraph, staticLegendHeight) => { } } +interface AnnotationWindowProps { + annotation: AnnotationInterface + dygraph: DygraphClass + active: boolean + staticLegendHeight: number +} + const AnnotationWindow = ({ annotation, dygraph, active, staticLegendHeight, -}) => ( +}: AnnotationWindowProps): JSX.Element => (
) -const {bool, number, shape} = PropTypes - -AnnotationWindow.propTypes = { - annotation: schema.annotation.isRequired, - dygraph: shape({}).isRequired, - staticLegendHeight: number, - active: bool, -} - export default AnnotationWindow diff --git a/ui/src/shared/components/Annotations.tsx b/ui/src/shared/components/Annotations.tsx index 139f3e32dd..6190530f93 100644 --- a/ui/src/shared/components/Annotations.tsx +++ b/ui/src/shared/components/Annotations.tsx @@ -3,6 +3,7 @@ import {connect} from 'react-redux' import Annotation from 'src/shared/components/Annotation' import NewAnnotation from 'src/shared/components/NewAnnotation' +import {SourceContext} from 'src/CheckSources' import {ADDING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' @@ -16,7 +17,7 @@ import { import {visibleAnnotations} from 'src/shared/annotations/helpers' import {ErrorHandling} from 'src/shared/decorators/errors' -import {AnnotationInterface, DygraphClass} from 'src/types' +import {AnnotationInterface, DygraphClass, Source} from 'src/types' import {UpdateAnnotationAction} from 'src/shared/actions/annotations' interface Props { @@ -56,17 +57,22 @@ class Annotations extends Component {
{mode === ADDING && this.tempAnnotation && ( - + + {(source: Source) => ( + + )} + )} {this.annotations.map(a => ( void - onShow: (MouseEvent) => void + onShow: (e: MouseEvent) => void activeCellID: string setActiveCell: (cellID: string) => void } interface LegendData { - x: string | null + x: number series: SeriesLegendData[] xHTML: string } @@ -49,7 +45,7 @@ interface State { class DygraphLegend extends PureComponent { private legendRef: HTMLElement | null = null - constructor(props) { + constructor(props: Props) { super(props) this.props.dygraph.updateOptions({ @@ -199,7 +195,7 @@ class DygraphLegend extends PureComponent { this.setState({filterText}) } - private handleSortLegend = sortType => () => { + private handleSortLegend = (sortType: string) => () => { this.setState({sortType, isAscending: !this.state.isAscending}) } @@ -209,7 +205,7 @@ class DygraphLegend extends PureComponent { this.props.onShow(e) } - private legendFormatter = legend => { + private legendFormatter = (legend: LegendData) => { if (!legend.x) { return '' } @@ -229,7 +225,7 @@ class DygraphLegend extends PureComponent { return '' } - private unhighlightCallback = e => { + private unhighlightCallback = (e: MouseEvent) => { const {top, bottom, left, right} = this.legendRef.getBoundingClientRect() const mouseY = e.clientY diff --git a/ui/src/shared/components/NewAnnotation.js b/ui/src/shared/components/NewAnnotation.tsx similarity index 69% rename from ui/src/shared/components/NewAnnotation.js rename to ui/src/shared/components/NewAnnotation.tsx index 98e2b6ebe8..a06e4dda6f 100644 --- a/ui/src/shared/components/NewAnnotation.js +++ b/ui/src/shared/components/NewAnnotation.tsx @@ -1,120 +1,47 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, MouseEvent} from 'react' import classnames from 'classnames' import {connect} from 'react-redux' import uuid from 'uuid' -import OnClickOutside from 'shared/components/OnClickOutside' -import AnnotationWindow from 'shared/components/AnnotationWindow' -import * as schema from 'shared/schemas' -import * as actions from 'shared/actions/annotations' +import OnClickOutside from 'src/shared/components/OnClickOutside' +import AnnotationWindow from 'src/shared/components/AnnotationWindow' +import * as actions from 'src/shared/actions/annotations' -import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'shared/constants' +import {DYGRAPH_CONTAINER_XLABEL_MARGIN} from 'src/shared/constants' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AnnotationInterface, DygraphClass, Source} from 'src/types' + +interface Props { + dygraph: DygraphClass + source: Source + isTempHovering: boolean + tempAnnotation: AnnotationInterface + addAnnotationAsync: (url: string, a: AnnotationInterface) => void + onDismissAddingAnnotation: () => void + onAddingAnnotationSuccess: () => void + onUpdateAnnotation: (a: AnnotationInterface) => void + onMouseEnterTempAnnotation: () => void + onMouseLeaveTempAnnotation: () => void + staticLegendHeight: number +} +interface State { + isMouseOver: boolean + gatherMode: string +} @ErrorHandling -class NewAnnotation extends Component { - state = { - isMouseOver: false, - gatherMode: 'startTime', - } - - clampWithinGraphTimerange = timestamp => { - const [xRangeStart] = this.props.dygraph.xAxisRange() - return Math.max(xRangeStart, timestamp) - } - - eventToTimestamp = ({pageX: pxBetweenMouseAndPage}) => { - const {left: pxBetweenGraphAndPage} = this.wrapper.getBoundingClientRect() - const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage - const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate) - const clamped = this.clampWithinGraphTimerange(timestamp) - return `${clamped}` - } - - handleMouseDown = e => { - const startTime = this.eventToTimestamp(e) - this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime}) - this.setState({gatherMode: 'endTime'}) - } - - handleMouseMove = e => { - if (this.props.isTempHovering === false) { - return - } - - const {tempAnnotation, onUpdateAnnotation} = this.props - const newTime = this.eventToTimestamp(e) - - if (this.state.gatherMode === 'startTime') { - onUpdateAnnotation({ - ...tempAnnotation, - startTime: newTime, - endTime: newTime, - }) - } else { - onUpdateAnnotation({...tempAnnotation, endTime: newTime}) - } - } - - handleMouseUp = e => { - const { - tempAnnotation, - onUpdateAnnotation, - addAnnotationAsync, - onAddingAnnotationSuccess, - onMouseLeaveTempAnnotation, - } = this.props - const createUrl = this.context.source.links.annotations - - const upTime = this.eventToTimestamp(e) - const downTime = tempAnnotation.startTime - const [startTime, endTime] = [downTime, upTime].sort() - const newAnnotation = {...tempAnnotation, startTime, endTime} - - onUpdateAnnotation(newAnnotation) - addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()}) - - onAddingAnnotationSuccess() - onMouseLeaveTempAnnotation() - - this.setState({ +class NewAnnotation extends Component { + public wrapperRef: React.RefObject + constructor(props: Props) { + super(props) + this.wrapperRef = React.createRef() + this.state = { isMouseOver: false, gatherMode: 'startTime', - }) - } - - handleMouseOver = e => { - this.setState({isMouseOver: true}) - this.handleMouseMove(e) - this.props.onMouseEnterTempAnnotation() - } - - handleMouseLeave = () => { - this.setState({isMouseOver: false}) - this.props.onMouseLeaveTempAnnotation() - } - - handleClickOutside = () => { - const {onDismissAddingAnnotation, isTempHovering} = this.props - - if (!isTempHovering) { - onDismissAddingAnnotation() } } - renderTimestamp(time) { - const timestamp = `${new Date(+time)}` - - return ( -
- Click or Drag to Annotate - {timestamp} -
- ) - } - - render() { + public render() { const { dygraph, isTempHovering, @@ -123,7 +50,6 @@ class NewAnnotation extends Component { staticLegendHeight, } = this.props const {isMouseOver} = this.state - const crosshairOne = Math.max(-1000, dygraph.toDomXCoord(startTime)) const crosshairTwo = dygraph.toDomXCoord(endTime) const crosshairHeight = `calc(100% - ${staticLegendHeight + @@ -154,7 +80,7 @@ class NewAnnotation extends Component { className={classnames('new-annotation', { hover: isTempHovering, })} - ref={el => (this.wrapper = el)} + ref={this.wrapperRef} onMouseMove={this.handleMouseMove} onMouseOver={this.handleMouseOver} onMouseLeave={this.handleMouseLeave} @@ -185,29 +111,98 @@ class NewAnnotation extends Component {
) } -} -const {bool, func, number, shape, string} = PropTypes + private clampWithinGraphTimerange = (timestamp: number): number => { + const [xRangeStart] = this.props.dygraph.xAxisRange() + return Math.max(xRangeStart, timestamp) + } -NewAnnotation.contextTypes = { - source: shape({ - links: shape({ - annotations: string, - }), - }), -} + private eventToTimestamp = ({ + pageX: pxBetweenMouseAndPage, + }: MouseEvent): number => { + const { + left: pxBetweenGraphAndPage, + } = this.wrapperRef.current.getBoundingClientRect() + const graphXCoordinate = pxBetweenMouseAndPage - pxBetweenGraphAndPage + const timestamp = this.props.dygraph.toDataXCoord(graphXCoordinate) + const clamped = this.clampWithinGraphTimerange(timestamp) + return clamped + } -NewAnnotation.propTypes = { - dygraph: shape({}).isRequired, - isTempHovering: bool, - tempAnnotation: schema.annotation.isRequired, - addAnnotationAsync: func.isRequired, - onDismissAddingAnnotation: func.isRequired, - onAddingAnnotationSuccess: func.isRequired, - onUpdateAnnotation: func.isRequired, - onMouseEnterTempAnnotation: func.isRequired, - onMouseLeaveTempAnnotation: func.isRequired, - staticLegendHeight: number, + private handleMouseDown = (e: MouseEvent) => { + const startTime = this.eventToTimestamp(e) + this.props.onUpdateAnnotation({...this.props.tempAnnotation, startTime}) + this.setState({gatherMode: 'endTime'}) + } + + private handleMouseMove = (e: MouseEvent) => { + if (this.props.isTempHovering === false) { + return + } + + const {tempAnnotation, onUpdateAnnotation} = this.props + const newTime = this.eventToTimestamp(e) + + if (this.state.gatherMode === 'startTime') { + onUpdateAnnotation({ + ...tempAnnotation, + startTime: newTime, + endTime: newTime, + }) + } else { + onUpdateAnnotation({...tempAnnotation, endTime: newTime}) + } + } + + private handleMouseUp = (e: MouseEvent) => { + const { + tempAnnotation, + onUpdateAnnotation, + addAnnotationAsync, + onAddingAnnotationSuccess, + onMouseLeaveTempAnnotation, + source, + } = this.props + const createUrl = source.links.annotations + + const upTime = this.eventToTimestamp(e) + const downTime = tempAnnotation.startTime + const [startTime, endTime] = [downTime, upTime].sort() + const newAnnotation = {...tempAnnotation, startTime, endTime} + + onUpdateAnnotation(newAnnotation) + addAnnotationAsync(createUrl, {...newAnnotation, id: uuid.v4()}) + + onAddingAnnotationSuccess() + onMouseLeaveTempAnnotation() + + this.setState({ + isMouseOver: false, + gatherMode: 'startTime', + }) + } + + private handleMouseOver = (e: MouseEvent) => { + this.setState({isMouseOver: true}) + this.handleMouseMove(e) + this.props.onMouseEnterTempAnnotation() + } + + private handleMouseLeave = () => { + this.setState({isMouseOver: false}) + this.props.onMouseLeaveTempAnnotation() + } + + private renderTimestamp(time: number): JSX.Element { + const timestamp = `${new Date(time)}` + + return ( +
+ Click or Drag to Annotate + {timestamp} +
+ ) + } } const mdtp = { diff --git a/ui/src/types/annotations.ts b/ui/src/types/annotations.ts index b338a25013..690e51386d 100644 --- a/ui/src/types/annotations.ts +++ b/ui/src/types/annotations.ts @@ -1,7 +1,7 @@ export interface AnnotationInterface { id: string - startTime: string - endTime: string + startTime: number + endTime: number text: string type: string } diff --git a/ui/src/types/dygraphs.ts b/ui/src/types/dygraphs.ts index 2aef990b6b..5c9f8bbead 100644 --- a/ui/src/types/dygraphs.ts +++ b/ui/src/types/dygraphs.ts @@ -495,6 +495,7 @@ export declare class DygraphClass { // tslint:disable-next-line:variable-name public width_: number + public graphDiv: HTMLElement constructor( container: HTMLElement | string, From 36355f18d92b96d8bb2ff2115844f1ca4b98128a Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 15 Jun 2018 14:35:39 -0700 Subject: [PATCH 05/18] Convert formatting test to ts --- ui/src/utils/formatting.ts | 17 -------------- ui/test/utils/formatting.test.js | 39 -------------------------------- ui/test/utils/formatting.test.ts | 19 ++++++++++++++++ 3 files changed, 19 insertions(+), 56 deletions(-) delete mode 100644 ui/test/utils/formatting.test.js create mode 100644 ui/test/utils/formatting.test.ts diff --git a/ui/src/utils/formatting.ts b/ui/src/utils/formatting.ts index 403232cd6d..afb2e5391f 100644 --- a/ui/src/utils/formatting.ts +++ b/ui/src/utils/formatting.ts @@ -117,23 +117,6 @@ export const numberValueFormatter = ( return `${prefix}${label}${suffix}` } -export const formatBytes = (bytes: number) => { - if (bytes === 0) { - return '0 Bytes' - } - - if (!bytes) { - return null - } - - const k = 1000 - const dm = 2 - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - - return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}` -} - export const formatRPDuration = (duration: string | null): string => { if (!duration) { return diff --git a/ui/test/utils/formatting.test.js b/ui/test/utils/formatting.test.js deleted file mode 100644 index d5887b08c5..0000000000 --- a/ui/test/utils/formatting.test.js +++ /dev/null @@ -1,39 +0,0 @@ -import {formatBytes, formatRPDuration} from 'utils/formatting' - -describe('Formatting helpers', () => { - describe('formatBytes', () => { - it('returns null when passed a falsey value', () => { - const actual = formatBytes(null) - - expect(actual).toBe(null) - }) - - it('returns the correct value when passed 0', () => { - const actual = formatBytes(0) - - expect(actual).toBe('0 Bytes') - }) - - it("converts a raw byte value into it's most appropriate unit", () => { - expect(formatBytes(1000)).toBe('1 KB') - expect(formatBytes(1000000)).toBe('1 MB') - expect(formatBytes(1000000000)).toBe('1 GB') - }) - }) - - describe('formatRPDuration', () => { - it("returns 'infinite' for a retention policy with a value of '0'", () => { - const actual = formatRPDuration('0') - - expect(actual).toBe('∞') - }) - - it('correctly formats retention policy durations', () => { - expect(formatRPDuration('24h0m0s')).toBe('24h') - - expect(formatRPDuration('168h0m0s')).toBe('7d') - - expect(formatRPDuration('200h32m3s')).toBe('8d8h32m3s') - }) - }) -}) diff --git a/ui/test/utils/formatting.test.ts b/ui/test/utils/formatting.test.ts new file mode 100644 index 0000000000..f814633e83 --- /dev/null +++ b/ui/test/utils/formatting.test.ts @@ -0,0 +1,19 @@ +import {formatRPDuration} from 'src/utils/formatting' + +describe('Formatting helpers', () => { + describe('formatRPDuration', () => { + it("returns 'infinite' for a retention policy with a value of '0'", () => { + const actual = formatRPDuration('0') + + expect(actual).toBe('∞') + }) + + it('correctly formats retention policy durations', () => { + expect(formatRPDuration('24h0m0s')).toBe('24h') + + expect(formatRPDuration('168h0m0s')).toBe('7d') + + expect(formatRPDuration('200h32m3s')).toBe('8d8h32m3s') + }) + }) +}) From 6caef8e87c6c68950e459168eeee1849a8ecd1df Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 15 Jun 2018 15:20:24 -0700 Subject: [PATCH 06/18] Convert Annotation action and reducer to TS --- ui/src/shared/actions/annotations.ts | 18 +++++- ui/src/shared/annotations/helpers.ts | 1 + ui/src/shared/apis/annotation.js | 40 ------------ ui/src/shared/apis/annotation.ts | 63 +++++++++++++++++++ .../{annotations.js => annotations.ts} | 14 ++++- ui/src/types/annotations.ts | 1 + ...nnotations.test.js => annotations.test.ts} | 36 ++++++----- 7 files changed, 112 insertions(+), 61 deletions(-) delete mode 100644 ui/src/shared/apis/annotation.js create mode 100644 ui/src/shared/apis/annotation.ts rename ui/src/shared/reducers/{annotations.js => annotations.ts} (88%) rename ui/test/annotations/reducers/{annotations.test.js => annotations.test.ts} (59%) diff --git a/ui/src/shared/actions/annotations.ts b/ui/src/shared/actions/annotations.ts index 2f62285fbb..47a8421ff1 100644 --- a/ui/src/shared/actions/annotations.ts +++ b/ui/src/shared/actions/annotations.ts @@ -1,7 +1,19 @@ import * as api from 'src/shared/apis/annotation' - import {AnnotationInterface} from 'src/types' +export type Action = + | EditingAnnotationAction + | DismissEditingAnnotationAction + | AddingAnnotationAction + | AddingAnnotationSuccessAction + | DismissAddingAnnotationAction + | MouseEnterTempAnnotationAction + | MouseLeaveTempAnnotationAction + | LoadAnnotationsAction + | UpdateAnnotationAction + | DeleteAnnotationAction + | AddAnnotationAction + export interface EditingAnnotationAction { type: 'EDITING_ANNOTATION' } @@ -122,8 +134,8 @@ export const addAnnotationAsync = ( } export interface AnnotationRange { - since: string - until: string + since: number + until: number } export const getAnnotationsAsync = ( diff --git a/ui/src/shared/annotations/helpers.ts b/ui/src/shared/annotations/helpers.ts index 5035171144..7b44bb1785 100644 --- a/ui/src/shared/annotations/helpers.ts +++ b/ui/src/shared/annotations/helpers.ts @@ -11,6 +11,7 @@ export const TEMP_ANNOTATION: AnnotationInterface = { type: '', startTime: null, endTime: null, + links: {self: ''}, } export const visibleAnnotations = ( diff --git a/ui/src/shared/apis/annotation.js b/ui/src/shared/apis/annotation.js deleted file mode 100644 index e36c24859d..0000000000 --- a/ui/src/shared/apis/annotation.js +++ /dev/null @@ -1,40 +0,0 @@ -import AJAX from 'src/utils/ajax' - -const msToRFC = ms => ms && new Date(parseInt(ms, 10)).toISOString() -const rfcToMS = rfc3339 => rfc3339 && JSON.stringify(Date.parse(rfc3339)) -const annoToMillisecond = anno => ({ - ...anno, - startTime: rfcToMS(anno.startTime), - endTime: rfcToMS(anno.endTime), -}) -const annoToRFC = anno => ({ - ...anno, - startTime: msToRFC(anno.startTime), - endTime: msToRFC(anno.endTime), -}) - -export const createAnnotation = async (url, annotation) => { - const data = annoToRFC(annotation) - const response = await AJAX({method: 'POST', url, data}) - return annoToMillisecond(response.data) -} - -export const getAnnotations = async (url, since, until) => { - const {data} = await AJAX({ - method: 'GET', - url, - params: {since: msToRFC(since), until: msToRFC(until)}, - }) - return data.annotations.map(annoToMillisecond) -} - -export const deleteAnnotation = async annotation => { - const url = annotation.links.self - await AJAX({method: 'DELETE', url}) -} - -export const updateAnnotation = async annotation => { - const url = annotation.links.self - const data = annoToRFC(annotation) - await AJAX({method: 'PATCH', url, data}) -} diff --git a/ui/src/shared/apis/annotation.ts b/ui/src/shared/apis/annotation.ts new file mode 100644 index 0000000000..d5d82e9858 --- /dev/null +++ b/ui/src/shared/apis/annotation.ts @@ -0,0 +1,63 @@ +import AJAX from 'src/utils/ajax' +import {AnnotationInterface} from 'src/types' + +const msToRFCString = (ms: number) => + ms && new Date(Math.round(ms)).toISOString() + +const rfcStringToMS = (rfc3339: string) => rfc3339 && Date.parse(rfc3339) + +interface ServerAnnotation { + id: string + startTime: string + endTime: string + text: string + type: string + links: {self: string} +} + +const annoToMillisecond = ( + annotation: ServerAnnotation +): AnnotationInterface => ({ + ...annotation, + startTime: rfcStringToMS(annotation.startTime), + endTime: rfcStringToMS(annotation.endTime), +}) + +const annoToRFC = (annotation: AnnotationInterface): ServerAnnotation => ({ + ...annotation, + startTime: msToRFCString(annotation.startTime), + endTime: msToRFCString(annotation.endTime), +}) + +export const createAnnotation = async ( + url: string, + annotation: AnnotationInterface +) => { + const data = annoToRFC(annotation) + const response = await AJAX({method: 'POST', url, data}) + return annoToMillisecond(response.data) +} + +export const getAnnotations = async ( + url: string, + since: number, + until: number +) => { + const {data} = await AJAX({ + method: 'GET', + url, + params: {since: msToRFCString(since), until: msToRFCString(until)}, + }) + return data.annotations.map(annoToMillisecond) +} + +export const deleteAnnotation = async (annotation: AnnotationInterface) => { + const url = annotation.links.self + await AJAX({method: 'DELETE', url}) +} + +export const updateAnnotation = async (annotation: AnnotationInterface) => { + const url = annotation.links.self + const data = annoToRFC(annotation) + await AJAX({method: 'PATCH', url, data}) +} diff --git a/ui/src/shared/reducers/annotations.js b/ui/src/shared/reducers/annotations.ts similarity index 88% rename from ui/src/shared/reducers/annotations.js rename to ui/src/shared/reducers/annotations.ts index 658f2803ed..f1c3ebb657 100644 --- a/ui/src/shared/reducers/annotations.js +++ b/ui/src/shared/reducers/annotations.ts @@ -1,12 +1,24 @@ import {ADDING, EDITING, TEMP_ANNOTATION} from 'src/shared/annotations/helpers' +import {Action} from 'src/shared/actions/annotations' +import {AnnotationInterface} from 'src/types' + +export interface AnnotationState { + mode: string + isTempHovering: boolean + annotations: AnnotationInterface[] +} + const initialState = { mode: null, isTempHovering: false, annotations: [], } -const annotationsReducer = (state = initialState, action) => { +const annotationsReducer = ( + state: AnnotationState = initialState, + action: Action +) => { switch (action.type) { case 'EDITING_ANNOTATION': { return { diff --git a/ui/src/types/annotations.ts b/ui/src/types/annotations.ts index 690e51386d..e35bb06285 100644 --- a/ui/src/types/annotations.ts +++ b/ui/src/types/annotations.ts @@ -4,4 +4,5 @@ export interface AnnotationInterface { endTime: number text: string type: string + links: {self: string} } diff --git a/ui/test/annotations/reducers/annotations.test.js b/ui/test/annotations/reducers/annotations.test.ts similarity index 59% rename from ui/test/annotations/reducers/annotations.test.js rename to ui/test/annotations/reducers/annotations.test.ts index fb7cddffd2..8e402e2364 100644 --- a/ui/test/annotations/reducers/annotations.test.js +++ b/ui/test/annotations/reducers/annotations.test.ts @@ -1,45 +1,47 @@ -import reducer from 'shared/reducers/annotations' +import reducer from 'src/shared/reducers/annotations' +import {AnnotationInterface} from 'src/types' +import {AnnotationState} from 'src/shared/reducers/annotations' import { addAnnotation, deleteAnnotation, loadAnnotations, updateAnnotation, -} from 'shared/actions/annotations' +} from 'src/shared/actions/annotations' -const a1 = { +const a1: AnnotationInterface = { id: '1', - group: '', - name: 'anno1', - time: '1515716169000', - duration: '', + startTime: 1515716169000, + endTime: 1515716169000, + type: '', text: 'you have no swoggels', + links: {self: 'to/thine/own/self/be/true'}, } -const a2 = { +const a2: AnnotationInterface = { id: '2', - group: '', - name: 'anno1', - time: '1515716169000', - duration: '', - text: 'you have no swoggels', + startTime: 1515716169000, + endTime: 1515716169002, + type: '', + text: 'you have so many swoggels', + links: {self: 'self/in/eye/of/beholder'}, } -const state = { +const state: AnnotationState = { + isTempHovering: false, mode: null, annotations: [], } describe('Shared.Reducers.annotations', () => { it('can load the annotations', () => { - const expected = [{time: '0', duration: ''}] + const expected = [a1] const actual = reducer(state, loadAnnotations(expected)) expect(actual.annotations).toEqual(expected) }) - it('can update an annotation', () => { - const expected = [{...a1, time: ''}] + const expected = [{...a1, startTime: 6666666666666}] const actual = reducer( {...state, annotations: [a1]}, updateAnnotation(expected[0]) From d0b36ae5df6508a52f5576c3f0f4796f9265f5d2 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 15 Jun 2018 17:05:01 -0700 Subject: [PATCH 07/18] Allow hyphens and underscores in basepath --- server/server.go | 2 +- server/server_test.go | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index 9d7f44e5ac..6fe3cea36a 100644 --- a/server/server.go +++ b/server/server.go @@ -540,6 +540,6 @@ func clientUsage(values client.Values) *client.Usage { } func validBasepath(basepath string) bool { - re := regexp.MustCompile(`(\/{1}\w+)+`) + re := regexp.MustCompile(`(\/{1}[\w-]+)+`) return re.ReplaceAllLiteralString(basepath, "") == "" } diff --git a/server/server_test.go b/server/server_test.go index f6dd40c423..9a8591cde0 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -43,6 +43,13 @@ func Test_validBasepath(t *testing.T) { }, want: true, }, + { + name: "Basepath can include numbers, hyphens, and underscores", + args: args{ + basepath: "/3shishka-bob/-rus4s_rus-1_s-", + }, + want: true, + }, { name: "Basepath is not empty and invalid - no slashes", args: args{ From 654ea88e30d4ba161fb43120145ba5c4e9533998 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 15 Jun 2018 17:08:00 -0700 Subject: [PATCH 08/18] Fix issue where no selected tempvar value --- ui/src/shared/components/LayoutCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/components/LayoutCell.tsx b/ui/src/shared/components/LayoutCell.tsx index 8ac80ba031..76d39b0201 100644 --- a/ui/src/shared/components/LayoutCell.tsx +++ b/ui/src/shared/components/LayoutCell.tsx @@ -78,7 +78,7 @@ export default class LayoutCell extends Component { (acc, template) => { const {tempVar} = template const templateValue = template.values.find(v => v.selected) - const value = templateValue.value + const value = _.get(templateValue, 'value', str) const regex = new RegExp(tempVar, 'g') return acc.replace(regex, value) }, From 20c96bdd5b72688569e4dd60d84f8f7b11caf6e1 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Fri, 15 Jun 2018 17:10:38 -0700 Subject: [PATCH 09/18] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c87e20bc1f..f7bb38de69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ 1. [#3527](https://github.com/influxdata/chronograf/pull/3527): Ensure cell queries use constraints from TimeSelector 1. [#3573](https://github.com/influxdata/chronograf/pull/3573): Fix Gauge color selection bug 1. [#3649](https://github.com/influxdata/chronograf/pull/3649): Fix erroneous icons in Date Picker widget +1. [#3697](https://github.com/influxdata/chronograf/pull/3697): Fix allowing hyphens in basepath ## v1.5.0.0 [2018-05-15-RC] From da77a03dcb698cd4cd6b0ef94972c24edaadffea Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Fri, 15 Jun 2018 17:12:48 -0700 Subject: [PATCH 10/18] Update Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e01e7e5cc5..14691a788c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ 1. [#3527](https://github.com/influxdata/chronograf/pull/3527): Ensure cell queries use constraints from TimeSelector 1. [#3573](https://github.com/influxdata/chronograf/pull/3573): Fix Gauge color selection bug 1. [#3649](https://github.com/influxdata/chronograf/pull/3649): Fix erroneous icons in Date Picker widget +1. [#3698](https://github.com/influxdata/chronograf/pull/3698): Fix error in cell when tempVar returns no values ## v1.5.0.0 [2018-05-15-RC] From 08bf9bcd27bce8d9271dbaa6caddac00a8b3ef1e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 15 Jun 2018 17:22:21 -0700 Subject: [PATCH 11/18] Source form and page --- ui/src/logs/actions/index.ts | 3 +- ui/src/shared/apis/index.ts | 60 ++- ui/src/shared/copy/notifications.ts | 6 +- ui/src/sources/components/SourceForm.js | 208 ---------- ui/src/sources/components/SourceForm.tsx | 224 +++++++++++ .../{SourcePage.js => SourcePage.tsx} | 379 +++++++++--------- ui/src/types/index.ts | 3 +- ui/src/types/sources.ts | 2 + ...SourceForm.test.js => SourceForm.test.tsx} | 3 +- 9 files changed, 465 insertions(+), 423 deletions(-) delete mode 100644 ui/src/sources/components/SourceForm.js create mode 100644 ui/src/sources/components/SourceForm.tsx rename ui/src/sources/containers/{SourcePage.js => SourcePage.tsx} (53%) rename ui/test/sources/components/{SourceForm.test.js => SourceForm.test.tsx} (95%) diff --git a/ui/src/logs/actions/index.ts b/ui/src/logs/actions/index.ts index fa8ac6a897..05d603adbd 100644 --- a/ui/src/logs/actions/index.ts +++ b/ui/src/logs/actions/index.ts @@ -450,8 +450,7 @@ export const populateNamespacesAsync = ( export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async ( dispatch ): Promise => { - const response = await getSource(sourceID) - const source = response.data + const source = await getSource(sourceID) const proxyLink = getDeep(source, 'links.proxy', null) diff --git a/ui/src/shared/apis/index.ts b/ui/src/shared/apis/index.ts index 6828c694c0..eb51aefeb3 100644 --- a/ui/src/shared/apis/index.ts +++ b/ui/src/shared/apis/index.ts @@ -9,29 +9,51 @@ export function getSources() { }) } -export function getSource(id) { - return AJAX({ - url: null, - resource: 'sources', - id, - }) +export const getSource = async (id: string): Promise => { + try { + const {data: source} = await AJAX({ + url: null, + resource: 'sources', + id, + }) + + return source + } catch (error) { + throw error + } } -export function createSource(attributes) { - return AJAX({ - url: null, - resource: 'sources', - method: 'POST', - data: attributes, - }) +export const createSource = async ( + attributes: Partial +): Promise => { + try { + const {data: source} = await AJAX({ + url: null, + resource: 'sources', + method: 'POST', + data: attributes, + }) + + return source + } catch (error) { + throw error + } } -export function updateSource(newSource) { - return AJAX({ - url: newSource.links.self, - method: 'PATCH', - data: newSource, - }) +export const updateSource = async ( + newSource: Partial +): Promise => { + try { + const {data: source} = await AJAX({ + url: newSource.links.self, + method: 'PATCH', + data: newSource, + }) + + return source + } catch (error) { + throw error + } } export function deleteSource(source) { diff --git a/ui/src/shared/copy/notifications.ts b/ui/src/shared/copy/notifications.ts index 4e99e9a747..73d4cd765e 100644 --- a/ui/src/shared/copy/notifications.ts +++ b/ui/src/shared/copy/notifications.ts @@ -155,8 +155,10 @@ export const notifyUnableToRetrieveSources = () => 'Unable to retrieve sources.' export const notifyUnableToConnectSource = sourceName => `Unable to connect to source ${sourceName}.` -export const notifyErrorConnectingToSource = errorMessage => - `Unable to connect to InfluxDB source: ${errorMessage}` +export const notifyErrorConnectingToSource = errorMessage => ({ + ...defaultErrorNotification, + message: `Unable to connect to InfluxDB source: ${errorMessage}`, +}) // Multitenancy User Notifications // ---------------------------------------------------------------------------- diff --git a/ui/src/sources/components/SourceForm.js b/ui/src/sources/components/SourceForm.js deleted file mode 100644 index 6e092e609e..0000000000 --- a/ui/src/sources/components/SourceForm.js +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import {connect} from 'react-redux' -import _ from 'lodash' - -import {insecureSkipVerifyText} from 'shared/copy/tooltipText' - -import {SUPERADMIN_ROLE} from 'src/auth/Authorized' - -export const SourceForm = ({ - source, - editMode, - onSubmit, - onInputChange, - onBlurSourceURL, - isUsingAuth, - gotoPurgatory, - isInitialSource, - me, -}) => ( -
- {isUsingAuth && isInitialSource ? ( -
- {me.role === SUPERADMIN_ROLE ? ( -

- {me.currentOrganization.name} has no connections -

- ) : ( -

- {me.currentOrganization.name} has no connections - available to {me.role}s -

- )} -
Add a Connection below:
-
- ) : null} - -
-
- - -
-
- - -
-
- - -
-
- - -
- {_.get(source, 'type', '').includes('enterprise') ? ( -
- - -
- ) : null} -
- - -
-
- - -
-
-
- - -
-
- {_.get(source, 'url', '').startsWith('https') ? ( -
-
- - -
- -
- ) : null} -
- - -
- {isUsingAuth ? ( - - ) : null} -
-
-
-) - -const {bool, func, shape, string} = PropTypes - -SourceForm.propTypes = { - source: shape({ - url: string.isRequired, - name: string.isRequired, - username: string.isRequired, - password: string.isRequired, - telegraf: string.isRequired, - insecureSkipVerify: bool.isRequired, - default: bool.isRequired, - metaUrl: string.isRequired, - }).isRequired, - editMode: bool.isRequired, - onInputChange: func.isRequired, - onSubmit: func.isRequired, - onBlurSourceURL: func.isRequired, - me: shape({ - role: string, - currentOrganization: shape({ - id: string.isRequired, - name: string.isRequired, - }), - }), - isUsingAuth: bool, - isInitialSource: bool, - gotoPurgatory: func, -} - -const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me}) - -export default connect(mapStateToProps)(SourceForm) diff --git a/ui/src/sources/components/SourceForm.tsx b/ui/src/sources/components/SourceForm.tsx new file mode 100644 index 0000000000..39071b10f8 --- /dev/null +++ b/ui/src/sources/components/SourceForm.tsx @@ -0,0 +1,224 @@ +import React, {PureComponent, FocusEvent, MouseEvent, ChangeEvent} from 'react' +import classnames from 'classnames' +import {connect} from 'react-redux' +import _ from 'lodash' + +import {insecureSkipVerifyText} from 'src/shared/copy/tooltipText' + +import {SUPERADMIN_ROLE} from 'src/auth/Authorized' +import {Source, Me} from 'src/types' + +interface Props { + me: Me + source: Partial + editMode: boolean + isUsingAuth: boolean + gotoPurgatory: () => void + isInitialSource: boolean + onSubmit: (e: MouseEvent) => void + onInputChange: (e: ChangeEvent) => void + onBlurSourceURL: (e: FocusEvent) => void +} + +export class SourceForm extends PureComponent { + public render() { + const { + source, + onSubmit, + isUsingAuth, + onInputChange, + gotoPurgatory, + onBlurSourceURL, + isInitialSource, + } = this.props + return ( +
+ {isUsingAuth && isInitialSource && this.authIndicatior} +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ {this.isEnterprise && ( +
+ + +
+ )} +
+ + +
+
+ + +
+
+
+ + +
+
+ {this.isHTTPS && ( +
+
+ + +
+ +
+ )} +
+ + +
+ {isUsingAuth && ( + + )} +
+
+
+ ) + } + + private get authIndicatior(): JSX.Element { + const {me} = this.props + return ( +
+ {me.role.name === SUPERADMIN_ROLE ? ( +

+ {me.currentOrganization.name} has no connections +

+ ) : ( +

+ {me.currentOrganization.name} has no connections + available to {me.role}s +

+ )} +
Add a Connection below:
+
+ ) + } + + private get submitText(): string { + const {editMode} = this.props + if (editMode) { + return 'Save Changes' + } + + return 'Add Connection' + } + + private get submitIconClass(): string { + const {editMode} = this.props + return `icon ${editMode ? 'checkmark' : 'plus'}` + } + + private get submitClass(): string { + const {editMode} = this.props + return classnames('btn btn-block', { + 'btn-primary': editMode, + 'btn-success': !editMode, + }) + } + + private get isEnterprise(): boolean { + const {source} = this.props + return _.get(source, 'type', '').includes('enterprise') + } + + private get isHTTPS(): boolean { + const {source} = this.props + return _.get(source, 'url', '').startsWith('https') + } +} + +const mapStateToProps = ({auth: {isUsingAuth, me}}) => ({isUsingAuth, me}) + +export default connect(mapStateToProps)(SourceForm) diff --git a/ui/src/sources/containers/SourcePage.js b/ui/src/sources/containers/SourcePage.tsx similarity index 53% rename from ui/src/sources/containers/SourcePage.js rename to ui/src/sources/containers/SourcePage.tsx index 7d44ace13b..359d030bbe 100644 --- a/ui/src/sources/containers/SourcePage.js +++ b/ui/src/sources/containers/SourcePage.tsx @@ -1,47 +1,67 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import {withRouter} from 'react-router' +import React, {PureComponent, MouseEvent, ChangeEvent} from 'react' +import {withRouter, WithRouterProps} from 'react-router' import _ from 'lodash' -import {getSource} from 'shared/apis' -import {createSource, updateSource} from 'shared/apis' +import {getSource} from 'src/shared/apis' +import {createSource, updateSource} from 'src/shared/apis' import { addSource as addSourceAction, updateSource as updateSourceAction, -} from 'shared/actions/sources' -import {notify as notifyAction} from 'shared/actions/notifications' + AddSource, + UpdateSource, +} from 'src/shared/actions/sources' +import { + notify as notifyAction, + PubishNotification, +} from 'src/shared/actions/notifications' import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' -import Notifications from 'shared/components/Notifications' +import Notifications from 'src/shared/components/Notifications' import SourceForm from 'src/sources/components/SourceForm' -import FancyScrollbar from 'shared/components/FancyScrollbar' -import SourceIndicator from 'shared/components/SourceIndicator' -import {DEFAULT_SOURCE} from 'shared/constants' -const initialPath = '/sources/new' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import SourceIndicator from 'src/shared/components/SourceIndicator' +import {DEFAULT_SOURCE} from 'src/shared/constants' +import {Source} from 'src/types' + +const INITIAL_PATH = '/sources/new' import { - notifyErrorConnectingToSource, - notifySourceCreationSucceeded, - notifySourceCreationFailed, notifySourceUdpated, notifySourceUdpateFailed, -} from 'shared/copy/notifications' + notifySourceCreationFailed, + notifyErrorConnectingToSource, + notifySourceCreationSucceeded, +} from 'src/shared/copy/notifications' import {ErrorHandling} from 'src/shared/decorators/errors' +interface Props extends WithRouterProps { + notify: PubishNotification + addSource: AddSource + updateSource: UpdateSource +} + +interface State { + isCreated: boolean + isLoading: boolean + source: Partial + editMode: boolean + isInitialSource: boolean +} + @ErrorHandling -class SourcePage extends Component { +class SourcePage extends PureComponent { constructor(props) { super(props) this.state = { isLoading: true, + isCreated: false, source: DEFAULT_SOURCE, editMode: props.params.id !== undefined, - isInitialSource: props.router.location.pathname === initialPath, + isInitialSource: props.router.location.pathname === INITIAL_PATH, } } - componentDidMount() { + public async componentDidMount() { const {editMode} = this.state const {params, notify} = this.props @@ -49,156 +69,19 @@ class SourcePage extends Component { return this.setState({isLoading: false}) } - getSource(params.id) - .then(({data: source}) => { - this.setState({ - source: {...DEFAULT_SOURCE, ...source}, - isLoading: false, - }) + try { + const source = await getSource(params.id) + this.setState({ + source: {...DEFAULT_SOURCE, ...source}, + isLoading: false, }) - .catch(error => { - notify(notifyErrorConnectingToSource(this._parseError(error))) - this.setState({isLoading: false}) - }) - } - - handleInputChange = e => { - let val = e.target.value - const name = e.target.name - - if (e.target.type === 'checkbox') { - val = e.target.checked + } catch (error) { + notify(notifyErrorConnectingToSource(this.parseError(error))) + this.setState({isLoading: false}) } - - this.setState(prevState => { - const source = { - ...prevState.source, - [name]: val, - } - - return {...prevState, source} - }) } - handleBlurSourceURL = () => { - const {source, editMode} = this.state - if (editMode) { - this.setState(this._normalizeSource) - return - } - - if (!source.url) { - return - } - - this.setState(this._normalizeSource, this._createSourceOnBlur) - } - - handleSubmit = e => { - e.preventDefault() - const {isCreated, editMode} = this.state - const isNewSource = !editMode - - if (!isCreated && isNewSource) { - return this.setState(this._normalizeSource, this._createSource) - } - - this.setState(this._normalizeSource, this._updateSource) - } - - gotoPurgatory = () => { - const {router} = this.props - router.push('/purgatory') - } - - _normalizeSource({source}) { - const url = source.url.trim() - if (source.url.startsWith('http')) { - return {source: {...source, url}} - } - return {source: {...source, url: `http://${url}`}} - } - - _createSourceOnBlur = () => { - const {source} = this.state - // if there is a type on source it has already been created - if (source.type) { - return - } - createSource(source) - .then(({data: sourceFromServer}) => { - this.props.addSource(sourceFromServer) - this.setState({ - source: {...DEFAULT_SOURCE, ...sourceFromServer}, - isCreated: true, - }) - }) - .catch(err => { - // dont want to flash this until they submit - const error = this._parseError(err) - console.error('Error creating InfluxDB connection: ', error) - }) - } - - _createSource = () => { - const {source} = this.state - const {notify} = this.props - createSource(source) - .then(({data: sourceFromServer}) => { - this.props.addSource(sourceFromServer) - this._redirect(sourceFromServer) - notify(notifySourceCreationSucceeded(source.name)) - }) - .catch(error => { - notify(notifySourceCreationFailed(source.name, this._parseError(error))) - }) - } - - _updateSource = () => { - const {source} = this.state - const {notify} = this.props - updateSource(source) - .then(({data: sourceFromServer}) => { - this.props.updateSource(sourceFromServer) - this._redirect(sourceFromServer) - notify(notifySourceUdpated(source.name)) - }) - .catch(error => { - notify(notifySourceUdpateFailed(source.name, this._parseError(error))) - }) - } - - _redirect = source => { - const {isInitialSource} = this.state - const {params, router} = this.props - - if (isInitialSource) { - return this._redirectToApp(source) - } - - router.push(`/sources/${params.sourceID}/manage-sources`) - } - - _redirectToApp = source => { - const {location, router} = this.props - const {redirectPath} = location.query - - if (!redirectPath) { - return router.push(`/sources/${source.id}/hosts`) - } - - const fixedPath = redirectPath.replace( - /\/sources\/[^/]*/, - `/sources/${source.id}` - ) - return router.push(fixedPath) - } - - _parseError = error => { - return _.get(error, ['data', 'message'], error) - } - - render() { + public render() { const {isLoading, source, editMode, isInitialSource} = this.state if (isLoading) { @@ -248,31 +131,147 @@ class SourcePage extends Component {
) } + + private handleSubmit = (e: MouseEvent): void => { + e.preventDefault() + const {isCreated, editMode} = this.state + const isNewSource = !editMode + + if (!isCreated && isNewSource) { + return this.setState(this.normalizeSource, this.createSource) + } + + this.setState(this.normalizeSource, this.updateSource) + } + + private gotoPurgatory = (): void => { + const {router} = this.props + router.push('/purgatory') + } + + private normalizeSource({source}) { + const url = source.url.trim() + if (source.url.startsWith('http')) { + return {source: {...source, url}} + } + return {source: {...source, url: `http://${url}`}} + } + + private createSourceOnBlur = async () => { + const {source} = this.state + // if there is a type on source it has already been created + if (source.type) { + return + } + + try { + const sourceFromServer = await createSource(source) + this.props.addSource(sourceFromServer) + this.setState({ + source: {...DEFAULT_SOURCE, ...sourceFromServer}, + isCreated: true, + }) + } catch (err) { + // dont want to flash this until they submit + const error = this.parseError(err) + console.error('Error creating InfluxDB connection: ', error) + } + } + + private createSource = async () => { + const {source} = this.state + const {notify} = this.props + try { + const sourceFromServer = await createSource(source) + this.props.addSource(sourceFromServer) + this.redirect(sourceFromServer) + notify(notifySourceCreationSucceeded(source.name)) + } catch (err) { + // dont want to flash this until they submit + notify(notifySourceCreationFailed(source.name, this.parseError(err))) + } + } + + private updateSource = async () => { + const {source} = this.state + const {notify} = this.props + try { + const sourceFromServer = await updateSource(source) + this.props.updateSource(sourceFromServer) + this.redirect(sourceFromServer) + notify(notifySourceUdpated(source.name)) + } catch (error) { + notify(notifySourceUdpateFailed(source.name, this.parseError(error))) + } + } + + private redirect = source => { + const {isInitialSource} = this.state + const {params, router} = this.props + + if (isInitialSource) { + return this.redirectToApp(source) + } + + router.push(`/sources/${params.sourceID}/manage-sources`) + } + + private parseError = (error): string => { + return _.get(error, ['data', 'message'], error) + } + + private redirectToApp = source => { + const {location, router} = this.props + const {redirectPath} = location.query + + if (!redirectPath) { + return router.push(`/sources/${source.id}/hosts`) + } + + const fixedPath = redirectPath.replace( + /\/sources\/[^/]*/, + `/sources/${source.id}` + ) + return router.push(fixedPath) + } + + private handleInputChange = (e: ChangeEvent) => { + let val = e.target.value + const name = e.target.name + + if (e.target.type === 'checkbox') { + val = e.target.checked as any + } + + this.setState(prevState => { + const source = { + ...prevState.source, + [name]: val, + } + + return {...prevState, source} + }) + } + + private handleBlurSourceURL = () => { + const {source, editMode} = this.state + if (editMode) { + this.setState(this.normalizeSource) + return + } + + if (!source.url) { + return + } + + this.setState(this.normalizeSource, this.createSourceOnBlur) + } } -const {func, shape, string} = PropTypes - -SourcePage.propTypes = { - params: shape({ - id: string, - sourceID: string, - }), - router: shape({ - push: func.isRequired, - }).isRequired, - location: shape({ - query: shape({ - redirectPath: string, - }).isRequired, - }).isRequired, - notify: func.isRequired, - addSource: func.isRequired, - updateSource: func.isRequired, +const mdtp = { + notify: notifyAction, + addSource: addSourceAction, + updateSource: updateSourceAction, } -const mapDispatchToProps = dispatch => ({ - notify: bindActionCreators(notifyAction, dispatch), - addSource: bindActionCreators(addSourceAction, dispatch), - updateSource: bindActionCreators(updateSourceAction, dispatch), -}) -export default connect(null, mapDispatchToProps)(withRouter(SourcePage)) +export default connect(null, mdtp)(withRouter(SourcePage)) diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index 60da481c9f..00fa6fc97b 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -25,7 +25,7 @@ import { TagValues, } from './query' import {AlertRule, Kapacitor, Task, RuleValues} from './kapacitor' -import {Source, SourceLinks} from './sources' +import {NewSource, Source, SourceLinks} from './sources' import {DropdownAction, DropdownItem, Constructable} from './shared' import { Notification, @@ -70,6 +70,7 @@ export { TagValues, AlertRule, Kapacitor, + NewSource, Source, SourceLinks, DropdownAction, diff --git a/ui/src/types/sources.ts b/ui/src/types/sources.ts index e912ae0472..2ba7fb6c40 100644 --- a/ui/src/types/sources.ts +++ b/ui/src/types/sources.ts @@ -1,5 +1,7 @@ import {Kapacitor, Service} from './' +export type NewSource = Pick> + export interface Source { id: string name: string diff --git a/ui/test/sources/components/SourceForm.test.js b/ui/test/sources/components/SourceForm.test.tsx similarity index 95% rename from ui/test/sources/components/SourceForm.test.js rename to ui/test/sources/components/SourceForm.test.tsx index 8375706d61..7e9e21f0fb 100644 --- a/ui/test/sources/components/SourceForm.test.js +++ b/ui/test/sources/components/SourceForm.test.tsx @@ -2,6 +2,7 @@ import React from 'react' import {shallow} from 'enzyme' import {SourceForm} from 'src/sources/components/SourceForm' +import {me} from 'test/resources' const setup = (override = {}) => { const noop = () => {} @@ -23,7 +24,7 @@ const setup = (override = {}) => { isUsingAuth: false, gotoPurgatory: noop, isInitialSource: false, - me: {}, + me, ...override, } From bf0749547a804885021c609cac25da0f878259a9 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Mon, 18 Jun 2018 01:02:44 -0700 Subject: [PATCH 12/18] Increase go test timeout to avoid invalid failures --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2aae583c55..37525c161a 100644 --- a/Makefile +++ b/Makefile @@ -95,7 +95,7 @@ internal.pb.go: bolt/internal/internal.proto test: jstest gotest gotestrace gotest: - go test ./... + go test -timeout 10s ./... gotestrace: go test -race ./... From e6cd29da58523ac8b43624828e57123081a91850 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Tue, 12 Jun 2018 13:20:30 -0700 Subject: [PATCH 13/18] Introduce new template variables UI PR: #3683 --- ui/src/dashboards/actions/index.ts | 3 +- .../components/CellEditorOverlay.tsx | 6 +- ui/src/dashboards/components/QueryMaker.tsx | 3 +- .../components/TemplateControlBar.tsx | 71 ---- .../components/TemplateControlDropdown.tsx | 53 --- .../components/template_variables/Row.js | 383 ------------------ .../template_variables/RowValues.js | 44 -- .../components/template_variables/Table.js | 80 ---- .../template_variables/TableInput.js | 43 -- .../TemplateQueryBuilder.js | 134 ------ .../TemplateVariableManager.js | 242 ----------- ui/src/dashboards/constants/index.ts | 79 ---- ui/src/dashboards/containers/DashboardPage.js | 39 +- ui/src/dashboards/reducers/ui.js | 2 +- ui/src/dashboards/utils/tempVars.ts | 65 +-- ui/src/shared/apis/query.ts | 2 +- .../components/DropdownLoadingPlaceholder.tsx | 24 ++ .../components/SimpleOverlayTechnology.tsx | 12 + ui/src/shared/constants/index.tsx | 5 +- ui/src/style/chronograf.scss | 1 + .../components/dropdown-placeholder.scss | 14 + .../components/edit-template-variable.scss | 153 +++++++ .../components/template-control-bar.scss | 44 +- ui/src/style/pages/dashboards.scss | 6 + .../components/CSVTemplateBuilder.tsx | 91 +++++ .../components/DatabasesTemplateBuilder.tsx | 94 +++++ .../components/FieldKeysTemplateBuilder.tsx | 39 ++ .../components/KeysTemplateBuilder.tsx | 244 +++++++++++ .../MeasurementsTemplateBuilder.tsx | 165 ++++++++ .../components/TagKeysTemplateBuilder.tsx | 42 ++ .../components/TagValuesTemplateBuilder.tsx | 308 ++++++++++++++ .../components/TemplateControlBar.tsx | 129 ++++++ .../components/TemplateControlDropdown.tsx | 117 ++++++ .../components/TemplateMetaQueryPreview.tsx | 61 +++ .../components/TemplatePreviewList.tsx | 52 +++ .../components/TemplateVariableEditor.tsx | 288 +++++++++++++ ui/src/tempVars/constants/index.ts | 173 ++++++++ ui/src/types/dashboard.ts | 2 +- ui/src/types/index.ts | 8 + ui/src/types/tempVars.ts | 47 ++- ui/test/dashboards/reducers/ui.test.ts | 9 +- ui/test/dashboards/templating.test.js | 2 +- ui/test/fixtures/index.ts | 48 +-- ui/test/resources.ts | 11 +- .../components/TemplateControlBar.test.tsx | 15 +- 45 files changed, 2151 insertions(+), 1302 deletions(-) delete mode 100644 ui/src/dashboards/components/TemplateControlBar.tsx delete mode 100644 ui/src/dashboards/components/TemplateControlDropdown.tsx delete mode 100644 ui/src/dashboards/components/template_variables/Row.js delete mode 100644 ui/src/dashboards/components/template_variables/RowValues.js delete mode 100644 ui/src/dashboards/components/template_variables/Table.js delete mode 100644 ui/src/dashboards/components/template_variables/TableInput.js delete mode 100644 ui/src/dashboards/components/template_variables/TemplateQueryBuilder.js delete mode 100644 ui/src/dashboards/components/template_variables/TemplateVariableManager.js create mode 100644 ui/src/shared/components/DropdownLoadingPlaceholder.tsx create mode 100644 ui/src/shared/components/SimpleOverlayTechnology.tsx create mode 100644 ui/src/style/components/dropdown-placeholder.scss create mode 100644 ui/src/style/components/edit-template-variable.scss create mode 100644 ui/src/tempVars/components/CSVTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/DatabasesTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/FieldKeysTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/KeysTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/MeasurementsTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/TagKeysTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/TagValuesTemplateBuilder.tsx create mode 100644 ui/src/tempVars/components/TemplateControlBar.tsx create mode 100644 ui/src/tempVars/components/TemplateControlDropdown.tsx create mode 100644 ui/src/tempVars/components/TemplateMetaQueryPreview.tsx create mode 100644 ui/src/tempVars/components/TemplatePreviewList.tsx create mode 100644 ui/src/tempVars/components/TemplateVariableEditor.tsx create mode 100644 ui/src/tempVars/constants/index.ts rename ui/test/{dashboards => tempVars}/components/TemplateControlBar.test.tsx (73%) diff --git a/ui/src/dashboards/actions/index.ts b/ui/src/dashboards/actions/index.ts index cc2c61f35d..658574bd6e 100644 --- a/ui/src/dashboards/actions/index.ts +++ b/ui/src/dashboards/actions/index.ts @@ -61,6 +61,7 @@ import { Cell, Source, Template, + TemplateType, URLQueryParams, } from 'src/types' import {CellType, DashboardName} from 'src/types/dashboard' @@ -444,7 +445,7 @@ export const getChronografVersion = () => async (): Promise => { const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => { const templates = getDeep(dashboard, 'templates', []).map( template => { - if (template.type === 'csv') { + if (template.type === TemplateType.CSV) { return template } diff --git a/ui/src/dashboards/components/CellEditorOverlay.tsx b/ui/src/dashboards/components/CellEditorOverlay.tsx index 31baee8985..6520416685 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.tsx +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -19,10 +19,8 @@ import {getQueryConfigAndStatus} from 'src/shared/apis' import {IS_STATIC_LEGEND} from 'src/shared/constants' import {nextSource} from 'src/dashboards/utils/sources' -import { - removeUnselectedTemplateValues, - TYPE_QUERY_CONFIG, -} from 'src/dashboards/constants' +import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants' +import {removeUnselectedTemplateValues} from 'src/tempVars/constants' import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames' import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import { diff --git a/ui/src/dashboards/components/QueryMaker.tsx b/ui/src/dashboards/components/QueryMaker.tsx index 13e4fcecc2..1aa69c5a87 100644 --- a/ui/src/dashboards/components/QueryMaker.tsx +++ b/ui/src/dashboards/components/QueryMaker.tsx @@ -6,7 +6,8 @@ import QueryTabList from 'src/shared/components/QueryTabList' import QueryTextArea from 'src/dashboards/components/QueryTextArea' import SchemaExplorer from 'src/shared/components/SchemaExplorer' import {buildQuery} from 'src/utils/influxql' -import {TYPE_QUERY_CONFIG, TEMPLATE_RANGE} from 'src/dashboards/constants' +import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants' +import {TEMPLATE_RANGE} from 'src/tempVars/constants' import {QueryConfig, Source, SourceLinks, TimeRange} from 'src/types' import {CellEditorOverlayActions} from 'src/dashboards/components/CellEditorOverlay' diff --git a/ui/src/dashboards/components/TemplateControlBar.tsx b/ui/src/dashboards/components/TemplateControlBar.tsx deleted file mode 100644 index 08464cd513..0000000000 --- a/ui/src/dashboards/components/TemplateControlBar.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, {Component} from 'react' -import _ from 'lodash' - -import classnames from 'classnames' -import uuid from 'uuid' - -import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' -import TemplateControlDropdown from 'src/dashboards/components/TemplateControlDropdown' -import {Template} from 'src/types/tempVars' - -interface Props { - meRole: string - isUsingAuth: boolean - templates: Template[] - isOpen: boolean - onOpenTemplateManager: () => void - onSelectTemplate: (id: string) => void -} - -class TemplateControlBar extends Component { - public shouldComponentUpdate(nextProps) { - return !_.isEqual(this.props, nextProps) - } - - public render() { - const { - isOpen, - templates, - onSelectTemplate, - onOpenTemplateManager, - meRole, - isUsingAuth, - } = this.props - - return ( -
-
-
- {templates && templates.length ? ( - templates.map(template => ( - - )) - ) : ( -
- This dashboard does not have any{' '} - Template Variables -
- )} -
- - - -
-
- ) - } -} - -export default TemplateControlBar diff --git a/ui/src/dashboards/components/TemplateControlDropdown.tsx b/ui/src/dashboards/components/TemplateControlDropdown.tsx deleted file mode 100644 index 0e0c34ca9a..0000000000 --- a/ui/src/dashboards/components/TemplateControlDropdown.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, {SFC} from 'react' - -import Dropdown from 'src/shared/components/Dropdown' -import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar' -import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized' -import {Template} from 'src/types/tempVars' - -interface Props { - template: Template - meRole: string - isUsingAuth: boolean - onSelectTemplate: (id: string) => void -} - -// TODO: change Dropdown to a MultiSelectDropdown, `selected` to -// the full array, and [item] to all `selected` values when we update -// this component to support multiple values - -const TemplateControlDropdown: SFC = ({ - template, - onSelectTemplate, - isUsingAuth, - meRole, -}) => { - const dropdownItems = template.values.map(value => ({ - ...value, - text: value.value, - })) - - const dropdownStyle = template.values.length - ? {minWidth: calculateDropdownWidth(template.values)} - : null - - const selectedItem = dropdownItems.find(item => item.selected) || - dropdownItems[0] || {text: '(No values)'} - - return ( -
- - -
- ) -} - -export default TemplateControlDropdown diff --git a/ui/src/dashboards/components/template_variables/Row.js b/ui/src/dashboards/components/template_variables/Row.js deleted file mode 100644 index c589309c55..0000000000 --- a/ui/src/dashboards/components/template_variables/Row.js +++ /dev/null @@ -1,383 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import {connect} from 'react-redux' -import {bindActionCreators} from 'redux' - -import uniq from 'lodash/uniq' - -import OnClickOutside from 'react-onclickoutside' -import classnames from 'classnames' - -import Dropdown from 'shared/components/Dropdown' -import TemplateQueryBuilder from 'src/dashboards/components/template_variables/TemplateQueryBuilder' -import TableInput from 'src/dashboards/components/template_variables/TableInput' -import RowValues from 'src/dashboards/components/template_variables/RowValues' -import ConfirmButton from 'src/shared/components/ConfirmButton' - -import {getTempVarValuesBySourceQuery as getTempVarValuesBySourceQueryAJAX} from 'src/dashboards/apis' - -import parsers from 'shared/parsing' - -import {TEMPLATE_TYPES} from 'src/dashboards/constants' -import generateTemplateVariableQuery from 'src/dashboards/utils/tempVars' - -import {errorThrown as errorThrownAction} from 'shared/actions/errors' -import {notify as notifyAction} from 'shared/actions/notifications' - -import {notifyTempVarAlreadyExists} from 'shared/copy/notifications' -import {ErrorHandling} from 'src/shared/decorators/errors' - -const compact = values => uniq(values).filter(value => /\S/.test(value)) - -const TemplateVariableRow = ({ - template: {id, tempVar, values}, - isEditing, - selectedType, - selectedDatabase, - selectedMeasurement, - onSelectType, - onSelectDatabase, - onSelectMeasurement, - selectedTagKey, - onSelectTagKey, - onStartEdit, - onCancelEdit, - autoFocusTarget, - onSubmit, - onErrorThrown, - onDeleteTempVar, - source, -}) => ( -
-
- -
-
- t.type === selectedType).text} - className="dropdown-140" - /> -
-
- - -
-
- {isEditing ? ( -
- - -
- ) : ( -
- -
- )} -
-
-) - -@ErrorHandling -class RowWrapper extends Component { - constructor(props) { - super(props) - const { - template: {type, query, isNew}, - } = this.props - - this.state = { - isEditing: !!isNew, - isNew: !!isNew, - hasBeenSavedToComponentStateOnce: !isNew, - selectedType: type, - selectedDatabase: query && query.db, - selectedMeasurement: query && query.measurement, - selectedTagKey: query && query.tagKey, - autoFocusTarget: 'tempVar', - } - } - - handleSubmit = ({ - selectedDatabase: database, - selectedMeasurement: measurement, - selectedTagKey: tagKey, - selectedType: type, - }) => async e => { - e.preventDefault() - - const { - source, - template, - template: {id}, - onRunQuerySuccess, - onRunQueryFailure, - tempVarAlreadyExists, - notify, - } = this.props - - const _tempVar = e.target.tempVar.value.replace(/\u003a/g, '') - const tempVar = `\u003a${_tempVar}\u003a` // add ':'s - - if (tempVarAlreadyExists(tempVar, id)) { - return notify(notifyTempVarAlreadyExists(_tempVar)) - } - - this.setState({ - isEditing: false, - hasBeenSavedToComponentStateOnce: true, - }) - - const {query, tempVars} = generateTemplateVariableQuery({ - type, - tempVar, - query: { - database, - // rp, TODO - measurement, - tagKey, - }, - }) - - const queryConfig = { - type, - tempVars, - query, - database, - // rp: TODO - measurement, - tagKey, - } - - try { - let parsedData - if (type === 'csv') { - parsedData = e.target.values.value.split(',').map(value => value.trim()) - } else { - parsedData = await this.getTempVarValuesBySourceQuery( - source, - queryConfig - ) - } - - onRunQuerySuccess(template, queryConfig, compact(parsedData), tempVar) - } catch (error) { - onRunQueryFailure(error) - } - } - - handleClickOutside() { - this.handleCancelEdit() - } - - handleStartEdit = name => () => { - this.setState({isEditing: true, autoFocusTarget: name}) - } - - handleCancelEdit = () => { - const { - template: { - type, - query: {db, measurement, tagKey}, - id, - }, - onDelete, - } = this.props - const {hasBeenSavedToComponentStateOnce} = this.state - - if (!hasBeenSavedToComponentStateOnce) { - return onDelete(id) - } - this.setState({ - selectedType: type, - selectedDatabase: db, - selectedMeasurement: measurement, - selectedTagKey: tagKey, - isEditing: false, - }) - } - - handleSelectType = item => { - this.setState({ - selectedType: item.type, - selectedDatabase: null, - selectedMeasurement: null, - selectedTagKey: null, - }) - } - - handleSelectDatabase = item => { - this.setState({selectedDatabase: item.text}) - } - - handleSelectMeasurement = item => { - this.setState({selectedMeasurement: item.text}) - } - - handleSelectTagKey = item => { - this.setState({selectedTagKey: item.text}) - } - - getTempVarValuesBySourceQuery = async ( - source, - {query, database, rp, tempVars, type, measurement, tagKey} - ) => { - try { - const {data} = await getTempVarValuesBySourceQueryAJAX(source, { - query, - db: database, - rp, - tempVars, - }) - const parsedData = parsers[type](data, tagKey || measurement) // tagKey covers tagKey and fieldKey - if (parsedData.errors.length) { - throw parsedData.errors - } - - return parsedData[type] - } catch (error) { - console.error(error) - throw error - } - } - - handleDelete = id => () => { - this.props.onDelete(id) - } - - render() { - const { - isEditing, - selectedType, - selectedDatabase, - selectedMeasurement, - selectedTagKey, - autoFocusTarget, - } = this.state - - return ( - - ) - } -} - -const {arrayOf, bool, func, shape, string} = PropTypes - -RowWrapper.propTypes = { - source: shape({ - links: shape({ - proxy: string, - }), - }).isRequired, - template: shape({ - type: string.isRequired, - tempVar: string.isRequired, - query: shape({ - db: string, - influxql: string, - measurement: string, - tagKey: string, - }), - values: arrayOf( - shape({ - value: string.isRequired, - type: string.isRequired, - selected: bool.isRequired, - }) - ).isRequired, - links: shape({ - self: string.isRequired, - }), - }), - onRunQuerySuccess: func.isRequired, - onRunQueryFailure: func.isRequired, - onDelete: func.isRequired, - tempVarAlreadyExists: func.isRequired, - notify: func.isRequired, -} - -TemplateVariableRow.propTypes = { - ...RowWrapper.propTypes, - selectedType: string.isRequired, - selectedDatabase: string, - selectedTagKey: string, - onSelectType: func.isRequired, - onSelectDatabase: func.isRequired, - onSelectTagKey: func.isRequired, - onStartEdit: func.isRequired, - onCancelEdit: func.isRequired, - onSubmit: func.isRequired, - onErrorThrown: func.isRequired, -} - -const mapDispatchToProps = dispatch => ({ - onErrorThrown: bindActionCreators(errorThrownAction, dispatch), - notify: bindActionCreators(notifyAction, dispatch), -}) - -export default connect(null, mapDispatchToProps)(OnClickOutside(RowWrapper)) diff --git a/ui/src/dashboards/components/template_variables/RowValues.js b/ui/src/dashboards/components/template_variables/RowValues.js deleted file mode 100644 index bc0a915de0..0000000000 --- a/ui/src/dashboards/components/template_variables/RowValues.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import TableInput from 'src/dashboards/components/template_variables/TableInput' - -const RowValues = ({ - selectedType, - values = [], - isEditing, - onStartEdit, - autoFocusTarget, -}) => { - const _values = values.map(v => v.value).join(', ') - - if (selectedType === 'csv') { - return ( - - ) - } - return ( -
- {values.length ? _values : 'No values to display'} -
- ) -} - -const {arrayOf, bool, func, shape, string} = PropTypes - -RowValues.propTypes = { - selectedType: string.isRequired, - values: arrayOf(shape()), - isEditing: bool.isRequired, - onStartEdit: func.isRequired, - autoFocusTarget: string, -} - -export default RowValues diff --git a/ui/src/dashboards/components/template_variables/Table.js b/ui/src/dashboards/components/template_variables/Table.js deleted file mode 100644 index 252d0f72a2..0000000000 --- a/ui/src/dashboards/components/template_variables/Table.js +++ /dev/null @@ -1,80 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import TemplateVariableRow from 'src/dashboards/components/template_variables/Row' - -const TemplateVariableTable = ({ - source, - templates, - onRunQuerySuccess, - onRunQueryFailure, - onDelete, - tempVarAlreadyExists, -}) => ( -
- {templates.length ? ( -
-
-
Variable
-
Type
-
Definition / Values
-
-
-
- {templates.map(t => ( - - ))} -
-
- ) : ( -
-

- You have no Template Variables, why not create one? -

-
- )} -
-) - -const {arrayOf, bool, func, shape, string} = PropTypes - -TemplateVariableTable.propTypes = { - source: shape({ - links: shape({ - proxy: string, - }), - }).isRequired, - templates: arrayOf( - shape({ - type: string.isRequired, - tempVar: string.isRequired, - query: shape({ - db: string, - influxql: string, - measurement: string, - tagKey: string, - }), - values: arrayOf( - shape({ - value: string.isRequired, - type: string.isRequired, - selected: bool.isRequired, - }) - ).isRequired, - }) - ), - onRunQuerySuccess: func.isRequired, - onRunQueryFailure: func.isRequired, - onDelete: func.isRequired, - tempVarAlreadyExists: func.isRequired, -} - -export default TemplateVariableTable diff --git a/ui/src/dashboards/components/template_variables/TableInput.js b/ui/src/dashboards/components/template_variables/TableInput.js deleted file mode 100644 index ebc50d612f..0000000000 --- a/ui/src/dashboards/components/template_variables/TableInput.js +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -const TableInput = ({ - name, - defaultValue, - isEditing, - onStartEdit, - autoFocusTarget, -}) => { - return isEditing ? ( -
- -
- ) : ( -
-
{defaultValue}
-
- ) -} - -const {bool, func, string} = PropTypes - -TableInput.propTypes = { - defaultValue: string, - isEditing: bool.isRequired, - onStartEdit: func.isRequired, - name: string.isRequired, - autoFocusTarget: string, -} - -export default TableInput diff --git a/ui/src/dashboards/components/template_variables/TemplateQueryBuilder.js b/ui/src/dashboards/components/template_variables/TemplateQueryBuilder.js deleted file mode 100644 index 198573fd54..0000000000 --- a/ui/src/dashboards/components/template_variables/TemplateQueryBuilder.js +++ /dev/null @@ -1,134 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import DatabaseDropdown from 'shared/components/DatabaseDropdown' -import MeasurementDropdown from 'src/dashboards/components/MeasurementDropdown' -import TagKeyDropdown from 'src/dashboards/components/TagKeyDropdown' - -const TemplateQueryBuilder = ({ - selectedType, - selectedDatabase, - selectedMeasurement, - selectedTagKey, - onSelectDatabase, - onSelectMeasurement, - onSelectTagKey, - onStartEdit, - onErrorThrown, - source, -}) => { - switch (selectedType) { - case 'csv': - return null - case 'databases': - return
SHOW DATABASES
- case 'measurements': - return ( -
- SHOW MEASUREMENTS ON - -
- ) - case 'fieldKeys': - case 'tagKeys': - return ( -
- - SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON - - - FROM - {selectedDatabase ? ( - - ) : ( -
No database selected
- )} -
- ) - case 'tagValues': - return ( -
- SHOW TAG VALUES ON - - FROM - {selectedDatabase ? ( - - ) : ( - 'Pick a DB' - )} - WITH KEY = - {selectedMeasurement ? ( - - ) : ( - 'Pick a Tag Key' - )} -
- ) - default: - return ( -
- n/a -
- ) - } -} - -const {func, shape, string} = PropTypes - -TemplateQueryBuilder.propTypes = { - selectedType: string.isRequired, - onSelectDatabase: func.isRequired, - onSelectMeasurement: func.isRequired, - onSelectTagKey: func.isRequired, - onStartEdit: func.isRequired, - selectedMeasurement: string, - selectedDatabase: string, - selectedTagKey: string, - onErrorThrown: func.isRequired, - source: shape({ - links: shape({ - proxy: string.isRequired, - }).isRequired, - }).isRequired, -} - -export default TemplateQueryBuilder diff --git a/ui/src/dashboards/components/template_variables/TemplateVariableManager.js b/ui/src/dashboards/components/template_variables/TemplateVariableManager.js deleted file mode 100644 index 5cdd9dbf6f..0000000000 --- a/ui/src/dashboards/components/template_variables/TemplateVariableManager.js +++ /dev/null @@ -1,242 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import classnames from 'classnames' -import uuid from 'uuid' - -import TemplateVariableTable from 'src/dashboards/components/template_variables/Table' - -import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants' - -const TemplateVariableManager = ({ - source, - onClose, - onDelete, - isEdited, - templates, - onAddVariable, - onRunQuerySuccess, - onRunQueryFailure, - tempVarAlreadyExists, - onSaveTemplatesSuccess, - onEditTemplateVariables, -}) => ( -
-
-
-

Template Variables

-
-
- - - -
-
-
- -
-
-) - -class TemplateVariableManagerWrapper extends Component { - constructor(props) { - super(props) - - this.state = { - rows: this.props.templates, - isEdited: false, - } - } - - onAddVariable = () => { - const {rows} = this.state - - const newRow = { - tempVar: '', - values: [], - id: uuid.v4(), - type: 'csv', - query: { - influxql: '', - db: '', - // rp, TODO - measurement: '', - tagKey: '', - }, - isNew: true, - } - - const newRows = [newRow, ...rows] - - this.setState({rows: newRows}) - } - - onRunQuerySuccess = (template, queryConfig, parsedData, tempVar) => { - const {rows} = this.state - const {id, links} = template - const { - type, - query: influxql, - database: db, - measurement, - tagKey, - } = queryConfig - - // Determine which is the selectedValue, if any - const currentRow = rows.find(row => row.id === id) - - let selectedValue - if (currentRow && currentRow.values && currentRow.values.length) { - const matchedValue = currentRow.values.find(val => val.selected) - if (matchedValue) { - selectedValue = matchedValue.value - } - } - - if ( - !selectedValue && - currentRow && - currentRow.values && - currentRow.values.length - ) { - selectedValue = currentRow.values[0].value - } - - if (!selectedValue) { - selectedValue = parsedData[0] - } - - const values = parsedData.map(value => ({ - value, - type: TEMPLATE_VARIABLE_TYPES[type], - selected: selectedValue === value, - })) - - const templateVariable = { - tempVar, - values, - id, - type, - query: { - influxql, - db, - // rp, TODO - measurement, - tagKey, - }, - links, - } - - const newRows = rows.map(r => (r.id === template.id ? templateVariable : r)) - - this.setState({rows: newRows, isEdited: true}) - } - - onSaveTemplatesSuccess = () => { - const {rows} = this.state - - const newRows = rows.map(row => ({...row, isNew: false})) - - this.setState({rows: newRows, isEdited: false}) - } - - onDeleteTemplateVariable = templateID => { - const {rows} = this.state - - const newRows = rows.filter(({id}) => id !== templateID) - - this.setState({rows: newRows, isEdited: true}) - } - - tempVarAlreadyExists = (testTempVar, testID) => { - const {rows: tempVars} = this.state - return tempVars.some( - ({tempVar, id}) => tempVar === testTempVar && id !== testID - ) - } - - handleDismissManager = () => { - const {onDismissOverlay} = this.props - const {isEdited} = this.state - - if ( - !isEdited || - (isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert - ) { - onDismissOverlay() - } - } - - render() { - const {rows, isEdited} = this.state - return ( - - ) - } -} - -const {arrayOf, bool, func, shape, string} = PropTypes - -TemplateVariableManager.propTypes = { - ...TemplateVariableManagerWrapper.propTypes, - onRunQuerySuccess: func.isRequired, - onSaveTemplatesSuccess: func.isRequired, - onAddVariable: func.isRequired, - isEdited: bool.isRequired, - onDelete: func.isRequired, -} - -TemplateVariableManagerWrapper.propTypes = { - onEditTemplateVariables: func.isRequired, - templates: arrayOf( - shape({ - type: string.isRequired, - tempVar: string.isRequired, - query: shape({ - db: string, - influxql: string, - }), - values: arrayOf( - shape({ - value: string.isRequired, - type: string.isRequired, - selected: bool.isRequired, - }) - ).isRequired, - }) - ), - onRunQueryFailure: func.isRequired, - onDismissOverlay: func, -} - -export default TemplateVariableManagerWrapper diff --git a/ui/src/dashboards/constants/index.ts b/ui/src/dashboards/constants/index.ts index 6416693020..99ec7a64a3 100644 --- a/ui/src/dashboards/constants/index.ts +++ b/ui/src/dashboards/constants/index.ts @@ -4,8 +4,6 @@ import { } from 'src/shared/constants/tableGraph' import {Cell, QueryConfig} from 'src/types' import {CellType, Dashboard, DecimalPlaces} from 'src/types/dashboard' -import {TimeRange} from 'src/types/query' -import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants' export const UNTITLED_GRAPH: string = 'Untitled Graph' @@ -106,84 +104,7 @@ export const NEW_DASHBOARD: NewDefaultDashboard = { cells: [NEW_DEFAULT_DASHBOARD_CELL], } -export const TEMPLATE_TYPES = [ - { - text: 'CSV', - type: 'csv', - }, - { - text: 'Databases', - type: 'databases', - }, - { - text: 'Measurements', - type: 'measurements', - }, - { - text: 'Field Keys', - type: 'fieldKeys', - }, - { - text: 'Tag Keys', - type: 'tagKeys', - }, - { - text: 'Tag Values', - type: 'tagValues', - }, -] - -export const TEMPLATE_VARIABLE_TYPES = { - csv: 'csv', - databases: 'database', - measurements: 'measurement', - fieldKeys: 'fieldKey', - tagKeys: 'tagKey', - tagValues: 'tagValue', -} - -interface TemplateVariableQueries { - databases: string - measurements: string - fieldKeys: string - tagKeys: string - tagValues: string -} - -export const TEMPLATE_VARIABLE_QUERIES: TemplateVariableQueries = { - databases: 'SHOW DATABASES', - measurements: 'SHOW MEASUREMENTS ON :database:', - fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:', - tagKeys: 'SHOW TAG KEYS ON :database: FROM :measurement:', - tagValues: - 'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:', -} - -export const MATCH_INCOMPLETE_TEMPLATES = /:[\w-]*/g - -export const applyMasks = query => { - const matchWholeTemplates = /:([\w-]*):/g - const maskForWholeTemplates = '😸$1😸' - return query.replace(matchWholeTemplates, maskForWholeTemplates) -} -export const insertTempVar = (query, tempVar) => { - return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar) -} -export const unMask = query => { - return query.replace(/😸/g, ':') -} -export const removeUnselectedTemplateValues = templates => { - return templates.map(template => { - const selectedValues = template.values.filter(value => value.selected) - return {...template, values: selectedValues} - }) -} - export const TYPE_QUERY_CONFIG: string = 'queryConfig' export const TYPE_SHIFTED: string = 'shifted queryConfig' export const TYPE_FLUX: string = 'flux' export const DASHBOARD_NAME_MAX_LENGTH: number = 50 -export const TEMPLATE_RANGE: TimeRange = { - upper: null, - lower: TEMP_VAR_DASHBOARD_TIME, -} diff --git a/ui/src/dashboards/containers/DashboardPage.js b/ui/src/dashboards/containers/DashboardPage.js index 5cfbaffbcc..5f7e2027b2 100644 --- a/ui/src/dashboards/containers/DashboardPage.js +++ b/ui/src/dashboards/containers/DashboardPage.js @@ -11,9 +11,8 @@ import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized' import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay' import DashboardHeader from 'src/dashboards/components/DashboardHeader' import Dashboard from 'src/dashboards/components/Dashboard' -import TemplateVariableManager from 'src/dashboards/components/template_variables/TemplateVariableManager' import ManualRefresh from 'src/shared/components/ManualRefresh' -import TemplateControlBar from 'src/dashboards/components/TemplateControlBar' +import TemplateControlBar from 'src/tempVars/components/TemplateControlBar' import {errorThrown as errorThrownAction} from 'shared/actions/errors' import {notify as notifyAction} from 'shared/actions/notifications' @@ -48,7 +47,6 @@ import {FORMAT_INFLUXQL, defaultTimeRange} from 'src/shared/data/timeRanges' import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas' import {ErrorHandling} from 'src/shared/decorators/errors' -import {OverlayContext} from 'src/shared/components/OverlayTechnology' import {getDeep} from 'src/utils/wrappers' @@ -174,31 +172,6 @@ class DashboardPage extends Component { return topInView && bottomInView } - handleOpenTemplateManager = () => { - const {handleShowOverlay, dashboard, source} = this.props - const options = { - dismissOnClickOutside: false, - dismissOnEscape: false, - } - - handleShowOverlay( - - {({onDismissOverlay}) => { - return ( - - ) - }} - , - options - ) - } - handleSaveEditedCell = newCell => { const { dashboardActions, @@ -311,10 +284,7 @@ class DashboardPage extends Component { dashboardActions.putDashboardByID(dashboardID) } - handleEditTemplateVariables = ( - templates, - onSaveTemplatesSuccess - ) => async () => { + handleSaveTemplateVariables = async templates => { const {location, dashboardActions, dashboard} = this.props try { @@ -322,7 +292,6 @@ class DashboardPage extends Component { ...dashboard, templates, }) - onSaveTemplatesSuccess() const deletedTempVars = dashboard.templates.filter( ({tempVar: oldTempVar}) => !templates.find(({tempVar: newTempVar}) => oldTempVar === newTempVar) @@ -481,9 +450,10 @@ class DashboardPage extends Component { templates={dashboard && dashboard.templates} meRole={meRole} isUsingAuth={isUsingAuth} + onSaveTemplates={this.handleSaveTemplateVariables} onSelectTemplate={this.handleSelectTemplate} - onOpenTemplateManager={this.handleOpenTemplateManager} isOpen={showTemplateControlBar} + source={source} /> )} {dashboard ? ( @@ -504,7 +474,6 @@ class DashboardPage extends Component { onDeleteCell={this.handleDeleteDashboardCell} onCloneCell={this.handleCloneCell} showTemplateControlBar={showTemplateControlBar} - onOpenTemplateManager={this.handleOpenTemplateManager} templatesIncludingDashTime={templatesIncludingDashTime} onSummonOverlayTechnologies={handleShowCellEditorOverlay} /> diff --git a/ui/src/dashboards/reducers/ui.js b/ui/src/dashboards/reducers/ui.js index 988d11a929..acdb672e6d 100644 --- a/ui/src/dashboards/reducers/ui.js +++ b/ui/src/dashboards/reducers/ui.js @@ -16,7 +16,7 @@ export const initialState = { activeCellID: '', } -import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants' +import {TEMPLATE_VARIABLE_TYPES} from 'src/tempVars/constants' const ui = (state = initialState, action) => { switch (action.type) { diff --git a/ui/src/dashboards/utils/tempVars.ts b/ui/src/dashboards/utils/tempVars.ts index f7c82c0b74..d122b8aedd 100644 --- a/ui/src/dashboards/utils/tempVars.ts +++ b/ui/src/dashboards/utils/tempVars.ts @@ -1,6 +1,5 @@ import _ from 'lodash' -import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants' import { Dashboard, Template, @@ -8,65 +7,7 @@ import { TemplateValue, URLQueryParams, } from 'src/types' -import {TemplateUpdate} from 'src/types/tempVars' - -interface PartialTemplateWithQuery { - query: string - tempVars: Array> -} - -const generateTemplateVariableQuery = ({ - type, - query: { - database, - // rp, TODO - measurement, - tagKey, - }, -}: Partial