Merge pull request #3519 from influxdata/ifql-table

Ifql table
pull/3574/head
Andrew Watkins 2018-05-24 14:24:48 -07:00 committed by GitHub
commit cd48790f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2415 additions and 1467 deletions

View File

@ -41,6 +41,7 @@
"@types/jest": "^22.1.4", "@types/jest": "^22.1.4",
"@types/lodash": "^4.14.104", "@types/lodash": "^4.14.104",
"@types/node": "^9.4.6", "@types/node": "^9.4.6",
"@types/papaparse": "^4.1.34",
"@types/prop-types": "^15.5.2", "@types/prop-types": "^15.5.2",
"@types/react": "^16.0.38", "@types/react": "^16.0.38",
"@types/react-dnd": "^2.0.36", "@types/react-dnd": "^2.0.36",
@ -133,6 +134,7 @@
"lodash": "^4.3.0", "lodash": "^4.3.0",
"moment": "^2.13.0", "moment": "^2.13.0",
"nano-date": "^2.0.1", "nano-date": "^2.0.1",
"papaparse": "^4.4.0",
"prop-types": "^15.6.1", "prop-types": "^15.6.1",
"query-string": "^5.0.0", "query-string": "^5.0.0",
"react": "^16.3.1", "react": "^16.3.1",

View File

