diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 6c28c8abbd3..aec32fd9282 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -24,6 +24,7 @@ "jquery": "^3.2.1", "lodash": "^4.17.10", "moment": "^2.22.2", + "papaparse": "^4.6.3", "react": "^16.6.3", "react-color": "^2.17.0", "react-custom-scrollbars": "^4.2.1", @@ -46,6 +47,7 @@ "@types/jquery": "^1.10.35", "@types/lodash": "^4.14.119", "@types/node": "^10.12.18", + "@types/papaparse": "^4.5.9", "@types/react": "^16.7.6", "@types/react-custom-scrollbars": "^4.0.5", "@types/react-test-renderer": "^16.0.3", diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx b/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx new file mode 100644 index 00000000000..ab2f09f2b74 --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx @@ -0,0 +1,12 @@ +import { storiesOf } from '@storybook/react'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; +import TableInputCSV from './TableInputCSV'; + +const TableInputStories = storiesOf('UI/Table/Input', module); + +TableInputStories.addDecorator(withCenteredStory); + +TableInputStories.add('default', () => { + return renderComponentWithTheme(TableInputCSV, {}); +}); diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx b/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx new file mode 100644 index 00000000000..70dba9e0b9d --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import renderer from 'react-test-renderer'; +import TableInputCSV from './TableInputCSV'; + +describe('TableInputCSV', () => { + it('renders correctly', () => { + const tree = renderer.create().toJSON(); + //expect(tree).toMatchSnapshot(); + expect(tree).toBeDefined(); + }); +}); diff --git a/packages/grafana-ui/src/components/Table/TableInputCSV.tsx b/packages/grafana-ui/src/components/Table/TableInputCSV.tsx new file mode 100644 index 00000000000..6b98c1c742f --- /dev/null +++ b/packages/grafana-ui/src/components/Table/TableInputCSV.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import debounce from 'lodash/debounce'; +import Papa, { ParseError, ParseMeta } from 'papaparse'; +import { TableData, Column } from '../../types/data'; + +// Subset of all parse configs +export interface ParseConfig { + delimiter?: string; // default: "," + newline?: string; // default: "\r\n" + quoteChar?: string; // default: '"' + encoding?: string; // default: "" + comments?: boolean | string; // default: false +} + +interface ParseResults { + table: TableData; + meta: ParseMeta; + errors: ParseError[]; +} + +export function parseCSV(text: string, config?: ParseConfig): ParseResults { + const results = Papa.parse(text, { ...config, dynamicTyping: true, skipEmptyLines: true }); + + const { data, meta, errors } = results; + if (!data || data.length < 1) { + if (!text) { + errors.length = 0; // clear other errors + } + errors.push({ + type: 'warning', // A generalization of the error + message: 'Empty Data', + code: 'empty', + row: 0, + }); + return { + table: { + columns: [], + rows: [], + type: 'table', + columnMap: {}, + } as TableData, + meta, + errors, + }; + } + + let same = true; + let cols = data[0].length; + data.forEach(row => { + if (cols !== row.length) { + same = false; + cols = Math.max(cols, row.length); + } + }); + + // Use a second pass to update the sizes + if (!same) { + errors.push({ + type: 'warning', // A generalization of the error + message: 'not all rows have the same width', + code: 'width', + row: 0, + }); + // Add null values to the end of all short arrays + data.forEach(row => { + const diff = cols - row.length; + for (let i = 0; i < diff; i++) { + row.push(null); + } + }); + } + + const first = results.data.shift(); + return { + table: { + columns: first.map((v: any, index: number) => { + if (!v) { + v = 'Column ' + (index + 1); + } + return { + text: v.toString().trim(), + } as Column; + }), + rows: results.data, + type: 'table', + columnMap: {}, + } as TableData, + meta, + errors, + }; +} + +interface Props { + config?: ParseConfig; +} + +interface State { + text: string; + results: ParseResults; +} + +class TableInputCSV extends React.PureComponent { + constructor(props: Props) { + super(props); + this.state = { + text: '', + results: parseCSV('', this.props.config), + }; + } + + readCSV = debounce(() => { + const results = parseCSV(this.state.text, this.props.config); + this.setState({ results }); + console.log('GOT:', results); + }, 150); + + componentDidUpdate(prevProps: Props, prevState: State) { + if (this.state.text !== prevState.text || this.props.config !== prevProps.config) { + this.readCSV(); + } + } + + handleChange = (event: any) => { + this.setState({ text: event.target.value }); + }; + handleBlur = (event: React.SyntheticEvent) => { + // console.log('BLUR', event); + }; + + render() { + const { table, errors } = this.state.results; + + return ( +
+