diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c5ee12d3..0c2b230a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 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. [#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 diff --git a/ui/src/admin/components/chronograf/AdminTabs.js b/ui/src/admin/components/chronograf/AdminTabs.js deleted file mode 100644 index 48ac81fd8..000000000 --- a/ui/src/admin/components/chronograf/AdminTabs.js +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { - isUserAuthorized, - ADMIN_ROLE, - SUPERADMIN_ROLE, -} from 'src/auth/Authorized' - -import {Tab, Tabs, TabPanel, TabPanels, TabList} from 'shared/components/Tabs' -import OrganizationsPage from 'src/admin/containers/chronograf/OrganizationsPage' -import UsersPage from 'src/admin/containers/chronograf/UsersPage' -import ProvidersPage from 'src/admin/containers/ProvidersPage' -import AllUsersPage from 'src/admin/containers/chronograf/AllUsersPage' - -const ORGANIZATIONS_TAB_NAME = 'All Orgs' -const PROVIDERS_TAB_NAME = 'Org Mappings' -const CURRENT_ORG_USERS_TAB_NAME = 'Current Org' -const ALL_USERS_TAB_NAME = 'All Users' - -const AdminTabs = ({ - me: {currentOrganization: meCurrentOrganization, role: meRole, id: meID}, -}) => { - const tabs = [ - { - requiredRole: ADMIN_ROLE, - type: CURRENT_ORG_USERS_TAB_NAME, - component: ( - - ), - }, - { - requiredRole: SUPERADMIN_ROLE, - type: ALL_USERS_TAB_NAME, - component: , - }, - { - requiredRole: SUPERADMIN_ROLE, - type: ORGANIZATIONS_TAB_NAME, - component: ( - - ), - }, - { - requiredRole: SUPERADMIN_ROLE, - type: PROVIDERS_TAB_NAME, - component: , - }, - ].filter(t => isUserAuthorized(meRole, t.requiredRole)) - - return ( - - - {tabs.map((t, i) => {tabs[i].type})} - - - {tabs.map((t, i) => ( - {t.component} - ))} - - - ) -} - -const {shape, string} = PropTypes - -AdminTabs.propTypes = { - me: shape({ - id: string.isRequired, - role: string.isRequired, - currentOrganization: shape({ - name: string.isRequired, - id: string.isRequired, - }), - }).isRequired, -} - -export default AdminTabs diff --git a/ui/src/admin/containers/chronograf/AdminChronografPage.js b/ui/src/admin/containers/chronograf/AdminChronografPage.js index 2815bd1fa..5e1eb9f77 100644 --- a/ui/src/admin/containers/chronograf/AdminChronografPage.js +++ b/ui/src/admin/containers/chronograf/AdminChronografPage.js @@ -2,10 +2,52 @@ import React from 'react' import PropTypes from 'prop-types' import {connect} from 'react-redux' -import AdminTabs from 'src/admin/components/chronograf/AdminTabs' +import SubSections from 'src/shared/components/SubSections' import FancyScrollbar from 'shared/components/FancyScrollbar' -const AdminChronografPage = ({me}) => ( +import UsersPage from 'src/admin/containers/chronograf/UsersPage' +import AllUsersPage from 'src/admin/containers/chronograf/AllUsersPage' +import OrganizationsPage from 'src/admin/containers/chronograf/OrganizationsPage' +import ProvidersPage from 'src/admin/containers/ProvidersPage' + +import { + isUserAuthorized, + ADMIN_ROLE, + SUPERADMIN_ROLE, +} from 'src/auth/Authorized' + +const sections = me => [ + { + url: 'current-organization', + name: 'Current Org', + enabled: isUserAuthorized(me.role, ADMIN_ROLE), + component: ( + + ), + }, + { + url: 'all-users', + name: 'All Users', + enabled: isUserAuthorized(me.role, SUPERADMIN_ROLE), + component: , + }, + { + url: 'all-organizations', + name: 'All Orgs', + enabled: isUserAuthorized(me.role, SUPERADMIN_ROLE), + component: ( + + ), + }, + { + url: 'organization-mappings', + name: 'Org Mappings', + enabled: isUserAuthorized(me.role, SUPERADMIN_ROLE), + component: , + }, +] + +const AdminChronografPage = ({me, source, params: {tab}}) => (
@@ -16,9 +58,12 @@ const AdminChronografPage = ({me}) => (
-
- -
+
@@ -35,6 +80,15 @@ AdminChronografPage.propTypes = { id: string.isRequired, }), }).isRequired, + params: shape({ + tab: string, + }).isRequired, + source: shape({ + id: string.isRequired, + links: shape({ + users: string.isRequired, + }), + }).isRequired, } const mapStateToProps = ({auth: {me}}) => ({ diff --git a/ui/src/dashboards/utils/tableGraph.ts b/ui/src/dashboards/utils/tableGraph.ts index 0650a429a..fc28bcb9f 100644 --- a/ui/src/dashboards/utils/tableGraph.ts +++ b/ui/src/dashboards/utils/tableGraph.ts @@ -159,7 +159,7 @@ export const orderTableColumns = (data, fieldOptions) => { }) const filteredFieldSortOrder = filter(fieldsSortOrder, f => f !== -1) const orderedData = map(data, row => { - return row.map((v, j, arr) => arr[filteredFieldSortOrder[j]] || v) + return row.map((__, j, arr) => arr[filteredFieldSortOrder[j]]) }) return orderedData[0].length ? orderedData : [[]] } diff --git a/ui/src/data_explorer/containers/DataExplorer.tsx b/ui/src/data_explorer/containers/DataExplorer.tsx index 57ae430f9..9b6e59d02 100644 --- a/ui/src/data_explorer/containers/DataExplorer.tsx +++ b/ui/src/data_explorer/containers/DataExplorer.tsx @@ -1,5 +1,4 @@ import React, {PureComponent} from 'react' -import PropTypes from 'prop-types' import {connect} from 'react-redux' import {bindActionCreators} from 'redux' import {withRouter, InjectedRouter} from 'react-router' @@ -52,15 +51,6 @@ interface State { @ErrorHandling export class DataExplorer extends PureComponent { - public static childContextTypes = { - source: PropTypes.shape({ - links: PropTypes.shape({ - proxy: PropTypes.string.isRequired, - self: PropTypes.string.isRequired, - }).isRequired, - }).isRequired, - } - constructor(props) { super(props) diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index b1602438e..e81855fa6 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -21,25 +21,33 @@ class BodyBuilder extends PureComponent { return b.declarations.map(d => { if (d.funcs) { return ( - +
+
{d.name} =
+ +
) } - return
{b.source}
+ return ( +
+ {b.source} +
+ ) }) } return ( ) }) diff --git a/ui/src/ifql/components/ExpressionNode.tsx b/ui/src/ifql/components/ExpressionNode.tsx index 5f6fabe91..8ef81749b 100644 --- a/ui/src/ifql/components/ExpressionNode.tsx +++ b/ui/src/ifql/components/ExpressionNode.tsx @@ -8,36 +8,37 @@ import {Func} from 'src/types/ifql' interface Props { funcNames: any[] - id: string + bodyID: string funcs: Func[] + declarationID?: string } // an Expression is a group of one or more functions class ExpressionNode extends PureComponent { public render() { - const {id, funcNames, funcs} = this.props + const {declarationID, bodyID, funcNames, funcs} = this.props return ( {({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => { return (
-

- -

{funcs.map(func => ( ))} +
) }} diff --git a/ui/src/ifql/components/From.tsx b/ui/src/ifql/components/From.tsx index 08b8c6365..4ecdf9b73 100644 --- a/ui/src/ifql/components/From.tsx +++ b/ui/src/ifql/components/From.tsx @@ -9,7 +9,8 @@ interface Props { funcID: string argKey: string value: string - expressionID: string + bodyID: string + declarationID: string onChangeArg: OnChangeArg } @@ -56,12 +57,13 @@ class From extends PureComponent { } private handleChooseDatabase = (item: DropdownItem): void => { - const {argKey, funcID, onChangeArg, expressionID} = this.props + const {argKey, funcID, onChangeArg, bodyID, declarationID} = this.props onChangeArg({ funcID, key: argKey, value: item.text, - expressionID, + bodyID, + declarationID, generate: true, }) } diff --git a/ui/src/ifql/components/FuncArg.tsx b/ui/src/ifql/components/FuncArg.tsx index 091786a48..03ef84748 100644 --- a/ui/src/ifql/components/FuncArg.tsx +++ b/ui/src/ifql/components/FuncArg.tsx @@ -14,7 +14,8 @@ interface Props { argKey: string value: string | boolean type: string - expressionID: string + bodyID: string + declarationID: string onChangeArg: OnChangeArg onGenerateScript: () => void } @@ -26,10 +27,11 @@ class FuncArg extends PureComponent { argKey, value, type, - funcName, + bodyID, funcID, + funcName, onChangeArg, - expressionID, + declarationID, onGenerateScript, } = this.props @@ -39,7 +41,8 @@ class FuncArg extends PureComponent { argKey={argKey} funcID={funcID} value={this.value} - expressionID={expressionID} + bodyID={bodyID} + declarationID={declarationID} onChangeArg={onChangeArg} /> ) @@ -60,8 +63,9 @@ class FuncArg extends PureComponent { value={this.value} argKey={argKey} funcID={funcID} - expressionID={expressionID} + bodyID={bodyID} onChangeArg={onChangeArg} + declarationID={declarationID} onGenerateScript={onGenerateScript} /> ) @@ -72,9 +76,10 @@ class FuncArg extends PureComponent { ) diff --git a/ui/src/ifql/components/FuncArgBool.tsx b/ui/src/ifql/components/FuncArgBool.tsx index a8ec37e8e..e87b0f8dc 100644 --- a/ui/src/ifql/components/FuncArgBool.tsx +++ b/ui/src/ifql/components/FuncArgBool.tsx @@ -7,7 +7,8 @@ interface Props { argKey: string value: boolean funcID: string - expressionID: string + bodyID: string + declarationID: string onChangeArg: OnChangeArg onGenerateScript: () => void } @@ -23,8 +24,15 @@ class FuncArgBool extends PureComponent { } private handleToggle = (value: boolean): void => { - const {argKey, funcID, expressionID, onChangeArg} = this.props - onChangeArg({funcID, key: argKey, value, generate: true, expressionID}) + const {argKey, funcID, bodyID, onChangeArg, declarationID} = this.props + onChangeArg({ + key: argKey, + value, + funcID, + bodyID, + declarationID, + generate: true, + }) } } diff --git a/ui/src/ifql/components/FuncArgInput.tsx b/ui/src/ifql/components/FuncArgInput.tsx index ea0070a80..644e8f316 100644 --- a/ui/src/ifql/components/FuncArgInput.tsx +++ b/ui/src/ifql/components/FuncArgInput.tsx @@ -7,7 +7,8 @@ interface Props { argKey: string value: string type: string - expressionID: string + bodyID: string + declarationID: string onChangeArg: OnChangeArg onGenerateScript: () => void } @@ -44,13 +45,14 @@ class FuncArgInput extends PureComponent { } private handleChange = (e: ChangeEvent) => { - const {funcID, argKey, expressionID} = this.props + const {funcID, argKey, bodyID, declarationID} = this.props this.props.onChangeArg({ funcID, key: argKey, value: e.target.value, - expressionID, + declarationID, + bodyID, }) } } diff --git a/ui/src/ifql/components/FuncArgs.tsx b/ui/src/ifql/components/FuncArgs.tsx index c8be0ac3c..10c75d4cf 100644 --- a/ui/src/ifql/components/FuncArgs.tsx +++ b/ui/src/ifql/components/FuncArgs.tsx @@ -6,15 +6,22 @@ import {Func} from 'src/types/ifql' interface Props { func: Func - expressionID: string + bodyID: string onChangeArg: OnChangeArg + declarationID: string onGenerateScript: () => void } @ErrorHandling export default class FuncArgs extends PureComponent { public render() { - const {expressionID, func, onChangeArg, onGenerateScript} = this.props + const { + func, + bodyID, + onChangeArg, + declarationID, + onGenerateScript, + } = this.props return (
@@ -25,10 +32,11 @@ export default class FuncArgs extends PureComponent { type={type} argKey={key} value={value} + bodyID={bodyID} funcID={func.id} funcName={func.name} onChangeArg={onChangeArg} - expressionID={expressionID} + declarationID={declarationID} onGenerateScript={onGenerateScript} /> ) diff --git a/ui/src/ifql/components/FuncNode.tsx b/ui/src/ifql/components/FuncNode.tsx index 7d83005ce..343f6bd1b 100644 --- a/ui/src/ifql/components/FuncNode.tsx +++ b/ui/src/ifql/components/FuncNode.tsx @@ -1,12 +1,13 @@ import React, {PureComponent, MouseEvent} from 'react' import FuncArgs from 'src/ifql/components/FuncArgs' -import {OnChangeArg, Func} from 'src/types/ifql' +import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { func: Func - expressionID: string - onDelete: (funcID: string, expressionID: string) => void + bodyID: string + declarationID?: string + onDelete: OnDeleteFuncNode onChangeArg: OnChangeArg onGenerateScript: () => void } @@ -17,6 +18,10 @@ interface State { @ErrorHandling export default class FuncNode extends PureComponent { + public static defaultProps: Partial = { + declarationID: '', + } + constructor(props) { super(props) this.state = { @@ -25,7 +30,13 @@ export default class FuncNode extends PureComponent { } public render() { - const {expressionID, func, onChangeArg, onGenerateScript} = this.props + const { + func, + bodyID, + onChangeArg, + declarationID, + onGenerateScript, + } = this.props const {isOpen} = this.state return ( @@ -36,8 +47,9 @@ export default class FuncNode extends PureComponent { {isOpen && ( )} @@ -49,7 +61,9 @@ export default class FuncNode extends PureComponent { } private handleDelete = (): void => { - this.props.onDelete(this.props.func.id, this.props.expressionID) + const {func, bodyID, declarationID} = this.props + + this.props.onDelete({funcID: func.id, bodyID, declarationID}) } private handleClick = (e: MouseEvent): void => { diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index 8654f6ef1..6e2fdae10 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -14,7 +14,8 @@ interface State { interface Props { funcs: string[] - expressionID: string + bodyID: string + declarationID: string onAddNode: OnAddNode } @@ -65,8 +66,9 @@ export class FuncSelector extends PureComponent { } private handleAddNode = (name: string) => { + const {bodyID, declarationID} = this.props this.handleCloseList() - this.props.onAddNode(name, this.props.expressionID) + this.props.onAddNode(name, bodyID, declarationID) } private get availableFuncs() { diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index ba7008824..2ad7d99b4 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -1,11 +1,12 @@ import React, {PureComponent} from 'react' import {connect} from 'react-redux' +import _ from 'lodash' import TimeMachine from 'src/ifql/components/TimeMachine' import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts' import {Suggestion, FlatBody} from 'src/types/ifql' -import {InputArg, Handlers} from 'src/types/ifql' +import {InputArg, Handlers, DeleteFuncNodeArgs, Func} from 'src/types/ifql' import {bodyNodes} from 'src/ifql/helpers' import {getSuggestions, getAST} from 'src/ifql/apis' @@ -44,7 +45,7 @@ export class IFQLPage extends PureComponent { ast: null, suggestions: [], script: - 'foo = from(db: "telegraf")\n\t|> filter() \n\t|> range(start: -15m)\n\nfrom(db: "telegraf")\n\t|> filter() \n\t|> range(start: -15m)\n\n', + 'baz = "baz"\n\nfoo = from(db: "telegraf")\n\t|> filter() \n\t|> range(start: -15m)\n\nbar = from(db: "telegraf")\n\t|> filter() \n\t|> range(start: -15m)\n\n', } } @@ -108,7 +109,7 @@ export class IFQLPage extends PureComponent { } private handleGenerateScript = (): void => { - this.getASTResponse(this.expressionsToScript) + this.getASTResponse(this.bodyToScript) } private handleChangeArg = ({ @@ -116,30 +117,41 @@ export class IFQLPage extends PureComponent { value, generate, funcID, - expressionID, + declarationID = '', + bodyID, }: InputArg): void => { - const body = this.state.body.map(expression => { - if (expression.id !== expressionID) { - return expression + const body = this.state.body.map(b => { + if (b.id !== bodyID) { + return b } - const funcs = expression.funcs.map(f => { - if (f.id !== funcID) { - return f - } - - const args = f.args.map(a => { - if (a.key === key) { - return {...a, value} + if (declarationID) { + const declarations = b.declarations.map(d => { + if (d.id !== declarationID) { + return d } - return a + const functions = this.editFuncArgs({ + funcs: d.funcs, + funcID, + key, + value, + }) + + return {...d, funcs: functions} }) - return {...f, args} + return {...b, declarations} + } + + const funcs = this.editFuncArgs({ + funcs: b.funcs, + funcID, + key, + value, }) - return {...expression, funcs} + return {...b, funcs} }) this.setState({body}, () => { @@ -149,9 +161,42 @@ export class IFQLPage extends PureComponent { }) } - private get expressionsToScript(): string { - return this.state.body.reduce((acc, expression) => { - return `${acc + this.funcsToScript(expression.funcs)}\n\n` + private editFuncArgs = ({funcs, funcID, key, value}): Func[] => { + return funcs.map(f => { + if (f.id !== funcID) { + return f + } + + const args = f.args.map(a => { + if (a.key === key) { + return {...a, value} + } + + return a + }) + + return {...f, args} + }) + } + + private get bodyToScript(): string { + return this.state.body.reduce((acc, b) => { + if (b.declarations.length) { + const declaration = _.get(b, 'declarations.0', false) + if (!declaration) { + return acc + } + + if (!declaration.funcs) { + return `${acc}${b.source}\n\n` + } + + return `${acc}${declaration.name} = ${this.funcsToScript( + declaration.funcs + )}\n\n` + } + + return `${acc}${this.funcsToScript(b.funcs)}\n\n` }, '') } @@ -183,51 +228,104 @@ export class IFQLPage extends PureComponent { this.setState({script}) } - private handleAddNode = (name: string, expressionID: string): void => { - const script = this.state.body.reduce((acc, expression) => { - if (expression.id === expressionID) { - const {funcs} = expression - return `${acc}${this.funcsToScript(funcs)}\n\t|> ${name}()\n\n` + private handleAddNode = ( + name: string, + bodyID: string, + declarationID: string + ): void => { + const script = this.state.body.reduce((acc, body) => { + const {id, source, funcs} = body + + if (id === bodyID) { + const declaration = body.declarations.find(d => d.id === declarationID) + if (declaration) { + return `${acc}${declaration.name} = ${this.appendFunc( + declaration.funcs, + name + )}` + } + + return `${acc}${this.appendFunc(funcs, name)}` } - return acc + expression.source + return `${acc}${this.formatSource(source)}` }, '') this.getASTResponse(script) } - private handleDeleteFuncNode = ( - funcID: string, - expressionID: string - ): void => { - // TODO: export this and test functionality + private appendFunc = (funcs, name): string => { + return `${this.funcsToScript(funcs)}\n\t|> ${name}()\n\n` + } + + private handleDeleteFuncNode = (ids: DeleteFuncNodeArgs): void => { + const {funcID, declarationID = '', bodyID} = ids + const script = this.state.body - .map((expression, expressionIndex) => { - if (expression.id !== expressionID) { - return expression.source + .map((body, bodyIndex) => { + if (body.id !== bodyID) { + return this.formatSource(body.source) } - const funcs = expression.funcs.filter(f => f.id !== funcID) - const source = funcs.reduce((acc, f, i) => { - if (i === 0) { - return `${f.source}` + const isLast = bodyIndex === this.state.body.length - 1 + + if (declarationID) { + const declaration = body.declarations.find( + d => d.id === declarationID + ) + + if (!declaration) { + return } - return `${acc}\n\t${f.source}` - }, '') - - const isLast = expressionIndex === this.state.body.length - 1 - if (isLast) { - return `${source}` + const functions = declaration.funcs.filter(f => f.id !== funcID) + const s = this.funcsToSource(functions) + return `${declaration.name} = ${this.formatLastSource(s, isLast)}` } - return `${source}\n\n` + const funcs = body.funcs.filter(f => f.id !== funcID) + const source = this.funcsToSource(funcs) + return this.formatLastSource(source, isLast) }) .join('') this.getASTResponse(script) } + private formatSource = (source: string): string => { + // currently a bug in the AST which does not add newlines to literal variable assignment bodies + if (!source.match(/\n\n/)) { + return `${source}\n\n` + } + + return `${source}` + } + + // formats the last line of a body string to include two new lines + private formatLastSource = (source: string, isLast: boolean): string => { + if (isLast) { + return `${source}` + } + + // currently a bug in the AST which does not add newlines to literal variable assignment bodies + if (!source.match(/\n\n/)) { + return `${source}\n\n` + } + + return `${source}\n\n` + } + + // funcsToSource takes a list of funtion nodes and returns an ifql script + private funcsToSource = (funcs): string => { + return funcs.reduce((acc, f, i) => { + if (i === 0) { + return `${f.source}` + } + + return `${acc}\n\t${f.source}` + }, '') + } + private getASTResponse = async (script: string) => { const {links} = this.props diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 5f8572b98..9d8b0d651 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -146,7 +146,10 @@ class Root extends PureComponent<{}, State> { path="kapacitors/:id/edit:hash" component={KapacitorPage} /> - + diff --git a/ui/src/kapacitor/components/AlertTabs.js b/ui/src/kapacitor/components/AlertTabs.js index 270337865..0f48d47aa 100644 --- a/ui/src/kapacitor/components/AlertTabs.js +++ b/ui/src/kapacitor/components/AlertTabs.js @@ -170,7 +170,7 @@ class AlertTabs extends Component { const showDeprecation = pagerDutyV1Enabled const pagerDutyDeprecationMessage = (
- PagerDuty v2 is being{' '} + PagerDuty v1 is being{' '} { 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) +} diff --git a/ui/src/shared/components/AutoRefresh.js b/ui/src/shared/components/AutoRefresh.js deleted file mode 100644 index 6e4033593..000000000 --- a/ui/src/shared/components/AutoRefresh.js +++ /dev/null @@ -1,292 +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 => { - this.setState({resolution}) - } - - render() { - const {timeSeries, queryASTs} = this.state - if (this.state.isFetching && this.state.lastQuerySuccessful) { - return ( - - ) - } - - return ( - - ) - } - - _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 diff --git a/ui/src/shared/components/AutoRefresh.tsx b/ui/src/shared/components/AutoRefresh.tsx new file mode 100644 index 000000000..b1df696c0 --- /dev/null +++ b/ui/src/shared/components/AutoRefresh.tsx @@ -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 +) => { + class Wrapper extends Component { + 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 ( +
+

No Results

+
+ ) + } + + if (isFetching && isLastQuerySuccessful) { + return ( + + ) + } + + return ( + + ) + } + + 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 => { + 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 diff --git a/ui/src/shared/components/AutoRefreshDropdown.js b/ui/src/shared/components/AutoRefreshDropdown.js index 92f338cd8..7a1d06830 100644 --- a/ui/src/shared/components/AutoRefreshDropdown.js +++ b/ui/src/shared/components/AutoRefreshDropdown.js @@ -41,7 +41,7 @@ class AutoRefreshDropdown extends Component { paused: +milliseconds === 0, })} > -
+
)}
  • - {preventCustomTimeRange ? '' : 'Relative '}Time Ranges + {preventCustomTimeRange ? '' : 'Relative '}Time
  • {timeRanges.map(item => { return ( diff --git a/ui/src/shared/constants/series.ts b/ui/src/shared/constants/series.ts new file mode 100644 index 000000000..3177b6d3e --- /dev/null +++ b/ui/src/shared/constants/series.ts @@ -0,0 +1,7 @@ +export const DEFAULT_TIME_SERIES = [ + { + response: { + results: [], + }, + }, +] diff --git a/ui/src/shared/data/autoRefreshes.js b/ui/src/shared/data/autoRefreshes.js index 5ce7bf63e..4778dafbb 100644 --- a/ui/src/shared/data/autoRefreshes.js +++ b/ui/src/shared/data/autoRefreshes.js @@ -2,28 +2,28 @@ const autoRefreshItems = [ {milliseconds: 0, inputValue: 'Paused', menuOption: 'Paused'}, { milliseconds: 5000, - inputValue: 'Every 5 seconds', - menuOption: 'Every 5 seconds', + inputValue: 'Every 5s', + menuOption: 'Every 5s', }, { milliseconds: 10000, - inputValue: 'Every 10 seconds', - menuOption: 'Every 10 seconds', + inputValue: 'Every 10s', + menuOption: 'Every 10s', }, { milliseconds: 15000, - inputValue: 'Every 15 seconds', - menuOption: 'Every 15 seconds', + inputValue: 'Every 15s', + menuOption: 'Every 15s', }, { milliseconds: 30000, - inputValue: 'Every 30 seconds', - menuOption: 'Every 30 seconds', + inputValue: 'Every 30s', + menuOption: 'Every 30s', }, { milliseconds: 60000, - inputValue: 'Every 60 seconds', - menuOption: 'Every 60 seconds', + inputValue: 'Every 60s', + menuOption: 'Every 60s', }, ] diff --git a/ui/src/shared/data/timeRanges.js b/ui/src/shared/data/timeRanges.js index 914f0ddd7..1631bcd93 100644 --- a/ui/src/shared/data/timeRanges.js +++ b/ui/src/shared/data/timeRanges.js @@ -2,73 +2,73 @@ export const timeRanges = [ { defaultGroupBy: '10s', seconds: 300, - inputValue: 'Past 5 minutes', + inputValue: 'Past 5m', lower: 'now() - 5m', upper: null, - menuOption: 'Past 5 minutes', + menuOption: 'Past 5m', }, { defaultGroupBy: '1m', seconds: 900, - inputValue: 'Past 15 minutes', + inputValue: 'Past 15m', lower: 'now() - 15m', upper: null, - menuOption: 'Past 15 minutes', + menuOption: 'Past 15m', }, { defaultGroupBy: '1m', seconds: 3600, - inputValue: 'Past hour', + inputValue: 'Past 1h', lower: 'now() - 1h', upper: null, - menuOption: 'Past hour', + menuOption: 'Past 1h', }, { defaultGroupBy: '1m', seconds: 21600, - inputValue: 'Past 6 hours', + inputValue: 'Past 6h', lower: 'now() - 6h', upper: null, - menuOption: 'Past 6 hours', + menuOption: 'Past 6h', }, { defaultGroupBy: '5m', seconds: 43200, - inputValue: 'Past 12 hours', + inputValue: 'Past 12h', lower: 'now() - 12h', upper: null, - menuOption: 'Past 12 hours', + menuOption: 'Past 12h', }, { defaultGroupBy: '10m', seconds: 86400, - inputValue: 'Past 24 hours', + inputValue: 'Past 24h', lower: 'now() - 24h', upper: null, - menuOption: 'Past 24 hours', + menuOption: 'Past 24h', }, { defaultGroupBy: '30m', seconds: 172800, - inputValue: 'Past 2 days', + inputValue: 'Past 2d', lower: 'now() - 2d', upper: null, - menuOption: 'Past 2 days', + menuOption: 'Past 2d', }, { defaultGroupBy: '1h', seconds: 604800, - inputValue: 'Past 7 days', + inputValue: 'Past 7d', lower: 'now() - 7d', upper: null, - menuOption: 'Past 7 days', + menuOption: 'Past 7d', }, { defaultGroupBy: '6h', seconds: 2592000, - inputValue: 'Past 30 days', + inputValue: 'Past 30d', lower: 'now() - 30d', upper: null, - menuOption: 'Past 30 days', + menuOption: 'Past 30d', }, ] diff --git a/ui/src/side_nav/containers/SideNav.tsx b/ui/src/side_nav/containers/SideNav.tsx index 7c8e93e62..5f28254bf 100644 --- a/ui/src/side_nav/containers/SideNav.tsx +++ b/ui/src/side_nav/containers/SideNav.tsx @@ -122,14 +122,16 @@ class SideNav extends PureComponent { - + Chronograf diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/func-node.scss index 84c3b1374..82dcfed97 100644 --- a/ui/src/style/components/func-node.scss +++ b/ui/src/style/components/func-node.scss @@ -14,7 +14,6 @@ width: auto; display: flex; color: $ix-text-default; - text-transform: uppercase; margin-bottom: $ix-marg-a; font-family: $ix-text-font; font-weight: 500; diff --git a/ui/src/types/ifql.ts b/ui/src/types/ifql.ts index 4cf3326e8..65bd406f9 100644 --- a/ui/src/types/ifql.ts +++ b/ui/src/types/ifql.ts @@ -1,7 +1,11 @@ // function definitions -export type OnDeleteFuncNode = (funcID: string, expressionID: string) => void +export type OnDeleteFuncNode = (ids: DeleteFuncNodeArgs) => void export type OnChangeArg = (inputArg: InputArg) => void -export type OnAddNode = (expressionID: string, funcName: string) => void +export type OnAddNode = ( + bodyID: string, + funcName: string, + declarationID: string +) => void export type OnGenerateScript = (script: string) => void export type OnChangeScript = (script: string) => void export type OnSubmitScript = () => void @@ -15,9 +19,16 @@ export interface Handlers { onGenerateScript: OnGenerateScript } +export interface DeleteFuncNodeArgs { + funcID: string + bodyID: string + declarationID?: string +} + export interface InputArg { funcID: string - expressionID: string + bodyID: string + declarationID?: string key: string value: string | boolean generate?: boolean diff --git a/ui/src/types/series.ts b/ui/src/types/series.ts new file mode 100644 index 000000000..3293f9a95 --- /dev/null +++ b/ui/src/types/series.ts @@ -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 +} diff --git a/ui/src/utils/groupBy.js b/ui/src/utils/groupBy.js index 74e9e7b09..ff6cd898e 100644 --- a/ui/src/utils/groupBy.js +++ b/ui/src/utils/groupBy.js @@ -2,8 +2,11 @@ import _ from 'lodash' import {shiftDate} from 'shared/query/helpers' import {map, reduce, forEach, concat, clone} from 'fast.js' -const groupByMap = (responses, responseIndex, groupByColumns) => { - const firstColumns = _.get(responses, [0, 'series', 0, 'columns']) +const groupByMap = (results, responseIndex, groupByColumns) => { + if (_.isEmpty(results)) { + return [] + } + const firstColumns = _.get(results, [0, 'series', 0, 'columns']) const accum = [ { responseIndex, @@ -15,14 +18,14 @@ const groupByMap = (responses, responseIndex, groupByColumns) => { ...firstColumns.slice(1), ], groupByColumns, - name: _.get(responses, [0, 'series', 0, 'name']), + name: _.get(results, [0, 'series', 0, 'name'], ''), values: [], }, ], }, ] - const seriesArray = _.get(responses, [0, 'series']) + const seriesArray = _.get(results, [0, 'series']) seriesArray.forEach(s => { const prevValues = accum[0].series[0].values const tagsToAdd = groupByColumns.map(gb => s.tags[gb]) @@ -35,13 +38,14 @@ const groupByMap = (responses, responseIndex, groupByColumns) => { const constructResults = (raw, groupBys) => { return _.flatten( map(raw, (response, index) => { - const responses = _.get(response, 'response.results', []) + const results = _.get(response, 'response.results', []) + + const successfulResults = _.filter(results, r => _.isNil(r.error)) if (groupBys[index]) { - return groupByMap(responses, index, groupBys[index]) + return groupByMap(successfulResults, index, groupBys[index]) } - - return map(responses, r => ({...r, responseIndex: index})) + return map(successfulResults, r => ({...r, responseIndex: index})) }) ) } @@ -81,22 +85,24 @@ const constructCells = serieses => { name: measurement, columns, groupByColumns, - values, + values = [], seriesIndex, responseIndex, tags = {}, }, ind ) => { - const rows = map(values || [], vals => ({ - vals, - })) + const rows = map(values, vals => ({vals})) + + const tagSet = map(Object.keys(tags), tag => `[${tag}=${tags[tag]}]`) + .sort() + .join('') const unsortedLabels = map(columns.slice(1), (field, i) => ({ label: groupByColumns && i <= groupByColumns.length - 1 ? `${field}` - : `${measurement}.${field}`, + : `${measurement}.${field}${tagSet}`, responseIndex, seriesIndex, })) @@ -221,8 +227,8 @@ export const groupByTimeSeriesTransform = (raw, groupBys) => { if (!groupBys) { groupBys = Array(raw.length).fill(false) } - const results = constructResults(raw, groupBys) + const results = constructResults(raw, groupBys) const serieses = constructSerieses(results) const {cells, sortedLabels, seriesLabels} = constructCells(serieses) diff --git a/ui/test/ifql/components/From.test.tsx b/ui/test/ifql/components/From.test.tsx index 4a8adad3d..d859f9728 100644 --- a/ui/test/ifql/components/From.test.tsx +++ b/ui/test/ifql/components/From.test.tsx @@ -9,7 +9,8 @@ const setup = () => { funcID: '1', argKey: 'db', value: 'db1', - expressionID: '2', + bodyID: '2', + declarationID: '1', onChangeArg: () => {}, } diff --git a/ui/test/ifql/components/FuncArg.test.tsx b/ui/test/ifql/components/FuncArg.test.tsx index 98949f4a8..7dabb0cc2 100644 --- a/ui/test/ifql/components/FuncArg.test.tsx +++ b/ui/test/ifql/components/FuncArg.test.tsx @@ -5,8 +5,9 @@ import FuncArg from 'src/ifql/components/FuncArg' const setup = () => { const props = { funcID: '', - expressionID: '', + bodyID: '', funcName: '', + declarationID: '', argKey: '', value: '', type: '', diff --git a/ui/test/ifql/components/FuncSelector.test.tsx b/ui/test/ifql/components/FuncSelector.test.tsx index 821887609..e4dfa27b1 100644 --- a/ui/test/ifql/components/FuncSelector.test.tsx +++ b/ui/test/ifql/components/FuncSelector.test.tsx @@ -8,7 +8,8 @@ import FuncList from 'src/ifql/components/FuncList' const setup = (override = {}) => { const props = { funcs: ['count', 'range'], - expressionID: '1', + bodyID: '1', + declarationID: '2', onAddNode: () => {}, ...override, } @@ -133,7 +134,7 @@ describe('IFQL.Components.FuncsButton', () => { const onAddNode = jest.fn() const {wrapper, props} = setup({onAddNode}) const [, func2] = props.funcs - const {expressionID} = props + const {bodyID, declarationID} = props const dropdownButton = wrapper.find('button') dropdownButton.simulate('click') @@ -148,7 +149,7 @@ describe('IFQL.Components.FuncsButton', () => { input.simulate('keyDown', {key: 'ArrowDown'}) input.simulate('keyDown', {key: 'Enter'}) - expect(onAddNode).toHaveBeenCalledWith(func2, expressionID) + expect(onAddNode).toHaveBeenCalledWith(func2, bodyID, declarationID) }) }) }) diff --git a/ui/test/shared/components/AutoRefresh.test.tsx b/ui/test/shared/components/AutoRefresh.test.tsx new file mode 100644 index 000000000..bf750e668 --- /dev/null +++ b/ui/test/shared/components/AutoRefresh.test.tsx @@ -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 { + public render(): JSX.Element { + return

    Here

    + } +} + +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 = {}) => { + const ARComponent = AutoRefresh(MyComponent) + + const props = {...defaultProps, ...overrides} + + return shallow() +} + +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) + }) + }) + }) + }) +})