@ -1,6 +1,9 @@
import _ from 'lodash'
import AJAX from 'src/utils/ajax' import AJAX from 'src/utils/ajax'
import {Service} from 'src/types' import {Service, ScriptResult} from 'src/types'
import {updateService} from 'src/shared/apis' import {updateService} from 'src/shared/apis'
import {parseResults} from 'src/shared/parsing/ifql'
export const getSuggestions = async (url: string) => { export const getSuggestions = async (url: string) => {
try { try {
@ -36,24 +39,28 @@ export const getAST = async (request: ASTRequest) => {
} }
} }
export const getTimeSeries = async (service: Service, script: string) => { export const getTimeSeries = async (
service: Service,
script: string
): Promise<ScriptResult[]> => {
const and = encodeURIComponent('&') const and = encodeURIComponent('&')
const mark = encodeURIComponent('?') const mark = encodeURIComponent('?')
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
try { try {
const data = await AJAX({ const {data} = await AJAX({
method: 'POST', method: 'POST',
url: `${ url: `${
service.links.proxy service.links.proxy
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`, }?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
headers: {'Content-Type': 'text/plain'},
}) })
return data return parseResults(data)
} catch (error) { } catch (error) {
console.error('Problem fetching data', error) console.error('Problem fetching data', error)
throw error.data.message
throw _.get(error, 'headers.x-influx-error', false) ||
_.get(error, 'data.message', 'unknown error 🤷')
} }
} }

View File

@ -0,0 +1,9 @@
import React, {SFC} from 'react'
const NoResult: SFC = () => (
<div className="graph-empty">
<p>No Results</p>
</div>
)
export default NoResult

View File

@ -0,0 +1,58 @@
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {ScriptResult} from 'src/types'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import TableSidebarItem from 'src/ifql/components/TableSidebarItem'
import {vis} from 'src/ifql/constants'
interface Props {
data: ScriptResult[]
selectedResultID: string
onSelectResult: (id: string) => void
}
@ErrorHandling
export default class TableSidebar extends PureComponent<Props> {
public render() {
const {data, selectedResultID, onSelectResult} = this.props
return (
<div className="time-machine--sidebar">
{!this.isDataEmpty && (
<div className="query-builder--heading" style={this.headingStyle}>
Results
</div>
)}
<FancyScrollbar>
<div className="time-machine-vis--sidebar query-builder--list">
{data.map(({name, id}) => {
return (
<TableSidebarItem
id={id}
key={id}
name={name}
onSelect={onSelectResult}
isSelected={id === selectedResultID}
/>
)
})}
</div>
</FancyScrollbar>
</div>
)
}
get headingStyle(): CSSProperties {
return {
height: `${vis.TABLE_ROW_HEIGHT + 2.5}px`,
backgroundColor: '#31313d',
borderBottom: '2px solid #383846', // $g5-pepper
}
}
get isDataEmpty(): boolean {
return _.isEmpty(this.props.data)
}
}

View File

@ -0,0 +1,36 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
name: string
id: string
isSelected: boolean
onSelect: (id: string) => void
}
@ErrorHandling
export default class TableSidebarItem extends PureComponent<Props> {
public render() {
return (
<div
className={`query-builder--list-item ${this.active}`}
onClick={this.handleClick}
>
{this.props.name}
</div>
)
}
private get active(): string {
if (this.props.isSelected) {
return 'active'
}
return ''
}
private handleClick = (): void => {
this.props.onSelect(this.props.id)
}
}

View File

@ -9,16 +9,17 @@ import {
OnChangeScript, OnChangeScript,
OnSubmitScript, OnSubmitScript,
FlatBody, FlatBody,
Status, ScriptStatus,
ScriptResult,
} from 'src/types/ifql' } from 'src/types/ifql'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants' import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
interface Props { interface Props {
data: string data: ScriptResult[]
script: string script: string
body: Body[] body: Body[]
status: Status status: ScriptStatus
suggestions: Suggestion[] suggestions: Suggestion[]
onChangeScript: OnChangeScript onChangeScript: OnChangeScript
onSubmitScript: OnSubmitScript onSubmitScript: OnSubmitScript

View File

@ -0,0 +1,65 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {ScriptResult} from 'src/types'
import {vis} from 'src/ifql/constants'
@ErrorHandling
export default class TimeMachineTable extends PureComponent<ScriptResult> {
public render() {
const {data} = this.props
return (
<div style={{flex: '1 1 auto'}}>
<AutoSizer>
{({height, width}) => (
<ColumnSizer
width={width}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
columnCount={this.columnCount}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
cellRenderer={this.cellRenderer}
columnCount={this.columnCount}
columnWidth={getColumnWidth}
height={height}
rowCount={data.length}
rowHeight={vis.TABLE_ROW_HEIGHT}
width={adjustedWidth}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
</div>
)
}
private get columnCount(): number {
return _.get(this.props.data, '0', []).length
}
private cellRenderer = ({
columnIndex,
key,
rowIndex,
style,
}: GridCellProps): React.ReactNode => {
const {data} = this.props
const headerRowClass = !rowIndex ? 'table-graph-cell__fixed-row' : ''
return (
<div
key={key}
style={style}
className={`table-graph-cell ${headerRowClass}`}
>
{data[rowIndex][columnIndex]}
</div>
)
}
}

View File

@ -1,40 +1,79 @@
import React, {PureComponent} from 'react' import React, {PureComponent, CSSProperties} from 'react'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import {ScriptResult} from 'src/types'
import TableSidebar from 'src/ifql/components/TableSidebar'
import TimeMachineTable from 'src/ifql/components/TimeMachineTable'
import {HANDLE_PIXELS} from 'src/shared/constants'
import NoResults from 'src/ifql/components/NoResults'
interface Props { interface Props {
data: string data: ScriptResult[]
}
interface State {
selectedResultID: string | null
} }
@ErrorHandling @ErrorHandling
class TimeMachineVis extends PureComponent<Props> { class TimeMachineVis extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {selectedResultID: this.initialResultID}
}
public componentDidUpdate(__, prevState) {
if (prevState.selectedResultID === null) {
this.setState({selectedResultID: this.initialResultID})
}
}
public render() { public render() {
return ( return (
<div className="time-machine-visualization"> <div className="time-machine-visualization" style={this.style}>
<div className="time-machine--graph"> {this.hasResults && (
<FancyScrollbar> <TableSidebar
<div className="time-machine--graph-body"> data={this.props.data}
{this.data.map((d, i) => { selectedResultID={this.state.selectedResultID}
return ( onSelectResult={this.handleSelectResult}
<div key={i} className="data-row"> />
{d} )}
</div> <div className="time-machine--vis">
) {this.shouldShowTable && (
})} <TimeMachineTable {...this.selectedResult} />
</div> )}
</FancyScrollbar> {!this.hasResults && <NoResults />}
</div> </div>
</div> </div>
) )
} }
private get data(): string[] { private get initialResultID(): string {
const {data} = this.props return _.get(this.props.data, '0.id', null)
if (!data) {
return ['Your query was syntactically correct but returned no data']
} }
return this.props.data.split('\n') private handleSelectResult = (selectedResultID: string): void => {
this.setState({selectedResultID})
}
private get style(): CSSProperties {
return {
padding: `${HANDLE_PIXELS}px`,
}
}
private get hasResults(): boolean {
return !!this.props.data.length
}
private get shouldShowTable(): boolean {
return !!this.props.data && !!this.selectedResult
}
private get selectedResult(): ScriptResult {
return this.props.data.find(d => d.id === this.state.selectedResultID)
} }
} }

View File

@ -3,5 +3,6 @@ import * as editor from 'src/ifql/constants/editor'
import * as argTypes from 'src/ifql/constants/argumentTypes' import * as argTypes from 'src/ifql/constants/argumentTypes'
import * as funcNames from 'src/ifql/constants/funcNames' import * as funcNames from 'src/ifql/constants/funcNames'
import * as builder from 'src/ifql/constants/builder' import * as builder from 'src/ifql/constants/builder'
import * as vis from 'src/ifql/constants/vis'
export {ast, funcNames, argTypes, editor, builder} export {ast, funcNames, argTypes, editor, builder, vis}

View File

@ -0,0 +1,2 @@
export const TABLE_ROW_HEIGHT = 30
export const TIME_COLUMN_WIDTH = 170

View File

@ -1,10 +1,17 @@
import React, {PureComponent, ReactChildren} from 'react' import React, {PureComponent, ReactChildren} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router' import {WithRouterProps} from 'react-router'
import {IFQLPage} from 'src/ifql'
import IFQLOverlay from 'src/ifql/components/IFQLOverlay' import IFQLOverlay from 'src/ifql/components/IFQLOverlay'
import {OverlayContext} from 'src/shared/components/OverlayTechnology' import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {Source, Service} from 'src/types' import {Source, Service, Notification} from 'src/types'
import {Links} from 'src/types/ifql'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {
updateScript as updateScriptAction,
UpdateScript,
} from 'src/ifql/actions'
import * as a from 'src/shared/actions/overlayTechnology' import * as a from 'src/shared/actions/overlayTechnology'
import * as b from 'src/shared/actions/services' import * as b from 'src/shared/actions/services'
@ -16,6 +23,10 @@ interface Props {
children: ReactChildren children: ReactChildren
showOverlay: a.ShowOverlay showOverlay: a.ShowOverlay
fetchServicesAsync: b.FetchServicesAsync fetchServicesAsync: b.FetchServicesAsync
notify: (message: Notification) => void
updateScript: UpdateScript
script: string
links: Links
} }
export class CheckServices extends PureComponent<Props & WithRouterProps> { export class CheckServices extends PureComponent<Props & WithRouterProps> {
@ -36,7 +47,22 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
} }
public render() { public render() {
return this.props.children const {services, sources, notify, updateScript, links, script} = this.props
if (!this.props.services.length) {
return null // put loading spinner here
}
return (
<IFQLPage
sources={sources}
services={services}
links={links}
script={script}
notify={notify}
updateScript={updateScript}
/>
)
} }
private overlay() { private overlay() {
@ -65,8 +91,17 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
const mdtp = { const mdtp = {
fetchServicesAsync: actions.fetchServicesAsync, fetchServicesAsync: actions.fetchServicesAsync,
showOverlay: actions.showOverlay, showOverlay: actions.showOverlay,
updateScript: updateScriptAction,
notify: notifyAction,
} }
const mstp = ({sources, services}) => ({sources, services}) const mstp = ({sources, services, links, script}) => {
return {
links: links.ifql,
script,
sources,
services,
}
}
export default connect(mstp, mdtp)(withRouter(CheckServices)) export default connect(mstp, mdtp)(CheckServices)

View File

@ -1,25 +1,22 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash' import _ from 'lodash'
import CheckServices from 'src/ifql/containers/CheckServices'
import TimeMachine from 'src/ifql/components/TimeMachine' import TimeMachine from 'src/ifql/components/TimeMachine'
import IFQLHeader from 'src/ifql/components/IFQLHeader' import IFQLHeader from 'src/ifql/components/IFQLHeader'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts' import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import {notify as notifyAction} from 'src/shared/actions/notifications'
import {analyzeSuccess} from 'src/shared/copy/notifications'
import { import {
updateScript as updateScriptAction, analyzeSuccess,
UpdateScript, ifqlTimeSeriesError,
} from 'src/ifql/actions' } from 'src/shared/copy/notifications'
import {UpdateScript} from 'src/ifql/actions'
import {bodyNodes} from 'src/ifql/helpers' import {bodyNodes} from 'src/ifql/helpers'
import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis' import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis'
import {funcNames, builder, argTypes} from 'src/ifql/constants' import {funcNames, builder, argTypes} from 'src/ifql/constants'
import {Source, Service, Notification} from 'src/types' import {Source, Service, Notification, ScriptResult} from 'src/types'
import { import {
Suggestion, Suggestion,
FlatBody, FlatBody,
@ -28,6 +25,7 @@ import {
Handlers, Handlers,
DeleteFuncNodeArgs, DeleteFuncNodeArgs,
Func, Func,
ScriptStatus,
} from 'src/types/ifql' } from 'src/types/ifql'
interface Status { interface Status {
@ -42,9 +40,6 @@ interface Props {
notify: (message: Notification) => void notify: (message: Notification) => void
script: string script: string
updateScript: UpdateScript updateScript: UpdateScript
params: {
sourceID: string
}
} }
interface Body extends FlatBody { interface Body extends FlatBody {
@ -54,9 +49,9 @@ interface Body extends FlatBody {
interface State { interface State {
body: Body[] body: Body[]
ast: object ast: object
data: string data: ScriptResult[]
status: ScriptStatus
suggestions: Suggestion[] suggestions: Suggestion[]
status: Status
} }
export const IFQLContext = React.createContext() export const IFQLContext = React.createContext()
@ -68,7 +63,7 @@ export class IFQLPage extends PureComponent<Props, State> {
this.state = { this.state = {
body: [], body: [],
ast: null, ast: null,
data: 'Hit "Get Data!" or Ctrl + Enter to run your script', data: [],
suggestions: [], suggestions: [],
status: { status: {
type: 'none', type: 'none',
@ -78,7 +73,7 @@ export class IFQLPage extends PureComponent<Props, State> {
} }
public async componentDidMount() { public async componentDidMount() {
const {links, script} = this.props const {links} = this.props
try { try {
const suggestions = await getSuggestions(links.suggestions) const suggestions = await getSuggestions(links.suggestions)
@ -87,7 +82,7 @@ export class IFQLPage extends PureComponent<Props, State> {
console.error('Could not get function suggestions: ', error) console.error('Could not get function suggestions: ', error)
} }
this.getASTResponse(script) this.getTimeSeries()
} }
public render() { public render() {
@ -95,7 +90,6 @@ export class IFQLPage extends PureComponent<Props, State> {
const {script} = this.props const {script} = this.props
return ( return (
<CheckServices>
<IFQLContext.Provider value={this.handlers}> <IFQLContext.Provider value={this.handlers}>
<KeyboardShortcuts onControlEnter={this.getTimeSeries}> <KeyboardShortcuts onControlEnter={this.getTimeSeries}>
<div className="page hosts-list-page"> <div className="page hosts-list-page">
@ -115,7 +109,6 @@ export class IFQLPage extends PureComponent<Props, State> {
</div> </div>
</KeyboardShortcuts> </KeyboardShortcuts>
</IFQLContext.Provider> </IFQLContext.Provider>
</CheckServices>
) )
} }
@ -401,6 +394,10 @@ export class IFQLPage extends PureComponent<Props, State> {
private getASTResponse = async (script: string) => { private getASTResponse = async (script: string) => {
const {links} = this.props const {links} = this.props
if (!script) {
return
}
try { try {
const ast = await getAST({url: links.ast, body: script}) const ast = await getAST({url: links.ast, body: script})
const suggestions = this.state.suggestions.map(s => { const suggestions = this.state.suggestions.map(s => {
@ -427,18 +424,28 @@ export class IFQLPage extends PureComponent<Props, State> {
} }
private getTimeSeries = async () => { private getTimeSeries = async () => {
const {script} = this.props const {script, links, notify} = this.props
this.setState({data: 'fetching data...'})
try { if (!script) {
const {data} = await getTimeSeries(this.service, script) return
this.setState({data})
} catch (error) {
this.setState({data: error})
console.error('Could not get timeSeries', error)
} }
this.getASTResponse(script) try {
await getAST({url: links.ast, body: script})
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
}
try {
const data = await getTimeSeries(this.service, script)
this.setState({data})
} catch (error) {
this.setState({data: []})
notify(ifqlTimeSeriesError(error))
console.error('Could not get timeSeries', error)
}
} }
private parseError = (error): Status => { private parseError = (error): Status => {
@ -448,13 +455,4 @@ export class IFQLPage extends PureComponent<Props, State> {
} }
} }
const mapStateToProps = ({links, services, sources, script}) => { export default IFQLPage
return {links: links.ifql, services, sources, script}
}
const mapDispatchToProps = {
notify: notifyAction,
updateScript: updateScriptAction,
}
export default connect(mapStateToProps, mapDispatchToProps)(IFQLPage)

View File

@ -36,7 +36,7 @@ import {
} from 'src/kapacitor' } from 'src/kapacitor'
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin' import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
import {SourcePage, ManageSources} from 'src/sources' import {SourcePage, ManageSources} from 'src/sources'
import {IFQLPage} from 'src/ifql' import {CheckServices} from 'src/ifql'
import NotFound from 'src/shared/components/NotFound' import NotFound from 'src/shared/components/NotFound'
import {getLinksAsync} from 'src/shared/actions/links' import {getLinksAsync} from 'src/shared/actions/links'
@ -158,7 +158,7 @@ class Root extends PureComponent<{}, State> {
<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} />
<Route path="manage-sources/:id/edit" component={SourcePage} /> <Route path="manage-sources/:id/edit" component={SourcePage} />
<Route path="delorean" component={IFQLPage} /> <Route path="delorean" component={CheckServices} />
</Route> </Route>
</Route> </Route>
<Route path="*" component={NotFound} /> <Route path="*" component={NotFound} />

View File

@ -640,3 +640,8 @@ export const ifqlUpdated = {
...defaultSuccessNotification, ...defaultSuccessNotification,
message: 'Connection Updated. Rejoice!', message: 'Connection Updated. Rejoice!',
} }
export const ifqlTimeSeriesError = (message: string) => ({
...defaultErrorNotification,
message: `Could not get data: ${message}`,
})

View File

@ -0,0 +1,38 @@
import Papa from 'papaparse'
import _ from 'lodash'
import uuid from 'uuid'
import {ScriptResult} from 'src/types'
export const parseResults = (response: string): ScriptResult[] => {
const trimmedReponse = response.trim()
if (_.isEmpty(trimmedReponse)) {
return []
}
return trimmedReponse.split(/\n\s*\n/).map(parseResult)
}
export const parseResult = (raw: string, index: number): ScriptResult => {
const lines = raw.split('\n')
const rawMetadata: string = lines
.filter(line => line.startsWith('#'))
.map(line => line.slice(1))
.join('\n')
const rawData: string = lines.filter(line => !line.startsWith('#')).join('\n')
const metadata = Papa.parse(rawMetadata).data
const data = Papa.parse(rawData).data
const headerRow = _.get(data, '0', [])
const measurementHeaderIndex = headerRow.findIndex(v => v === '_measurement')
const name = _.get(data, `1.${measurementHeaderIndex}`, `Result ${index}`)
return {
id: uuid.v4(),
name,
data,
metadata,
}
}

View File

@ -13,15 +13,25 @@
@include gradient-v($g2-kevlar, $g0-obsidian); @include gradient-v($g2-kevlar, $g0-obsidian);
} }
.time-machine--graph { .time-machine--sidebar,
width: calc(100% - 60px); .time-machine--vis {
height: calc(100% - 60px);
background-color: $g3-castle; background-color: $g3-castle;
border-radius: $radius; border-radius: $radius;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
flex-wrap: nowrap; flex-wrap: nowrap;
height: inherit;
}
.time-machine--sidebar {
flex-basis: 25%;
margin-right: 5px;
}
.time-machine--vis {
flex-basis: 75%;
flex-grow: 1;
} }
.time-machine--graph-header { .time-machine--graph-header {
@ -34,7 +44,6 @@
.time-machine--graph-header .nav.nav-tablist { .time-machine--graph-header .nav.nav-tablist {
width: 180px; width: 180px;
li { li {
justify-content: center; justify-content: center;
flex: 1 0 0; flex: 1 0 0;

View File

@ -10,7 +10,7 @@ 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
export interface Status { export interface ScriptStatus {
type: string type: string
text: string text: string
} }
@ -110,3 +110,12 @@ export interface Links {
suggestions: string suggestions: string
ast: string ast: string
} }
// ScriptResult is the result of a request to IFQL
// https://github.com/influxdata/platform/blob/master/query/docs/SPEC.md#response-format
export interface ScriptResult {
id: string
name: string
data: string[][]
metadata: string[][]
}

View File

@ -1,3 +1,4 @@
import {LayoutCell, LayoutQuery} from './layouts'
import {Service, NewService} from './services' import {Service, NewService} from './services'
import {AuthLinks, Organization, Role, User, Me} from './auth' import {AuthLinks, Organization, Role, User, Me} from './auth'
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard' import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
@ -21,6 +22,7 @@ import {AlertRule, Kapacitor, Task} from './kapacitor'
import {Source, SourceLinks} from './sources' import {Source, SourceLinks} from './sources'
import {DropdownAction, DropdownItem} from './shared' import {DropdownAction, DropdownItem} from './shared'
import {Notification, NotificationFunc} from './notifications' import {Notification, NotificationFunc} from './notifications'
import {ScriptResult, ScriptStatus} from './ifql'
export { export {
Me, Me,
@ -58,4 +60,8 @@ export {
Axes, Axes,
Service, Service,
NewService, NewService,
LayoutCell,
LayoutQuery,
ScriptResult,
ScriptStatus,
} }

16
ui/src/types/layouts.ts Normal file
View File

@ -0,0 +1,16 @@
export interface LayoutCell {
x: number
y: number
w: number
h: number
i: string
name: string
queries: LayoutQuery[]
}
export interface LayoutQuery {
query: string
label: string
wheres: string[]
groupbys: string[]
}

View File

@ -1,18 +1,23 @@
import {buildQuery} from 'utils/influxql' import {buildQuery} from 'src/utils/influxql'
import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants' import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import { import {
TEMP_VAR_DASHBOARD_TIME, TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME, TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants' } from 'src/shared/constants'
import {timeRanges} from 'shared/data/timeRanges' import {timeRanges} from 'src/shared/data/timeRanges'
import {Source, LayoutQuery, TimeRange} from 'src/types'
const buildCannedDashboardQuery = (query, {lower, upper}, host) => { const buildCannedDashboardQuery = (
query: LayoutQuery,
{lower, upper}: TimeRange,
host: string
): string => {
const {defaultGroupBy} = timeRanges.find(range => range.lower === lower) || { const {defaultGroupBy} = timeRanges.find(range => range.lower === lower) || {
defaultGroupBy: '5m', defaultGroupBy: '5m',
} }
const {wheres, groupbys} = query const {wheres, groupbys} = query
let text = query.text let text = query.query
if (upper) { if (upper) {
text += ` where time > '${lower}' AND time < '${upper}'` text += ` where time > '${lower}' AND time < '${upper}'`
@ -43,7 +48,12 @@ const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
return text return text
} }
export const buildQueriesForLayouts = (cell, source, timeRange, host) => { export const buildQueriesForLayouts = (
cell,
source: Source,
timeRange: TimeRange,
host: string
) => {
return cell.queries.map(query => { return cell.queries.map(query => {
let queryText let queryText
// Canned dashboards use an different a schema different from queryConfig. // Canned dashboards use an different a schema different from queryConfig.

View File

@ -6,14 +6,14 @@ const setup = () => {
const props = { const props = {
script: '', script: '',
body: [], body: [],
data: '', data: [],
status: {type: '', text: ''},
suggestions: [], suggestions: [],
onSubmitScript: () => {}, onSubmitScript: () => {},
onChangeScript: () => {}, onChangeScript: () => {},
onAnalyze: () => {}, onAnalyze: () => {},
onAppendFrom: () => {}, onAppendFrom: () => {},
onAppendJoin: () => {}, onAppendJoin: () => {},
status: {type: '', text: ''},
} }
const wrapper = shallow(<TimeMachine {...props} />) const wrapper = shallow(<TimeMachine {...props} />)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
import {parseResults} from 'src/shared/parsing/ifql'
import {
RESPONSE_NO_METADATA,
RESPONSE_METADATA,
RESPONSE_NO_MEASUREMENT,
LARGE_RESPONSE,
EXPECTED_METADATA,
EXPECTED_COLUMNS,
} from 'test/shared/parsing/constants'
describe('IFQL response parser', () => {
it('parseResults into the right number of tables', () => {
const result = parseResults(LARGE_RESPONSE)
expect(result).toHaveLength(47)
})
describe('headers', () => {
it('can parse headers when no metadata is present', () => {
const actual = parseResults(RESPONSE_NO_METADATA)[0].data[0]
expect(actual).toEqual(EXPECTED_COLUMNS)
})
it('can parse headers when metadata is present', () => {
const actual = parseResults(RESPONSE_METADATA)[0].data[0]
expect(actual).toEqual(EXPECTED_COLUMNS)
})
it('returns the approriate metadata', () => {
const actual = parseResults(RESPONSE_METADATA)[0].metadata
expect(actual).toEqual(EXPECTED_METADATA)
})
})
describe('name', () => {
it('uses the measurement as a name when present', () => {
const actual = parseResults(RESPONSE_METADATA)[0].name
const expected = 'cpu'
expect(actual).toBe(expected)
})
it('uses the index as a name if a measurement column is not present', () => {
const actual = parseResults(RESPONSE_NO_MEASUREMENT)[0].name
const expected = 'Result 0'
expect(actual).toBe(expected)
})
})
})

View File

@ -65,6 +65,10 @@
version "9.6.6" version "9.6.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.6.tgz#439b91f9caf3983cad2eef1e11f6bedcbf9431d2" resolved "https://registry.yarnpkg.com/@types/node/-/node-9.6.6.tgz#439b91f9caf3983cad2eef1e11f6bedcbf9431d2"
"@types/papaparse@^4.1.34":
version "4.1.34"
resolved "https://registry.yarnpkg.com/@types/papaparse/-/papaparse-4.1.34.tgz#893628fbb70243313e46d1b962989c023184783b"
"@types/prop-types@*", "@types/prop-types@^15.5.2": "@types/prop-types@*", "@types/prop-types@^15.5.2":
version "15.5.2" version "15.5.2"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.2.tgz#3c6b8dceb2906cc87fe4358e809f9d20c8d59be1"
@ -6297,6 +6301,10 @@ pako@~1.0.5:
version "1.0.6" version "1.0.6"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
papaparse@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-4.4.0.tgz#6bcdbda80873e00cfb0bdcd7a4571c72a9a40168"
parallel-transform@^1.1.0: parallel-transform@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"