@ -1,8 +1,9 @@
import React from 'react' ;
import { hot } from 'react-hot-loader' ;
import Select from 'react-select' ;
import _ from 'lodash' ;
import { ExploreState , ExploreUrlState , Query } from 'app/types/explore' ;
import { ExploreState , ExploreUrlState , HistoryItem , Query , QueryTransaction , Range } from 'app/types/explore' ;
import kbn from 'app/core/utils/kbn' ;
import colors from 'app/core/utils/colors' ;
import store from 'app/core/store' ;
@ -15,7 +16,6 @@ import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'
import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage' ;
import TableModel , { mergeTablesIntoModel } from 'app/core/table_model' ;
import ElapsedTime from './ElapsedTime' ;
import QueryRows from './QueryRows' ;
import Graph from './Graph' ;
import Logs from './Logs' ;
@ -53,6 +53,25 @@ function makeTimeSeriesList(dataList, options) {
} ) ;
}
/ * *
* Update the query history . Side - effect : store history in local storage
* /
function updateHistory ( history : HistoryItem [ ] , datasourceId : string , queries : string [ ] ) : HistoryItem [ ] {
const ts = Date . now ( ) ;
queries . forEach ( query = > {
history = [ { query , ts } , . . . history ] ;
} ) ;
if ( history . length > MAX_HISTORY_ITEMS ) {
history = history . slice ( 0 , MAX_HISTORY_ITEMS ) ;
}
// Combine all queries of a datasource type into one history
const historyKey = ` grafana.explore.history. ${ datasourceId } ` ;
store . setObject ( historyKey , history ) ;
return history ;
}
interface ExploreProps {
datasourceSrv : any ;
onChangeSplit : ( split : boolean , state? : ExploreState ) = > void ;
@ -83,6 +102,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} else {
const { datasource , queries , range } = props . urlState as ExploreUrlState ;
initialQueries = ensureQueries ( queries ) ;
const initialRange = range || { . . . DEFAULT_RANGE } ;
this . state = {
datasource : null ,
datasourceError : null ,
@ -90,23 +110,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceMissing : false ,
datasourceName : datasource ,
exploreDatasources : [ ] ,
graphResult : null ,
graphRange : initialRange ,
history : [ ] ,
latency : 0 ,
loading : false ,
logsResult : null ,
queries : initialQueries ,
queryErrors : [ ] ,
queryHints : [ ] ,
range : range || { . . . DEFAULT_RANGE } ,
requestOptions : null ,
queryTransactions : [ ] ,
range : initialRange ,
showingGraph : true ,
showingLogs : true ,
showingTable : true ,
supportsGraph : null ,
supportsLogs : null ,
supportsTable : null ,
tableResult : null ,
} ;
}
this . queryExpressions = initialQueries . map ( q = > q . query ) ;
@ -200,14 +215,30 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} ;
onAddQueryRow = index = > {
const { queries } = this . state ;
const { queries , queryTransactions } = this . state ;
// Local cache
this . queryExpressions [ index + 1 ] = '' ;
// Add row by generating new react key
const nextQueries = [
. . . queries . slice ( 0 , index + 1 ) ,
{ query : '' , key : generateQueryKey ( ) } ,
. . . queries . slice ( index + 1 ) ,
] ;
this . setState ( { queries : nextQueries } ) ;
// Ongoing transactions need to update their row indices
const nextQueryTransactions = queryTransactions . map ( qt = > {
if ( qt . rowIndex > index ) {
return {
. . . qt ,
rowIndex : qt.rowIndex + 1 ,
} ;
}
return qt ;
} ) ;
this . setState ( { queries : nextQueries , queryTransactions : nextQueryTransactions } ) ;
} ;
onChangeDatasource = async option = > {
@ -215,12 +246,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasource : null ,
datasourceError : null ,
datasourceLoading : true ,
graphResult : null ,
latency : 0 ,
logsResult : null ,
queryErrors : [ ] ,
queryHints : [ ] ,
tableResult : null ,
queryTransactions : [ ] ,
} ) ;
const datasourceName = option . value ;
const datasource = await this . props . datasourceSrv . get ( datasourceName ) ;
@ -231,9 +258,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
// Keep current value in local cache
this . queryExpressions [ index ] = value ;
// Replace query row on override
if ( override ) {
const { queries } = this . state ;
// Replace query row
const { queries , queryTransactions } = this . state ;
const nextQuery : Query = {
key : generateQueryKey ( index ) ,
query : value ,
@ -241,11 +268,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const nextQueries = [ . . . queries ] ;
nextQueries [ index ] = nextQuery ;
// Discard ongoing transaction related to row query
const nextQueryTransactions = queryTransactions . filter ( qt = > qt . rowIndex !== index ) ;
this . setState (
{
queryErrors : [ ] ,
queryHints : [ ] ,
queries : nextQueries ,
queryHints : [ ] ,
queryTransactions : nextQueryTransactions ,
} ,
this . onSubmit
) ;
@ -264,13 +294,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
this . queryExpressions = [ '' ] ;
this . setState (
{
graphResult : null ,
logsResult : null ,
latency : 0 ,
queries : ensureQueries ( ) ,
queryErrors : [ ] ,
queryHints : [ ] ,
tableResult : null ,
queryTransactions : [ ] ,
} ,
this . saveState
) ;
@ -308,15 +334,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} ;
onModifyQueries = ( action : object , index? : number ) = > {
const { datasource , queries } = this . state ;
const { datasource , queries , queryTransactions } = this . state ;
if ( datasource && datasource . modifyQuery ) {
let nextQueries ;
let nextQueryTransactions ;
if ( index === undefined ) {
// Modify all queries
nextQueries = queries . map ( ( q , i ) = > ( {
key : generateQueryKey ( i ) ,
query : datasource.modifyQuery ( this . queryExpressions [ i ] , action ) ,
} ) ) ;
// Discard all ongoing transactions
nextQueryTransactions = [ ] ;
} else {
// Modify query only at index
nextQueries = [
@ -327,20 +356,41 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
} ,
. . . queries . slice ( index + 1 ) ,
] ;
// Discard transactions related to row query
nextQueryTransactions = queryTransactions . filter ( qt = > qt . rowIndex !== index ) ;
}
this . queryExpressions = nextQueries . map ( q = > q . query ) ;
this . setState ( { queries : nextQueries } , ( ) = > this . onSubmit ( ) ) ;
this . setState (
{
queries : nextQueries ,
queryTransactions : nextQueryTransactions ,
} ,
( ) = > this . onSubmit ( )
) ;
}
} ;
onRemoveQueryRow = index = > {
const { queries } = this . state ;
const { queries , queryTransactions } = this . state ;
if ( queries . length <= 1 ) {
return ;
}
// Remove from local cache
this . queryExpressions = [ . . . this . queryExpressions . slice ( 0 , index ) , . . . this . queryExpressions . slice ( index + 1 ) ] ;
// Remove row from react state
const nextQueries = [ . . . queries . slice ( 0 , index ) , . . . queries . slice ( index + 1 ) ] ;
this . queryExpressions = nextQueries . map ( q = > q . query ) ;
this . setState ( { queries : nextQueries } , ( ) = > this . onSubmit ( ) ) ;
// Discard transactions related to row query
const nextQueryTransactions = queryTransactions . filter ( qt = > qt . rowIndex !== index ) ;
this . setState (
{
queries : nextQueries ,
queryTransactions : nextQueryTransactions ,
} ,
( ) = > this . onSubmit ( )
) ;
} ;
onSubmit = ( ) = > {
@ -349,7 +399,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
this . runTableQuery ( ) ;
}
if ( showingGraph && supportsGraph ) {
this . runGraphQuery ( ) ;
this . runGraphQueries ( ) ;
}
if ( showingLogs && supportsLogs ) {
this . runLogsQuery ( ) ;
@ -357,32 +407,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
this . saveState ( ) ;
} ;
onQuerySuccess ( datasourceId : string , queries : string [ ] ) : void {
// save queries to history
let { history } = this . state ;
const { datasource } = this . state ;
if ( datasource . meta . id !== datasourceId ) {
// Navigated away, queries did not matter
return ;
}
const ts = Date . now ( ) ;
queries . forEach ( query = > {
history = [ { query , ts } , . . . history ] ;
} ) ;
if ( history . length > MAX_HISTORY_ITEMS ) {
history = history . slice ( 0 , MAX_HISTORY_ITEMS ) ;
}
// Combine all queries of a datasource type into one history
const historyKey = ` grafana.explore.history. ${ datasourceId } ` ;
store . setObject ( historyKey , history ) ;
this . setState ( { history } ) ;
}
buildQueryOptions ( targetOptions : { format : string ; hinting? : boolean ; instant? : boolean } ) {
buildQueryOptions ( query : string , rowIndex : number , targetOptions : { format : string ; hinting? : boolean ; instant? : boolean } ) {
const { datasource , range } = this . state ;
const resolution = this . el . offsetWidth ;
const absoluteRange = {
@ -390,90 +415,215 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
to : parseDate ( range . to , true ) ,
} ;
const { interval } = kbn . calculateInterval ( absoluteRange , resolution , datasource . interval ) ;
const targets = this . queryExpressions . map ( ( q , i ) = > ( {
. . . targetOptions ,
// Target identifier is needed for table transformations
refId : i + 1 ,
expr : q ,
} ) ) ;
const targets = [
{
. . . targetOptions ,
// Target identifier is needed for table transformations
refId : rowIndex + 1 ,
expr : query ,
} ,
] ;
// Clone range for query request
const queryRange : Range = { . . . range } ;
return {
interval ,
range ,
targets ,
range : queryRange ,
} ;
}
startQueryTransaction ( query : string , rowIndex : number , resultType : string , options : any ) : QueryTransaction {
const queryOptions = this . buildQueryOptions ( query , rowIndex , options ) ;
const transaction : QueryTransaction = {
query ,
resultType ,
rowIndex ,
id : generateQueryKey ( ) ,
done : false ,
latency : 0 ,
options : queryOptions ,
} ;
// Using updater style because we might be modifying queryTransactions in quick succession
this . setState ( state = > {
const { queryTransactions } = state ;
// Discarding existing transactions of same type
const remainingTransactions = queryTransactions . filter (
qt = > ! ( qt . resultType === resultType && qt . rowIndex === rowIndex )
) ;
// Append new transaction
const nextQueryTransactions = [ . . . remainingTransactions , transaction ] ;
return {
queryHints : [ ] ,
queryTransactions : nextQueryTransactions ,
} ;
} ) ;
return transaction ;
}
async runGraphQuery() {
completeQueryTransaction (
transactionId : string ,
result : any ,
latency : number ,
hints : any [ ] ,
queries : string [ ] ,
datasourceId : string
) {
const { datasource } = this . state ;
if ( datasource . meta . id !== datasourceId ) {
// Navigated away, queries did not matter
return ;
}
this . setState ( state = > {
const { history , queryTransactions } = state ;
// Transaction might have been discarded
if ( ! queryTransactions . find ( qt = > qt . id === transactionId ) ) {
return null ;
}
// Mark transactions as complete
const nextQueryTransactions = queryTransactions . map ( qt = > {
if ( qt . id === transactionId ) {
return {
. . . qt ,
latency ,
result ,
done : true ,
} ;
}
return qt ;
} ) ;
const nextHistory = updateHistory ( history , datasourceId , queries ) ;
return {
history : nextHistory ,
queryHints : hints ,
queryTransactions : nextQueryTransactions ,
} ;
} ) ;
}
failQueryTransaction ( transactionId : string , error : string , datasourceId : string ) {
const { datasource } = this . state ;
if ( datasource . meta . id !== datasourceId ) {
// Navigated away, queries did not matter
return ;
}
this . setState ( state = > {
// Transaction might have been discarded
if ( ! state . queryTransactions . find ( qt = > qt . id === transactionId ) ) {
return null ;
}
// Mark transactions as complete
const nextQueryTransactions = state . queryTransactions . map ( qt = > {
if ( qt . id === transactionId ) {
return {
. . . qt ,
error ,
done : true ,
} ;
}
return qt ;
} ) ;
return {
queryTransactions : nextQueryTransactions ,
} ;
} ) ;
}
async runGraphQueries() {
const queries = [ . . . this . queryExpressions ] ;
if ( ! hasQuery ( queries ) ) {
return ;
}
this . setState ( { latency : 0 , loading : true , graphResult : null , queryErrors : [ ] , queryHints : [ ] } ) ;
const now = Date . now ( ) ;
const options = this . buildQueryOptions ( { format : 'time_series' , instant : false , hinting : true } ) ;
try {
const res = await datasource . query ( options ) ;
const result = makeTimeSeriesList ( res . data , options ) ;
const queryHints = res . hints ? makeHints ( res . hints ) : [ ] ;
const latency = Date . now ( ) - now ;
this . setState ( { latency , loading : false , graphResult : result , queryHints , requestOptions : options } ) ;
this . onQuerySuccess ( datasource . meta . id , queries ) ;
} catch ( response ) {
console . error ( response ) ;
const queryError = response . data ? response.data.error : response ;
this . setState ( { loading : false , queryErrors : [ queryError ] } ) ;
}
const { datasource } = this . state ;
const datasourceId = datasource . meta . id ;
// Run all queries concurrently
queries . forEach ( async ( query , rowIndex ) = > {
if ( query ) {
const transaction = this . startQueryTransaction ( query , rowIndex , 'Graph' , {
format : 'time_series' ,
instant : false ,
hinting : true ,
} ) ;
try {
const now = Date . now ( ) ;
const res = await datasource . query ( transaction . options ) ;
const latency = Date . now ( ) - now ;
const results = makeTimeSeriesList ( res . data , transaction . options ) ;
const queryHints = res . hints ? makeHints ( res . hints ) : [ ] ;
this . completeQueryTransaction ( transaction . id , results , latency , queryHints , queries , datasourceId ) ;
this . setState ( { graphRange : transaction.options.range } ) ;
} catch ( response ) {
console . error ( response ) ;
const queryError = response . data ? response.data.error : response ;
this . failQueryTransaction ( transaction . id , queryError , datasourceId ) ;
}
}
} ) ;
}
async runTableQuery() {
const queries = [ . . . this . queryExpressions ] ;
const { datasource } = this . state ;
if ( ! hasQuery ( queries ) ) {
return ;
}
this . setState ( { latency : 0 , loading : true , queryErrors : [ ] , queryHints : [ ] , tableResult : null } ) ;
const now = Date . now ( ) ;
const options = this . buildQueryOptions ( {
format : 'table' ,
instant : true ,
const { datasource } = this . state ;
const datasourceId = datasource . meta . id ;
// Run all queries concurrently
queries . forEach ( async ( query , rowIndex ) = > {
if ( query ) {
const transaction = this . startQueryTransaction ( query , rowIndex , 'Table' , { format : 'table' , instant : true } ) ;
try {
const now = Date . now ( ) ;
const res = await datasource . query ( transaction . options ) ;
const latency = Date . now ( ) - now ;
const results = mergeTablesIntoModel ( new TableModel ( ) , . . . res . data ) ;
this . completeQueryTransaction ( transaction . id , results , latency , [ ] , queries , datasourceId ) ;
} catch ( response ) {
console . error ( response ) ;
const queryError = response . data ? response.data.error : response ;
this . failQueryTransaction ( transaction . id , queryError , datasourceId ) ;
}
}
} ) ;
try {
const res = await datasource . query ( options ) ;
const tableModel = mergeTablesIntoModel ( new TableModel ( ) , . . . res . data ) ;
const latency = Date . now ( ) - now ;
this . setState ( { latency , loading : false , tableResult : tableModel , requestOptions : options } ) ;
this . onQuerySuccess ( datasource . meta . id , queries ) ;
} catch ( response ) {
console . error ( response ) ;
const queryError = response . data ? response.data.error : response ;
this . setState ( { loading : false , queryErrors : [ queryError ] } ) ;
}
}
async runLogsQuery() {
const queries = [ . . . this . queryExpressions ] ;
const { datasource } = this . state ;
if ( ! hasQuery ( queries ) ) {
return ;
}
this . setState ( { latency : 0 , loading : true , queryErrors : [ ] , queryHints : [ ] , logsResult : null } ) ;
const now = Date . now ( ) ;
const options = this . buildQueryOptions ( {
format : 'logs' ,
const { datasource } = this . state ;
const datasourceId = datasource . meta . id ;
// Run all queries concurrently
queries . forEach ( async ( query , rowIndex ) = > {
if ( query ) {
const transaction = this . startQueryTransaction ( query , rowIndex , 'Logs' , { format : 'logs' } ) ;
try {
const now = Date . now ( ) ;
const res = await datasource . query ( transaction . options ) ;
const latency = Date . now ( ) - now ;
const results = res . data ;
this . completeQueryTransaction ( transaction . id , results , latency , [ ] , queries , datasourceId ) ;
} catch ( response ) {
console . error ( response ) ;
const queryError = response . data ? response.data.error : response ;
this . failQueryTransaction ( transaction . id , queryError , datasourceId ) ;
}
}
} ) ;
try {
const res = await datasource . query ( options ) ;
const logsData = res . data ;
const latency = Date . now ( ) - now ;
this . setState ( { latency , loading : false , logsResult : logsData , requestOptions : options } ) ;
this . onQuerySuccess ( datasource . meta . id , queries ) ;
} catch ( response ) {
console . error ( response ) ;
const queryError = response . data ? response.data.error : response ;
this . setState ( { loading : false , queryErrors : [ queryError ] } ) ;
}
}
request = url = > {
@ -502,23 +652,18 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
datasourceLoading ,
datasourceMissing ,
exploreDatasources ,
graphResult ,
graphRang e ,
history ,
latency ,
loading ,
logsResult ,
queries ,
queryErrors ,
queryHints ,
queryTransactions ,
range ,
requestOptions ,
showingGraph ,
showingLogs ,
showingTable ,
supportsGraph ,
supportsLogs ,
supportsTable ,
tableResult ,
} = this . state ;
const showingBoth = showingGraph && showingTable ;
const graphHeight = showingBoth ? '200px' : '400px' ;
@ -527,6 +672,17 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
const tableButtonActive = showingBoth || showingTable ? 'active' : '' ;
const exploreClass = split ? 'explore explore-split' : 'explore' ;
const selectedDatasource = datasource ? exploreDatasources . find ( d = > d . label === datasource . name ) : undefined ;
const graphLoading = queryTransactions . some ( qt = > qt . resultType === 'Graph' && ! qt . done ) ;
const tableLoading = queryTransactions . some ( qt = > qt . resultType === 'Table' && ! qt . done ) ;
const logsLoading = queryTransactions . some ( qt = > qt . resultType === 'Logs' && ! qt . done ) ;
const graphResult = _ . flatten (
queryTransactions . filter ( qt = > qt . resultType === 'Graph' && qt . done && qt . result ) . map ( qt = > qt . result )
) ;
const tableResult = queryTransactions . filter ( qt = > qt . resultType === 'Table' && qt . done ) . map ( qt = > qt . result ) [ 0 ] ;
const logsResult = _ . flatten (
queryTransactions . filter ( qt = > qt . resultType === 'Logs' && qt . done ) . map ( qt = > qt . result )
) ;
const loading = queryTransactions . some ( qt = > ! qt . done ) ;
return (
< div className = { exploreClass } ref = { this . getRef } >
@ -539,12 +695,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
< / a >
< / div >
) : (
< div className = "navbar-buttons explore-first-button" >
< button className = "btn navbar-button" onClick = { this . onClickCloseSplit } >
Close Split
< div className = "navbar-buttons explore-first-button" >
< button className = "btn navbar-button" onClick = { this . onClickCloseSplit } >
Close Split
< / button >
< / div >
) }
< / div >
) }
{ ! datasourceMissing ? (
< div className = "navbar-buttons" >
< Select
@ -584,9 +740,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
< / div >
< div className = "navbar-buttons relative" >
< button className = "btn navbar-button--primary" onClick = { this . onSubmit } >
Run Query < i className = "fa fa-level-down run-icon" / >
Run Query { ' ' }
{ loading ? < i className = "fa fa-spinner fa-spin run-icon" / > : < i className = "fa fa-level-down run-icon" / > }
< / button >
{ loading || latency ? < ElapsedTime time = { latency } className = "text-info" / > : null }
< / div >
< / div >
@ -605,7 +761,6 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
< QueryRows
history = { history }
queries = { queries }
queryErrors = { queryErrors }
queryHints = { queryHints }
request = { this . request }
onAddQueryRow = { this . onAddQueryRow }
@ -614,6 +769,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onExecuteQuery = { this . onSubmit }
onRemoveQueryRow = { this . onRemoveQueryRow }
supportsLogs = { supportsLogs }
transactions = { queryTransactions }
/ >
< div className = "result-options" >
{ supportsGraph ? (
@ -635,23 +791,22 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
< main className = "m-t-2" >
{ supportsGraph &&
showingGraph &&
graphResult && (
showingGraph && (
< Graph
data = { graphResult }
height = { graphHeight }
loading = { l oading}
loading = { graphL oading}
id = { ` explore-graph- ${ position } ` }
options = { requestOptions }
range = { graphRange }
split = { split }
/ >
) }
{ supportsTable && showingTable ? (
< div className = "panel-container" >
< Table data = { tableResult } loading = { loading } onClickCell = { this . onClickTableCell } / >
< div className = "panel-container m-t-2 " >
< Table data = { tableResult } loading = { tab leL oading} onClickCell = { this . onClickTableCell } / >
< / div >
) : null }
{ supportsLogs && showingLogs ? < Logs data = { logsResult } loading = { loading } / > : null }
{ supportsLogs && showingLogs ? < Logs data = { logsResult } loading = { logsLo ading } / > : null }
< / main >
< / div >
) : null }