Merge branch 'master' into ifql/join
commit
46c30ed92f
|
@ -1,6 +1,7 @@
|
|||
## v1.5.1.0 [unreleased]
|
||||
|
||||
### Features
|
||||
1. [#3522](https://github.com/influxdata/chronograf/pull/3522): Add support for Template Variables in Cell Titles
|
||||
|
||||
### UI Improvements
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"@types/jest": "^22.1.4",
|
||||
"@types/lodash": "^4.14.104",
|
||||
"@types/node": "^9.4.6",
|
||||
"@types/papaparse": "^4.1.34",
|
||||
"@types/prop-types": "^15.5.2",
|
||||
"@types/react": "^16.0.38",
|
||||
"@types/react-dnd": "^2.0.36",
|
||||
|
@ -133,6 +134,7 @@
|
|||
"lodash": "^4.3.0",
|
||||
"moment": "^2.13.0",
|
||||
"nano-date": "^2.0.1",
|
||||
"papaparse": "^4.4.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"query-string": "^5.0.0",
|
||||
"react": "^16.3.1",
|
||||
|
@ -157,4 +159,4 @@
|
|||
"rome": "^2.1.22",
|
||||
"uuid": "^3.2.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
const {showWriteForm} = this.state
|
||||
|
||||
return (
|
||||
<div className="data-explorer">
|
||||
<>
|
||||
{showWriteForm ? (
|
||||
<OverlayTechnologies>
|
||||
<WriteDataForm
|
||||
|
@ -150,7 +150,7 @@ export class DataExplorer extends PureComponent<Props, State> {
|
|||
editQueryStatus={queryConfigActions.editQueryStatus}
|
||||
/>
|
||||
</ResizeContainer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import _ from 'lodash'
|
||||
|
||||
import AJAX from 'src/utils/ajax'
|
||||
import {Service} from 'src/types'
|
||||
import {Service, ScriptResult} from 'src/types'
|
||||
import {updateService} from 'src/shared/apis'
|
||||
import {parseResults} from 'src/shared/parsing/ifql'
|
||||
|
||||
export const getSuggestions = async (url: string) => {
|
||||
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 mark = encodeURIComponent('?')
|
||||
const garbage = script.replace(/\s/g, '') // server cannot handle whitespace
|
||||
|
||||
try {
|
||||
const data = await AJAX({
|
||||
const {data} = await AJAX({
|
||||
method: 'POST',
|
||||
url: `${
|
||||
service.links.proxy
|
||||
}?path=/v1/query${mark}orgName=defaulorgname${and}q=${garbage}`,
|
||||
headers: {'Content-Type': 'text/plain'},
|
||||
})
|
||||
|
||||
return data
|
||||
return parseResults(data)
|
||||
} catch (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,
|
||||
OnSubmitScript,
|
||||
FlatBody,
|
||||
Status,
|
||||
ScriptStatus,
|
||||
ScriptResult,
|
||||
} from 'src/types/ifql'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
|
||||
|
||||
interface Props {
|
||||
data: string
|
||||
data: ScriptResult[]
|
||||
script: string
|
||||
body: Body[]
|
||||
status: Status
|
||||
status: ScriptStatus
|
||||
suggestions: Suggestion[]
|
||||
onChangeScript: OnChangeScript
|
||||
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 FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import React, {PureComponent, CSSProperties} from 'react'
|
||||
import _ from 'lodash'
|
||||
|
||||
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 {
|
||||
data: string
|
||||
data: ScriptResult[]
|
||||
}
|
||||
|
||||
interface State {
|
||||
selectedResultID: string | null
|
||||
}
|
||||
|
||||
@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() {
|
||||
return (
|
||||
<div className="time-machine-visualization">
|
||||
<div className="time-machine--graph">
|
||||
<FancyScrollbar>
|
||||
<div className="time-machine--graph-body">
|
||||
{this.data.map((d, i) => {
|
||||
return (
|
||||
<div key={i} className="data-row">
|
||||
{d}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</FancyScrollbar>
|
||||
<div className="time-machine-visualization" style={this.style}>
|
||||
{this.hasResults && (
|
||||
<TableSidebar
|
||||
data={this.props.data}
|
||||
selectedResultID={this.state.selectedResultID}
|
||||
onSelectResult={this.handleSelectResult}
|
||||
/>
|
||||
)}
|
||||
<div className="time-machine--vis">
|
||||
{this.shouldShowTable && (
|
||||
<TimeMachineTable {...this.selectedResult} />
|
||||
)}
|
||||
{!this.hasResults && <NoResults />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get data(): string[] {
|
||||
const {data} = this.props
|
||||
if (!data) {
|
||||
return ['Your query was syntactically correct but returned no data']
|
||||
}
|
||||
private get initialResultID(): string {
|
||||
return _.get(this.props.data, '0.id', null)
|
||||
}
|
||||
|
||||
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 funcNames from 'src/ifql/constants/funcNames'
|
||||
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 {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 {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 b from 'src/shared/actions/services'
|
||||
|
||||
|
@ -16,6 +23,10 @@ interface Props {
|
|||
children: ReactChildren
|
||||
showOverlay: a.ShowOverlay
|
||||
fetchServicesAsync: b.FetchServicesAsync
|
||||
notify: (message: Notification) => void
|
||||
updateScript: UpdateScript
|
||||
script: string
|
||||
links: Links
|
||||
}
|
||||
|
||||
export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
||||
|
@ -36,7 +47,22 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
}
|
||||
|
||||
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() {
|
||||
|
@ -65,8 +91,17 @@ export class CheckServices extends PureComponent<Props & WithRouterProps> {
|
|||
const mdtp = {
|
||||
fetchServicesAsync: actions.fetchServicesAsync,
|
||||
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 {connect} from 'react-redux'
|
||||
import _ from 'lodash'
|
||||
|
||||
import CheckServices from 'src/ifql/containers/CheckServices'
|
||||
import TimeMachine from 'src/ifql/components/TimeMachine'
|
||||
import IFQLHeader from 'src/ifql/components/IFQLHeader'
|
||||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
|
||||
|
||||
import {notify as notifyAction} from 'src/shared/actions/notifications'
|
||||
import {analyzeSuccess} from 'src/shared/copy/notifications'
|
||||
import {
|
||||
updateScript as updateScriptAction,
|
||||
UpdateScript,
|
||||
} from 'src/ifql/actions'
|
||||
analyzeSuccess,
|
||||
ifqlTimeSeriesError,
|
||||
} from 'src/shared/copy/notifications'
|
||||
import {UpdateScript} from 'src/ifql/actions'
|
||||
|
||||
import {bodyNodes} from 'src/ifql/helpers'
|
||||
import {getSuggestions, getAST, getTimeSeries} from 'src/ifql/apis'
|
||||
import {builder, argTypes} from 'src/ifql/constants'
|
||||
|
||||
import {Source, Service, Notification} from 'src/types'
|
||||
import {Source, Service, Notification, ScriptResult} from 'src/types'
|
||||
import {
|
||||
Suggestion,
|
||||
FlatBody,
|
||||
|
@ -28,6 +25,7 @@ import {
|
|||
Handlers,
|
||||
DeleteFuncNodeArgs,
|
||||
Func,
|
||||
ScriptStatus,
|
||||
} from 'src/types/ifql'
|
||||
|
||||
interface Status {
|
||||
|
@ -42,9 +40,6 @@ interface Props {
|
|||
notify: (message: Notification) => void
|
||||
script: string
|
||||
updateScript: UpdateScript
|
||||
params: {
|
||||
sourceID: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Body extends FlatBody {
|
||||
|
@ -54,9 +49,9 @@ interface Body extends FlatBody {
|
|||
interface State {
|
||||
body: Body[]
|
||||
ast: object
|
||||
data: string
|
||||
data: ScriptResult[]
|
||||
status: ScriptStatus
|
||||
suggestions: Suggestion[]
|
||||
status: Status
|
||||
}
|
||||
|
||||
export const IFQLContext = React.createContext()
|
||||
|
@ -68,7 +63,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
this.state = {
|
||||
body: [],
|
||||
ast: null,
|
||||
data: 'Hit "Get Data!" or Ctrl + Enter to run your script',
|
||||
data: [],
|
||||
suggestions: [],
|
||||
status: {
|
||||
type: 'none',
|
||||
|
@ -78,7 +73,7 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const {links, script} = this.props
|
||||
const {links} = this.props
|
||||
|
||||
try {
|
||||
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)
|
||||
}
|
||||
|
||||
this.getASTResponse(script)
|
||||
this.getTimeSeries()
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -95,27 +90,25 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
const {script} = this.props
|
||||
|
||||
return (
|
||||
<CheckServices>
|
||||
<IFQLContext.Provider value={this.handlers}>
|
||||
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
|
||||
<div className="page hosts-list-page">
|
||||
{this.header}
|
||||
<TimeMachine
|
||||
data={data}
|
||||
body={body}
|
||||
script={script}
|
||||
status={status}
|
||||
suggestions={suggestions}
|
||||
onAnalyze={this.handleAnalyze}
|
||||
onAppendFrom={this.handleAppendFrom}
|
||||
onAppendJoin={this.handleAppendJoin}
|
||||
onChangeScript={this.handleChangeScript}
|
||||
onSubmitScript={this.handleSubmitScript}
|
||||
/>
|
||||
</div>
|
||||
</KeyboardShortcuts>
|
||||
</IFQLContext.Provider>
|
||||
</CheckServices>
|
||||
<IFQLContext.Provider value={this.handlers}>
|
||||
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
|
||||
<div className="page hosts-list-page">
|
||||
{this.header}
|
||||
<TimeMachine
|
||||
data={data}
|
||||
body={body}
|
||||
script={script}
|
||||
status={status}
|
||||
suggestions={suggestions}
|
||||
onAnalyze={this.handleAnalyze}
|
||||
onAppendFrom={this.handleAppendFrom}
|
||||
onAppendJoin={this.handleAppendJoin}
|
||||
onChangeScript={this.handleChangeScript}
|
||||
onSubmitScript={this.handleSubmitScript}
|
||||
/>
|
||||
</div>
|
||||
</KeyboardShortcuts>
|
||||
</IFQLContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -406,6 +399,10 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
private getASTResponse = async (script: string) => {
|
||||
const {links} = this.props
|
||||
|
||||
if (!script) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const ast = await getAST({url: links.ast, body: script})
|
||||
const body = bodyNodes(ast, this.state.suggestions)
|
||||
|
@ -419,18 +416,28 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
private getTimeSeries = async () => {
|
||||
const {script} = this.props
|
||||
this.setState({data: 'fetching data...'})
|
||||
const {script, links, notify} = this.props
|
||||
|
||||
try {
|
||||
const {data} = await getTimeSeries(this.service, script)
|
||||
this.setState({data})
|
||||
} catch (error) {
|
||||
this.setState({data: error})
|
||||
console.error('Could not get timeSeries', error)
|
||||
if (!script) {
|
||||
return
|
||||
}
|
||||
|
||||
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 => {
|
||||
|
@ -440,13 +447,4 @@ export class IFQLPage extends PureComponent<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({links, services, sources, script}) => {
|
||||
return {links: links.ifql, services, sources, script}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
notify: notifyAction,
|
||||
updateScript: updateScriptAction,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(IFQLPage)
|
||||
export default IFQLPage
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
} from 'src/kapacitor'
|
||||
import {AdminChronografPage, AdminInfluxDBPage} from 'src/admin'
|
||||
import {SourcePage, ManageSources} from 'src/sources'
|
||||
import {IFQLPage} from 'src/ifql'
|
||||
import {CheckServices} from 'src/ifql'
|
||||
import NotFound from 'src/shared/components/NotFound'
|
||||
|
||||
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/new" component={SourcePage} />
|
||||
<Route path="manage-sources/:id/edit" component={SourcePage} />
|
||||
<Route path="delorean" component={IFQLPage} />
|
||||
<Route path="delorean" component={CheckServices} />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route path="*" component={NotFound} />
|
||||
|
|
|
@ -56,6 +56,7 @@ export const saveToLocalStorage = ({
|
|||
timeRange,
|
||||
dataExplorer,
|
||||
dashTimeV1: {ranges},
|
||||
logs,
|
||||
script,
|
||||
}: LocalStorage): void => {
|
||||
try {
|
||||
|
@ -72,6 +73,7 @@ export const saveToLocalStorage = ({
|
|||
dataExplorer,
|
||||
dataExplorerQueryConfigs,
|
||||
script,
|
||||
logs,
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
|
|
|
@ -1,13 +1,22 @@
|
|||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
import {Source, Namespace, TimeRange, QueryConfig} from 'src/types'
|
||||
import {getSource} from 'src/shared/apis'
|
||||
import {getDatabasesWithRetentionPolicies} from 'src/shared/apis/databases'
|
||||
import {buildHistogramQueryConfig} from 'src/logs/utils'
|
||||
import {getDeep} from 'src/utils/wrappers'
|
||||
import buildQuery from 'src/utils/influxql'
|
||||
import {executeQueryAsync} from 'src/logs/api'
|
||||
import {LogsState} from 'src/types/localStorage'
|
||||
|
||||
type GetState = () => {logs: LogsState}
|
||||
|
||||
export enum ActionTypes {
|
||||
SetSource = 'LOGS_SET_SOURCE',
|
||||
SetNamespaces = 'LOGS_SET_NAMESPACES',
|
||||
SetTimeRange = 'LOGS_SET_TIMERANGE',
|
||||
SetNamespace = 'LOGS_SET_NAMESPACE',
|
||||
SetHistogramQueryConfig = 'LOGS_SET_HISTOGRAM_QUERY_CONFIG',
|
||||
SetHistogramData = 'LOGS_SET_HISTOGRAM_DATA',
|
||||
ChangeZoom = 'LOGS_CHANGE_ZOOM',
|
||||
}
|
||||
|
||||
interface SetSourceAction {
|
||||
|
@ -38,11 +47,36 @@ interface SetTimeRangeAction {
|
|||
}
|
||||
}
|
||||
|
||||
interface SetHistogramQueryConfig {
|
||||
type: ActionTypes.SetHistogramQueryConfig
|
||||
payload: {
|
||||
queryConfig: QueryConfig
|
||||
}
|
||||
}
|
||||
|
||||
interface SetHistogramData {
|
||||
type: ActionTypes.SetHistogramData
|
||||
payload: {
|
||||
data: object[]
|
||||
}
|
||||
}
|
||||
|
||||
interface ChangeZoomAction {
|
||||
type: ActionTypes.ChangeZoom
|
||||
payload: {
|
||||
data: object[]
|
||||
timeRange: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| SetSourceAction
|
||||
| SetNamespacesAction
|
||||
| SetTimeRangeAction
|
||||
| SetNamespaceAction
|
||||
| SetHistogramQueryConfig
|
||||
| SetHistogramData
|
||||
| ChangeZoomAction
|
||||
|
||||
export const setSource = (source: Source): SetSourceAction => ({
|
||||
type: ActionTypes.SetSource,
|
||||
|
@ -51,12 +85,75 @@ export const setSource = (source: Source): SetSourceAction => ({
|
|||
},
|
||||
})
|
||||
|
||||
export const setNamespace = (namespace: Namespace): SetNamespaceAction => ({
|
||||
type: ActionTypes.SetNamespace,
|
||||
payload: {
|
||||
namespace,
|
||||
},
|
||||
})
|
||||
export const executeHistogramQueryAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const queryConfig = getDeep<QueryConfig | null>(
|
||||
state,
|
||||
'logs.histogramQueryConfig',
|
||||
null
|
||||
)
|
||||
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
|
||||
const namespace = getDeep<Namespace | null>(
|
||||
state,
|
||||
'logs.currentNamespace',
|
||||
null
|
||||
)
|
||||
const proxyLink = getDeep<string | null>(
|
||||
state,
|
||||
'logs.currentSource.links.proxy',
|
||||
null
|
||||
)
|
||||
|
||||
if (queryConfig && timeRange && namespace && proxyLink) {
|
||||
const query = buildQuery(timeRange, queryConfig)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.SetHistogramData,
|
||||
payload: {
|
||||
data: [{response}],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const setHistogramQueryConfigAsync = () => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const namespace = getDeep<Namespace | null>(
|
||||
state,
|
||||
'logs.currentNamespace',
|
||||
null
|
||||
)
|
||||
const timeRange = getDeep<TimeRange | null>(state, 'logs.timeRange', null)
|
||||
|
||||
if (timeRange && namespace) {
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.SetHistogramQueryConfig,
|
||||
payload: {queryConfig},
|
||||
})
|
||||
|
||||
dispatch(executeHistogramQueryAsync())
|
||||
}
|
||||
}
|
||||
|
||||
export const setNamespaceAsync = (namespace: Namespace) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
dispatch({
|
||||
type: ActionTypes.SetNamespace,
|
||||
payload: {namespace},
|
||||
})
|
||||
|
||||
dispatch(setHistogramQueryConfigAsync())
|
||||
}
|
||||
|
||||
export const setNamespaces = (
|
||||
namespaces: Namespace[]
|
||||
|
@ -67,27 +164,70 @@ export const setNamespaces = (
|
|||
},
|
||||
})
|
||||
|
||||
export const setTimeRange = (timeRange: TimeRange): SetTimeRangeAction => ({
|
||||
type: ActionTypes.SetTimeRange,
|
||||
payload: {
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
export const setTimeRangeAsync = (timeRange: TimeRange) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
dispatch({
|
||||
type: ActionTypes.SetTimeRange,
|
||||
payload: {
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
dispatch(setHistogramQueryConfigAsync())
|
||||
}
|
||||
|
||||
export const getSourceAsync = (sourceID: string) => async dispatch => {
|
||||
export const populateNamespacesAsync = (proxyLink: string) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
|
||||
|
||||
if (namespaces && namespaces.length > 0) {
|
||||
dispatch(setNamespaces(namespaces))
|
||||
dispatch(setNamespaceAsync(namespaces[0]))
|
||||
}
|
||||
}
|
||||
|
||||
export const getSourceAndPopulateNamespacesAsync = (sourceID: string) => async (
|
||||
dispatch
|
||||
): Promise<void> => {
|
||||
const response = await getSource(sourceID)
|
||||
const source = response.data
|
||||
|
||||
const proxyLink = getDeep<string | null>(source, 'links.proxy', null)
|
||||
|
||||
if (proxyLink) {
|
||||
const namespaces = await getDatabasesWithRetentionPolicies(proxyLink)
|
||||
|
||||
if (namespaces && namespaces.length > 0) {
|
||||
dispatch(setNamespaces(namespaces))
|
||||
dispatch(setNamespace(namespaces[0]))
|
||||
}
|
||||
|
||||
dispatch(setSource(source))
|
||||
dispatch(populateNamespacesAsync(proxyLink))
|
||||
}
|
||||
}
|
||||
|
||||
export const changeZoomAsync = (timeRange: TimeRange) => async (
|
||||
dispatch,
|
||||
getState: GetState
|
||||
): Promise<void> => {
|
||||
const state = getState()
|
||||
const namespace = getDeep<Namespace | null>(
|
||||
state,
|
||||
'logs.currentNamespace',
|
||||
null
|
||||
)
|
||||
const proxyLink = getDeep<string | null>(
|
||||
state,
|
||||
'logs.currentSource.links.proxy',
|
||||
null
|
||||
)
|
||||
|
||||
if (namespace && proxyLink) {
|
||||
const queryConfig = buildHistogramQueryConfig(namespace, timeRange)
|
||||
const query = buildQuery(timeRange, queryConfig)
|
||||
const response = await executeQueryAsync(proxyLink, namespace, query)
|
||||
|
||||
dispatch({
|
||||
type: ActionTypes.ChangeZoom,
|
||||
payload: {
|
||||
data: [{response}],
|
||||
timeRange,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||
import {Namespace} from 'src/types'
|
||||
import {TimeSeriesResponse} from 'src/types/series'
|
||||
|
||||
export const executeQueryAsync = async (
|
||||
proxyLink: string,
|
||||
namespace: Namespace,
|
||||
query: string
|
||||
): Promise<TimeSeriesResponse> => {
|
||||
try {
|
||||
const {data} = await proxy({
|
||||
source: proxyLink,
|
||||
db: namespace.database,
|
||||
rp: namespace.retentionPolicy,
|
||||
query,
|
||||
tempVars: [],
|
||||
resolution: null,
|
||||
})
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import LineGraph from 'src/shared/components/LineGraph'
|
||||
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
|
||||
|
||||
import {TimeRange} from 'src/types'
|
||||
|
||||
interface Props {
|
||||
onZoom: (lower: string, upper: string) => void
|
||||
timeRange: TimeRange
|
||||
data: object[]
|
||||
}
|
||||
|
||||
class LogViewerChart extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {timeRange, data, onZoom} = this.props
|
||||
return (
|
||||
<LineGraph
|
||||
onZoom={onZoom}
|
||||
queries={[]}
|
||||
data={data}
|
||||
displayOptions={{animatedZooms: false}}
|
||||
setResolution={this.setResolution}
|
||||
isBarGraph={true}
|
||||
timeRange={timeRange}
|
||||
colors={DEFAULT_LINE_COLORS}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private setResolution = () => {}
|
||||
}
|
||||
|
||||
export default LogViewerChart
|
|
@ -26,24 +26,32 @@ class LogViewerHeader extends PureComponent<Props> {
|
|||
public render(): JSX.Element {
|
||||
const {timeRange} = this.props
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
className="dropdown-300"
|
||||
items={this.sourceDropDownItems}
|
||||
selected={this.selectedSource}
|
||||
onChoose={this.handleChooseSource}
|
||||
/>
|
||||
<Dropdown
|
||||
className="dropdown-300"
|
||||
items={this.namespaceDropDownItems}
|
||||
selected={this.selectedNamespace}
|
||||
onChoose={this.handleChooseNamespace}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
/>
|
||||
</>
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Log Viewer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">
|
||||
<Dropdown
|
||||
className="dropdown-300"
|
||||
items={this.sourceDropDownItems}
|
||||
selected={this.selectedSource}
|
||||
onChoose={this.handleChooseSource}
|
||||
/>
|
||||
<Dropdown
|
||||
className="dropdown-180"
|
||||
iconName="disks"
|
||||
items={this.namespaceDropDownItems}
|
||||
selected={this.selectedNamespace}
|
||||
onChoose={this.handleChooseNamespace}
|
||||
/>
|
||||
<TimeRangeDropdown
|
||||
onChooseTimeRange={this.handleChooseTimeRange}
|
||||
selected={timeRange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import {Filter} from 'src/logs/containers/LogsPage'
|
||||
|
||||
interface Props {
|
||||
filter: Filter
|
||||
onDelete: (id: string) => () => void
|
||||
onToggleStatus: (id: string) => () => void
|
||||
onToggleOperator: (id: string) => () => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
expanded: boolean
|
||||
}
|
||||
|
||||
class LogsFilter extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
filter: {id},
|
||||
onDelete,
|
||||
} = this.props
|
||||
const {expanded} = this.state
|
||||
|
||||
return (
|
||||
<li className={this.className} onMouseLeave={this.handleMouseLeave}>
|
||||
{this.label}
|
||||
<div className="logs-viewer--filter-remove" onClick={onDelete(id)} />
|
||||
{expanded && this.renderTooltip}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
private get label(): JSX.Element {
|
||||
const {
|
||||
filter: {key, operator, value},
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<span
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
>{`${key} ${operator} ${value}`}</span>
|
||||
)
|
||||
}
|
||||
|
||||
private get className(): string {
|
||||
const {expanded} = this.state
|
||||
const {
|
||||
filter: {enabled},
|
||||
} = this.props
|
||||
|
||||
return classnames('logs-viewer--filter', {
|
||||
active: expanded,
|
||||
disabled: !enabled,
|
||||
})
|
||||
}
|
||||
|
||||
private handleMouseEnter = (): void => {
|
||||
this.setState({expanded: true})
|
||||
}
|
||||
|
||||
private handleMouseLeave = (): void => {
|
||||
this.setState({expanded: false})
|
||||
}
|
||||
|
||||
private get renderTooltip(): JSX.Element {
|
||||
const {
|
||||
filter: {id, enabled, operator},
|
||||
onDelete,
|
||||
onToggleStatus,
|
||||
onToggleOperator,
|
||||
} = this.props
|
||||
|
||||
const toggleStatusText = enabled ? 'Disable' : 'Enable'
|
||||
const toggleOperatorText = operator === '==' ? '!=' : '=='
|
||||
|
||||
return (
|
||||
<ul className="logs-viewer--filter-tooltip">
|
||||
<li onClick={onToggleStatus(id)}>{toggleStatusText}</li>
|
||||
<li onClick={onToggleOperator(id)}>{toggleOperatorText}</li>
|
||||
<li onClick={onDelete(id)}>Delete</li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsFilter
|
|
@ -0,0 +1,84 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import {Filter} from 'src/logs/containers/LogsPage'
|
||||
import FilterBlock from 'src/logs/components/LogsFilter'
|
||||
|
||||
interface Props {
|
||||
numResults: number
|
||||
filters: Filter[]
|
||||
onUpdateFilters: (fitlers: Filter[]) => void
|
||||
}
|
||||
|
||||
class LogsFilters extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {numResults} = this.props
|
||||
|
||||
return (
|
||||
<div className="logs-viewer--filter-bar">
|
||||
<label className="logs-viewer--results-text">
|
||||
Query returned <strong>{numResults} Events</strong>
|
||||
</label>
|
||||
<ul className="logs-viewer--filters">{this.renderFilters}</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get renderFilters(): JSX.Element[] {
|
||||
const {filters} = this.props
|
||||
|
||||
return filters.map(filter => (
|
||||
<FilterBlock
|
||||
key={filter.id}
|
||||
filter={filter}
|
||||
onDelete={this.handleDeleteFilter}
|
||||
onToggleStatus={this.handleToggleFilterStatus}
|
||||
onToggleOperator={this.handleToggleFilterOperator}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
private handleDeleteFilter = (id: string) => (): void => {
|
||||
const {filters, onUpdateFilters} = this.props
|
||||
|
||||
const filteredFilters = filters.filter(filter => filter.id !== id)
|
||||
|
||||
onUpdateFilters(filteredFilters)
|
||||
}
|
||||
|
||||
private handleToggleFilterStatus = (id: string) => (): void => {
|
||||
const {filters, onUpdateFilters} = this.props
|
||||
|
||||
const filteredFilters = filters.map(filter => {
|
||||
if (filter.id === id) {
|
||||
return {...filter, enabled: !filter.enabled}
|
||||
}
|
||||
|
||||
return filter
|
||||
})
|
||||
|
||||
onUpdateFilters(filteredFilters)
|
||||
}
|
||||
|
||||
private handleToggleFilterOperator = (id: string) => (): void => {
|
||||
const {filters, onUpdateFilters} = this.props
|
||||
|
||||
const filteredFilters = filters.map(filter => {
|
||||
if (filter.id === id) {
|
||||
return {...filter, operator: this.toggleOperator(filter.operator)}
|
||||
}
|
||||
|
||||
return filter
|
||||
})
|
||||
|
||||
onUpdateFilters(filteredFilters)
|
||||
}
|
||||
|
||||
private toggleOperator = (op: string): string => {
|
||||
if (op === '==') {
|
||||
return '!='
|
||||
}
|
||||
|
||||
return '=='
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsFilters
|
|
@ -0,0 +1,13 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
class LogsGraphContainer extends PureComponent {
|
||||
public render() {
|
||||
return (
|
||||
<div className="logs-viewer--graph-container">
|
||||
<div className="logs-viewer--graph">{this.props.children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsGraphContainer
|
|
@ -0,0 +1,43 @@
|
|||
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
|
||||
|
||||
interface Props {
|
||||
searchString: string
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
onSearch: () => void
|
||||
}
|
||||
|
||||
class LogsSearchBar extends PureComponent<Props> {
|
||||
public render() {
|
||||
const {searchString, onSearch, onChange} = this.props
|
||||
|
||||
return (
|
||||
<div className="logs-viewer--search-bar">
|
||||
<div className="logs-viewer--search-input">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search logs using Keywords or Regular Expressions..."
|
||||
value={searchString}
|
||||
onChange={onChange}
|
||||
onKeyDown={this.handleInputKeyDown}
|
||||
className="form-control input-sm"
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span className="icon search" />
|
||||
</div>
|
||||
<button className="btn btn-sm btn-primary" onClick={onSearch}>
|
||||
<span className="icon search" />
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private handleInputKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key === 'Enter') {
|
||||
return this.props.onSearch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsSearchBar
|
|
@ -0,0 +1,17 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
|
||||
interface Props {
|
||||
thing: string
|
||||
}
|
||||
|
||||
class LogsTableContainer extends PureComponent<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="logs-viewer--table-container">
|
||||
<p>{this.props.thing}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default LogsTableContainer
|
|
@ -1,6 +1,7 @@
|
|||
import React, {Component} from 'react'
|
||||
import classnames from 'classnames'
|
||||
import moment from 'moment'
|
||||
import _ from 'lodash'
|
||||
|
||||
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
|
||||
import timeRanges from 'src/logs/data/timeRanges'
|
||||
|
@ -56,16 +57,12 @@ class TimeRangeDropdown extends Component<Props, State> {
|
|||
|
||||
public render() {
|
||||
const {selected, preventCustomTimeRange, page} = this.props
|
||||
const {customTimeRange, isCustomTimeRangeOpen, isOpen} = this.state
|
||||
const {customTimeRange, isCustomTimeRangeOpen} = this.state
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className="time-range-dropdown">
|
||||
<div
|
||||
className={classnames('dropdown dropdown-290', {
|
||||
open: isOpen,
|
||||
})}
|
||||
>
|
||||
<div className={this.dropdownClassName}>
|
||||
<div
|
||||
className="btn btn-sm btn-default dropdown-toggle"
|
||||
onClick={this.toggleMenu}
|
||||
|
@ -132,6 +129,21 @@ class TimeRangeDropdown extends Component<Props, State> {
|
|||
)
|
||||
}
|
||||
|
||||
private get dropdownClassName(): string {
|
||||
const {
|
||||
isOpen,
|
||||
customTimeRange: {lower, upper},
|
||||
} = this.state
|
||||
|
||||
const absoluteTimeRange = !_.isEmpty(lower) && !_.isEmpty(upper)
|
||||
|
||||
return classnames('dropdown', {
|
||||
'dropdown-290': absoluteTimeRange,
|
||||
'dropdown-120': !absoluteTimeRange,
|
||||
open: isOpen,
|
||||
})
|
||||
}
|
||||
|
||||
private findTimeRangeInputValue = ({upper, lower}: TimeRange) => {
|
||||
if (upper && lower) {
|
||||
if (upper === 'now()') {
|
||||
|
|
|
@ -1,9 +1,29 @@
|
|||
import React, {PureComponent} from 'react'
|
||||
import React, {PureComponent, ChangeEvent} from 'react'
|
||||
import {connect} from 'react-redux'
|
||||
import {getSourceAsync, setTimeRange, setNamespace} from 'src/logs/actions'
|
||||
import {
|
||||
getSourceAndPopulateNamespacesAsync,
|
||||
setTimeRangeAsync,
|
||||
setNamespaceAsync,
|
||||
executeHistogramQueryAsync,
|
||||
changeZoomAsync,
|
||||
} from 'src/logs/actions'
|
||||
import {getSourcesAsync} from 'src/shared/actions/sources'
|
||||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
import LogViewerHeader from 'src/logs/components/LogViewerHeader'
|
||||
import Graph from 'src/logs/components/LogsGraph'
|
||||
import Table from 'src/logs/components/LogsTable'
|
||||
import SearchBar from 'src/logs/components/LogsSearchBar'
|
||||
import FilterBar from 'src/logs/components/LogsFilterBar'
|
||||
import LogViewerChart from 'src/logs/components/LogViewerChart'
|
||||
|
||||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
|
||||
export interface Filter {
|
||||
id: string
|
||||
key: string
|
||||
value: string
|
||||
operator: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sources: Source[]
|
||||
|
@ -12,12 +32,39 @@ interface Props {
|
|||
currentNamespace: Namespace
|
||||
getSource: (sourceID: string) => void
|
||||
getSources: () => void
|
||||
setTimeRange: (timeRange: TimeRange) => void
|
||||
setNamespace: (namespace: Namespace) => void
|
||||
setTimeRangeAsync: (timeRange: TimeRange) => void
|
||||
setNamespaceAsync: (namespace: Namespace) => void
|
||||
changeZoomAsync: (timeRange: TimeRange) => void
|
||||
executeHistogramQueryAsync: () => void
|
||||
timeRange: TimeRange
|
||||
histogramData: object[]
|
||||
}
|
||||
|
||||
class LogsPage extends PureComponent<Props> {
|
||||
interface State {
|
||||
searchString: string
|
||||
filters: Filter[]
|
||||
}
|
||||
|
||||
const DUMMY_FILTERS = [
|
||||
{
|
||||
id: '0',
|
||||
key: 'host',
|
||||
value: 'prod1-rsavage.local',
|
||||
operator: '==',
|
||||
enabled: true,
|
||||
},
|
||||
]
|
||||
|
||||
class LogsPage extends PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
searchString: '',
|
||||
filters: DUMMY_FILTERS,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
if (!this.props.currentSource) {
|
||||
this.props.getSource(this.props.sources[0].id)
|
||||
|
@ -29,27 +76,47 @@ class LogsPage extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {searchString, filters} = this.state
|
||||
|
||||
return (
|
||||
<div className="page hosts-list-page">
|
||||
<div className="page-header full-width">
|
||||
<div className="page-header__container">
|
||||
<div className="page-header__left">
|
||||
<h1 className="page-header__title">Log Viewer</h1>
|
||||
</div>
|
||||
<div className="page-header__right">{this.header}</div>
|
||||
</div>
|
||||
<div className="page">
|
||||
{this.header}
|
||||
<div className="page-contents logs-viewer">
|
||||
<Graph>{this.chart}</Graph>
|
||||
<SearchBar
|
||||
searchString={searchString}
|
||||
onChange={this.handleSearchInputChange}
|
||||
onSearch={this.handleSubmitSearch}
|
||||
/>
|
||||
<FilterBar
|
||||
numResults={300}
|
||||
filters={filters}
|
||||
onUpdateFilters={this.handleUpdateFilters}
|
||||
/>
|
||||
<Table thing="snooo" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get chart(): JSX.Element {
|
||||
const {histogramData, timeRange} = this.props
|
||||
return (
|
||||
<LogViewerChart
|
||||
timeRange={timeRange}
|
||||
data={histogramData}
|
||||
onZoom={this.handleChartZoom}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
private get header(): JSX.Element {
|
||||
const {
|
||||
sources,
|
||||
currentSource,
|
||||
currentNamespaces,
|
||||
timeRange,
|
||||
currentNamespace,
|
||||
timeRange,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
|
@ -66,8 +133,23 @@ class LogsPage extends PureComponent<Props> {
|
|||
)
|
||||
}
|
||||
|
||||
private handleSearchInputChange = (
|
||||
e: ChangeEvent<HTMLInputElement>
|
||||
): void => {
|
||||
this.setState({searchString: e.target.value})
|
||||
}
|
||||
|
||||
private handleSubmitSearch = (): void => {
|
||||
// do the thing
|
||||
}
|
||||
|
||||
private handleUpdateFilters = (filters: Filter[]): void => {
|
||||
this.setState({filters})
|
||||
}
|
||||
|
||||
private handleChooseTimerange = (timeRange: TimeRange) => {
|
||||
this.props.setTimeRange(timeRange)
|
||||
this.props.setTimeRangeAsync(timeRange)
|
||||
this.props.executeHistogramQueryAsync()
|
||||
}
|
||||
|
||||
private handleChooseSource = (sourceID: string) => {
|
||||
|
@ -75,27 +157,41 @@ class LogsPage extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
private handleChooseNamespace = (namespace: Namespace) => {
|
||||
// Do flip
|
||||
this.props.setNamespace(namespace)
|
||||
this.props.setNamespaceAsync(namespace)
|
||||
}
|
||||
|
||||
private handleChartZoom = (lower, upper) => {
|
||||
if (lower) {
|
||||
this.props.changeZoomAsync({lower, upper})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({
|
||||
sources,
|
||||
logs: {currentSource, currentNamespaces, timeRange, currentNamespace},
|
||||
logs: {
|
||||
currentSource,
|
||||
currentNamespaces,
|
||||
timeRange,
|
||||
currentNamespace,
|
||||
histogramData,
|
||||
},
|
||||
}) => ({
|
||||
sources,
|
||||
currentSource,
|
||||
currentNamespaces,
|
||||
timeRange,
|
||||
currentNamespace,
|
||||
histogramData,
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
getSource: getSourceAsync,
|
||||
getSource: getSourceAndPopulateNamespacesAsync,
|
||||
getSources: getSourcesAsync,
|
||||
setTimeRange,
|
||||
setNamespace,
|
||||
setTimeRangeAsync,
|
||||
setNamespaceAsync,
|
||||
executeHistogramQueryAsync,
|
||||
changeZoomAsync,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LogsPage)
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import {Source, Namespace, TimeRange} from 'src/types'
|
||||
import {ActionTypes, Action} from 'src/logs/actions'
|
||||
import {LogsState} from 'src/types/localStorage'
|
||||
|
||||
interface LogsState {
|
||||
currentSource: Source | null
|
||||
currentNamespaces: Namespace[]
|
||||
currentNamespace: Namespace | null
|
||||
timeRange: TimeRange
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
const defaultState: LogsState = {
|
||||
currentSource: null,
|
||||
currentNamespaces: [],
|
||||
timeRange: {lower: 'now() - 1m', upper: null},
|
||||
currentNamespace: null,
|
||||
histogramQueryConfig: null,
|
||||
histogramData: [],
|
||||
}
|
||||
|
||||
export default (state: LogsState = defaultState, action: Action) => {
|
||||
|
@ -25,6 +20,13 @@ export default (state: LogsState = defaultState, action: Action) => {
|
|||
return {...state, timeRange: action.payload.timeRange}
|
||||
case ActionTypes.SetNamespace:
|
||||
return {...state, currentNamespace: action.payload.namespace}
|
||||
case ActionTypes.SetHistogramQueryConfig:
|
||||
return {...state, histogramQueryConfig: action.payload.queryConfig}
|
||||
case ActionTypes.SetHistogramData:
|
||||
return {...state, histogramData: action.payload.data}
|
||||
case ActionTypes.ChangeZoom:
|
||||
const {timeRange, data} = action.payload
|
||||
return {...state, timeRange, histogramData: data}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import uuid from 'uuid'
|
||||
import {TimeRange, Namespace, QueryConfig} from 'src/types'
|
||||
|
||||
const fields = [
|
||||
{
|
||||
alias: '',
|
||||
args: [
|
||||
{
|
||||
alias: '',
|
||||
type: 'field',
|
||||
value: 'message',
|
||||
},
|
||||
],
|
||||
type: 'func',
|
||||
value: 'count',
|
||||
},
|
||||
]
|
||||
|
||||
const defaultQueryConfig = {
|
||||
areTagsAccepted: false,
|
||||
fields,
|
||||
fill: '0',
|
||||
groupBy: {time: '2m', tags: []},
|
||||
measurement: 'syslog',
|
||||
rawText: null,
|
||||
shifts: [],
|
||||
tags: {},
|
||||
}
|
||||
|
||||
export const buildHistogramQueryConfig = (
|
||||
namespace: Namespace,
|
||||
range: TimeRange
|
||||
): QueryConfig => {
|
||||
const id = uuid.v4()
|
||||
return {
|
||||
...defaultQueryConfig,
|
||||
id,
|
||||
range,
|
||||
database: namespace.database,
|
||||
retentionPolicy: namespace.retentionPolicy,
|
||||
}
|
||||
}
|
|
@ -4,24 +4,7 @@ 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[]
|
||||
}
|
||||
import {Template} from 'src/types'
|
||||
|
||||
interface Query {
|
||||
host: string | string[]
|
||||
|
|
|
@ -4,6 +4,7 @@ import _ from 'lodash'
|
|||
import {fetchTimeSeries} from 'src/shared/apis/query'
|
||||
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
|
||||
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
|
||||
import {Template} from 'src/types'
|
||||
|
||||
interface Axes {
|
||||
bounds: {
|
||||
|
@ -21,25 +22,6 @@ interface Query {
|
|||
id: 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
|
||||
|
@ -49,6 +31,7 @@ export interface Props {
|
|||
axes: Axes
|
||||
editQueryStatus: () => void
|
||||
grabDataForDownload: (timeSeries: TimeSeriesServerResponse[]) => void
|
||||
onSetResolution?: (resolution: number) => void
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -180,9 +163,13 @@ const AutoRefresh = (
|
|||
)
|
||||
}
|
||||
|
||||
private setResolution = resolution => {
|
||||
private setResolution = (resolution: number) => {
|
||||
const {onSetResolution} = this.props
|
||||
if (resolution !== this.state.resolution) {
|
||||
this.setState({resolution})
|
||||
if (onSetResolution) {
|
||||
onSetResolution(resolution)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -88,8 +88,8 @@ class Dygraph extends Component {
|
|||
|
||||
this.dygraph = new Dygraphs(graphRef, timeSeries, {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
...OPTIONS,
|
||||
...options,
|
||||
})
|
||||
|
||||
const {w} = this.dygraph.getArea()
|
||||
|
|
|
@ -4,7 +4,7 @@ import WidgetCell from 'shared/components/WidgetCell'
|
|||
import LayoutCell from 'shared/components/LayoutCell'
|
||||
import RefreshingGraph from 'shared/components/RefreshingGraph'
|
||||
import {buildQueriesForLayouts} from 'utils/buildQueriesForLayouts'
|
||||
import {IS_STATIC_LEGEND} from 'src/shared/constants'
|
||||
import {IS_STATIC_LEGEND, defaultIntervalValue} from 'src/shared/constants'
|
||||
|
||||
import _ from 'lodash'
|
||||
|
||||
|
@ -24,6 +24,7 @@ const getSource = (cell, source, sources, defaultSource) => {
|
|||
class LayoutState extends Component {
|
||||
state = {
|
||||
cellData: [],
|
||||
resolution: defaultIntervalValue,
|
||||
}
|
||||
|
||||
grabDataForDownload = cellData => {
|
||||
|
@ -36,9 +37,14 @@ class LayoutState extends Component {
|
|||
{...this.props}
|
||||
{...this.state}
|
||||
grabDataForDownload={this.grabDataForDownload}
|
||||
onSetResolution={this.handleSetResolution}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
handleSetResolution = resolution => {
|
||||
this.setState({resolution})
|
||||
}
|
||||
}
|
||||
|
||||
const Layout = (
|
||||
|
@ -60,6 +66,7 @@ const Layout = (
|
|||
sources,
|
||||
onZoom,
|
||||
cellData,
|
||||
resolution,
|
||||
templates,
|
||||
timeRange,
|
||||
isEditable,
|
||||
|
@ -69,6 +76,7 @@ const Layout = (
|
|||
autoRefresh,
|
||||
manualRefresh,
|
||||
onDeleteCell,
|
||||
onSetResolution,
|
||||
onCancelEditCell,
|
||||
onStopAddAnnotation,
|
||||
onSummonOverlayTechnologies,
|
||||
|
@ -79,7 +87,9 @@ const Layout = (
|
|||
<LayoutCell
|
||||
cell={cell}
|
||||
cellData={cellData}
|
||||
templates={templates}
|
||||
isEditable={isEditable}
|
||||
resolution={resolution}
|
||||
onEditCell={onEditCell}
|
||||
onCloneCell={onCloneCell}
|
||||
onDeleteCell={onDeleteCell}
|
||||
|
@ -115,6 +125,7 @@ const Layout = (
|
|||
timeRange,
|
||||
host
|
||||
)}
|
||||
onSetResolution={onSetResolution}
|
||||
/>
|
||||
)}
|
||||
</LayoutCell>
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, {Component, ReactElement} from 'react'
|
|||
import _ from 'lodash'
|
||||
|
||||
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
|
||||
|
||||
import LayoutCellMenu from 'src/shared/components/LayoutCellMenu'
|
||||
import LayoutCellHeader from 'src/shared/components/LayoutCellHeader'
|
||||
import {notify} from 'src/shared/actions/notifications'
|
||||
|
@ -11,7 +10,9 @@ import download from 'src/external/download.js'
|
|||
import {ErrorHandling} from 'src/shared/decorators/errors'
|
||||
import {dataToCSV} from 'src/shared/parsing/dataToCSV'
|
||||
import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers'
|
||||
import {Cell, CellQuery} from 'src/types/dashboard'
|
||||
import {PREDEFINED_TEMP_VARS} from 'src/shared/constants'
|
||||
|
||||
import {Cell, CellQuery, Template} from 'src/types/'
|
||||
import {TimeSeriesServerResponse} from 'src/types/series'
|
||||
|
||||
interface Props {
|
||||
|
@ -23,6 +24,8 @@ interface Props {
|
|||
isEditable: boolean
|
||||
onCancelEditCell: () => void
|
||||
cellData: TimeSeriesServerResponse[]
|
||||
templates: Template[]
|
||||
resolution: number
|
||||
}
|
||||
|
||||
@ErrorHandling
|
||||
|
@ -44,12 +47,51 @@ export default class LayoutCell extends Component<Props> {
|
|||
onCSVDownload={this.handleCSVDownload}
|
||||
/>
|
||||
</Authorized>
|
||||
<LayoutCellHeader cellName={cell.name} isEditable={isEditable} />
|
||||
<LayoutCellHeader cellName={this.cellName} isEditable={isEditable} />
|
||||
<div className="dash-graph--container">{this.renderGraph}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private get cellName(): string {
|
||||
const {
|
||||
cell: {name},
|
||||
} = this.props
|
||||
return this.replaceTemplateVariables(name)
|
||||
}
|
||||
|
||||
private get userDefinedTemplateVariables(): Template[] {
|
||||
const {templates} = this.props
|
||||
return templates.filter(temp => {
|
||||
const isPredefinedTempVar: boolean = !!PREDEFINED_TEMP_VARS.find(
|
||||
t => t === temp.tempVar
|
||||
)
|
||||
return !isPredefinedTempVar
|
||||
})
|
||||
}
|
||||
|
||||
private replaceTemplateVariables = (str: string): string => {
|
||||
const isTemplated: boolean = _.get(str.match(/:/g), 'length', 0) >= 2 // tempVars are wrapped in :
|
||||
|
||||
if (isTemplated) {
|
||||
const renderedString = _.reduce<Template, string>(
|
||||
this.userDefinedTemplateVariables,
|
||||
(acc, template) => {
|
||||
const {tempVar} = template
|
||||
const templateValue = template.values.find(v => v.selected)
|
||||
const value = templateValue.value
|
||||
const regex = new RegExp(tempVar, 'g')
|
||||
return acc.replace(regex, value)
|
||||
},
|
||||
str
|
||||
)
|
||||
|
||||
return renderedString
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
private get queries(): CellQuery[] {
|
||||
const {cell} = this.props
|
||||
return _.get(cell, ['queries'], [])
|
||||
|
|
|
@ -199,6 +199,7 @@ LineGraph.propTypes = {
|
|||
displayOptions: shape({
|
||||
stepPlot: bool,
|
||||
stackedGraph: bool,
|
||||
animatedZooms: bool,
|
||||
}),
|
||||
activeQueryIndex: number,
|
||||
ruleValues: shape({}),
|
||||
|
|
|
@ -40,6 +40,7 @@ const RefreshingGraph = ({
|
|||
fieldOptions,
|
||||
timeFormat,
|
||||
decimalPlaces,
|
||||
onSetResolution,
|
||||
resizerTopHeight,
|
||||
staticLegend,
|
||||
manualRefresh, // when changed, re-mounts the component
|
||||
|
@ -72,6 +73,7 @@ const RefreshingGraph = ({
|
|||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
inView={inView}
|
||||
onSetResolution={onSetResolution}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -92,6 +94,7 @@ const RefreshingGraph = ({
|
|||
prefix={prefix}
|
||||
suffix={suffix}
|
||||
inView={inView}
|
||||
onSetResolution={onSetResolution}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -118,6 +121,7 @@ const RefreshingGraph = ({
|
|||
grabDataForDownload={grabDataForDownload}
|
||||
handleSetHoverTime={handleSetHoverTime}
|
||||
isInCEO={isInCEO}
|
||||
onSetResolution={onSetResolution}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -147,6 +151,7 @@ const RefreshingGraph = ({
|
|||
grabDataForDownload={grabDataForDownload}
|
||||
handleSetHoverTime={handleSetHoverTime}
|
||||
showSingleStat={type === 'line-plus-single-stat'}
|
||||
onSetResolution={onSetResolution}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -197,6 +202,7 @@ RefreshingGraph.propTypes = {
|
|||
hoverTime: string.isRequired,
|
||||
handleSetHoverTime: func.isRequired,
|
||||
isInCEO: bool,
|
||||
onSetResolution: func,
|
||||
}
|
||||
|
||||
RefreshingGraph.defaultProps = {
|
||||
|
|
|
@ -438,8 +438,9 @@ export const DEFAULT_SOURCE = {
|
|||
metaUrl: '',
|
||||
}
|
||||
|
||||
export const defaultIntervalValue = '333'
|
||||
export const intervalValuesPoints = [
|
||||
{value: '333', type: 'points', selected: true},
|
||||
{value: defaultIntervalValue, type: 'points', selected: true},
|
||||
]
|
||||
|
||||
export const interval = {
|
||||
|
|
|
@ -640,3 +640,8 @@ export const ifqlUpdated = {
|
|||
...defaultSuccessNotification,
|
||||
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,
|
||||
}
|
||||
}
|
|
@ -80,7 +80,7 @@ class SideNav extends PureComponent<Props> {
|
|||
<FeatureFlag name="log-viewer">
|
||||
<NavBlock
|
||||
highlightWhen={['logs']}
|
||||
icon="cubo-node"
|
||||
icon="text-block"
|
||||
link={'/logs'}
|
||||
location={location}
|
||||
>
|
||||
|
|
|
@ -83,6 +83,7 @@
|
|||
@import 'pages/tickscript-editor';
|
||||
@import 'pages/time-machine';
|
||||
@import 'pages/manage-providers';
|
||||
@import 'pages/logs-viewer';
|
||||
|
||||
// TODO
|
||||
@import 'unsorted';
|
||||
|
|
|
@ -174,7 +174,7 @@
|
|||
min-width: 350px;
|
||||
user-select: text;
|
||||
transform: translateX(-50%);
|
||||
box-shadow: 0 0 10px 2px $g2-kevlar;
|
||||
@extend %drop-shadow;
|
||||
|
||||
&.hidden {
|
||||
display: none !important;
|
||||
|
|
|
@ -13,15 +13,25 @@
|
|||
@include gradient-v($g2-kevlar, $g0-obsidian);
|
||||
}
|
||||
|
||||
.time-machine--graph {
|
||||
width: calc(100% - 60px);
|
||||
height: calc(100% - 60px);
|
||||
.time-machine--sidebar,
|
||||
.time-machine--vis {
|
||||
background-color: $g3-castle;
|
||||
border-radius: $radius;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
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 {
|
||||
|
@ -34,7 +44,6 @@
|
|||
|
||||
.time-machine--graph-header .nav.nav-tablist {
|
||||
width: 180px;
|
||||
|
||||
li {
|
||||
justify-content: center;
|
||||
flex: 1 0 0;
|
||||
|
|
|
@ -125,3 +125,8 @@ $scrollbar-offset: 3px;
|
|||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
// Shadows
|
||||
%drop-shadow {
|
||||
box-shadow: 0 0 10px 2px $g2-kevlar;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
Styles for Logs Viewer Page
|
||||
----------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
$logs-viewer-graph-height: 240px;
|
||||
$logs-viewer-search-height: 46px;
|
||||
$logs-viewer-filter-height: 42px;
|
||||
$logs-viewer-gutter: 60px;
|
||||
|
||||
.logs-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.logs-viewer--graph-container {
|
||||
padding: 22px ($logs-viewer-gutter - 16px) 10px ($logs-viewer-gutter - 16px);
|
||||
height: $logs-viewer-graph-height;
|
||||
@include gradient-v($g2-kevlar, $g0-obsidian);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.logs-viewer--search-bar {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex-wrap: nowrap;
|
||||
padding: 0 $logs-viewer-gutter;
|
||||
height: $logs-viewer-search-height;
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
.logs-viewer--table-container {
|
||||
padding: 12px $logs-viewer-gutter 30px $logs-viewer-gutter;
|
||||
height: calc(100% - #{$logs-viewer-graph-height + $logs-viewer-search-height + $logs-viewer-filter-height});
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
// Search Bar
|
||||
.logs-viewer--search-input {
|
||||
flex: 1 0 0;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
|
||||
> span.icon.search {
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 12px;
|
||||
transform: translateY(-50%);
|
||||
color: $g8-storm;
|
||||
transition: color 0.25s ease;
|
||||
}
|
||||
|
||||
> input.form-control.input-sm {
|
||||
padding-left: 30px;
|
||||
}
|
||||
|
||||
> input.form-control.input-sm:focus + span.icon.search {
|
||||
color: $c-pool;
|
||||
}
|
||||
}
|
||||
|
||||
// Filters Bar
|
||||
.logs-viewer--filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include no-user-select();
|
||||
padding: 0 $logs-viewer-gutter;
|
||||
height: $logs-viewer-filter-height;
|
||||
background-color: $g3-castle;
|
||||
}
|
||||
|
||||
.logs-viewer--results-text {
|
||||
margin: 0 12px 0 33px;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
line-height: 13px;
|
||||
font-weight: 500;
|
||||
color: $g9-mountain;
|
||||
|
||||
strong {
|
||||
color: $g15-platinum;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer--filters {
|
||||
flex: 1 0 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logs-viewer--filter {
|
||||
position: relative;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
padding: 0 2px 0 8px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
background-color: $g5-pepper;
|
||||
color: $g13-mist;
|
||||
font-weight: 600;
|
||||
margin: 2px;
|
||||
|
||||
&.disabled {
|
||||
background-color: $g4-onyx;
|
||||
color: $g9-mountain;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $g6-smoke;
|
||||
color: $g15-platinum;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.logs-viewer--filter-remove {
|
||||
outline: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
position: relative;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
background-color: $g8-storm;
|
||||
transition: background-color 0.25s ease;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:before {
|
||||
transform: translate(-50%, -50%) rotate(-45deg);
|
||||
}
|
||||
|
||||
&:after {
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
background-color: $c-dreamsicle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logs-viewer--filter-tooltip {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-radius: $radius;
|
||||
z-index: 9999;
|
||||
@extend %drop-shadow;
|
||||
background-color: $g4-onyx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $g11-sidewalk;
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: $g5-pepper;
|
||||
color: $g18-cloud;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Graph
|
||||
.logs-viewer--graph {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px 16px;
|
||||
}
|
|
@ -96,7 +96,7 @@ export enum CellType {
|
|||
Guide = 'guide',
|
||||
}
|
||||
|
||||
interface TemplateValue {
|
||||
export interface TemplateValue {
|
||||
value: string
|
||||
type: string
|
||||
selected: boolean
|
||||
|
|
|
@ -10,7 +10,7 @@ export type OnGenerateScript = (script: string) => void
|
|||
export type OnChangeScript = (script: string) => void
|
||||
export type OnSubmitScript = () => void
|
||||
|
||||
export interface Status {
|
||||
export interface ScriptStatus {
|
||||
type: string
|
||||
text: string
|
||||
}
|
||||
|
@ -122,3 +122,12 @@ export interface Links {
|
|||
suggestions: 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 {AuthLinks, Organization, Role, User, Me} from './auth'
|
||||
import {Template, Cell, CellQuery, Legend, Axes} from './dashboard'
|
||||
|
@ -21,6 +22,7 @@ import {AlertRule, Kapacitor, Task} from './kapacitor'
|
|||
import {Source, SourceLinks} from './sources'
|
||||
import {DropdownAction, DropdownItem} from './shared'
|
||||
import {Notification, NotificationFunc} from './notifications'
|
||||
import {ScriptResult, ScriptStatus} from './ifql'
|
||||
|
||||
export {
|
||||
Me,
|
||||
|
@ -58,4 +60,8 @@ export {
|
|||
Axes,
|
||||
Service,
|
||||
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,4 +1,13 @@
|
|||
import {QueryConfig, TimeRange} from 'src/types'
|
||||
import {QueryConfig, TimeRange, Namespace, Source} from 'src/types'
|
||||
|
||||
export interface LogsState {
|
||||
currentSource: Source | null
|
||||
currentNamespaces: Namespace[]
|
||||
currentNamespace: Namespace | null
|
||||
timeRange: TimeRange
|
||||
histogramQueryConfig: QueryConfig | null
|
||||
histogramData: object[]
|
||||
}
|
||||
|
||||
export interface LocalStorage {
|
||||
VERSION: VERSION
|
||||
|
@ -8,6 +17,7 @@ export interface LocalStorage {
|
|||
dataExplorerQueryConfigs: DataExplorerQueryConfigs
|
||||
timeRange: TimeRange
|
||||
script: string
|
||||
logs: LogsState
|
||||
}
|
||||
|
||||
export type VERSION = 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 {
|
||||
TEMP_VAR_DASHBOARD_TIME,
|
||||
TEMP_VAR_UPPER_DASHBOARD_TIME,
|
||||
} 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) || {
|
||||
defaultGroupBy: '5m',
|
||||
}
|
||||
const {wheres, groupbys} = query
|
||||
|
||||
let text = query.text
|
||||
let text = query.query
|
||||
|
||||
if (upper) {
|
||||
text += ` where time > '${lower}' AND time < '${upper}'`
|
||||
|
@ -43,7 +48,12 @@ const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
|
|||
return text
|
||||
}
|
||||
|
||||
export const buildQueriesForLayouts = (cell, source, timeRange, host) => {
|
||||
export const buildQueriesForLayouts = (
|
||||
cell,
|
||||
source: Source,
|
||||
timeRange: TimeRange,
|
||||
host: string
|
||||
) => {
|
||||
return cell.queries.map(query => {
|
||||
let queryText
|
||||
// Canned dashboards use an different a schema different from queryConfig.
|
|
@ -8,15 +8,9 @@ import {
|
|||
TYPE_IFQL,
|
||||
} from 'src/dashboards/constants'
|
||||
import {shiftTimeRange} from 'src/shared/query/helpers'
|
||||
import {QueryConfig, Field, GroupBy, TimeShift} from 'src/types'
|
||||
import {QueryConfig, Field, GroupBy, TimeShift, TimeRange} from 'src/types'
|
||||
|
||||
export const quoteIfTimestamp = ({
|
||||
lower,
|
||||
upper,
|
||||
}: {
|
||||
lower: string
|
||||
upper: string
|
||||
}): {lower: string; upper: string} => {
|
||||
export const quoteIfTimestamp = ({lower, upper}: TimeRange): TimeRange => {
|
||||
if (lower && lower.includes('Z') && !lower.includes("'")) {
|
||||
lower = `'${lower}'`
|
||||
}
|
||||
|
@ -29,7 +23,7 @@ export const quoteIfTimestamp = ({
|
|||
}
|
||||
|
||||
export default function buildInfluxQLQuery(
|
||||
timeRange,
|
||||
timeRange: TimeRange,
|
||||
config: QueryConfig,
|
||||
shift: string = ''
|
||||
): string {
|
||||
|
@ -66,7 +60,7 @@ function buildSelect(
|
|||
// type arg will reason about new query types i.e. IFQL, GraphQL, or queryConfig
|
||||
export const buildQuery = (
|
||||
type: string,
|
||||
timeRange: object,
|
||||
timeRange: TimeRange,
|
||||
config: QueryConfig,
|
||||
shift: TimeShift | null = null
|
||||
): string => {
|
||||
|
@ -201,5 +195,7 @@ function buildFill(fill: string): string {
|
|||
return ` FILL(${fill})`
|
||||
}
|
||||
|
||||
export const buildRawText = (config: QueryConfig, timeRange: object): string =>
|
||||
config.rawText || buildInfluxQLQuery(timeRange, config) || ''
|
||||
export const buildRawText = (
|
||||
config: QueryConfig,
|
||||
timeRange: TimeRange
|
||||
): string => config.rawText || buildInfluxQLQuery(timeRange, config) || ''
|
||||
|
|
|
@ -6,14 +6,14 @@ const setup = () => {
|
|||
const props = {
|
||||
script: '',
|
||||
body: [],
|
||||
data: '',
|
||||
status: {type: '', text: ''},
|
||||
data: [],
|
||||
suggestions: [],
|
||||
onSubmitScript: () => {},
|
||||
onChangeScript: () => {},
|
||||
onAnalyze: () => {},
|
||||
onAppendFrom: () => {},
|
||||
onAppendJoin: () => {},
|
||||
status: {type: '', text: ''},
|
||||
}
|
||||
|
||||
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"
|
||||
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":
|
||||
version "15.5.2"
|
||||
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"
|
||||
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:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"
|
||||
|
|
Loading…
Reference in New Issue