Commit 2e02a8c8 by David Kaltschmidt

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
parent e761fb19
......@@ -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', () => {
......
......@@ -4,24 +4,11 @@ import { Graph } from './Graph';
import { mockData } from './__mocks__/mockData';
const setup = (propOverrides?: object) => {
const props = Object.assign(
{
data: mockData().slice(0, 19),
options: {
interval: '20s',
range: { from: 'now-6h', to: 'now' },
targets: [
{
format: 'time_series',
instant: false,
hinting: true,
expr: 'prometheus_http_request_duration_seconds_bucket',
},
],
},
},
propOverrides
);
const props = {
data: mockData().slice(0, 19),
range: { from: 'now-6h', to: 'now' },
...propOverrides,
};
// Enzyme.shallow did not work well with jquery.flop. Mocking the draw function.
Graph.prototype.draw = jest.fn();
......
......@@ -5,6 +5,8 @@ import { withSize } from 'react-sizeme';
import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.time';
import { Range } from 'app/types/explore';
import * as dateMath from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2';
......@@ -74,7 +76,7 @@ interface GraphProps {
height?: string; // e.g., '200px'
id?: string;
loading?: boolean;
options: any;
range: Range;
split?: boolean;
size?: { width: number; height: number };
}
......@@ -101,7 +103,7 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
componentDidUpdate(prevProps: GraphProps) {
if (
prevProps.data !== this.props.data ||
prevProps.options !== this.props.options ||
prevProps.range !== this.props.range ||
prevProps.split !== this.props.split ||
prevProps.height !== this.props.height ||
(prevProps.size && prevProps.size.width !== this.props.size.width)
......@@ -120,22 +122,22 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
};
draw() {
const { options: userOptions, size } = this.props;
const { range, size } = this.props;
const data = this.getGraphData();
const $el = $(`#${this.props.id}`);
if (!data) {
$el.empty();
return;
let series = [{ data: [[0, 0]] }];
if (data && data.length > 0) {
series = data.map((ts: TimeSeries) => ({
color: ts.color,
label: ts.label,
data: ts.getFlotPairs('null'),
}));
}
const series = data.map((ts: TimeSeries) => ({
color: ts.color,
label: ts.label,
data: ts.getFlotPairs('null'),
}));
const ticks = (size.width || 0) / 100;
let { from, to } = userOptions.range;
let { from, to } = range;
if (!moment.isMoment(from)) {
from = dateMath.parse(from, false);
}
......@@ -157,7 +159,6 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
const options = {
...FLOT_OPTIONS,
...dynamicOptions,
...userOptions,
};
$.plot($el, series, options);
}
......@@ -166,16 +167,10 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
const { height = '100px', id = 'graph', loading = false } = this.props;
const data = this.getGraphData();
if (!loading && data.length === 0) {
return (
<div className="panel-container">
<div className="muted m-a-1">The queries returned no time series to graph.</div>
</div>
);
}
return (
<div>
{this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
<>
{this.props.data &&
this.props.data.length > MAX_NUMBER_OF_TIME_SERIES &&
!this.state.showAllTimeSeries && (
<div className="time-series-disclaimer">
<i className="fa fa-fw fa-warning disclaimer-icon" />
......@@ -186,10 +181,11 @@ export class Graph extends PureComponent<GraphProps, GraphState> {
</div>
)}
<div className="panel-container">
{loading && <div className="explore-graph__loader" />}
<div id={id} className="explore-graph" style={{ height }} />
<Legend data={data} />
</div>
</div>
</>
);
}
}
......
......@@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
// TODO make this datasource-plugin-dependent
import QueryField from './PromQueryField';
import QueryTransactions from './QueryTransactions';
class QueryRow extends PureComponent<any, {}> {
onChangeQuery = (value, override?: boolean) => {
......@@ -44,9 +45,14 @@ class QueryRow extends PureComponent<any, {}> {
};
render() {
const { history, query, queryError, queryHint, request, supportsLogs } = this.props;
const { history, query, queryHint, request, supportsLogs, transactions } = this.props;
const transactionWithError = transactions.find(t => t.error);
const queryError = transactionWithError ? transactionWithError.error : null;
return (
<div className="query-row">
<div className="query-row-status">
<QueryTransactions transactions={transactions} />
</div>
<div className="query-row-field">
<QueryField
error={queryError}
......@@ -78,7 +84,7 @@ class QueryRow extends PureComponent<any, {}> {
export default class QueryRows extends PureComponent<any, {}> {
render() {
const { className = '', queries, queryErrors, queryHints, ...handlers } = this.props;
const { className = '', queries, queryHints, transactions, ...handlers } = this.props;
return (
<div className={className}>
{queries.map((q, index) => (
......@@ -86,7 +92,7 @@ export default class QueryRows extends PureComponent<any, {}> {
key={q.key}
index={index}
query={q.query}
queryError={queryErrors[index]}
transactions={transactions.filter(t => t.rowIndex === index)}
queryHint={queryHints[index]}
{...handlers}
/>
......
import React, { PureComponent } from 'react';
import { QueryTransaction as QueryTransactionModel } from 'app/types/explore';
import ElapsedTime from './ElapsedTime';
function formatLatency(value) {
return `${(value / 1000).toFixed(1)}s`;
}
interface QueryTransactionProps {
transaction: QueryTransactionModel;
}
class QueryTransaction extends PureComponent<QueryTransactionProps> {
render() {
const { transaction } = this.props;
const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading';
return (
<div className={className}>
<div className="query-transaction__type">{transaction.resultType}:</div>
<div className="query-transaction__duration">
{transaction.done ? formatLatency(transaction.latency) : <ElapsedTime />}
</div>
</div>
);
}
}
interface QueryTransactionsProps {
transactions: QueryTransactionModel[];
}
export default class QueryTransactions extends PureComponent<QueryTransactionsProps> {
render() {
const { transactions } = this.props;
return (
<div className="query-transactions">
{transactions.map((t, i) => <QueryTransaction key={`${t.query}:${t.resultType}`} transaction={t} />)}
</div>
);
}
}
......@@ -51,7 +51,7 @@ export default class Table extends PureComponent<TableProps> {
minRows={0}
noDataText={noDataText}
resolveData={data => prepareRows(data, columnNames)}
showPagination={data}
showPagination={Boolean(data)}
/>
);
}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<Fragment>
<div
className="panel-container"
>
......@@ -458,11 +458,11 @@ exports[`Render should render component 1`] = `
}
/>
</div>
</div>
</Fragment>
`;
exports[`Render should render component with disclaimer 1`] = `
<div>
<Fragment>
<div
className="time-series-disclaimer"
>
......@@ -956,17 +956,26 @@ exports[`Render should render component with disclaimer 1`] = `
}
/>
</div>
</div>
</Fragment>
`;
exports[`Render should show query return no time series 1`] = `
<div
className="panel-container"
>
<Fragment>
<div
className="muted m-a-1"
className="panel-container"
>
The queries returned no time series to graph.
<div
className="explore-graph"
id="graph"
style={
Object {
"height": "100px",
}
}
/>
<Legend
data={Array []}
/>
</div>
</div>
</Fragment>
`;
......@@ -3,6 +3,11 @@ interface ExploreDatasource {
label: string;
}
export interface HistoryItem {
ts: number;
query: string;
}
export interface Range {
from: string;
to: string;
......@@ -13,6 +18,18 @@ export interface Query {
key?: string;
}
export interface QueryTransaction {
id: string;
done: boolean;
error?: string;
latency: number;
options: any;
query: string;
result?: any; // Table / Timeseries / Logs
resultType: string;
rowIndex: number;
}
export interface TextMatch {
text: string;
start: number;
......@@ -27,11 +44,8 @@ export interface ExploreState {
datasourceMissing: boolean;
datasourceName?: string;
exploreDatasources: ExploreDatasource[];
graphResult: any;
history: any[];
latency: number;
loading: any;
logsResult: any;
graphRange: Range;
history: HistoryItem[];
/**
* Initial rows of queries to push down the tree.
* Modifications do not end up here, but in `this.queryExpressions`.
......@@ -39,22 +53,17 @@ export interface ExploreState {
*/
queries: Query[];
/**
* Errors caused by the running the query row.
*/
queryErrors: any[];
/**
* Hints gathered for the query row.
*/
queryHints: any[];
queryTransactions: QueryTransaction[];
range: Range;
requestOptions: any;
showingGraph: boolean;
showingLogs: boolean;
showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult: any;
}
export interface ExploreUrlState {
......
......@@ -74,7 +74,7 @@
}
}
.elapsed-time {
.navbar .elapsed-time {
position: absolute;
left: 0;
right: 0;
......@@ -87,6 +87,37 @@
flex-wrap: wrap;
}
.explore-graph__loader {
height: 2px;
position: relative;
overflow: hidden;
background: $table-border;
margin: $panel-margin / 2;
}
.explore-graph__loader:after {
content: ' ';
display: block;
width: 25%;
top: 0;
top: -50%;
height: 250%;
position: absolute;
animation: loader 2s cubic-bezier(0.17, 0.67, 0.83, 0.67);
animation-iteration-count: 100;
z-index: 2;
background: $blue;
}
@keyframes loader {
from {
left: -25%;
}
to {
left: 100%;
}
}
.datasource-picker {
min-width: 200px;
}
......@@ -119,6 +150,7 @@
.query-row {
display: flex;
position: relative;
& + & {
margin-top: 0.5rem;
......@@ -129,11 +161,53 @@
white-space: nowrap;
}
.query-row-status {
position: absolute;
top: 0;
right: 90px;
z-index: 1024;
display: flex;
flex-direction: column;
justify-content: center;
height: 34px;
}
.query-row-field {
margin-right: 3px;
width: 100%;
}
.query-transactions {
display: table;
}
.query-transaction {
display: table-row;
color: $text-color-faint;
line-height: 1.44;
}
.query-transaction--loading {
animation: query-loading-color-change 1s alternate 100;
}
@keyframes query-loading-color-change {
from {
color: $text-color-faint;
}
to {
color: $blue;
}
}
.query-transaction__type,
.query-transaction__duration {
display: table-cell;
font-size: $font-size-xs;
text-align: right;
padding-right: 0.25em;
}
.explore {
.logs {
.logs-entries {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment