Commit 2c8e1cbd by ryan

merge with master

parents 2f8dd898 30f0c350
......@@ -11,11 +11,11 @@
"url": "http://github.com/grafana/grafana.git"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/core": "^7.3.4",
"@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-typescript": "^7.1.0",
"@babel/preset-typescript": "^7.3.3",
"@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
"@types/angular": "^1.6.6",
"@types/chalk": "^2.2.0",
......@@ -24,26 +24,28 @@
"@types/d3": "^4.10.1",
"@types/enzyme": "^3.1.13",
"@types/inquirer": "^0.0.43",
"@types/jest": "^23.3.2",
"@types/jest": "^24.0.11",
"@types/jquery": "^1.10.35",
"@types/node": "^8.0.31",
"@types/papaparse": "^4.5.9",
"@types/react": "^16.8.8",
"@types/react-dom": "^16.8.2",
"@types/react-grid-layout": "^0.16.6",
"@types/react-select": "^2.0.4",
"@types/react-transition-group": "^2.0.15",
"@types/react-virtualized": "^9.18.12",
"@types/clipboard": "^2.0.1",
"angular-mocks": "1.6.6",
"autoprefixer": "^6.4.0",
"axios": "^0.17.1",
"autoprefixer": "^9.4.10",
"axios": "^0.18.0",
"babel-core": "^7.0.0-bridge",
"babel-jest": "^23.6.0",
"babel-jest": "^24.5.0",
"babel-loader": "^8.0.4",
"babel-plugin-angularjs-annotate": "^0.9.0",
"babel-plugin-angularjs-annotate": "^0.10.0",
"chalk": "^2.4.2",
"clean-webpack-plugin": "^0.1.19",
"clean-webpack-plugin": "^2.0.0",
"concurrently": "^4.1.0",
"css-loader": "^0.28.7",
"css-loader": "^2.1.1",
"enzyme": "^3.6.0",
"enzyme-adapter-react-16": "^1.5.0",
"enzyme-to-json": "^3.3.4",
......@@ -51,11 +53,11 @@
"es6-shim": "^0.35.3",
"execa": "^1.0.0",
"expect.js": "~0.2.0",
"expose-loader": "^0.7.3",
"file-loader": "^1.1.11",
"fork-ts-checker-webpack-plugin": "^0.4.9",
"expose-loader": "0.7.5",
"file-loader": "^3.0.1",
"fork-ts-checker-webpack-plugin": "^1.0.0",
"gaze": "^1.1.2",
"glob": "~7.0.0",
"glob": "~7.1.3",
"grunt": "1.0.1",
"grunt-angular-templates": "^1.1.0",
"grunt-cli": "~1.2.0",
......@@ -69,29 +71,29 @@
"grunt-sass-lint": "^0.2.4",
"grunt-usemin": "3.1.1",
"grunt-webpack": "^3.0.2",
"html-loader": "^0.5.1",
"html-webpack-harddisk-plugin": "^0.2.0",
"html-loader": "0.5.5",
"html-webpack-harddisk-plugin": "^1.0.1",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.3.1",
"inquirer": "^6.2.2",
"jest": "^23.6.0",
"jest": "^24.5.0",
"jest-date-mock": "^1.0.6",
"lint-staged": "^8.1.3",
"load-grunt-tasks": "3.5.2",
"mini-css-extract-plugin": "^0.4.0",
"mini-css-extract-plugin": "^0.5.0",
"mocha": "^4.0.1",
"monaco-editor": "^0.15.6",
"ng-annotate-loader": "^0.6.1",
"ng-annotate-webpack-plugin": "^0.3.0",
"ngtemplate-loader": "^2.0.1",
"node-sass": "^4.11.0",
"npm": "^5.4.2",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"ora": "^3.1.0",
"npm": "^6.9.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"ora": "^3.2.0",
"phantomjs-prebuilt": "^2.1.15",
"postcss-browser-reporter": "^0.5.0",
"postcss-loader": "^2.0.6",
"postcss-reporter": "^5.0.0",
"postcss-loader": "^3.0.0",
"postcss-reporter": "^6.0.1",
"prettier": "1.16.4",
"react-hot-loader": "^4.3.6",
"react-test-renderer": "^16.5.0",
......@@ -99,27 +101,27 @@
"regexp-replace-loader": "^1.0.1",
"rimraf": "^2.6.3",
"sass-lint": "^1.10.2",
"sass-loader": "^7.0.1",
"sass-loader": "7.1.0",
"semver": "^5.6.0",
"sinon": "1.17.6",
"style-loader": "^0.21.0",
"style-loader": "0.23.1",
"systemjs": "0.20.19",
"systemjs-plugin-css": "^0.1.36",
"ts-jest": "^23.10.4",
"ts-loader": "^5.1.0",
"ts-node": "^8.0.2",
"tslib": "^1.9.3",
"tslint": "^5.8.0",
"terser-webpack-plugin": "^1.2.3",
"ts-jest": "^24.0.0",
"ts-loader": "5.3.3",
"ts-node": "8.0.2",
"tslib": "1.9.3",
"tslint": "5.14.0",
"tslint-loader": "^3.5.3",
"tslint-react": "^3.6.0",
"typescript": "^3.0.3",
"uglifyjs-webpack-plugin": "^1.2.7",
"webpack": "4.19.1",
"webpack-bundle-analyzer": "^2.9.0",
"typescript": "3.3.3333",
"webpack": "4.29.6",
"webpack-bundle-analyzer": "3.1.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^2.1.4",
"webpack-dev-server": "^3.1.0",
"webpack-merge": "^4.1.0",
"webpack-cli": "3.2.3",
"webpack-dev-server": "3.2.1",
"webpack-merge": "4.2.1",
"zone.js": "^0.7.2"
},
"scripts": {
......@@ -176,7 +178,7 @@
"baron": "^3.0.3",
"brace": "^0.10.0",
"classnames": "^2.2.6",
"clipboard": "^1.7.1",
"clipboard": "^2.0.4",
"d3": "^4.11.0",
"d3-scale-chromatic": "^1.3.0",
"eventemitter3": "^2.0.3",
......@@ -188,6 +190,7 @@
"mousetrap": "^1.6.0",
"mousetrap-global-bind": "^1.1.0",
"nodemon": "^1.18.10",
"papaparse": "^4.6.3",
"prismjs": "^1.6.0",
"prop-types": "^15.6.2",
"rc-cascader": "^0.14.0",
......
......@@ -45,7 +45,7 @@ $arrowSize: 15px;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(100% -$arrowSize);
left: calc(100%-#{$arrowSize});
}
&[data-placement^='right'] {
......
......@@ -2,7 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import { FormField, Props } from './FormField';
const setup = (propOverrides?: object) => {
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
label: 'Test',
labelWidth: 11,
......@@ -15,10 +15,23 @@ const setup = (propOverrides?: object) => {
return shallow(<FormField {...props} />);
};
describe('Render', () => {
it('should render component', () => {
describe('FormField', () => {
it('should render component with default inputEl', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
it('should render component with custom inputEl', () => {
const wrapper = setup({
inputEl: (
<>
<span>Input</span>
<button>Ok</button>
</>
),
});
expect(wrapper).toMatchSnapshot();
});
});
......@@ -5,6 +5,7 @@ export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
labelWidth?: number;
inputWidth?: number;
inputEl?: React.ReactNode;
}
const defaultProps = {
......@@ -12,14 +13,18 @@ const defaultProps = {
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 (
<div className="form-field">
<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>
);
};
FormField.displayName = 'FormField';
FormField.defaultProps = defaultProps;
export { FormField };
// 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
className="form-field"
>
......
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>
);
});
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';
......@@ -40,8 +40,6 @@ export function makeDummyTable(columnCount: number, rowCount: number): TableData
const suffix = (rowId + 1).toString();
return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
}),
type: 'table',
columnMap: {},
};
}
......
......@@ -8,6 +8,7 @@ import {
CellMeasurerCache,
CellMeasurer,
GridCellProps,
Index,
} from 'react-virtualized';
import { Themeable } from '../../types/theme';
......@@ -26,6 +27,7 @@ import { stringToJsRegex } from '../../utils/index';
export interface Props extends Themeable {
data: TableData;
minColumnWidth: number;
showHeader: boolean;
fixedHeader: boolean;
fixedColumns: number;
......@@ -46,6 +48,7 @@ interface State {
interface ColumnRenderInfo {
header: string;
width: number;
builder: TableCellBuilder;
}
......@@ -64,6 +67,7 @@ export class Table extends Component<Props, State> {
fixedHeader: true,
fixedColumns: 0,
rotate: false,
minColumnWidth: 150,
};
constructor(props: Props) {
......@@ -76,7 +80,7 @@ export class Table extends Component<Props, State> {
this.renderer = this.initColumns(props);
this.measurer = new CellMeasurerCache({
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 */
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) => {
let title = col.text;
......@@ -131,6 +136,7 @@ export class Table extends Component<Props, State> {
return {
header: title,
width: columnWidth,
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() {
const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
const { data } = this.state;
......@@ -269,7 +279,7 @@ export class Table extends Component<Props, State> {
rowCount={rowCount}
overscanColumnCount={8}
overscanRowCount={8}
columnWidth={this.measurer.columnWidth}
columnWidth={this.getColumnWidth}
deferredMeasurementCache={this.measurer}
cellRenderer={this.cellRenderer}
rowHeight={this.measurer.rowHeight}
......
......@@ -17,7 +17,7 @@ exports[`Render should render with base threshold 1`] = `
],
"results": Array [
Object {
"isThrow": false,
"type": "return",
"value": undefined,
},
],
......
......@@ -14,6 +14,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles';
// Forms
export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField';
export { SecretFormField } from './SecretFormFied/SecretFormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
......
......@@ -51,27 +51,13 @@ export enum NullValueMode {
export type TimeSeriesVMs = TimeSeriesVM[];
export interface Column {
text: string;
title?: string;
type?: string;
sort?: boolean;
desc?: boolean;
filterable?: boolean;
text: string; // The column name
type?: 'time' | 'number' | 'string' | 'object'; // not used anywhere? can we remove?
filterable?: boolean; // currently only set by elasticsearch, and used in the table panel
unit?: string;
}
export interface TableData {
columns: Column[];
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;
}
import { ComponentClass } from 'react';
import { TimeSeries, LoadingState, TableData } from './data';
import { LoadingState, TableData } from './data';
import { TimeRange } from './time';
import { ScopedVars } from './datasource';
export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
export interface PanelProps<T = any> {
panelData: PanelData;
data?: TableData[];
timeRange: TimeRange;
loading: LoadingState;
options: T;
......@@ -16,11 +16,6 @@ export interface PanelProps<T = any> {
replaceVariables: InterpolateFunction;
}
export interface PanelData {
timeSeries?: TimeSeries[];
tableData?: TableData;
}
export interface PanelEditorProps<T = any> {
options: T;
onOptionsChange: (options: T) => void;
......
......@@ -2,7 +2,6 @@
exports[`processTableData basic processing should generate a header and fix widths 1`] = `
Object {
"columnMap": Object {},
"columns": Array [
Object {
"text": "Column 1",
......@@ -31,13 +30,11 @@ Object {
null,
],
],
"type": "table",
}
`;
exports[`processTableData basic processing should read header and two rows 1`] = `
Object {
"columnMap": Object {},
"columns": Array [
Object {
"text": "a",
......@@ -61,6 +58,5 @@ Object {
6,
],
],
"type": "table",
}
`;
export * from './processTimeSeries';
export * from './singlestat';
export * from './processTableData';
export * from './valueFormats/valueFormats';
export * from './colors';
export * from './namedColorsPalette';
......
import { parseCSV } from './processTableData';
import { parseCSV, toTableData } from './processTableData';
describe('processTableData', () => {
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([]);
});
});
......@@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
import Papa, { ParseError, ParseMeta } from 'papaparse';
// Types
import { TableData, Column } from '../types';
import { TableData, Column, TimeSeries } from '../types';
// Subset of all parse options
export interface TableParseOptions {
......@@ -70,8 +70,6 @@ export function matchRowSizes(table: TableData): TableData {
return {
columns,
rows: fixedRows,
type: table.type,
columnMap: table.columnMap,
};
}
......@@ -118,8 +116,6 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return {
columns: [],
rows: [],
type: 'table',
columnMap: {},
};
}
......@@ -130,11 +126,48 @@ export function parseCSV(text: string, options?: TableParseOptions, details?: Ta
return matchRowSizes({
columns: makeColumns(header),
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 {
if (isNumber(sortIndex)) {
const copy = {
......
......@@ -4,24 +4,43 @@ import isNumber from 'lodash/isNumber';
import { colors } from './colors';
// Types
import { TimeSeries, TimeSeriesVMs, NullValueMode, TimeSeriesValue } from '../types';
import { getFlotPairs } from './flotPairs';
import { TimeSeriesVMs, NullValueMode, TimeSeriesValue, TableData } from '../types';
interface Options {
timeSeries: TimeSeries[];
data: TableData[];
xColumn?: number; // Time (or null to guess)
yColumn?: number; // Value (or null to guess)
nullValueMode: NullValueMode;
}
export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeSeriesVMs {
const vmSeries = timeSeries.map((item, index) => {
// NOTE: this should move to processTableData.ts
// 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 label = item.target;
const label = item.columns[yColumn].text;
// Use external calculator just to make sure it works :)
const result = getFlotPairs({
rows: item.datapoints,
xIndex: 1,
yIndex: 0,
rows: item.rows,
xIndex: xColumn,
yIndex: yColumn,
nullValueMode,
});
......@@ -50,9 +69,9 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
let previousValue = 0;
let previousDeltaUp = true;
for (let i = 0; i < item.datapoints.length; i++) {
currentValue = item.datapoints[i][0];
currentTime = item.datapoints[i][1];
for (let i = 0; i < item.rows.length; i++) {
currentValue = item.rows[i][yColumn];
currentTime = item.rows[i][xColumn];
if (typeof currentTime !== 'number') {
continue;
......@@ -103,7 +122,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
if (previousValue > currentValue) {
// counter reset
previousDeltaUp = false;
if (i === item.datapoints.length - 1) {
if (i === item.rows.length - 1) {
// reset on last
delta += currentValue;
}
......
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 [];
}
......@@ -2,7 +2,7 @@ import React from 'react';
interface StateHolderProps<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 }> {
......
......@@ -101,7 +101,7 @@ func (dc *databaseCache) Set(key string, value interface{}, expire time.Duration
// insert or update depending on if item already exist
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)
} else {
sql := `INSERT INTO cache_data (cache_key,data,created_at,expires) VALUES(?,?,?,?)`
......
......@@ -5,7 +5,6 @@ import (
"time"
"github.com/bmizerany/assert"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
......@@ -54,3 +53,20 @@ func TestDatabaseStorageGarbageCollection(t *testing.T) {
_, err = db.Get("key5")
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)
}
......@@ -36,6 +36,10 @@ func (r RoleType) Includes(other RoleType) bool {
return other != ROLE_ADMIN
}
if r == ROLE_VIEWER {
return other == ROLE_VIEWER
}
return false
}
......
......@@ -9,7 +9,7 @@ import { TagFilter } from './components/TagFilter/TagFilter';
import { SideMenu } from './components/sidemenu/SideMenu';
import { MetricSelect } from './components/Select/MetricSelect';
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';
export function registerAngularDirectives() {
......@@ -59,4 +59,11 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }],
['templateSrv', { watchDepth: 'reference' }],
]);
react2AngularDirective('secretFormField', SecretFormField, [
'value',
'isConfigured',
'inputWidth',
['onReset', { watchDepth: 'reference', wrapApply: true }],
['onChange', { watchDepth: 'reference', wrapApply: true }],
]);
}
import React, { FC } from 'react';
import React, { FC, CSSProperties } from 'react';
import Transition, { ExitHandler } from 'react-transition-group/Transition';
interface Props {
......@@ -10,12 +10,12 @@ interface Props {
}
export const FadeIn: FC<Props> = props => {
const defaultStyle = {
const defaultStyle: CSSProperties = {
transition: `opacity ${props.duration}ms linear`,
opacity: 0,
};
const transitionStyles = {
const transitionStyles: { [str: string]: CSSProperties } = {
exited: { opacity: 0, display: 'none' },
entering: { opacity: 0 },
entered: { opacity: 1 },
......
import React from 'react';
import React, { CSSProperties, FC } from 'react';
import Transition from 'react-transition-group/Transition';
interface Style {
......@@ -16,11 +16,18 @@ export const defaultStyle: Style = {
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:
// ENTERING, ENTERED, EXITING, EXITED
// https://reactcommunity.org/react-transition-group/
const transitionStyles = {
// https://reactcommunity.or[g/react-transition-group/
const transitionStyles: { [str: string]: CSSProperties } = {
exited: { maxHeight: 0 },
entering: { maxHeight: maxHeight },
entered: { maxHeight: 'unset', overflow: 'visible' },
......@@ -34,6 +41,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
style={{
...style,
...transitionStyles[state],
inProp,
}}
>
{children}
......
......@@ -11,10 +11,10 @@ interface Props {
}
export class CopyToClipboard extends PureComponent<Props> {
clipboardjs: any;
clipboardjs: ClipboardJS;
myRef: any;
constructor(props) {
constructor(props: Props) {
super(props);
this.myRef = React.createRef();
}
......
import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
import EmptyListCTA from './EmptyListCTA';
const model = {
......@@ -16,7 +16,7 @@ const model = {
describe('EmptyListCTA', () => {
it('renders correctly', () => {
const tree = renderer.create(<EmptyListCTA model={model} />).toJSON();
const tree = shallow(<EmptyListCTA model={model} />);
expect(tree).toMatchSnapshot();
});
});
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
interface Props {
......
import _ from 'lodash';
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;
if (str === '') {
return true;
......@@ -16,8 +17,8 @@ function typeaheadMatcher(this: any, item) {
}
export class FormDropdownCtrl {
inputElement: any;
linkElement: any;
inputElement: JQLite;
linkElement: JQLite;
model: any;
display: any;
text: any;
......@@ -37,7 +38,13 @@ export class FormDropdownCtrl {
debounce: number;
/** @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.linkElement = $element.find('a').first();
this.linkMode = true;
......@@ -99,7 +106,7 @@ export class FormDropdownCtrl {
}
}
getOptionsInternal(query) {
getOptionsInternal(query: string) {
const result = this.getOptions({ $query: query });
if (this.isPromiseLike(result)) {
return result;
......@@ -107,7 +114,7 @@ export class FormDropdownCtrl {
return this.$q.when(result);
}
isPromiseLike(obj) {
isPromiseLike(obj: any) {
return obj && typeof obj.then === 'function';
}
......@@ -117,7 +124,7 @@ export class FormDropdownCtrl {
} else {
// if we have text use it
if (this.lookupText) {
this.getOptionsInternal('').then(options => {
this.getOptionsInternal('').then((options: any) => {
const item = _.find(options, { value: this.model });
this.updateDisplay(item ? item.text : this.model);
});
......@@ -127,12 +134,12 @@ export class FormDropdownCtrl {
}
}
typeaheadSource(query, callback) {
this.getOptionsInternal(query).then(options => {
typeaheadSource(query: string, callback: (res: any) => void) {
this.getOptionsInternal(query).then((options: any) => {
this.optionCache = options;
// extract texts
const optionTexts = _.map(options, op => {
const optionTexts = _.map(options, (op: any) => {
return _.escape(op.text);
});
......@@ -147,7 +154,7 @@ export class FormDropdownCtrl {
});
}
typeaheadUpdater(text) {
typeaheadUpdater(text: string) {
if (text === this.text) {
clearTimeout(this.cancelBlur);
this.inputElement.focus();
......@@ -159,7 +166,7 @@ export class FormDropdownCtrl {
return text;
}
switchToLink(fromClick) {
switchToLink(fromClick: boolean) {
if (this.linkMode && !fromClick) {
return;
}
......@@ -178,7 +185,7 @@ export class FormDropdownCtrl {
this.cancelBlur = setTimeout(this.switchToLink.bind(this), 200);
}
updateValue(text) {
updateValue(text: string) {
text = _.unescape(text);
if (text === '' || this.text === text) {
......@@ -214,7 +221,7 @@ export class FormDropdownCtrl {
});
}
updateDisplay(text) {
updateDisplay(text: string) {
this.text = text;
this.display = this.$sce.trustAsHtml(this.templateSrv.highlightVariablesAsHtml(text));
}
......
......@@ -232,7 +232,7 @@ export class JsonExplorer {
// some pretty handling of number arrays
if (this.isNumberArray()) {
this.json.forEach((val, index) => {
this.json.forEach((val: any, index: number) => {
if (index > 0) {
arrayWrapperSpan.appendChild(createElement('span', 'array-comma', ','));
}
......
......@@ -16,7 +16,7 @@ export class LayoutSelectorCtrl {
mode: string;
/** @ngInject */
constructor(private $rootScope) {
constructor(private $rootScope: any) {
this.mode = store.get('grafana.list.layout.mode') || 'grid';
}
......@@ -46,18 +46,18 @@ export function layoutSelector() {
}
/** @ngInject */
export function layoutMode($rootScope) {
export function layoutMode($rootScope: any) {
return {
restrict: 'A',
scope: {},
link: (scope, elem) => {
link: (scope: any, elem: any) => {
const layout = store.get('grafana.list.layout.mode') || 'grid';
let className = 'card-list-layout-' + layout;
elem.addClass(className);
$rootScope.onAppEvent(
'layout-mode-changed',
(evt, newLayout) => {
(evt: any, newLayout: any) => {
elem.removeClass(className);
className = 'card-list-layout-' + newLayout;
elem.addClass(className);
......
......@@ -9,6 +9,7 @@
// - reactComponent (generic directive for delegating off to React Components)
// - reactDirective (factory for creating specific directives that correspond to reactComponent directives)
import { kebabCase } from 'lodash';
import React from 'react';
import ReactDOM from 'react-dom';
import angular from 'angular';
......@@ -155,11 +156,17 @@ function getPropExpression(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) {
const index = Object.keys(attrs).filter(attr => {
return attr.toLowerCase() === propName.toLowerCase();
})[0];
/**
* Finds the normalized attribute knowing that React props accept any type of capitalization and it also handles
* kabab case attributes which can be used in case the attribute would also be a standard html attribute and would be
* evaluated by the browser as such.
* @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];
}
......@@ -274,7 +281,9 @@ const reactDirective = $injector => {
// watch each property name and trigger an update whenever something changes,
// to update scope.props with new values
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.
......
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;
type?: string;
sort?: boolean;
desc?: boolean;
filterable?: boolean;
unit?: string;
}
export default class TableModel {
columns: Column[];
export default class TableModel implements TableData {
columns: MutableColumn[];
rows: any[];
type: string;
columnMap: any;
......
......@@ -7,7 +7,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
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 config from 'app/core/config';
import appEvents from 'app/core/app_events';
......
import React, { PureComponent } from 'react';
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 { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
import {
......
......@@ -11,16 +11,15 @@ import {
DataQueryResponse,
DataQueryError,
LoadingState,
PanelData,
TableData,
TimeRange,
TimeSeries,
ScopedVars,
toTableData,
} from '@grafana/ui';
interface RenderProps {
loading: LoadingState;
panelData: PanelData;
data: TableData[];
}
export interface Props {
......@@ -44,7 +43,7 @@ export interface State {
isFirstLoad: boolean;
loading: LoadingState;
response: DataQueryResponse;
panelData: PanelData;
data?: TableData[];
}
export class DataPanel extends Component<Props, State> {
......@@ -64,7 +63,6 @@ export class DataPanel extends Component<Props, State> {
response: {
data: [],
},
panelData: {},
isFirstLoad: true,
};
}
......@@ -149,7 +147,7 @@ export class DataPanel extends Component<Props, State> {
this.setState({
loading: LoadingState.Done,
response: resp,
panelData: this.getPanelData(resp),
data: toTableData(resp.data),
isFirstLoad: false,
});
} 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() {
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
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
......@@ -206,7 +190,7 @@ export class DataPanel extends Component<Props, State> {
return (
<>
{loading === LoadingState.Loading && this.renderLoadingState()}
{this.props.children({ loading, panelData })}
{this.props.children({ loading, data })}
</>
);
}
......
......@@ -11,7 +11,7 @@ import { DataPanel } from './DataPanel';
import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary';
// 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 { profiler } from 'app/core/profiler';
import config from 'app/core/config';
......@@ -19,7 +19,7 @@ import config from 'app/core/config';
// Types
import { DashboardModel, PanelModel } from '../state';
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 templateSrv from 'app/features/templating/template_srv';
......@@ -139,10 +139,10 @@ export class PanelChrome extends PureComponent<Props, State> {
}
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 { timeRange, renderCounter } = this.state;
const PanelComponent = plugin.exports.reactPanel.panel;
......@@ -157,7 +157,7 @@ export class PanelChrome extends PureComponent<Props, State> {
<div className="panel-content">
<PanelComponent
loading={loading}
panelData={panelData}
data={data}
timeRange={timeRange}
options={panel.getOptions(plugin.exports.reactPanel.defaults)}
width={width - 2 * config.theme.panelPadding.horizontal}
......@@ -188,8 +188,8 @@ export class PanelChrome extends PureComponent<Props, State> {
onDataResponse={this.onDataResponse}
onError={this.onDataError}
>
{({ loading, panelData }) => {
return this.renderPanelPlugin(loading, panelData, width, height);
{({ loading, data }) => {
return this.renderPanelPlugin(loading, data, width, height);
}}
</DataPanel>
) : (
......
......@@ -111,12 +111,12 @@ export class PanelModel {
cachedPluginOptions?: any;
legend?: { show: boolean };
constructor(model) {
constructor(model: any) {
this.events = new Emitter();
// copy properties from persisted model
for (const property in model) {
this[property] = model[property];
(this as any)[property] = model[property];
}
// defaults
......@@ -150,7 +150,7 @@ export class PanelModel {
}
}
getOptions(panelDefaults) {
getOptions(panelDefaults: any) {
return _.defaultsDeep(this.options || {}, panelDefaults);
}
......@@ -227,7 +227,7 @@ export class PanelModel {
}
return {
...acc,
[property]: this[property],
[property]: (this as any)[property],
};
}, {});
}
......@@ -236,7 +236,7 @@ export class PanelModel {
const prevOptions = this.cachedPluginOptions[pluginId] || {};
Object.keys(prevOptions).map(property => {
this[property] = prevOptions[property];
(this as any)[property] = prevOptions[property];
});
}
......@@ -252,7 +252,7 @@ export class PanelModel {
continue;
}
delete this[key];
delete (this as any)[key];
}
this.cachedPluginOptions[oldPluginId] = oldOptions;
......
......@@ -4,8 +4,7 @@ import store from 'app/core/store';
// Models
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { PanelData, TimeRange, TimeSeries } from '@grafana/ui';
import { TableData } from '@grafana/ui/src';
import { TimeRange } from '@grafana/ui';
// Utils
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));
}
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());
};
......@@ -3,7 +3,7 @@ import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import Page from 'app/core/components/Page/Page';
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 { NavModel, StoreState, FolderState } from 'app/types';
import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
......
......@@ -41,7 +41,9 @@ export class PlaylistSrv {
const dash = this.dashboards[this.index];
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);
// this is done inside timeout to make sure digest happens after
......
import React, { PureComponent } from 'react';
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 { TeamGroup } from '../../types';
import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions';
......
import React, { PureComponent } from 'react';
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 { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { TeamMember, User } from 'app/types';
......
import { SyntheticEvent } from 'react';
export class MssqlConfigCtrl {
static templateUrl = 'partials/config.html';
......@@ -7,4 +9,16 @@ export class MssqlConfigCtrl {
constructor($scope) {
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;
};
}
......@@ -17,15 +17,15 @@
<span class="gf-form-label width-7">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div>
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
</div>
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="text" class="gf-form-input" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
</div>
<div class="gf-form">
<secret-form-field
isConfigured="ctrl.current.secureJsonFields.password"
value="ctrl.current.secureJsonData.password"
on-reset="ctrl.onPasswordReset"
on-change="ctrl.onPasswordChange"
inputWidth="9"
/>
</div>
</div>
<div class="gf-form">
......
import _ from 'lodash';
import { SyntheticEvent } from 'react';
export class PostgresConfigCtrl {
static templateUrl = 'partials/config.html';
......@@ -52,6 +53,18 @@ export class PostgresConfigCtrl {
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
postgresVersions = [
{ name: '9.3', value: 903 },
......
......@@ -17,16 +17,17 @@
<span class="gf-form-label width-7">User</span>
<input type="text" class="gf-form-input" ng-model='ctrl.current.user' placeholder="user"></input>
</div>
<div class="gf-form max-width-15" ng-if="!ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="password" class="gf-form-input" ng-model='ctrl.current.secureJsonData.password' placeholder="password"></input>
</div>
<div class="gf-form max-width-19" ng-if="ctrl.current.secureJsonFields.password">
<span class="gf-form-label width-7">Password</span>
<input type="text" class="gf-form-input" disabled="disabled" value="configured">
<a class="btn btn-secondary gf-form-btn" href="#" ng-click="ctrl.current.secureJsonFields.password = false">reset</a>
<div class="gf-form">
<secret-form-field
isConfigured="ctrl.current.secureJsonFields.password"
value="ctrl.current.secureJsonData.password"
on-reset="ctrl.onPasswordReset"
on-change="ctrl.onPasswordChange"
inputWidth="9"
/>
</div>
</div>
<div class="gf-form">
<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">
......
......@@ -351,7 +351,7 @@ const timeSrv = {
};
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;
const query = {
range: { from: time({ seconds: 63 }), to: time({ seconds: 183 }) },
......
......@@ -32,14 +32,14 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>
......
......@@ -37,14 +37,14 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>
......
......@@ -16,13 +16,13 @@ interface Props extends PanelProps<Options> {}
export class GraphPanel extends PureComponent<Props> {
render() {
const { panelData, timeRange, width, height } = this.props;
const { data, timeRange, width, height } = this.props;
const { showLines, showBars, showPoints } = this.props.options;
let vmSeries: TimeSeriesVMs;
if (panelData.timeSeries) {
if (data) {
vmSeries = processTimeSeries({
timeSeries: panelData.timeSeries,
data,
nullValueMode: NullValueMode.Ignore,
});
}
......
......@@ -8,7 +8,7 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
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 {
static templateUrl = 'module.html';
......@@ -112,7 +112,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
scopedVars: _.extend({}, this.panel.scopedVars),
};
if (dataList.length > 0 && dataList[0].type === 'table') {
if (dataList.length > 0 && isTableData(dataList[0])) {
this.dataType = 'table';
const tableData = dataList.map(this.tableHandler.bind(this));
this.setTableValues(tableData, data);
......
......@@ -4,28 +4,33 @@ import React, { PureComponent, CSSProperties } from 'react';
// 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 { getDisplayProcessor } from '@grafana/ui';
import { ProcessedValuesRepeater } from './ProcessedValuesRepeater';
export const getSingleStatValues = (props: PanelProps<SingleStatBaseOptions>): DisplayValue[] => {
const { panelData, replaceVariables, options } = props;
const { data, replaceVariables, options } = props;
const { valueOptions, valueMappings } = options;
const { unit, decimals, stat } = valueOptions;
const processor = getDisplayProcessor({
unit: valueOptions.unit,
decimals: valueOptions.decimals,
unit,
decimals,
mappings: valueMappings,
thresholds: options.thresholds,
prefix: replaceVariables(valueOptions.prefix),
suffix: replaceVariables(valueOptions.suffix),
theme: config.theme,
});
return processSingleStatPanelData({
panelData: panelData,
stat: valueOptions.stat,
}).map(stat => processor(stat.value));
return processTimeSeries({
data,
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>> {
......@@ -50,14 +55,14 @@ export class SingleStatPanel extends PureComponent<PanelProps<SingleStatOptions>
};
render() {
const { height, width, options, panelData, renderCounter } = this.props;
const { height, width, options, data, renderCounter } = this.props;
return (
<ProcessedValuesRepeater
getProcessedValues={this.getProcessedValues}
renderValue={this.renderValue}
width={width}
height={height}
source={panelData}
source={data}
renderCounter={renderCounter}
orientation={options.orientation}
/>
......
......@@ -6,6 +6,7 @@ import { transformDataToTable } from './transformers';
import { tablePanelEditor } from './editor';
import { columnOptionsTab } from './column_options';
import { TableRenderer } from './renderer';
import { isTableData } from '@grafana/ui';
class TablePanelCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
......@@ -104,7 +105,7 @@ class TablePanelCtrl extends MetricsPanelCtrl {
// automatically correct transform mode based on data
if (this.dataRaw && this.dataRaw.length) {
if (this.dataRaw[0].type === 'table') {
if (isTableData(this.dataRaw[0])) {
this.panel.transform = 'table';
} else {
if (this.dataRaw[0].type === 'docs') {
......
......@@ -14,15 +14,15 @@ export class TablePanel extends Component<Props> {
}
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 (
<ThemeContext.Consumer>
{theme => <Table {...this.props} {...options} theme={theme} data={panelData.tableData} />}
{theme => <Table {...this.props} {...options} theme={theme} data={data[0]} />}
</ThemeContext.Consumer>
);
}
......
......@@ -12,8 +12,7 @@ module.exports = function(options) {
options: {
importLoaders: 2,
url: options.preserveUrl,
sourceMap: options.sourceMap,
minimize: options.minimize,
sourceMap: options.sourceMap
},
},
{
......
......@@ -19,13 +19,6 @@ module.exports = merge(common, {
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: {
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=&.]+)?$/,
loader: 'file-loader'
......@@ -59,7 +52,7 @@ module.exports = merge(common, {
},
plugins: [
new CleanWebpackPlugin('../../public/build', { allowExternal: true }),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: "grafana.[name].[hash].css"
}),
......
......@@ -56,7 +56,7 @@ module.exports = merge(common, {
plugins: [
[require('@rtsao/plugin-proposal-class-properties'), { loose: true }],
'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',
],
presets: [
......@@ -98,7 +98,7 @@ module.exports = merge(common, {
},
plugins: [
new CleanWebpackPlugin('../public/build', { allowExternal: true }),
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../../public/views/index.html'),
template: path.resolve(__dirname, '../../public/views/index-template.html'),
......
'use strict';
const merge = require('webpack-merge');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const common = require('./webpack.common.js');
const path = require('path');
const ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
......@@ -43,14 +43,14 @@ module.exports = merge(common, {
},
},
require('./sass.rule.js')({
sourceMap: false, minimize: false, preserveUrl: false
sourceMap: false, preserveUrl: false
})
]
},
optimization: {
minimizer: [
new UglifyJsPlugin({
cache: true,
new TerserPlugin({
cache: false,
parallel: true,
sourceMap: true
}),
......
......@@ -27,7 +27,7 @@
"noUnusedLocals": true,
"baseUrl": "public",
"pretty": true,
"typeRoots": ["node_modules/@types", "types"],
"typeRoots": ["node_modules/@types", "public/app/types"],
"paths": {
"app": ["app"],
"sass": ["sass"]
......
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment