Merge branch 'master' into table/field-options

table/field-options
ebb-tide 2018-04-30 15:12:31 -07:00
commit 9cbfb58fde
39 changed files with 900 additions and 551 deletions

View File

@ -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

View File

@ -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: (
<UsersPage meID={meID} meCurrentOrganization={meCurrentOrganization} />
),
},
{
requiredRole: SUPERADMIN_ROLE,
type: ALL_USERS_TAB_NAME,
component: <AllUsersPage meID={meID} />,
},
{
requiredRole: SUPERADMIN_ROLE,
type: ORGANIZATIONS_TAB_NAME,
component: (
<OrganizationsPage meCurrentOrganization={meCurrentOrganization} />
),
},
{
requiredRole: SUPERADMIN_ROLE,
type: PROVIDERS_TAB_NAME,
component: <ProvidersPage />,
},
].filter(t => isUserAuthorized(meRole, t.requiredRole))
return (
<Tabs className="row">
<TabList customClass="col-md-2 admin-tabs">
{tabs.map((t, i) => <Tab key={tabs[i].type}>{tabs[i].type}</Tab>)}
</TabList>
<TabPanels customClass="col-md-10 admin-tabs--content">
{tabs.map((t, i) => (
<TabPanel key={tabs[i].type}>{t.component}</TabPanel>
))}
</TabPanels>
</Tabs>
)
}
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

View File

