+ );
+ }
+}
+
+export default InputQueryEditor;
diff --git a/public/app/plugins/datasource/input/README.md b/public/app/plugins/datasource/input/README.md
new file mode 100644
index 00000000000..facfeca8faf
--- /dev/null
+++ b/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.
diff --git a/public/app/plugins/datasource/input/datasource.test.ts b/public/app/plugins/datasource/input/datasource.test.ts
new file mode 100644
index 00000000000..164ba2d05c9
--- /dev/null
+++ b/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({
+ 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);
+ });
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/input/datasource.ts b/public/app/plugins/datasource/input/datasource.ts
new file mode 100644
index 00000000000..2f65de6b40e
--- /dev/null
+++ b/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 {
+ 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): Promise {
+ 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;
diff --git a/public/app/plugins/datasource/input/img/input.svg b/public/app/plugins/datasource/input/img/input.svg
new file mode 100644
index 00000000000..5d2d8ce6c5b
--- /dev/null
+++ b/public/app/plugins/datasource/input/img/input.svg
@@ -0,0 +1,14 @@
+
+
+
+
\ No newline at end of file
diff --git a/public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx b/public/app/plugins/datasource/input/legacy/CSVInputWrapper.tsx
new file mode 100644
index 00000000000..0974a9f44c1
--- /dev/null
+++ b/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 {
+ 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 ;
+ }
+}
+
+coreModule.directive('csvInput', [
+ 'reactDirective',
+ reactDirective => {
+ return reactDirective(Wraper, ['data', 'onParsed']);
+ },
+]);
diff --git a/public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts b/public/app/plugins/datasource/input/legacy/InputConfigCtrl.ts
new file mode 100644
index 00000000000..acca7e94327
--- /dev/null
+++ b/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;
diff --git a/public/app/plugins/datasource/input/legacy/config.html b/public/app/plugins/datasource/input/legacy/config.html
new file mode 100644
index 00000000000..7760879e4b7
--- /dev/null
+++ b/public/app/plugins/datasource/input/legacy/config.html
@@ -0,0 +1,16 @@
+
+
Shared Data:
+ Enter CSV
+
+
+
+
+ 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.
+
+ NOTE: Changes to this data will only be reflected after a browser refresh.
+