diff --git a/ui/src/dashboards/components/Visualization.tsx b/ui/src/dashboards/components/Visualization.tsx index b807d478f1..d915290aec 100644 --- a/ui/src/dashboards/components/Visualization.tsx +++ b/ui/src/dashboards/components/Visualization.tsx @@ -17,6 +17,7 @@ import { import {ColorString, ColorNumber} from 'src/types/colors' interface Props { + axes: Axes type: CellType source: Source autoRefresh: number @@ -24,7 +25,6 @@ interface Props { timeRange: TimeRange queryConfigs: QueryConfig[] editQueryStatus: () => void - axes: Axes tableOptions: TableOptions timeFormat: string decimalPlaces: DecimalPlaces diff --git a/ui/src/dashboards/constants/index.ts b/ui/src/dashboards/constants/index.ts index c18266bc8a..a412305f37 100644 --- a/ui/src/dashboards/constants/index.ts +++ b/ui/src/dashboards/constants/index.ts @@ -63,6 +63,7 @@ export const NEW_DEFAULT_DASHBOARD_CELL: NewDefaultCell = { timeFormat: DEFAULT_TIME_FORMAT, decimalPlaces: DEFAULT_DECIMAL_PLACES, fieldOptions: [DEFAULT_TIME_FIELD], + inView: true, } interface EmptyDefaultDashboardCell { diff --git a/ui/src/dashboards/containers/DashboardPage.tsx b/ui/src/dashboards/containers/DashboardPage.tsx index 3cafd9757b..27dd9908af 100644 --- a/ui/src/dashboards/containers/DashboardPage.tsx +++ b/ui/src/dashboards/containers/DashboardPage.tsx @@ -25,6 +25,7 @@ import idNormalizer, {TYPE_ID} from 'src/normalizers/id' import {millisecondTimeRange} from 'src/dashboards/utils/time' import {getDeep} from 'src/utils/wrappers' import {updateDashboardLinks} from 'src/dashboards/utils/dashboardSwitcherLinks' +import AutoRefresh from 'src/utils/AutoRefresh' // APIs import {loadDashboardLinks} from 'src/dashboards/apis' @@ -110,39 +111,33 @@ interface Props extends ManualRefreshProps, WithRouterProps { } interface State { - isEditMode: boolean - selectedCell: DashboardsModels.Cell | null scrollTop: number + isEditMode: boolean windowHeight: number + selectedCell: DashboardsModels.Cell | null dashboardLinks: DashboardsModels.DashboardSwitcherLinks } @ErrorHandling class DashboardPage extends Component { - private intervalID: number - public constructor(props: Props) { super(props) this.state = { + scrollTop: 0, isEditMode: false, selectedCell: null, - scrollTop: 0, windowHeight: window.innerHeight, dashboardLinks: EMPTY_LINKS, } } public async componentDidMount() { - const {source, getAnnotationsAsync, timeRange, autoRefresh} = this.props - - const annotationRange = millisecondTimeRange(timeRange) - getAnnotationsAsync(source.links.annotations, annotationRange) + const {autoRefresh} = this.props if (autoRefresh) { - this.intervalID = window.setInterval(() => { - getAnnotationsAsync(source.links.annotations, annotationRange) - }, autoRefresh) + AutoRefresh.poll(autoRefresh) + AutoRefresh.subscribe(this.fetchAnnotations) } window.addEventListener('resize', this.handleWindowResize, true) @@ -152,45 +147,37 @@ class DashboardPage extends Component { this.getDashboardLinks() } - public componentWillReceiveProps(nextProps: Props) { - const {source, getAnnotationsAsync, timeRange} = this.props - if (this.props.autoRefresh !== nextProps.autoRefresh) { - clearInterval(this.intervalID) - this.intervalID = null - const annotationRange = millisecondTimeRange(timeRange) - if (nextProps.autoRefresh) { - this.intervalID = window.setInterval(() => { - getAnnotationsAsync(source.links.annotations, annotationRange) - }, nextProps.autoRefresh) - } - } + public fetchAnnotations = () => { + const {source, timeRange, getAnnotationsAsync} = this.props + const rangeMs = millisecondTimeRange(timeRange) + getAnnotationsAsync(source.links.annotations, rangeMs) } public componentDidUpdate(prevProps: Props) { + const {dashboard, autoRefresh} = this.props + const prevPath = getDeep(prevProps.location, 'pathname', null) const thisPath = getDeep(this.props.location, 'pathname', null) - const templates = getDeep( - this.props.dashboard, - 'templates', - [] - ).map(t => t.tempVar) - const prevTemplates = getDeep( - prevProps.dashboard, - 'templates', - [] - ).map(t => t.tempVar) - const isTemplateDeleted: boolean = - _.intersection(templates, prevTemplates).length !== prevTemplates.length + const templates = this.parseTempVar(dashboard) + const prevTemplates = this.parseTempVar(prevProps.dashboard) + + const intersection = _.intersection(templates, prevTemplates) + const isTemplateDeleted = intersection.length !== prevTemplates.length if ((prevPath && thisPath && prevPath !== thisPath) || isTemplateDeleted) { this.getDashboard() } + + if (autoRefresh !== prevProps.autoRefresh) { + AutoRefresh.poll(autoRefresh) + } } public componentWillUnmount() { - clearInterval(this.intervalID) - this.intervalID = null + AutoRefresh.stopPolling() + AutoRefresh.unsubscribe(this.fetchAnnotations) + window.removeEventListener('resize', this.handleWindowResize, true) this.props.handleDismissEditingAnnotation() } @@ -217,7 +204,6 @@ class DashboardPage extends Component { cellQueryStatus, thresholdsListType, thresholdsListColors, - inPresentationMode, handleChooseAutoRefresh, handleShowCellEditorOverlay, @@ -346,6 +332,12 @@ class DashboardPage extends Component { ) } + public parseTempVar( + dashboard: DashboardsModels.Dashboard + ): TempVarsModels.Template[] { + return getDeep(dashboard, 'templates', []).map(t => t.tempVar) + } + private handleWindowResize = (): void => { this.setState({windowHeight: window.innerHeight}) } diff --git a/ui/src/data_explorer/components/VisView.tsx b/ui/src/data_explorer/components/VisView.tsx index 3de5fc0ac6..aa645651bb 100644 --- a/ui/src/data_explorer/components/VisView.tsx +++ b/ui/src/data_explorer/components/VisView.tsx @@ -4,7 +4,7 @@ import Table from './Table' import RefreshingGraph from 'src/shared/components/RefreshingGraph' import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes' -import {Source, Query, Template} from 'src/types' +import {Source, Query, Template, CellType} from 'src/types' interface Props { view: string @@ -39,18 +39,18 @@ const DataExplorerVisView: SFC = ({ return ( ) } return ( { } public componentDidMount() { - const {source} = this.props + const {source, autoRefresh} = this.props const {query} = qs.parse(location.search, {ignoreQueryPrefix: true}) + + AutoRefresh.poll(autoRefresh) + if (query && query.length) { const qc = this.props.queryConfigs[0] this.props.queryConfigActions.editRawTextAsync( @@ -75,6 +79,13 @@ export class DataExplorer extends PureComponent { } } + public componentDidUpdate(prevProps: Props) { + const {autoRefresh} = this.props + if (autoRefresh !== prevProps.autoRefresh) { + AutoRefresh.poll(autoRefresh) + } + } + public componentWillReceiveProps(nextProps: Props) { const {router} = this.props const {queryConfigs, timeRange} = nextProps @@ -89,6 +100,10 @@ export class DataExplorer extends PureComponent { } } + public componentWillUnmount() { + AutoRefresh.stopPolling() + } + public render() { const { source, diff --git a/ui/src/kapacitor/components/RuleDetailsText.js b/ui/src/kapacitor/components/RuleDetailsText.tsx similarity index 64% rename from ui/src/kapacitor/components/RuleDetailsText.js rename to ui/src/kapacitor/components/RuleDetailsText.tsx index 38cab56095..9581fdaeb8 100644 --- a/ui/src/kapacitor/components/RuleDetailsText.js +++ b/ui/src/kapacitor/components/RuleDetailsText.tsx @@ -1,19 +1,16 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' import {ErrorHandling} from 'src/shared/decorators/errors' +import {AlertRule} from 'src/types' + +interface Props { + rule: AlertRule + updateDetails: (id: string, value: string) => void +} + @ErrorHandling -class RuleDetailsText extends Component { - constructor(props) { - super(props) - } - - handleUpdateDetails = e => { - const {rule, updateDetails} = this.props - updateDetails(rule.id, e.target.value) - } - - render() { +class RuleDetailsText extends PureComponent { + public render() { const {rule} = this.props return (
@@ -27,13 +24,10 @@ class RuleDetailsText extends Component {
) } + + private handleUpdateDetails = e => { + const {rule, updateDetails} = this.props + updateDetails(rule.id, e.target.value) + } } - -const {shape, func} = PropTypes - -RuleDetailsText.propTypes = { - rule: shape().isRequired, - updateDetails: func.isRequired, -} - export default RuleDetailsText diff --git a/ui/src/kapacitor/components/RuleGraph.js b/ui/src/kapacitor/components/RuleGraph.js deleted file mode 100644 index 7dbe4f8996..0000000000 --- a/ui/src/kapacitor/components/RuleGraph.js +++ /dev/null @@ -1,71 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import buildInfluxQLQuery from 'utils/influxql' -import AutoRefresh from 'shared/components/AutoRefresh' -import LineGraph from 'shared/components/LineGraph' -import TimeRangeDropdown from 'shared/components/TimeRangeDropdown' -import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay' - -const RefreshingLineGraph = AutoRefresh(LineGraph) - -import {LINE_COLORS_RULE_GRAPH} from 'src/shared/constants/graphColorPalettes' - -const {shape, string, func} = PropTypes -const RuleGraph = ({ - query, - source, - timeRange: {lower}, - timeRange, - rule, - onChooseTimeRange, -}) => { - const autoRefreshMs = 30000 - const queryText = buildInfluxQLQuery({lower}, query) - const queries = [{host: source.links.proxy, text: queryText}] - - if (!queryText) { - return ( -
-

- Select a Time-Series to preview on a graph -

-
- ) - } - - return ( -
-
-

Preview Data from

- -
- -
- ) -} - -RuleGraph.propTypes = { - source: shape({ - links: shape({ - proxy: string.isRequired, - }).isRequired, - }).isRequired, - query: shape({}).isRequired, - rule: shape({}).isRequired, - timeRange: shape({}).isRequired, - onChooseTimeRange: func.isRequired, -} - -export default RuleGraph diff --git a/ui/src/kapacitor/components/RuleGraph.tsx b/ui/src/kapacitor/components/RuleGraph.tsx new file mode 100644 index 0000000000..e9a718fc10 --- /dev/null +++ b/ui/src/kapacitor/components/RuleGraph.tsx @@ -0,0 +1,127 @@ +// Libraries +import React, {PureComponent, CSSProperties} from 'react' +import TimeSeries from 'src/shared/components/time_series/TimeSeries' + +// Components +import Dygraph from 'src/shared/components/Dygraph' +import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown' + +// Utils +import buildInfluxQLQuery from 'src/utils/influxql' +import buildQueries from 'src/utils/buildQueriesForGraphs' +import underlayCallback from 'src/kapacitor/helpers/ruleGraphUnderlay' +import {timeSeriesToDygraph} from 'src/utils/timeSeriesTransformers' + +// Constants +import {LINE_COLORS_RULE_GRAPH} from 'src/shared/constants/graphColorPalettes' + +// Types +import {Source, AlertRule, QueryConfig, Query, TimeRange} from 'src/types' + +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + source: Source + query: QueryConfig + rule: AlertRule + timeRange: TimeRange + onChooseTimeRange: (tR: TimeRange) => void +} + +@ErrorHandling +class RuleGraph extends PureComponent { + public render() { + const {source, onChooseTimeRange, timeRange, rule} = this.props + + if (!this.queryText) { + return ( +
+

