Quellcode durchsuchen

DirectInput: new alpha datasource that lets you enter data via CSV

Initial alpha datasource that saves data directly in a panel or in the shared datasource configs.
Ryan McKinley vor 6 Jahren
Ursprung
Commit
08a22c806f

+ 9 - 9
packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx

@@ -12,14 +12,14 @@ TableInputStories.addDecorator(withCenteredStory);
 
 TableInputStories.add('default', () => {
   return (
-    <div style={{ width: '90%', height: '90vh' }}>
-      <TableInputCSV
-        text={'a,b,c\n1,2,3'}
-        onSeriesParsed={(data: SeriesData[], text: string) => {
-          console.log('Data', data, text);
-          action('Data')(data, text);
-        }}
-      />
-    </div>
+    <TableInputCSV
+      width={400}
+      height={'90vh'}
+      text={'a,b,c\n1,2,3'}
+      onSeriesParsed={(data: SeriesData[], text: string) => {
+        console.log('Data', data, text);
+        action('Data')(data, text);
+      }}
+    />
   );
 });

+ 2 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx

@@ -9,6 +9,8 @@ describe('TableInputCSV', () => {
     const tree = renderer
       .create(
         <TableInputCSV
+          width={'100%'}
+          height={200}
           text={'a,b,c\n1,2,3'}
           onSeriesParsed={(data: SeriesData[], text: string) => {
             // console.log('Table:', table, 'from:', text);

+ 24 - 21
packages/grafana-ui/src/components/Table/TableInputCSV.tsx

@@ -1,12 +1,13 @@
 import React from 'react';
 import debounce from 'lodash/debounce';
 import { SeriesData } from '../../types/data';
-import { AutoSizer } from 'react-virtualized';
 import { CSVConfig, readCSV } from '../../utils/csv';
 
 interface Props {
   config?: CSVConfig;
   text: string;
+  width: string | number;
+  height: string | number;
   onSeriesParsed: (data: SeriesData[], text: string) => void;
 }
 
@@ -18,7 +19,7 @@ interface State {
 /**
  * Expects the container div to have size set and will fill it 100%
  */
-class TableInputCSV extends React.PureComponent<Props, State> {
+export class TableInputCSV extends React.PureComponent<Props, State> {
   constructor(props: Props) {
     super(props);
 
@@ -58,28 +59,30 @@ class TableInputCSV extends React.PureComponent<Props, State> {
   };
 
   render() {
+    const { width, height } = this.props;
     const { data } = this.state;
-
     return (
-      <AutoSizer>
-        {({ height, width }) => (
-          <div className="gf-table-input-csv" style={{ width, height }}>
-            <textarea placeholder="Enter CSV here..." value={this.state.text} onChange={this.onTextChange} />
-            {data && (
-              <footer>
-                {data.map((series, index) => {
-                  return (
-                    <span key={index}>
-                      Rows:{series.rows.length}, Columns:{series.fields.length} &nbsp;
-                      <i className="fa fa-check-circle" />
-                    </span>
-                  );
-                })}
-              </footer>
-            )}
-          </div>
+      <div className="gf-table-input-csv">
+        <textarea
+          style={{ width, height }}
+          placeholder="Enter CSV here..."
+          value={this.state.text}
+          onChange={this.onTextChange}
+          className="gf-form-input"
+        />
+        {data && (
+          <footer>
+            {data.map((series, index) => {
+              return (
+                <span key={index}>
+                  Rows:{series.rows.length}, Columns:{series.fields.length} &nbsp;
+                  <i className="fa fa-check-circle" />
+                </span>
+              );
+            })}
+          </footer>
         )}
-      </AutoSizer>
+      </div>
     );
   }
 }

+ 1 - 3
packages/grafana-ui/src/components/Table/_TableInputCSV.scss

@@ -5,7 +5,6 @@
 .gf-table-input-csv textarea {
   height: 100%;
   width: 100%;
-  resize: none;
 }
 
 .gf-table-input-csv footer {
@@ -13,8 +12,7 @@
   bottom: 15px;
   right: 15px;
   border: 1px solid #222;
-  background: #ccc;
+  background: $online;
   padding: 1px 4px;
   font-size: 80%;
-  cursor: pointer;
 }

+ 3 - 0
packages/grafana-ui/src/components/index.ts

@@ -32,6 +32,9 @@ export { UnitPicker } from './UnitPicker/UnitPicker';
 export { StatsPicker } from './StatsPicker/StatsPicker';
 export { Input, InputStatus } from './Input/Input';
 
+export { Table } from './Table/Table';
+export { TableInputCSV } from './Table/TableInputCSV';
+
 // Visualizations
 export { BigValue } from './BigValue/BigValue';
 export { Gauge } from './Gauge/Gauge';

+ 6 - 0
packages/grafana-ui/src/types/datasource.ts

@@ -116,6 +116,11 @@ export interface DataSourceApi<TQuery extends DataQuery = DataQuery> {
    */
   name?: string;
 
+  /**
+   *  Set after constructor is called by Grafana
+   */
+  id?: number;
+
   /**
    * Set after constructor call, as the data source instance is the most common thing to pass around
    * we attach the components to this instance for easy access
@@ -275,6 +280,7 @@ export interface DataSourceSettings {
  * as this data model is available to every user who has access to a data source (Viewers+).
  */
 export interface DataSourceInstanceSettings {
+  id: number;
   type: string;
   name: string;
   meta: PluginMeta;

+ 4 - 0
packages/grafana-ui/src/utils/csv.ts

@@ -316,6 +316,10 @@ function getHeaderLine(key: string, fields: Field[], config: CSVConfig): string
 }
 
 export function toCSV(data: SeriesData[], config?: CSVConfig): string {
+  if (!data) {
+    return '';
+  }
+
   let csv = '';
   config = defaults(config, {
     delimiter: ',',

+ 2 - 0
public/app/features/plugins/built_in_plugins.ts

@@ -11,6 +11,7 @@ import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
 import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
 import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
 import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
+import * as inputDatasourcePlugin from 'app/plugins/datasource/input/module';
 import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
 import * as azureMonitorPlugin from 'app/plugins/datasource/grafana-azure-monitor-datasource/module';
 
@@ -45,6 +46,7 @@ const builtInPlugins = {
   'app/plugins/datasource/mssql/module': mssqlPlugin,
   'app/plugins/datasource/prometheus/module': prometheusPlugin,
   'app/plugins/datasource/testdata/module': testDataDSPlugin,
+  'app/plugins/datasource/input/module': inputDatasourcePlugin,
   'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
   'app/plugins/datasource/grafana-azure-monitor-datasource/module': azureMonitorPlugin,
 

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

@@ -65,6 +65,7 @@ export class DatasourceSrv {
           instanceSettings: dsConfig,
         });
 
+        instance.id = dsConfig.id;
         instance.name = name;
         instance.components = dsPlugin.components;
         instance.meta = dsConfig.meta;

+ 3 - 0
public/app/features/plugins/specs/datasource_srv.test.ts

@@ -23,18 +23,21 @@ describe('datasource_srv', () => {
     beforeEach(() => {
       config.datasources = {
         buildInDs: {
+          id: 1,
           type: 'b',
           name: 'buildIn',
           meta: { builtIn: true } as PluginMeta,
           jsonData: {},
         },
         nonBuildIn: {
+          id: 2,
           type: 'e',
           name: 'external1',
           meta: { builtIn: false } as PluginMeta,
           jsonData: {},
         },
         nonExplore: {
+          id: 3,
           type: 'e2',
           name: 'external2',
           meta: {} as PluginMeta,

+ 98 - 0
public/app/plugins/datasource/input/InputQueryEditor.tsx

@@ -0,0 +1,98 @@
+// Libraries
+import React, { PureComponent } from 'react';
+
+// Types
+import { InputDatasource } from './datasource';
+import { InputQuery } from './types';
+
+import { FormLabel, Select, QueryEditorProps, SelectOptionItem, SeriesData, TableInputCSV, toCSV } from '@grafana/ui';
+
+type Props = QueryEditorProps<InputDatasource, InputQuery>;
+
+const options = [
+  { value: 'panel', label: 'Panel', description: 'Save data in the panel configuration.' },
+  { value: 'shared', label: 'Shared', description: 'Save data in the shared datasource object.' },
+];
+
+interface State {
+  text: string;
+}
+
+export class InputQueryEditor extends PureComponent<Props, State> {
+  state = {
+    text: '',
+  };
+
+  onComponentDidMount() {
+    const { query } = this.props;
+    const text = query.data ? toCSV(query.data) : '';
+    this.setState({ text });
+  }
+
+  onSourceChange = (item: SelectOptionItem) => {
+    const { datasource, query, onChange, onRunQuery } = this.props;
+    let data: SeriesData[] | undefined = undefined;
+    if (item.value === 'panel') {
+      if (query.data) {
+        return;
+      }
+      data = [...datasource.data];
+      if (!data) {
+        data = [
+          {
+            fields: [],
+            rows: [],
+          },
+        ];
+      }
+      this.setState({ text: toCSV(data) });
+    }
+    onChange({ ...query, data });
+    onRunQuery();
+  };
+
+  onSeriesParsed = (data: SeriesData[], text: string) => {
+    const { query, onChange, onRunQuery } = this.props;
+    this.setState({ text });
+    if (!data) {
+      data = [
+        {
+          fields: [],
+          rows: [],
+        },
+      ];
+    }
+    onChange({ ...query, data });
+    onRunQuery();
+  };
+
+  render() {
+    const { datasource, query } = this.props;
+    const { id, name } = datasource;
+    const { text } = this.state;
+
+    const selected = query.data ? options[0] : options[1];
+    return (
+      <div>
+        <div className="gf-form">
+          <FormLabel width={4}>Data</FormLabel>
+          <Select width={6} options={options} value={selected} onChange={this.onSourceChange} />
+
+          <div className="btn btn-link">
+            {query.data ? (
+              datasource.getDescription(query.data)
+            ) : (
+              <a href={`datasources/edit/${id}/`}>
+                {name}: {datasource.getDescription(datasource.data)} &nbsp;&nbsp;
+                <i className="fa fa-pencil-square-o" />
+              </a>
+            )}
+          </div>
+        </div>
+        {query.data && <TableInputCSV text={text} onSeriesParsed={this.onSeriesParsed} width={'100%'} height={200} />}
+      </div>
+    );
+  }
+}
+
+export default InputQueryEditor;

+ 3 - 0
public/app/plugins/datasource/input/README.md

@@ -0,0 +1,3 @@
+# Table Datasource -  Native Plugin
+
+This datasource lets you define results directly in CSV.  The values are stored either in a shared datasource, or directly in panels.

+ 34 - 0
public/app/plugins/datasource/input/datasource.test.ts

@@ -0,0 +1,34 @@
+import InputDatasource from './datasource';
+import { InputQuery } from './types';
+import { readCSV, DataSourceInstanceSettings, PluginMeta } from '@grafana/ui';
+import { getQueryOptions } from 'test/helpers/getQueryOptions';
+
+describe('InputDatasource', () => {
+  const data = readCSV('a,b,c\n1,2,3\n4,5,6');
+  const instanceSettings: DataSourceInstanceSettings = {
+    id: 1,
+    type: 'x',
+    name: 'xxx',
+    meta: {} as PluginMeta,
+    jsonData: {
+      data,
+    },
+  };
+
+  describe('when querying', () => {
+    test('should return the saved data with a query', () => {
+      const ds = new InputDatasource(instanceSettings);
+      const options = getQueryOptions<InputQuery>({
+        targets: [{ refId: 'Z' }],
+      });
+
+      return ds.query(options).then(rsp => {
+        expect(rsp.data.length).toBe(1);
+
+        const series = rsp.data[0];
+        expect(series.refId).toBe('Z');
+        expect(series.rows).toEqual(data[0].rows);
+      });
+    });
+  });
+});

+ 110 - 0
public/app/plugins/datasource/input/datasource.ts

@@ -0,0 +1,110 @@
+// Types
+import {
+  DataQueryOptions,
+  SeriesData,
+  DataQueryResponse,
+  DataSourceApi,
+  DataSourceInstanceSettings,
+} from '@grafana/ui/src/types';
+import { InputQuery } from './types';
+
+export class InputDatasource implements DataSourceApi<InputQuery> {
+  data: SeriesData[];
+
+  // Filled in by grafana plugin system
+  name?: string;
+
+  // Filled in by grafana plugin system
+  id?: number;
+
+  /** @ngInject */
+  constructor(instanceSettings: DataSourceInstanceSettings) {
+    if (instanceSettings.jsonData) {
+      this.data = instanceSettings.jsonData.data;
+    }
+
+    if (!this.data) {
+      this.data = [];
+    }
+  }
+
+  getDescription(data: SeriesData[]): string {
+    if (!data) {
+      return '';
+    }
+    if (data.length > 1) {
+      const count = data.reduce((acc, series) => {
+        return acc + series.rows.length;
+      }, 0);
+      return `${data.length} Series, ${count} Rows`;
+    }
+    const series = data[0];
+    return `${series.fields.length} Fields, ${series.rows.length} Rows`;
+  }
+
+  /**
+   * Convert a query to a simple text string
+   */
+  getQueryDisplayText(query: InputQuery): string {
+    if (query.data) {
+      return 'Panel Data: ' + this.getDescription(query.data);
+    }
+    return `Shared Data From: ${this.name} (${this.getDescription(this.data)})`;
+  }
+
+  metricFindQuery(query: string, options?: any) {
+    return new Promise((resolve, reject) => {
+      const names = [];
+      for (const series of this.data) {
+        for (const field of series.fields) {
+          // TODO, match query/options?
+          names.push({
+            text: field.name,
+          });
+        }
+      }
+      resolve(names);
+    });
+  }
+
+  query(options: DataQueryOptions<InputQuery>): Promise<DataQueryResponse> {
+    const results: SeriesData[] = [];
+    for (const query of options.targets) {
+      if (query.hide) {
+        continue;
+      }
+      const data = query.data ? query.data : this.data;
+      for (const series of data) {
+        results.push({
+          refId: query.refId,
+          ...series,
+        });
+      }
+    }
+    return Promise.resolve({ data: results });
+  }
+
+  testDatasource() {
+    return new Promise((resolve, reject) => {
+      let rowCount = 0;
+      let info = `${this.data.length} Series:`;
+      for (const series of this.data) {
+        info += ` [${series.fields.length} Fields, ${series.rows.length} Rows]`;
+        rowCount += series.rows.length;
+      }
+
+      if (rowCount > 0) {
+        resolve({
+          status: 'success',
+          message: info,
+        });
+      }
+      reject({
+        status: 'error',
+        message: 'No Data Entered',
+      });
+    });
+  }
+}
+
+export default InputDatasource;

+ 14 - 0
public/app/plugins/datasource/input/img/input.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
+<defs>
+<style>.cls-1{fill:url(#linear-gradient);}</style>
+<linearGradient id="linear-gradient" x1="50" y1="101.02" x2="50" y2="4.05" gradientTransform="matrix(1, 0, 0, -1, 0, 102)" gradientUnits="userSpaceOnUse">
+<stop offset="0" stop-color="#70b0df"/>
+<stop offset="0.5" stop-color="#1b81c5"/>
+<stop offset="1" stop-color="#4a98ce"/>
+</linearGradient>
+</defs>
+<g><path class="cls-1" d="M889.5,814.1h-201v50.2H701c20.8,0,37.7,16.9,37.7,37.7c0,20.8-16.9,37.7-37.7,37.7H600.5c-20.8,0-37.7-16.9-37.7-37.7c0-20.8,16.9-37.7,37.7-37.7h12.6v-50.2H110.5C55,814.1,10,769.1,10,713.6V286.5c0-55.5,45-100.5,100.5-100.5h502.6v-50.3h-12.6c-20.8,0-37.7-16.9-37.7-37.7c0-20.8,16.9-37.7,37.7-37.7H701c20.8,0,37.7,16.9,37.7,37.7c0,20.8-16.9,37.7-37.7,37.7h-12.6v50.3h201c55.5,0,100.5,45,100.5,100.5v427.2C990,769.1,945,814.1,889.5,814.1z M562.8,738.8h50.3V261.3h-50.3 M688.5,261.3v477.5h50.3V261.3H688.5z"/></g>
+</svg>

+ 45 - 0
public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx

@@ -0,0 +1,45 @@
+import React, { Component } from 'react';
+
+import coreModule from 'app/core/core_module';
+
+import { TableInputCSV, SeriesData, toCSV } from '@grafana/ui';
+
+interface Props {
+  data: SeriesData[];
+  onParsed: (data: SeriesData[]) => void;
+}
+
+interface State {
+  data: SeriesData[];
+  text: string;
+}
+
+/**
+ * Angular wrapper around TableInputCSV
+ */
+class Wraper extends Component<Props, State> {
+  constructor(props) {
+    super(props);
+    this.state = {
+      text: toCSV(props.data),
+      data: props.data,
+    };
+  }
+
+  onSeriesParsed = (data: SeriesData[], text: string) => {
+    this.setState({ data, text });
+    this.props.onParsed(data);
+  };
+
+  render() {
+    const { text } = this.state;
+    return <TableInputCSV text={text} onSeriesParsed={this.onSeriesParsed} width={'100%'} height={300} />;
+  }
+}
+
+coreModule.directive('csvInput', [
+  'reactDirective',
+  reactDirective => {
+    return reactDirective(Wraper, ['data', 'onParsed']);
+  },
+]);

+ 21 - 0
public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts

@@ -0,0 +1,21 @@
+import { SeriesData } from '@grafana/ui';
+
+// Loads the angular wrapping directive
+import './CSVInputWrapper';
+
+export class TableConfigCtrl {
+  static templateUrl = 'legacy/config.html';
+
+  current: any; // the Current Configuration (set by the plugin infra)
+
+  /** @ngInject */
+  constructor($scope: any, $injector: any) {
+    console.log('TableConfigCtrl Init', this);
+  }
+
+  onParsed = (data: SeriesData[]) => {
+    this.current.jsonData.data = data;
+  };
+}
+
+export default TableConfigCtrl;

+ 16 - 0
public/app/plugins/datasource/input/legacy/config.html

@@ -0,0 +1,16 @@
+<div class="gf-form-group">
+  <h4>Shared Data:</h4>
+  <span>Enter CSV</span>
+  <csv-input
+    data="ctrl.current.jsonData.data"
+    onParsed="ctrl.onParsed"
+    ></csv-input>
+</div>
+
+<div class="grafana-info-box">
+    This data is stored in the datasource json and is returned to every user
+    in the initial request for any datasource.  This is an appropriate place
+    to enter a few values.  Large datasets will perform better in other datasources.
+  <br/><br/>
+    <b>NOTE:</b> Changes to this data will only be reflected after a browser refresh.
+</div>

+ 6 - 0
public/app/plugins/datasource/input/module.ts

@@ -0,0 +1,6 @@
+import Datasource from './datasource';
+
+import InputQueryEditor from './InputQueryEditor';
+import InputConfigCtrl from './legacy/InputConfigCtrl';
+
+export { Datasource, InputQueryEditor as QueryEditor, InputConfigCtrl as ConfigCtrl };

+ 24 - 0
public/app/plugins/datasource/input/plugin.json

@@ -0,0 +1,24 @@
+{
+  "type": "datasource",
+  "name": "Direct Input",
+  "id": "input",
+  "state": "alpha",
+
+  "metrics": true,
+  "alerting": false,
+  "annotations": false,
+  "logs": false,
+  "explore": false,
+
+  "info": {
+    "description": "User Input Data Source for Grafana",
+    "author": {
+      "name": "Grafana Project",
+      "url": "https://grafana.com"
+    },
+    "logos": {
+      "small": "img/input.svg",
+      "large": "img/input.svg"
+    }
+  }
+}

+ 6 - 0
public/app/plugins/datasource/input/types.ts

@@ -0,0 +1,6 @@
+import { DataQuery, SeriesData } from '@grafana/ui/src/types';
+
+export interface InputQuery extends DataQuery {
+  // Data saved in the panel
+  data?: SeriesData[];
+}

+ 15 - 9
public/app/plugins/datasource/mixed/datasource.ts

@@ -1,19 +1,21 @@
-import angular from 'angular';
 import _ from 'lodash';
 
-class MixedDatasource {
+import { DataSourceApi, DataQuery, DataQueryOptions } from '@grafana/ui';
+import DatasourceSrv from 'app/features/plugins/datasource_srv';
+
+class MixedDatasource implements DataSourceApi<DataQuery> {
   /** @ngInject */
-  constructor(private $q, private datasourceSrv) {}
+  constructor(private datasourceSrv: DatasourceSrv) {}
 
-  query(options) {
+  query(options: DataQueryOptions<DataQuery>) {
     const sets = _.groupBy(options.targets, 'datasource');
-    const promises = _.map(sets, targets => {
+    const promises = _.map(sets, (targets: DataQuery[]) => {
       const dsName = targets[0].datasource;
       if (dsName === '-- Mixed --') {
-        return this.$q([]);
+        return Promise.resolve([]);
       }
 
-      const filtered = _.filter(targets, t => {
+      const filtered = _.filter(targets, (t: DataQuery) => {
         return !t.hide;
       });
 
@@ -22,16 +24,20 @@ class MixedDatasource {
       }
 
       return this.datasourceSrv.get(dsName).then(ds => {
-        const opt = angular.copy(options);
+        const opt = _.cloneDeep(options);
         opt.targets = filtered;
         return ds.query(opt);
       });
     });
 
-    return this.$q.all(promises).then(results => {
+    return Promise.all(promises).then(results => {
       return { data: _.flatten(_.map(results, 'data')) };
     });
   }
+
+  testDatasource() {
+    return Promise.resolve({});
+  }
 }
 
 export { MixedDatasource, MixedDatasource as Datasource };

+ 2 - 2
scripts/ci-frontend-metrics.sh

@@ -3,8 +3,8 @@
 echo -e "Collecting code stats (typescript errors & more)"
 
 ERROR_COUNT_LIMIT=6816
-DIRECTIVES_LIMIT=173
-CONTROLLERS_LIMIT=137
+DIRECTIVES_LIMIT=175
+CONTROLLERS_LIMIT=138
 
 ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --noImplicitAny true | grep -oP 'Found \K(\d+)')"
 DIRECTIVES="$(grep -r -o  directive public/app/**/*  | wc -l)"