Przeglądaj źródła

Pluggable components from datasource plugins

- when instantiating a datasource, the datasource service checks if the
  plugin module exports Explore components, and if so, attaches them to
  the datasource
- Explore component makes all major internal pluggable from a datasource
  `exploreComponents` property
- Moved Prometheus query field to promehteus datasource and registered
  it as an exported Explore component
- Added new Start page for Explore, also exported from the datasource
David Kaltschmidt 7 lat temu
rodzic
commit
d0776937b5

+ 1 - 0
public/app/core/utils/explore.test.ts

@@ -2,6 +2,7 @@ import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explor
 import { ExploreState } from 'app/types/explore';
 
 const DEFAULT_EXPLORE_STATE: ExploreState = {
+  customComponents: {},
   datasource: null,
   datasourceError: null,
   datasourceLoading: null,

+ 76 - 48
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,13 @@ 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 DefaultQueryRows from './QueryRows';
+import DefaultGraph from './Graph';
+import DefaultLogs from './Logs';
+import DefaultTable from './Table';
 import ErrorBoundary from './ErrorBoundary';
-import QueryRows from './QueryRows';
-import Graph from './Graph';
-import Logs from './Logs';
-import Table from './Table';
 import TimePicker from './TimePicker';
 import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
-import { RawTimeRange } from 'app/types/series';
 
 const MAX_HISTORY_ITEMS = 100;
 
@@ -96,6 +96,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       initialQueries = ensureQueries(queries);
       const initialRange = range || { ...DEFAULT_RANGE };
       this.state = {
+        customComponents: {},
         datasource: null,
         datasourceError: null,
         datasourceLoading: null,
@@ -176,8 +177,13 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
       query: this.queryExpressions[i],
     }));
 
+    const customComponents = {
+      ...datasource.exploreComponents,
+    };
+
     this.setState(
       {
+        customComponents,
         datasource,
         datasourceError,
         history,
@@ -330,6 +336,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) {
@@ -385,9 +398,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
               q.query = this.queryExpressions[i];
               return i === index
                 ? {
-                    key: generateQueryKey(index),
-                    query: datasource.modifyQuery(q.query, action),
-                  }
+                  key: generateQueryKey(index),
+                  query: datasource.modifyQuery(q.query, action),
+                }
                 : q;
             });
             nextQueryTransactions = queryTransactions
@@ -721,6 +734,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
   render() {
     const { position, split } = this.props;
     const {
+      customComponents,
       datasource,
       datasourceError,
       datasourceLoading,
@@ -760,6 +774,14 @@ 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 = queryTransactions.length === 0 && customComponents.StartPage;
+
+    // Custom components
+    const Graph = customComponents.Graph || DefaultGraph;
+    const Logs = customComponents.Logs || DefaultLogs;
+    const QueryRows = customComponents.QueryRows || DefaultQueryRows;
+    const StartPage = customComponents.StartPage;
+    const Table = customComponents.Table || DefaultTable;
 
     return (
       <div className={exploreClass} ref={this.getRef}>
@@ -772,12 +794,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
               </a>
             </div>
           ) : (
-            <div className="navbar-buttons explore-first-button">
-              <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
-                Close Split
+              <div className="navbar-buttons explore-first-button">
+                <button className="btn navbar-button" onClick={this.onClickCloseSplit}>
+                  Close Split
               </button>
-            </div>
-          )}
+              </div>
+            )}
           {!datasourceMissing ? (
             <div className="navbar-buttons">
               <Select
@@ -836,6 +858,7 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
         {datasource && !datasourceError ? (
           <div className="explore-container">
             <QueryRows
+              customComponents={customComponents}
               datasource={datasource}
               history={history}
               queries={queries}
@@ -847,43 +870,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>

+ 16 - 14
public/app/features/explore/QueryField.tsx

@@ -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: [],
@@ -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>
     );
   }

+ 10 - 7
public/app/features/explore/QueryRows.tsx

@@ -2,9 +2,8 @@ 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';
 
 function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint {
   const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0);
@@ -24,6 +23,7 @@ interface QueryRowEventHandlers {
 
 interface QueryRowCommonProps {
   className?: string;
+  customComponents: any;
   datasource: any;
   history: HistoryItem[];
   // Temporarily
@@ -37,7 +37,7 @@ type QueryRowProps = QueryRowCommonProps &
     query: string;
   };
 
-class QueryRow extends PureComponent<QueryRowProps> {
+class DefaultQueryRow extends PureComponent<QueryRowProps> {
   onChangeQuery = (value, override?: boolean) => {
     const { index, onChangeQuery } = this.props;
     if (onChangeQuery) {
@@ -78,14 +78,15 @@ class QueryRow extends PureComponent<QueryRowProps> {
   };
 
   render() {
-    const { datasource, history, query, supportsLogs, transactions } = this.props;
+    const { customComponents, datasource, history, query, supportsLogs, transactions } = this.props;
     const transactionWithError = transactions.find(t => t.error !== undefined);
     const hint = getFirstHintFromTransactions(transactions);
     const queryError = transactionWithError ? transactionWithError.error : null;
+    const QueryField = customComponents.QueryField || DefaultQueryField;
     return (
       <div className="query-row">
         <div className="query-row-status">
-          <QueryTransactions transactions={transactions} />
+          <QueryTransactionStatus transactions={transactions} />
         </div>
         <div className="query-row-field">
           <QueryField
@@ -123,12 +124,14 @@ type QueryRowsProps = QueryRowCommonProps &
 
 export default class QueryRows extends PureComponent<QueryRowsProps> {
   render() {
-    const { className = '', queries, transactions, ...handlers } = this.props;
+    const { className = '', customComponents, queries, transactions, ...handlers } = this.props;
+    const QueryRow = customComponents.QueryRow || DefaultQueryRow;
     return (
       <div className={className}>
         {queries.map((q, index) => (
           <QueryRow
             key={q.key}
+            customComponents={customComponents}
             index={index}
             query={q.query}
             transactions={transactions.filter(t => t.rowIndex === index)}

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

+ 1 - 0
public/app/features/plugins/datasource_srv.ts

@@ -64,6 +64,7 @@ export class DatasourceSrv {
         const instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig });
         instance.meta = pluginDef;
         instance.name = name;
+        instance.exploreComponents = plugin.ExploreComponents;
         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


+ 15 - 18
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, TypeaheadFieldState } from 'app/features/explore/QueryField';
 
 const HISTOGRAM_GROUP = '__histograms__';
 const METRIC_MARK = 'metric';
@@ -261,19 +260,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>
+    );
+  }
+}

+ 9 - 0
public/app/plugins/datasource/prometheus/module.ts

@@ -2,13 +2,22 @@ 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';
 }
 
+const ExploreComponents = {
+  QueryField: PromQueryField,
+  StartPage: PrometheusStartPage,
+};
+
 export {
   PrometheusDatasource as Datasource,
   PrometheusQueryCtrl as QueryCtrl,
   PrometheusConfigCtrl as ConfigCtrl,
   PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+  ExploreComponents,
 };

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

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

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

@@ -6,6 +6,7 @@ export interface PluginExports {
   ConfigCtrl?: any;
   AnnotationsQueryCtrl?: any;
   PanelOptions?: any;
+  ExploreComponents?: any;
 }
 
 export interface PanelPlugin {

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