Merge branch 'master' into hugoh/library-panel-api

This commit is contained in:
Hugo Häggmark
2020-12-04 13:50:43 +01:00
128 changed files with 2219 additions and 2239 deletions
+4 -6
View File
@@ -506,15 +506,13 @@ steps:
- test-backend
- test-frontend
- name: release-next-npm-packages
- name: release-canary-npm-packages
image: grafana/build-container:1.3.0
commands:
- ./node_modules/.bin/lerna bootstrap
- echo "//registry.npmjs.org/:_authToken=$${NPM_TOKEN}" >> ~/.npmrc
- ./scripts/circle-release-next-packages.sh
- ./scripts/circle-release-canary-packages.sh
environment:
NPM_TOKEN:
from_secret: npm_token
GITHUB_PACKAGE_TOKEN:
from_secret: github_package_token
depends_on:
- end-to-end-tests
+1
View File
@@ -64,6 +64,7 @@ RUN export GF_GID_NAME=$(getent group $GF_GID | cut -d':' -f1) && \
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
"$GF_PATHS_PROVISIONING/dashboards" \
"$GF_PATHS_PROVISIONING/notifiers" \
"$GF_PATHS_PROVISIONING/plugins" \
"$GF_PATHS_LOGS" \
"$GF_PATHS_PLUGINS" \
"$GF_PATHS_DATA" && \
+1
View File
@@ -58,6 +58,7 @@ RUN mkdir -p "$GF_PATHS_HOME/.aws" && \
mkdir -p "$GF_PATHS_PROVISIONING/datasources" \
"$GF_PATHS_PROVISIONING/dashboards" \
"$GF_PATHS_PROVISIONING/notifiers" \
"$GF_PATHS_PROVISIONING/plugins" \
"$GF_PATHS_LOGS" \
"$GF_PATHS_PLUGINS" \
"$GF_PATHS_DATA" && \
@@ -115,7 +115,6 @@ If that is the case, then use the following format for headings:
- Grafana, Loki, and Prometheus are always capitalized unless part of a code block.
- API names are always Title Case, followed by "API"—for example, "Dashboard Permissions API"
- Git is always capitalized, unless part of a code block.
- Abbreviations are always capitalized (such as API, HTTP, ID, JSON, SQL, or URL) unless they are part of a code block.
- Menu and submenu titles always use sentence case: capitalize the first word, and lowercase the rest.
- "Dashboards" when referring to the submenu title.
@@ -125,6 +124,22 @@ If that is the case, then use the following format for headings:
- Lowercase "dashboards" when referring to multiple dashboards.
- **Exceptions:** If a term is lowercased in the Grafana UI, then match the UI.
#### Git, GitHub
Git is always capitalized, unless part of a code block. GitHub is the correct spelling and capitalization.
#### Integrations
In general, "integration" is not capitalized. Only capitalize it if it is capitalized in the UI or part of a proper noun, like the name of a specific integration.
The first letter of the name of an integration is always capitalized, even if the original named source is lowercase.
**Examples:**
- MySQL Integration
- CockroachDB Integration
- Etcd Integration
- I installed an integration on my local Grafana.
### Links and references
When referencing another document, use "Refer to" rather than alternatives such as "See" or "Check out."
@@ -8,6 +8,7 @@
environment:
INFLUXDB_ADMIN_USER: grafana
INFLUXDB_ADMIN_PASSWORD: grafana
INFLUXDB_DB: site
volumes:
- ./docker/blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
+10 -9
View File
@@ -147,19 +147,20 @@ Since not all datasources have the same configuration settings we only have the
| tlsAuthWithCACert | boolean | _All_ | Enable TLS authentication using CA cert |
| tlsSkipVerify | boolean | _All_ | Controls whether a client verifies the server's certificate chain and host name. |
| graphiteVersion | string | Graphite | Graphite version |
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source |
| httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET |
| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET |
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL and MSSQL | Lowest interval/step value that should be used for this data source. |
| httpMode | string | Influxdb | HTTP Method. 'GET', 'POST', defaults to GET |
| httpMethod | string | Prometheus | HTTP Method. 'GET', 'POST', defaults to GET |
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60/70) |
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
| logMessageField | string | Elasticsearch | Which field should be used as the log message |
| logLevelField | string | Elasticsearch | Which field should be used to indicate the priority of the log message |
| sigV4AuthType | string | Elasticsearch | SigV4 auth provider. default/credentials/keys |
| sigV4ExternalId | string | Elasticsearch | Optional SigV4 External ID |
| sigV4AssumeRoleArn | string | Elasticsearch | Optional SigV4 ARN role to assume |
| sigV4Region | string | Elasticsearch | SigV4 AWS region |
| sigV4Profile | string | Elasticsearch | Optional SigV4 credentials profile |
| sigV4Auth | boolean | Elasticsearch | Enable usage of SigV4 |
| sigV4AuthType | string | Elasticsearch | SigV4 auth provider. default/credentials/keys |
| sigV4ExternalId | string | Elasticsearch | Optional SigV4 External ID |
| sigV4AssumeRoleArn | string | Elasticsearch | Optional SigV4 ARN role to assume |
| sigV4Region | string | Elasticsearch | SigV4 AWS region |
| sigV4Profile | string | Elasticsearch | Optional SigV4 credentials profile |
| authType | string | Cloudwatch | Auth provider. default/credentials/keys |
| externalId | string | Cloudwatch | Optional External ID |
| assumeRoleArn | string | Cloudwatch | Optional ARN role to assume |
@@ -194,7 +195,7 @@ Secure json data is a map of settings that will be encrypted with [secret key]({
| basicAuthPassword | string | _All_ | password for basic authentication |
| accessKey | string | Cloudwatch | Access key for connecting to Cloudwatch |
| secretKey | string | Cloudwatch | Secret key for connecting to Cloudwatch |
| sigV4AccessKey | string | Elasticsearch | SigV4 access key. Required when using keys auth provider |
| sigV4AccessKey | string | Elasticsearch | SigV4 access key. Required when using keys auth provider |
| sigV4SecretKey | string | Elasticsearch | SigV4 secret key. Required when using keys auth provider |
#### Custom HTTP headers for datasources
+3 -3
View File
@@ -185,9 +185,9 @@
"style-loader": "1.1.3",
"terser-webpack-plugin": "2.3.5",
"ts-jest": "26.4.4",
"ts-node": "8.8.1",
"tslib": "2.0.1",
"typescript": "4.0.2",
"ts-node": "9.0.0",
"tslib": "2.0.3",
"typescript": "4.1.2",
"webpack": "4.41.5",
"webpack-bundle-analyzer": "3.6.0",
"webpack-cleanup-plugin": "0.5.1",
+23 -1
View File
@@ -27,7 +27,29 @@ Every commit to master that has changes within the `packages` directory is a sub
<lerna.json version>-<COMMIT_SHA>
```
Automatic prereleases are published under the `canary` dist tag.
Automatic prereleases are published under the `canary` dist tag to the [github package registry](https://docs.github.com/en/free-pro-team@latest/packages/publishing-and-managing-packages/about-github-packages).
#### Consuming prereleases
As mentioned above the `canary` releases are published to the Github package registry rather than the NPM registry. If you wish to make use of these prereleases please follow these steps:
1. You must use a personal access token to install packages from Github. To create an access token [click here](https://github.com/settings/tokens) and create a token with the `read:packages` scope. Make a copy of the token.
2. Create / modify your `~/.npmrc` file with the following:
```
@grafana:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken={INSERT_GH_TOKEN_HERE}
```
3. Update the package.json of your project to use either the `canary` channel or a version of the `canary` channel
```json
// plugin's package.json
{
...
"@grafana/data": "canary"
}
```
### Manual release
+1 -1
View File
@@ -53,6 +53,6 @@
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"sinon": "8.1.1",
"typescript": "4.0.2"
"typescript": "4.1.2"
}
}
@@ -160,7 +160,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi
});
values.push({
name,
name: '',
field: config,
display: displayValue,
view,
+2 -2
View File
@@ -36,14 +36,14 @@
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"ts-loader": "6.2.1",
"ts-node": "8.8.1"
"ts-node": "9.0.0"
},
"types": "src/index.ts",
"dependencies": {
"@grafana/tsconfig": "^1.0.0-rc1",
"commander": "5.0.0",
"execa": "4.0.0",
"typescript": "4.0.2",
"typescript": "4.1.2",
"yaml": "^1.8.3"
}
}
+1 -1
View File
@@ -54,7 +54,7 @@
"execa": "4.0.0",
"resolve-as-bin": "2.1.0",
"ts-loader": "6.2.1",
"typescript": "4.0.2",
"typescript": "4.1.2",
"yaml": "^1.8.3"
}
}
+1 -1
View File
@@ -41,7 +41,7 @@
"rollup-plugin-terser": "5.3.0",
"rollup-plugin-typescript2": "0.26.0",
"rollup-plugin-visualizer": "3.3.1",
"typescript": "4.0.2"
"typescript": "4.1.2"
},
"types": "src/index.ts"
}
+3 -3
View File
@@ -103,9 +103,9 @@
"terser-webpack-plugin": "2.3.5",
"ts-jest": "26.4.4",
"ts-loader": "6.2.1",
"ts-node": "8.8.1",
"tslib": "2.0.1",
"typescript": "4.0.2",
"ts-node": "9.0.0",
"tslib": "2.0.3",
"typescript": "4.1.2",
"url-loader": "^2.0.1",
"webpack": "4.41.5"
},
@@ -22,7 +22,7 @@ const compile = () =>
const savePackage = ({ path, pkg }: { path: string; pkg: {} }) =>
useSpinner('Updating package.json', async () => {
new Promise((resolve, reject) => {
new Promise<void>((resolve, reject) => {
fs.writeFile(path, JSON.stringify(pkg, null, 2), err => {
if (err) {
reject(err);
@@ -61,7 +61,7 @@ const copyFiles = () => {
return useSpinner(`Moving ${files.join(', ')} files`, async () => {
const promises = files.map(file => {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const basedir = path.dirname(`${distDir}/${file}`);
if (!fs.existsSync(basedir)) {
fs.mkdirSync(basedir, { recursive: true });
@@ -85,7 +85,7 @@ const copySassFiles = () => {
return useSpinner(`Copy scss files ${files.join(', ')} files`, async () => {
const sassDir = path.resolve(cwd, '../../public/sass/');
const promises = files.map(file => {
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const name = file.replace('.generated', '');
fs.copyFile(`${sassDir}/${file}`, `${distDir}/sass/${name}`, err => {
if (err) {
+2 -15
View File
@@ -26,7 +26,9 @@ module.exports = {
check: true,
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop: any) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
@@ -44,21 +46,6 @@ module.exports = {
configFile: path.resolve(__dirname, 'tsconfig.json'),
},
},
{
loader: require.resolve('react-docgen-typescript-loader'),
options: {
tsconfigPath: path.resolve(__dirname, 'tsconfig.json'),
// https://github.com/styleguidist/react-docgen-typescript#parseroptions
// @ts-ignore
propFilter: prop => {
if (prop.parent) {
return !prop.parent.fileName.includes('node_modules/@types/react/');
}
return true;
},
},
},
],
},
{
+7 -7
View File
@@ -77,12 +77,12 @@
"@rollup/plugin-commonjs": "16.0.0",
"@rollup/plugin-image": "2.0.5",
"@rollup/plugin-node-resolve": "10.0.0",
"@storybook/addon-essentials": "6.1.2",
"@storybook/addon-controls": "6.1.2",
"@storybook/addon-knobs": "6.1.2",
"@storybook/addon-storysource": "6.1.2",
"@storybook/react": "6.1.2",
"@storybook/theming": "6.1.2",
"@storybook/addon-essentials": "6.1.9",
"@storybook/addon-controls": "6.1.9",
"@storybook/addon-knobs": "6.1.9",
"@storybook/addon-storysource": "6.1.9",
"@storybook/react": "6.1.9",
"@storybook/theming": "6.1.9",
"@types/classnames": "2.2.7",
"@types/common-tags": "^1.8.0",
"@types/d3": "5.7.2",
@@ -111,7 +111,7 @@
"rollup-plugin-visualizer": "4.2.0",
"storybook-dark-mode": "1.0.3",
"ts-loader": "8.0.11",
"typescript": "4.0.2",
"typescript": "4.1.2",
"webpack-filter-warnings-plugin": "1.2.1"
},
"types": "src/index.ts"
@@ -9,7 +9,7 @@ import { getColorsFromSeverity } from '../../utils/colors';
export type AlertVariant = 'success' | 'warning' | 'error' | 'info';
export interface Props extends HTMLAttributes<HTMLElement> {
export interface Props extends HTMLAttributes<HTMLDivElement> {
title: string;
/** On click handler for alert button, mostly used for dismissing the alert */
onRemove?: (event: React.MouseEvent) => void;
@@ -39,13 +39,13 @@ function getIconFromSeverity(severity: AlertVariant): string {
}
}
export const Alert: FC<Props> = React.forwardRef<HTMLElement, Props>(
({ title, buttonText, onButtonClick, onRemove, children, buttonContent, severity = 'error', ...restProps }) => {
export const Alert: FC<Props> = React.forwardRef<HTMLDivElement, Props>(
({ title, buttonText, onButtonClick, onRemove, children, buttonContent, severity = 'error', ...restProps }, ref) => {
const theme = useTheme();
const styles = getStyles(theme, severity, !!buttonContent);
return (
<div className={styles.alert} aria-label={selectors.components.Alert.alert(severity)} {...restProps}>
<div ref={ref} className={styles.alert} aria-label={selectors.components.Alert.alert(severity)} {...restProps}>
<div className={styles.icon}>
<Icon size="xl" name={getIconFromSeverity(severity) as IconName} />
</div>
@@ -4,11 +4,11 @@ exports[`FormField should render component with custom inputEl 1`] = `
<div
className="form-field"
>
<Component
<FormLabel
width={11}
>
Test
</Component>
</FormLabel>
<span>
Input
</span>
@@ -22,11 +22,11 @@ exports[`FormField should render component with default inputEl 1`] = `
<div
className="form-field"
>
<Component
<FormLabel
width={11}
>
Test
</Component>
</FormLabel>
<input
className="gf-form-input width-12"
onChange={[MockFunction]}
@@ -197,7 +197,7 @@ export const AsyncValidation = () => {
const validateAsync = (shouldPass: boolean) => async () => {
try {
await new Promise<ValidateResult>((resolve, reject) => {
await new Promise<ValidateResult | void>((resolve, reject) => {
setTimeout(() => {
if (shouldPass) {
resolve();
@@ -114,14 +114,15 @@ export const GraphNG: React.FC<GraphNGProps> = ({
}
const fmt = field.display ?? defaultFormatter;
const scale = config.unit || '__fixed';
const isNewScale = !builder.hasScale(scale);
const scaleKey = config.unit || '__fixed';
if (isNewScale && customConfig.axisPlacement !== AxisPlacement.Hidden) {
builder.addScale({ scaleKey: scale, min: field.config.min, max: field.config.max });
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
// The builder will manage unique scaleKeys and combine where appropriate
builder.addScale({ scaleKey, min: field.config.min, max: field.config.max });
builder.addAxis({
scaleKey: scale,
scaleKey,
label: customConfig.axisLabel,
size: customConfig.axisWidth,
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
formatValue: v => formattedValueToString(fmt(v)),
theme,
@@ -136,7 +137,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
const pointsMode = customConfig.mode === GraphMode.Points ? PointMode.Always : customConfig.points;
builder.addSeries({
scaleKey: scale,
scaleKey,
mode: customConfig.mode!,
lineColor: seriesColor,
lineWidth: customConfig.lineWidth,
@@ -149,7 +150,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
});
if (hasLegend.current) {
const axisPlacement = builder.getAxisPlacement(scale);
const axisPlacement = builder.getAxisPlacement(scaleKey);
legendItems.push({
color: seriesColor,
@@ -1,10 +1,13 @@
import { SelectableValue } from '@grafana/data';
import { SelectOptions } from './types';
import { SelectableOptGroup } from './types';
/**
* Normalize the value format to SelectableValue[] | []. Only used for single select
*/
export const cleanValue = (value: any, options: SelectOptions): SelectableValue[] | [] => {
export const cleanValue = (
value: any,
options: Array<SelectableValue | SelectableOptGroup | SelectableOptGroup[]>
): SelectableValue[] | [] => {
if (Array.isArray(value)) {
return value.filter(Boolean);
}
@@ -23,7 +26,10 @@ export const cleanValue = (value: any, options: SelectOptions): SelectableValue[
/**
* Find the label for a string|number value inside array of options or optgroups
*/
export const findSelectedValue = (value: string | number, options: SelectOptions): SelectableValue | null => {
export const findSelectedValue = (
value: string | number,
options: Array<SelectableValue | SelectableOptGroup | SelectableOptGroup[]>
): SelectableValue | null => {
for (const option of options) {
if ('options' in option) {
let found = findSelectedValue(value, option.options);
@@ -13,7 +13,7 @@ interface DemoBoxProps {
const DemoBox: FC<DemoBoxProps> = ({ bg, border, children }) => {
const style = cx(
css`
padding: 16px 32px;
padding: 32px 32px 16px 32px;
background: ${bg};
width: 100%;
`,
@@ -24,18 +24,7 @@ const DemoBox: FC<DemoBoxProps> = ({ bg, border, children }) => {
: null
);
return (
<div className={style}>
<div
className={css`
padding-bottom: 16px;
`}
>
{name}
</div>
{children}
</div>
);
return <div className={style}>{children}</div>;
};
const DemoText: FC<{ color?: string; bold?: boolean; size?: number }> = ({ color, bold, size, children }) => {
@@ -57,7 +57,7 @@ exports[`TimePickerContent renders correctly in full screen 1`] = `
}
visible={false}
/>
<Component
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
@@ -76,7 +76,7 @@ exports[`TimePickerContent renders correctly in full screen 1`] = `
<div
className="css-1ogeuxc"
/>
<Component
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
@@ -94,7 +94,7 @@ exports[`TimePickerContent renders correctly in full screen 1`] = `
/>
</CustomScrollbar>
</div>
<Component
<TimePickerFooter
onChangeTimeZone={[Function]}
timeZone="utc"
/>
@@ -158,7 +158,7 @@ exports[`TimePickerContent renders correctly in narrow screen 1`] = `
}
visible={true}
/>
<Component
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
@@ -177,7 +177,7 @@ exports[`TimePickerContent renders correctly in narrow screen 1`] = `
<div
className="css-1ogeuxc"
/>
<Component
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
@@ -325,7 +325,7 @@ exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
}
visible={false}
/>
<Component
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
@@ -344,7 +344,7 @@ exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
<div
className="css-1ogeuxc"
/>
<Component
<TimeRangeList
onSelect={[Function]}
options={Array []}
timeZone="utc"
@@ -362,7 +362,7 @@ exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
/>
</CustomScrollbar>
</div>
<Component
<TimePickerFooter
onChangeTimeZone={[Function]}
timeZone="utc"
/>
@@ -23,10 +23,6 @@ export const UPlotChart: React.FC<PlotProps> = props => {
props.config
);
const getPlotInstance = useCallback(() => {
if (!plotInstance.current) {
throw new Error("Plot hasn't initialised yet");
}
return plotInstance.current;
}, []);
@@ -72,13 +68,15 @@ export const UPlotChart: React.FC<PlotProps> = props => {
// Memoize plot context
const plotCtx = useMemo(() => {
return buildPlotContext(Boolean(plotInstance.current), canvasRef, props.data, registerPlugin, getPlotInstance);
return buildPlotContext(canvasRef, props.data, registerPlugin, getPlotInstance);
}, [plotInstance, canvasRef, props.data, registerPlugin, getPlotInstance]);
return (
<PlotContext.Provider value={plotCtx}>
<div ref={plotCtx.canvasRef} data-testid="uplot-main-div" />
{props.children}
<div style={{ position: 'relative' }}>
<div ref={plotCtx.canvasRef} data-testid="uplot-main-div" />
{props.children}
</div>
</PlotContext.Provider>
);
};
@@ -3,13 +3,14 @@ import uPlot, { Axis } from 'uplot';
import { PlotConfigBuilder } from '../types';
import { measureText } from '../../../utils/measureText';
import { AxisPlacement } from '../config';
import { optMinMax } from './UPlotScaleBuilder';
export interface AxisProps {
scaleKey: string;
theme: GrafanaTheme;
label?: string;
show?: boolean;
size?: number;
size?: number | null;
placement?: AxisPlacement;
grid?: boolean;
formatValue?: (v: any) => string;
@@ -19,6 +20,16 @@ export interface AxisProps {
}
export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
merge(props: AxisProps) {
this.props.size = optMinMax('max', this.props.size, props.size);
if (!this.props.label) {
this.props.label = props.label;
}
if (this.props.placement === AxisPlacement.Auto) {
this.props.placement = props.placement;
}
}
getConfig(): Axis {
const {
scaleKey,
@@ -42,7 +53,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
side: getUPlotSideFromAxis(placement),
font: `12px 'Roboto'`,
labelFont: `12px 'Roboto'`,
size: calculateAxisSize,
size: this.props.size ?? calculateAxisSize,
grid: {
show: grid,
stroke: gridColor,
@@ -107,8 +118,7 @@ function calculateAxisSize(self: uPlot, values: string[], axisIdx: number) {
}
}
let axisWidth = measureText(maxLength, 12).width + 18;
return axisWidth;
return measureText(maxLength, 12).width + 18;
}
/** Format time axis ticks */
@@ -61,7 +61,6 @@ describe('UPlotConfigBuilder', () => {
formatValue: () => 'test value',
grid: false,
show: true,
size: 1,
theme: { isDark: true, palette: { gray25: '#ffffff' }, colors: { text: 'gray' } } as GrafanaTheme,
values: [],
});
@@ -8,13 +8,17 @@ export class UPlotConfigBuilder {
private series: UPlotSeriesBuilder[] = [];
private axes: Record<string, UPlotAxisBuilder> = {};
private scales: UPlotScaleBuilder[] = [];
private registeredScales: string[] = [];
hasLeftAxis = false;
addAxis(props: AxisProps) {
props.placement = props.placement ?? AxisPlacement.Auto;
if (this.axes[props.scaleKey]) {
this.axes[props.scaleKey].merge(props);
return;
}
// Handle auto placement logic
if (props.placement === AxisPlacement.Auto) {
props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left;
@@ -36,15 +40,16 @@ export class UPlotConfigBuilder {
this.series.push(new UPlotSeriesBuilder(props));
}
/** Add or update the scale with the scale key */
addScale(props: ScaleProps) {
this.registeredScales.push(props.scaleKey);
const current = this.scales.find(v => v.props.scaleKey === props.scaleKey);
if (current) {
current.merge(props);
return;
}
this.scales.push(new UPlotScaleBuilder(props));
}
hasScale(scaleKey: string) {
return this.registeredScales.indexOf(scaleKey) > -1;
}
getConfig() {
const config: PlotSeriesConfig = { series: [{}] };
config.axes = Object.values(this.axes).map(a => a.getConfig());
@@ -0,0 +1,21 @@
import { optMinMax } from './UPlotScaleBuilder';
describe('UPlotScaleBuilder', () => {
it('opt min max', () => {
expect(7).toEqual(optMinMax('min', null, 7));
expect(7).toEqual(optMinMax('min', undefined, 7));
expect(7).toEqual(optMinMax('min', 20, 7));
expect(7).toEqual(optMinMax('min', 7, null));
expect(7).toEqual(optMinMax('min', 7, undefined));
expect(7).toEqual(optMinMax('min', 7, 20));
expect(7).toEqual(optMinMax('max', null, 7));
expect(7).toEqual(optMinMax('max', undefined, 7));
expect(7).toEqual(optMinMax('max', 5, 7));
expect(7).toEqual(optMinMax('max', 7, null));
expect(7).toEqual(optMinMax('max', 7, undefined));
expect(7).toEqual(optMinMax('max', 7, 5));
});
});
@@ -9,6 +9,11 @@ export interface ScaleProps {
}
export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
merge(props: ScaleProps) {
this.props.min = optMinMax('min', this.props.min, props.min);
this.props.max = optMinMax('max', this.props.max, props.max);
}
getConfig() {
const { isTime, scaleKey } = this.props;
if (isTime) {
@@ -29,3 +34,18 @@ export class UPlotScaleBuilder extends PlotConfigBuilder<ScaleProps, Scale> {
};
}
}
export function optMinMax(minmax: 'min' | 'max', a?: number | null, b?: number | null): undefined | number | null {
const hasA = !(a === undefined || a === null);
const hasB = !(b === undefined || b === null);
if (hasA) {
if (!hasB) {
return a;
}
if (minmax === 'min') {
return a! < b! ? a : b;
}
return a! > b! ? a : b;
}
return b;
}
@@ -21,8 +21,7 @@ interface PlotPluginsContextType {
}
interface PlotContextType extends PlotPluginsContextType {
isPlotReady: boolean;
getPlotInstance: () => uPlot;
getPlotInstance: () => uPlot | undefined;
getSeries: () => Series[];
getCanvas: () => PlotCanvasContextType;
canvasRef: any;
@@ -127,28 +126,31 @@ export const usePlotData = (): PlotDataAPI => {
};
export const buildPlotContext = (
isPlotReady: boolean,
canvasRef: any,
data: AlignedFrameWithGapTest,
registerPlugin: any,
getPlotInstance: () => uPlot
getPlotInstance: () => uPlot | undefined
): PlotContextType => {
return {
isPlotReady,
canvasRef,
data,
registerPlugin,
getPlotInstance,
getSeries: () => getPlotInstance().series,
getCanvas: () => ({
width: getPlotInstance().width,
height: getPlotInstance().height,
plot: {
width: getPlotInstance().bbox.width / window.devicePixelRatio,
height: getPlotInstance().bbox.height / window.devicePixelRatio,
top: getPlotInstance().bbox.top / window.devicePixelRatio,
left: getPlotInstance().bbox.left / window.devicePixelRatio,
},
}),
getSeries: () => getPlotInstance()!.series,
getCanvas: () => {
const plotInstance = getPlotInstance()!;
const bbox = plotInstance.bbox;
const pxRatio = window.devicePixelRatio;
return {
width: plotInstance.width,
height: plotInstance.height,
plot: {
width: bbox.width / pxRatio,
height: bbox.height / pxRatio,
top: bbox.top / pxRatio,
left: bbox.left / pxRatio,
},
};
},
};
};
@@ -18,8 +18,7 @@ export function EventsCanvas<T>({ id, events, renderEventMarker, mapEventToXYCoo
const eventMarkers = useMemo(() => {
const markers: React.ReactNode[] = [];
if (!plotCtx.isPlotReady || events.length === 0) {
if (!plotCtx.getPlotInstance() || events.length === 0) {
return markers;
}
@@ -41,9 +40,9 @@ export function EventsCanvas<T>({ id, events, renderEventMarker, mapEventToXYCoo
}
return <>{markers}</>;
}, [events, renderEventMarker, renderToken, plotCtx.isPlotReady]);
}, [events, renderEventMarker, renderToken, plotCtx]);
if (!plotCtx.isPlotReady) {
if (!plotCtx.getPlotInstance()) {
return null;
}
@@ -10,8 +10,9 @@ interface XYCanvasProps {}
*/
export const XYCanvas: React.FC<XYCanvasProps> = ({ children }) => {
const plotContext = usePlotContext();
const plotInstance = plotContext.getPlotInstance();
if (!plotContext.isPlotReady) {
if (!plotInstance) {
return null;
}
@@ -20,8 +21,8 @@ export const XYCanvas: React.FC<XYCanvasProps> = ({ children }) => {
className={css`
position: absolute;
overflow: visible;
left: ${plotContext.getPlotInstance().bbox.left / window.devicePixelRatio}px;
top: ${plotContext.getPlotInstance().bbox.top / window.devicePixelRatio}px;
left: ${plotInstance.bbox.left / window.devicePixelRatio}px;
top: ${plotInstance.bbox.top / window.devicePixelRatio}px;
`}
>
{children}
@@ -75,7 +75,7 @@ export const SelectionPlugin: React.FC<SelectionPluginProps> = ({ onSelect, onDi
};
}, []);
if (!plotCtx.isPlotReady || !children || !selection) {
if (!plotCtx.getPlotInstance() || !children || !selection) {
return null;
}
@@ -25,7 +25,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({ mode = 'single', t
return (
<CursorPlugin id={pluginId}>
{({ focusedSeriesIdx, focusedPointIdx, coords }) => {
if (!plotContext.isPlotReady) {
if (!plotContext.getPlotInstance()) {
return null;
}
+2
View File
@@ -80,6 +80,7 @@ export type IconName =
| 'compass'
| 'sliders-v-alt'
| 'bell'
| 'bell-slash'
| 'database'
| 'user'
| 'camera'
@@ -198,6 +199,7 @@ export const getAvailableIcons = (): IconName[] => [
'compass',
'sliders-v-alt',
'bell',
'bell-slash',
'database',
'user',
'camera',
+1 -1
View File
@@ -11,7 +11,7 @@
"devDependencies": {
"enzyme": "^3.8.0",
"enzyme-adapter-react-16": "^1.2.0",
"typescript": "4.0.2"
"typescript": "4.1.2"
},
"dependencies": {
"@grafana/data": "7.4.0-pre.0",
+3
View File
@@ -132,6 +132,9 @@ func newNotFound() *Avatar {
avatar := &Avatar{notFound: true}
// load user_profile png into buffer
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
// variable.
// nolint:gosec
path := filepath.Join(setting.StaticRootPath, "img", "user_profile.png")
if data, err := ioutil.ReadFile(path); err != nil {
+9 -3
View File
@@ -15,7 +15,6 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/dashdiffs"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/guardian"
@@ -334,7 +333,7 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) Response {
dashRedirect := dtos.DashboardRedirect{RedirectUri: url}
return JSON(200, &dashRedirect)
}
log.Warnf("Failed to get slug from database, %s", err.Error())
hs.log.Warn("Failed to get slug from database", "err", err)
}
filePath := hs.Cfg.DefaultHomeDashboardPath
@@ -342,11 +341,18 @@ func (hs *HTTPServer) GetHomeDashboard(c *models.ReqContext) Response {
filePath = filepath.Join(hs.Cfg.StaticRootPath, "dashboards/home.json")
}
// It's safe to ignore gosec warning G304 since the variable part of the file path comes from a configuration
// variable
// nolint:gosec
file, err := os.Open(filePath)
if err != nil {
return Error(500, "Failed to load home dashboard", err)
}
defer file.Close()
defer func() {
if err := file.Close(); err != nil {
hs.log.Warn("Failed to close dashboard file", "path", filePath, "err", err)
}
}()
dash := dtos.DashboardFullWithMeta{}
dash.Meta.IsHome = true
@@ -291,6 +291,10 @@ func extractFile(file *zip.File, filePath string) (err error) {
fileMode = os.FileMode(0755)
}
// We can ignore the gosec G304 warning on this one, since the variable part of the file path stems
// from command line flag "pluginsDir", and the only possible damage would be writing to the wrong directory.
// If the user shouldn't be writing to this directory, they shouldn't have the permission in the file system.
// nolint:gosec
dst, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
if os.IsPermission(err) {
+4 -1
View File
@@ -44,8 +44,11 @@ func (client *GrafanaComClient) GetPlugin(pluginId, repoUrl string) (models.Plug
}
func (client *GrafanaComClient) DownloadFile(pluginName string, tmpFile *os.File, url string, checksum string) (err error) {
// Try handling url like local file path first
// Try handling URL as a local file path first
if _, err := os.Stat(url); err == nil {
// We can ignore this gosec G304 warning since `url` stems from command line flag "pluginUrl". If the
// user shouldn't be able to read the file, it should be handled through filesystem permissions.
// nolint:gosec
f, err := os.Open(url)
if err != nil {
return errutil.Wrap("Failed to read plugin archive", err)
+4
View File
@@ -21,5 +21,9 @@ func (i IoUtilImp) ReadDir(path string) ([]os.FileInfo, error) {
}
func (i IoUtilImp) ReadFile(filename string) ([]byte, error) {
// We can ignore the gosec G304 warning on this one, since the variable part of the file path stems
// from command line flag "pluginsDir". If the user shouldn't be reading from this directory, they shouldn't have
// the permission in the file system.
// nolint:gosec
return ioutil.ReadFile(filename)
}
+2 -2
View File
@@ -16,8 +16,8 @@ func GetGrafanaPluginDir(currentOS string) string {
return returnOsDefault(currentOS)
}
// getGrafanaRoot tries to get root of directory when developing grafana ie repo root. It is not perfect it just
// checks what is the binary path and tries to guess based on that but if it is not running in dev env you get a bogus
// getGrafanaRoot tries to get root of directory when developing grafana, ie. repo root. It is not perfect, it just
// checks what is the binary path and tries to guess based on that, but if it is not running in dev env you get a bogus
// path back.
func getGrafanaRoot() (string, error) {
ex, err := os.Executable()
+5 -1
View File
@@ -116,7 +116,11 @@ func executeServer(configFile, homePath, pidFile, packaging string, traceDiagnos
if err != nil {
panic(err)
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
log.Error("Failed to write trace diagnostics", "path", traceDiagnostics.file, "err", err)
}
}()
if err := trace.Start(f); err != nil {
panic(err)
@@ -45,11 +45,18 @@ func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) (
// setup client
blob := NewStorageClient(az.account_name, az.account_key)
// We can ignore the gosec G304 warning on this one because `imageDiskPath` comes
// from alert notifiers and is only used to upload images generated by alerting.
// nolint:gosec
file, err := os.Open(imageDiskPath)
if err != nil {
return "", err
}
defer file.Close()
defer func() {
if err := file.Close(); err != nil {
az.log.Warn("Failed to close file", "path", imageDiskPath, "err", err)
}
}()
randomFileName, err := util.GetRandomString(30)
if err != nil {
@@ -149,11 +149,19 @@ func (u *Uploader) uploadFile(
key string,
) error {
u.log.Debug("Opening image file", "path", imageDiskPath)
// We can ignore the gosec G304 warning on this one because `imageDiskPath` comes
// from alert notifiers and is only used to upload images generated by alerting.
// nolint:gosec
fileReader, err := os.Open(imageDiskPath)
if err != nil {
return err
}
defer fileReader.Close()
defer func() {
if err := fileReader.Close(); err != nil {
u.log.Warn("Failed to close file", "err", err, "path", imageDiskPath)
}
}()
// Set public access if not generating a signed URL
pubAcc := !u.enableSignedURLs
+9 -1
View File
@@ -76,16 +76,24 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string,
key := u.path + rand + pngExt
log.Debugf("Uploading image to s3. bucket = %s, path = %s", u.bucket, key)
// We can ignore the gosec G304 warning on this one because `imageDiskPath` comes
// from alert notifiers and is only used to upload images generated by alerting.
// nolint:gosec
file, err := os.Open(imageDiskPath)
if err != nil {
return "", err
}
defer file.Close()
defer func() {
if err := file.Close(); err != nil {
u.log.Warn("Failed to close file", "path", imageDiskPath, "err", err)
}
}()
sess, err = session.NewSession(cfg)
if err != nil {
return "", err
}
uploader := s3manager.NewUploader(sess)
result, err := uploader.UploadWithContext(ctx, &s3manager.UploadInput{
Bucket: aws.String(u.bucket),
+5 -2
View File
@@ -45,7 +45,7 @@ func (u *WebdavUploader) PublicURL(filename string) string {
return publicURL.String()
}
func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
func (u *WebdavUploader) Upload(ctx context.Context, imgToUpload string) (string, error) {
url, _ := url.Parse(u.url)
filename, err := util.GetRandomString(20)
if err != nil {
@@ -55,7 +55,10 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
filename += pngExt
url.Path = path.Join(url.Path, filename)
imgData, err := ioutil.ReadFile(pa)
// We can ignore the gosec G304 warning on this one because `imgToUpload` comes
// from alert notifiers and is only used to upload images generated by alerting.
// nolint:gosec
imgData, err := ioutil.ReadFile(imgToUpload)
if err != nil {
return "", err
}
+4
View File
@@ -80,6 +80,10 @@ func Warnf(format string, v ...interface{}) {
Root.Warn(message)
}
func Error(msg string, args ...interface{}) {
Root.Error(msg, args...)
}
func Errorf(skip int, format string, v ...interface{}) {
Root.Error(fmt.Sprintf(format, v...))
}
+78 -76
View File
@@ -7,103 +7,105 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
. "github.com/smartystreets/goconvey/convey"
)
func TestMiddlewareAuth(t *testing.T) {
Convey("Given the grafana middleware", t, func() {
reqSignIn := Auth(&AuthOptions{ReqSignedIn: true})
reqSignIn := Auth(&AuthOptions{ReqSignedIn: true})
middlewareScenario(t, "ReqSignIn true and unauthenticated request", func(sc *scenarioContext) {
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
middlewareScenario(t, "ReqSignIn true and unauthenticated request", func(t *testing.T, sc *scenarioContext) {
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure").exec()
sc.fakeReq("GET", "/secure").exec()
Convey("Should redirect to login", func() {
So(sc.resp.Code, ShouldEqual, 302)
})
assert.Equal(t, 302, sc.resp.Code)
})
middlewareScenario(t, "ReqSignIn true and unauthenticated API request", func(t *testing.T, sc *scenarioContext) {
sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/api/secure").exec()
assert.Equal(t, 401, sc.resp.Code)
})
t.Run("Anonymous auth enabled", func(t *testing.T) {
const orgID int64 = 1
origEnabled := setting.AnonymousEnabled
t.Cleanup(func() {
setting.AnonymousEnabled = origEnabled
})
middlewareScenario(t, "ReqSignIn true and unauthenticated API request", func(sc *scenarioContext) {
sc.m.Get("/api/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/api/secure").exec()
Convey("Should return 401", func() {
So(sc.resp.Code, ShouldEqual, 401)
})
origName := setting.AnonymousOrgName
t.Cleanup(func() {
setting.AnonymousOrgName = origName
})
setting.AnonymousEnabled = true
setting.AnonymousOrgName = "test"
Convey("Anonymous auth enabled", func() {
origEnabled := setting.AnonymousEnabled
t.Cleanup(func() {
setting.AnonymousEnabled = origEnabled
})
origName := setting.AnonymousOrgName
t.Cleanup(func() {
setting.AnonymousOrgName = origName
})
setting.AnonymousEnabled = true
setting.AnonymousOrgName = "test"
middlewareScenario(t, "ReqSignIn true and request with forceLogin in query string", func(
t *testing.T, sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
query.Result = &models.Org{Id: 1, Name: "test"}
query.Result = &models.Org{Id: orgID, Name: "test"}
return nil
})
middlewareScenario(t, "ReqSignIn true and request with forceLogin in query string", func(sc *scenarioContext) {
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure?forceLogin=true").exec()
sc.fakeReq("GET", "/secure?forceLogin=true").exec()
Convey("Should redirect to login", func() {
So(sc.resp.Code, ShouldEqual, 302)
location, ok := sc.resp.Header()["Location"]
So(ok, ShouldBeTrue)
So(location[0], ShouldEqual, "/login")
})
})
middlewareScenario(t, "ReqSignIn true and request with same org provided in query string", func(sc *scenarioContext) {
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure?orgId=1").exec()
Convey("Should not redirect to login", func() {
So(sc.resp.Code, ShouldEqual, 200)
})
})
middlewareScenario(t, "ReqSignIn true and request with different org provided in query string", func(sc *scenarioContext) {
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure?orgId=2").exec()
Convey("Should redirect to login", func() {
So(sc.resp.Code, ShouldEqual, 302)
location, ok := sc.resp.Header()["Location"]
So(ok, ShouldBeTrue)
So(location[0], ShouldEqual, "/login")
})
})
assert.Equal(t, 302, sc.resp.Code)
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, "/login", location[0])
})
Convey("snapshot public mode or signed in", func() {
middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func(sc *scenarioContext) {
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
So(sc.resp.Code, ShouldEqual, 401)
middlewareScenario(t, "ReqSignIn true and request with same org provided in query string", func(
t *testing.T, sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
query.Result = &models.Org{Id: orgID, Name: "test"}
return nil
})
middlewareScenario(t, "Snapshot public mode enabled and unauthenticated request should return 200", func(sc *scenarioContext) {
setting.SnapshotPublicMode = true
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", fmt.Sprintf("/secure?orgId=%d", orgID)).exec()
assert.Equal(t, 200, sc.resp.Code)
})
middlewareScenario(t, "ReqSignIn true and request with different org provided in query string", func(
t *testing.T, sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetOrgByNameQuery) error {
query.Result = &models.Org{Id: orgID, Name: "test"}
return nil
})
sc.m.Get("/secure", reqSignIn, sc.defaultHandler)
sc.fakeReq("GET", "/secure?orgId=2").exec()
assert.Equal(t, 302, sc.resp.Code)
location, ok := sc.resp.Header()["Location"]
assert.True(t, ok)
assert.Equal(t, "/login", location[0])
})
})
middlewareScenario(t, "Snapshot public mode disabled and unauthenticated request should return 401", func(
t *testing.T, sc *scenarioContext) {
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
assert.Equal(t, 401, sc.resp.Code)
})
middlewareScenario(t, "Snapshot public mode enabled and unauthenticated request should return 200", func(
t *testing.T, sc *scenarioContext) {
setting.SnapshotPublicMode = true
sc.m.Get("/api/snapshot", SnapshotPublicModeOrSignedIn(), sc.defaultHandler)
sc.fakeReq("GET", "/api/snapshot").exec()
assert.Equal(t, 200, sc.resp.Code)
})
}
+38 -41
View File
@@ -8,6 +8,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMiddlewareDashboardRedirect(t *testing.T) {
@@ -22,62 +24,57 @@ func TestMiddlewareDashboardRedirect(t *testing.T) {
fakeDash.HasAcl = false
fakeDash.Uid = util.GenerateShortUID()
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
middlewareScenario(t, "GET dashboard by legacy url", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
middlewareScenario(t, "GET dashboard by legacy url", func(sc *scenarioContext) {
sc.m.Get("/dashboard/db/:slug", redirectFromLegacyDashboardUrl, sc.defaultHandler)
sc.fakeReqWithParams("GET", "/dashboard/db/dash?orgId=1&panelId=2", map[string]string{}).exec()
Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
So(sc.resp.Code, ShouldEqual, 301)
resp := sc.resp.Result()
defer resp.Body.Close()
redirectURL, err := resp.Location()
So(err, ShouldBeNil)
So(redirectURL.Path, ShouldEqual, models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug))
So(len(redirectURL.Query()), ShouldEqual, 2)
})
assert.Equal(t, 301, sc.resp.Code)
resp := sc.resp.Result()
resp.Body.Close()
redirectURL, err := resp.Location()
require.NoError(t, err)
assert.Equal(t, models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug), redirectURL.Path)
assert.Equal(t, 2, len(redirectURL.Query()))
})
middlewareScenario(t, "GET dashboard solo by legacy url", func(sc *scenarioContext) {
middlewareScenario(t, "GET dashboard solo by legacy url", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("test", func(query *models.GetDashboardQuery) error {
query.Result = fakeDash
return nil
})
sc.m.Get("/dashboard-solo/db/:slug", redirectFromLegacyDashboardSoloUrl, sc.defaultHandler)
sc.fakeReqWithParams("GET", "/dashboard-solo/db/dash?orgId=1&panelId=2", map[string]string{}).exec()
Convey("Should redirect to new dashboard url with a 301 Moved Permanently", func() {
So(sc.resp.Code, ShouldEqual, 301)
resp := sc.resp.Result()
defer resp.Body.Close()
redirectURL, err := resp.Location()
So(err, ShouldBeNil)
expectedURL := models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1)
So(redirectURL.Path, ShouldEqual, expectedURL)
So(len(redirectURL.Query()), ShouldEqual, 2)
})
assert.Equal(t, 301, sc.resp.Code)
resp := sc.resp.Result()
resp.Body.Close()
redirectURL, err := resp.Location()
require.NoError(t, err)
expectedURL := models.GetDashboardUrl(fakeDash.Uid, fakeDash.Slug)
expectedURL = strings.Replace(expectedURL, "/d/", "/d-solo/", 1)
assert.Equal(t, expectedURL, redirectURL.Path)
assert.Equal(t, 2, len(redirectURL.Query()))
})
})
Convey("Given the dashboard legacy edit panel middleware", t, func() {
bus.ClearBusHandlers()
middlewareScenario(t, "GET dashboard by legacy edit url", func(t *testing.T, sc *scenarioContext) {
sc.m.Get("/d/:uid/:slug", RedirectFromLegacyPanelEditURL(), sc.defaultHandler)
middlewareScenario(t, "GET dashboard by legacy edit url", func(sc *scenarioContext) {
sc.m.Get("/d/:uid/:slug", RedirectFromLegacyPanelEditURL(), sc.defaultHandler)
sc.fakeReqWithParams("GET", "/d/asd/dash?orgId=1&panelId=12&fullscreen&edit", map[string]string{}).exec()
sc.fakeReqWithParams("GET", "/d/asd/dash?orgId=1&panelId=12&fullscreen&edit", map[string]string{}).exec()
Convey("Should redirect to new dashboard edit url with a 301 Moved Permanently", func() {
So(sc.resp.Code, ShouldEqual, 301)
resp := sc.resp.Result()
defer resp.Body.Close()
redirectURL, err := resp.Location()
So(err, ShouldBeNil)
So(redirectURL.String(), ShouldEqual, "/d/asd/d/asd/dash?editPanel=12&orgId=1")
})
})
assert.Equal(t, 301, sc.resp.Code)
resp := sc.resp.Result()
resp.Body.Close()
redirectURL, err := resp.Location()
require.NoError(t, err)
assert.Equal(t, "/d/asd/d/asd/dash?editPanel=12&orgId=1", redirectURL.String())
})
}
+10 -7
View File
@@ -54,10 +54,13 @@ func GetContextHandler(
Logger: log.New("context"),
}
orgId := int64(0)
orgIdHeader := ctx.Req.Header.Get("X-Grafana-Org-Id")
if orgIdHeader != "" {
orgId, _ = strconv.ParseInt(orgIdHeader, 10, 64)
orgID := int64(0)
orgIDHeader := ctx.Req.Header.Get("X-Grafana-Org-Id")
if orgIDHeader != "" {
orgIDParsed, err := strconv.ParseInt(orgIDHeader, 10, 64)
if err == nil {
orgID = orgIDParsed
}
}
// the order in which these are tested are important
@@ -68,9 +71,9 @@ func GetContextHandler(
switch {
case initContextWithRenderAuth(ctx, renderService):
case initContextWithApiKey(ctx):
case initContextWithBasicAuth(ctx, orgId):
case initContextWithAuthProxy(remoteCache, ctx, orgId):
case initContextWithToken(ats, ctx, orgId):
case initContextWithBasicAuth(ctx, orgID):
case initContextWithAuthProxy(remoteCache, ctx, orgID):
case initContextWithToken(ats, ctx, orgID):
case initContextWithAnonymousUser(ctx):
}
+100 -113
View File
@@ -4,149 +4,136 @@ import (
"encoding/json"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/bus"
authLogin "github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/login"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMiddlewareBasicAuth(t *testing.T) {
Convey("Given the basic auth", t, func() {
var oldBasicAuthEnabled = setting.BasicAuthEnabled
var oldDisableBruteForceLoginProtection = setting.DisableBruteForceLoginProtection
var id int64 = 12
var origBasicAuthEnabled = setting.BasicAuthEnabled
var origDisableBruteForceLoginProtection = setting.DisableBruteForceLoginProtection
t.Cleanup(func() {
setting.BasicAuthEnabled = origBasicAuthEnabled
setting.DisableBruteForceLoginProtection = origDisableBruteForceLoginProtection
})
setting.BasicAuthEnabled = true
setting.DisableBruteForceLoginProtection = true
Convey("Setup", func() {
setting.BasicAuthEnabled = true
setting.DisableBruteForceLoginProtection = true
bus.ClearBusHandlers()
bus.ClearBusHandlers()
const id int64 = 12
middlewareScenario(t, "Valid API key", func(t *testing.T, sc *scenarioContext) {
const orgID int64 = 2
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
require.NoError(t, err)
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
middlewareScenario(t, "Valid API key", func(sc *scenarioContext) {
var orgID int64 = 2
keyhash, err := util.EncodePassword("v5nAwpMafFP6znaS4urhdWDLS5511M42", "asd")
So(err, ShouldBeNil)
authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
bus.AddHandler("test", func(query *models.GetApiKeyByNameQuery) error {
query.Result = &models.ApiKey{OrgId: orgID, Role: models.ROLE_EDITOR, Key: keyhash}
return nil
})
assert.Equal(t, 200, sc.resp.Code)
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, orgID, sc.context.OrgId)
assert.Equal(t, models.ROLE_EDITOR, sc.context.OrgRole)
})
authHeader := util.GetBasicAuthHeader("api_key", "eyJrIjoidjVuQXdwTWFmRlA2em5hUzR1cmhkV0RMUzU1MTFNNDIiLCJuIjoiYXNkIiwiaWQiOjF9")
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
middlewareScenario(t, "Handle auth", func(t *testing.T, sc *scenarioContext) {
const password = "MyPass"
const salt = "Salt"
const orgID int64 = 2
Convey("Should return 200", func() {
So(sc.resp.Code, ShouldEqual, 200)
})
t.Cleanup(bus.ClearBusHandlers)
Convey("Should init middleware context", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.OrgId, ShouldEqual, orgID)
So(sc.context.OrgRole, ShouldEqual, models.ROLE_EDITOR)
})
bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error {
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
}
query.User = &models.User{
Password: encoded,
Salt: salt,
}
return nil
})
middlewareScenario(t, "Handle auth", func(sc *scenarioContext) {
var password = "MyPass"
var salt = "Salt"
var orgID int64 = 2
bus.AddHandler("grafana-auth", func(query *models.LoginUserQuery) error {
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
}
query.User = &models.User{
Password: encoded,
Salt: salt,
}
return nil
})
bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: orgID, UserId: id}
return nil
})
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should init middleware context with users", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.OrgId, ShouldEqual, orgID)
So(sc.context.UserId, ShouldEqual, id)
})
bus.ClearBusHandlers()
bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: orgID, UserId: id}
return nil
})
middlewareScenario(t, "Auth sequence", func(sc *scenarioContext) {
var password = "MyPass"
var salt = "Salt"
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
authLogin.Init()
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, orgID, sc.context.OrgId)
assert.Equal(t, id, sc.context.UserId)
})
bus.AddHandler("user-query", func(query *models.GetUserByLoginQuery) error {
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
}
query.Result = &models.User{
Password: encoded,
Id: id,
Salt: salt,
}
return nil
})
middlewareScenario(t, "Auth sequence", func(t *testing.T, sc *scenarioContext) {
const password = "MyPass"
const salt = "Salt"
bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{UserId: query.UserId}
return nil
})
login.Init()
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
Convey("Should init middleware context with user", func() {
So(sc.context.IsSignedIn, ShouldEqual, true)
So(sc.context.UserId, ShouldEqual, id)
})
bus.AddHandler("user-query", func(query *models.GetUserByLoginQuery) error {
encoded, err := util.EncodePassword(password, salt)
if err != nil {
return err
}
query.Result = &models.User{
Password: encoded,
Id: id,
Salt: salt,
}
return nil
})
middlewareScenario(t, "Should return error if user is not found", func(sc *scenarioContext) {
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("user", "password")
sc.exec()
err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson)
So(err, ShouldNotBeNil)
So(sc.resp.Code, ShouldEqual, 401)
So(sc.respJson["message"], ShouldEqual, errStringInvalidUsernamePassword)
bus.AddHandler("get-sign-user", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{UserId: query.UserId}
return nil
})
middlewareScenario(t, "Should return error if user & password do not match", func(sc *scenarioContext) {
bus.AddHandler("user-query", func(loginUserQuery *models.GetUserByLoginQuery) error {
return nil
})
authHeader := util.GetBasicAuthHeader("myUser", password)
sc.fakeReq("GET", "/").withAuthorizationHeader(authHeader).exec()
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("killa", "gorilla")
sc.exec()
assert.True(t, sc.context.IsSignedIn)
assert.Equal(t, id, sc.context.UserId)
})
err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson)
So(err, ShouldNotBeNil)
middlewareScenario(t, "Should return error if user is not found", func(t *testing.T, sc *scenarioContext) {
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("user", "password")
sc.exec()
So(sc.resp.Code, ShouldEqual, 401)
So(sc.respJson["message"], ShouldEqual, errStringInvalidUsernamePassword)
err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson)
require.Error(t, err)
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, errStringInvalidUsernamePassword, sc.respJson["message"])
})
middlewareScenario(t, "Should return error if user & password do not match", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("user-query", func(loginUserQuery *models.GetUserByLoginQuery) error {
return nil
})
Convey("Destroy", func() {
setting.BasicAuthEnabled = oldBasicAuthEnabled
setting.DisableBruteForceLoginProtection = oldDisableBruteForceLoginProtection
})
sc.fakeReq("GET", "/")
sc.req.SetBasicAuth("killa", "gorilla")
sc.exec()
err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson)
require.Error(t, err)
assert.Equal(t, 401, sc.resp.Code)
assert.Equal(t, errStringInvalidUsernamePassword, sc.respJson["message"])
})
}
File diff suppressed because it is too large Load Diff
+44 -50
View File
@@ -7,61 +7,55 @@ import (
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
)
func TestOrgRedirectMiddleware(t *testing.T) {
Convey("Can redirect to correct org", t, func() {
middlewareScenario(t, "when setting a correct org for the user", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
return nil
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
}
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
Convey("change org and redirect", func() {
So(sc.resp.Code, ShouldEqual, 302)
})
middlewareScenario(t, "when setting a correct org for the user", func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
return nil
})
middlewareScenario(t, "when setting an invalid org for user", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
Convey("not allowed to change org", func() {
So(sc.resp.Code, ShouldEqual, 404)
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 0,
UnhashedToken: "",
}, nil
}
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
assert.Equal(t, 302, sc.resp.Code)
})
middlewareScenario(t, "when setting an invalid org for user", func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.SetUsingOrgCommand) error {
return fmt.Errorf("")
})
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 1, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
sc.m.Get("/", sc.defaultHandler)
sc.fakeReq("GET", "/?orgId=3").exec()
assert.Equal(t, 404, sc.resp.Code)
})
}
+145 -114
View File
@@ -9,40 +9,40 @@ import (
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
)
func TestMiddlewareQuota(t *testing.T) {
Convey("Given the grafana quota middleware", t, func() {
setting.AnonymousEnabled = false
setting.Quota = setting.QuotaSettings{
Enabled: true,
Org: &setting.OrgQuota{
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
},
User: &setting.UserQuota{
Org: 5,
},
Global: &setting.GlobalQuota{
Org: 5,
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
Session: 5,
},
}
setting.AnonymousEnabled = false
setting.Quota = setting.QuotaSettings{
Enabled: true,
Org: &setting.OrgQuota{
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
},
User: &setting.UserQuota{
Org: 5,
},
Global: &setting.GlobalQuota{
Org: 5,
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
Session: 5,
},
}
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
qs := &quota.QuotaService{
AuthTokenService: fakeAuthTokenService,
}
QuotaFn := Quota(qs)
fakeAuthTokenService := auth.NewFakeUserAuthTokenService()
qs := &quota.QuotaService{
AuthTokenService: fakeAuthTokenService,
}
quotaFn := Quota(qs)
middlewareScenario(t, "with user not logged in", func(sc *scenarioContext) {
t.Run("With user not logged in", func(t *testing.T) {
middlewareScenario(t, "and global quota not reached", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
@@ -52,48 +52,12 @@ func TestMiddlewareQuota(t *testing.T) {
return nil
})
Convey("global quota not reached", func() {
sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("global quota reached", func() {
setting.Quota.Global.User = 4
sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 403)
})
Convey("global session quota not reached", func() {
setting.Quota.Global.Session = 10
sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
Convey("global session quota reached", func() {
setting.Quota.Global.Session = 1
sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
So(sc.resp.Code, ShouldEqual, 403)
})
sc.m.Get("/user", quotaFn("user"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
assert.Equal(t, 200, sc.resp.Code)
})
middlewareScenario(t, "with user logged in", func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
middlewareScenario(t, "and global quota reached", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
@@ -103,8 +67,20 @@ func TestMiddlewareQuota(t *testing.T) {
return nil
})
bus.AddHandler("userQuota", func(query *models.GetUserQuotaByTargetQuery) error {
query.Result = &models.UserQuotaDTO{
origUser := setting.Quota.Global.User
t.Cleanup(func() {
setting.Quota.Global.User = origUser
})
setting.Quota.Global.User = 4
sc.m.Get("/user", quotaFn("user"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
assert.Equal(t, 403, sc.resp.Code)
})
middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) {
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: 4,
@@ -112,57 +88,112 @@ func TestMiddlewareQuota(t *testing.T) {
return nil
})
bus.AddHandler("orgQuota", func(query *models.GetOrgQuotaByTargetQuery) error {
query.Result = &models.OrgQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: 4,
}
return nil
origSession := setting.Quota.Global.Session
t.Cleanup(func() {
setting.Quota.Global.Session = origSession
})
setting.Quota.Global.Session = 10
Convey("global datasource quota reached", func() {
setting.Quota.Global.DataSource = 4
sc.m.Get("/ds", QuotaFn("data_source"), sc.defaultHandler)
sc.fakeReq("GET", "/ds").exec()
So(sc.resp.Code, ShouldEqual, 403)
})
sc.m.Get("/user", quotaFn("session"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
assert.Equal(t, 200, sc.resp.Code)
})
Convey("user Org quota not reached", func() {
setting.Quota.User.Org = 5
sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
sc.fakeReq("GET", "/org").exec()
So(sc.resp.Code, ShouldEqual, 200)
middlewareScenario(t, "and global session quota reached", func(t *testing.T, sc *scenarioContext) {
origSession := setting.Quota.Global.Session
t.Cleanup(func() {
setting.Quota.Global.Session = origSession
})
setting.Quota.Global.Session = 1
Convey("user Org quota reached", func() {
setting.Quota.User.Org = 4
sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
sc.fakeReq("GET", "/org").exec()
So(sc.resp.Code, ShouldEqual, 403)
})
sc.m.Get("/user", quotaFn("session"), sc.defaultHandler)
sc.fakeReq("GET", "/user").exec()
assert.Equal(t, 403, sc.resp.Code)
})
})
Convey("org dashboard quota not reached", func() {
setting.Quota.Org.Dashboard = 10
sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
middlewareScenario(t, "with user logged in", func(t *testing.T, sc *scenarioContext) {
sc.withTokenSessionCookie("token")
bus.AddHandler("test", func(query *models.GetSignedInUserQuery) error {
query.Result = &models.SignedInUser{OrgId: 2, UserId: 12}
return nil
})
Convey("org dashboard quota reached", func() {
setting.Quota.Org.Dashboard = 4
sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec()
So(sc.resp.Code, ShouldEqual, 403)
})
sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*models.UserToken, error) {
return &models.UserToken{
UserId: 12,
UnhashedToken: "",
}, nil
}
Convey("org dashboard quota reached but quotas disabled", func() {
setting.Quota.Org.Dashboard = 4
setting.Quota.Enabled = false
sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec()
So(sc.resp.Code, ShouldEqual, 200)
})
bus.AddHandler("globalQuota", func(query *models.GetGlobalQuotaByTargetQuery) error {
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: 4,
}
return nil
})
bus.AddHandler("userQuota", func(query *models.GetUserQuotaByTargetQuery) error {
query.Result = &models.UserQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: 4,
}
return nil
})
bus.AddHandler("orgQuota", func(query *models.GetOrgQuotaByTargetQuery) error {
query.Result = &models.OrgQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: 4,
}
return nil
})
t.Run("global datasource quota reached", func(t *testing.T) {
setting.Quota.Global.DataSource = 4
sc.m.Get("/ds", quotaFn("data_source"), sc.defaultHandler)
sc.fakeReq("GET", "/ds").exec()
assert.Equal(t, 403, sc.resp.Code)
})
t.Run("user Org quota not reached", func(t *testing.T) {
setting.Quota.User.Org = 5
sc.m.Get("/org", quotaFn("org"), sc.defaultHandler)
sc.fakeReq("GET", "/org").exec()
assert.Equal(t, 200, sc.resp.Code)
})
t.Run("user Org quota reached", func(t *testing.T) {
setting.Quota.User.Org = 4
sc.m.Get("/org", quotaFn("org"), sc.defaultHandler)
sc.fakeReq("GET", "/org").exec()
assert.Equal(t, 403, sc.resp.Code)
})
t.Run("org dashboard quota not reached", func(t *testing.T) {
setting.Quota.Org.Dashboard = 10
sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec()
assert.Equal(t, 200, sc.resp.Code)
})
t.Run("org dashboard quota reached", func(t *testing.T) {
setting.Quota.Org.Dashboard = 4
sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec()
assert.Equal(t, 403, sc.resp.Code)
})
t.Run("org dashboard quota reached but quotas disabled", func(t *testing.T) {
setting.Quota.Org.Dashboard = 4
setting.Quota.Enabled = false
sc.m.Get("/dashboard", quotaFn("dashboard"), sc.defaultHandler)
sc.fakeReq("GET", "/dashboard").exec()
assert.Equal(t, 200, sc.resp.Code)
})
})
}
+3
View File
@@ -52,6 +52,9 @@ func stack(skip int) []byte {
// Print this much at least. If we can't find the source, it won't show.
fmt.Fprintf(buf, "%s:%d (0x%x)\n", file, line, pc)
if file != lastFile {
// We can ignore the gosec G304 warning on this one because `file`
// comes from the runtime.Caller() function.
// nolint:gosec
data, err := ioutil.ReadFile(file)
if err != nil {
continue
+21 -17
View File
@@ -2,6 +2,7 @@ package middleware
import (
"path/filepath"
"strings"
"testing"
"github.com/grafana/grafana/pkg/bus"
@@ -9,52 +10,55 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
macaron "gopkg.in/macaron.v1"
)
func TestRecoveryMiddleware(t *testing.T) {
setting.ErrTemplateName = "error-template"
Convey("Given an api route that panics", t, func() {
t.Run("Given an API route that panics", func(t *testing.T) {
apiURL := "/api/whatever"
recoveryScenario(t, "recovery middleware should return json", apiURL, func(sc *scenarioContext) {
sc.handlerFunc = PanicHandler
recoveryScenario(t, "recovery middleware should return json", apiURL, func(t *testing.T, sc *scenarioContext) {
sc.handlerFunc = panicHandler
sc.fakeReq("GET", apiURL).exec()
sc.req.Header.Add("content-type", "application/json")
So(sc.resp.Code, ShouldEqual, 500)
So(sc.respJson["message"], ShouldStartWith, "Internal Server Error - Check the Grafana server logs for the detailed error message.")
So(sc.respJson["error"], ShouldStartWith, "Server Error")
assert.Equal(t, 500, sc.resp.Code)
assert.Equal(t, "Internal Server Error - Check the Grafana server logs for the detailed error message.", sc.respJson["message"])
assert.True(t, strings.HasPrefix(sc.respJson["error"].(string), "Server Error"))
})
})
Convey("Given a non-api route that panics", t, func() {
t.Run("Given a non-API route that panics", func(t *testing.T) {
apiURL := "/whatever"
recoveryScenario(t, "recovery middleware should return html", apiURL, func(sc *scenarioContext) {
sc.handlerFunc = PanicHandler
recoveryScenario(t, "recovery middleware should return html", apiURL, func(t *testing.T, sc *scenarioContext) {
sc.handlerFunc = panicHandler
sc.fakeReq("GET", apiURL).exec()
So(sc.resp.Code, ShouldEqual, 500)
So(sc.resp.Header().Get("content-type"), ShouldEqual, "text/html; charset=UTF-8")
So(sc.resp.Body.String(), ShouldContainSubstring, "<title>Grafana - Error</title>")
assert.Equal(t, 500, sc.resp.Code)
assert.Equal(t, "text/html; charset=UTF-8", sc.resp.Header().Get("content-type"))
assert.True(t, strings.Contains(sc.resp.Body.String(), "<title>Grafana - Error</title>"))
})
})
}
func PanicHandler(c *models.ReqContext) {
func panicHandler(c *models.ReqContext) {
panic("Handler has panicked")
}
func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
Convey(desc, func() {
t.Run(desc, func(t *testing.T) {
defer bus.ClearBusHandlers()
sc := &scenarioContext{
t: t,
url: url,
}
viewsPath, _ := filepath.Abs("../../public/views")
viewsPath, err := filepath.Abs("../../public/views")
require.NoError(t, err)
sc.m = macaron.New()
sc.m.Use(Recovery())
@@ -81,6 +85,6 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
sc.m.Get(url, sc.defaultHandler)
fn(sc)
fn(t, sc)
})
}
+16 -5
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"gopkg.in/macaron.v1"
@@ -11,10 +12,11 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/setting"
"github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
type scenarioContext struct {
t *testing.T
m *macaron.Macaron
context *models.ReqContext
resp *httptest.ResponseRecorder
@@ -47,15 +49,19 @@ func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioC
}
func (sc *scenarioContext) fakeReq(method, url string) *scenarioContext {
sc.t.Helper()
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
convey.So(err, convey.ShouldBeNil)
require.NoError(sc.t, err)
sc.req = req
return sc
}
func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map[string]string) *scenarioContext {
sc.t.Helper()
sc.resp = httptest.NewRecorder()
req, err := http.NewRequest(method, url, nil)
q := req.URL.Query()
@@ -63,7 +69,7 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
q.Add(k, v)
}
req.URL.RawQuery = q.Encode()
convey.So(err, convey.ShouldBeNil)
require.NoError(sc.t, err)
sc.req = req
return sc
@@ -75,15 +81,20 @@ func (sc *scenarioContext) handler(fn handlerFunc) *scenarioContext {
}
func (sc *scenarioContext) exec() {
sc.t.Helper()
if sc.apiKey != "" {
sc.t.Logf(`Adding header "Authorization: Bearer %s"`, sc.apiKey)
sc.req.Header.Add("Authorization", "Bearer "+sc.apiKey)
}
if sc.authHeader != "" {
sc.t.Logf(`Adding header "Authorization: %s"`, sc.authHeader)
sc.req.Header.Add("Authorization", sc.authHeader)
}
if sc.tokenSessionCookie != "" {
sc.t.Log(`Adding cookie`, "name", setting.LoginCookieName, "value", sc.tokenSessionCookie)
sc.req.AddCookie(&http.Cookie{
Name: setting.LoginCookieName,
Value: sc.tokenSessionCookie,
@@ -94,9 +105,9 @@ func (sc *scenarioContext) exec() {
if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" {
err := json.NewDecoder(sc.resp.Body).Decode(&sc.respJson)
convey.So(err, convey.ShouldBeNil)
require.NoError(sc.t, err)
}
}
type scenarioFunc func(c *scenarioContext)
type scenarioFunc func(t *testing.T, c *scenarioContext)
type handlerFunc func(c *models.ReqContext)
+9 -2
View File
@@ -91,18 +91,25 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT
func loadPluginDashboard(pluginId, path string) (*models.Dashboard, error) {
plugin, exists := Plugins[pluginId]
if !exists {
return nil, PluginNotFoundError{pluginId}
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plugin.PluginDir` is based
// on plugin folder structure on disk and not user input. `path` comes from the
// `plugin.json` configuration file for the loaded plugin
dashboardFilePath := filepath.Join(plugin.PluginDir, path)
reader, err := os.Open(dashboardFilePath)
if err != nil {
return nil, err
}
defer reader.Close()
defer func() {
if err := reader.Close(); err != nil {
plog.Warn("Failed to close file", "path", dashboardFilePath, "err", err)
}
}()
data, err := simplejson.NewFromReader(reader)
if err != nil {
+12 -1
View File
@@ -87,6 +87,9 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
log.Debug("Getting signature state of plugin", "plugin", plugin.Id, "isBackend", plugin.Backend)
manifestPath := filepath.Join(plugin.PluginDir, "MANIFEST.txt")
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `manifestPath` is based
// on plugin the folder structure on disk and not user input.
byteValue, err := ioutil.ReadFile(manifestPath)
if err != nil || len(byteValue) < 10 {
log.Debug("Plugin is unsigned", "id", plugin.Id)
@@ -109,11 +112,19 @@ func getPluginSignatureState(log log.Logger, plugin *PluginBase) PluginSignature
for p, hash := range manifest.Files {
// Open the file
fp := filepath.Join(plugin.PluginDir, p)
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `fp` is based
// on the manifest file for a plugin and not user input.
f, err := os.Open(fp)
if err != nil {
return PluginSignatureModified
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
log.Warn("Failed to close plugin file", "path", fp, "err", err)
}
}()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
+22 -2
View File
@@ -269,11 +269,18 @@ func (pm *PluginManager) scan(pluginDir string, requireSigned bool) error {
}
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `jsonFPath` is based
// on plugin the folder structure on disk and not user input.
reader, err := os.Open(jsonFPath)
if err != nil {
return err
}
defer reader.Close()
defer func() {
if err := reader.Close(); err != nil {
scanner.log.Warn("Failed to close JSON file", "path", jsonFPath, "err", err)
}
}()
jsonParser := json.NewDecoder(reader)
@@ -328,6 +335,9 @@ func (s *PluginScanner) walker(currentPath string, f os.FileInfo, err error) err
return nil
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `currentPath` is based
// on plugin the folder structure on disk and not user input.
if err := s.loadPlugin(currentPath); err != nil {
s.log.Error("Failed to load plugin", "error", err, "pluginPath", filepath.Dir(currentPath))
s.errors = append(s.errors, err)
@@ -343,7 +353,11 @@ func (s *PluginScanner) loadPlugin(pluginJSONFilePath string) error {
if err != nil {
return err
}
defer reader.Close()
defer func() {
if err := reader.Close(); err != nil {
s.log.Warn("Failed to close JSON file", "path", pluginJSONFilePath, "err", err)
}
}()
jsonParser := json.NewDecoder(reader)
pluginCommon := PluginBase{}
@@ -463,6 +477,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
return nil, PluginNotFoundError{pluginId}
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
// on plugin the folder structure on disk and not user input.
path := filepath.Join(plug.PluginDir, fmt.Sprintf("%s.md", strings.ToUpper(name)))
exists, err := fs.Exists(path)
if err != nil {
@@ -480,6 +497,9 @@ func GetPluginMarkdown(pluginId string, name string) ([]byte, error) {
return make([]byte, 0), nil
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `plug.PluginDir` is based
// on plugin the folder structure on disk and not user input.
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
+77 -68
View File
@@ -30,94 +30,102 @@ func TestNotificationService(t *testing.T) {
}
evalCtx := NewEvalContext(context.Background(), testRule)
notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification", evalCtx, true, func(scenarioCtx *scenarioContext) {
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
notificationServiceScenario(t, "Given alert rule with upload image enabled should render and upload image and send notification",
evalCtx, true, func(sc *scenarioContext) {
err := sc.notificationService.SendIfNeeded(evalCtx)
require.NoError(sc.t, err)
require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but wasn't")
require.Equalf(t, 1, scenarioCtx.imageUploadCount, "expected image to be uploaded, but wasn't")
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
require.Equalf(sc.t, 1, sc.renderCount, "expected render to be called, but wasn't")
require.Equalf(sc.t, 1, sc.imageUploadCount, "expected image to be uploaded, but wasn't")
require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
notificationServiceScenario(t, "Given alert rule with upload image enabled but no renderer available should render and upload unavailable image and send notification", evalCtx, true, func(scenarioCtx *scenarioContext) {
scenarioCtx.rendererAvailable = false
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
notificationServiceScenario(t,
"Given alert rule with upload image enabled but no renderer available should render and upload unavailable image and send notification",
evalCtx, true, func(sc *scenarioContext) {
sc.rendererAvailable = false
err := sc.notificationService.SendIfNeeded(evalCtx)
require.NoError(sc.t, err)
require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but it wasn't")
require.Equalf(t, 1, scenarioCtx.imageUploadCount, "expected image to be uploaded, but it wasn't")
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
require.Equalf(sc.t, 1, sc.renderCount, "expected render to be called, but it wasn't")
require.Equalf(sc.t, 1, sc.imageUploadCount, "expected image to be uploaded, but it wasn't")
require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
notificationServiceScenario(t, "Given alert rule with upload image disabled should not render and upload image, but send notification", evalCtx, false, func(scenarioCtx *scenarioContext) {
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
notificationServiceScenario(
t, "Given alert rule with upload image disabled should not render and upload image, but send notification",
evalCtx, false, func(sc *scenarioContext) {
err := sc.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
require.Equalf(t, 0, scenarioCtx.renderCount, "expected render not to be called, but it was")
require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image not to be uploaded, but it was")
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
require.Equalf(sc.t, 0, sc.renderCount, "expected render not to be called, but it was")
require.Equalf(sc.t, 0, sc.imageUploadCount, "expected image not to be uploaded, but it was")
require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
notificationServiceScenario(t, "Given alert rule with upload image enabled and render times out should send notification", evalCtx, true, func(scenarioCtx *scenarioContext) {
setting.AlertingNotificationTimeout = 200 * time.Millisecond
scenarioCtx.renderProvider = func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) {
wait := make(chan bool)
notificationServiceScenario(t, "Given alert rule with upload image enabled and render times out should send notification",
evalCtx, true, func(sc *scenarioContext) {
setting.AlertingNotificationTimeout = 200 * time.Millisecond
sc.renderProvider = func(ctx context.Context, opts rendering.Opts) (*rendering.RenderResult, error) {
wait := make(chan bool)
go func() {
time.Sleep(1 * time.Second)
wait <- true
}()
go func() {
time.Sleep(1 * time.Second)
wait <- true
}()
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return nil, err
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return nil, err
}
break
case <-wait:
}
break
case <-wait:
return nil, nil
}
err := sc.notificationService.SendIfNeeded(evalCtx)
require.NoError(sc.t, err)
return nil, nil
}
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
require.Equalf(sc.t, 0, sc.renderCount, "expected render not to be called, but it was")
require.Equalf(sc.t, 0, sc.imageUploadCount, "expected image not to be uploaded, but it was")
require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
require.Equalf(t, 0, scenarioCtx.renderCount, "expected render not to be called, but it was")
require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image not to be uploaded, but it was")
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
notificationServiceScenario(t, "Given alert rule with upload image enabled and upload times out should send notification",
evalCtx, true, func(sc *scenarioContext) {
setting.AlertingNotificationTimeout = 200 * time.Millisecond
sc.uploadProvider = func(ctx context.Context, path string) (string, error) {
wait := make(chan bool)
notificationServiceScenario(t, "Given alert rule with upload image enabled and upload times out should send notification", evalCtx, true, func(scenarioCtx *scenarioContext) {
setting.AlertingNotificationTimeout = 200 * time.Millisecond
scenarioCtx.uploadProvider = func(ctx context.Context, path string) (string, error) {
wait := make(chan bool)
go func() {
time.Sleep(1 * time.Second)
wait <- true
}()
go func() {
time.Sleep(1 * time.Second)
wait <- true
}()
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return "", err
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
return "", err
}
break
case <-wait:
}
break
case <-wait:
return "", nil
}
err := sc.notificationService.SendIfNeeded(evalCtx)
require.NoError(sc.t, err)
return "", nil
}
err := scenarioCtx.notificationService.SendIfNeeded(evalCtx)
require.NoError(t, err)
require.Equalf(t, 1, scenarioCtx.renderCount, "expected render to be called, but wasn't")
require.Equalf(t, 0, scenarioCtx.imageUploadCount, "expected image not to be uploaded, but it was")
require.Truef(t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
require.Equalf(sc.t, 1, sc.renderCount, "expected render to be called, but wasn't")
require.Equalf(sc.t, 0, sc.imageUploadCount, "expected image not to be uploaded, but it was")
require.Truef(sc.t, evalCtx.Ctx.Value(notificationSent{}).(bool), "expected notification to be sent, but wasn't")
})
}
type scenarioContext struct {
t *testing.T
evalCtx *EvalContext
notificationService *notificationService
imageUploadCount int
@@ -175,6 +183,7 @@ func notificationServiceScenario(t *testing.T, name string, evalCtx *EvalContext
setting.AlertingNotificationTimeout = 30 * time.Second
scenarioCtx := &scenarioContext{
t: t,
evalCtx: evalCtx,
}
+18 -4
View File
@@ -2,6 +2,7 @@ package notifiers
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"os"
@@ -158,6 +159,9 @@ func (dn *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
}
func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `imagePath` comes
// from the alert `evalContext` that generates the images.
f, err := os.Open(imagePath)
if err != nil {
if os.IsNotExist(err) {
@@ -168,12 +172,20 @@ func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath str
return err
}
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
dn.log.Warn("Failed to close file", "path", imagePath, "err", err)
}
}()
var b bytes.Buffer
w := multipart.NewWriter(&b)
defer func() {
if err := w.Close(); err != nil {
// Should be OK since we already close it on non-error path
dn.log.Warn("Failed to close multipart writer", "err", err)
}
}()
fw, err := w.CreateFormField("payload_json")
if err != nil {
return err
@@ -192,7 +204,9 @@ func (dn *DiscordNotifier) embedImage(cmd *models.SendWebhookSync, imagePath str
return err
}
w.Close()
if err := w.Close(); err != nil {
return fmt.Errorf("failed to close multipart writer: %w", err)
}
cmd.Body = b.String()
cmd.ContentType = w.FormDataContentType()
+5 -1
View File
@@ -292,7 +292,11 @@ func (pn *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, m
if err != nil {
return nil, b, err
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
pn.log.Warn("Failed to close file", "path", evalContext.ImageOnDiskPath, "err", err)
}
}()
fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath)
if err != nil {
+30 -14
View File
@@ -321,7 +321,7 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
return err
}
if sn.Token != "" && sn.UploadImage {
err = slackFileUpload(evalContext, sn.log, "https://slack.com/api/files.upload", sn.Recipient, sn.Token)
err = sn.slackFileUpload(evalContext, sn.log, "https://slack.com/api/files.upload", sn.Recipient, sn.Token)
if err != nil {
return err
}
@@ -329,12 +329,14 @@ func (sn *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
return nil
}
func slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
func (sn *SlackNotifier) slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error {
if evalContext.ImageOnDiskPath == "" {
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `setting.HomePath` comes from Grafana's configuration file.
evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png")
}
log.Info("Uploading to slack via file.upload API")
headers, uploadBody, err := generateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
headers, uploadBody, err := sn.generateSlackBody(evalContext.ImageOnDiskPath, token, recipient)
if err != nil {
return err
}
@@ -346,37 +348,51 @@ func slackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url stri
return nil
}
func generateSlackBody(file string, token string, recipient string) (map[string]string, bytes.Buffer, error) {
func (sn *SlackNotifier) generateSlackBody(path string, token string, recipient string) (map[string]string, bytes.Buffer, error) {
// Slack requires all POSTs to files.upload to present
// an "application/x-www-form-urlencoded" encoded querystring
// See https://api.slack.com/methods/files.upload
var b bytes.Buffer
w := multipart.NewWriter(&b)
defer func() {
if err := w.Close(); err != nil {
// Shouldn't matter since we already close w explicitly on the non-error path
sn.log.Warn("Failed to close multipart writer", "err", err)
}
}()
// Add the generated image file
f, err := os.Open(file)
// We can ignore the gosec G304 warning on this one because `imagePath` comes
// from the alert `evalContext` that generates the images. `evalContext` in turn derives the root of the file
// path from configuration variables.
// nolint:gosec
f, err := os.Open(path)
if err != nil {
return nil, b, err
}
defer f.Close()
fw, err := w.CreateFormFile("file", file)
defer func() {
if err := f.Close(); err != nil {
sn.log.Warn("Failed to close file", "path", path, "err", err)
}
}()
fw, err := w.CreateFormFile("file", path)
if err != nil {
return nil, b, err
}
_, err = io.Copy(fw, f)
if err != nil {
if _, err := io.Copy(fw, f); err != nil {
return nil, b, err
}
// Add the authorization token
err = w.WriteField("token", token)
if err != nil {
if err := w.WriteField("token", token); err != nil {
return nil, b, err
}
// Add the channel(s) to POST to
err = w.WriteField("channels", recipient)
if err != nil {
if err := w.WriteField("channels", recipient); err != nil {
return nil, b, err
}
w.Close()
if err := w.Close(); err != nil {
return nil, b, fmt.Errorf("failed to close multipart writer: %w", err)
}
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
"Authorization": "auth_token=\"" + token + "\"",
+2
View File
@@ -95,6 +95,8 @@ func (server *Server) Dial() error {
if server.Config.RootCACert != "" {
certPool = x509.NewCertPool()
for _, caCertFile := range strings.Split(server.Config.RootCACert, " ") {
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `caCertFile` comes from ldap config.
pem, err := ioutil.ReadFile(caCertFile)
if err != nil {
return err
+2
View File
@@ -115,6 +115,8 @@ func readConfig(configFile string) (*Config, error) {
logger.Info("LDAP enabled, reading config file", "file", configFile)
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from grafana configuration file
fileBytes, err := ioutil.ReadFile(configFile)
if err != nil {
return nil, errutil.Wrap("Failed to load LDAP config file", err)
@@ -19,6 +19,9 @@ type configReader struct {
func (cr *configReader) parseConfigs(file os.FileInfo) ([]*config, error) {
filename, _ := filepath.Abs(filepath.Join(cr.path, file.Name()))
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
@@ -343,11 +343,17 @@ type dashboardJSONFile struct {
}
func (fr *FileReader) readDashboardFromFile(path string, lastModified time.Time, folderID int64) (*dashboardJSONFile, error) {
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `path` comes from the provisioning configuration file.
reader, err := os.Open(path)
if err != nil {
return nil, err
}
defer reader.Close()
defer func() {
if err := reader.Close(); err != nil {
fr.log.Warn("Failed to close file", "path", path, "err", err)
}
}()
all, err := ioutil.ReadAll(reader)
if err != nil {
@@ -49,6 +49,9 @@ func (cr *configReader) readConfig(path string) ([]*configs, error) {
func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*configs, error) {
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
@@ -61,6 +61,9 @@ func (cr *configReader) readConfig(path string) ([]*notificationsAsConfig, error
func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) (*notificationsAsConfig, error) {
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
@@ -69,6 +69,8 @@ func (cr *configReaderImpl) parsePluginConfig(path string, file os.FileInfo) (*p
return nil, err
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
+9 -1
View File
@@ -96,7 +96,12 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string,
if err != nil {
return nil, err
}
defer out.Close()
defer func() {
if err := out.Close(); err != nil {
// We already close the file explicitly in the non-error path, so shouldn't be a problem
rs.log.Warn("Failed to close file", "path", filePath, "err", err)
}
}()
_, err = io.Copy(out, resp.Body)
if err != nil {
// check that we didn't timeout while receiving the response.
@@ -107,6 +112,9 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, renderKey string,
rs.log.Error("Remote rendering request failed", "error", err)
return nil, fmt.Errorf("remote rendering request failed: %w", err)
}
if err := out.Close(); err != nil {
return nil, fmt.Errorf("failed to write to %q: %w", filePath, err)
}
return &RenderResult{FilePath: filePath}, err
}
@@ -5,56 +5,45 @@ import (
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
"github.com/stretchr/testify/require"
"xorm.io/xorm"
. "github.com/smartystreets/goconvey/convey"
)
func TestMigrations(t *testing.T) {
testDBs := []sqlutil.TestDB{
sqlutil.SQLite3TestDB(),
}
testDB := sqlutil.SQLite3TestDB()
const query = `select count(*) as count from migration_log`
result := struct{ Count int }{}
for _, testDB := range testDBs {
sql := `select count(*) as count from migration_log`
r := struct {
Count int64
}{}
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
require.NoError(t, err)
Convey("Initial "+testDB.DriverName+" migration", t, func() {
x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr)
So(err, ShouldBeNil)
err = NewDialect(x).CleanDB()
require.NoError(t, err)
err = NewDialect(x).CleanDB()
So(err, ShouldBeNil)
_, err = x.SQL(query).Get(&result)
require.Error(t, err)
_, err = x.SQL(sql).Get(&r)
So(err, ShouldNotBeNil)
mg := NewMigrator(x)
AddMigrations(mg)
expectedMigrations := mg.MigrationsCount()
mg := NewMigrator(x)
AddMigrations(mg)
err = mg.Start()
require.NoError(t, err)
err = mg.Start()
So(err, ShouldBeNil)
has, err := x.SQL(query).Get(&result)
require.NoError(t, err)
require.True(t, has)
has, err := x.SQL(sql).Get(&r)
So(err, ShouldBeNil)
So(has, ShouldBeTrue)
// we currently skip to migrations. We should rewrite skipped migrations to write in the log as well.
// until then we have to keep this
expectedMigrations := mg.MigrationsCount()
So(r.Count, ShouldEqual, expectedMigrations)
require.Equal(t, expectedMigrations, result.Count)
mg = NewMigrator(x)
AddMigrations(mg)
mg = NewMigrator(x)
AddMigrations(mg)
err = mg.Start()
So(err, ShouldBeNil)
err = mg.Start()
require.NoError(t, err)
has, err = x.SQL(sql).Get(&r)
So(err, ShouldBeNil)
So(has, ShouldBeTrue)
So(r.Count, ShouldEqual, expectedMigrations)
})
}
has, err = x.SQL(query).Get(&result)
require.NoError(t, err)
require.True(t, has)
require.Equal(t, expectedMigrations, result.Count)
}
+2
View File
@@ -138,6 +138,8 @@ func (e fileExpander) Expand(s string) (string, error) {
return "", err
}
// nolint:gosec
// We can ignore the gosec G304 warning on this one because `s` comes from configuration section keys
f, err := ioutil.ReadFile(s)
if err != nil {
return "", err
+77 -108
View File
@@ -4,156 +4,125 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/components/simplejson"
p "github.com/prometheus/common/model"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestPrometheus(t *testing.T) {
Convey("Prometheus", t, func() {
dsInfo := &models.DataSource{
JsonData: simplejson.New(),
dsInfo := &models.DataSource{
JsonData: simplejson.New(),
}
t.Run("converting metric name", func(t *testing.T) {
metric := map[p.LabelName]p.LabelValue{
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
Convey("converting metric name", func() {
metric := map[p.LabelName]p.LabelValue{
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
query := &PrometheusQuery{
LegendFormat: "legend {{app}} {{ device }} {{broken}}",
}
query := &PrometheusQuery{
LegendFormat: "legend {{app}} {{ device }} {{broken}}",
}
require.Equal(t, "legend backend mobile ", formatLegend(metric, query))
})
So(formatLegend(metric, query), ShouldEqual, "legend backend mobile ")
})
t.Run("build full series name", func(t *testing.T) {
metric := map[p.LabelName]p.LabelValue{
p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
Convey("build full series name", func() {
metric := map[p.LabelName]p.LabelValue{
p.LabelName(p.MetricNameLabel): p.LabelValue("http_request_total"),
p.LabelName("app"): p.LabelValue("backend"),
p.LabelName("device"): p.LabelValue("mobile"),
}
query := &PrometheusQuery{
LegendFormat: "",
}
query := &PrometheusQuery{
LegendFormat: "",
}
require.Equal(t, `http_request_total{app="backend", device="mobile"}`, formatLegend(metric, query))
})
So(formatLegend(metric, query), ShouldEqual, `http_request_total{app="backend", device="mobile"}`)
})
Convey("parsing query model with step", func() {
json := `{
t.Run("parsing query model with step", func(t *testing.T) {
json := `{
"expr": "go_goroutines",
"format": "time_series",
"refId": "A"
}`
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
Convey("with 48h time range", func() {
queryContext.TimeRange = tsdb.NewTimeRange("12h", "now")
queryContext.TimeRange = tsdb.NewTimeRange("12h", "now")
models, err := parseQuery(dsInfo, queryModels, queryContext)
So(err, ShouldBeNil)
models, err := parseQuery(dsInfo, queryModels, queryContext)
require.NoError(t, err)
require.Equal(t, time.Second*30, models[0].Step)
})
model := models[0]
So(model.Step, ShouldEqual, time.Second*30)
})
})
Convey("parsing query model without step parameter", func() {
json := `{
t.Run("parsing query model without step parameter", func(t *testing.T) {
json := `{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}`
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
Convey("with 48h time range", func() {
queryContext.TimeRange = tsdb.NewTimeRange("48h", "now")
queryContext.TimeRange = tsdb.NewTimeRange("48h", "now")
models, err := parseQuery(dsInfo, queryModels, queryContext)
require.NoError(t, err)
require.Equal(t, time.Minute*2, models[0].Step)
models, err := parseQuery(dsInfo, queryModels, queryContext)
queryContext.TimeRange = tsdb.NewTimeRange("1h", "now")
models, err = parseQuery(dsInfo, queryModels, queryContext)
require.NoError(t, err)
require.Equal(t, time.Second*15, models[0].Step)
})
So(err, ShouldBeNil)
model := models[0]
So(model.Step, ShouldEqual, time.Minute*2)
})
Convey("with 1h time range", func() {
queryContext.TimeRange = tsdb.NewTimeRange("1h", "now")
models, err := parseQuery(dsInfo, queryModels, queryContext)
So(err, ShouldBeNil)
model := models[0]
So(model.Step, ShouldEqual, time.Second*15)
})
})
Convey("parsing query model with intervalFactor", func() {
Convey("high intervalFactor", func() {
json := `{
t.Run("parsing query model with high intervalFactor", func(t *testing.T) {
json := `{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 10,
"refId": "A"
}`
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
Convey("with 48h time range", func() {
queryContext.TimeRange = tsdb.NewTimeRange("48h", "now")
queryContext.TimeRange = tsdb.NewTimeRange("48h", "now")
models, err := parseQuery(dsInfo, queryModels, queryContext)
models, err := parseQuery(dsInfo, queryModels, queryContext)
require.NoError(t, err)
require.Equal(t, time.Minute*20, models[0].Step)
})
So(err, ShouldBeNil)
model := models[0]
So(model.Step, ShouldEqual, time.Minute*20)
})
})
Convey("low intervalFactor", func() {
json := `{
t.Run("parsing query model with low intervalFactor", func(t *testing.T) {
json := `{
"expr": "go_goroutines",
"format": "time_series",
"intervalFactor": 1,
"refId": "A"
}`
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
jsonModel, _ := simplejson.NewJson([]byte(json))
queryContext := &tsdb.TsdbQuery{}
queryModels := []*tsdb.Query{
{Model: jsonModel},
}
Convey("with 48h time range", func() {
queryContext.TimeRange = tsdb.NewTimeRange("48h", "now")
queryContext.TimeRange = tsdb.NewTimeRange("48h", "now")
models, err := parseQuery(dsInfo, queryModels, queryContext)
So(err, ShouldBeNil)
model := models[0]
So(model.Step, ShouldEqual, time.Minute*2)
})
})
})
models, err := parseQuery(dsInfo, queryModels, queryContext)
require.NoError(t, err)
require.Equal(t, time.Minute*2, models[0].Step)
})
}
+39 -43
View File
@@ -6,14 +6,14 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey"
"github.com/stretchr/testify/require"
)
func TestTestdataScenarios(t *testing.T) {
Convey("random walk ", t, func() {
t.Run("random walk ", func(t *testing.T) {
scenario := ScenarioRegistry["random_walk"]
Convey("Should start at the requested value", func() {
t.Run("Should start at the requested value", func(t *testing.T) {
req := &tsdb.TsdbQuery{
TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()),
Queries: []*tsdb.Query{
@@ -24,17 +24,17 @@ func TestTestdataScenarios(t *testing.T) {
query.Model.Set("startValue", 1.234)
result := scenario.Handler(req.Queries[0], req)
points := result.Series[0].Points
require.NotNil(t, result.Series)
So(result.Series, ShouldNotBeNil)
So(points[0][0].Float64, ShouldEqual, 1.234)
points := result.Series[0].Points
require.Equal(t, 1.234, points[0][0].Float64)
})
})
Convey("random walk table", t, func() {
t.Run("random walk table", func(t *testing.T) {
scenario := ScenarioRegistry["random_walk_table"]
Convey("Should return a table that looks like value/min/max", func() {
t.Run("Should return a table that looks like value/min/max", func(t *testing.T) {
req := &tsdb.TsdbQuery{
TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()),
Queries: []*tsdb.Query{
@@ -45,18 +45,18 @@ func TestTestdataScenarios(t *testing.T) {
result := scenario.Handler(req.Queries[0], req)
table := result.Tables[0]
So(len(table.Rows), ShouldBeGreaterThan, 50)
require.Greater(t, len(table.Rows), 50)
for _, row := range table.Rows {
value := row[1]
min := row[2]
max := row[3]
So(min, ShouldBeLessThan, value)
So(max, ShouldBeGreaterThan, value)
require.Less(t, min, value)
require.Greater(t, max, value)
}
})
Convey("Should return a table with some nil values", func() {
t.Run("Should return a table with some nil values", func(t *testing.T) {
req := &tsdb.TsdbQuery{
TimeRange: tsdb.NewFakeTimeRange("5m", "now", time.Now()),
Queries: []*tsdb.Query{
@@ -73,7 +73,7 @@ func TestTestdataScenarios(t *testing.T) {
nil2 := false
nil3 := false
So(len(table.Rows), ShouldBeGreaterThan, 50)
require.Greater(t, len(table.Rows), 50)
for _, row := range table.Rows {
if row[1] == nil {
nil1 = true
@@ -86,41 +86,37 @@ func TestTestdataScenarios(t *testing.T) {
}
}
So(nil1, ShouldBeTrue)
So(nil2, ShouldBeTrue)
So(nil3, ShouldBeTrue)
require.True(t, nil1)
require.True(t, nil2)
require.True(t, nil3)
})
})
}
func TestToLabels(t *testing.T) {
Convey("read labels", t, func() {
tags := make(map[string]string)
tags["job"] = "foo"
tags["instance"] = "bar"
func TestParseLabels(t *testing.T) {
expectedTags := map[string]string{
"job": "foo",
"instance": "bar",
}
query1 := tsdb.Query{
Model: simplejson.NewFromAny(map[string]interface{}{
"labels": `{job="foo", instance="bar"}`,
}),
}
query1 := tsdb.Query{
Model: simplejson.NewFromAny(map[string]interface{}{
"labels": `{job="foo", instance="bar"}`,
}),
}
require.Equal(t, expectedTags, parseLabels(&query1))
So(parseLabels(&query1), ShouldResemble, tags)
query2 := tsdb.Query{
Model: simplejson.NewFromAny(map[string]interface{}{
"labels": `job=foo, instance=bar`,
}),
}
require.Equal(t, expectedTags, parseLabels(&query2))
query2 := tsdb.Query{
Model: simplejson.NewFromAny(map[string]interface{}{
"labels": `job=foo, instance=bar`,
}),
}
So(parseLabels(&query2), ShouldResemble, tags)
query3 := tsdb.Query{
Model: simplejson.NewFromAny(map[string]interface{}{
"labels": `job = foo,instance = bar`,
}),
}
So(parseLabels(&query3), ShouldResemble, tags)
})
query3 := tsdb.Query{
Model: simplejson.NewFromAny(map[string]interface{}{
"labels": `job = foo,instance = bar`,
}),
}
require.Equal(t, expectedTags, parseLabels(&query3))
}
@@ -47,7 +47,7 @@ export const ChangePassword: FC<Props> = ({ onSubmit, onSkip }) => {
{onSkip && (
<Tooltip
content="If you skip you will be prompted to change password next time you login."
content="If you skip you will be prompted to change password next time you log in."
placement="bottom"
>
<LinkButton variant="link" onClick={onSkip} aria-label={selectors.pages.Login.skip}>
@@ -7,7 +7,7 @@ exports[`Render should render component 1`] = `
<div
className="gf-form gf-form--grow"
>
<Component
<FilterInput
inputClassName="gf-form-input width-20"
labelClassName="gf-form--has-input-icon"
onChange={[MockFunction]}
@@ -57,7 +57,7 @@ describe('CustomEndpointTransport', () => {
expect(fetchSpy).toHaveBeenCalledTimes(1);
// wait out the retry-after and call again - great success
await new Promise(resolve => setTimeout(() => resolve(), 1001));
await new Promise(resolve => setTimeout(() => resolve(null), 1001));
await expect(transport.sendEvent(event)).resolves.toBeTruthy();
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
@@ -13,7 +13,7 @@ exports[`Render should render alert rules 1`] = `
<div
className="gf-form gf-form--grow"
>
<Component
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
@@ -145,7 +145,7 @@ exports[`Render should render component 1`] = `
<div
className="gf-form gf-form--grow"
>
<Component
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<Component
<LoadingPlaceholder
text="Evaluating rule"
/>
`;
@@ -43,7 +43,7 @@ exports[`Render should render CTA if there are no API keys 1`] = `
proTip="Remember you can provide view-only API access to other applications."
title="You haven't added any API Keys yet."
/>
<Component
<SlideDown
in={false}
>
<div
@@ -126,11 +126,11 @@ exports[`Render should render CTA if there are no API keys 1`] = `
<div
className="gf-form max-width-21"
>
<Component
<FormLabel
tooltip="The api key life duration. For example 1d if your key is going to last for one day. All the supported units are: s,m,h,d,w,M,y"
>
Time to live
</Component>
</FormLabel>
<Input
className="gf-form-input"
onChange={[Function]}
@@ -161,7 +161,7 @@ exports[`Render should render CTA if there are no API keys 1`] = `
</div>
</form>
</div>
</Component>
</SlideDown>
</PageContents>
</Page>
`;
@@ -15,7 +15,7 @@ exports[`Render should render component 1`] = `
onPasteCopiedPanel={[Function]}
/>
<div>
<Component
<HorizontalGroup
justify="center"
>
<Button
@@ -25,7 +25,7 @@ exports[`Render should render component 1`] = `
>
Convert to row
</Button>
</Component>
</HorizontalGroup>
</div>
</div>
</div>
@@ -33,7 +33,7 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
// eslint-disable-next-line react/display-name
const renderLabel = (includeDescription = true, includeCounter = false) => (isExpanded = false) => (
<HorizontalGroup justify="space-between">
<Label description={includeDescription ? item.description : undefined}>
<Label category={item.category?.splice(1)} description={includeDescription ? item.description : undefined}>
{item.name}
{!isExpanded && includeCounter && item.getItemsCount && <Counter value={item.getItemsCount(property.value)} />}
</Label>
@@ -98,8 +98,12 @@ export const OverrideEditor: React.FC<OverrideEditorProps> = ({
);
let configPropertiesOptions = registry.list().map(item => {
let label = item.name;
if (item.category && item.category.length > 1) {
label = [...item.category!.slice(1), item.name].join(' > ');
}
return {
label: item.name,
label,
value: item.id,
description: item.description,
};
@@ -337,22 +337,22 @@ exports[`DashboardPage Dashboard is fetching slowly Should render slow init stat
<div
className="dashboard-loading__text"
>
<Component
<VerticalGroup
spacing="md"
>
<Component
<HorizontalGroup
align="center"
justify="center"
spacing="xs"
>
<Component
<Spinner
inline={true}
/>
Fetching
</Component>
</HorizontalGroup>
<Component
<HorizontalGroup
align="center"
justify="center"
>
@@ -364,8 +364,8 @@ exports[`DashboardPage Dashboard is fetching slowly Should render slow init stat
>
Cancel loading dashboard
</Button>
</Component>
</Component>
</HorizontalGroup>
</VerticalGroup>
</div>
</div>
`;
@@ -1,6 +1,5 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { isEqual } from 'lodash';
import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice, ScopedVars } from '@grafana/data';
import { AngularComponent, config, getTemplateSrv } from '@grafana/runtime';
import { ClickOutsideWrapper, Icon, IconName, Tooltip, stylesFactory } from '@grafana/ui';
@@ -52,8 +51,8 @@ export class PanelHeader extends PureComponent<Props, State> {
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
return {
x: event.clientX,
y: event.clientY,
x: Math.floor(event.clientX),
y: Math.floor(event.clientY),
};
};
@@ -62,7 +61,7 @@ export class PanelHeader extends PureComponent<Props, State> {
};
isClick = (clickCoordinates: ClickCoordinates) => {
return isEqual(clickCoordinates, this.clickCoordinates);
return clickCoordinates.x === this.clickCoordinates.x && clickCoordinates.y === this.clickCoordinates.y;
};
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -16,11 +16,11 @@ exports[`Render should render component 1`] = `
}
}
>
<Component
<FormLabel
tooltip="The name is used when you select the data source in panels. The Default data source is preselected in new panels."
>
Name
</Component>
</FormLabel>
<Input
aria-label="Data source settings page name input field"
className="gf-form-input max-width-23"
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ErrorContainer should render component 1`] = `
<Component
<FadeIn
duration={100}
in={true}
>
@@ -29,5 +29,5 @@ exports[`ErrorContainer should render component 1`] = `
</div>
</div>
</div>
</Component>
</FadeIn>
`;
@@ -37,7 +37,7 @@ exports[`Explore should render component 1`] = `
richHistoryButtonActive={false}
/>
</div>
<Component
<ErrorContainer
queryError={Object {}}
/>
<AutoSizer
+2 -2
View File
@@ -9,12 +9,12 @@ export abstract class GrafanaLiveScope {
/**
* Load the real namespaces
*/
abstract async getChannelSupport(namespace: string): Promise<LiveChannelSupport | undefined>;
abstract getChannelSupport(namespace: string): Promise<LiveChannelSupport | undefined>;
/**
* List the possible values within this scope
*/
abstract async listNamespaces(): Promise<Array<SelectableValue<string>>>;
abstract listNamespaces(): Promise<Array<SelectableValue<string>>>;
}
export interface CoreGrafanaLiveFeature {
@@ -14,7 +14,7 @@ export const getDashboardNavModel = (state: StoreState): NavModel => {
}
// This needs to be copied to avoid mutating the store in a selector
nav.main.children = [...navModel.main.children];
nav.main.children = [...(navModel.main.children ?? [])];
for (const item of nav.main.children) {
item.active = false;
@@ -35,7 +35,7 @@ exports[`Render should render organization and preferences 1`] = `
<PageContents
isLoading={false}
>
<Component>
<VerticalGroup>
<OrgProfile
onSubmit={[Function]}
orgName="Cool org"
@@ -43,7 +43,7 @@ exports[`Render should render organization and preferences 1`] = `
<SharedPreferences
resourceUri="org"
/>
</Component>
</VerticalGroup>
</PageContents>
</Page>
`;
@@ -44,7 +44,7 @@ export class PluginDashboards extends PureComponent<Props, State> {
const { dashboards } = this.state;
return this.import(dashboards[index], true).then(() => {
if (index + 1 < dashboards.length) {
return new Promise(resolve => {
return new Promise<void>(resolve => {
setTimeout(() => {
this.importNext(index + 1).then(() => {
resolve();
@@ -171,7 +171,6 @@ export class QueryGroupOptionsEditor extends PureComponent<Props, State> {
type="text"
className="width-6"
placeholder="60"
name={name}
spellCheck={false}
onBlur={this.onCacheTimeoutBlur}
defaultValue={options.cacheTimeout ?? ''}
@@ -205,7 +204,6 @@ export class QueryGroupOptionsEditor extends PureComponent<Props, State> {
type="number"
className="width-6"
placeholder={`${realMd}`}
name={name}
spellCheck={false}
onBlur={this.onMaxDataPointsBlur}
defaultValue={value}
@@ -246,7 +244,6 @@ export class QueryGroupOptionsEditor extends PureComponent<Props, State> {
type="text"
className="width-6"
placeholder={`${minIntervalOnDs}`}
name={name}
spellCheck={false}
onBlur={this.onMinIntervalBlur}
defaultValue={options.minInterval ?? ''}
@@ -23,7 +23,7 @@ exports[`Render should render component 1`] = `
className="page-action-bar__spacer"
/>
</div>
<Component
<SlideDown
in={false}
>
<div
@@ -68,7 +68,7 @@ exports[`Render should render component 1`] = `
</div>
</form>
</div>
</Component>
</SlideDown>
<EmptyListCTA
buttonIcon="users-alt"
buttonTitle="Add Group"
@@ -114,7 +114,7 @@ exports[`Render should render groups table 1`] = `
Add group
</button>
</div>
<Component
<SlideDown
in={false}
>
<div
@@ -159,7 +159,7 @@ exports[`Render should render groups table 1`] = `
</div>
</form>
</div>
</Component>
</SlideDown>
<div
className="admin-list-table"
>
@@ -41,7 +41,7 @@ exports[`Render should render teams table 1`] = `
<div
className="gf-form gf-form--grow"
>
<Component
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
@@ -132,7 +132,7 @@ exports[`Render should render teams table 1`] = `
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -184,7 +184,7 @@ exports[`Render should render teams table 1`] = `
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -236,7 +236,7 @@ exports[`Render should render teams table 1`] = `
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -288,7 +288,7 @@ exports[`Render should render teams table 1`] = `
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -340,7 +340,7 @@ exports[`Render should render teams table 1`] = `
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -376,7 +376,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
<div
className="gf-form gf-form--grow"
>
<Component
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
@@ -467,7 +467,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
<td
className="text-right"
>
<Component
<DeleteButton
disabled={true}
onConfirm={[Function]}
size="sm"
@@ -503,7 +503,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
<div
className="gf-form gf-form--grow"
>
<Component
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
@@ -594,7 +594,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on and signedin us
<td
className="text-right"
>
<Component
<DeleteButton
disabled={true}
onConfirm={[Function]}
size="sm"
@@ -21,7 +21,7 @@ exports[`Render should render team members when sync enabled 1`] = `
<td>
testName
</td>
<Component
<WithFeatureToggle
featureToggle={false}
>
<td
@@ -35,7 +35,7 @@ exports[`Render should render team members when sync enabled 1`] = `
</span>
</div>
</td>
</Component>
</WithFeatureToggle>
<td>
<TagBadge
count={0}
@@ -48,7 +48,7 @@ exports[`Render should render team members when sync enabled 1`] = `
<td
className="text-right"
>
<Component
<DeleteButton
disabled={true}
onConfirm={[Function]}
size="sm"
@@ -78,7 +78,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
<td>
testName
</td>
<Component
<WithFeatureToggle
featureToggle={false}
>
<td
@@ -134,11 +134,11 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
/>
</div>
</td>
</Component>
</WithFeatureToggle>
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -168,7 +168,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
<td>
testName
</td>
<Component
<WithFeatureToggle
featureToggle={true}
>
<td
@@ -224,11 +224,11 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
/>
</div>
</td>
</Component>
</WithFeatureToggle>
<td
className="text-right"
>
<Component
<DeleteButton
disabled={false}
onConfirm={[Function]}
size="sm"
@@ -258,7 +258,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render s
<td>
testName
</td>
<Component
<WithFeatureToggle
featureToggle={true}
>
<td
@@ -272,11 +272,11 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render s
</span>
</div>
</td>
</Component>
</WithFeatureToggle>
<td
className="text-right"
>
<Component
<DeleteButton
disabled={true}
onConfirm={[Function]}
size="sm"

Some files were not shown because too many files have changed in this diff Show More