Commit 08a22c80 by Ryan McKinley Committed by GitHub

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.
parent d27de81a
......@@ -12,14 +12,14 @@ TableInputStories.addDecorator(withCenteredStory);
TableInputStories.add('default', () => {
return (
<div style={{ width: '90%', height: '90vh' }}>
<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);
}}
/>
</div>
);
});
......@@ -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);
......
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,13 +59,17 @@ 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} />
<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) => {
......@@ -78,8 +83,6 @@ class TableInputCSV extends React.PureComponent<Props, State> {
</footer>
)}
</div>
)}
</AutoSizer>
);
}
}
......
......@@ -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;
}
......@@ -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';
......
......@@ -117,6 +117,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;
......
......@@ -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: ',',
......
......@@ -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,
......
......@@ -65,6 +65,7 @@ export class DatasourceSrv {
instanceSettings: dsConfig,
});
instance.id = dsConfig.id;
instance.name = name;
instance.components = dsPlugin.components;
instance.meta = dsConfig.meta;
......
......@@ -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,
......
// 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;
# 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.
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);
});
});
});
});
// 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;
<?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>
\ No newline at end of file
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']);
},
]);
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;
<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>
import Datasource from './datasource';
import InputQueryEditor from './InputQueryEditor';
import InputConfigCtrl from './legacy/InputConfigCtrl';
export { Datasource, InputQueryEditor as QueryEditor, InputConfigCtrl as ConfigCtrl };
{
"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"
}
}
}
import { DataQuery, SeriesData } from '@grafana/ui/src/types';
export interface InputQuery extends DataQuery {
// Data saved in the panel
data?: SeriesData[];
}
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 };
......@@ -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)"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment