Explorar el Código

Merge pull request #13844 from grafana/davkal/explore-empty-page

Explore: Pluggable components from datasource plugins
David hace 7 años
padre
commit
758ec4bc70

+ 57 - 38
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<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>

+ 21 - 19
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<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>
     );
   }

+ 6 - 5
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<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

+ 8 - 8
public/app/features/explore/QueryTransactions.tsx → public/app/features/explore/QueryTransactionStatus.tsx

@@ -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>
     );
   }

+ 4 - 2
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);
       })

+ 35 - 0
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) => (
+  <div>
+    <h1>PromQL Cheat Sheet</h1>
+    {CHEAT_SHEET_ITEMS.map(item => (
+      <div className="cheat-sheet-item" key={item.expression}>
+        <div className="cheat-sheet-item__title">{item.title}</div>
+        <div className="cheat-sheet-item__expression" onClick={e => props.onClickQuery(item.expression)}>
+          <code>{item.expression}</code>
+        </div>
+        <div className="cheat-sheet-item__label">{item.label}</div>
+      </div>
+    ))}
+  </div>
+);

+ 0 - 0
public/app/features/explore/PromQueryField.test.tsx → public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx


+ 16 - 22
public/app/features/explore/PromQueryField.tsx → 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<PromQueryFieldProps, PromQueryF
           )}
         </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>
+          <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}
+          />
           {error ? <div className="prom-query-field-info text-error">{error}</div> : null}
           {hint ? (
             <div className="prom-query-field-info text-warning">

+ 60 - 0
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<any, { active: string }> {
+  state = {
+    active: 'start',
+  };
+
+  onClickTab = active => {
+    this.setState({ active });
+  };
+
+  render() {
+    const { active } = this.state;
+    const customCss = '';
+
+    return (
+      <div style={{ margin: '45px 0', border: '1px solid #ddd', borderRadius: 5 }}>
+        <div className="page-header-canvas">
+          <div className="page-container">
+            <div className="page-header">
+              <nav>
+                <ul className={`gf-tabs ${customCss}`}>
+                  {TAB_MENU_ITEMS.map((tab, idx) => {
+                    const tabClasses = classNames({
+                      'gf-tabs-link': true,
+                      active: tab.id === active,
+                    });
+
+                    return (
+                      <li className="gf-tabs-item" key={tab.id}>
+                        <a className={tabClasses} onClick={() => this.onClickTab(tab.id)}>
+                          <i className={tab.icon} />
+                          {tab.text}
+                        </a>
+                      </li>
+                    );
+                  })}
+                </ul>
+              </nav>
+            </div>
+          </div>
+        </div>
+        <div className="page-container page-body">
+          {active === 'start' && <PromCheatSheet onClickQuery={this.props.onClickQuery} />}
+        </div>
+      </div>
+    );
+  }
+}

+ 5 - 0
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,
 };

+ 5 - 1
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<any>;
 }
 
 export interface DataSourcesState {

+ 1 - 0
public/app/types/explore.ts

@@ -146,6 +146,7 @@ export interface TextMatch {
 }
 
 export interface ExploreState {
+  StartPage?: any;
   datasource: any;
   datasourceError: any;
   datasourceLoading: boolean | null;

+ 8 - 0
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 {

+ 17 - 1
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;
+}

+ 1 - 1
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: [