@ -2,10 +2,52 @@ import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import {connect} from 'react-redux' 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' 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: (
<UsersPage meID={me.id} meCurrentOrganization={me.currentOrganization} />
),
},
{
url: 'all-users',
name: 'All Users',
enabled: isUserAuthorized(me.role, SUPERADMIN_ROLE),
component: <AllUsersPage meID={me.id} />,
},
{
url: 'all-organizations',
name: 'All Orgs',
enabled: isUserAuthorized(me.role, SUPERADMIN_ROLE),
component: (
<OrganizationsPage meCurrentOrganization={me.currentOrganization} />
),
},
{
url: 'organization-mappings',
name: 'Org Mappings',
enabled: isUserAuthorized(me.role, SUPERADMIN_ROLE),
component: <ProvidersPage />,
},
]
const AdminChronografPage = ({me, source, params: {tab}}) => (
<div className="page"> <div className="page">
<div className="page-header"> <div className="page-header">
<div className="page-header__container"> <div className="page-header__container">
@ -16,9 +58,12 @@ const AdminChronografPage = ({me}) => (
</div> </div>
<FancyScrollbar className="page-contents"> <FancyScrollbar className="page-contents">
<div className="container-fluid"> <div className="container-fluid">
<div className="row"> <SubSections
<AdminTabs me={me} /> sections={sections(me)}
</div> activeSection={tab}
parentUrl="admin-chronograf"
sourceID={source.id}
/>
</div> </div>
</FancyScrollbar> </FancyScrollbar>
</div> </div>
@ -35,6 +80,15 @@ AdminChronografPage.propTypes = {
id: string.isRequired, id: string.isRequired,
}), }),
}).isRequired, }).isRequired,
params: shape({
tab: string,
}).isRequired,
source: shape({
id: string.isRequired,
links: shape({
users: string.isRequired,
}),
}).isRequired,
} }
const mapStateToProps = ({auth: {me}}) => ({ const mapStateToProps = ({auth: {me}}) => ({

View File

@ -159,7 +159,7 @@ export const orderTableColumns = (data, fieldOptions) => {
}) })
const filteredFieldSortOrder = filter(fieldsSortOrder, f => f !== -1) const filteredFieldSortOrder = filter(fieldsSortOrder, f => f !== -1)
const orderedData = map(data, row => { 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 : [[]] return orderedData[0].length ? orderedData : [[]]
} }

View File

@ -1,5 +1,4 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {bindActionCreators} from 'redux' import {bindActionCreators} from 'redux'
import {withRouter, InjectedRouter} from 'react-router' import {withRouter, InjectedRouter} from 'react-router'
@ -52,15 +51,6 @@ interface State {
@ErrorHandling @ErrorHandling
export class DataExplorer extends PureComponent<Props, State> { export class DataExplorer extends PureComponent<Props, State> {
public static childContextTypes = {
source: PropTypes.shape({
links: PropTypes.shape({
proxy: PropTypes.string.isRequired,
self: PropTypes.string.isRequired,
}).isRequired,
}).isRequired,
}
constructor(props) { constructor(props) {
super(props) super(props)

View File

@ -21,25 +21,33 @@ class BodyBuilder extends PureComponent<Props> {
return b.declarations.map(d => { return b.declarations.map(d => {
if (d.funcs) { if (d.funcs) {
return ( return (
<ExpressionNode <div key={b.id}>
id={d.id} <div className="func-node--name">{d.name} =</div>
key={d.id} <ExpressionNode
funcNames={this.funcNames} key={b.id}
funcs={d.funcs} bodyID={b.id}
/> declarationID={d.id}
funcNames={this.funcNames}
funcs={d.funcs}
/>
</div>
) )
} }
return <div key={b.id}>{b.source}</div> return (
<div className="func-node--name" key={b.id}>
{b.source}
</div>
)
}) })
} }
return ( return (
<ExpressionNode <ExpressionNode
id={b.id}
key={b.id} key={b.id}
funcNames={this.funcNames} bodyID={b.id}
funcs={b.funcs} funcs={b.funcs}
funcNames={this.funcNames}
/> />
) )
}) })

View File

@ -8,36 +8,37 @@ import {Func} from 'src/types/ifql'
interface Props { interface Props {
funcNames: any[] funcNames: any[]
id: string bodyID: string
funcs: Func[] funcs: Func[]
declarationID?: string
} }
// an Expression is a group of one or more functions // an Expression is a group of one or more functions
class ExpressionNode extends PureComponent<Props> { class ExpressionNode extends PureComponent<Props> {
public render() { public render() {
const {id, funcNames, funcs} = this.props const {declarationID, bodyID, funcNames, funcs} = this.props
return ( return (
<IFQLContext.Consumer> <IFQLContext.Consumer>
{({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => { {({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => {
return ( return (
<div className="func-nodes-container"> <div className="func-nodes-container">
<h4>
<FuncSelector
expressionID={id}
funcs={funcNames}
onAddNode={onAddNode}
/>
</h4>
{funcs.map(func => ( {funcs.map(func => (
<FuncNode <FuncNode
key={func.id} key={func.id}
func={func} func={func}
expressionID={func.id} bodyID={bodyID}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}
onDelete={onDeleteFuncNode} onDelete={onDeleteFuncNode}
declarationID={declarationID}
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
/> />
))} ))}
<FuncSelector
bodyID={bodyID}
funcs={funcNames}
onAddNode={onAddNode}
declarationID={declarationID}
/>
</div> </div>
) )
}} }}

View File

@ -9,7 +9,8 @@ interface Props {
funcID: string funcID: string
argKey: string argKey: string
value: string value: string
expressionID: string bodyID: string
declarationID: string
onChangeArg: OnChangeArg onChangeArg: OnChangeArg
} }
@ -56,12 +57,13 @@ class From extends PureComponent<Props, State> {
} }
private handleChooseDatabase = (item: DropdownItem): void => { private handleChooseDatabase = (item: DropdownItem): void => {
const {argKey, funcID, onChangeArg, expressionID} = this.props const {argKey, funcID, onChangeArg, bodyID, declarationID} = this.props
onChangeArg({ onChangeArg({
funcID, funcID,
key: argKey, key: argKey,
value: item.text, value: item.text,
expressionID, bodyID,
declarationID,
generate: true, generate: true,
}) })
} }

View File

@ -14,7 +14,8 @@ interface Props {
argKey: string argKey: string
value: string | boolean value: string | boolean
type: string type: string
expressionID: string bodyID: string
declarationID: string
onChangeArg: OnChangeArg onChangeArg: OnChangeArg
onGenerateScript: () => void onGenerateScript: () => void
} }
@ -26,10 +27,11 @@ class FuncArg extends PureComponent<Props> {
argKey, argKey,
value, value,
type, type,
funcName, bodyID,
funcID, funcID,
funcName,
onChangeArg, onChangeArg,
expressionID, declarationID,
onGenerateScript, onGenerateScript,
} = this.props } = this.props
@ -39,7 +41,8 @@ class FuncArg extends PureComponent<Props> {
argKey={argKey} argKey={argKey}
funcID={funcID} funcID={funcID}
value={this.value} value={this.value}
expressionID={expressionID} bodyID={bodyID}
declarationID={declarationID}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}
/> />
) )
@ -60,8 +63,9 @@ class FuncArg extends PureComponent<Props> {
value={this.value} value={this.value}
argKey={argKey} argKey={argKey}
funcID={funcID} funcID={funcID}
expressionID={expressionID} bodyID={bodyID}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
/> />
) )
@ -72,9 +76,10 @@ class FuncArg extends PureComponent<Props> {
<FuncArgBool <FuncArgBool
value={this.boolValue} value={this.boolValue}
argKey={argKey} argKey={argKey}
bodyID={bodyID}
funcID={funcID} funcID={funcID}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}
expressionID={expressionID} declarationID={declarationID}
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
/> />
) )

View File

@ -7,7 +7,8 @@ interface Props {
argKey: string argKey: string
value: boolean value: boolean
funcID: string funcID: string
expressionID: string bodyID: string
declarationID: string
onChangeArg: OnChangeArg onChangeArg: OnChangeArg
onGenerateScript: () => void onGenerateScript: () => void
} }
@ -23,8 +24,15 @@ class FuncArgBool extends PureComponent<Props> {
} }
private handleToggle = (value: boolean): void => { private handleToggle = (value: boolean): void => {
const {argKey, funcID, expressionID, onChangeArg} = this.props const {argKey, funcID, bodyID, onChangeArg, declarationID} = this.props
onChangeArg({funcID, key: argKey, value, generate: true, expressionID}) onChangeArg({
key: argKey,
value,
funcID,
bodyID,
declarationID,
generate: true,
})
} }
} }

View File

