merge with master

This commit is contained in:
ryan 2019-03-20 08:16:34 -07:00
commit 2c8e1cbdb1
63 changed files with 2515 additions and 3394 deletions

View File

@ -11,11 +11,11 @@
"url": "http://github.com/grafana/grafana.git" "url": "http://github.com/grafana/grafana.git"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.1.2", "@babel/core": "^7.3.4",
"@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/preset-env": "^7.1.0", "@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.0", "@babel/preset-typescript": "^7.3.3",
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1", "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
"@types/angular": "^1.6.6", "@types/angular": "^1.6.6",
"@types/chalk": "^2.2.0", "@types/chalk": "^2.2.0",
@ -24,26 +24,28 @@
"@types/d3": "^4.10.1", "@types/d3": "^4.10.1",
"@types/enzyme": "^3.1.13", "@types/enzyme": "^3.1.13",
"@types/inquirer": "^0.0.43", "@types/inquirer": "^0.0.43",
"@types/jest": "^23.3.2", "@types/jest": "^24.0.11",
"@types/jquery": "^1.10.35", "@types/jquery": "^1.10.35",
"@types/node": "^8.0.31", "@types/node": "^8.0.31",
"@types/papaparse": "^4.5.9",
"@types/react": "^16.8.8", "@types/react": "^16.8.8",
"@types/react-dom": "^16.8.2", "@types/react-dom": "^16.8.2",
"@types/react-grid-layout": "^0.16.6", "@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4", "@types/react-select": "^2.0.4",
"@types/react-transition-group": "^2.0.15", "@types/react-transition-group": "^2.0.15",
"@types/react-virtualized": "^9.18.12", "@types/react-virtualized": "^9.18.12",
"@types/clipboard": "^2.0.1",
"angular-mocks": "1.6.6", "angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0", "autoprefixer": "^9.4.10",
"axios": "^0.17.1", "axios": "^0.18.0",
"babel-core": "^7.0.0-bridge", "babel-core": "^7.0.0-bridge",
"babel-jest": "^23.6.0", "babel-jest": "^24.5.0",
"babel-loader": "^8.0.4", "babel-loader": "^8.0.4",
"babel-plugin-angularjs-annotate": "^0.9.0", "babel-plugin-angularjs-annotate": "^0.10.0",
"chalk": "^2.4.2", "chalk": "^2.4.2",
"clean-webpack-plugin": "^0.1.19", "clean-webpack-plugin": "^2.0.0",
"concurrently": "^4.1.0", "concurrently": "^4.1.0",
"css-loader": "^0.28.7", "css-loader": "^2.1.1",
"enzyme": "^3.6.0", "enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0", "enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.4", "enzyme-to-json": "^3.3.4",
@ -51,11 +53,11 @@
"es6-shim": "^0.35.3", "es6-shim": "^0.35.3",
"execa": "^1.0.0", "execa": "^1.0.0",
"expect.js": "~0.2.0", "expect.js": "~0.2.0",
"expose-loader": "^0.7.3", "expose-loader": "0.7.5",
"file-loader": "^1.1.11", "file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^0.4.9", "fork-ts-checker-webpack-plugin": "^1.0.0",
"gaze": "^1.1.2", "gaze": "^1.1.2",
"glob": "~7.0.0", "glob": "~7.1.3",
"grunt": "1.0.1", "grunt": "1.0.1",
"grunt-angular-templates": "^1.1.0", "grunt-angular-templates": "^1.1.0",
"grunt-cli": "~1.2.0", "grunt-cli": "~1.2.0",
@ -69,29 +71,29 @@
"grunt-sass-lint": "^0.2.4", "grunt-sass-lint": "^0.2.4",
"grunt-usemin": "3.1.1", "grunt-usemin": "3.1.1",
"grunt-webpack": "^3.0.2", "grunt-webpack": "^3.0.2",
"html-loader": "^0.5.1", "html-loader": "0.5.5",
"html-webpack-harddisk-plugin": "^0.2.0", "html-webpack-harddisk-plugin": "^1.0.1",
"html-webpack-plugin": "^3.2.0", "html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1", "husky": "^1.3.1",
"inquirer": "^6.2.2", "inquirer": "^6.2.2",
"jest": "^23.6.0", "jest": "^24.5.0",
"jest-date-mock": "^1.0.6", "jest-date-mock": "^1.0.6",
"lint-staged": "^8.1.3", "lint-staged": "^8.1.3",
"load-grunt-tasks": "3.5.2", "load-grunt-tasks": "3.5.2",
"mini-css-extract-plugin": "^0.4.0", "mini-css-extract-plugin": "^0.5.0",
"mocha": "^4.0.1", "mocha": "^4.0.1",
"monaco-editor": "^0.15.6", "monaco-editor": "^0.15.6",
"ng-annotate-loader": "^0.6.1", "ng-annotate-loader": "^0.6.1",
"ng-annotate-webpack-plugin": "^0.3.0", "ng-annotate-webpack-plugin": "^0.3.0",
"ngtemplate-loader": "^2.0.1", "ngtemplate-loader": "^2.0.1",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"npm": "^5.4.2", "npm": "^6.9.0",
"optimize-css-assets-webpack-plugin": "^4.0.2", "optimize-css-assets-webpack-plugin": "^5.0.1",
"ora": "^3.1.0", "ora": "^3.2.0",
"phantomjs-prebuilt": "^2.1.15", "phantomjs-prebuilt": "^2.1.15",
"postcss-browser-reporter": "^0.5.0", "postcss-browser-reporter": "^0.5.0",
"postcss-loader": "^2.0.6", "postcss-loader": "^3.0.0",
"postcss-reporter": "^5.0.0", "postcss-reporter": "^6.0.1",
"prettier": "1.16.4", "prettier": "1.16.4",
"react-hot-loader": "^4.3.6", "react-hot-loader": "^4.3.6",
"react-test-renderer": "^16.5.0", "react-test-renderer": "^16.5.0",
@ -99,27 +101,27 @@
"regexp-replace-loader": "^1.0.1", "regexp-replace-loader": "^1.0.1",
"rimraf": "^2.6.3", "rimraf": "^2.6.3",
"sass-lint": "^1.10.2", "sass-lint": "^1.10.2",
"sass-loader": "^7.0.1", "sass-loader": "7.1.0",
"semver": "^5.6.0", "semver": "^5.6.0",
"sinon": "1.17.6", "sinon": "1.17.6",
"style-loader": "^0.21.0", "style-loader": "0.23.1",
"systemjs": "0.20.19", "systemjs": "0.20.19",
"systemjs-plugin-css": "^0.1.36", "systemjs-plugin-css": "^0.1.36",
"ts-jest": "^23.10.4", "terser-webpack-plugin": "^1.2.3",
"ts-loader": "^5.1.0", "ts-jest": "^24.0.0",
"ts-node": "^8.0.2", "ts-loader": "5.3.3",
"tslib": "^1.9.3", "ts-node": "8.0.2",
"tslint": "^5.8.0", "tslib": "1.9.3",
"tslint": "5.14.0",
"tslint-loader": "^3.5.3", "tslint-loader": "^3.5.3",
"tslint-react": "^3.6.0", "tslint-react": "^3.6.0",
"typescript": "^3.0.3", "typescript": "3.3.3333",
"uglifyjs-webpack-plugin": "^1.2.7", "webpack": "4.29.6",
"webpack": "4.19.1", "webpack-bundle-analyzer": "3.1.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-cleanup-plugin": "^0.5.1", "webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^2.1.4", "webpack-cli": "3.2.3",
"webpack-dev-server": "^3.1.0", "webpack-dev-server": "3.2.1",
"webpack-merge": "^4.1.0", "webpack-merge": "4.2.1",
"zone.js": "^0.7.2" "zone.js": "^0.7.2"
}, },
"scripts": { "scripts": {
@ -176,7 +178,7 @@
"baron": "^3.0.3", "baron": "^3.0.3",
"brace": "^0.10.0", "brace": "^0.10.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"clipboard": "^1.7.1", "clipboard": "^2.0.4",
"d3": "^4.11.0", "d3": "^4.11.0",
"d3-scale-chromatic": "^1.3.0", "d3-scale-chromatic": "^1.3.0",
"eventemitter3": "^2.0.3", "eventemitter3": "^2.0.3",
@ -188,6 +190,7 @@
"mousetrap": "^1.6.0", "mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
"nodemon": "^1.18.10", "nodemon": "^1.18.10",
"papaparse": "^4.6.3",
"prismjs": "^1.6.0", "prismjs": "^1.6.0",
"prop-types": "^15.6.2", "prop-types": "^15.6.2",
"rc-cascader": "^0.14.0", "rc-cascader": "^0.14.0",

View File

@ -45,7 +45,7 @@ $arrowSize: 15px;
border-right-color: transparent; border-right-color: transparent;
border-top-color: transparent; border-top-color: transparent;
top: 0; top: 0;
left: calc(100% -$arrowSize); left: calc(100%-#{$arrowSize});
} }
&[data-placement^='right'] { &[data-placement^='right'] {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { FormField, Props } from './FormField'; import { FormField, Props } from './FormField';
const setup = (propOverrides?: object) => { const setup = (propOverrides?: Partial<Props>) => {
const props: Props = { const props: Props = {
label: 'Test', label: 'Test',
labelWidth: 11, labelWidth: 11,
@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => {
return shallow(<FormField {...props} />); return shallow(<FormField {...props} />);
}; };
describe('Render', () => { describe('FormField', () => {
it('should render component', () => { it('should render component with default inputEl', () => {
const wrapper = setup(); const wrapper = setup();
expect(wrapper).toMatchSnapshot(); expect(wrapper).toMatchSnapshot();
}); });
it('should render component with custom inputEl', () => {
const wrapper = setup({
inputEl: (
<>
<span>Input</span>
<button>Ok</button>
</>
),
});
expect(wrapper).toMatchSnapshot();
});
}); });

View File

@ -5,6 +5,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string; label: string;
labelWidth?: number; labelWidth?: number;
inputWidth?: number; inputWidth?: number;
inputEl?: React.ReactNode;
} }
const defaultProps = { const defaultProps = {
@ -12,14 +13,18 @@ const defaultProps = {
inputWidth: 12, inputWidth: 12,
}; };
const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => { /**
* Default form field including label used in Grafana UI. Default input element is simple <input />. You can also pass
* custom inputEl if required in which case inputWidth and inputProps are ignored.
*/
export const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => {
return ( return (
<div className="form-field"> <div className="form-field">
<FormLabel width={labelWidth}>{label}</FormLabel> <FormLabel width={labelWidth}>{label}</FormLabel>
<input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} /> {inputEl || <input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />}
</div> </div>
); );
}; };
FormField.displayName = 'FormField';
FormField.defaultProps = defaultProps; FormField.defaultProps = defaultProps;
export { FormField };

View File

@ -1,6 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = ` exports[`FormField should render component with custom inputEl 1`] = `
<div
className="form-field"
>
<Component
width={11}
>
Test
</Component>
<span>
Input
</span>
<button>
Ok
</button>
</div>
`;
exports[`FormField should render component with default inputEl 1`] = `
<div <div
className="form-field" className="form-field"
> >

View File

@ -0,0 +1,38 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { boolean } from '@storybook/addon-knobs';
import { SecretFormField } from './SecretFormField';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { UseState } from '../../utils/storybook/UseState';
const SecretFormFieldStories = storiesOf('UI/SecretFormField/SecretFormField', module);
SecretFormFieldStories.addDecorator(withCenteredStory);
const getSecretFormFieldKnobs = () => {
return {
isConfigured: boolean('Set configured state', false),
};
};
SecretFormFieldStories.add('default', () => {
const knobs = getSecretFormFieldKnobs();
return (
<UseState initialState="Input value">
{(value, setValue) => (
<SecretFormField
label={'Secret field'}
labelWidth={10}
value={value}
isConfigured={knobs.isConfigured}
onChange={e => setValue(e.currentTarget.value)}
onReset={() => {
action('Value was reset')('');
setValue('');
}}
/>
)}
</UseState>
);
});

View File

@ -0,0 +1,71 @@
import { omit } from 'lodash';
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormField } from '..';
interface Props extends InputHTMLAttributes<HTMLInputElement> {
// Function to use when reset is clicked. Means you have to reset the input value yourself as this is uncontrolled
// component (or do something else if required).
onReset: () => void;
isConfigured: boolean;
label?: string;
labelWidth?: number;
inputWidth?: number;
// Placeholder of the input field when in non configured state.
placeholder?: string;
}
const defaultProps = {
inputWidth: 12,
placeholder: 'Password',
label: 'Password',
};
/**
* Form field that has 2 states configured and not configured. If configured it will not show its contents and adds
* a reset button that will clear the input and makes it accessible. In non configured state it behaves like normal
* form field. This is used for passwords or anything that is encrypted on the server and is later returned encrypted
* to the user (like datasource passwords).
*/
export const SecretFormField: FunctionComponent<Props> = ({
label,
labelWidth,
inputWidth,
onReset,
isConfigured,
placeholder,
...inputProps
}: Props) => {
return (
<FormField
label={label!}
labelWidth={labelWidth}
inputEl={
isConfigured ? (
<>
<input
type="text"
className={`gf-form-input width-${inputWidth! - 2}`}
disabled={true}
value="configured"
{...omit(inputProps, 'value')}
/>
<button className="btn btn-secondary gf-form-btn" onClick={onReset}>
reset
</button>
</>
) : (
<input
type="password"
className={`gf-form-input width-${inputWidth}`}
placeholder={placeholder}
{...inputProps}
/>
)
}
/>
);
};
SecretFormField.defaultProps = defaultProps;
SecretFormField.displayName = 'SecretFormField';

View File

@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
const suffix = (rowId + 1).toString(); const suffix = (rowId + 1).toString();
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix); return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
}), }),
type: 'table',
columnMap: {},
}; };
} }

