diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index d29f4c22e0c..7de20f31e69 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -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 { } } - 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 { 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 { ); }; + // 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 { render() { const { position, split } = this.props; const { + StartPage, datasource, datasourceError, datasourceLoading, @@ -760,6 +773,7 @@ export class Explore extends React.PureComponent { 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 (
@@ -847,43 +861,48 @@ export class Explore extends React.PureComponent { supportsLogs={supportsLogs} transactions={queryTransactions} /> -
- {supportsGraph ? ( - - ) : null} - {supportsTable ? ( - - ) : null} - {supportsLogs ? ( - - ) : null} -
-
- {supportsGraph && - showingGraph && ( - - )} - {supportsTable && showingTable ? ( -
- - - ) : null} - {supportsLogs && showingLogs ? : null} + {showStartPages && } + {!showStartPages && ( + <> +
+ {supportsGraph ? ( + + ) : null} + {supportsTable ? ( + + ) : null} + {supportsLogs ? ( + + ) : null} +
+ + {supportsGraph && + showingGraph && ( + + )} + {supportsTable && showingTable ? ( +
+
+ + ) : null} + {supportsLogs && showingLogs ? : null} + + )} diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 9350682f1a0..73d743fe8e6 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -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 { +export class QueryField extends React.PureComponent { menuEl: HTMLElement | null; placeholdersBuffer: PlaceholdersBuffer; plugins: any[]; @@ -72,7 +72,7 @@ class QueryField extends React.PureComponent p); this.state = { suggestions: [], @@ -102,7 +102,7 @@ class QueryField extends React.PureComponent - {this.renderMenu()} - +
+
+ {this.renderMenu()} + +
); } diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index aaa7dfcd20b..3a232cb658a 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -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 { 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 (
- +
{ +class QueryTransactionStatusItem extends PureComponent { render() { const { transaction } = this.props; const className = transaction.done ? 'query-transaction' : 'query-transaction query-transaction--loading'; @@ -26,16 +26,16 @@ class QueryTransaction extends PureComponent { } } -interface QueryTransactionsProps { - transactions: QueryTransactionModel[]; +interface QueryTransactionStatusProps { + transactions: QueryTransaction[]; } -export default class QueryTransactions extends PureComponent { +export default class QueryTransactionStatus extends PureComponent { render() { const { transactions } = this.props; return (
- {transactions.map((t, i) => )} + {transactions.map((t, i) => )}
); } diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 71a417a882f..fed455472c9 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -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); }) diff --git a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx new file mode 100644 index 00000000000..a2d3a03d794 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx @@ -0,0 +1,35 @@ +import React from 'react'; + +const CHEAT_SHEET_ITEMS = [ + { + title: 'Request Rate', + expression: 'rate(http_request_total[5m])', + label: + 'Given an HTTP request counter, this query calculates the per-second average request rate over the last 5 minutes.', + }, + { + title: '95th Percentile of Request Latencies', + expression: 'histogram_quantile(0.95, sum(rate(prometheus_http_request_duration_seconds_bucket[5m])) by (le))', + label: 'Calculates the 95th percentile of HTTP request rate over 5 minute windows.', + }, + { + title: 'Alerts Firing', + expression: 'sort_desc(sum(sum_over_time(ALERTS{alertstate="firing"}[24h])) by (alertname))', + label: 'Sums up the alerts that have been firing over the last 24 hours.', + }, +]; + +export default (props: any) => ( +
+

PromQL Cheat Sheet

+ {CHEAT_SHEET_ITEMS.map(item => ( +
+
{item.title}
+
props.onClickQuery(item.expression)}> + {item.expression} +
+
{item.label}
+
+ ))} +
+); diff --git a/public/app/features/explore/PromQueryField.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx similarity index 100% rename from public/app/features/explore/PromQueryField.test.tsx rename to public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx diff --git a/public/app/features/explore/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx similarity index 89% rename from public/app/features/explore/PromQueryField.tsx rename to public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index c7afe606734..b6f2c0f2e08 100644 --- a/public/app/features/explore/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -7,11 +7,10 @@ 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'; +import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; +import BracesPlugin from 'app/features/explore/slate-plugins/braces'; +import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; +import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -51,10 +50,7 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad return [...options, ...metricsOptions]; } -export function willApplySuggestion( - suggestion: string, - { typeaheadContext, typeaheadText }: TypeaheadFieldState -): string { +export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string { // Modify suggestion based on context switch (typeaheadContext) { case 'context-labels': { @@ -261,19 +257,17 @@ class PromQueryField extends React.PureComponent
-
- -
+ {error ?
{error}
: null} {hint ? (
diff --git a/public/app/plugins/datasource/prometheus/components/PromStart.tsx b/public/app/plugins/datasource/prometheus/components/PromStart.tsx new file mode 100644 index 00000000000..548360b342d --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/PromStart.tsx @@ -0,0 +1,60 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; + +import PromCheatSheet from './PromCheatSheet'; + +const TAB_MENU_ITEMS = [ + { + text: 'Start', + id: 'start', + icon: 'fa fa-rocket', + }, +]; + +export default class PromStart extends PureComponent { + state = { + active: 'start', + }; + + onClickTab = active => { + this.setState({ active }); + }; + + render() { + const { active } = this.state; + const customCss = ''; + + return ( +
+
+
+
+ +
+
+
+
+ {active === 'start' && } +
+
+ ); + } +} diff --git a/public/app/plugins/datasource/prometheus/module.ts b/public/app/plugins/datasource/prometheus/module.ts index d7e0b8ebe2c..814f6fbe60a 100644 --- a/public/app/plugins/datasource/prometheus/module.ts +++ b/public/app/plugins/datasource/prometheus/module.ts @@ -2,6 +2,9 @@ import { PrometheusDatasource } from './datasource'; import { PrometheusQueryCtrl } from './query_ctrl'; import { PrometheusConfigCtrl } from './config_ctrl'; +import PrometheusStartPage from './components/PromStart'; +import PromQueryField from './components/PromQueryField'; + class PrometheusAnnotationsQueryCtrl { static templateUrl = 'partials/annotations.editor.html'; } @@ -11,4 +14,6 @@ export { PrometheusQueryCtrl as QueryCtrl, PrometheusConfigCtrl as ConfigCtrl, PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl, + PromQueryField as ExploreQueryField, + PrometheusStartPage as ExploreStartPage, }; diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 8e1991dcd9f..5522f3a11ce 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -1,5 +1,5 @@ import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector'; -import { Plugin } from './plugins'; +import { Plugin, PluginExports, PluginMeta } from './plugins'; export interface DataSource { id: number; @@ -16,6 +16,10 @@ export interface DataSource { isDefault: boolean; jsonData: { authType: string; defaultRegion: string }; readOnly: boolean; + meta?: PluginMeta; + pluginExports?: PluginExports; + init?: () => void; + testDatasource?: () => Promise; } export interface DataSourcesState { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 2c2f5a77cf9..a4fb74634fe 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -146,6 +146,7 @@ export interface TextMatch { } export interface ExploreState { + StartPage?: any; datasource: any; datasourceError: any; datasourceLoading: boolean | null; diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 826ce2d48ec..1b5499f88b7 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -6,6 +6,8 @@ export interface PluginExports { ConfigCtrl?: any; AnnotationsQueryCtrl?: any; PanelOptions?: any; + ExploreQueryField?: any; + ExploreStartPage?: any; } export interface PanelPlugin { @@ -25,6 +27,12 @@ export interface PluginMeta { name: string; info: PluginMetaInfo; includes: PluginInclude[]; + + // Datasource-specific + metrics?: boolean; + logs?: boolean; + explore?: boolean; + annotations?: boolean; } export interface PluginInclude { diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 91e07bc46e6..0f8c335760a 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -52,7 +52,7 @@ } .result-options { - margin-top: 2 * $panel-margin; + margin: 2 * $panel-margin 0; } .time-series-disclaimer { @@ -322,3 +322,19 @@ .ReactTable .rt-tr .rt-td:last-child { text-align: right; } + +// TODO Experimental + +.cheat-sheet-item { + margin: 2*$panel-margin 0; + width: 50%; +} + +.cheat-sheet-item__title { + font-size: $font-size-h3; +} + +.cheat-sheet-item__expression { + margin: $panel-margin/2 0; + cursor: pointer; +} diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index d367016c4fb..dc4a7f363a2 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -16,7 +16,7 @@ module.exports = { publicPath: "public/build/", }, resolve: { - extensions: ['.ts', '.tsx', '.es6', '.js', '.json'], + extensions: ['.ts', '.tsx', '.es6', '.js', '.json', '.svg'], alias: { }, modules: [