@ -7,7 +7,8 @@ interface Props {
argKey: string argKey: string
value: string value: string
type: string type: string
expressionID: string bodyID: string
declarationID: string
onChangeArg: OnChangeArg onChangeArg: OnChangeArg
onGenerateScript: () => void onGenerateScript: () => void
} }
@ -44,13 +45,14 @@ class FuncArgInput extends PureComponent<Props> {
} }
private handleChange = (e: ChangeEvent<HTMLInputElement>) => { private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const {funcID, argKey, expressionID} = this.props const {funcID, argKey, bodyID, declarationID} = this.props
this.props.onChangeArg({ this.props.onChangeArg({
funcID, funcID,
key: argKey, key: argKey,
value: e.target.value, value: e.target.value,
expressionID, declarationID,
bodyID,
}) })
} }
} }

View File

@ -6,15 +6,22 @@ import {Func} from 'src/types/ifql'
interface Props { interface Props {
func: Func func: Func
expressionID: string bodyID: string
onChangeArg: OnChangeArg onChangeArg: OnChangeArg
declarationID: string
onGenerateScript: () => void onGenerateScript: () => void
} }
@ErrorHandling @ErrorHandling
export default class FuncArgs extends PureComponent<Props> { export default class FuncArgs extends PureComponent<Props> {
public render() { public render() {
const {expressionID, func, onChangeArg, onGenerateScript} = this.props const {
func,
bodyID,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
return ( return (
<div className="func-args"> <div className="func-args">
@ -25,10 +32,11 @@ export default class FuncArgs extends PureComponent<Props> {
type={type} type={type}
argKey={key} argKey={key}
value={value} value={value}
bodyID={bodyID}
funcID={func.id} funcID={func.id}
funcName={func.name} funcName={func.name}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}
expressionID={expressionID} declarationID={declarationID}
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
/> />
) )

View File

@ -1,12 +1,13 @@
import React, {PureComponent, MouseEvent} from 'react' import React, {PureComponent, MouseEvent} from 'react'
import FuncArgs from 'src/ifql/components/FuncArgs' 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' import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props { interface Props {
func: Func func: Func
expressionID: string bodyID: string
onDelete: (funcID: string, expressionID: string) => void declarationID?: string
onDelete: OnDeleteFuncNode
onChangeArg: OnChangeArg onChangeArg: OnChangeArg
onGenerateScript: () => void onGenerateScript: () => void
} }
@ -17,6 +18,10 @@ interface State {
@ErrorHandling @ErrorHandling
export default class FuncNode extends PureComponent<Props, State> { export default class FuncNode extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
declarationID: '',
}
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -25,7 +30,13 @@ export default class FuncNode extends PureComponent<Props, State> {
} }
public render() { public render() {
const {expressionID, func, onChangeArg, onGenerateScript} = this.props const {
func,
bodyID,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
const {isOpen} = this.state const {isOpen} = this.state
return ( return (
@ -36,8 +47,9 @@ export default class FuncNode extends PureComponent<Props, State> {
{isOpen && ( {isOpen && (
<FuncArgs <FuncArgs
func={func} func={func}
bodyID={bodyID}
onChangeArg={onChangeArg} onChangeArg={onChangeArg}
expressionID={expressionID} declarationID={declarationID}
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
/> />
)} )}
@ -49,7 +61,9 @@ export default class FuncNode extends PureComponent<Props, State> {
} }
private handleDelete = (): void => { 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<HTMLElement>): void => { private handleClick = (e: MouseEvent<HTMLElement>): void => {

View File

@ -14,7 +14,8 @@ interface State {
interface Props { interface Props {
funcs: string[] funcs: string[]
expressionID: string bodyID: string
declarationID: string
onAddNode: OnAddNode onAddNode: OnAddNode
} }
@ -65,8 +66,9 @@ export class FuncSelector extends PureComponent<Props, State> {
} }
private handleAddNode = (name: string) => { private handleAddNode = (name: string) => {
const {bodyID, declarationID} = this.props
this.handleCloseList() this.handleCloseList()
this.props.onAddNode(name, this.props.expressionID) this.props.onAddNode(name, bodyID, declarationID)
} }
private get availableFuncs() { private get availableFuncs() {

View File

@ -1,11 +1,12 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import _ from 'lodash'
import TimeMachine from 'src/ifql/components/TimeMachine' import TimeMachine from 'src/ifql/components/TimeMachine'
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts' import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import {Suggestion, FlatBody} from 'src/types/ifql' 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 {bodyNodes} from 'src/ifql/helpers'
import {getSuggestions, getAST} from 'src/ifql/apis' import {getSuggestions, getAST} from 'src/ifql/apis'
@ -44,7 +45,7 @@ export class IFQLPage extends PureComponent<Props, State> {
ast: null, ast: null,
suggestions: [], suggestions: [],
script: 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<Props, State> {
} }
private handleGenerateScript = (): void => { private handleGenerateScript = (): void => {
this.getASTResponse(this.expressionsToScript) this.getASTResponse(this.bodyToScript)
} }
private handleChangeArg = ({ private handleChangeArg = ({
@ -116,30 +117,41 @@ export class IFQLPage extends PureComponent<Props, State> {
value, value,
generate, generate,
funcID, funcID,
expressionID, declarationID = '',
bodyID,
}: InputArg): void => { }: InputArg): void => {
const body = this.state.body.map(expression => { const body = this.state.body.map(b => {
if (expression.id !== expressionID) { if (b.id !== bodyID) {
return expression return b
} }
const funcs = expression.funcs.map(f => { if (declarationID) {
if (f.id !== funcID) { const declarations = b.declarations.map(d => {
return f if (d.id !== declarationID) {
} return d
const args = f.args.map(a => {
if (a.key === key) {
return {...a, value}
} }
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}, () => { this.setState({body}, () => {
@ -149,9 +161,42 @@ export class IFQLPage extends PureComponent<Props, State> {
}) })
} }
private get expressionsToScript(): string { private editFuncArgs = ({funcs, funcID, key, value}): Func[] => {
return this.state.body.reduce((acc, expression) => { return funcs.map(f => {
return `${acc + this.funcsToScript(expression.funcs)}\n\n` 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<Props, State> {
this.setState({script}) this.setState({script})
} }
private handleAddNode = (name: string, expressionID: string): void => { private handleAddNode = (
const script = this.state.body.reduce((acc, expression) => { name: string,
if (expression.id === expressionID) { bodyID: string,
const {funcs} = expression declarationID: string
return `${acc}${this.funcsToScript(funcs)}\n\t|> ${name}()\n\n` ): 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) this.getASTResponse(script)
} }
private handleDeleteFuncNode = ( private appendFunc = (funcs, name): string => {
funcID: string, return `${this.funcsToScript(funcs)}\n\t|> ${name}()\n\n`
expressionID: string }
): void => {
// TODO: export this and test functionality private handleDeleteFuncNode = (ids: DeleteFuncNodeArgs): void => {
const {funcID, declarationID = '', bodyID} = ids
const script = this.state.body const script = this.state.body
.map((expression, expressionIndex) => { .map((body, bodyIndex) => {
if (expression.id !== expressionID) { if (body.id !== bodyID) {
return expression.source return this.formatSource(body.source)
} }
const funcs = expression.funcs.filter(f => f.id !== funcID) const isLast = bodyIndex === this.state.body.length - 1
const source = funcs.reduce((acc, f, i) => {
if (i === 0) { if (declarationID) {
return `${f.source}` const declaration = body.declarations.find(
d => d.id === declarationID
)
if (!declaration) {
return
} }
return `${acc}\n\t${f.source}` const functions = declaration.funcs.filter(f => f.id !== funcID)
}, '') const s = this.funcsToSource(functions)
return `${declaration.name} = ${this.formatLastSource(s, isLast)}`
const isLast = expressionIndex === this.state.body.length - 1
if (isLast) {
return `${source}`
} }
return `${source}\n\n` const funcs = body.funcs.filter(f => f.id !== funcID)
const source = this.funcsToSource(funcs)
return this.formatLastSource(source, isLast)
}) })
.join('') .join('')
this.getASTResponse(script) 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) => { private getASTResponse = async (script: string) => {
const {links} = this.props const {links} = this.props

View File

@ -146,7 +146,10 @@ class Root extends PureComponent<{}, State> {
path="kapacitors/:id/edit:hash" path="kapacitors/:id/edit:hash"
component={KapacitorPage} component={KapacitorPage}
/> />
<Route path="admin-chronograf" component={AdminChronografPage} /> <Route
path="admin-chronograf/:tab"
component={AdminChronografPage}
/>
<Route path="admin-influxdb/:tab" component={AdminInfluxDBPage} /> <Route path="admin-influxdb/:tab" component={AdminInfluxDBPage} />
<Route path="manage-sources" component={ManageSources} /> <Route path="manage-sources" component={ManageSources} />
<Route path="manage-sources/new" component={SourcePage} /> <Route path="manage-sources/new" component={SourcePage} />

View File

@ -170,7 +170,7 @@ class AlertTabs extends Component {
const showDeprecation = pagerDutyV1Enabled const showDeprecation = pagerDutyV1Enabled
const pagerDutyDeprecationMessage = ( const pagerDutyDeprecationMessage = (
<div> <div>
PagerDuty v2 is being{' '} PagerDuty v1 is being{' '}
{ {
<a <a
href="https://v2.developer.pagerduty.com/docs/v1-rest-api-decommissioning-faq" href="https://v2.developer.pagerduty.com/docs/v1-rest-api-decommissioning-faq"

View File

@ -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)
}

View File

@ -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 (
<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

View File

@ -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

View File

@ -41,7 +41,7 @@ class AutoRefreshDropdown extends Component {
paused: +milliseconds === 0, paused: +milliseconds === 0,
})} })}
> >
<div className={classnames('dropdown dropdown-160', {open: isOpen})}> <div className={classnames('dropdown dropdown-120', {open: isOpen})}>
<div <div
className="btn btn-sm btn-default dropdown-toggle" className="btn btn-sm btn-default dropdown-toggle"
onClick={this.toggleMenu} onClick={this.toggleMenu}
@ -56,7 +56,7 @@ class AutoRefreshDropdown extends Component {
<span className="caret" /> <span className="caret" />
</div> </div>
<ul className="dropdown-menu"> <ul className="dropdown-menu">
<li className="dropdown-header">AutoRefresh Interval</li> <li className="dropdown-header">AutoRefresh</li>
{autoRefreshItems.map(item => ( {autoRefreshItems.map(item => (
<li className="dropdown-item" key={item.menuOption}> <li className="dropdown-item" key={item.menuOption}>
<a href="#" onClick={this.handleSelection(item.milliseconds)}> <a href="#" onClick={this.handleSelection(item.milliseconds)}>

View File

@ -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 {

View File

@ -0,0 +1,16 @@
import React, {PureComponent} from 'react'
class InvalidData extends PureComponent<{}> {
public render() {
return (
<div className="graph-empty">
<p>
The data returned from the query can't be visualized with this graph
type.<br />Try updating the query or selecting a different graph type.
</p>
</div>
)
}
}
export default InvalidData

View File

@ -1,3 +1,4 @@
import _ from 'lodash'
import React, {Component} from 'react' import React, {Component} from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import Dygraph from 'shared/components/Dygraph' import Dygraph from 'shared/components/Dygraph'
@ -6,17 +7,34 @@ import SingleStat from 'src/shared/components/SingleStat'
import {timeSeriesToDygraph} from 'utils/timeSeriesTransformers' import {timeSeriesToDygraph} from 'utils/timeSeriesTransformers'
import {colorsStringSchema} from 'shared/schemas' import {colorsStringSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandlingWith} from 'src/shared/decorators/errors'
import InvalidData from 'src/shared/components/InvalidData'
@ErrorHandling const validateTimeSeries = timeseries => {
return _.every(timeseries, r =>
_.every(
r,
(v, i) => (i === 0 && Date.parse(v)) || _.isNumber(v) || _.isNull(v)
)
)
}
@ErrorHandlingWith(InvalidData)
class LineGraph extends Component { class LineGraph extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.isValidData = true
} }
componentWillMount() { componentWillMount() {
const {data, isInDataExplorer} = this.props const {data, isInDataExplorer} = this.props
this.parseTimeSeries(data, isInDataExplorer)
}
parseTimeSeries(data, isInDataExplorer) {
this._timeSeries = timeSeriesToDygraph(data, isInDataExplorer) this._timeSeries = timeSeriesToDygraph(data, isInDataExplorer)
this.isValidData = validateTimeSeries(
_.get(this._timeSeries, 'timeSeries', [])
)
} }
componentWillUpdate(nextProps) { componentWillUpdate(nextProps) {
@ -25,14 +43,15 @@ class LineGraph extends Component {
data !== nextProps.data || data !== nextProps.data ||
activeQueryIndex !== nextProps.activeQueryIndex activeQueryIndex !== nextProps.activeQueryIndex
) { ) {
this._timeSeries = timeSeriesToDygraph( this.parseTimeSeries(nextProps.data, nextProps.isInDataExplorer)
nextProps.data,
nextProps.isInDataExplorer
)
} }
} }
render() { render() {
if (!this.isValidData) {
return <InvalidData />
}
const { const {
data, data,
axes, axes,

View File

@ -65,6 +65,7 @@ const RefreshingGraph = ({
templates={templates} templates={templates}
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
cellHeight={cellHeight} cellHeight={cellHeight}
editQueryStatus={editQueryStatus}
prefix={prefix} prefix={prefix}
suffix={suffix} suffix={suffix}
inView={inView} inView={inView}
@ -83,6 +84,7 @@ const RefreshingGraph = ({
autoRefresh={autoRefresh} autoRefresh={autoRefresh}
cellHeight={cellHeight} cellHeight={cellHeight}
resizerTopHeight={resizerTopHeight} resizerTopHeight={resizerTopHeight}
editQueryStatus={editQueryStatus}
resizeCoords={resizeCoords} resizeCoords={resizeCoords}
cellID={cellID} cellID={cellID}
prefix={prefix} prefix={prefix}
@ -110,6 +112,7 @@ const RefreshingGraph = ({
fieldOptions={fieldOptions} fieldOptions={fieldOptions}
timeFormat={timeFormat} timeFormat={timeFormat}
decimalPlaces={decimalPlaces} decimalPlaces={decimalPlaces}
editQueryStatus={editQueryStatus}
resizerTopHeight={resizerTopHeight} resizerTopHeight={resizerTopHeight}
handleSetHoverTime={handleSetHoverTime} handleSetHoverTime={handleSetHoverTime}
isInCEO={isInCEO} isInCEO={isInCEO}

View File

@ -191,14 +191,14 @@ class TableGraph extends Component {
} }
} }
handleClickFieldName = fieldName => () => { handleClickFieldName = clickedFieldName => () => {
const {tableOptions, fieldOptions, timeFormat, decimalPlaces} = this.props
const {data, sort} = this.state const {data, sort} = this.state
const {tableOptions, timeFormat, fieldOptions, decimalPlaces} = this.props
if (fieldName === sort.field) { if (clickedFieldName === sort.field) {
sort.direction = sort.direction === ASCENDING ? DESCENDING : ASCENDING sort.direction = sort.direction === ASCENDING ? DESCENDING : ASCENDING
} else { } else {
sort.field = fieldName sort.field = clickedFieldName
sort.direction = DEFAULT_SORT_DIRECTION sort.direction = DEFAULT_SORT_DIRECTION
} }
@ -430,12 +430,12 @@ class TableGraph extends Component {
enableFixedRowScroll={true} enableFixedRowScroll={true}
scrollToRow={scrollToRow} scrollToRow={scrollToRow}
scrollToColumn={scrollToColumn} scrollToColumn={scrollToColumn}
sort={sort}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
hoveredColumnIndex={hoveredColumnIndex} hoveredColumnIndex={hoveredColumnIndex}
hoveredRowIndex={hoveredRowIndex} hoveredRowIndex={hoveredRowIndex}
hoverTime={hoverTime} hoverTime={hoverTime}
colors={colors} colors={colors}
sort={sort}
fieldOptions={fieldOptions} fieldOptions={fieldOptions}
tableOptions={tableOptions} tableOptions={tableOptions}
timeFormat={timeFormat} timeFormat={timeFormat}

View File

@ -85,7 +85,7 @@ class TimeRangeDropdown extends Component {
<div className="time-range-dropdown"> <div className="time-range-dropdown">
<div <div
className={classnames('dropdown', { className={classnames('dropdown', {
'dropdown-160': isRelativeTimeRange, 'dropdown-120': isRelativeTimeRange,
'dropdown-210': isNow, 'dropdown-210': isNow,
'dropdown-290': !isRelativeTimeRange && !isNow, 'dropdown-290': !isRelativeTimeRange && !isNow,
open: isOpen, open: isOpen,
@ -109,7 +109,7 @@ class TimeRangeDropdown extends Component {
> >
{preventCustomTimeRange ? null : ( {preventCustomTimeRange ? null : (
<div> <div>
<li className="dropdown-header">Absolute Time Ranges</li> <li className="dropdown-header">Absolute Time</li>
<li <li
className={ className={
isCustomTimeRangeOpen isCustomTimeRangeOpen
@ -118,13 +118,13 @@ class TimeRangeDropdown extends Component {
} }
> >
<a href="#" onClick={this.showCustomTimeRange}> <a href="#" onClick={this.showCustomTimeRange}>
Custom Date Picker Date Picker
</a> </a>
</li> </li>
</div> </div>
)} )}
<li className="dropdown-header"> <li className="dropdown-header">
{preventCustomTimeRange ? '' : 'Relative '}Time Ranges {preventCustomTimeRange ? '' : 'Relative '}Time
</li> </li>
{timeRanges.map(item => { {timeRanges.map(item => {
return ( return (

View File

@ -0,0 +1,7 @@
export const DEFAULT_TIME_SERIES = [
{
response: {
results: [],
},
},
]

View File

@ -2,28 +2,28 @@ const autoRefreshItems = [
{milliseconds: 0, inputValue: 'Paused', menuOption: 'Paused'}, {milliseconds: 0, inputValue: 'Paused', menuOption: 'Paused'},
{ {
milliseconds: 5000, milliseconds: 5000,
inputValue: 'Every 5 seconds', inputValue: 'Every 5s',
menuOption: 'Every 5 seconds', menuOption: 'Every 5s',
}, },
{ {
milliseconds: 10000, milliseconds: 10000,
inputValue: 'Every 10 seconds', inputValue: 'Every 10s',
menuOption: 'Every 10 seconds', menuOption: 'Every 10s',
}, },
{ {
milliseconds: 15000, milliseconds: 15000,
inputValue: 'Every 15 seconds', inputValue: 'Every 15s',
menuOption: 'Every 15 seconds', menuOption: 'Every 15s',
}, },
{ {
milliseconds: 30000, milliseconds: 30000,
inputValue: 'Every 30 seconds', inputValue: 'Every 30s',
menuOption: 'Every 30 seconds', menuOption: 'Every 30s',
}, },
{ {
milliseconds: 60000, milliseconds: 60000,
inputValue: 'Every 60 seconds', inputValue: 'Every 60s',
menuOption: 'Every 60 seconds', menuOption: 'Every 60s',
}, },
] ]

View File

@ -2,73 +2,73 @@ export const timeRanges = [
{ {
defaultGroupBy: '10s', defaultGroupBy: '10s',
seconds: 300, seconds: 300,
inputValue: 'Past 5 minutes', inputValue: 'Past 5m',
lower: 'now() - 5m', lower: 'now() - 5m',
upper: null, upper: null,
menuOption: 'Past 5 minutes', menuOption: 'Past 5m',
}, },
{ {
defaultGroupBy: '1m', defaultGroupBy: '1m',
seconds: 900, seconds: 900,
inputValue: 'Past 15 minutes', inputValue: 'Past 15m',
lower: 'now() - 15m', lower: 'now() - 15m',
upper: null, upper: null,
menuOption: 'Past 15 minutes', menuOption: 'Past 15m',
}, },
{ {
defaultGroupBy: '1m', defaultGroupBy: '1m',
seconds: 3600, seconds: 3600,
inputValue: 'Past hour', inputValue: 'Past 1h',
lower: 'now() - 1h', lower: 'now() - 1h',
upper: null, upper: null,
menuOption: 'Past hour', menuOption: 'Past 1h',
}, },
{ {
defaultGroupBy: '1m', defaultGroupBy: '1m',
seconds: 21600, seconds: 21600,
inputValue: 'Past 6 hours', inputValue: 'Past 6h',
lower: 'now() - 6h', lower: 'now() - 6h',
upper: null, upper: null,
menuOption: 'Past 6 hours', menuOption: 'Past 6h',
}, },
{ {
defaultGroupBy: '5m', defaultGroupBy: '5m',
seconds: 43200, seconds: 43200,
inputValue: 'Past 12 hours', inputValue: 'Past 12h',
lower: 'now() - 12h', lower: 'now() - 12h',
upper: null, upper: null,
menuOption: 'Past 12 hours', menuOption: 'Past 12h',
}, },
{ {
defaultGroupBy: '10m', defaultGroupBy: '10m',
seconds: 86400, seconds: 86400,
inputValue: 'Past 24 hours', inputValue: 'Past 24h',
lower: 'now() - 24h', lower: 'now() - 24h',
upper: null, upper: null,
menuOption: 'Past 24 hours', menuOption: 'Past 24h',
}, },
{ {
defaultGroupBy: '30m', defaultGroupBy: '30m',
seconds: 172800, seconds: 172800,
inputValue: 'Past 2 days', inputValue: 'Past 2d',
lower: 'now() - 2d', lower: 'now() - 2d',
upper: null, upper: null,
menuOption: 'Past 2 days', menuOption: 'Past 2d',
}, },
{ {
defaultGroupBy: '1h', defaultGroupBy: '1h',
seconds: 604800, seconds: 604800,
inputValue: 'Past 7 days', inputValue: 'Past 7d',
lower: 'now() - 7d', lower: 'now() - 7d',
upper: null, upper: null,
menuOption: 'Past 7 days', menuOption: 'Past 7d',
}, },
{ {
defaultGroupBy: '6h', defaultGroupBy: '6h',
seconds: 2592000, seconds: 2592000,
inputValue: 'Past 30 days', inputValue: 'Past 30d',
lower: 'now() - 30d', lower: 'now() - 30d',
upper: null, upper: null,
menuOption: 'Past 30 days', menuOption: 'Past 30d',
}, },
] ]

View File

@ -122,14 +122,16 @@ class SideNav extends PureComponent<Props> {
<NavBlock <NavBlock
highlightWhen={['admin-chronograf', 'admin-influxdb']} highlightWhen={['admin-chronograf', 'admin-influxdb']}
icon="crown2" icon="crown2"
link={`${sourcePrefix}/admin-chronograf`} link={`${sourcePrefix}/admin-chronograf/current-organization`}
location={location} location={location}
> >
<NavHeader <NavHeader
link={`${sourcePrefix}/admin-chronograf`} link={`${sourcePrefix}/admin-chronograf/current-organization`}
title="Admin" title="Admin"
/> />
<NavListItem link={`${sourcePrefix}/admin-chronograf`}> <NavListItem
link={`${sourcePrefix}/admin-chronograf/current-organization`}
>
Chronograf Chronograf
</NavListItem> </NavListItem>
<NavListItem link={`${sourcePrefix}/admin-influxdb/databases`}> <NavListItem link={`${sourcePrefix}/admin-influxdb/databases`}>

View File

@ -14,7 +14,6 @@
width: auto; width: auto;
display: flex; display: flex;
color: $ix-text-default; color: $ix-text-default;
text-transform: uppercase;
margin-bottom: $ix-marg-a; margin-bottom: $ix-marg-a;
font-family: $ix-text-font; font-family: $ix-text-font;
font-weight: 500; font-weight: 500;

View File

@ -1,7 +1,11 @@
// function definitions // function definitions
export type OnDeleteFuncNode = (funcID: string, expressionID: string) => void export type OnDeleteFuncNode = (ids: DeleteFuncNodeArgs) => void
export type OnChangeArg = (inputArg: InputArg) => 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 OnGenerateScript = (script: string) => void
export type OnChangeScript = (script: string) => void export type OnChangeScript = (script: string) => void
export type OnSubmitScript = () => void export type OnSubmitScript = () => void
@ -15,9 +19,16 @@ export interface Handlers {
onGenerateScript: OnGenerateScript onGenerateScript: OnGenerateScript
} }
export interface DeleteFuncNodeArgs {
funcID: string
bodyID: string
declarationID?: string
}
export interface InputArg { export interface InputArg {
funcID: string funcID: string
expressionID: string bodyID: string
declarationID?: string
key: string key: string
value: string | boolean value: string | boolean
generate?: boolean generate?: boolean

20
ui/src/types/series.ts Normal file
View File

@ -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
}

View File

@ -2,8 +2,11 @@ import _ from 'lodash'
import {shiftDate} from 'shared/query/helpers' import {shiftDate} from 'shared/query/helpers'
import {map, reduce, forEach, concat, clone} from 'fast.js' import {map, reduce, forEach, concat, clone} from 'fast.js'
const groupByMap = (responses, responseIndex, groupByColumns) => { const groupByMap = (results, responseIndex, groupByColumns) => {
const firstColumns = _.get(responses, [0, 'series', 0, 'columns']) if (_.isEmpty(results)) {
return []
}
const firstColumns = _.get(results, [0, 'series', 0, 'columns'])
const accum = [ const accum = [
{ {
responseIndex, responseIndex,
@ -15,14 +18,14 @@ const groupByMap = (responses, responseIndex, groupByColumns) => {
...firstColumns.slice(1), ...firstColumns.slice(1),
], ],
groupByColumns, groupByColumns,
name: _.get(responses, [0, 'series', 0, 'name']), name: _.get(results, [0, 'series', 0, 'name'], ''),
values: [], values: [],
}, },
], ],
}, },
] ]
const seriesArray = _.get(responses, [0, 'series']) const seriesArray = _.get(results, [0, 'series'])
seriesArray.forEach(s => { seriesArray.forEach(s => {
const prevValues = accum[0].series[0].values const prevValues = accum[0].series[0].values
const tagsToAdd = groupByColumns.map(gb => s.tags[gb]) const tagsToAdd = groupByColumns.map(gb => s.tags[gb])
@ -35,13 +38,14 @@ const groupByMap = (responses, responseIndex, groupByColumns) => {
const constructResults = (raw, groupBys) => { const constructResults = (raw, groupBys) => {
return _.flatten( return _.flatten(
map(raw, (response, index) => { 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]) { if (groupBys[index]) {
return groupByMap(responses, index, groupBys[index]) return groupByMap(successfulResults, index, groupBys[index])
} }
return map(successfulResults, r => ({...r, responseIndex: index}))
return map(responses, r => ({...r, responseIndex: index}))
}) })
) )
} }
@ -81,22 +85,24 @@ const constructCells = serieses => {
name: measurement, name: measurement,
columns, columns,
groupByColumns, groupByColumns,
values, values = [],
seriesIndex, seriesIndex,
responseIndex, responseIndex,
tags = {}, tags = {},
}, },
ind ind
) => { ) => {
const rows = map(values || [], vals => ({ const rows = map(values, vals => ({vals}))
vals,
})) const tagSet = map(Object.keys(tags), tag => `[${tag}=${tags[tag]}]`)
.sort()
.join('')
const unsortedLabels = map(columns.slice(1), (field, i) => ({ const unsortedLabels = map(columns.slice(1), (field, i) => ({
label: label:
groupByColumns && i <= groupByColumns.length - 1 groupByColumns && i <= groupByColumns.length - 1
? `${field}` ? `${field}`
: `${measurement}.${field}`, : `${measurement}.${field}${tagSet}`,
responseIndex, responseIndex,
seriesIndex, seriesIndex,
})) }))
@ -221,8 +227,8 @@ export const groupByTimeSeriesTransform = (raw, groupBys) => {
if (!groupBys) { if (!groupBys) {
groupBys = Array(raw.length).fill(false) groupBys = Array(raw.length).fill(false)
} }
const results = constructResults(raw, groupBys)
const results = constructResults(raw, groupBys)
const serieses = constructSerieses(results) const serieses = constructSerieses(results)
const {cells, sortedLabels, seriesLabels} = constructCells(serieses) const {cells, sortedLabels, seriesLabels} = constructCells(serieses)

View File

@ -9,7 +9,8 @@ const setup = () => {
funcID: '1', funcID: '1',
argKey: 'db', argKey: 'db',
value: 'db1', value: 'db1',
expressionID: '2', bodyID: '2',
declarationID: '1',
onChangeArg: () => {}, onChangeArg: () => {},
} }

View File

@ -5,8 +5,9 @@ import FuncArg from 'src/ifql/components/FuncArg'
const setup = () => { const setup = () => {
const props = { const props = {
funcID: '', funcID: '',
expressionID: '', bodyID: '',
funcName: '', funcName: '',
declarationID: '',
argKey: '', argKey: '',
value: '', value: '',
type: '', type: '',

View File

@ -8,7 +8,8 @@ import FuncList from 'src/ifql/components/FuncList'
const setup = (override = {}) => { const setup = (override = {}) => {
const props = { const props = {
funcs: ['count', 'range'], funcs: ['count', 'range'],
expressionID: '1', bodyID: '1',
declarationID: '2',
onAddNode: () => {}, onAddNode: () => {},
...override, ...override,
} }
@ -133,7 +134,7 @@ describe('IFQL.Components.FuncsButton', () => {
const onAddNode = jest.fn() const onAddNode = jest.fn()
const {wrapper, props} = setup({onAddNode}) const {wrapper, props} = setup({onAddNode})
const [, func2] = props.funcs const [, func2] = props.funcs
const {expressionID} = props const {bodyID, declarationID} = props
const dropdownButton = wrapper.find('button') const dropdownButton = wrapper.find('button')
dropdownButton.simulate('click') dropdownButton.simulate('click')
@ -148,7 +149,7 @@ describe('IFQL.Components.FuncsButton', () => {
input.simulate('keyDown', {key: 'ArrowDown'}) input.simulate('keyDown', {key: 'ArrowDown'})
input.simulate('keyDown', {key: 'Enter'}) input.simulate('keyDown', {key: 'Enter'})
expect(onAddNode).toHaveBeenCalledWith(func2, expressionID) expect(onAddNode).toHaveBeenCalledWith(func2, bodyID, declarationID)
}) })
}) })
}) })

View File

@ -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)
})
})
})
})
})