View File

@ -8,6 +8,7 @@ import {
CellMeasurerCache, CellMeasurerCache,
CellMeasurer, CellMeasurer,
GridCellProps, GridCellProps,
Index,
} from 'react-virtualized'; } from 'react-virtualized';
import { Themeable } from '../../types/theme'; import { Themeable } from '../../types/theme';
@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index';
export interface Props extends Themeable { export interface Props extends Themeable {
data: TableData; data: TableData;
minColumnWidth: number;
showHeader: boolean; showHeader: boolean;
fixedHeader: boolean; fixedHeader: boolean;
fixedColumns: number; fixedColumns: number;
@ -46,6 +48,7 @@ interface State {
interface ColumnRenderInfo { interface ColumnRenderInfo {
header: string; header: string;
width: number;
builder: TableCellBuilder; builder: TableCellBuilder;
} }
@ -64,6 +67,7 @@ export class Table extends Component<Props, State> {
fixedHeader: true, fixedHeader: true,
fixedColumns: 0, fixedColumns: 0,
rotate: false, rotate: false,
minColumnWidth: 150,
}; };
constructor(props: Props) { constructor(props: Props) {
@ -76,7 +80,7 @@ export class Table extends Component<Props, State> {
this.renderer = this.initColumns(props); this.renderer = this.initColumns(props);
this.measurer = new CellMeasurerCache({ this.measurer = new CellMeasurerCache({
defaultHeight: 30, defaultHeight: 30,
defaultWidth: 150, fixedWidth: true,
}); });
} }
@ -110,7 +114,8 @@ export class Table extends Component<Props, State> {
/** Given the configuration, setup how each column gets rendered */ /** Given the configuration, setup how each column gets rendered */
initColumns(props: Props): ColumnRenderInfo[] { initColumns(props: Props): ColumnRenderInfo[] {
const { styles, data } = props; const { styles, data, width, minColumnWidth } = props;
const columnWidth = Math.max(width / data.columns.length, minColumnWidth);
return data.columns.map((col, index) => { return data.columns.map((col, index) => {
let title = col.text; let title = col.text;
@ -131,6 +136,7 @@ export class Table extends Component<Props, State> {
return { return {
header: title, header: title,
width: columnWidth,
builder: getCellBuilder(col, style, this.props), builder: getCellBuilder(col, style, this.props),
}; };
}); });
@ -228,6 +234,10 @@ export class Table extends Component<Props, State> {
); );
}; };
getColumnWidth = (col: Index): number => {
return this.renderer[col.index].width;
};
render() { render() {
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props; const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
const { data } = this.state; const { data } = this.state;
@ -269,7 +279,7 @@ export class Table extends Component<Props, State> {
rowCount={rowCount} rowCount={rowCount}
overscanColumnCount={8} overscanColumnCount={8}
overscanRowCount={8} overscanRowCount={8}
columnWidth={this.measurer.columnWidth} columnWidth={this.getColumnWidth}
deferredMeasurementCache={this.measurer} deferredMeasurementCache={this.measurer}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
rowHeight={this.measurer.rowHeight} rowHeight={this.measurer.rowHeight}

View File

@ -17,7 +17,7 @@ exports[`Render should render with base threshold 1`] = `
], ],
"results": Array [ "results": Array [
Object { Object {
"isThrow": false, "type": "return",
"value": undefined, "value": undefined,
}, },
], ],

View File

@ -14,6 +14,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles';
// Forms // Forms
export { FormLabel } from './FormLabel/FormLabel'; export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField'; export { FormField } from './FormField/FormField';
export { SecretFormField } from './SecretFormFied/SecretFormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker'; export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';

View File

@ -51,27 +51,13 @@ export enum NullValueMode {
export type TimeSeriesVMs = TimeSeriesVM[]; export type TimeSeriesVMs = TimeSeriesVM[];
export interface Column { export interface Column {
text: string; text: string; // The column name
title?: string; type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove?
type?: string; filterable?: boolean; // currently only set by elasticsearch, and used in the table panel
sort?: boolean;
desc?: boolean;
filterable?: boolean;
unit?: string; unit?: string;
} }
export interface TableData { export interface TableData {
columns: Column[]; columns: Column[];
rows: any[]; rows: any[];
type: string;
columnMap: any;
}
export type SingleStatValue = number | string | null;
/*
* So we can add meta info like tags & series name
*/
export interface SingleStatValueInfo {
value: SingleStatValue;
} }

View File

@ -1,12 +1,12 @@
import { ComponentClass } from 'react'; import { ComponentClass } from 'react';
import { TimeSeries, LoadingState, TableData } from './data'; import { LoadingState, TableData } from './data';
import { TimeRange } from './time'; import { TimeRange } from './time';
import { ScopedVars } from './datasource'; import { ScopedVars } from './datasource';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelProps<T = any> { export interface PanelProps<T = any> {
panelData: PanelData; data?: TableData[];
timeRange: TimeRange; timeRange: TimeRange;
loading: LoadingState; loading: LoadingState;
options: T; options: T;
@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
replaceVariables: InterpolateFunction; replaceVariables: InterpolateFunction;
} }
export interface PanelData {
timeSeries?: TimeSeries[];
tableData?: TableData;
}
export interface PanelEditorProps<T = any> { export interface PanelEditorProps<T = any> {
options: T; options: T;
onOptionsChange: (options: T) => void; onOptionsChange: (options: T) => void;

View File

@ -2,7 +2,6 @@
exports[`processTableData basic processing should generate a header and fix widths 1`] = ` exports[`processTableData basic processing should generate a header and fix widths 1`] = `
Object { Object {
"columnMap": Object {},
"columns": Array [ "columns": Array [
Object { Object {
"text": "Column 1", "text": "Column 1",
@ -31,13 +30,11 @@ Object {
null, null,
], ],
], ],
"type": "table",
} }
`; `;
exports[`processTableData basic processing should read header and two rows 1`] = ` exports[`processTableData basic processing should read header and two rows 1`] = `
Object { Object {
"columnMap": Object {},
"columns": Array [ "columns": Array [
Object { Object {
"text": "a", "text": "a",
@ -61,6 +58,5 @@ Object {
6, 6,
], ],
], ],
"type": "table",
} }
`; `;

View File

@ -1,5 +1,5 @@
export * from './processTimeSeries'; export * from './processTimeSeries';
export * from './singlestat'; export * from './processTableData';
export * from './valueFormats/valueFormats'; export * from './valueFormats/valueFormats';
export * from './colors'; export * from './colors';
export * from './namedColorsPalette'; export * from './namedColorsPalette';

View File

@ -1,4 +1,4 @@
import { parseCSV } from './processTableData'; import { parseCSV, toTableData } from './processTableData';
describe('processTableData', () => { describe('processTableData', () => {
describe('basic processing', () => { describe('basic processing', () => {
@ -18,3 +18,41 @@ describe('processTableData', () => {
}); });
}); });
}); });
describe('toTableData', () => {
it('converts timeseries to table skipping nulls', () => {
const input1 = {
target: 'Field Name',
datapoints: [[100, 1], [200, 2]],
};
const input2 = {
// without target
target: '',
datapoints: [[100, 1], [200, 2]],
};
const data = toTableData([null, input1, input2, null, null]);
expect(data.length).toBe(2);
expect(data[0].columns[0].text).toBe(input1.target);
expect(data[0].rows).toBe(input1.datapoints);
// Default name
expect(data[1].columns[0].text).toEqual('Value');
});
it('keeps tableData unchanged', () => {
const input = {
columns: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
rows: [[100, 'A', 1], [200, 'B', 2], [300, 'C', 3]],
};
const data = toTableData([null, input, null, null]);
expect(data.length).toBe(1);
expect(data[0]).toBe(input);
});
it('supports null values OK', () => {
expect(toTableData([null, null, null, null])).toEqual([]);
expect(toTableData(undefined)).toEqual([]);
expect(toTableData((null as unknown) as any[])).toEqual([]);
expect(toTableData([])).toEqual([]);
});
});

View File

@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
import Papa, { ParseError, ParseMeta } from 'papaparse'; import Papa, { ParseError, ParseMeta } from 'papaparse';
// Types // Types
import { TableData, Column } from '../types'; import { TableData, Column, TimeSeries } from '../types';
// Subset of all parse options // Subset of all parse options
export interface TableParseOptions { export interface TableParseOptions {
@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
return { return {
columns, columns,
rows: fixedRows, rows: fixedRows,
type: table.type,
columnMap: table.columnMap,
}; };
} }
@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return { return {
columns: [], columns: [],
rows: [], rows: [],
type: 'table',
columnMap: {},
}; };
} }
@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return matchRowSizes({ return matchRowSizes({
columns: makeColumns(header), columns: makeColumns(header),
rows: results.data, rows: results.data,
type: 'table',
columnMap: {},
}); });
} }
function convertTimeSeriesToTableData(timeSeries: TimeSeries): TableData {
return {
columns: [
{
text: timeSeries.target || 'Value',
unit: timeSeries.unit,
},
{
text: 'Time',
type: 'time',
unit: 'dateTimeAsIso',
},
],
rows: timeSeries.datapoints,
};
}
export const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
export const toTableData = (results?: any[]): TableData[] => {
if (!results) {
return [];
}
return results
.filter(d => !!d)
.map(data => {
if (data.hasOwnProperty('columns')) {
return data as TableData;
}
if (data.hasOwnProperty('datapoints')) {
return convertTimeSeriesToTableData(data);
}
// TODO, try to convert JSON to table?
console.warn('Can not convert', data);
throw new Error('Unsupported data format');
});
};
export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData { export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
if (isNumber(sortIndex)) { if (isNumber(sortIndex)) {
const copy = { const copy = {

View File

@ -4,24 +4,43 @@ import isNumber from 'lodash/isNumber';
import { colors } from './colors'; import { colors } from './colors';
// Types // Types
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
import { getFlotPairs } from './flotPairs'; import { getFlotPairs } from './flotPairs';
import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
interface Options { interface Options {
timeSeries: TimeSeries[]; data: TableData[];
xColumn?: number; // Time (or null to guess)
yColumn?: number; // Value (or null to guess)
nullValueMode: NullValueMode; nullValueMode: NullValueMode;
} }
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { // NOTE: this should move to processTableData.ts
const vmSeries = timeSeries.map((item, index) => { // I left it as is so the merge changes are more clear.
export function processTimeSeries({ data, xColumn, yColumn, nullValueMode }: Options): TimeSeriesVMs {
const vmSeries = data.map((item, index) => {
if (!isNumber(xColumn)) {
xColumn = 1; // Default timeseries colum. TODO, find first time field!
}
if (!isNumber(yColumn)) {
yColumn = 0; // TODO, find first non-time field
}
// TODO? either % or throw error?
if (xColumn >= item.columns.length) {
throw new Error('invalid colum: ' + xColumn);
}
if (yColumn >= item.columns.length) {
throw new Error('invalid colum: ' + yColumn);
}
const colorIndex = index % colors.length; const colorIndex = index % colors.length;
const label = item.target; const label = item.columns[yColumn].text;
// Use external calculator just to make sure it works :) // Use external calculator just to make sure it works :)
const result = getFlotPairs({ const result = getFlotPairs({
rows: item.datapoints, rows: item.rows,
xIndex: 1, xIndex: xColumn,
yIndex: 0, yIndex: yColumn,
nullValueMode, nullValueMode,
}); });
@ -50,9 +69,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
let previousValue = 0; let previousValue = 0;
let previousDeltaUp = true; let previousDeltaUp = true;
for (let i = 0; i < item.datapoints.length; i++) { for (let i = 0; i < item.rows.length; i++) {
currentValue = item.datapoints[i][0]; currentValue = item.rows[i][yColumn];
currentTime = item.datapoints[i][1]; currentTime = item.rows[i][xColumn];
if (typeof currentTime !== 'number') { if (typeof currentTime !== 'number') {
continue; continue;
@ -103,7 +122,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
if (previousValue > currentValue) { if (previousValue > currentValue) {
// counter reset // counter reset
previousDeltaUp = false; previousDeltaUp = false;
if (i === item.datapoints.length - 1) { if (i === item.rows.length - 1) {
// reset on last // reset on last
delta += currentValue; delta += currentValue;
} }

View File

@ -1,33 +0,0 @@
import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
import { processTimeSeries } from './processTimeSeries';
export interface SingleStatProcessingOptions {
panelData: PanelData;
stat: string;
}
//
// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
//
export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
const { panelData, stat } = options;
if (panelData.timeSeries) {
const timeSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
nullValueMode: NullValueMode.Null,
});
return timeSeries.map((series, index) => {
const value = stat !== 'name' ? series.stats[stat] : series.label;
return {
value: value,
};
});
} else if (panelData.tableData) {
throw { message: 'Panel data not supported' };
}
return [];
}

View File

@ -2,7 +2,7 @@ import React from 'react';
interface StateHolderProps<T> { interface StateHolderProps<T> {
initialState: T; initialState: T;
children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element; children: (currentState: T, updateState: (nextState: T) => void) => React.ReactNode;
} }
export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> { export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {

View File

@ -101,7 +101,7 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration
// insert or update depending on if item already exist // insert or update depending on if item already exist
if has { if has {
sql := `UPDATE cache_data SET data=?, created=?, expire=? WHERE cache_key='?'` sql := `UPDATE cache_data SET data=?, created_at=?, expires=? WHERE cache_key=?`
_, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key) _, err = session.Exec(sql, data, getTime().Unix(), expiresInSeconds, key)
} else { } else {
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)` sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`

View File

@ -5,7 +5,6 @@ import (
"time" "time"
"github.com/bmizerany/assert" "github.com/bmizerany/assert"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
) )
@ -54,3 +53,20 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) {
_, err = db.Get("key5") _, err = db.Get("key5")
assert.Equal(t, err, nil) assert.Equal(t, err, nil)
} }
func TestSecondSet(t *testing.T) {
var err error
sqlstore := sqlstore.InitTestDB(t)
db := &databaseCache{
SQLStore: sqlstore,
log: log.New("remotecache.database"),
}
obj := &CacheableStruct{String: "hey!"}
err = db.Set("killa-gorilla", obj, 0)
err = db.Set("killa-gorilla", obj, 0)
assert.Equal(t, err, nil)
}

View File

@ -36,6 +36,10 @@ func (r RoleType) Includes(other RoleType) bool {
return other != ROLE_ADMIN return other != ROLE_ADMIN
} }
if r == ROLE_VIEWER {
return other == ROLE_VIEWER
}
return false return false
} }

View File

@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu'; import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect'; import { MetricSelect } from './components/Select/MetricSelect';
import AppNotificationList from './components/AppNotifications/AppNotificationList'; import AppNotificationList from './components/AppNotifications/AppNotificationList';
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui'; import { ColorPicker, SeriesColorPickerPopoverWithTheme, SecretFormField } from '@grafana/ui';
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor'; import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
export function registerAngularDirectives() { export function registerAngularDirectives() {
@ -59,4 +59,11 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }], ['datasource', { watchDepth: 'reference' }],
['templateSrv', { watchDepth: 'reference' }], ['templateSrv', { watchDepth: 'reference' }],
]); ]);
react2AngularDirective('secretFormField', SecretFormField, [
'value',
'isConfigured',
'inputWidth',
['onReset', { watchDepth: 'reference', wrapApply: true }],
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
} }

View File

@ -1,4 +1,4 @@
import React, { FC } from 'react'; import React, { FC, CSSProperties } from 'react';
import Transition, { ExitHandler } from 'react-transition-group/Transition'; import Transition, { ExitHandler } from 'react-transition-group/Transition';
interface Props { interface Props {
@ -10,12 +10,12 @@ interface Props {
} }
export const FadeIn: FC<Props> = props => { export const FadeIn: FC<Props> = props => {
const defaultStyle = { const defaultStyle: CSSProperties = {
transition: `opacity ${props.duration}ms linear`, transition: `opacity ${props.duration}ms linear`,
opacity: 0, opacity: 0,
}; };
const transitionStyles = { const transitionStyles: { [str: string]: CSSProperties } = {
exited: { opacity: 0, display: 'none' }, exited: { opacity: 0, display: 'none' },
entering: { opacity: 0 }, entering: { opacity: 0 },
entered: { opacity: 1 }, entered: { opacity: 1 },

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { CSSProperties, FC } from 'react';
import Transition from 'react-transition-group/Transition'; import Transition from 'react-transition-group/Transition';
interface Style { interface Style {
@ -16,11 +16,18 @@ export const defaultStyle: Style = {
overflow: 'hidden', overflow: 'hidden',
}; };
export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => { export interface Props {
children: React.ReactNode;
in: boolean;
maxHeight?: number;
style?: CSSProperties;
}
export const SlideDown: FC<Props> = ({ children, in: inProp, maxHeight = defaultMaxHeight, style = defaultStyle }) => {
// There are 4 main states a Transition can be in: // There are 4 main states a Transition can be in:
// ENTERING, ENTERED, EXITING, EXITED // ENTERING, ENTERED, EXITING, EXITED
// https://reactcommunity.org/react-transition-group/ // https://reactcommunity.or[g/react-transition-group/
const transitionStyles = { const transitionStyles: { [str: string]: CSSProperties } = {
exited: { maxHeight: 0 }, exited: { maxHeight: 0 },
entering: { maxHeight: maxHeight }, entering: { maxHeight: maxHeight },
entered: { maxHeight: 'unset', overflow: 'visible' }, entered: { maxHeight: 'unset', overflow: 'visible' },
@ -34,6 +41,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
style={{ style={{
...style, ...style,
...transitionStyles[state], ...transitionStyles[state],
inProp,
}} }}
> >
{children} {children}

View File

@ -11,10 +11,10 @@ interface Props {
} }
export class CopyToClipboard extends PureComponent<Props> { export class CopyToClipboard extends PureComponent<Props> {
clipboardjs: any; clipboardjs: ClipboardJS;
myRef: any; myRef: any;
constructor(props) { constructor(props: Props) {
super(props); super(props);
this.myRef = React.createRef(); this.myRef = React.createRef();
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import renderer from 'react-test-renderer'; import { shallow } from 'enzyme';
import EmptyListCTA from './EmptyListCTA'; import EmptyListCTA from './EmptyListCTA';
const model = { const model = {
@ -16,7 +16,7 @@ const model = {
describe('EmptyListCTA', () => { describe('EmptyListCTA', () => {
it('renders correctly', () => { it('renders correctly', () => {
const tree = renderer.create(<EmptyListCTA model={model} />).toJSON(); const tree = shallow(<EmptyListCTA model={model} />);
expect(tree).toMatchSnapshot(); expect(tree).toMatchSnapshot();
}); });
}); });

View File

@ -1,5 +1,4 @@
import React, { PureComponent, createRef } from 'react'; import React, { PureComponent, createRef } from 'react';
// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
interface Props { interface Props {

View File

@ -1,7 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import coreModule from '../../core_module'; import coreModule from '../../core_module';
import { ISCEService, IQService } from 'angular';
function typeaheadMatcher(this: any, item) { function typeaheadMatcher(this: any, item: string) {
let str = this.query; let str = this.query;
if (str === '') { if (str === '') {
return true; return true;
@ -16,8 +17,8 @@ function typeaheadMatcher(this: any, item) {
} }
export class FormDropdownCtrl { export class FormDropdownCtrl {
inputElement: any; inputElement: JQLite;
linkElement: any; linkElement: JQLite;
model: any; model: any;
display: any; display: any;
text: any; text: any;
@ -37,7 +38,13 @@ export class FormDropdownCtrl {
debounce: number; debounce: number;
/** @ngInject */ /** @ngInject */
constructor(private $scope, $element, private $sce, private templateSrv, private $q) { constructor(
private $scope: any,
$element: JQLite,
private $sce: ISCEService,
private templateSrv: any,
private $q: IQService
) {
this.inputElement = $element.find('input').first(); this.inputElement = $element.find('input').first();
this.linkElement = $element.find('a').first(); this.linkElement = $element.find('a').first();
this.linkMode = true; this.linkMode = true;
@ -99,7 +106,7 @@ export class FormDropdownCtrl {
} }
} }
getOptionsInternal(query) { getOptionsInternal(query: string) {
const result = this.getOptions({ $query: query }); const result = this.getOptions({ $query: query });
if (this.isPromiseLike(result)) { if (this.isPromiseLike(result)) {
return result; return result;
@ -107,7 +114,7 @@ export class FormDropdownCtrl {
return this.$q.when(result); return this.$q.when(result);
} }
isPromiseLike(obj) { isPromiseLike(obj: any) {
return obj && typeof obj.then === 'function'; return obj && typeof obj.then === 'function';
} }
@ -117,7 +124,7 @@ export class FormDropdownCtrl {
} else { } else {
// if we have text use it // if we have text use it
if (this.lookupText) { if (this.lookupText) {
this.getOptionsInternal('').then(options => { this.getOptionsInternal('').then((options: any) => {
const item = _.find(options, { value: this.model }); const item = _.find(options, { value: this.model });
this.updateDisplay(item ? item.text : this.model); this.updateDisplay(item ? item.text : this.model);
}); });
@ -127,12 +134,12 @@ export class FormDropdownCtrl {
} }
} }
typeaheadSource(query, callback) { typeaheadSource(query: string, callback: (res: any) => void) {
this.getOptionsInternal(query).then(options => { this.getOptionsInternal(query).then((options: any) => {
this.optionCache = options; this.optionCache = options;
// extract texts // extract texts
const optionTexts = _.map(options, op => { const optionTexts = _.map(options, (op: any) => {
return _.escape(op.text); return _.escape(op.text);
}); });
@ -147,7 +154,7 @@ export class FormDropdownCtrl {
}); });
} }
typeaheadUpdater(text) { typeaheadUpdater(text: string) {
if (text === this.text) { if (text === this.text) {
clearTimeout(this.cancelBlur); clearTimeout(this.cancelBlur);
this.inputElement.focus(); this.inputElement.focus();
@ -159,7 +166,7 @@ export class FormDropdownCtrl {
return text; return text;
} }
switchToLink(fromClick) { switchToLink(fromClick: boolean) {
if (this.linkMode && !fromClick) { if (this.linkMode && !fromClick) {
return; return;
} }
@ -178,7 +185,7 @@ export class FormDropdownCtrl {
this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200); this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
} }
updateValue(text) { updateValue(text: string) {
text = _.unescape(text); text = _.unescape(text);
if (text === '' || this.text === text) { if (text === '' || this.text === text) {
@ -214,7 +221,7 @@ export class FormDropdownCtrl {
}); });
} }
updateDisplay(text) { updateDisplay(text: string) {
this.text = text; this.text = text;
this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text)); this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
} }

View File

@ -232,7 +232,7 @@ export class JsonExplorer {
// some pretty handling of number arrays // some pretty handling of number arrays
if (this.isNumberArray()) { if (this.isNumberArray()) {
this.json.forEach((val, index) => { this.json.forEach((val: any, index: number) => {
if (index > 0) { if (index > 0) {
arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ',')); arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
} }

View File

@ -16,7 +16,7 @@ export class LayoutSelectorCtrl {
mode: string; mode: string;
/** @ngInject */ /** @ngInject */
constructor(private $rootScope) { constructor(private $rootScope: any) {
this.mode = store.get('grafana.list.layout.mode') || 'grid'; this.mode = store.get('grafana.list.layout.mode') || 'grid';
} }
@ -46,18 +46,18 @@ export function layoutSelector() {
} }
/** @ngInject */ /** @ngInject */
export function layoutMode($rootScope) { export function layoutMode($rootScope: any) {
return { return {
restrict: 'A', restrict: 'A',
scope: {}, scope: {},
link: (scope, elem) => { link: (scope: any, elem: any) => {
const layout = store.get('grafana.list.layout.mode') || 'grid'; const layout = store.get('grafana.list.layout.mode') || 'grid';
let className = 'card-list-layout-' + layout; let className = 'card-list-layout-' + layout;
elem.addClass(className); elem.addClass(className);
$rootScope.onAppEvent( $rootScope.onAppEvent(
'layout-mode-changed', 'layout-mode-changed',
(evt, newLayout) => { (evt: any, newLayout: any) => {
elem.removeClass(className); elem.removeClass(className);
className = 'card-list-layout-' + newLayout; className = 'card-list-layout-' + newLayout;
elem.addClass(className); elem.addClass(className);

View File

@ -9,6 +9,7 @@
// - reactComponent (generic directive for delegating off to React Components) // - reactComponent (generic directive for delegating off to React Components)
// - reactDirective (factory for creating specific directives that correspond to reactComponent directives) // - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
import { kebabCase } from 'lodash';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import angular from 'angular'; import angular from 'angular';
@ -155,11 +156,17 @@ function getPropExpression(prop) {
return Array.isArray(prop) ? prop[0] : prop; return Array.isArray(prop) ? prop[0] : prop;
} }
// find the normalized attribute knowing that React props accept any type of capitalization /**
function findAttribute(attrs, propName) { * Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
const index = Object.keys(attrs).filter(attr => { * kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
return attr.toLowerCase() === propName.toLowerCase(); * evaluated by the browser as such.
})[0]; * @param attrs All attributes of the component.
* @param propName Name of the prop that react component expects.
*/
function findAttribute(attrs: string, propName: string): string {
const index = Object.keys(attrs).find(attr => {
return attr.toLowerCase() === propName.toLowerCase() || attr.toLowerCase() === kebabCase(propName);
});
return attrs[index]; return attrs[index];
} }
@ -274,7 +281,9 @@ const reactDirective = $injector => {
// watch each property name and trigger an update whenever something changes, // watch each property name and trigger an update whenever something changes,
// to update scope.props with new values // to update scope.props with new values
const propExpressions = props.map(prop => { const propExpressions = props.map(prop => {
return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop]; return Array.isArray(prop)
? [findAttribute(attrs, prop[0]), getPropConfig(prop)]
: findAttribute(attrs, prop);
}); });
// If we don't have any props, then our watch statement won't fire. // If we don't have any props, then our watch statement won't fire.

View File

@ -1,17 +1,18 @@
import _ from 'lodash'; import _ from 'lodash';
import { Column, TableData } from '@grafana/ui';
interface Column { /**
text: string; * Extends the standard Column class with variables that get
* mutated in the angular table panel.
*/
interface MutableColumn extends Column {
title?: string; title?: string;
type?: string;
sort?: boolean; sort?: boolean;
desc?: boolean; desc?: boolean;
filterable?: boolean;
unit?: string;
} }
export default class TableModel { export default class TableModel implements TableData {
columns: Column[]; columns: MutableColumn[];
rows: any[]; rows: any[];
type: string; type: string;
columnMap: any; columnMap: any;

View File

@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors'; import { getApiKeys, getApiKeysCount } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import SlideDown from 'app/core/components/Animations/SlideDown'; import { SlideDown } from 'app/core/components/Animations/SlideDown';
import ApiKeysAddedModal from './ApiKeysAddedModal'; import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config'; import config from 'app/core/config';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import SlideDown from 'app/core/components/Animations/SlideDown'; import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { StoreState, FolderInfo } from 'app/types'; import { StoreState, FolderInfo } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
import { import {

View File

@ -11,16 +11,15 @@ import {
DataQueryResponse, DataQueryResponse,
DataQueryError, DataQueryError,
LoadingState, LoadingState,
PanelData,
TableData, TableData,
TimeRange, TimeRange,
TimeSeries,
ScopedVars, ScopedVars,
toTableData,
} from '@grafana/ui'; } from '@grafana/ui';
interface RenderProps { interface RenderProps {
loading: LoadingState; loading: LoadingState;
panelData: PanelData; data: TableData[];
} }
export interface Props { export interface Props {
@ -44,7 +43,7 @@ export interface State {
isFirstLoad: boolean; isFirstLoad: boolean;
loading: LoadingState; loading: LoadingState;
response: DataQueryResponse; response: DataQueryResponse;
panelData: PanelData; data?: TableData[];
} }
export class DataPanel extends Component<Props, State> { export class DataPanel extends Component<Props, State> {
@ -64,7 +63,6 @@ export class DataPanel extends Component<Props, State> {
response: { response: {
data: [], data: [],
}, },
panelData: {},
isFirstLoad: true, isFirstLoad: true,
}; };
} }
@ -149,7 +147,7 @@ export class DataPanel extends Component<Props, State> {
this.setState({ this.setState({
loading: LoadingState.Done, loading: LoadingState.Done,
response: resp, response: resp,
panelData: this.getPanelData(resp), data: toTableData(resp.data),
isFirstLoad: false, isFirstLoad: false,
}); });
} catch (err) { } catch (err) {
@ -172,23 +170,9 @@ export class DataPanel extends Component<Props, State> {
} }
}; };
getPanelData(response: DataQueryResponse) {
if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') {
return {
tableData: response.data[0] as TableData,
timeSeries: null,
};
}
return {
timeSeries: response.data as TimeSeries[],
tableData: null,
};
}
render() { render() {
const { queries } = this.props; const { queries } = this.props;
const { loading, isFirstLoad, panelData } = this.state; const { loading, isFirstLoad, data } = this.state;
// do not render component until we have first data // do not render component until we have first data
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
@ -206,7 +190,7 @@ export class DataPanel extends Component<Props, State> {
return ( return (
<> <>
{loading === LoadingState.Loading && this.renderLoadingState()} {loading === LoadingState.Loading && this.renderLoadingState()}
{this.props.children({ loading, panelData })} {this.props.children({ loading, data })}
</> </>
); );
} }

View File

@ -11,7 +11,7 @@ import { DataPanel } from './DataPanel';
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'; import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
// Utils // Utils
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel'; import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
import { profiler } from 'app/core/profiler'; import { profiler } from 'app/core/profiler';
import config from 'app/core/config'; import config from 'app/core/config';
@ -19,7 +19,7 @@ import config from 'app/core/config';
// Types // Types
import { DashboardModel, PanelModel } from '../state'; import { DashboardModel, PanelModel } from '../state';
import { PanelPlugin } from 'app/types'; import { PanelPlugin } from 'app/types';
import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui'; import { DataQueryResponse, TimeRange, LoadingState, TableData, DataQueryError, toTableData } from '@grafana/ui';
import { ScopedVars } from '@grafana/ui'; import { ScopedVars } from '@grafana/ui';
import templateSrv from 'app/features/templating/template_srv'; import templateSrv from 'app/features/templating/template_srv';
@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent<Props, State> {
} }
get getDataForPanel() { get getDataForPanel() {
return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null; return this.hasPanelSnapshot ? toTableData(this.props.panel.snapshotData) : null;
} }
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { renderPanelPlugin(loading: LoadingState, data: TableData[], width: number, height: number): JSX.Element {
const { panel, plugin } = this.props; const { panel, plugin } = this.props;
const { timeRange, renderCounter } = this.state; const { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.reactPanel.panel; const PanelComponent = plugin.exports.reactPanel.panel;
@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent<Props, State> {
<div className="panel-content"> <div className="panel-content">
<PanelComponent <PanelComponent
loading={loading} loading={loading}
panelData={panelData} data={data}
timeRange={timeRange} timeRange={timeRange}
options={panel.getOptions(plugin.exports.reactPanel.defaults)} options={panel.getOptions(plugin.exports.reactPanel.defaults)}
width={width - 2 * config.theme.panelPadding.horizontal} width={width - 2 * config.theme.panelPadding.horizontal}
@ -188,8 +188,8 @@ export class PanelChrome extends PureComponent<Props, State> {
onDataResponse={this.onDataResponse} onDataResponse={this.onDataResponse}
onError={this.onDataError} onError={this.onDataError}
> >
{({ loading, panelData }) => { {({ loading, data }) => {
return this.renderPanelPlugin(loading, panelData, width, height); return this.renderPanelPlugin(loading, data, width, height);
}} }}
</DataPanel> </DataPanel>
) : ( ) : (

View File

@ -111,12 +111,12 @@ export class PanelModel {
cachedPluginOptions?: any; cachedPluginOptions?: any;
legend?: { show: boolean }; legend?: { show: boolean };
constructor(model) { constructor(model: any) {
this.events = new Emitter(); this.events = new Emitter();
// copy properties from persisted model // copy properties from persisted model
for (const property in model) { for (const property in model) {
this[property] = model[property]; (this as any)[property] = model[property];
} }
// defaults // defaults
@ -150,7 +150,7 @@ export class PanelModel {
} }
} }
getOptions(panelDefaults) { getOptions(panelDefaults: any) {
return _.defaultsDeep(this.options || {}, panelDefaults); return _.defaultsDeep(this.options || {}, panelDefaults);
} }
@ -227,7 +227,7 @@ export class PanelModel {
} }
return { return {
...acc, ...acc,
[property]: this[property], [property]: (this as any)[property],
}; };
}, {}); }, {});
} }
@ -236,7 +236,7 @@ export class PanelModel {
const prevOptions = this.cachedPluginOptions[pluginId] || {}; const prevOptions = this.cachedPluginOptions[pluginId] || {};
Object.keys(prevOptions).map(property => { Object.keys(prevOptions).map(property => {
this[property] = prevOptions[property]; (this as any)[property] = prevOptions[property];
}); });
} }
@ -252,7 +252,7 @@ export class PanelModel {
continue; continue;
} }
delete this[key]; delete (this as any)[key];
} }
this.cachedPluginOptions[oldPluginId] = oldOptions; this.cachedPluginOptions[oldPluginId] = oldOptions;

View File

@ -4,8 +4,7 @@ import store from 'app/core/store';
// Models // Models
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { PanelData, TimeRange, TimeSeries } from '@grafana/ui'; import { TimeRange } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
// Utils // Utils
import { isString as _isString } from 'lodash'; import { isString as _isString } from 'lodash';
@ -170,19 +169,3 @@ export function getResolution(panel: PanelModel): number {
return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24)); return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24));
} }
const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints');
const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns');
export const snapshotDataToPanelData = (panel: PanelModel): PanelData => {
const snapshotData = panel.snapshotData;
if (isTimeSeries(snapshotData[0])) {
return {
timeSeries: snapshotData,
} as PanelData;
} else if (isTableData(snapshotData[0])) {
return {
tableData: snapshotData[0],
} as PanelData;
}
throw new Error('snapshotData is invalid:' + snapshotData.toString());
};

View File

@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Page from 'app/core/components/Page/Page'; import Page from 'app/core/components/Page/Page';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import SlideDown from 'app/core/components/Animations/SlideDown'; import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { getNavModel } from 'app/core/selectors/navModel'; import { getNavModel } from 'app/core/selectors/navModel';
import { NavModel, StoreState, FolderState } from 'app/types'; import { NavModel, StoreState, FolderState } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';

View File

@ -41,7 +41,9 @@ export class PlaylistSrv {
const dash = this.dashboards[this.index]; const dash = this.dashboards[this.index];
const queryParams = this.$location.search(); const queryParams = this.$location.search();
const filteredParams = _.pickBy(queryParams, value => value !== null); const filteredParams = _.pickBy(queryParams, key => {
return key === 'kiosk' || key === 'autofitpanels' || key === 'orgId';
});
const nextDashboardUrl = locationUtil.stripBaseFromUrl(dash.url); const nextDashboardUrl = locationUtil.stripBaseFromUrl(dash.url);
// this is done inside timeout to make sure digest happens after // this is done inside timeout to make sure digest happens after

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown'; import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
import { TeamGroup } from '../../types'; import { TeamGroup } from '../../types';
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import SlideDown from 'app/core/components/Animations/SlideDown'; import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { UserPicker } from 'app/core/components/Select/UserPicker'; import { UserPicker } from 'app/core/components/Select/UserPicker';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User } from 'app/types'; import { TeamMember, User } from 'app/types';

View File

@ -1,3 +1,5 @@
import { SyntheticEvent } from 'react';
export class MssqlConfigCtrl { export class MssqlConfigCtrl {
static templateUrl = 'partials/config.html'; static templateUrl = 'partials/config.html';
@ -7,4 +9,16 @@ export class MssqlConfigCtrl {
constructor($scope) { constructor($scope) {
this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false'; this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false';
} }
onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
event.preventDefault();
this.current.secureJsonFields.password = false;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = '';
};
onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = event.currentTarget.value;
};
} }

View File

@ -17,15 +17,15 @@
<span class="gf-form-label width-7">User</span> <span class="gf-form-label width-7">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input> <input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div> </div>
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password"> <div class="gf-form">
<span class="gf-form-label width-7">Password</span> <secret-form-field
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input> isConfigured="ctrl.current.secureJsonFields.password"
</div> value="ctrl.current.secureJsonData.password"
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password"> on-reset="ctrl.onPasswordReset"
<span class="gf-form-label width-7">Password</span> on-change="ctrl.onPasswordChange"
<input type="text" class="gf-form-input" disabled="disabled" value="configured"> inputWidth="9"
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a> />
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">

View File

@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { SyntheticEvent } from 'react';
export class PostgresConfigCtrl { export class PostgresConfigCtrl {
static templateUrl = 'partials/config.html'; static templateUrl = 'partials/config.html';
@ -52,6 +53,18 @@ export class PostgresConfigCtrl {
this.showTimescaleDBHelp = !this.showTimescaleDBHelp; this.showTimescaleDBHelp = !this.showTimescaleDBHelp;
} }
onPasswordReset = (event: SyntheticEvent<HTMLInputElement>) => {
event.preventDefault();
this.current.secureJsonFields.password = false;
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = '';
};
onPasswordChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.current.secureJsonData = this.current.secureJsonData || {};
this.current.secureJsonData.password = event.currentTarget.value;
};
// the value portion is derived from postgres server_version_num/100 // the value portion is derived from postgres server_version_num/100
postgresVersions = [ postgresVersions = [
{ name: '9.3', value: 903 }, { name: '9.3', value: 903 },

View File

@ -17,16 +17,17 @@
<span class="gf-form-label width-7">User</span> <span class="gf-form-label width-7">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input> <input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div> </div>
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password"> <div class="gf-form">
<span class="gf-form-label width-7">Password</span> <secret-form-field
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input> isConfigured="ctrl.current.secureJsonFields.password"
</div> value="ctrl.current.secureJsonData.password"
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password"> on-reset="ctrl.onPasswordReset"
<span class="gf-form-label width-7">Password</span> on-change="ctrl.onPasswordChange"
<input type="text" class="gf-form-input" disabled="disabled" value="configured"> inputWidth="9"
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a> />
</div> </div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-7">SSL Mode</label> <label class="gf-form-label width-7">SSL Mode</label>
<div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon"> <div class="gf-form-select-wrapper max-width-15 gf-form-select-wrapper--has-help-icon">

View File

@ -351,7 +351,7 @@ const timeSrv = {
}; };
describe('PrometheusDatasource', () => { describe('PrometheusDatasource', () => {
describe('When querying prometheus with one target using query editor target spec', async () => { describe('When querying prometheus with one target using query editor target spec', () => {
let results; let results;
const query = { const query = {
range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) }, range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },

View File

@ -32,14 +32,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
}; };
render() { render() {
const { height, width, options, panelData, renderCounter } = this.props; const { height, width, options, data, renderCounter } = this.props;
return ( return (
<ProcessedValuesRepeater <ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues} getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue} renderValue={this.renderValue}
width={width} width={width}
height={height} height={height}
source={panelData} source={data}
renderCounter={renderCounter} renderCounter={renderCounter}
orientation={options.orientation} orientation={options.orientation}
/> />

View File

@ -37,14 +37,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
}; };
render() { render() {
const { height, width, options, panelData, renderCounter } = this.props; const { height, width, options, data, renderCounter } = this.props;
return ( return (
<ProcessedValuesRepeater <ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues} getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue} renderValue={this.renderValue}
width={width} width={width}
height={height} height={height}
source={panelData} source={data}
renderCounter={renderCounter} renderCounter={renderCounter}
orientation={options.orientation} orientation={options.orientation}
/> />

View File

@ -16,13 +16,13 @@ interface Props extends PanelProps<Options> {}
export class GraphPanel extends PureComponent<Props> { export class GraphPanel extends PureComponent<Props> {
render() { render() {
const { panelData, timeRange, width, height } = this.props; const { data, timeRange, width, height } = this.props;
const { showLines, showBars, showPoints } = this.props.options; const { showLines, showBars, showPoints } = this.props.options;
let vmSeries: TimeSeriesVMs; let vmSeries: TimeSeriesVMs;
if (panelData.timeSeries) { if (data) {
vmSeries = processTimeSeries({ vmSeries = processTimeSeries({
timeSeries: panelData.timeSeries, data,
nullValueMode: NullValueMode.Ignore, nullValueMode: NullValueMode.Ignore,
}); });
} }

View File

@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config'; import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk'; import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui'; import { GrafanaThemeType, getValueFormat, getColorFromHexRgbOrName, isTableData } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl { class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
scopedVars: _.extend({}, this.panel.scopedVars), scopedVars: _.extend({}, this.panel.scopedVars),
}; };
if (dataList.length > 0 && dataList[0].type === 'table') { if (dataList.length > 0 && isTableData(dataList[0])) {
this.dataType = 'table'; this.dataType = 'table';
const tableData = dataList.map(this.tableHandler.bind(this)); const tableData = dataList.map(this.tableHandler.bind(this));
this.setTableValues(tableData, data); this.setTableValues(tableData, data);

View File

@ -4,28 +4,33 @@ import React, { PureComponent, CSSProperties } from 'react';
// Types // Types
import { SingleStatOptions, SingleStatBaseOptions } from './types'; import { SingleStatOptions, SingleStatBaseOptions } from './types';
import { processSingleStatPanelData, DisplayValue, PanelProps } from '@grafana/ui'; import { DisplayValue, PanelProps, processTimeSeries, NullValueMode } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { getDisplayProcessor } from '@grafana/ui'; import { getDisplayProcessor } from '@grafana/ui';
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater'; import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => { export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
const { panelData, replaceVariables, options } = props; const { data, replaceVariables, options } = props;
const { valueOptions, valueMappings } = options; const { valueOptions, valueMappings } = options;
const { unit, decimals, stat } = valueOptions;
const processor = getDisplayProcessor({ const processor = getDisplayProcessor({
unit: valueOptions.unit, unit,
decimals: valueOptions.decimals, decimals,
mappings: valueMappings, mappings: valueMappings,
thresholds: options.thresholds, thresholds: options.thresholds,
prefix: replaceVariables(valueOptions.prefix), prefix: replaceVariables(valueOptions.prefix),
suffix: replaceVariables(valueOptions.suffix), suffix: replaceVariables(valueOptions.suffix),
theme: config.theme, theme: config.theme,
}); });
return processSingleStatPanelData({
panelData: panelData, return processTimeSeries({
stat: valueOptions.stat, data,
}).map(stat => processor(stat.value)); nullValueMode: NullValueMode.Null,
}).map((series, index) => {
const value = stat !== 'name' ? series.stats[stat] : series.label;
return processor(value);
});
}; };
export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> { export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>> {
@ -50,14 +55,14 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
}; };
render() { render() {
const { height, width, options, panelData, renderCounter } = this.props; const { height, width, options, data, renderCounter } = this.props;
return ( return (
<ProcessedValuesRepeater <ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues} getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue} renderValue={this.renderValue}
width={width} width={width}
height={height} height={height}
source={panelData} source={data}
renderCounter={renderCounter} renderCounter={renderCounter}
orientation={options.orientation} orientation={options.orientation}
/> />

View File

@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers';
import { tablePanelEditor } from './editor'; import { tablePanelEditor } from './editor';
import { columnOptionsTab } from './column_options'; import { columnOptionsTab } from './column_options';
import { TableRenderer } from './renderer'; import { TableRenderer } from './renderer';
import { isTableData } from '@grafana/ui';
class TablePanelCtrl extends MetricsPanelCtrl { class TablePanelCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html'; static templateUrl = 'module.html';
@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
// automatically correct transform mode based on data // automatically correct transform mode based on data
if (this.dataRaw && this.dataRaw.length) { if (this.dataRaw && this.dataRaw.length) {
if (this.dataRaw[0].type === 'table') { if (isTableData(this.dataRaw[0])) {
this.panel.transform = 'table'; this.panel.transform = 'table';
} else { } else {
if (this.dataRaw[0].type === 'docs') { if (this.dataRaw[0].type === 'docs') {

View File

@ -14,15 +14,15 @@ export class TablePanel extends Component<Props> {
} }
render() { render() {
const { panelData, options } = this.props; const { data, options } = this.props;
if (!panelData || !panelData.tableData) { if (data.length < 1) {
return <div>No Table Data...</div>; return <div>No Table Data...</div>;
} }
return ( return (
<ThemeContext.Consumer> <ThemeContext.Consumer>
{theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />} {theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
</ThemeContext.Consumer> </ThemeContext.Consumer>
); );
} }

View File

@ -12,8 +12,7 @@ module.exports = function(options) {
options: { options: {
importLoaders: 2, importLoaders: 2,
url: options.preserveUrl, url: options.preserveUrl,
sourceMap: options.sourceMap, sourceMap: options.sourceMap
minimize: options.minimize,
}, },
}, },
{ {

View File

@ -19,13 +19,6 @@ module.exports = merge(common, {
light: './public/sass/grafana.light.scss', light: './public/sass/grafana.light.scss',
}, },
output: {
path: path.resolve(__dirname, '../../public/build'),
filename: '[name].[hash].js',
// Keep publicPath relative for host.com/grafana/ deployments
publicPath: "public/build/",
},
module: { module: {
rules: [ rules: [
{ {
@ -50,7 +43,7 @@ module.exports = merge(common, {
}, },
}, },
}, },
require('./sass.rule.js')({ sourceMap: false, minimize: false, preserveUrl: false }), require('./sass.rule.js')({ sourceMap: false, preserveUrl: false }),
{ {
test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, test: /\.(png|jpg|gif|ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/,
loader: 'file-loader' loader: 'file-loader'
@ -59,7 +52,7 @@ module.exports = merge(common, {
}, },
plugins: [ plugins: [
new CleanWebpackPlugin('../../public/build', { allowExternal: true }), new CleanWebpackPlugin(),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "grafana.[name].[hash].css" filename: "grafana.[name].[hash].css"
}), }),

View File

@ -56,7 +56,7 @@ module.exports = merge(common, {
plugins: [ plugins: [
[require('@rtsao/plugin-proposal-class-properties'), { loose: true }], [require('@rtsao/plugin-proposal-class-properties'), { loose: true }],
'angularjs-annotate', 'angularjs-annotate',
'syntax-dynamic-import', // needed for `() => import()` in routes.ts '@babel/plugin-syntax-dynamic-import', // needed for `() => import()` in routes.ts
'react-hot-loader/babel', 'react-hot-loader/babel',
], ],
presets: [ presets: [
@ -98,7 +98,7 @@ module.exports = merge(common, {
}, },
plugins: [ plugins: [
new CleanWebpackPlugin('../public/build', { allowExternal: true }), new CleanWebpackPlugin(),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'), filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index-template.html'), template: path.resolve(__dirname, '../../public/views/index-template.html'),

View File

@ -1,7 +1,7 @@
'use strict'; 'use strict';
const merge = require('webpack-merge'); const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin');
const common = require('./webpack.common.js'); const common = require('./webpack.common.js');
const path = require('path'); const path = require('path');
const ngAnnotatePlugin = require('ng-annotate-webpack-plugin'); const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
@ -43,14 +43,14 @@ module.exports = merge(common, {
}, },
}, },
require('./sass.rule.js')({ require('./sass.rule.js')({
sourceMap: false, minimize: false, preserveUrl: false sourceMap: false, preserveUrl: false
}) })
] ]
}, },
optimization: { optimization: {
minimizer: [ minimizer: [
new UglifyJsPlugin({ new TerserPlugin({
cache: true, cache: false,
parallel: true, parallel: true,
sourceMap: true sourceMap: true
}), }),

View File

@ -27,7 +27,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"baseUrl": "public", "baseUrl": "public",
"pretty": true, "pretty": true,
"typeRoots": ["node_modules/@types", "types"], "typeRoots": ["node_modules/@types", "public/app/types"],
"paths": { "paths": {
"app": ["app"], "app": ["app"],
"sass": ["sass"] "sass": ["sass"]

5070
yarn.lock

File diff suppressed because it is too large Load Diff