From 2e02a8c8550f656cd8895e283892eb722a3693e4 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 22 Oct 2018 17:51:42 +0200 Subject: [PATCH 1/5] Explore: query transactions Existing querying was grouped together before handed over to the datasource. This slowed down result display to however long the slowest query took. - create one query transaction per result viewer (graph, table, etc.) and query row - track latencies for each transaction - show results as soon as they are being received - loading indicator on graph and query button to indicate that queries are still running and that results are incomplete - properly discard transactions when removing or changing queries --- public/app/core/utils/explore.test.ts | 9 +- public/app/features/explore/Explore.tsx | 429 ++++++++++++------ public/app/features/explore/Graph.test.tsx | 23 +- public/app/features/explore/Graph.tsx | 42 +- public/app/features/explore/QueryRows.tsx | 12 +- .../features/explore/QueryTransactions.tsx | 42 ++ public/app/features/explore/Table.tsx | 2 +- .../explore/__snapshots__/Graph.test.tsx.snap | 29 +- public/app/types/explore.ts | 31 +- public/sass/pages/_explore.scss | 76 +++- 10 files changed, 484 insertions(+), 211 deletions(-) create mode 100644 public/app/features/explore/QueryTransactions.tsx diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 915b47e14e2..04159e81164 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -8,23 +8,18 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { datasourceMissing: false, datasourceName: '', exploreDatasources: [], - graphResult: null, + graphRange: DEFAULT_RANGE, history: [], - latency: 0, - loading: false, - logsResult: null, queries: [], - queryErrors: [], queryHints: [], + queryTransactions: [], range: DEFAULT_RANGE, - requestOptions: null, showingGraph: true, showingLogs: true, showingTable: true, supportsGraph: null, supportsLogs: null, supportsTable: null, - tableResult: null, }; describe('state functions', () => { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d7326a5bfd1..93c0847d6ed 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { } 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 { 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 { }; 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 { 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 { // 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 { 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 { 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 { }; 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 { }, ...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 { this.runTableQuery(); } if (showingGraph && supportsGraph) { - this.runGraphQuery(); + this.runGraphQueries(); } if (showingLogs && supportsLogs) { this.runLogsQuery(); @@ -357,32 +407,7 @@ export class Explore extends React.PureComponent { 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 { 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 { datasourceLoading, datasourceMissing, exploreDatasources, - graphResult, + graphRange, 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 { 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 (
@@ -539,12 +695,12 @@ export class Explore extends React.PureComponent {
) : ( -
- -
- )} + + )} {!datasourceMissing ? (
1 ? `Value #${refId}` : 'Value'; + const valueText = resultCount > 1 || valueWithRefId ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 1459d05ac75..a3f60f2006b 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -91,7 +91,7 @@ height: 2px; position: relative; overflow: hidden; - background: $table-border; + background: $text-color-faint; margin: $panel-margin / 2; } From a121cd0e49d37caa42b7ba92427f9b05dedf8c1e Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 24 Oct 2018 11:08:15 +0200 Subject: [PATCH 5/5] Fix race condition on add/remove query row --- public/app/features/explore/Explore.tsx | 167 +++++++++++----------- public/app/features/explore/QueryRows.tsx | 16 ++- 2 files changed, 94 insertions(+), 89 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index cc049a5c8bf..bac063116f1 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -33,16 +33,6 @@ import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; const MAX_HISTORY_ITEMS = 100; -function makeHints(transactions: QueryTransaction[]) { - const hintsByIndex = []; - transactions.forEach(qt => { - if (qt.hints && qt.hints.length > 0) { - hintsByIndex[qt.rowIndex] = qt.hints[0]; - } - }); - return hintsByIndex; -} - function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; @@ -222,30 +212,32 @@ export class Explore extends React.PureComponent { }; onAddQueryRow = index => { - 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(state => { + const { queries, queryTransactions } = state; - // 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; - }); + // 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, queryTransactions: nextQueryTransactions }); + // 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; + }); + + return { queries: nextQueries, queryTransactions: nextQueryTransactions }; + }); }; onChangeDatasource = async option => { @@ -265,25 +257,24 @@ export class Explore extends React.PureComponent { this.queryExpressions[index] = value; if (override) { - // Replace query row - const { queries, queryTransactions } = this.state; - const nextQuery: Query = { - key: generateQueryKey(index), - query: value, - }; - const nextQueries = [...queries]; - nextQueries[index] = nextQuery; + this.setState(state => { + // Replace query row + const { queries, queryTransactions } = state; + const nextQuery: Query = { + key: generateQueryKey(index), + query: value, + }; + const nextQueries = [...queries]; + nextQueries[index] = nextQuery; - // Discard ongoing transaction related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + // Discard ongoing transaction related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - this.setState( - { + return { queries: nextQueries, queryTransactions: nextQueryTransactions, - }, - this.onSubmit - ); + }; + }, this.onSubmit); } }; @@ -383,36 +374,39 @@ export class Explore extends React.PureComponent { }; onModifyQueries = (action: object, index?: number) => { - const { datasource, queries, queryTransactions } = this.state; + const { datasource } = 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 = [ - ...queries.slice(0, index), - { - key: generateQueryKey(index), - query: datasource.modifyQuery(this.queryExpressions[index], action), - }, - ...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, - queryTransactions: nextQueryTransactions, + state => { + const { queries, queryTransactions } = state; + 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 = [ + ...queries.slice(0, index), + { + key: generateQueryKey(index), + query: datasource.modifyQuery(this.queryExpressions[index], action), + }, + ...queries.slice(index + 1), + ]; + // Discard transactions related to row query + nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + } + this.queryExpressions = nextQueries.map(q => q.query); + return { + queries: nextQueries, + queryTransactions: nextQueryTransactions, + }; }, () => this.onSubmit() ); @@ -420,23 +414,25 @@ export class Explore extends React.PureComponent { }; onRemoveQueryRow = index => { - 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.setState( + state => { + const { queries, queryTransactions } = state; + if (queries.length <= 1) { + return null; + } + // Remove row from react state + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; - // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + // Discard transactions related to row query + const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); - this.setState( - { - queries: nextQueries, - queryTransactions: nextQueryTransactions, + return { + queries: nextQueries, + queryTransactions: nextQueryTransactions, + }; }, () => this.onSubmit() ); @@ -708,6 +704,7 @@ export class Explore extends React.PureComponent { // Copy state, but copy queries including modifications return { ...this.state, + queryTransactions: [], queries: ensureQueries(this.queryExpressions.map(query => ({ query }))), }; } @@ -758,7 +755,6 @@ export class Explore extends React.PureComponent { queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done).map(qt => qt.result) ); const loading = queryTransactions.some(qt => !qt.done); - const queryHints = makeHints(queryTransactions); return (
@@ -837,7 +833,6 @@ export class Explore extends React.PureComponent { qt.hints && qt.hints.length > 0); + if (transaction) { + return transaction.hints[0]; + } + return undefined; +} + class QueryRow extends PureComponent { onChangeQuery = (value, override?: boolean) => { const { index, onChangeQuery } = this.props; @@ -45,8 +55,9 @@ class QueryRow extends PureComponent { }; render() { - const { history, query, queryHint, request, supportsLogs, transactions } = this.props; + const { history, query, request, supportsLogs, transactions } = this.props; const transactionWithError = transactions.find(t => t.error); + const hint = getFirstHintFromTransactions(transactions); const queryError = transactionWithError ? transactionWithError.error : null; return (
@@ -56,7 +67,7 @@ class QueryRow extends PureComponent {
{ index={index} query={q.query} transactions={transactions.filter(t => t.rowIndex === index)} - queryHint={queryHints[index]} {...handlers} /> ))}