mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #13844 from grafana/davkal/explore-empty-page
Explore: Pluggable components from datasource plugins
This commit is contained in:
@@ -4,6 +4,7 @@ import Select from 'react-select';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore';
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import colors from 'app/core/utils/colors';
|
||||
import store from 'app/core/store';
|
||||
@@ -16,14 +17,14 @@ 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 ErrorBoundary from './ErrorBoundary';
|
||||
import QueryRows from './QueryRows';
|
||||
import Graph from './Graph';
|
||||
import Logs from './Logs';
|
||||
import Table from './Table';
|
||||
import ErrorBoundary from './ErrorBoundary';
|
||||
import TimePicker from './TimePicker';
|
||||
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
import { DataSource } from 'app/types/datasources';
|
||||
|
||||
const MAX_HISTORY_ITEMS = 100;
|
||||
|
||||
@@ -148,7 +149,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
}
|
||||
}
|
||||
|
||||
async setDatasource(datasource) {
|
||||
async setDatasource(datasource: DataSource) {
|
||||
const supportsGraph = datasource.meta.metrics;
|
||||
const supportsLogs = datasource.meta.logs;
|
||||
const supportsTable = datasource.meta.metrics;
|
||||
@@ -176,8 +177,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
query: this.queryExpressions[i],
|
||||
}));
|
||||
|
||||
// Custom components
|
||||
const StartPage = datasource.pluginExports.ExploreStartPage;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
StartPage,
|
||||
datasource,
|
||||
datasourceError,
|
||||
history,
|
||||
@@ -330,6 +335,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
);
|
||||
};
|
||||
|
||||
// Use this in help pages to set page to a single query
|
||||
onClickQuery = query => {
|
||||
const nextQueries = [{ query, key: generateQueryKey() }];
|
||||
this.queryExpressions = nextQueries.map(q => q.query);
|
||||
this.setState({ queries: nextQueries }, this.onSubmit);
|
||||
};
|
||||
|
||||
onClickSplit = () => {
|
||||
const { onChangeSplit } = this.props;
|
||||
if (onChangeSplit) {
|
||||
@@ -721,6 +733,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
render() {
|
||||
const { position, split } = this.props;
|
||||
const {
|
||||
StartPage,
|
||||
datasource,
|
||||
datasourceError,
|
||||
datasourceLoading,
|
||||
@@ -760,6 +773,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result)
|
||||
);
|
||||
const loading = queryTransactions.some(qt => !qt.done);
|
||||
const showStartPages = StartPage && queryTransactions.length === 0;
|
||||
|
||||
return (
|
||||
<div className={exploreClass} ref={this.getRef}>
|
||||
@@ -847,43 +861,48 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
supportsLogs={supportsLogs}
|
||||
transactions={queryTransactions}
|
||||
/>
|
||||
<div className="result-options">
|
||||
{supportsGraph ? (
|
||||
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{supportsTable ? (
|
||||
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
) : null}
|
||||
{supportsLogs ? (
|
||||
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||
Logs
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<main className="m-t-2">
|
||||
<ErrorBoundary>
|
||||
{supportsGraph &&
|
||||
showingGraph && (
|
||||
<Graph
|
||||
data={graphResult}
|
||||
height={graphHeight}
|
||||
loading={graphLoading}
|
||||
id={`explore-graph-${position}`}
|
||||
range={graphRange}
|
||||
split={split}
|
||||
/>
|
||||
)}
|
||||
{supportsTable && showingTable ? (
|
||||
<div className="panel-container m-t-2">
|
||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
||||
</div>
|
||||
) : null}
|
||||
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={logsLoading} /> : null}
|
||||
{showStartPages && <StartPage onClickQuery={this.onClickQuery} />}
|
||||
{!showStartPages && (
|
||||
<>
|
||||
<div className="result-options">
|
||||
{supportsGraph ? (
|
||||
<button className={`btn toggle-btn ${graphButtonActive}`} onClick={this.onClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{supportsTable ? (
|
||||
<button className={`btn toggle-btn ${tableButtonActive}`} onClick={this.onClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
) : null}
|
||||
{supportsLogs ? (
|
||||
<button className={`btn toggle-btn ${logsButtonActive}`} onClick={this.onClickLogsButton}>
|
||||
Logs
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{supportsGraph &&
|
||||
showingGraph && (
|
||||
<Graph
|
||||
data={graphResult}
|
||||
height={graphHeight}
|
||||
loading={graphLoading}
|
||||
id={`explore-graph-${position}`}
|
||||
range={graphRange}
|
||||
split={split}
|
||||
/>
|
||||
)}
|
||||
{supportsTable && showingTable ? (
|
||||
<div className="panel-container m-t-2">
|
||||
<Table data={tableResult} loading={tableLoading} onClickCell={this.onClickTableCell} />
|
||||
</div>
|
||||
) : null}
|
||||
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={logsLoading} /> : null}
|
||||
</>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
|
||||
|
||||
describe('groupMetricsByPrefix()', () => {
|
||||
it('returns an empty group for no metrics', () => {
|
||||
expect(groupMetricsByPrefix([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns options grouped by prefix', () => {
|
||||
expect(groupMetricsByPrefix(['foo_metric'])).toMatchObject([
|
||||
{
|
||||
value: 'foo',
|
||||
children: [
|
||||
{
|
||||
value: 'foo_metric',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns options without prefix as toplevel option', () => {
|
||||
expect(groupMetricsByPrefix(['metric'])).toMatchObject([
|
||||
{
|
||||
value: 'metric',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns recording rules grouped separately', () => {
|
||||
expect(groupMetricsByPrefix([':foo_metric:'])).toMatchObject([
|
||||
{
|
||||
value: RECORDING_RULES_GROUP,
|
||||
children: [
|
||||
{
|
||||
value: ':foo_metric:',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,294 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import Cascader from 'rc-cascader';
|
||||
import PluginPrism from 'slate-prism';
|
||||
import Prism from 'prismjs';
|
||||
|
||||
import { TypeaheadOutput } from 'app/types/explore';
|
||||
|
||||
// dom also includes Element polyfills
|
||||
import { getNextCharacter, getPreviousCousin } from './utils/dom';
|
||||
import BracesPlugin from './slate-plugins/braces';
|
||||
import RunnerPlugin from './slate-plugins/runner';
|
||||
|
||||
import TypeaheadField, { TypeaheadInput, TypeaheadFieldState } from './QueryField';
|
||||
|
||||
const HISTOGRAM_GROUP = '__histograms__';
|
||||
const METRIC_MARK = 'metric';
|
||||
const PRISM_SYNTAX = 'promql';
|
||||
export const RECORDING_RULES_GROUP = '__recording_rules__';
|
||||
|
||||
export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
|
||||
// Filter out recording rules and insert as first option
|
||||
const ruleRegex = /:\w+:/;
|
||||
const ruleNames = metrics.filter(metric => ruleRegex.test(metric));
|
||||
const rulesOption = {
|
||||
label: 'Recording rules',
|
||||
value: RECORDING_RULES_GROUP,
|
||||
children: ruleNames
|
||||
.slice()
|
||||
.sort()
|
||||
.map(name => ({ label: name, value: name })),
|
||||
};
|
||||
|
||||
const options = ruleNames.length > 0 ? [rulesOption] : [];
|
||||
|
||||
const metricsOptions = _.chain(metrics)
|
||||
.filter(metric => !ruleRegex.test(metric))
|
||||
.groupBy(metric => metric.split(delimiter)[0])
|
||||
.map((metricsForPrefix: string[], prefix: string): CascaderOption => {
|
||||
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
||||
const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => ({ label: m, value: m }));
|
||||
return {
|
||||
children,
|
||||
label: prefix,
|
||||
value: prefix,
|
||||
};
|
||||
})
|
||||
.sortBy('label')
|
||||
.value();
|
||||
|
||||
return [...options, ...metricsOptions];
|
||||
}
|
||||
|
||||
export function willApplySuggestion(
|
||||
suggestion: string,
|
||||
{ typeaheadContext, typeaheadText }: TypeaheadFieldState
|
||||
): string {
|
||||
// Modify suggestion based on context
|
||||
switch (typeaheadContext) {
|
||||
case 'context-labels': {
|
||||
const nextChar = getNextCharacter();
|
||||
if (!nextChar || nextChar === '}' || nextChar === ',') {
|
||||
suggestion += '=';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'context-label-values': {
|
||||
// Always add quotes and remove existing ones instead
|
||||
if (!typeaheadText.match(/^(!?=~?"|")/)) {
|
||||
suggestion = `"${suggestion}`;
|
||||
}
|
||||
if (getNextCharacter() !== '"') {
|
||||
suggestion = `${suggestion}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
return suggestion;
|
||||
}
|
||||
|
||||
interface CascaderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: CascaderOption[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface PromQueryFieldProps {
|
||||
datasource: any;
|
||||
error?: string | JSX.Element;
|
||||
hint?: any;
|
||||
history?: any[];
|
||||
initialQuery?: string | null;
|
||||
metricsByPrefix?: CascaderOption[];
|
||||
onClickHintFix?: (action: any) => void;
|
||||
onPressEnter?: () => void;
|
||||
onQueryChange?: (value: string, override?: boolean) => void;
|
||||
supportsLogs?: boolean; // To be removed after Logging gets its own query field
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
logLabelOptions: any[];
|
||||
metricsOptions: any[];
|
||||
metricsByPrefix: CascaderOption[];
|
||||
syntaxLoaded: boolean;
|
||||
}
|
||||
|
||||
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
|
||||
plugins: any[];
|
||||
languageProvider: any;
|
||||
|
||||
constructor(props: PromQueryFieldProps, context) {
|
||||
super(props, context);
|
||||
|
||||
if (props.datasource.languageProvider) {
|
||||
this.languageProvider = props.datasource.languageProvider;
|
||||
}
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
RunnerPlugin({ handler: props.onPressEnter }),
|
||||
PluginPrism({
|
||||
onlyIn: node => node.type === 'code_block',
|
||||
getSyntax: node => 'promql',
|
||||
}),
|
||||
];
|
||||
|
||||
this.state = {
|
||||
logLabelOptions: [],
|
||||
metricsByPrefix: [],
|
||||
metricsOptions: [],
|
||||
syntaxLoaded: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.languageProvider) {
|
||||
this.languageProvider.start().then(() => this.onReceiveMetrics());
|
||||
}
|
||||
}
|
||||
|
||||
onChangeLogLabels = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
let query;
|
||||
if (selectedOptions.length === 1) {
|
||||
if (selectedOptions[0].children.length === 0) {
|
||||
query = selectedOptions[0].value;
|
||||
} else {
|
||||
// Ignore click on group
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const key = selectedOptions[0].value;
|
||||
const value = selectedOptions[1].value;
|
||||
query = `{${key}="${value}"}`;
|
||||
}
|
||||
this.onChangeQuery(query, true);
|
||||
};
|
||||
|
||||
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||
let query;
|
||||
if (selectedOptions.length === 1) {
|
||||
if (selectedOptions[0].children.length === 0) {
|
||||
query = selectedOptions[0].value;
|
||||
} else {
|
||||
// Ignore click on group
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const prefix = selectedOptions[0].value;
|
||||
const metric = selectedOptions[1].value;
|
||||
if (prefix === HISTOGRAM_GROUP) {
|
||||
query = `histogram_quantile(0.95, sum(rate(${metric}[5m])) by (le))`;
|
||||
} else {
|
||||
query = metric;
|
||||
}
|
||||
}
|
||||
this.onChangeQuery(query, true);
|
||||
};
|
||||
|
||||
onChangeQuery = (value: string, override?: boolean) => {
|
||||
// Send text change to parent
|
||||
const { onQueryChange } = this.props;
|
||||
if (onQueryChange) {
|
||||
onQueryChange(value, override);
|
||||
}
|
||||
};
|
||||
|
||||
onClickHintFix = () => {
|
||||
const { hint, onClickHintFix } = this.props;
|
||||
if (onClickHintFix && hint && hint.fix) {
|
||||
onClickHintFix(hint.fix.action);
|
||||
}
|
||||
};
|
||||
|
||||
onReceiveMetrics = () => {
|
||||
const { histogramMetrics, metrics } = this.languageProvider;
|
||||
if (!metrics) {
|
||||
return;
|
||||
}
|
||||
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||
Prism.languages[PRISM_SYNTAX][METRIC_MARK] = {
|
||||
alias: 'variable',
|
||||
pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
|
||||
};
|
||||
|
||||
// Build metrics tree
|
||||
const metricsByPrefix = groupMetricsByPrefix(metrics);
|
||||
const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm }));
|
||||
const metricsOptions = [
|
||||
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions },
|
||||
...metricsByPrefix,
|
||||
];
|
||||
|
||||
this.setState({ metricsOptions, syntaxLoaded: true });
|
||||
};
|
||||
|
||||
onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => {
|
||||
if (!this.languageProvider) {
|
||||
return { suggestions: [] };
|
||||
}
|
||||
|
||||
const { history } = this.props;
|
||||
const { prefix, text, value, wrapperNode } = typeahead;
|
||||
|
||||
// Get DOM-dependent context
|
||||
const wrapperClasses = Array.from(wrapperNode.classList);
|
||||
const labelKeyNode = getPreviousCousin(wrapperNode, '.attr-name');
|
||||
const labelKey = labelKeyNode && labelKeyNode.textContent;
|
||||
const nextChar = getNextCharacter();
|
||||
|
||||
const result = this.languageProvider.provideCompletionItems(
|
||||
{ text, value, prefix, wrapperClasses, labelKey },
|
||||
{ history }
|
||||
);
|
||||
|
||||
console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { error, hint, initialQuery, supportsLogs } = this.props;
|
||||
const { logLabelOptions, metricsOptions, syntaxLoaded } = this.state;
|
||||
const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined;
|
||||
|
||||
return (
|
||||
<div className="prom-query-field">
|
||||
<div className="prom-query-field-tools">
|
||||
{supportsLogs ? (
|
||||
<Cascader options={logLabelOptions} onChange={this.onChangeLogLabels}>
|
||||
<button className="btn navbar-button navbar-button--tight">Log labels</button>
|
||||
</Cascader>
|
||||
) : (
|
||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
||||
</Cascader>
|
||||
)}
|
||||
</div>
|
||||
<div className="prom-query-field-wrapper">
|
||||
<div className="slate-query-field-wrapper">
|
||||
<TypeaheadField
|
||||
additionalPlugins={this.plugins}
|
||||
cleanText={cleanText}
|
||||
initialValue={initialQuery}
|
||||
onTypeahead={this.onTypeahead}
|
||||
onWillApplySuggestion={willApplySuggestion}
|
||||
onValueChanged={this.onChangeQuery}
|
||||
placeholder="Enter a PromQL query"
|
||||
portalOrigin="prometheus"
|
||||
syntaxLoaded={syntaxLoaded}
|
||||
/>
|
||||
</div>
|
||||
{error ? <div className="prom-query-field-info text-error">{error}</div> : null}
|
||||
{hint ? (
|
||||
<div className="prom-query-field-info text-warning">
|
||||
{hint.label}{' '}
|
||||
{hint.fix ? (
|
||||
<a className="text-link muted" onClick={this.onClickHintFix}>
|
||||
{hint.fix.label}
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default PromQueryField;
|
||||
@@ -27,7 +27,7 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean {
|
||||
return suggestions && suggestions.length > 0;
|
||||
}
|
||||
|
||||
interface TypeaheadFieldProps {
|
||||
interface QueryFieldProps {
|
||||
additionalPlugins?: any[];
|
||||
cleanText?: (text: string) => string;
|
||||
initialValue: string | null;
|
||||
@@ -35,14 +35,14 @@ interface TypeaheadFieldProps {
|
||||
onFocus?: () => void;
|
||||
onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput;
|
||||
onValueChanged?: (value: Value) => void;
|
||||
onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string;
|
||||
onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string;
|
||||
placeholder?: string;
|
||||
portalOrigin?: string;
|
||||
syntax?: string;
|
||||
syntaxLoaded?: boolean;
|
||||
}
|
||||
|
||||
export interface TypeaheadFieldState {
|
||||
export interface QueryFieldState {
|
||||
suggestions: CompletionItemGroup[];
|
||||
typeaheadContext: string | null;
|
||||
typeaheadIndex: number;
|
||||
@@ -60,7 +60,7 @@ export interface TypeaheadInput {
|
||||
wrapperNode: Element;
|
||||
}
|
||||
|
||||
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
|
||||
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
|
||||
menuEl: HTMLElement | null;
|
||||
placeholdersBuffer: PlaceholdersBuffer;
|
||||
plugins: any[];
|
||||
@@ -72,7 +72,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || '');
|
||||
|
||||
// Base plugins
|
||||
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins];
|
||||
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p);
|
||||
|
||||
this.state = {
|
||||
suggestions: [],
|
||||
@@ -102,7 +102,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: TypeaheadFieldProps) {
|
||||
componentWillReceiveProps(nextProps: QueryFieldProps) {
|
||||
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
||||
// Need a bogus edit to re-render the editor after syntax has fully loaded
|
||||
const change = this.state.value
|
||||
@@ -434,19 +434,21 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="slate-query-field">
|
||||
{this.renderMenu()}
|
||||
<Editor
|
||||
autoCorrect={false}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onFocus={this.handleFocus}
|
||||
placeholder={this.props.placeholder}
|
||||
plugins={this.plugins}
|
||||
spellCheck={false}
|
||||
value={this.state.value}
|
||||
/>
|
||||
<div className="slate-query-field-wrapper">
|
||||
<div className="slate-query-field">
|
||||
{this.renderMenu()}
|
||||
<Editor
|
||||
autoCorrect={false}
|
||||
onBlur={this.handleBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onFocus={this.handleFocus}
|
||||
placeholder={this.props.placeholder}
|
||||
plugins={this.plugins}
|
||||
spellCheck={false}
|
||||
value={this.state.value}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import React, { PureComponent } from 'react';
|
||||
|
||||
import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explore';
|
||||
|
||||
// TODO make this datasource-plugin-dependent
|
||||
import QueryField from './PromQueryField';
|
||||
import QueryTransactions from './QueryTransactions';
|
||||
import DefaultQueryField from './QueryField';
|
||||
import QueryTransactionStatus from './QueryTransactionStatus';
|
||||
import { DataSource } from 'app/types';
|
||||
|
||||
function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
|
||||
const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
|
||||
@@ -24,7 +24,7 @@ interface QueryRowEventHandlers {
|
||||
|
||||
interface QueryRowCommonProps {
|
||||
className?: string;
|
||||
datasource: any;
|
||||
datasource: DataSource;
|
||||
history: HistoryItem[];
|
||||
// Temporarily
|
||||
supportsLogs?: boolean;
|
||||
@@ -82,10 +82,11 @@ class QueryRow extends PureComponent<QueryRowProps> {
|
||||
const transactionWithError = transactions.find(t => t.error !== undefined);
|
||||
const hint = getFirstHintFromTransactions(transactions);
|
||||
const queryError = transactionWithError ? transactionWithError.error : null;
|
||||
const QueryField = datasource.pluginExports.ExploreQueryField || DefaultQueryField;
|
||||
return (
|
||||
<div className="query-row">
|
||||
<div className="query-row-status">
|
||||
<QueryTransactions transactions={transactions} />
|
||||
<QueryTransactionStatus transactions={transactions} />
|
||||
</div>
|
||||
<div className="query-row-field">
|
||||
<QueryField
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { QueryTransaction as QueryTransactionModel } from 'app/types/explore';
|
||||
import { QueryTransaction } from 'app/types/explore';
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
|
||||
function formatLatency(value) {
|
||||
return `${(value / 1000).toFixed(1)}s`;
|
||||
}
|
||||
|
||||
interface QueryTransactionProps {
|
||||
transaction: QueryTransactionModel;
|
||||
interface QueryTransactionStatusItemProps {
|
||||
transaction: QueryTransaction;
|
||||
}
|
||||
|
||||
class QueryTransaction extends PureComponent<QueryTransactionProps> {
|
||||
class QueryTransactionStatusItem extends PureComponent<QueryTransactionStatusItemProps> {
|
||||
render() {
|
||||
const { transaction } = this.props;
|
||||
const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading';
|
||||
@@ -26,16 +26,16 @@ class QueryTransaction extends PureComponent<QueryTransactionProps> {
|
||||
}
|
||||
}
|
||||
|
||||
interface QueryTransactionsProps {
|
||||
transactions: QueryTransactionModel[];
|
||||
interface QueryTransactionStatusProps {
|
||||
transactions: QueryTransaction[];
|
||||
}
|
||||
|
||||
export default class QueryTransactions extends PureComponent<QueryTransactionsProps> {
|
||||
export default class QueryTransactionStatus extends PureComponent<QueryTransactionStatusProps> {
|
||||
render() {
|
||||
const { transactions } = this.props;
|
||||
return (
|
||||
<div className="query-transactions">
|
||||
{transactions.map((t, i) => <QueryTransaction key={`${t.query}:${t.resultType}`} transaction={t} />)}
|
||||
{transactions.map((t, i) => <QueryTransactionStatusItem key={`${t.query}:${t.resultType}`} transaction={t} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,9 +8,10 @@ import { importPluginModule } from './plugin_loader';
|
||||
|
||||
// Types
|
||||
import { DataSourceApi } from 'app/types/series';
|
||||
import { DataSource } from 'app/types';
|
||||
|
||||
export class DatasourceSrv {
|
||||
datasources: any;
|
||||
datasources: { [name: string]: DataSource };
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $q, private $injector, private $rootScope, private templateSrv) {
|
||||
@@ -61,9 +62,10 @@ export class DatasourceSrv {
|
||||
throw new Error('Plugin module is missing Datasource constructor');
|
||||
}
|
||||
|
||||
const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
|
||||
const instance: DataSource = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
|
||||
instance.meta = pluginDef;
|
||||
instance.name = name;
|
||||
instance.pluginExports = plugin;
|
||||
this.datasources[name] = instance;
|
||||
deferred.resolve(instance);
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user