commit
8267df972a
|
@ -11,6 +11,7 @@
|
||||||
1. [#3215](https://github.com/influxdata/chronograf/pull/3215): Fix Template Variables Control Bar to top of dashboard page
|
1. [#3215](https://github.com/influxdata/chronograf/pull/3215): Fix Template Variables Control Bar to top of dashboard page
|
||||||
1. [#3214](https://github.com/influxdata/chronograf/pull/3214): Remove extra click when creating dashboard cell
|
1. [#3214](https://github.com/influxdata/chronograf/pull/3214): Remove extra click when creating dashboard cell
|
||||||
1. [#3256](https://github.com/influxdata/chronograf/pull/3256): Reduce font sizes in dashboards for increased space efficiency
|
1. [#3256](https://github.com/influxdata/chronograf/pull/3256): Reduce font sizes in dashboards for increased space efficiency
|
||||||
|
1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
|
||||||
|
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
|
||||||
|
|
||||||
|
import {intervalValuesPoints} from 'src/shared/constants'
|
||||||
|
|
||||||
|
interface TemplateQuery {
|
||||||
|
db: string
|
||||||
|
rp: string
|
||||||
|
influxql: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TemplateValue {
|
||||||
|
type: string
|
||||||
|
value: string
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Template {
|
||||||
|
type: string
|
||||||
|
tempVar: string
|
||||||
|
query: TemplateQuery
|
||||||
|
values: TemplateValue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
host: string | string[]
|
||||||
|
text: string
|
||||||
|
database: string
|
||||||
|
db: string
|
||||||
|
rp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fetchTimeSeries = async (
|
||||||
|
queries: Query[],
|
||||||
|
resolution: number,
|
||||||
|
templates: Template[],
|
||||||
|
editQueryStatus: () => void
|
||||||
|
) => {
|
||||||
|
const timeSeriesPromises = queries.map(query => {
|
||||||
|
const {host, database, rp} = query
|
||||||
|
// the key `database` was used upstream in HostPage.js, and since as of this writing
|
||||||
|
// the codebase has not been fully converted to TypeScript, it's not clear where else
|
||||||
|
// it may be used, but this slight modification is intended to allow for the use of
|
||||||
|
// `database` while moving over to `db` for consistency over time
|
||||||
|
const db = _.get(query, 'db', database)
|
||||||
|
|
||||||
|
const templatesWithIntervalVals = templates.map(temp => {
|
||||||
|
if (temp.tempVar === ':interval:') {
|
||||||
|
if (resolution) {
|
||||||
|
const values = temp.values.map(v => ({
|
||||||
|
...v,
|
||||||
|
value: `${_.toInteger(Number(resolution) / 3)}`,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {...temp, values}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...temp, values: intervalValuesPoints}
|
||||||
|
}
|
||||||
|
return temp
|
||||||
|
})
|
||||||
|
|
||||||
|
const tempVars = removeUnselectedTemplateValues(templatesWithIntervalVals)
|
||||||
|
|
||||||
|
const source = host
|
||||||
|
return fetchTimeSeriesAsync(
|
||||||
|
{source, db, rp, query, tempVars, resolution},
|
||||||
|
editQueryStatus
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(timeSeriesPromises)
|
||||||
|
}
|
|
@ -1,294 +0,0 @@
|
||||||
import React, {Component} from 'react'
|
|
||||||
import PropTypes from 'prop-types'
|
|
||||||
import _ from 'lodash'
|
|
||||||
|
|
||||||
import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries'
|
|
||||||
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
|
|
||||||
import {intervalValuesPoints} from 'src/shared/constants'
|
|
||||||
import {getQueryConfig} from 'shared/apis'
|
|
||||||
|
|
||||||
const AutoRefresh = ComposedComponent => {
|
|
||||||
class wrapper extends Component {
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
this.state = {
|
|
||||||
lastQuerySuccessful: true,
|
|
||||||
timeSeries: [],
|
|
||||||
resolution: null,
|
|
||||||
queryASTs: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const {queries, templates, autoRefresh, type} = this.props
|
|
||||||
this.executeQueries(queries, templates)
|
|
||||||
if (type === 'table') {
|
|
||||||
const queryASTs = await this.getQueryASTs(queries, templates)
|
|
||||||
this.setState({queryASTs})
|
|
||||||
}
|
|
||||||
if (autoRefresh) {
|
|
||||||
this.intervalID = setInterval(
|
|
||||||
() => this.executeQueries(queries, templates),
|
|
||||||
autoRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getQueryASTs = async (queries, templates) => {
|
|
||||||
return await Promise.all(
|
|
||||||
queries.map(async q => {
|
|
||||||
const host = _.isArray(q.host) ? q.host[0] : q.host
|
|
||||||
const url = host.replace('proxy', 'queries')
|
|
||||||
const text = q.text
|
|
||||||
const {data} = await getQueryConfig(url, [{query: text}], templates)
|
|
||||||
return data.queries[0].queryAST
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentWillReceiveProps(nextProps) {
|
|
||||||
const inViewDidUpdate = this.props.inView !== nextProps.inView
|
|
||||||
|
|
||||||
const queriesDidUpdate = this.queryDifference(
|
|
||||||
this.props.queries,
|
|
||||||
nextProps.queries
|
|
||||||
).length
|
|
||||||
|
|
||||||
const tempVarsDidUpdate = !_.isEqual(
|
|
||||||
this.props.templates,
|
|
||||||
nextProps.templates
|
|
||||||
)
|
|
||||||
|
|
||||||
const shouldRefetch =
|
|
||||||
queriesDidUpdate || tempVarsDidUpdate || inViewDidUpdate
|
|
||||||
|
|
||||||
if (shouldRefetch) {
|
|
||||||
if (this.props.type === 'table') {
|
|
||||||
const queryASTs = await this.getQueryASTs(
|
|
||||||
nextProps.queries,
|
|
||||||
nextProps.templates
|
|
||||||
)
|
|
||||||
this.setState({queryASTs})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.executeQueries(
|
|
||||||
nextProps.queries,
|
|
||||||
nextProps.templates,
|
|
||||||
nextProps.inView
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.autoRefresh !== nextProps.autoRefresh || shouldRefetch) {
|
|
||||||
clearInterval(this.intervalID)
|
|
||||||
|
|
||||||
if (nextProps.autoRefresh) {
|
|
||||||
this.intervalID = setInterval(
|
|
||||||
() =>
|
|
||||||
this.executeQueries(
|
|
||||||
nextProps.queries,
|
|
||||||
nextProps.templates,
|
|
||||||
nextProps.inView
|
|
||||||
),
|
|
||||||
nextProps.autoRefresh
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queryDifference = (left, right) => {
|
|
||||||
const leftStrs = left.map(q => `${q.host}${q.text}`)
|
|
||||||
const rightStrs = right.map(q => `${q.host}${q.text}`)
|
|
||||||
return _.difference(
|
|
||||||
_.union(leftStrs, rightStrs),
|
|
||||||
_.intersection(leftStrs, rightStrs)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
executeQueries = async (
|
|
||||||
queries,
|
|
||||||
templates = [],
|
|
||||||
inView = this.props.inView
|
|
||||||
) => {
|
|
||||||
const {editQueryStatus, grabDataForDownload} = this.props
|
|
||||||
const {resolution} = this.state
|
|
||||||
if (!inView) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!queries.length) {
|
|
||||||
this.setState({timeSeries: []})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({isFetching: true})
|
|
||||||
|
|
||||||
const timeSeriesPromises = queries.map(query => {
|
|
||||||
const {host, database, rp} = query
|
|
||||||
// the key `database` was used upstream in HostPage.js, and since as of this writing
|
|
||||||
// the codebase has not been fully converted to TypeScript, it's not clear where else
|
|
||||||
// it may be used, but this slight modification is intended to allow for the use of
|
|
||||||
// `database` while moving over to `db` for consistency over time
|
|
||||||
const db = _.get(query, 'db', database)
|
|
||||||
|
|
||||||
const templatesWithIntervalVals = templates.map(temp => {
|
|
||||||
if (temp.tempVar === ':interval:') {
|
|
||||||
if (resolution) {
|
|
||||||
// resize event
|
|
||||||
return {
|
|
||||||
...temp,
|
|
||||||
values: temp.values.map(v => ({
|
|
||||||
...v,
|
|
||||||
value: `${_.toInteger(Number(resolution) / 3)}`,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...temp,
|
|
||||||
values: intervalValuesPoints,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return temp
|
|
||||||
})
|
|
||||||
|
|
||||||
const tempVars = removeUnselectedTemplateValues(
|
|
||||||
templatesWithIntervalVals
|
|
||||||
)
|
|
||||||
return fetchTimeSeriesAsync(
|
|
||||||
{
|
|
||||||
source: host,
|
|
||||||
db,
|
|
||||||
rp,
|
|
||||||
query,
|
|
||||||
tempVars,
|
|
||||||
resolution,
|
|
||||||
},
|
|
||||||
editQueryStatus
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const timeSeries = await Promise.all(timeSeriesPromises)
|
|
||||||
const newSeries = timeSeries.map(response => ({response}))
|
|
||||||
const lastQuerySuccessful = this._resultsForQuery(newSeries)
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
timeSeries: newSeries,
|
|
||||||
lastQuerySuccessful,
|
|
||||||
isFetching: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (grabDataForDownload) {
|
|
||||||
grabDataForDownload(timeSeries)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearInterval(this.intervalID)
|
|
||||||
this.intervalID = false
|
|
||||||
}
|
|
||||||
|
|
||||||
setResolution = resolution => {
|
|
||||||
if (resolution !== this.state.resolution) {
|
|
||||||
this.setState({resolution})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {timeSeries, queryASTs} = this.state
|
|
||||||
if (this.state.isFetching && this.state.lastQuerySuccessful) {
|
|
||||||
return (
|
|
||||||
<ComposedComponent
|
|
||||||
{...this.props}
|
|
||||||
data={timeSeries}
|
|
||||||
setResolution={this.setResolution}
|
|
||||||
isFetchingInitially={false}
|
|
||||||
isRefreshing={true}
|
|
||||||
queryASTs={queryASTs}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ComposedComponent
|
|
||||||
{...this.props}
|
|
||||||
data={timeSeries}
|
|
||||||
setResolution={this.setResolution}
|
|
||||||
queryASTs={queryASTs}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_resultsForQuery = data =>
|
|
||||||
data.length
|
|
||||||
? data.every(({response}) =>
|
|
||||||
_.get(response, 'results', []).every(
|
|
||||||
result =>
|
|
||||||
Object.keys(result).filter(k => k !== 'statement_id').length !==
|
|
||||||
0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.defaultProps = {
|
|
||||||
inView: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
array,
|
|
||||||
arrayOf,
|
|
||||||
bool,
|
|
||||||
element,
|
|
||||||
func,
|
|
||||||
number,
|
|
||||||
oneOfType,
|
|
||||||
shape,
|
|
||||||
string,
|
|
||||||
} = PropTypes
|
|
||||||
|
|
||||||
wrapper.propTypes = {
|
|
||||||
type: string.isRequired,
|
|
||||||
children: element,
|
|
||||||
autoRefresh: number.isRequired,
|
|
||||||
inView: bool,
|
|
||||||
templates: arrayOf(
|
|
||||||
shape({
|
|
||||||
type: string.isRequired,
|
|
||||||
tempVar: string.isRequired,
|
|
||||||
query: shape({
|
|
||||||
db: string,
|
|
||||||
rp: string,
|
|
||||||
influxql: string,
|
|
||||||
}),
|
|
||||||
values: arrayOf(
|
|
||||||
shape({
|
|
||||||
type: string.isRequired,
|
|
||||||
value: string.isRequired,
|
|
||||||
selected: bool,
|
|
||||||
})
|
|
||||||
).isRequired,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
queries: arrayOf(
|
|
||||||
shape({
|
|
||||||
host: oneOfType([string, arrayOf(string)]),
|
|
||||||
text: string,
|
|
||||||
}).isRequired
|
|
||||||
).isRequired,
|
|
||||||
axes: shape({
|
|
||||||
bounds: shape({
|
|
||||||
y: array,
|
|
||||||
y2: array,
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
editQueryStatus: func,
|
|
||||||
grabDataForDownload: func,
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AutoRefresh
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
import React, {Component, ComponentClass} from 'react'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
import {getQueryConfig} from 'src/shared/apis'
|
||||||
|
import {fetchTimeSeries} from 'src/shared/apis/query'
|
||||||
|
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
|
||||||
|
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
|
||||||
|
|
||||||
|
interface Axes {
|
||||||
|
bounds: {
|
||||||
|
y: number[]
|
||||||
|
y2: number[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
host: string | string[]
|
||||||
|
text: string
|
||||||
|
database: string
|
||||||
|
db: string
|
||||||
|
rp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TemplateQuery {
|
||||||
|
db: string
|
||||||
|
rp: string
|
||||||
|
influxql: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TemplateValue {
|
||||||
|
type: string
|
||||||
|
value: string
|
||||||
|
selected: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Template {
|
||||||
|
type: string
|
||||||
|
tempVar: string
|
||||||
|
query: TemplateQuery
|
||||||
|
values: TemplateValue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
type: string
|
||||||
|
autoRefresh: number
|
||||||
|
inView: boolean
|
||||||
|
templates: Template[]
|
||||||
|
queries: Query[]
|
||||||
|
axes: Axes
|
||||||
|
editQueryStatus: () => void
|
||||||
|
grabDataForDownload: (timeSeries: TimeSeriesServerResponse[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryAST {
|
||||||
|
groupBy?: {
|
||||||
|
tags: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
isFetching: boolean
|
||||||
|
isLastQuerySuccessful: boolean
|
||||||
|
timeSeries: TimeSeriesServerResponse[]
|
||||||
|
resolution: number | null
|
||||||
|
queryASTs?: QueryAST[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OriginalProps {
|
||||||
|
data: TimeSeriesServerResponse[]
|
||||||
|
setResolution: (resolution: number) => void
|
||||||
|
isFetchingInitially?: boolean
|
||||||
|
isRefreshing?: boolean
|
||||||
|
queryASTs?: QueryAST[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const AutoRefresh = (
|
||||||
|
ComposedComponent: ComponentClass<OriginalProps & Props>
|
||||||
|
) => {
|
||||||
|
class Wrapper extends Component<Props, State> {
|
||||||
|
public static defaultProps = {
|
||||||
|
inView: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
private intervalID: NodeJS.Timer | null
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props)
|
||||||
|
this.state = {
|
||||||
|
isFetching: false,
|
||||||
|
isLastQuerySuccessful: true,
|
||||||
|
timeSeries: DEFAULT_TIME_SERIES,
|
||||||
|
resolution: null,
|
||||||
|
queryASTs: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidMount() {
|
||||||
|
if (this.isTable) {
|
||||||
|
const queryASTs = await this.getQueryASTs()
|
||||||
|
this.setState({queryASTs})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startNewPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async componentDidUpdate(prevProps: Props) {
|
||||||
|
if (!this.isPropsDifferent(prevProps)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isTable) {
|
||||||
|
const queryASTs = await this.getQueryASTs()
|
||||||
|
this.setState({queryASTs})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startNewPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeQueries = async () => {
|
||||||
|
const {editQueryStatus, grabDataForDownload, inView, queries} = this.props
|
||||||
|
const {resolution} = this.state
|
||||||
|
|
||||||
|
if (!inView) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!queries.length) {
|
||||||
|
this.setState({timeSeries: DEFAULT_TIME_SERIES})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({isFetching: true})
|
||||||
|
const templates: Template[] = _.get(this.props, 'templates', [])
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timeSeries = await fetchTimeSeries(
|
||||||
|
queries,
|
||||||
|
resolution,
|
||||||
|
templates,
|
||||||
|
editQueryStatus
|
||||||
|
)
|
||||||
|
const newSeries = timeSeries.map((response: TimeSeriesResponse) => ({
|
||||||
|
response,
|
||||||
|
}))
|
||||||
|
const isLastQuerySuccessful = this.hasResultsForQuery(newSeries)
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
timeSeries: newSeries,
|
||||||
|
isLastQuerySuccessful,
|
||||||
|
isFetching: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (grabDataForDownload) {
|
||||||
|
grabDataForDownload(newSeries)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.clearInterval()
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {
|
||||||
|
timeSeries,
|
||||||
|
queryASTs,
|
||||||
|
isFetching,
|
||||||
|
isLastQuerySuccessful,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
const hasValues = _.some(timeSeries, s => {
|
||||||
|
const results = _.get(s, 'response.results', [])
|
||||||
|
const v = _.some(results, r => r.series)
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hasValues) {
|
||||||
|
return (
|
||||||
|
<div className="graph-empty">
|
||||||
|
<p>No Results</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFetching && isLastQuerySuccessful) {
|
||||||
|
return (
|
||||||
|
<ComposedComponent
|
||||||
|
{...this.props}
|
||||||
|
data={timeSeries}
|
||||||
|
setResolution={this.setResolution}
|
||||||
|
isFetchingInitially={false}
|
||||||
|
isRefreshing={true}
|
||||||
|
queryASTs={queryASTs}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ComposedComponent
|
||||||
|
{...this.props}
|
||||||
|
data={timeSeries}
|
||||||
|
setResolution={this.setResolution}
|
||||||
|
queryASTs={queryASTs}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private setResolution = resolution => {
|
||||||
|
if (resolution !== this.state.resolution) {
|
||||||
|
this.setState({resolution})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearInterval() {
|
||||||
|
if (!this.intervalID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clearInterval(this.intervalID)
|
||||||
|
this.intervalID = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPropsDifferent(nextProps: Props) {
|
||||||
|
return (
|
||||||
|
this.props.inView !== nextProps.inView ||
|
||||||
|
!!this.queryDifference(this.props.queries, nextProps.queries).length ||
|
||||||
|
!_.isEqual(this.props.templates, nextProps.templates) ||
|
||||||
|
this.props.autoRefresh !== nextProps.autoRefresh
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private startNewPolling() {
|
||||||
|
this.clearInterval()
|
||||||
|
|
||||||
|
const {autoRefresh} = this.props
|
||||||
|
|
||||||
|
this.executeQueries()
|
||||||
|
|
||||||
|
if (autoRefresh) {
|
||||||
|
this.intervalID = setInterval(this.executeQueries, autoRefresh)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private queryDifference = (left, right) => {
|
||||||
|
const mapper = q => `${q.host}${q.text}`
|
||||||
|
const leftStrs = left.map(mapper)
|
||||||
|
const rightStrs = right.map(mapper)
|
||||||
|
return _.difference(
|
||||||
|
_.union(leftStrs, rightStrs),
|
||||||
|
_.intersection(leftStrs, rightStrs)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get isTable(): boolean {
|
||||||
|
return this.props.type === 'table'
|
||||||
|
}
|
||||||
|
|
||||||
|
private getQueryASTs = async (): Promise<QueryAST[]> => {
|
||||||
|
const {queries, templates} = this.props
|
||||||
|
|
||||||
|
return await Promise.all(
|
||||||
|
queries.map(async q => {
|
||||||
|
const host = _.isArray(q.host) ? q.host[0] : q.host
|
||||||
|
const url = host.replace('proxy', 'queries')
|
||||||
|
const text = q.text
|
||||||
|
const {data} = await getQueryConfig(url, [{query: text}], templates)
|
||||||
|
return data.queries[0].queryAST
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasResultsForQuery = (data): boolean => {
|
||||||
|
if (!data.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
data.every(({resp}) =>
|
||||||
|
_.get(resp, 'results', []).every(r => Object.keys(r).length > 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoRefresh
|
|
@ -1,3 +1,4 @@
|
||||||
|
import _ from 'lodash'
|
||||||
import React, {PureComponent} from 'react'
|
import React, {PureComponent} from 'react'
|
||||||
import Dygraph from 'dygraphs'
|
import Dygraph from 'dygraphs'
|
||||||
import {connect} from 'react-redux'
|
import {connect} from 'react-redux'
|
||||||
|
@ -35,7 +36,7 @@ class Crosshair extends PureComponent<Props> {
|
||||||
private get isVisible() {
|
private get isVisible() {
|
||||||
const {hoverTime} = this.props
|
const {hoverTime} = this.props
|
||||||
|
|
||||||
return hoverTime !== 0
|
return hoverTime !== 0 && _.isFinite(hoverTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get crosshairLeft(): number {
|
private get crosshairLeft(): number {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const DEFAULT_TIME_SERIES = [
|
||||||
|
{
|
||||||
|
response: {
|
||||||
|
results: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
|
@ -0,0 +1,20 @@
|
||||||
|
export type TimeSeriesValue = string | number | Date | null
|
||||||
|
|
||||||
|
export interface Series {
|
||||||
|
name: string
|
||||||
|
columns: string[]
|
||||||
|
values: TimeSeriesValue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
series: Series[]
|
||||||
|
statement_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesResponse {
|
||||||
|
results: Result[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TimeSeriesServerResponse {
|
||||||
|
response: TimeSeriesResponse
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import AutoRefresh, {
|
||||||
|
Props,
|
||||||
|
OriginalProps,
|
||||||
|
} from 'src/shared/components/AutoRefresh'
|
||||||
|
import React, {Component} from 'react'
|
||||||
|
import {shallow} from 'enzyme'
|
||||||
|
|
||||||
|
type ComponentProps = Props & OriginalProps
|
||||||
|
|
||||||
|
class MyComponent extends Component<ComponentProps> {
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return <p>Here</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const axes = {
|
||||||
|
bounds: {
|
||||||
|
y: [1],
|
||||||
|
y2: [2],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
type: 'table',
|
||||||
|
autoRefresh: 1,
|
||||||
|
inView: true,
|
||||||
|
templates: [],
|
||||||
|
queries: [],
|
||||||
|
axes,
|
||||||
|
editQueryStatus: () => {},
|
||||||
|
grabDataForDownload: () => {},
|
||||||
|
data: [],
|
||||||
|
setResolution: () => {},
|
||||||
|
isFetchingInitially: false,
|
||||||
|
isRefreshing: false,
|
||||||
|
queryASTs: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const setup = (overrides: Partial<ComponentProps> = {}) => {
|
||||||
|
const ARComponent = AutoRefresh(MyComponent)
|
||||||
|
|
||||||
|
const props = {...defaultProps, ...overrides}
|
||||||
|
|
||||||
|
return shallow(<ARComponent {...props} />)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Shared.Components.AutoRefresh', () => {
|
||||||
|
describe('render', () => {
|
||||||
|
describe('when there are no results', () => {
|
||||||
|
it('renders the no results component', () => {
|
||||||
|
const wrapped = setup()
|
||||||
|
expect(wrapped.find('.graph-empty').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when there are results', () => {
|
||||||
|
it('renderes the wrapped component', () => {
|
||||||
|
const wrapped = setup()
|
||||||
|
const timeSeries = [
|
||||||
|
{
|
||||||
|
response: {
|
||||||
|
results: [{series: [1]}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
wrapped.update()
|
||||||
|
wrapped.setState({timeSeries})
|
||||||
|
process.nextTick(() => {
|
||||||
|
expect(wrapped.find(MyComponent).exists()).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in New Issue