commit
cd48790f0c
|
@ -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",
|
||||||
|
|
|
@ -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 🤷')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import React, {SFC} from 'react'
|
||||||
|
|
||||||
|
const NoResult: SFC = () => (
|
||||||
|
<div className="graph-empty">
|
||||||
|
<p>No Results</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default NoResult
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const TABLE_ROW_HEIGHT = 30
|
||||||
|
export const TIME_COLUMN_WIDTH = 170
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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}`,
|
||||||
|
})
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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[][]
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]
|
||||||
|
}
|
|
@ -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.
|
|
@ -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
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue