mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into 8px-system-margins3
This commit is contained in:
@@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra
|
||||
|
||||
`npm install @grafana/ui`
|
||||
|
||||
## Development
|
||||
|
||||
For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead.
|
||||
|
||||
## Building @grafana/ui
|
||||
To build @grafana/ui run `npm run gui:build` script *from Grafana repository root*. The build will be created in `packages/grafana-ui/dist` directory. Following steps from [Development](#development) you can test built package.
|
||||
|
||||
## Releasing new version
|
||||
To release new version run `npm run gui:release` script *from Grafana repository root*. The script will prepare the distribution package as well as prompt you to bump library version and publish it to the NPM registry.
|
||||
|
||||
### Automatic version bump
|
||||
When running `npm run gui:release` package.json file will be automatically updated. Also, package.json file will be commited and pushed to upstream branch.
|
||||
|
||||
### Manual version bump
|
||||
To use `package.json` defined version run `npm run gui:release --usePackageJsonVersion` *from Grafana repository root*.
|
||||
|
||||
### Preparing release package without publishing to NPM registry
|
||||
For testing purposes there is `npm run gui:releasePrepare` task that prepares distribution package without publishing it to the NPM registry.
|
||||
|
||||
### V1 release process overview
|
||||
1. Package is compiled with TSC. Typings are created in `/dist` directory, and the compiled js lands in `/compiled` dir
|
||||
2. Rollup creates a CommonJS package based on compiled sources, and outputs it to `/dist` directory
|
||||
3. Readme, changelog and index.js files are moved to `/dist` directory
|
||||
4. Package version is bumped in both `@grafana/ui` package dir and in dist directory.
|
||||
5. Version commit is created and pushed to master branch
|
||||
5. Package is published to npm
|
||||
|
||||
|
||||
## Versioning
|
||||
To limit the confusion related to @grafana/ui and Grafana versioning we decided to keep the major version in sync between those two.
|
||||
This means, that first version of @grafana/ui is taged with 6.0.0-alpha.0 to keep version in sync with Grafana 6.0 release.
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -39,7 +39,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
this.props.onChange(color);
|
||||
};
|
||||
|
||||
handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
onChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
|
||||
const newColor = tinycolor(event.currentTarget.value);
|
||||
|
||||
this.setState({
|
||||
@@ -51,7 +51,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
}
|
||||
};
|
||||
|
||||
handleBlur = () => {
|
||||
onBlur = () => {
|
||||
const newColor = tinycolor(this.state.value);
|
||||
|
||||
if (!newColor.isValid()) {
|
||||
@@ -84,7 +84,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
<input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
|
||||
<input className="gf-form-input" value={value} onChange={this.onChange} onBlur={this.onBlur} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
static displayName = displayName;
|
||||
pickerTriggerRef = createRef<HTMLDivElement>();
|
||||
|
||||
handleColorChange = (color: string) => {
|
||||
onColorChange = (color: string) => {
|
||||
const { onColorChange, onChange } = this.props;
|
||||
const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
|
||||
|
||||
@@ -25,7 +25,7 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
render() {
|
||||
const popoverElement = React.createElement(popover, {
|
||||
...this.props,
|
||||
onChange: this.handleColorChange,
|
||||
onChange: this.onColorChange,
|
||||
});
|
||||
const { theme, children } = this.props;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
changeHandler(getColorFromHexRgbOrName(color, theme.type));
|
||||
};
|
||||
|
||||
handleTabChange = (tab: PickerType | keyof T) => {
|
||||
onTabChange = (tab: PickerType | keyof T) => {
|
||||
return () => this.setState({ activePicker: tab });
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
<>
|
||||
{Object.keys(customPickers).map(key => {
|
||||
return (
|
||||
<div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
|
||||
<div className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key}>
|
||||
{customPickers[key].name}
|
||||
</div>
|
||||
);
|
||||
@@ -119,10 +119,10 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
return (
|
||||
<div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
|
||||
<div className="ColorPickerPopover__tabs">
|
||||
<div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
|
||||
<div className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')}>
|
||||
Colors
|
||||
</div>
|
||||
<div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
|
||||
<div className={this.getTabClassName('spectrum')} onClick={this.onTabChange('spectrum')}>
|
||||
Custom
|
||||
</div>
|
||||
{this.renderCustomPickerTabs()}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
|
||||
const BasicGreen = getColorDefinitionByName('green');
|
||||
const BasicBlue = getColorDefinitionByName('blue');
|
||||
const BasicRed = getColorDefinitionByName('red');
|
||||
const LightBlue = getColorDefinitionByName('light-blue');
|
||||
|
||||
const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
|
||||
@@ -41,7 +41,7 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
|
||||
'Selected color',
|
||||
{
|
||||
Green: BasicGreen.variants.dark,
|
||||
Red: BasicBlue.variants.dark,
|
||||
Red: BasicRed.variants.dark,
|
||||
'Light blue': LightBlue.variants.dark,
|
||||
},
|
||||
'red'
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import propDeprecationWarning from '../../utils/propDeprecationWarning';
|
||||
import deprecationWarning from '../../utils/deprecationWarning';
|
||||
import { ColorPickerProps } from './ColorPickerPopover';
|
||||
|
||||
export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => {
|
||||
const { onColorChange } = props;
|
||||
if (onColorChange) {
|
||||
propDeprecationWarning(componentName, 'onColorChange', 'onChange');
|
||||
deprecationWarning(componentName, 'onColorChange', 'onChange');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { DeleteButton } from './DeleteButton';
|
||||
|
||||
const CenteredStory: FunctionComponent<{}> = ({ children }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '100vh ',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
storiesOf('UI/DeleteButton', module)
|
||||
.addDecorator(story => <CenteredStory>{story()}</CenteredStory>)
|
||||
.addDecorator(withCenteredStory)
|
||||
.add('default', () => {
|
||||
return <DeleteButton onConfirm={() => {}} />;
|
||||
return (
|
||||
<DeleteButton
|
||||
onConfirm={() => {
|
||||
action('Delete Confirmed')('delete!');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import TableInputCSV from './TableInputCSV';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { TableData } from '../../types/data';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
||||
const TableInputStories = storiesOf('UI/Table/Input', module);
|
||||
|
||||
TableInputStories.addDecorator(withCenteredStory);
|
||||
|
||||
TableInputStories.add('default', () => {
|
||||
return (
|
||||
<div style={{ width: '90%', height: '90vh' }}>
|
||||
<TableInputCSV
|
||||
text={'a,b,c\n1,2,3'}
|
||||
onTableParsed={(table: TableData, text: string) => {
|
||||
console.log('Table', table, text);
|
||||
action('Table')(table, text);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
import renderer from 'react-test-renderer';
|
||||
import TableInputCSV from './TableInputCSV';
|
||||
import { TableData } from '../../types/data';
|
||||
|
||||
describe('TableInputCSV', () => {
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(
|
||||
<TableInputCSV
|
||||
text={'a,b,c\n1,2,3'}
|
||||
onTableParsed={(table: TableData, text: string) => {
|
||||
// console.log('Table:', table, 'from:', text);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
.toJSON();
|
||||
//expect(tree).toMatchSnapshot();
|
||||
expect(tree).toBeDefined();
|
||||
});
|
||||
});
|
||||
95
packages/grafana-ui/src/components/Table/TableInputCSV.tsx
Normal file
95
packages/grafana-ui/src/components/Table/TableInputCSV.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { parseCSV, TableParseOptions, TableParseDetails } from '../../utils/processTableData';
|
||||
import { TableData } from '../../types/data';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
interface Props {
|
||||
options?: TableParseOptions;
|
||||
text: string;
|
||||
onTableParsed: (table: TableData, text: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
text: string;
|
||||
table: TableData;
|
||||
details: TableParseDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expects the container div to have size set and will fill it 100%
|
||||
*/
|
||||
class TableInputCSV extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Shoud this happen in onComponentMounted?
|
||||
const { text, options, onTableParsed } = props;
|
||||
const details = {};
|
||||
const table = parseCSV(text, options, details);
|
||||
this.state = {
|
||||
text,
|
||||
table,
|
||||
details,
|
||||
};
|
||||
onTableParsed(table, text);
|
||||
}
|
||||
|
||||
readCSV = debounce(() => {
|
||||
const details = {};
|
||||
const table = parseCSV(this.state.text, this.props.options, details);
|
||||
this.setState({ table, details });
|
||||
}, 150);
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
const { text } = this.state;
|
||||
if (text !== prevState.text || this.props.options !== prevProps.options) {
|
||||
this.readCSV();
|
||||
}
|
||||
// If the props text has changed, replace our local version
|
||||
if (this.props.text !== prevProps.text && this.props.text !== text) {
|
||||
this.setState({ text: this.props.text });
|
||||
}
|
||||
|
||||
if (this.state.table !== prevState.table) {
|
||||
this.props.onTableParsed(this.state.table, this.state.text);
|
||||
}
|
||||
}
|
||||
|
||||
onFooterClicked = (event: any) => {
|
||||
console.log('Errors', this.state);
|
||||
const message = this.state.details
|
||||
.errors!.map(err => {
|
||||
return err.message;
|
||||
})
|
||||
.join('\n');
|
||||
alert('CSV Parsing Errors:\n' + message);
|
||||
};
|
||||
|
||||
onTextChange = (event: any) => {
|
||||
this.setState({ text: event.target.value });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { table, details } = this.state;
|
||||
|
||||
const hasErrors = details.errors && details.errors.length > 0;
|
||||
const footerClassNames = hasErrors ? 'gf-table-input-csv-err' : '';
|
||||
|
||||
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} />
|
||||
<footer onClick={this.onFooterClicked} className={footerClassNames}>
|
||||
Rows:{table.rows.length}, Columns:{table.columns.length}
|
||||
{hasErrors ? <i className="fa fa-exclamation-triangle" /> : <i className="fa fa-check-circle" />}
|
||||
</footer>
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default TableInputCSV;
|
||||
24
packages/grafana-ui/src/components/Table/_TableInputCSV.scss
Normal file
24
packages/grafana-ui/src/components/Table/_TableInputCSV.scss
Normal file
@@ -0,0 +1,24 @@
|
||||
.gf-table-input-csv {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gf-table-input-csv textarea {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.gf-table-input-csv footer {
|
||||
position: absolute;
|
||||
bottom: 15px;
|
||||
right: 15px;
|
||||
border: 1px solid #222;
|
||||
background: #ccc;
|
||||
padding: 1px 4px;
|
||||
font-size: 80%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.gf-table-input-csv footer.gf-table-input-csv-err {
|
||||
background: yellow;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@import 'CustomScrollbar/CustomScrollbar';
|
||||
@import 'DeleteButton/DeleteButton';
|
||||
@import 'ThresholdsEditor/ThresholdsEditor';
|
||||
@import 'Table/TableInputCSV';
|
||||
@import 'Tooltip/Tooltip';
|
||||
@import 'Select/Select';
|
||||
@import 'PanelOptionsGroup/PanelOptionsGroup';
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface TimeSeriesVMs {
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface Column {
|
||||
export interface Column {
|
||||
text: string;
|
||||
title?: string;
|
||||
type?: string;
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
|
||||
Object {
|
||||
"columnMap": Object {},
|
||||
"columns": Array [
|
||||
Object {
|
||||
"text": "Column 1",
|
||||
},
|
||||
Object {
|
||||
"text": "Column 2",
|
||||
},
|
||||
Object {
|
||||
"text": "Column 3",
|
||||
},
|
||||
],
|
||||
"rows": Array [
|
||||
Array [
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
],
|
||||
Array [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
],
|
||||
Array [
|
||||
5,
|
||||
6,
|
||||
null,
|
||||
],
|
||||
],
|
||||
"type": "table",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`processTableData basic processing should read header and two rows 1`] = `
|
||||
Object {
|
||||
"columnMap": Object {},
|
||||
"columns": Array [
|
||||
Object {
|
||||
"text": "a",
|
||||
},
|
||||
Object {
|
||||
"text": "b",
|
||||
},
|
||||
Object {
|
||||
"text": "c",
|
||||
},
|
||||
],
|
||||
"rows": Array [
|
||||
Array [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
],
|
||||
Array [
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
],
|
||||
],
|
||||
"type": "table",
|
||||
}
|
||||
`;
|
||||
6
packages/grafana-ui/src/utils/deprecationWarning.ts
Normal file
6
packages/grafana-ui/src/utils/deprecationWarning.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const deprecationWarning = (file: string, oldName: string, newName: string) => {
|
||||
const message = `[Deprecation warning] ${file}: ${oldName} is deprecated. Use ${newName} instead`;
|
||||
console.warn(message);
|
||||
};
|
||||
|
||||
export default deprecationWarning;
|
||||
@@ -2,4 +2,6 @@ export * from './processTimeSeries';
|
||||
export * from './valueFormats/valueFormats';
|
||||
export * from './colors';
|
||||
export * from './namedColorsPalette';
|
||||
export * from './string';
|
||||
export * from './deprecationWarning';
|
||||
export { getMappedValue } from './valueMappings';
|
||||
|
||||
20
packages/grafana-ui/src/utils/processTableData.test.ts
Normal file
20
packages/grafana-ui/src/utils/processTableData.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { parseCSV } from './processTableData';
|
||||
|
||||
describe('processTableData', () => {
|
||||
describe('basic processing', () => {
|
||||
it('should read header and two rows', () => {
|
||||
const text = 'a,b,c\n1,2,3\n4,5,6';
|
||||
expect(parseCSV(text)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should generate a header and fix widths', () => {
|
||||
const text = '1\n2,3,4\n5,6';
|
||||
const table = parseCSV(text, {
|
||||
headerIsFirstLine: false,
|
||||
});
|
||||
expect(table.rows.length).toBe(3);
|
||||
|
||||
expect(table).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
133
packages/grafana-ui/src/utils/processTableData.ts
Normal file
133
packages/grafana-ui/src/utils/processTableData.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { TableData, Column } from '../types/index';
|
||||
|
||||
import Papa, { ParseError, ParseMeta } from 'papaparse';
|
||||
|
||||
// Subset of all parse options
|
||||
export interface TableParseOptions {
|
||||
headerIsFirstLine?: boolean; // Not a papa-parse option
|
||||
delimiter?: string; // default: ","
|
||||
newline?: string; // default: "\r\n"
|
||||
quoteChar?: string; // default: '"'
|
||||
encoding?: string; // default: ""
|
||||
comments?: boolean | string; // default: false
|
||||
}
|
||||
|
||||
export interface TableParseDetails {
|
||||
meta?: ParseMeta;
|
||||
errors?: ParseError[];
|
||||
}
|
||||
|
||||
/**
|
||||
* This makes sure the header and all rows have equal length.
|
||||
*
|
||||
* @param table (immutable)
|
||||
* @returns a new table that has equal length rows, or the same
|
||||
* table if no changes were needed
|
||||
*/
|
||||
export function matchRowSizes(table: TableData): TableData {
|
||||
const { rows } = table;
|
||||
let { columns } = table;
|
||||
|
||||
let sameSize = true;
|
||||
let size = columns.length;
|
||||
rows.forEach(row => {
|
||||
if (size !== row.length) {
|
||||
sameSize = false;
|
||||
size = Math.max(size, row.length);
|
||||
}
|
||||
});
|
||||
if (sameSize) {
|
||||
return table;
|
||||
}
|
||||
|
||||
// Pad Columns
|
||||
if (size !== columns.length) {
|
||||
const diff = size - columns.length;
|
||||
columns = [...columns];
|
||||
for (let i = 0; i < diff; i++) {
|
||||
columns.push({
|
||||
text: 'Column ' + (columns.length + 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pad Rows
|
||||
const fixedRows: any[] = [];
|
||||
rows.forEach(row => {
|
||||
const diff = size - row.length;
|
||||
if (diff > 0) {
|
||||
row = [...row];
|
||||
for (let i = 0; i < diff; i++) {
|
||||
row.push(null);
|
||||
}
|
||||
}
|
||||
fixedRows.push(row);
|
||||
});
|
||||
|
||||
return {
|
||||
columns,
|
||||
rows: fixedRows,
|
||||
type: table.type,
|
||||
columnMap: table.columnMap,
|
||||
};
|
||||
}
|
||||
|
||||
function makeColumns(values: any[]): Column[] {
|
||||
return values.map((value, index) => {
|
||||
if (!value) {
|
||||
value = 'Column ' + (index + 1);
|
||||
}
|
||||
return {
|
||||
text: value.toString().trim(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert CSV text into a valid TableData object
|
||||
*
|
||||
* @param text
|
||||
* @param options
|
||||
* @param details, if exists the result will be filled with debugging details
|
||||
*/
|
||||
export function parseCSV(text: string, options?: TableParseOptions, details?: TableParseDetails): TableData {
|
||||
const results = Papa.parse(text, { ...options, dynamicTyping: true, skipEmptyLines: true });
|
||||
const { data, meta, errors } = results;
|
||||
|
||||
// Fill the parse details for debugging
|
||||
if (details) {
|
||||
details.errors = errors;
|
||||
details.meta = meta;
|
||||
}
|
||||
|
||||
if (!data || data.length < 1) {
|
||||
// Show a more reasonable warning on empty input text
|
||||
if (details && !text) {
|
||||
errors.length = 0;
|
||||
errors.push({
|
||||
code: 'empty',
|
||||
message: 'Empty input text',
|
||||
type: 'warning',
|
||||
row: 0,
|
||||
});
|
||||
details.errors = errors;
|
||||
}
|
||||
return {
|
||||
columns: [],
|
||||
rows: [],
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Assume the first line is the header unless the config says its not
|
||||
const headerIsNotFirstLine = options && options.headerIsFirstLine === false;
|
||||
const header = headerIsNotFirstLine ? [] : results.data.shift();
|
||||
|
||||
return matchRowSizes({
|
||||
columns: makeColumns(header),
|
||||
rows: results.data,
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
});
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
const propDeprecationWarning = (componentName: string, propName: string, newPropName: string) => {
|
||||
const message = `[Deprecation warning] ${componentName}: ${propName} is deprecated. Use ${newPropName} instead`;
|
||||
console.warn(message);
|
||||
};
|
||||
|
||||
export default propDeprecationWarning;
|
||||
15
packages/grafana-ui/src/utils/string.test.ts
Normal file
15
packages/grafana-ui/src/utils/string.test.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { stringToJsRegex } from '@grafana/ui';
|
||||
|
||||
describe('stringToJsRegex', () => {
|
||||
it('should parse the valid regex value', () => {
|
||||
const output = stringToJsRegex('/validRegexp/');
|
||||
expect(output).toBeInstanceOf(RegExp);
|
||||
});
|
||||
|
||||
it('should throw error on invalid regex value', () => {
|
||||
const input = '/etc/hostname';
|
||||
expect(() => {
|
||||
stringToJsRegex(input);
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
13
packages/grafana-ui/src/utils/string.ts
Normal file
13
packages/grafana-ui/src/utils/string.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function stringToJsRegex(str: string): RegExp {
|
||||
if (str[0] !== '/') {
|
||||
return new RegExp('^' + str + '$');
|
||||
}
|
||||
|
||||
const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`'${str}' is not a valid regular expression.`);
|
||||
}
|
||||
|
||||
return new RegExp(match[1], match[2]);
|
||||
}
|
||||
Reference in New Issue
Block a user