+ Select a Time-Series to preview on a graph +

+
+ ) + } + + return ( +
+
+

Preview Data from

+ +
+
+ + {data => { + const {labels, timeSeries, dygraphSeries} = timeSeriesToDygraph( + data.timeSeries, + 'rule-builder' + ) + + return ( + + ) + }} + +
+
+ ) + } + + private get options() { + return { + rightGap: 0, + yRangePad: 10, + labelsKMB: true, + fillGraph: true, + axisLabelWidth: 60, + animatedZooms: true, + drawAxesAtZero: true, + axisLineColor: '#383846', + gridLineColor: '#383846', + connectSeparatedPoints: true, + } + } + + private get containerStyle(): CSSProperties { + return { + width: 'calc(100% - 32px)', + height: 'calc(100% - 16px)', + position: 'absolute', + top: '8px', + } + } + + private get style(): CSSProperties { + return {height: '100%'} + } + + private get queryText(): string { + const {timeRange, query} = this.props + const lower = timeRange.lower + return buildInfluxQLQuery({lower}, query) + } + + private get queries(): Query[] { + const {query, timeRange} = this.props + return buildQueries([query], timeRange) + } +} + +export default RuleGraph diff --git a/ui/src/kapacitor/constants/index.ts b/ui/src/kapacitor/constants/index.ts index 074a2779c1..ecc93e2f2c 100644 --- a/ui/src/kapacitor/constants/index.ts +++ b/ui/src/kapacitor/constants/index.ts @@ -98,6 +98,17 @@ export const OUTSIDE_RANGE: string = 'outside range' export const EQUAL_TO_OR_GREATER_THAN: string = 'equal to or greater' export const EQUAL_TO_OR_LESS_THAN: string = 'equal to or less than' +export enum ThresholdOperators { + EqualTo = 'equal to', + LessThan = 'less than', + GreaterThan = 'greater than', + NotEqualTo = 'not equal to', + InsideRange = 'inside range', + OutsideRange = 'outside range', + EqualToOrGreaterThan = 'equal to or greater', + EqualToOrLessThan = 'equal to or less than', +} + export const THRESHOLD_OPERATORS: string[] = [ GREATER_THAN, EQUAL_TO_OR_GREATER_THAN, diff --git a/ui/src/kapacitor/constants/tableSizing.js b/ui/src/kapacitor/constants/tableSizing.ts similarity index 100% rename from ui/src/kapacitor/constants/tableSizing.js rename to ui/src/kapacitor/constants/tableSizing.ts diff --git a/ui/src/kapacitor/helpers/ruleGraphUnderlay.js b/ui/src/kapacitor/helpers/ruleGraphUnderlay.ts similarity index 70% rename from ui/src/kapacitor/helpers/ruleGraphUnderlay.js rename to ui/src/kapacitor/helpers/ruleGraphUnderlay.ts index 8423c08b83..27d7ad5791 100644 --- a/ui/src/kapacitor/helpers/ruleGraphUnderlay.js +++ b/ui/src/kapacitor/helpers/ruleGraphUnderlay.ts @@ -1,33 +1,35 @@ -import { - EQUAL_TO, - LESS_THAN, - NOT_EQUAL_TO, - GREATER_THAN, - INSIDE_RANGE, - OUTSIDE_RANGE, - EQUAL_TO_OR_LESS_THAN, - EQUAL_TO_OR_GREATER_THAN, -} from 'src/kapacitor/constants' +import {ThresholdOperators} from 'src/kapacitor/constants' const HIGHLIGHT = 'rgba(78, 216, 160, 0.3)' const BACKGROUND = 'rgba(41, 41, 51, 1)' -const getFillColor = operator => { +const getFillColor = (operator: ThresholdOperators) => { const backgroundColor = BACKGROUND const highlightColor = HIGHLIGHT - if (operator === OUTSIDE_RANGE) { + if (operator === ThresholdOperators.OutsideRange) { return backgroundColor } - if (operator === NOT_EQUAL_TO) { + if (operator === ThresholdOperators.NotEqualTo) { return backgroundColor } return highlightColor } -const underlayCallback = rule => (canvas, area, dygraph) => { +interface Area { + x: number + y: number + w: number + h: number +} + +const underlayCallback = rule => ( + canvas: CanvasRenderingContext2D, + area: Area, + dygraph: Dygraph +) => { const {values} = rule const {operator, value} = values @@ -40,21 +42,21 @@ const underlayCallback = rule => (canvas, area, dygraph) => { let highlightEnd = 0 switch (operator) { - case `${EQUAL_TO_OR_GREATER_THAN}`: - case `${GREATER_THAN}`: { + case ThresholdOperators.EqualToOrGreaterThan: + case ThresholdOperators.GreaterThan: { highlightStart = value highlightEnd = dygraph.yAxisRange()[1] break } - case `${EQUAL_TO_OR_LESS_THAN}`: - case `${LESS_THAN}`: { + case ThresholdOperators.EqualToOrLessThan: + case ThresholdOperators.LessThan: { highlightStart = dygraph.yAxisRange()[0] highlightEnd = value break } - case `${EQUAL_TO}`: { + case ThresholdOperators.LessThan: { const width = theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0]) highlightStart = +value - width @@ -62,7 +64,7 @@ const underlayCallback = rule => (canvas, area, dygraph) => { break } - case `${NOT_EQUAL_TO}`: { + case ThresholdOperators.NotEqualTo: { const width = theOnePercent * (dygraph.yAxisRange()[1] - dygraph.yAxisRange()[0]) highlightStart = +value - width @@ -73,7 +75,7 @@ const underlayCallback = rule => (canvas, area, dygraph) => { break } - case `${OUTSIDE_RANGE}`: { + case ThresholdOperators.OutsideRange: { highlightStart = Math.min(+value, +values.rangeValue) highlightEnd = Math.max(+value, +values.rangeValue) @@ -82,7 +84,7 @@ const underlayCallback = rule => (canvas, area, dygraph) => { break } - case `${INSIDE_RANGE}`: { + case ThresholdOperators.InsideRange: { highlightStart = Math.min(+value, +values.rangeValue) highlightEnd = Math.max(+value, +values.rangeValue) break diff --git a/ui/src/shared/components/Dygraph.tsx b/ui/src/shared/components/Dygraph.tsx index 00bef02941..54dccd5f50 100644 --- a/ui/src/shared/components/Dygraph.tsx +++ b/ui/src/shared/components/Dygraph.tsx @@ -1,23 +1,28 @@ +// Libraries import React, {Component, CSSProperties, MouseEvent} from 'react' import {connect} from 'react-redux' import _ from 'lodash' import NanoDate from 'nano-date' import ReactResizeDetector from 'react-resize-detector' -import {getDeep} from 'src/utils/wrappers' +// Components import D from 'src/external/dygraph' import DygraphLegend from 'src/shared/components/DygraphLegend' import StaticLegend from 'src/shared/components/StaticLegend' import Annotations from 'src/shared/components/Annotations' import Crosshair from 'src/shared/components/Crosshair' +// Utils import getRange, {getStackedRange} from 'src/shared/parsing/getRangeForDygraph' +import {getDeep} from 'src/utils/wrappers' +import {numberValueFormatter} from 'src/utils/formatting' + +// Constants import { AXES_SCALE_OPTIONS, DEFAULT_AXIS, } from 'src/dashboards/constants/cellEditor' import {buildDefaultYLabel} from 'src/shared/presenters' -import {numberValueFormatter} from 'src/utils/formatting' import {NULL_HOVER_TIME} from 'src/shared/constants/tableGraph' import { OPTIONS, @@ -26,14 +31,16 @@ import { CHAR_PIXELS, barPlotter, } from 'src/shared/graphs/helpers' -import {ErrorHandling} from 'src/shared/decorators/errors' - import {getLineColorsHexes} from 'src/shared/constants/graphColorPalettes' const {LOG, BASE_10, BASE_2} = AXES_SCALE_OPTIONS +import {ErrorHandling} from 'src/shared/decorators/errors' + +// Types import { Axes, Query, + CellType, RuleValues, TimeRange, DygraphData, @@ -46,6 +53,7 @@ import {LineColor} from 'src/types/colors' const Dygraphs = D as Constructable interface Props { + type: CellType cellID: string queries: Query[] timeSeries: DygraphData @@ -59,11 +67,11 @@ interface Props { ruleValues?: RuleValues axes?: Axes isGraphFilled?: boolean - isBarGraph?: boolean staticLegend?: boolean setResolution?: (w: number) => void onZoom?: (timeRange: TimeRange) => void mode?: string + underlayCallback?: () => void } interface State { @@ -95,6 +103,7 @@ class Dygraph extends Component { staticLegend: false, setResolution: () => {}, handleSetHoverTime: () => {}, + underlayCallback: () => {}, } private graphRef: React.RefObject @@ -113,11 +122,12 @@ class Dygraph extends Component { public componentDidMount() { const { - axes: {y, y2}, - isGraphFilled: fillGraph, - isBarGraph, + type, options, labels, + axes: {y, y2}, + isGraphFilled: fillGraph, + underlayCallback, } = this.props const timeSeries = this.timeSeries @@ -150,10 +160,11 @@ class Dygraph extends Component { zoomCallback: (lower: number, upper: number) => this.handleZoom(lower, upper), drawCallback: () => this.handleDraw(), + underlayCallback, highlightCircleSize: 3, } - if (isBarGraph) { + if (type === CellType.Bar) { defaultOptions = { ...defaultOptions, plotter: barPlotter, @@ -183,7 +194,8 @@ class Dygraph extends Component { labels, axes: {y, y2}, options, - isBarGraph, + type, + underlayCallback, } = this.props const dygraph = this.dygraph @@ -229,7 +241,8 @@ class Dygraph extends Component { }, colors: LINE_COLORS, series: this.colorDygraphSeries, - plotter: isBarGraph ? barPlotter : null, + plotter: type === CellType.Bar ? barPlotter : null, + underlayCallback, } dygraph.updateOptions(updateOptions) diff --git a/ui/src/shared/components/GaugeChart.tsx b/ui/src/shared/components/GaugeChart.tsx index 663feecc7e..9470232a3c 100644 --- a/ui/src/shared/components/GaugeChart.tsx +++ b/ui/src/shared/components/GaugeChart.tsx @@ -1,7 +1,7 @@ import React, {PureComponent} from 'react' import _ from 'lodash' -import getLastValues, {TimeSeriesResponse} from 'src/shared/parsing/lastValues' +import getLastValues from 'src/shared/parsing/lastValues' import Gauge from 'src/shared/components/Gauge' import {DEFAULT_GAUGE_COLORS} from 'src/shared/constants/thresholds' @@ -9,6 +9,7 @@ import {stringifyColorValues} from 'src/shared/constants/colorOperations' import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants' import {ErrorHandling} from 'src/shared/decorators/errors' import {DecimalPlaces} from 'src/types/dashboards' +import {TimeSeriesServerResponse} from 'src/types/series' interface Color { type: string @@ -19,9 +20,8 @@ interface Color { } interface Props { - data: TimeSeriesResponse[] + data: TimeSeriesServerResponse[] decimalPlaces: DecimalPlaces - isFetchingInitially: boolean cellID: string cellHeight?: number colors?: Color[] @@ -37,15 +37,7 @@ class GaugeChart extends PureComponent { } public render() { - const {isFetchingInitially, colors, prefix, suffix} = this.props - - if (isFetchingInitially) { - return ( -
-

-

- ) - } + const {colors, prefix, suffix} = this.props return (
diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js deleted file mode 100644 index 6bccd5a5b6..0000000000 --- a/ui/src/shared/components/Layout.js +++ /dev/null @@ -1,203 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import WidgetCell from 'shared/components/WidgetCell' -import LayoutCell from 'shared/components/LayoutCell' -import RefreshingGraph from 'shared/components/RefreshingGraph' -import {buildQueriesForLayouts} from 'utils/buildQueriesForLayouts' -import {IS_STATIC_LEGEND, defaultIntervalValue} from 'src/shared/constants' - -import _ from 'lodash' - -import {colorsStringSchema} from 'shared/schemas' -import {ErrorHandling} from 'src/shared/decorators/errors' - -const getSource = (cell, source, sources, defaultSource) => { - const s = _.get(cell, ['queries', '0', 'source'], null) - if (!s) { - return source - } - - return sources.find(src => src.links.self === s) || defaultSource -} - -@ErrorHandling -class LayoutState extends Component { - state = { - cellData: [], - resolution: defaultIntervalValue, - } - - grabDataForDownload = cellData => { - this.setState({cellData}) - } - - render() { - return ( - - ) - } - - handleSetResolution = resolution => { - this.setState({resolution}) - } -} - -const Layout = ( - { - host, - cell, - cell: { - h: cellHeight, - axes, - type, - colors, - legend, - timeFormat, - fieldOptions, - tableOptions, - decimalPlaces, - }, - source, - sources, - onZoom, - cellData, - resolution, - templates, - timeRange, - isEditable, - isDragging, - onEditCell, - onCloneCell, - autoRefresh, - manualRefresh, - onDeleteCell, - onSetResolution, - onStopAddAnnotation, - onSummonOverlayTechnologies, - grabDataForDownload, - }, - {source: defaultSource} -) => ( - - {cell.isWidget ? ( - - ) : ( - - )} - -) - -const {arrayOf, bool, func, number, shape, string} = PropTypes - -const propTypes = { - isDragging: bool, - autoRefresh: number.isRequired, - manualRefresh: number, - timeRange: shape({ - lower: string.isRequired, - }), - cell: shape({ - // isWidget cells will not have queries - isWidget: bool, - queries: arrayOf( - shape({ - label: string, - text: string, - query: string, - }).isRequired - ), - x: number.isRequired, - y: number.isRequired, - w: number.isRequired, - h: number.isRequired, - i: string.isRequired, - name: string.isRequired, - type: string.isRequired, - colors: colorsStringSchema, - tableOptions: shape({ - verticalTimeAxis: bool.isRequired, - sortBy: shape({ - internalName: string.isRequired, - displayName: string.isRequired, - visible: bool.isRequired, - }).isRequired, - wrapping: string.isRequired, - fixFirstColumn: bool.isRequired, - }), - timeFormat: string, - decimalPlaces: shape({ - isEnforced: bool.isRequired, - digits: number.isRequired, - }), - fieldOptions: arrayOf( - shape({ - internalName: string.isRequired, - displayName: string.isRequired, - visible: bool.isRequired, - }).isRequired - ), - }).isRequired, - templates: arrayOf(shape()), - host: string, - source: shape({ - links: shape({ - proxy: string.isRequired, - }).isRequired, - }).isRequired, - onPositionChange: func, - onEditCell: func, - onDeleteCell: func, - onCloneCell: func, - onSummonOverlayTechnologies: func, - isStatusPage: bool, - isEditable: bool, - onZoom: func, - sources: arrayOf(shape()), -} - -LayoutState.propTypes = {...propTypes} -Layout.propTypes = { - ...propTypes, - grabDataForDownload: func, - cellData: arrayOf(shape({})), -} - -export default LayoutState diff --git a/ui/src/shared/components/Layout.tsx b/ui/src/shared/components/Layout.tsx new file mode 100644 index 0000000000..7b86795ff9 --- /dev/null +++ b/ui/src/shared/components/Layout.tsx @@ -0,0 +1,112 @@ +// Libraries +import React, {Component} from 'react' +import _ from 'lodash' + +// Components +import WidgetCell from 'src/shared/components/WidgetCell' +import LayoutCell from 'src/shared/components/LayoutCell' +import RefreshingGraph from 'src/shared/components/RefreshingGraph' + +// Utils +import {buildQueriesForLayouts} from 'src/utils/buildQueriesForLayouts' + +// Constants +import {IS_STATIC_LEGEND} from 'src/shared/constants' + +// Types +import {TimeRange, Cell, Template, Source} from 'src/types' + +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + cell: Cell + timeRange: TimeRange + templates: Template[] + source: Source + sources: Source[] + host: string + autoRefresh: number + isEditable: boolean + manualRefresh: number + onZoom: () => void + onDeleteCell: () => void + onCloneCell: () => void + onSummonOverlayTechnologies: () => void +} + +@ErrorHandling +class Layout extends Component { + public state = { + cellData: [], + } + + public render() { + const { + cell, + host, + source, + sources, + onZoom, + timeRange, + autoRefresh, + manualRefresh, + templates, + isEditable, + onCloneCell, + onDeleteCell, + onSummonOverlayTechnologies, + } = this.props + const {cellData} = this.state + + return ( + + {cell.isWidget ? ( + + ) : ( + + )} + + ) + } + + private grabDataForDownload = cellData => { + this.setState({cellData}) + } + + private getSource = (cell, source, sources, defaultSource) => { + const s = _.get(cell, ['queries', '0', 'source'], null) + + if (!s) { + return source + } + + return sources.find(src => src.links.self === s) || defaultSource + } +} + +export default Layout diff --git a/ui/src/shared/components/LayoutCell.tsx b/ui/src/shared/components/LayoutCell.tsx index 92bd70848d..ed776662fd 100644 --- a/ui/src/shared/components/LayoutCell.tsx +++ b/ui/src/shared/components/LayoutCell.tsx @@ -24,7 +24,6 @@ interface Props { isEditable: boolean cellData: TimeSeriesServerResponse[] templates: Template[] - resolution: number } @ErrorHandling diff --git a/ui/src/shared/components/LayoutCellHeader.js b/ui/src/shared/components/LayoutCellHeader.tsx similarity index 69% rename from ui/src/shared/components/LayoutCellHeader.js rename to ui/src/shared/components/LayoutCellHeader.tsx index 668cc19f0c..4192d982f9 100644 --- a/ui/src/shared/components/LayoutCellHeader.js +++ b/ui/src/shared/components/LayoutCellHeader.tsx @@ -1,8 +1,12 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, {SFC} from 'react' import {isCellUntitled} from 'src/dashboards/utils/cellGetters' -const LayoutCellHeader = ({isEditable, cellName}) => { +interface Props { + isEditable: boolean + cellName: string +} + +const LayoutCellHeader: SFC = ({isEditable, cellName}) => { const headingClass = `dash-graph--heading ${ isEditable ? 'dash-graph--draggable dash-graph--heading-draggable' : '' }` @@ -22,11 +26,4 @@ const LayoutCellHeader = ({isEditable, cellName}) => { ) } -const {bool, string} = PropTypes - -LayoutCellHeader.propTypes = { - isEditable: bool, - cellName: string, -} - export default LayoutCellHeader diff --git a/ui/src/shared/components/LayoutRenderer.js b/ui/src/shared/components/LayoutRenderer.tsx similarity index 64% rename from ui/src/shared/components/LayoutRenderer.js rename to ui/src/shared/components/LayoutRenderer.tsx index a3ce8f81e0..f52731d54b 100644 --- a/ui/src/shared/components/LayoutRenderer.js +++ b/ui/src/shared/components/LayoutRenderer.tsx @@ -1,12 +1,16 @@ +// Libraries import React, {Component} from 'react' -import PropTypes from 'prop-types' import ReactGridLayout, {WidthProvider} from 'react-grid-layout' + +// Components +import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' +import Layout from 'src/shared/components/Layout' +const GridLayout = WidthProvider(ReactGridLayout) + +// Utils import {fastMap} from 'src/utils/fast' -import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized' - -import Layout from 'src/shared/components/Layout' - +// Constants import { // TODO: get these const values dynamically STATUS_PAGE_ROW_COUNT, @@ -14,13 +18,37 @@ import { PAGE_CONTAINER_MARGIN, LAYOUT_MARGIN, DASHBOARD_LAYOUT_ROW_HEIGHT, -} from 'shared/constants' +} from 'src/shared/constants' + +// Types +import {TimeRange, Cell, Template, Source} from 'src/types' + import {ErrorHandling} from 'src/shared/decorators/errors' -const GridLayout = WidthProvider(ReactGridLayout) +interface Props { + source: Source + cells: Cell[] + timeRange: TimeRange + templates: Template[] + sources: Source[] + host: string + autoRefresh: number + manualRefresh: number + isStatusPage: boolean + isEditable: boolean + onZoom?: () => void + onCloneCell?: () => void + onDeleteCell?: () => void + onSummonOverlayTechnologies?: () => void + onPositionChange?: (cells: Cell[]) => void +} + +interface State { + rowHeight: number +} @ErrorHandling -class LayoutRenderer extends Component { +class LayoutRenderer extends Component { constructor(props) { super(props) @@ -29,7 +57,80 @@ class LayoutRenderer extends Component { } } - handleLayoutChange = layout => { + public render() { + const { + host, + cells, + source, + sources, + onZoom, + templates, + timeRange, + isEditable, + autoRefresh, + manualRefresh, + onDeleteCell, + onCloneCell, + onSummonOverlayTechnologies, + } = this.props + + const {rowHeight} = this.state + const isDashboard = !!this.props.onPositionChange + + return ( + + + {fastMap(cells, cell => ( +
+ + + +
+ ))} +
+
+ ) + } + + private handleLayoutChange = layout => { if (!this.props.onPositionChange) { return } @@ -67,7 +168,7 @@ class LayoutRenderer extends Component { } // ensures that Status Page height fits the window - calculateRowHeight = () => { + private calculateRowHeight = () => { const {isStatusPage} = this.props return isStatusPage @@ -79,147 +180,6 @@ class LayoutRenderer extends Component { STATUS_PAGE_ROW_COUNT : DASHBOARD_LAYOUT_ROW_HEIGHT } - - render() { - const { - host, - cells, - source, - sources, - onZoom, - templates, - timeRange, - isEditable, - onEditCell, - autoRefresh, - manualRefresh, - onDeleteCell, - onCloneCell, - onSummonOverlayTechnologies, - } = this.props - - const {rowHeight} = this.state - const isDashboard = !!this.props.onPositionChange - - return ( - - - {fastMap(cells, cell => ( -
- - - -
- ))} -
-
- ) - } -} - -const {arrayOf, bool, func, number, shape, string} = PropTypes - -LayoutRenderer.propTypes = { - autoRefresh: number.isRequired, - manualRefresh: number, - timeRange: shape({ - lower: string.isRequired, - }), - cells: arrayOf( - shape({ - // isWidget cells will not have queries - isWidget: bool, - queries: arrayOf( - shape({ - label: string, - text: string, - query: string, - }).isRequired - ), - x: number.isRequired, - y: number.isRequired, - w: number.isRequired, - h: number.isRequired, - i: string.isRequired, - name: string.isRequired, - type: string.isRequired, - timeFormat: string, - tableOptions: shape({ - verticalTimeAxis: bool.isRequired, - sortBy: shape({ - internalName: string.isRequired, - displayName: string.isRequired, - visible: bool.isRequired, - }).isRequired, - wrapping: string.isRequired, - fixFirstColumn: bool.isRequired, - }), - fieldOptions: arrayOf( - shape({ - internalName: string.isRequired, - displayName: string.isRequired, - visible: bool.isRequired, - }).isRequired - ), - }).isRequired - ), - templates: arrayOf(shape()), - host: string, - source: shape({ - links: shape({ - proxy: string.isRequired, - }).isRequired, - }).isRequired, - onPositionChange: func, - onEditCell: func, - onDeleteCell: func, - onCloneCell: func, - onSummonOverlayTechnologies: func, - isStatusPage: bool, - isEditable: bool, - onZoom: func, - sources: arrayOf(shape({})), } export default LayoutRenderer diff --git a/ui/src/shared/components/LineGraph.tsx b/ui/src/shared/components/LineGraph.tsx index 3028435476..71fd1e6dd0 100644 --- a/ui/src/shared/components/LineGraph.tsx +++ b/ui/src/shared/components/LineGraph.tsx @@ -1,65 +1,48 @@ +// Libraries import React, {PureComponent, CSSProperties} from 'react' import Dygraph from 'src/shared/components/Dygraph' +import {withRouter, RouteComponentProps} from 'react-router' import _ from 'lodash' +// Components import SingleStat from 'src/shared/components/SingleStat' +import {ErrorHandlingWith} from 'src/shared/decorators/errors' +import InvalidData from 'src/shared/components/InvalidData' + +// Utils import { timeSeriesToDygraph, TimeSeriesToDyGraphReturnType, } from 'src/utils/timeSeriesTransformers' -import {ErrorHandlingWith} from 'src/shared/decorators/errors' -import InvalidData from 'src/shared/components/InvalidData' -import {Query, Axes, RuleValues, TimeRange} from 'src/types' -import {DecimalPlaces} from 'src/types/dashboards' +// Types import {ColorString} from 'src/types/colors' -import {Data} from 'src/types/dygraphs' - -const validateTimeSeries = ts => { - return _.every(ts, r => - _.every( - r, - (v, i: number) => - (i === 0 && Date.parse(v)) || _.isNumber(v) || _.isNull(v) - ) - ) -} +import {DecimalPlaces} from 'src/types/dashboards' +import {TimeSeriesServerResponse} from 'src/types/series' +import {Query, Axes, TimeRange, RemoteDataState, CellType} from 'src/types' interface Props { axes: Axes - title: string + type: CellType + queries: Query[] + timeRange: TimeRange + colors: ColorString[] + loading: RemoteDataState + decimalPlaces: DecimalPlaces + data: TimeSeriesServerResponse[] cellID: string cellHeight: number - isFetchingInitially: boolean - isRefreshing: boolean - isGraphFilled: boolean - isBarGraph: boolean staticLegend: boolean - showSingleStat: boolean - displayOptions: { - stepPlot: boolean - stackedGraph: boolean - animatedZooms: boolean - } - activeQueryIndex: number - ruleValues: RuleValues - timeRange: TimeRange - isInDataExplorer: boolean onZoom: () => void - data: Data - queries: Query[] - colors: ColorString[] - decimalPlaces: DecimalPlaces - underlayCallback?: () => void - setResolution: () => void handleSetHoverTime: () => void + activeQueryIndex?: number } +type LineGraphProps = Props & RouteComponentProps + @ErrorHandlingWith(InvalidData) -class LineGraph extends PureComponent { - public static defaultProps: Partial = { - underlayCallback: () => {}, - isGraphFilled: true, +class LineGraph extends PureComponent { + public static defaultProps: Partial = { staticLegend: false, } @@ -67,15 +50,16 @@ class LineGraph extends PureComponent { private timeSeries: TimeSeriesToDyGraphReturnType public componentWillMount() { - const {data, isInDataExplorer} = this.props - this.parseTimeSeries(data, isInDataExplorer) + const {data} = this.props + this.parseTimeSeries(data) } - public parseTimeSeries(data, isInDataExplorer) { - this.timeSeries = timeSeriesToDygraph(data, isInDataExplorer) - this.isValidData = validateTimeSeries( - _.get(this.timeSeries, 'timeSeries', []) - ) + public parseTimeSeries(data) { + const {location} = this.props + + this.timeSeries = timeSeriesToDygraph(data, location.pathname) + const timeSeries = _.get(this.timeSeries, 'timeSeries', []) + this.isValidData = this.validateTimeSeries(timeSeries) } public componentWillUpdate(nextProps) { @@ -84,7 +68,7 @@ class LineGraph extends PureComponent { data !== nextProps.data || activeQueryIndex !== nextProps.activeQueryIndex ) { - this.parseTimeSeries(nextProps.data, nextProps.isInDataExplorer) + this.parseTimeSeries(nextProps.data) } } @@ -96,54 +80,41 @@ class LineGraph extends PureComponent { const { data, axes, - title, + type, colors, cellID, onZoom, + loading, queries, timeRange, cellHeight, - ruleValues, - isBarGraph, - isRefreshing, - setResolution, - isGraphFilled, - showSingleStat, - displayOptions, staticLegend, decimalPlaces, - underlayCallback, - isFetchingInitially, handleSetHoverTime, } = this.props const {labels, timeSeries, dygraphSeries} = this.timeSeries - // If data for this graph is being fetched for the first time, show a graph-wide spinner. - if (isFetchingInitially) { - return - } - const options = { - ...displayOptions, - title, - labels, rightGap: 0, yRangePad: 10, labelsKMB: true, fillGraph: true, - underlayCallback, axisLabelWidth: 60, + animatedZooms: true, drawAxesAtZero: true, axisLineColor: '#383846', gridLineColor: '#383846', connectSeparatedPoints: true, + stepPlot: type === 'line-stepplot', + stackedGraph: type === 'line-stacked', } return (
- {isRefreshing && } + {loading === RemoteDataState.Loading && } { queries={queries} options={options} timeRange={timeRange} - isBarGraph={isBarGraph} timeSeries={timeSeries} - ruleValues={ruleValues} staticLegend={staticLegend} dygraphSeries={dygraphSeries} - setResolution={setResolution} + isGraphFilled={this.isGraphFilled} containerStyle={this.containerStyle} handleSetHoverTime={handleSetHoverTime} - isGraphFilled={showSingleStat ? false : isGraphFilled} > - {showSingleStat && ( + {type === CellType.LinePlusSingleStat && ( { suffix={this.suffix} cellHeight={cellHeight} decimalPlaces={decimalPlaces} - isFetchingInitially={isFetchingInitially} /> )} @@ -179,6 +146,26 @@ class LineGraph extends PureComponent { ) } + private validateTimeSeries = ts => { + return _.every(ts, r => + _.every( + r, + (v, i: number) => + (i === 0 && Date.parse(v)) || _.isNumber(v) || _.isNull(v) + ) + ) + } + + private get isGraphFilled(): boolean { + const {type} = this.props + + if (type === CellType.LinePlusSingleStat) { + return false + } + + return true + } + private get style(): CSSProperties { return {height: '100%'} } @@ -221,10 +208,4 @@ const GraphLoadingDots = () => (
) -const GraphSpinner = () => ( -
-
-
-) - -export default LineGraph +export default withRouter(LineGraph) diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js deleted file mode 100644 index 1ff4a61537..0000000000 --- a/ui/src/shared/components/RefreshingGraph.js +++ /dev/null @@ -1,231 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import {connect} from 'react-redux' - -import {emptyGraphCopy} from 'src/shared/copy/cell' -import {bindActionCreators} from 'redux' - -import AutoRefresh from 'shared/components/AutoRefresh' -import LineGraph from 'shared/components/LineGraph' -import SingleStat from 'shared/components/SingleStat' -import GaugeChart from 'shared/components/GaugeChart' -import TableGraph from 'shared/components/TableGraph' - -import {colorsStringSchema} from 'shared/schemas' -import {setHoverTime} from 'src/dashboards/actions' -import { - DEFAULT_TIME_FORMAT, - DEFAULT_DECIMAL_PLACES, -} from 'src/dashboards/constants' - -const RefreshingLineGraph = AutoRefresh(LineGraph) -const RefreshingSingleStat = AutoRefresh(SingleStat) -const RefreshingGaugeChart = AutoRefresh(GaugeChart) -const RefreshingTableGraph = AutoRefresh(TableGraph) - -const RefreshingGraph = ({ - axes, - inView, - type, - colors, - onZoom, - cellID, - queries, - source, - templates, - timeRange, - cellHeight, - autoRefresh, - fieldOptions, - timeFormat, - tableOptions, - decimalPlaces, - onSetResolution, - resizerTopHeight, - staticLegend, - manualRefresh, // when changed, re-mounts the component - editQueryStatus, - handleSetHoverTime, - grabDataForDownload, - isInCEO, -}) => { - const prefix = (axes && axes.y.prefix) || '' - const suffix = (axes && axes.y.suffix) || '' - if (!queries.length) { - return ( -
-

{emptyGraphCopy}

-
- ) - } - - if (type === 'single-stat') { - return ( - - ) - } - - if (type === 'gauge') { - return ( - - ) - } - - if (type === 'table') { - return ( - - ) - } - - const displayOptions = { - stepPlot: type === 'line-stepplot', - stackedGraph: type === 'line-stacked', - } - - return ( - - ) -} - -const {arrayOf, bool, func, number, shape, string} = PropTypes - -RefreshingGraph.propTypes = { - timeRange: shape({ - lower: string.isRequired, - }), - autoRefresh: number.isRequired, - manualRefresh: number, - templates: arrayOf(shape()), - type: string.isRequired, - cellHeight: number, - resizerTopHeight: number, - axes: shape(), - queries: arrayOf(shape()).isRequired, - editQueryStatus: func, - staticLegend: bool, - onZoom: func, - grabDataForDownload: func, - colors: colorsStringSchema, - cellID: string, - inView: bool, - tableOptions: shape({ - verticalTimeAxis: bool.isRequired, - sortBy: shape({ - internalName: string.isRequired, - displayName: string.isRequired, - visible: bool.isRequired, - }).isRequired, - wrapping: string.isRequired, - fixFirstColumn: bool.isRequired, - }), - fieldOptions: arrayOf( - shape({ - internalName: string.isRequired, - displayName: string.isRequired, - visible: bool.isRequired, - }).isRequired - ), - timeFormat: string.isRequired, - decimalPlaces: shape({ - isEnforced: bool.isRequired, - digits: number.isRequired, - }).isRequired, - handleSetHoverTime: func.isRequired, - isInCEO: bool, - onSetResolution: func, - source: shape().isRequired, -} - -RefreshingGraph.defaultProps = { - manualRefresh: 0, - staticLegend: false, - inView: true, - timeFormat: DEFAULT_TIME_FORMAT, - decimalPlaces: DEFAULT_DECIMAL_PLACES, -} - -const mapStateToProps = ({annotations: {mode}}) => ({ - mode, -}) - -const mapDispatchToProps = dispatch => ({ - handleSetHoverTime: bindActionCreators(setHoverTime, dispatch), -}) - -export default connect(mapStateToProps, mapDispatchToProps)(RefreshingGraph) diff --git a/ui/src/shared/components/RefreshingGraph.tsx b/ui/src/shared/components/RefreshingGraph.tsx new file mode 100644 index 0000000000..8b690dd4a3 --- /dev/null +++ b/ui/src/shared/components/RefreshingGraph.tsx @@ -0,0 +1,238 @@ +// Libraries +import React, {PureComponent} from 'react' +import {connect} from 'react-redux' +import _ from 'lodash' + +// Components +import LineGraph from 'src/shared/components/LineGraph' +import GaugeChart from 'src/shared/components/GaugeChart' +import TableGraph from 'src/shared/components/TableGraph' +import SingleStat from 'src/shared/components/SingleStat' +import TimeSeries from 'src/shared/components/time_series/TimeSeries' + +// Constants +import {emptyGraphCopy} from 'src/shared/copy/cell' +import { + DEFAULT_TIME_FORMAT, + DEFAULT_DECIMAL_PLACES, +} from 'src/dashboards/constants' + +// Actions +import {setHoverTime} from 'src/dashboards/actions' + +// Types +import {ColorString} from 'src/types/colors' +import {Source, Axes, TimeRange, Template, Query, CellType} from 'src/types' +import {TableOptions, FieldOption, DecimalPlaces} from 'src/types/dashboards' + +interface Props { + axes: Axes + source: Source + queries: Query[] + timeRange: TimeRange + colors: ColorString[] + templates: Template[] + tableOptions: TableOptions + fieldOptions: FieldOption[] + decimalPlaces: DecimalPlaces + type: CellType + cellID: string + inView: boolean + isInCEO: boolean + timeFormat: string + cellHeight: number + autoRefresh: number + staticLegend: boolean + manualRefresh: number + resizerTopHeight: number + onZoom: () => void + editQueryStatus: () => void + onSetResolution: () => void + grabDataForDownload: () => void + handleSetHoverTime: () => void +} + +class RefreshingGraph extends PureComponent { + public static defaultProps: Partial = { + inView: true, + manualRefresh: 0, + staticLegend: false, + timeFormat: DEFAULT_TIME_FORMAT, + decimalPlaces: DEFAULT_DECIMAL_PLACES, + } + + public render() { + const {inView, type, queries, source, templates} = this.props + + if (!queries.length) { + return ( +
+

{emptyGraphCopy}

+
+ ) + } + + return ( + + {({timeSeries, loading}) => { + switch (type) { + case CellType.SingleStat: + return this.singleStat(timeSeries) + case CellType.Table: + return this.table(timeSeries) + case CellType.Gauge: + return this.gauge(timeSeries) + default: + return this.lineGraph(timeSeries, loading) + } + }} + + ) + } + + private singleStat = (data): JSX.Element => { + const {colors, cellHeight, decimalPlaces, manualRefresh} = this.props + + return ( + + ) + } + + private table = (data): JSX.Element => { + const { + colors, + fieldOptions, + timeFormat, + tableOptions, + decimalPlaces, + manualRefresh, + handleSetHoverTime, + grabDataForDownload, + isInCEO, + } = this.props + + return ( + + ) + } + + private gauge = (data): JSX.Element => { + const { + colors, + cellID, + cellHeight, + decimalPlaces, + manualRefresh, + resizerTopHeight, + } = this.props + + return ( + + ) + } + + private lineGraph = (data, loading): JSX.Element => { + const { + axes, + type, + colors, + onZoom, + cellID, + queries, + timeRange, + cellHeight, + decimalPlaces, + staticLegend, + manualRefresh, + handleSetHoverTime, + } = this.props + + return ( + + ) + } + + private get queries(): Query[] { + const {queries, type} = this.props + if (type === CellType.SingleStat) { + return [queries[0]] + } + + if (type === CellType.Gauge) { + return [queries[0]] + } + + return queries + } + + private get prefix(): string { + const {axes} = this.props + + return _.get(axes, 'y.prefix', '') + } + + private get suffix(): string { + const {axes} = this.props + return _.get(axes, 'y.suffix', '') + } +} + +const mapStateToProps = ({annotations: {mode}}) => ({ + mode, +}) + +const mdtp = { + handleSetHoverTime: setHoverTime, +} + +export default connect(mapStateToProps, mdtp)(RefreshingGraph) diff --git a/ui/src/shared/components/SingleStat.tsx b/ui/src/shared/components/SingleStat.tsx index 78416998e6..68ea903238 100644 --- a/ui/src/shared/components/SingleStat.tsx +++ b/ui/src/shared/components/SingleStat.tsx @@ -8,11 +8,10 @@ import {DYGRAPH_CONTAINER_V_MARGIN} from 'src/shared/constants' import {generateThresholdsListHexs} from 'src/shared/constants/colorOperations' import {ColorString} from 'src/types/colors' import {CellType, DecimalPlaces} from 'src/types/dashboards' -import {Data} from 'src/types/dygraphs' +import {TimeSeriesServerResponse} from 'src/types/series' import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { - isFetchingInitially: boolean decimalPlaces: DecimalPlaces cellHeight: number colors: ColorString[] @@ -20,7 +19,7 @@ interface Props { suffix?: string lineGraph: boolean staticLegendHeight?: number - data: Data + data: TimeSeriesServerResponse[] } @ErrorHandling @@ -31,16 +30,6 @@ class SingleStat extends PureComponent { } public render() { - const {isFetchingInitially} = this.props - - if (isFetchingInitially) { - return ( -
-

-

- ) - } - return (
{this.resizerBox} diff --git a/ui/src/shared/components/time_series/TimeSeries.tsx b/ui/src/shared/components/time_series/TimeSeries.tsx new file mode 100644 index 0000000000..d690ad4709 --- /dev/null +++ b/ui/src/shared/components/time_series/TimeSeries.tsx @@ -0,0 +1,152 @@ +// Library +import React, {Component} from 'react' +import _ from 'lodash' + +// API +import {fetchTimeSeries} from 'src/shared/apis/query' + +// Types +import {Template, Source, Query, RemoteDataState} from 'src/types' +import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series' + +// Utils +import AutoRefresh from 'src/utils/AutoRefresh' + +export const DEFAULT_TIME_SERIES = [{response: {results: []}}] + +interface RenderProps { + timeSeries: TimeSeriesServerResponse[] + loading: RemoteDataState +} + +interface Props { + source: Source + queries: Query[] + children: (r: RenderProps) => JSX.Element + inView?: boolean + templates?: Template[] +} + +interface State { + loading: RemoteDataState + isFirstFetch: boolean + timeSeries: TimeSeriesServerResponse[] +} + +class TimeSeries extends Component { + public static defaultProps = { + inView: true, + templates: [], + } + + constructor(props: Props) { + super(props) + this.state = { + timeSeries: DEFAULT_TIME_SERIES, + loading: RemoteDataState.NotStarted, + isFirstFetch: false, + } + } + + public async componentDidMount() { + const isFirstFetch = true + this.executeQueries(isFirstFetch) + AutoRefresh.subscribe(this.executeQueries) + } + + public componentWillUnmount() { + AutoRefresh.unsubscribe(this.executeQueries) + } + + public async componentDidUpdate(prevProps: Props) { + if (!this.isPropsDifferent(prevProps)) { + return + } + + this.executeQueries() + } + + public executeQueries = async (isFirstFetch: boolean = false) => { + const {source, inView, queries, templates} = this.props + + if (!inView) { + return + } + + if (!queries.length) { + return this.setState({timeSeries: DEFAULT_TIME_SERIES}) + } + + this.setState({loading: RemoteDataState.Loading, isFirstFetch}) + + const TEMP_RES = 300 + + try { + const timeSeries = await fetchTimeSeries( + source, + queries, + TEMP_RES, + templates + ) + + const newSeries = timeSeries.map((response: TimeSeriesResponse) => ({ + response, + })) + + this.setState({ + timeSeries: newSeries, + loading: RemoteDataState.Done, + }) + } catch (err) { + console.error(err) + } + } + + public render() { + const {timeSeries, loading, isFirstFetch} = this.state + + const hasValues = _.some(timeSeries, s => { + const results = _.get(s, 'response.results', []) + const v = _.some(results, r => r.series) + return v + }) + + if (isFirstFetch && loading === RemoteDataState.Loading) { + return ( +
+

+

+ ) + } + + if (!hasValues) { + return ( +
+

No Results

+
+ ) + } + + return this.props.children({timeSeries, loading}) + } + + private isPropsDifferent(nextProps: Props) { + const isSourceDifferent = !_.isEqual(this.props.source, nextProps.source) + + return ( + this.props.inView !== nextProps.inView || + !!this.queryDifference(this.props.queries, nextProps.queries).length || + !_.isEqual(this.props.templates, nextProps.templates) || + isSourceDifferent + ) + } + + private queryDifference = (left, right) => { + const mapper = q => `${q.text}` + const l = left.map(mapper) + const r = right.map(mapper) + return _.difference(_.union(l, r), _.intersection(l, r)) + } +} + +export default TimeSeries diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.ts similarity index 100% rename from ui/src/shared/constants/index.tsx rename to ui/src/shared/constants/index.ts diff --git a/ui/src/shared/constants/queryFillOptions.js b/ui/src/shared/constants/queryFillOptions.ts similarity index 100% rename from ui/src/shared/constants/queryFillOptions.js rename to ui/src/shared/constants/queryFillOptions.ts diff --git a/ui/src/shared/constants/series.ts b/ui/src/shared/constants/series.ts index 3177b6d3e3..baa5b79cb8 100644 --- a/ui/src/shared/constants/series.ts +++ b/ui/src/shared/constants/series.ts @@ -1,3 +1,4 @@ +// TODO: Delete me! export const DEFAULT_TIME_SERIES = [ { response: { diff --git a/ui/src/shared/constants/timeRange.js b/ui/src/shared/constants/timeRange.ts similarity index 100% rename from ui/src/shared/constants/timeRange.js rename to ui/src/shared/constants/timeRange.ts diff --git a/ui/src/shared/constants/timeShift.js b/ui/src/shared/constants/timeShift.ts similarity index 100% rename from ui/src/shared/constants/timeShift.js rename to ui/src/shared/constants/timeShift.ts diff --git a/ui/src/shared/parsing/lastValues.ts b/ui/src/shared/parsing/lastValues.ts index a3b9eea94b..230c78c2b0 100644 --- a/ui/src/shared/parsing/lastValues.ts +++ b/ui/src/shared/parsing/lastValues.ts @@ -1,31 +1,14 @@ import _ from 'lodash' import {Data} from 'src/types/dygraphs' +import {TimeSeriesServerResponse} from 'src/types/series' interface Result { lastValues: number[] series: string[] } -type SeriesValue = number | string - -interface Series { - name: string - values: SeriesValue[][] | null - columns: string[] | null -} - -interface TimeSeriesResult { - series: Series[] -} - -export interface TimeSeriesResponse { - response: { - results: TimeSeriesResult[] - } -} - export default function( - timeSeriesResponse: TimeSeriesResponse[] | Data | null + timeSeriesResponse: TimeSeriesServerResponse[] | Data | null ): Result { const values = _.get( timeSeriesResponse, diff --git a/ui/src/status/containers/StatusPage.tsx b/ui/src/status/containers/StatusPage.tsx index 4f65ceb205..624e8c4ec9 100644 --- a/ui/src/status/containers/StatusPage.tsx +++ b/ui/src/status/containers/StatusPage.tsx @@ -1,18 +1,30 @@ +// Libraries import React, {Component} from 'react' +// Components import FancyScrollbar from 'src/shared/components/FancyScrollbar' import LayoutRenderer from 'src/shared/components/LayoutRenderer' -import {STATUS_PAGE_TIME_RANGE} from 'src/shared/data/timeRanges' -import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' import PageHeader from 'src/reusable_ui/components/page_layout/PageHeader' +// Constants +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' +import {STATUS_PAGE_TIME_RANGE} from 'src/shared/data/timeRanges' import {fixtureStatusPageCells} from 'src/status/fixtures' -import {ErrorHandling} from 'src/shared/decorators/errors' import { TEMP_VAR_DASHBOARD_TIME, TEMP_VAR_UPPER_DASHBOARD_TIME, } from 'src/shared/constants' -import {Source, Cell} from 'src/types' + +// Types +import { + Source, + Template, + Cell, + TemplateType, + TemplateValueType, +} from 'src/types' + +import {ErrorHandling} from 'src/shared/decorators/errors' interface State { cells: Cell[] @@ -39,36 +51,6 @@ class StatusPage extends Component { const {source} = this.props const {cells} = this.state - const dashboardTime = { - id: 'dashtime', - tempVar: TEMP_VAR_DASHBOARD_TIME, - type: 'constant', - values: [ - { - value: timeRange.lower, - type: 'constant', - selected: true, - localSelected: true, - }, - ], - } - - const upperDashboardTime = { - id: 'upperdashtime', - tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME, - type: 'constant', - values: [ - { - value: 'now()', - type: 'constant', - selected: true, - localSelected: true, - }, - ], - } - - const templates = [dashboardTime, upperDashboardTime] - return (
{
{cells.length ? ( ) : ( Loading Status Page... @@ -97,6 +81,40 @@ class StatusPage extends Component {
) } + + private get templates(): Template[] { + const dashboardTime = { + id: 'dashtime', + tempVar: TEMP_VAR_DASHBOARD_TIME, + type: TemplateType.Constant, + label: '', + values: [ + { + value: timeRange.lower, + type: TemplateValueType.Constant, + selected: true, + localSelected: true, + }, + ], + } + + const upperDashboardTime = { + id: 'upperdashtime', + tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME, + type: TemplateType.Constant, + label: '', + values: [ + { + value: 'now()', + type: TemplateValueType.Constant, + selected: true, + localSelected: true, + }, + ], + } + + return [dashboardTime, upperDashboardTime] + } } export default StatusPage diff --git a/ui/src/status/fixtures.ts b/ui/src/status/fixtures.ts index 116b07d2c2..c36030fe56 100644 --- a/ui/src/status/fixtures.ts +++ b/ui/src/status/fixtures.ts @@ -2,8 +2,7 @@ import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes' import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants' import {NEW_DEFAULT_DASHBOARD_CELL} from 'src/dashboards/constants/index' import {DEFAULT_AXIS} from 'src/dashboards/constants/cellEditor' -import {Cell, CellQuery, Axes} from 'src/types' -import {CellType} from 'src/types/dashboards' +import {Cell, CellQuery, Axes, CellType} from 'src/types' const emptyQuery: CellQuery = { query: '', diff --git a/ui/src/types/dashboards.ts b/ui/src/types/dashboards.ts index 619575f788..0552ffb44e 100644 --- a/ui/src/types/dashboards.ts +++ b/ui/src/types/dashboards.ts @@ -76,6 +76,7 @@ export interface Cell { links: CellLinks legend: Legend isWidget?: boolean + inView: boolean } export enum CellType { diff --git a/ui/src/utils/AutoRefresh.ts b/ui/src/utils/AutoRefresh.ts new file mode 100644 index 0000000000..a16e36138e --- /dev/null +++ b/ui/src/utils/AutoRefresh.ts @@ -0,0 +1,42 @@ +type func = (...args: any[]) => any + +class AutoRefresh { + public subscribers: func[] = [] + + private intervalID: NodeJS.Timer + + public subscribe(fn: func) { + this.subscribers = [...this.subscribers, fn] + } + + public unsubscribe(fn: func) { + this.subscribers = this.subscribers.filter(f => f !== fn) + } + + public poll(refreshMs: number) { + this.clearInterval() + + if (refreshMs) { + this.intervalID = setInterval(this.refresh, refreshMs) + } + } + + public stopPolling() { + this.clearInterval() + } + + private clearInterval() { + if (!this.intervalID) { + return + } + + clearInterval(this.intervalID) + this.intervalID = null + } + + private refresh = () => { + this.subscribers.forEach(fn => fn()) + } +} + +export default new AutoRefresh() diff --git a/ui/src/utils/timeSeriesTransformers.ts b/ui/src/utils/timeSeriesTransformers.ts index e0fab6a5a0..4e636b5305 100644 --- a/ui/src/utils/timeSeriesTransformers.ts +++ b/ui/src/utils/timeSeriesTransformers.ts @@ -27,9 +27,10 @@ interface TimeSeriesToTableGraphReturnType { export const timeSeriesToDygraph = ( raw: TimeSeriesServerResponse[], - isInDataExplorer: boolean + pathname: string = '' ): TimeSeriesToDyGraphReturnType => { const isTable = false + const isInDataExplorer = pathname.includes('data-explorer') const {sortedLabels, sortedTimeSeries} = groupByTimeSeriesTransform( raw, isTable diff --git a/ui/test/fixtures/index.ts b/ui/test/fixtures/index.ts index 94d80b1fa0..8e8df13021 100644 --- a/ui/test/fixtures/index.ts +++ b/ui/test/fixtures/index.ts @@ -185,6 +185,7 @@ export const cell: Cell = { self: '/chronograf/v1/dashboards/9/cells/67435af2-17bf-4caa-a5fc-0dd1ffb40dab', }, + inView: true, } export const fullTimeRange = { diff --git a/ui/test/resources.ts b/ui/test/resources.ts index 84edd01129..3627e8f946 100644 --- a/ui/test/resources.ts +++ b/ui/test/resources.ts @@ -698,4 +698,5 @@ export const cell: Cell = { '/chronograf/v1/dashboards/10/cells/8b3b7897-49b1-422c-9443-e9b778bcbf12', }, legend: {}, + inView: true, } diff --git a/ui/test/utils/timeSeriesTransformers.test.js b/ui/test/utils/timeSeriesTransformers.test.ts similarity index 89% rename from ui/test/utils/timeSeriesTransformers.test.js rename to ui/test/utils/timeSeriesTransformers.test.ts index ce953435ff..33ab77094e 100644 --- a/ui/test/utils/timeSeriesTransformers.test.js +++ b/ui/test/utils/timeSeriesTransformers.test.ts @@ -21,6 +21,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -30,6 +31,7 @@ describe('timeSeriesToDygraph', () => { ], }, { + statement_id: 0, series: [ { name: 'm1', @@ -71,6 +73,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -100,6 +103,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -109,6 +113,7 @@ describe('timeSeriesToDygraph', () => { ], }, { + statement_id: 0, series: [ { name: 'm1', @@ -124,6 +129,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm3', @@ -160,6 +166,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -175,6 +182,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -212,6 +220,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -227,6 +236,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'm1', @@ -240,8 +250,7 @@ describe('timeSeriesToDygraph', () => { }, ] - const isInDataExplorer = true - const actual = timeSeriesToDygraph(influxResponse, isInDataExplorer) + const actual = timeSeriesToDygraph(influxResponse, 'data-explorer') const expected = {} @@ -254,6 +263,7 @@ describe('timeSeriesToDygraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'mb', @@ -263,6 +273,7 @@ describe('timeSeriesToDygraph', () => { ], }, { + statement_id: 0, series: [ { name: 'ma', @@ -272,6 +283,7 @@ describe('timeSeriesToDygraph', () => { ], }, { + statement_id: 0, series: [ { name: 'mc', @@ -281,6 +293,7 @@ describe('timeSeriesToDygraph', () => { ], }, { + statement_id: 0, series: [ { name: 'mc', @@ -309,6 +322,7 @@ describe('timeSeriesToTableGraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'mb', @@ -318,6 +332,7 @@ describe('timeSeriesToTableGraph', () => { ], }, { + statement_id: 0, series: [ { name: 'ma', @@ -327,6 +342,7 @@ describe('timeSeriesToTableGraph', () => { ], }, { + statement_id: 0, series: [ { name: 'mc', @@ -336,6 +352,7 @@ describe('timeSeriesToTableGraph', () => { ], }, { + statement_id: 0, series: [ { name: 'mc', @@ -349,38 +366,7 @@ describe('timeSeriesToTableGraph', () => { }, ] - const qASTs = [ - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - ] - - const actual = timeSeriesToTableGraph(influxResponse, qASTs) + const actual = timeSeriesToTableGraph(influxResponse) const expected = [ ['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2'], [1000, 1, 1, null, null], @@ -397,6 +383,7 @@ describe('timeSeriesToTableGraph', () => { response: { results: [ { + statement_id: 0, series: [ { name: 'mb', @@ -406,6 +393,7 @@ describe('timeSeriesToTableGraph', () => { ], }, { + statement_id: 0, series: [ { name: 'ma', @@ -415,6 +403,7 @@ describe('timeSeriesToTableGraph', () => { ], }, { + statement_id: 0, series: [ { name: 'mc', @@ -424,6 +413,7 @@ describe('timeSeriesToTableGraph', () => { ], }, { + statement_id: 0, series: [ { name: 'mc', @@ -437,38 +427,7 @@ describe('timeSeriesToTableGraph', () => { }, ] - const qASTs = [ - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - { - groupBy: { - time: { - interval: '2s', - }, - }, - }, - ] - - const actual = timeSeriesToTableGraph(influxResponse, qASTs) + const actual = timeSeriesToTableGraph(influxResponse) const expected = ['time', 'ma.f1', 'mb.f1', 'mc.f1', 'mc.f2'] expect(actual.data[0]).toEqual(expected) @@ -476,10 +435,7 @@ describe('timeSeriesToTableGraph', () => { it('returns an array of an empty array if there is an empty response', () => { const influxResponse = [] - - const qASTs = [] - - const actual = timeSeriesToTableGraph(influxResponse, qASTs) + const actual = timeSeriesToTableGraph(influxResponse) const expected = [[]] expect(actual.data).toEqual(expected) @@ -535,7 +491,12 @@ describe('transformTableData', () => { [3000, 2000, 1000], ] const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION} - const tableOptions = {verticalTimeAxis: true} + const sortBy = {internalName: 'time', displayName: 'Time', visible: true} + const tableOptions = { + verticalTimeAxis: true, + sortBy, + fixFirstColumn: true, + } const timeFormat = DEFAULT_TIME_FORMAT const decimalPlaces = DEFAULT_DECIMAL_PLACES const fieldOptions = [ @@ -552,6 +513,7 @@ describe('transformTableData', () => { timeFormat, decimalPlaces ) + const expected = [ ['time', 'f1', 'f2'], [2000, 1000, 3000], @@ -570,7 +532,12 @@ describe('transformTableData', () => { [3000, 2000, 1000], ] const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION} - const tableOptions = {verticalTimeAxis: true} + const sortBy = {internalName: 'time', displayName: 'Time', visible: true} + const tableOptions = { + verticalTimeAxis: true, + sortBy, + fixFirstColumn: true, + } const timeFormat = DEFAULT_TIME_FORMAT const decimalPlaces = DEFAULT_DECIMAL_PLACES const fieldOptions = [ @@ -602,7 +569,12 @@ describe('transformTableData', () => { ] const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION} - const tableOptions = {verticalTimeAxis: true} + const sortBy = {internalName: 'time', displayName: 'Time', visible: true} + const tableOptions = { + verticalTimeAxis: true, + sortBy, + fixFirstColumn: true, + } const timeFormat = DEFAULT_TIME_FORMAT const decimalPlaces = DEFAULT_DECIMAL_PLACES const fieldOptions = [ @@ -636,7 +608,12 @@ describe('if verticalTimeAxis is false', () => { ] const sort = {field: 'time', direction: DEFAULT_SORT_DIRECTION} - const tableOptions = {verticalTimeAxis: false} + const sortBy = {internalName: 'time', displayName: 'Time', visible: true} + const tableOptions = { + sortBy, + fixFirstColumn: true, + verticalTimeAxis: false, + } const timeFormat = DEFAULT_TIME_FORMAT const decimalPlaces = DEFAULT_DECIMAL_PLACES const fieldOptions = [ @@ -672,7 +649,12 @@ describe('if verticalTimeAxis is false', () => { ] const sort = {field: 'f1', direction: DEFAULT_SORT_DIRECTION} - const tableOptions = {verticalTimeAxis: false} + const sortBy = {internalName: 'time', displayName: 'Time', visible: true} + const tableOptions = { + sortBy, + fixFirstColumn: true, + verticalTimeAxis: false, + } const timeFormat = DEFAULT_TIME_FORMAT const decimalPlaces = DEFAULT_DECIMAL_PLACES const fieldOptions = [