mirror of
https://github.com/grafana/grafana.git
synced 2025-12-23 21:34:21 +08:00
Compare commits
87 Commits
zoltan/pos
...
v6.6.x
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fa63cfc34 | ||
|
|
f416c57743 | ||
|
|
4127f8374c | ||
|
|
7163726493 | ||
|
|
daa8035c48 | ||
|
|
448cf2a74d | ||
|
|
b7f77459c4 | ||
|
|
4d02406d6d | ||
|
|
b2034d693d | ||
|
|
95e304b277 | ||
|
|
1fb7155a58 | ||
|
|
7656e72df9 | ||
|
|
61ac3085de | ||
|
|
d016a08303 | ||
|
|
d2ab56961f | ||
|
|
c0cfcc2b32 | ||
|
|
90e7909cef | ||
|
|
23f4477cfd | ||
|
|
5f42c258a5 | ||
|
|
5f786b0824 | ||
|
|
5071be541b | ||
|
|
b6564b85b1 | ||
|
|
4770126073 | ||
|
|
21bf8b71bc | ||
|
|
dcb8beecb1 | ||
|
|
64568a1938 | ||
|
|
9b3241a629 | ||
|
|
4bc6bf5e54 | ||
|
|
828ba74674 | ||
|
|
568bbf4ff7 | ||
|
|
17fc5251e1 | ||
|
|
b1d3fec9a8 | ||
|
|
12d3576666 | ||
|
|
476f9b6224 | ||
|
|
29c6fa4114 | ||
|
|
248f73a00f | ||
|
|
4fa2e9b90a | ||
|
|
04c2e41733 | ||
|
|
4c21a1e016 | ||
|
|
604a603e82 | ||
|
|
9dd964f503 | ||
|
|
338c2b738e | ||
|
|
5bc6a3505d | ||
|
|
3b5efdbc84 | ||
|
|
a3cea78f40 | ||
|
|
36f02aaef7 | ||
|
|
2aefb73876 | ||
|
|
94c374d187 | ||
|
|
6eb60b943a | ||
|
|
8c14a6e070 | ||
|
|
a069b5d639 | ||
|
|
49255fbb6a | ||
|
|
0c843ae8d2 | ||
|
|
52a5645c85 | ||
|
|
9ad66b7fed | ||
|
|
af10ba3f1f | ||
|
|
5c11bbdfb4 | ||
|
|
872bc2d973 | ||
|
|
cbace87b56 | ||
|
|
f59b9b6545 | ||
|
|
3ac81e50d7 | ||
|
|
0378c66dcd | ||
|
|
79de911d0a | ||
|
|
eecd09d1c8 | ||
|
|
14ae363aaa | ||
|
|
18a92cc540 | ||
|
|
cfb8912200 | ||
|
|
47c57a1b9d | ||
|
|
ce3f43c6d0 | ||
|
|
6717d43921 | ||
|
|
a951bab782 | ||
|
|
841e140f5b | ||
|
|
bbd2014e9d | ||
|
|
8ce48b98dc | ||
|
|
60419f7e72 | ||
|
|
38e4db88d1 | ||
|
|
d619c529f0 | ||
|
|
a8643d89be | ||
|
|
a7c52c7dc8 | ||
|
|
7ad14532a3 | ||
|
|
23f977f000 | ||
|
|
c172fe8915 | ||
|
|
ddeee1820d | ||
|
|
f28fd41c3b | ||
|
|
57fb967fec | ||
|
|
9046263122 | ||
|
|
2306826cff |
@@ -125,7 +125,7 @@ jobs:
|
|||||||
- node_modules
|
- node_modules
|
||||||
- run:
|
- run:
|
||||||
name: run end-to-end tests
|
name: run end-to-end tests
|
||||||
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests:ci'
|
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests'
|
||||||
no_output_timeout: 5m
|
no_output_timeout: 5m
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: public/e2e-tests/screenShots/theTruth
|
path: public/e2e-tests/screenShots/theTruth
|
||||||
@@ -167,7 +167,7 @@ jobs:
|
|||||||
- node_modules
|
- node_modules
|
||||||
- run:
|
- run:
|
||||||
name: run end-to-end tests
|
name: run end-to-end tests
|
||||||
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests:ci'
|
command: 'env BASE_URL=http://127.0.0.1:3000 yarn e2e-tests'
|
||||||
no_output_timeout: 5m
|
no_output_timeout: 5m
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: public/e2e-tests/screenShots/theTruth
|
path: public/e2e-tests/screenShots/theTruth
|
||||||
@@ -419,6 +419,12 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: build grafana frontend
|
name: build grafana frontend
|
||||||
command: './scripts/build/build.sh --fast --frontend-only'
|
command: './scripts/build/build.sh --fast --frontend-only'
|
||||||
|
- run:
|
||||||
|
name: es-check install
|
||||||
|
command: 'yarn global add es-check'
|
||||||
|
- run:
|
||||||
|
name: es-check run
|
||||||
|
command: 'es-check es5 ./public/build/*.js'
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: frontend-dependency-cache-{{ checksum "yarn.lock" }}
|
key: frontend-dependency-cache-{{ checksum "yarn.lock" }}
|
||||||
paths:
|
paths:
|
||||||
@@ -1211,6 +1217,7 @@ workflows:
|
|||||||
- shellcheck
|
- shellcheck
|
||||||
- mysql-integration-test
|
- mysql-integration-test
|
||||||
- postgres-integration-test
|
- postgres-integration-test
|
||||||
|
filters: *filter-only-master
|
||||||
- build-ee-msi:
|
- build-ee-msi:
|
||||||
requires:
|
requires:
|
||||||
- build-all-enterprise
|
- build-all-enterprise
|
||||||
@@ -1323,6 +1330,7 @@ workflows:
|
|||||||
- shellcheck
|
- shellcheck
|
||||||
- mysql-integration-test
|
- mysql-integration-test
|
||||||
- postgres-integration-test
|
- postgres-integration-test
|
||||||
|
filters: *filter-only-release
|
||||||
- build-ee-msi:
|
- build-ee-msi:
|
||||||
requires:
|
requires:
|
||||||
- build-all-enterprise
|
- build-all-enterprise
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ COPY packages packages
|
|||||||
RUN yarn install --pure-lockfile
|
RUN yarn install --pure-lockfile
|
||||||
|
|
||||||
COPY Gruntfile.js tsconfig.json tslint.json .browserslistrc ./
|
COPY Gruntfile.js tsconfig.json tslint.json .browserslistrc ./
|
||||||
COPY public public
|
COPY public public
|
||||||
COPY scripts scripts
|
COPY scripts scripts
|
||||||
COPY emails emails
|
COPY emails emails
|
||||||
|
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
RUN ./node_modules/.bin/grunt build
|
RUN ./node_modules/.bin/grunt build
|
||||||
|
|
||||||
FROM ubuntu:18.10
|
FROM ubuntu:18.04
|
||||||
|
|
||||||
LABEL maintainer="Grafana team <hello@grafana.com>"
|
LABEL maintainer="Grafana team <hello@grafana.com>"
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
{
|
{
|
||||||
"targetBlank": false,
|
"targetBlank": false,
|
||||||
"title": "Drill it down",
|
"title": "Drill it down",
|
||||||
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source?var-seriesName=${__series.name}&var-labelDatacenter=${__series.labels.datacenter}&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}&var-valueTime=${__value.time}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}"
|
"url": "/d/wfTJJL5Wz/datalinks-source?var-seriesName=${__series.name}&var-labelDatacenter=${__series.labels.datacenter}&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}&var-valueTime=${__value.time}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
{
|
{
|
||||||
"targetBlank": false,
|
"targetBlank": false,
|
||||||
"title": "Drill it down",
|
"title": "Drill it down",
|
||||||
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source?var-seriesName=${__series.name}&var-valueTime=${__value.time}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}&var-fieldName=${__field.name}"
|
"url": "/d/wfTJJL5Wz/datalinks-source?var-seriesName=${__series.name}&var-valueTime=${__value.time}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}&var-fieldName=${__field.name}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -246,7 +246,7 @@
|
|||||||
{
|
{
|
||||||
"targetBlank": true,
|
"targetBlank": true,
|
||||||
"title": "Drill it down!",
|
"title": "Drill it down!",
|
||||||
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source\n?var-fieldName=${__field.name}\n&var-labelDatacenter=${__series.labels.datacenter}\n&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}\n&var-valueNumeric=${__value.numeric}\n&var-valueText=${__value.text}\n&var-valueCalc=${__value.calc}"
|
"url": "/d/wfTJJL5Wz/datalinks-source\n?var-fieldName=${__field.name}\n&var-labelDatacenter=${__series.labels.datacenter}\n&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}\n&var-valueNumeric=${__value.numeric}\n&var-valueText=${__value.text}\n&var-valueCalc=${__value.calc}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mappings": [
|
"mappings": [
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"title": "Drill it down",
|
"title": "Drill it down",
|
||||||
"url": "http://localhost:3000/d/wfTJJL5Wz/datalinks-source?var-fieldName=${__field.name}&var-labelDatacenter=${__series.labels.datacenter}&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}&var-valueCalc=${__value.calc}"
|
"url": "/d/wfTJJL5Wz/datalinks-source?var-fieldName=${__field.name}&var-labelDatacenter=${__series.labels.datacenter}&var-labelDatacenterRegion=${__series.labels[\"datacenter.region\"]}&var-valueNumeric=${__value.numeric}&var-valueText=${__value.text}&var-valueCalc=${__value.calc}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mappings": [],
|
"mappings": [],
|
||||||
|
|||||||
@@ -222,7 +222,7 @@
|
|||||||
"text": "A",
|
"text": "A",
|
||||||
"value": ["A"]
|
"value": ["A"]
|
||||||
},
|
},
|
||||||
"datasource": "TestData DB-1",
|
"datasource": "gdev-testdata",
|
||||||
"definition": "*",
|
"definition": "*",
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"includeAll": true,
|
"includeAll": true,
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
"text": "AA",
|
"text": "AA",
|
||||||
"value": ["AA"]
|
"value": ["AA"]
|
||||||
},
|
},
|
||||||
"datasource": "TestData DB-1",
|
"datasource": "gdev-testdata",
|
||||||
"definition": "$datacenter.*",
|
"definition": "$datacenter.*",
|
||||||
"hide": 0,
|
"hide": 0,
|
||||||
"includeAll": true,
|
"includeAll": true,
|
||||||
|
|||||||
71
devenv/docker/loadtest/annotations_by_tag_test.js
Normal file
71
devenv/docker/loadtest/annotations_by_tag_test.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { sleep, check, group } from 'k6';
|
||||||
|
import { createClient, createBasicAuthClient } from './modules/client.js';
|
||||||
|
import { createTestOrgIfNotExists, createTestdataDatasourceIfNotExists } from './modules/util.js';
|
||||||
|
|
||||||
|
export let options = {
|
||||||
|
noCookiesReset: true
|
||||||
|
};
|
||||||
|
|
||||||
|
let endpoint = __ENV.URL || 'http://localhost:3000';
|
||||||
|
const client = createClient(endpoint);
|
||||||
|
|
||||||
|
export const setup = () => {
|
||||||
|
const basicAuthClient = createBasicAuthClient(endpoint, 'admin', 'admin');
|
||||||
|
const orgId = createTestOrgIfNotExists(basicAuthClient);
|
||||||
|
const datasourceId = createTestdataDatasourceIfNotExists(basicAuthClient);
|
||||||
|
client.withOrgId(orgId);
|
||||||
|
return {
|
||||||
|
orgId: orgId,
|
||||||
|
datasourceId: datasourceId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (data) => {
|
||||||
|
group("annotation by tag test", () => {
|
||||||
|
if (__ITER === 0) {
|
||||||
|
group("user authenticates thru ui with username and password", () => {
|
||||||
|
let res = client.ui.login('admin', 'admin');
|
||||||
|
|
||||||
|
check(res, {
|
||||||
|
'response status is 200': (r) => r.status === 200,
|
||||||
|
'response has cookie \'grafana_session\' with 32 characters': (r) => r.cookies.grafana_session[0].value.length === 32,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__ITER !== 0) {
|
||||||
|
group("batch tsdb requests with annotations by tag", () => {
|
||||||
|
const batchCount = 20;
|
||||||
|
const requests = [];
|
||||||
|
const payload = {
|
||||||
|
from: '1547765247624',
|
||||||
|
to: '1547768847624',
|
||||||
|
queries: [{
|
||||||
|
refId: 'A',
|
||||||
|
scenarioId: 'random_walk',
|
||||||
|
intervalMs: 10000,
|
||||||
|
maxDataPoints: 433,
|
||||||
|
datasourceId: data.datasourceId,
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
requests.push({ method: 'GET', url: '/api/annotations?from=1580825186534&to=1580846786535' });
|
||||||
|
|
||||||
|
for (let n = 0; n < batchCount; n++) {
|
||||||
|
requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload });
|
||||||
|
}
|
||||||
|
|
||||||
|
let responses = client.batch(requests);
|
||||||
|
for (let n = 0; n < batchCount; n++) {
|
||||||
|
check(responses[n], {
|
||||||
|
'response status is 200': (r) => r.status === 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sleep(5)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const teardown = (data) => {}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"useWorkspaces": true,
|
"useWorkspaces": true,
|
||||||
"packages": ["packages/*"],
|
"packages": ["packages/*"],
|
||||||
"version": "6.6.0-pre"
|
"version": "6.6.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"name": "grafana",
|
"name": "grafana",
|
||||||
"version": "6.6.0-pre",
|
"version": "6.6.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "http://github.com/grafana/grafana.git"
|
"url": "http://github.com/grafana/grafana.git"
|
||||||
@@ -163,7 +163,6 @@
|
|||||||
"jest-ci": "mkdir -p reports/junit && export JEST_JUNIT_OUTPUT_DIR=reports/junit && jest --ci --reporters=default --reporters=jest-junit --maxWorkers 2",
|
"jest-ci": "mkdir -p reports/junit && export JEST_JUNIT_OUTPUT_DIR=reports/junit && jest --ci --reporters=default --reporters=jest-junit --maxWorkers 2",
|
||||||
"e2e": "cd packages/grafana-e2e && yarn start --env BASE_URL=$BASE_URL,CIRCLE_SHA1=$CIRCLE_SHA1,SLOWMO=$SLOWMO --config integrationFolder=../../public/e2e-tests/integration,screenshotsFolder=../../public/e2e-tests/screenShots,videosFolder=../../public/e2e-tests/videos,fileServerFolder=./cypress,viewportWidth=1920,viewportHeight=1080,trashAssetsBeforeRuns=false",
|
"e2e": "cd packages/grafana-e2e && yarn start --env BASE_URL=$BASE_URL,CIRCLE_SHA1=$CIRCLE_SHA1,SLOWMO=$SLOWMO --config integrationFolder=../../public/e2e-tests/integration,screenshotsFolder=../../public/e2e-tests/screenShots,videosFolder=../../public/e2e-tests/videos,fileServerFolder=./cypress,viewportWidth=1920,viewportHeight=1080,trashAssetsBeforeRuns=false",
|
||||||
"e2e-tests": "yarn e2e",
|
"e2e-tests": "yarn e2e",
|
||||||
"e2e-tests:ci": "yarn e2e --record",
|
|
||||||
"e2e-tests:debug": "SLOWMO=1 yarn e2e --headed --no-exit",
|
"e2e-tests:debug": "SLOWMO=1 yarn e2e --headed --no-exit",
|
||||||
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
"api-tests": "jest --notify --watch --config=devenv/e2e-api-tests/jest.js",
|
||||||
"storybook": "cd packages/grafana-ui && yarn storybook --ci",
|
"storybook": "cd packages/grafana-ui && yarn storybook --ci",
|
||||||
@@ -266,6 +265,7 @@
|
|||||||
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
"tether-drop": "https://github.com/torkelo/drop/tarball/master",
|
||||||
"tinycolor2": "1.4.1",
|
"tinycolor2": "1.4.1",
|
||||||
"tti-polyfill": "0.2.2",
|
"tti-polyfill": "0.2.2",
|
||||||
|
"url-search-params-polyfill": "7.0.1",
|
||||||
"xss": "1.0.3"
|
"xss": "1.0.3"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"author": "Grafana Labs",
|
"author": "Grafana Labs",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"name": "@grafana/data",
|
"name": "@grafana/data",
|
||||||
"version": "6.6.0-pre",
|
"version": "6.6.2",
|
||||||
"description": "Grafana Data Library",
|
"description": "Grafana Data Library",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"typescript"
|
"typescript"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Vector } from '../types/vector';
|
import { Vector } from '../types/vector';
|
||||||
import { DataFrame } from '../types/dataFrame';
|
import { DataFrame } from '../types/dataFrame';
|
||||||
|
import { DisplayProcessor } from '../types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This abstraction will present the contents of a DataFrame as if
|
* This abstraction will present the contents of a DataFrame as if
|
||||||
@@ -55,6 +56,20 @@ export class DataFrameView<T = any> implements Vector<T> {
|
|||||||
return this.data.length;
|
return this.data.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getFieldDisplayProcessor(colIndex: number): DisplayProcessor | null {
|
||||||
|
if (!this.dataFrame || !this.dataFrame.fields) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = this.dataFrame.fields[colIndex];
|
||||||
|
|
||||||
|
if (!field || !field.display) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.display;
|
||||||
|
}
|
||||||
|
|
||||||
get(idx: number) {
|
get(idx: number) {
|
||||||
this.index = idx;
|
this.index = idx;
|
||||||
return this.obj;
|
return this.obj;
|
||||||
|
|||||||
@@ -33,15 +33,16 @@ export type DataSourceOptionsType<DSType extends DataSourceApi<any, any>> = DSTy
|
|||||||
export class DataSourcePlugin<
|
export class DataSourcePlugin<
|
||||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||||
TQuery extends DataQuery = DataSourceQueryType<DSType>,
|
TQuery extends DataQuery = DataSourceQueryType<DSType>,
|
||||||
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType>
|
TOptions extends DataSourceJsonData = DataSourceOptionsType<DSType>,
|
||||||
|
TSecureOptions = {}
|
||||||
> extends GrafanaPlugin<DataSourcePluginMeta<TOptions>> {
|
> extends GrafanaPlugin<DataSourcePluginMeta<TOptions>> {
|
||||||
components: DataSourcePluginComponents<DSType, TQuery, TOptions> = {};
|
components: DataSourcePluginComponents<DSType, TQuery, TOptions, TSecureOptions> = {};
|
||||||
|
|
||||||
constructor(public DataSourceClass: DataSourceConstructor<DSType, TQuery, TOptions>) {
|
constructor(public DataSourceClass: DataSourceConstructor<DSType, TQuery, TOptions>) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfigEditor(editor: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>) {
|
setConfigEditor(editor: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>) {
|
||||||
this.components.ConfigEditor = editor;
|
this.components.ConfigEditor = editor;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,8 @@ interface PluginMetaQueryOptions {
|
|||||||
export interface DataSourcePluginComponents<
|
export interface DataSourcePluginComponents<
|
||||||
DSType extends DataSourceApi<TQuery, TOptions>,
|
DSType extends DataSourceApi<TQuery, TOptions>,
|
||||||
TQuery extends DataQuery = DataQuery,
|
TQuery extends DataQuery = DataQuery,
|
||||||
TOptions extends DataSourceJsonData = DataSourceJsonData
|
TOptions extends DataSourceJsonData = DataSourceJsonData,
|
||||||
|
TSecureOptions = {}
|
||||||
> {
|
> {
|
||||||
QueryCtrl?: any;
|
QueryCtrl?: any;
|
||||||
AnnotationsQueryCtrl?: any;
|
AnnotationsQueryCtrl?: any;
|
||||||
@@ -141,7 +143,7 @@ export interface DataSourcePluginComponents<
|
|||||||
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
ExploreMetricsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
||||||
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
ExploreLogsQueryField?: ComponentType<ExploreQueryFieldProps<DSType, TQuery, TOptions>>;
|
||||||
ExploreStartPage?: ComponentType<ExploreStartPageProps>;
|
ExploreStartPage?: ComponentType<ExploreStartPageProps>;
|
||||||
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions>>;
|
ConfigEditor?: ComponentType<DataSourcePluginOptionsEditorProps<TOptions, TSecureOptions>>;
|
||||||
MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>;
|
MetadataInspector?: ComponentType<MetadataInspectorProps<DSType, TQuery, TOptions>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,7 +278,7 @@ export abstract class DataSourceApi<
|
|||||||
*/
|
*/
|
||||||
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
|
annotationQuery?(options: AnnotationQueryRequest<TQuery>): Promise<AnnotationEvent[]>;
|
||||||
|
|
||||||
interpolateVariablesInQueries?(queries: TQuery[]): TQuery[];
|
interpolateVariablesInQueries?(queries: TQuery[], scopedVars: ScopedVars | {}): TQuery[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetadataInspectorProps<
|
export interface MetadataInspectorProps<
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ export interface LogRowModel {
|
|||||||
logLevel: LogLevel;
|
logLevel: LogLevel;
|
||||||
raw: string;
|
raw: string;
|
||||||
searchWords?: string[];
|
searchWords?: string[];
|
||||||
timestamp: string; // ISO with nanosec precision
|
|
||||||
timeFromNow: string;
|
timeFromNow: string;
|
||||||
timeEpochMs: number;
|
timeEpochMs: number;
|
||||||
timeLocal: string;
|
timeLocal: string;
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export interface PanelModel<TOptions = any> {
|
|||||||
id: number;
|
id: number;
|
||||||
options: TOptions;
|
options: TOptions;
|
||||||
pluginVersion?: string;
|
pluginVersion?: string;
|
||||||
|
scopedVars?: ScopedVars;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getParser,
|
getParser,
|
||||||
LogsParsers,
|
LogsParsers,
|
||||||
calculateStats,
|
calculateStats,
|
||||||
|
getLogLevelFromKey,
|
||||||
} from './logs';
|
} from './logs';
|
||||||
|
|
||||||
describe('getLoglevel()', () => {
|
describe('getLoglevel()', () => {
|
||||||
@@ -23,6 +24,10 @@ describe('getLoglevel()', () => {
|
|||||||
expect(getLogLevel('[Warn]')).toBe('warning');
|
expect(getLogLevel('[Warn]')).toBe('warning');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns correct log level when level is capitalized', () => {
|
||||||
|
expect(getLogLevel('WARN')).toBe(LogLevel.warn);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns log level on line contains a log level', () => {
|
it('returns log level on line contains a log level', () => {
|
||||||
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
|
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
|
||||||
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
|
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
|
||||||
@@ -33,6 +38,15 @@ describe('getLoglevel()', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getLogLevelFromKey()', () => {
|
||||||
|
it('returns correct log level', () => {
|
||||||
|
expect(getLogLevelFromKey('info')).toBe(LogLevel.info);
|
||||||
|
});
|
||||||
|
it('returns correct log level when level is capitalized', () => {
|
||||||
|
expect(getLogLevelFromKey('INFO')).toBe(LogLevel.info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('calculateLogsLabelStats()', () => {
|
describe('calculateLogsLabelStats()', () => {
|
||||||
test('should return no stats for empty rows', () => {
|
test('should return no stats for empty rows', () => {
|
||||||
expect(calculateLogsLabelStats([], '')).toEqual([]);
|
expect(calculateLogsLabelStats([], '')).toEqual([]);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function getLogLevel(line: string): LogLevel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getLogLevelFromKey(key: string): LogLevel {
|
export function getLogLevelFromKey(key: string): LogLevel {
|
||||||
const level = (LogLevel as any)[key];
|
const level = (LogLevel as any)[key.toLowerCase()];
|
||||||
if (level) {
|
if (level) {
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"author": "Grafana Labs",
|
"author": "Grafana Labs",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"name": "@grafana/e2e",
|
"name": "@grafana/e2e",
|
||||||
"version": "6.4.0-pre",
|
"version": "6.6.2",
|
||||||
"description": "Grafana End to End Test Library",
|
"description": "Grafana End to End Test Library",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"grafana",
|
"grafana",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"author": "Grafana Labs",
|
"author": "Grafana Labs",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"name": "@grafana/runtime",
|
"name": "@grafana/runtime",
|
||||||
"version": "6.6.0-pre",
|
"version": "6.6.2",
|
||||||
"description": "Grafana Runtime Library",
|
"description": "Grafana Runtime Library",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"grafana",
|
"grafana",
|
||||||
@@ -21,8 +21,8 @@
|
|||||||
"build": "grafana-toolkit package:build --scope=runtime"
|
"build": "grafana-toolkit package:build --scope=runtime"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grafana/data": "^6.6.0-pre",
|
"@grafana/data": "6.6.2",
|
||||||
"@grafana/ui": "^6.6.0-pre",
|
"@grafana/ui": "6.6.2",
|
||||||
"systemjs": "0.20.19",
|
"systemjs": "0.20.19",
|
||||||
"systemjs-plugin-css": "0.1.37"
|
"systemjs-plugin-css": "0.1.37"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface BuildInfo {
|
|||||||
commit: string;
|
commit: string;
|
||||||
isEnterprise: boolean; // deprecated: use licenseInfo.hasLicense instead
|
isEnterprise: boolean; // deprecated: use licenseInfo.hasLicense instead
|
||||||
env: string;
|
env: string;
|
||||||
|
edition: string;
|
||||||
latestVersion: string;
|
latestVersion: string;
|
||||||
hasUpdate: boolean;
|
hasUpdate: boolean;
|
||||||
}
|
}
|
||||||
@@ -16,11 +17,14 @@ interface FeatureToggles {
|
|||||||
inspect: boolean;
|
inspect: boolean;
|
||||||
expressions: boolean;
|
expressions: boolean;
|
||||||
newEdit: boolean;
|
newEdit: boolean;
|
||||||
|
meta: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LicenseInfo {
|
interface LicenseInfo {
|
||||||
hasLicense: boolean;
|
hasLicense: boolean;
|
||||||
expiry: number;
|
expiry: number;
|
||||||
|
licenseUrl: string;
|
||||||
|
stateInfo: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GrafanaBootConfig {
|
export class GrafanaBootConfig {
|
||||||
@@ -60,8 +64,10 @@ export class GrafanaBootConfig {
|
|||||||
inspect: false,
|
inspect: false,
|
||||||
expressions: false,
|
expressions: false,
|
||||||
newEdit: false,
|
newEdit: false,
|
||||||
|
meta: false,
|
||||||
};
|
};
|
||||||
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
licenseInfo: LicenseInfo = {} as LicenseInfo;
|
||||||
|
phantomJSRenderer = false;
|
||||||
|
|
||||||
constructor(options: GrafanaBootConfig) {
|
constructor(options: GrafanaBootConfig) {
|
||||||
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
|
this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"author": "Grafana Labs",
|
"author": "Grafana Labs",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"name": "@grafana/toolkit",
|
"name": "@grafana/toolkit",
|
||||||
"version": "6.6.0-pre",
|
"version": "6.6.2",
|
||||||
"description": "Grafana Toolkit",
|
"description": "Grafana Toolkit",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"grafana",
|
"grafana",
|
||||||
@@ -28,8 +28,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "7.6.4",
|
"@babel/core": "7.6.4",
|
||||||
"@babel/preset-env": "7.6.3",
|
"@babel/preset-env": "7.6.3",
|
||||||
"@grafana/data": "^6.6.0-pre",
|
"@grafana/data": "6.6.2",
|
||||||
"@grafana/ui": "^6.6.0-pre",
|
"@grafana/ui": "6.6.2",
|
||||||
"@types/command-exists": "^1.2.0",
|
"@types/command-exists": "^1.2.0",
|
||||||
"@types/execa": "^0.9.0",
|
"@types/execa": "^0.9.0",
|
||||||
"@types/expect-puppeteer": "3.3.1",
|
"@types/expect-puppeteer": "3.3.1",
|
||||||
@@ -62,9 +62,12 @@
|
|||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"inquirer": "^6.3.1",
|
"inquirer": "^6.3.1",
|
||||||
"jest": "24.8.0",
|
"jest": "24.8.0",
|
||||||
|
"jest-canvas-mock": "2.1.2",
|
||||||
"jest-cli": "^24.8.0",
|
"jest-cli": "^24.8.0",
|
||||||
"jest-coverage-badges": "^1.1.2",
|
"jest-coverage-badges": "^1.1.2",
|
||||||
"jest-junit": "^6.4.0",
|
"jest-junit": "^6.4.0",
|
||||||
|
"less": "^3.10.3",
|
||||||
|
"less-loader": "^5.0.0",
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.15",
|
||||||
"md5-file": "^4.0.0",
|
"md5-file": "^4.0.0",
|
||||||
"mini-css-extract-plugin": "^0.7.0",
|
"mini-css-extract-plugin": "^0.7.0",
|
||||||
@@ -95,9 +98,7 @@
|
|||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typescript": "3.7.2",
|
"typescript": "3.7.2",
|
||||||
"url-loader": "^2.0.1",
|
"url-loader": "^2.0.1",
|
||||||
"webpack": "4.35.0",
|
"webpack": "4.35.0"
|
||||||
"less": "^3.10.3",
|
|
||||||
"less-loader": "^5.0.0"
|
|
||||||
},
|
},
|
||||||
"_moduleAliases": {
|
"_moduleAliases": {
|
||||||
"puppeteer": "node_modules/puppeteer-core"
|
"puppeteer": "node_modules/puppeteer-core"
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ export const prepare = useSpinner<void>('Preparing', async () => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
// Copy only if local tsconfig does not exist. Otherwise this will work, but have odd behavior
|
// Copy only if local tsconfig does not exist. Otherwise this will work, but have odd behavior
|
||||||
copyIfNonExistent(
|
copyIfNonExistent(
|
||||||
resolvePath(process.cwd(), 'tsconfig.json'),
|
resolvePath(__dirname, '../../config/tsconfig.plugin.local.json'),
|
||||||
resolvePath(__dirname, '../../config/tsconfig.plugin.local.json')
|
resolvePath(process.cwd(), 'tsconfig.json')
|
||||||
),
|
),
|
||||||
// Copy only if local prettierrc does not exist. Otherwise this will work, but have odd behavior
|
// Copy only if local prettierrc does not exist. Otherwise this will work, but have odd behavior
|
||||||
copyIfNonExistent(
|
copyIfNonExistent(
|
||||||
resolvePath(process.cwd(), '.prettierrc.js'),
|
resolvePath(__dirname, '../../config/prettier.plugin.rc.js'),
|
||||||
resolvePath(__dirname, '../../config/prettier.plugin.rc.js')
|
resolvePath(process.cwd(), '.prettierrc.js')
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -149,7 +149,9 @@ export const lintPlugin = useSpinner<Fixable>('Linting', async ({ fix }) => {
|
|||||||
|
|
||||||
if (lintResults.length > 0) {
|
if (lintResults.length > 0) {
|
||||||
console.log('\n');
|
console.log('\n');
|
||||||
const failures: RuleFailure[] = lintResults.flat();
|
const failures = lintResults.reduce<RuleFailure[]>((failures, result) => {
|
||||||
|
return [...failures, ...result.failures];
|
||||||
|
}, []);
|
||||||
failures.forEach(f => {
|
failures.forEach(f => {
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
console.log(
|
console.log(
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const jestConfig = (baseDir: string = process.cwd()) => {
|
|||||||
const setupFile = getSetupFile(setupFilePath);
|
const setupFile = getSetupFile(setupFilePath);
|
||||||
const shimsFile = getSetupFile(shimsFilePath);
|
const shimsFile = getSetupFile(shimsFilePath);
|
||||||
|
|
||||||
const setupFiles = [setupFile, shimsFile].filter(f => f);
|
const setupFiles = [setupFile, shimsFile, 'jest-canvas-mock'].filter(f => f);
|
||||||
const defaultJestConfig = {
|
const defaultJestConfig = {
|
||||||
preset: 'ts-jest',
|
preset: 'ts-jest',
|
||||||
verbose: false,
|
verbose: false,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"author": "Grafana Labs",
|
"author": "Grafana Labs",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"name": "@grafana/ui",
|
"name": "@grafana/ui",
|
||||||
"version": "6.6.0-pre",
|
"version": "6.6.2",
|
||||||
"description": "Grafana Components Library",
|
"description": "Grafana Components Library",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"grafana",
|
"grafana",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"build": "grafana-toolkit package:build --scope=ui"
|
"build": "grafana-toolkit package:build --scope=ui"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@grafana/data": "^6.6.0-pre",
|
"@grafana/data": "6.6.2",
|
||||||
"@grafana/slate-react": "0.22.9-grafana",
|
"@grafana/slate-react": "0.22.9-grafana",
|
||||||
"@torkelo/react-select": "2.1.1",
|
"@torkelo/react-select": "2.1.1",
|
||||||
"@types/react-color": "2.17.0",
|
"@types/react-color": "2.17.0",
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"rc-drawer": "3.0.2",
|
"rc-drawer": "3.0.2",
|
||||||
"rc-time-picker": "^3.7.2",
|
"rc-time-picker": "^3.7.2",
|
||||||
"react": "16.12.0",
|
"react": "16.12.0",
|
||||||
"react-calendar": "2.18.1",
|
"react-calendar": "2.19.2",
|
||||||
"react-color": "2.17.0",
|
"react-color": "2.17.0",
|
||||||
"react-custom-scrollbars": "4.2.1",
|
"react-custom-scrollbars": "4.2.1",
|
||||||
"react-dom": "16.12.0",
|
"react-dom": "16.12.0",
|
||||||
|
|||||||
@@ -54,6 +54,7 @@
|
|||||||
background: none;
|
background: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
font-size: 21px;
|
font-size: 21px;
|
||||||
@@ -78,6 +79,11 @@
|
|||||||
|
|
||||||
.alert-body {
|
.alert-body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $white;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-icon-on-top {
|
.alert-icon-on-top {
|
||||||
|
|||||||
@@ -131,7 +131,8 @@ export class BarGauge extends PureComponent<Props> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = display(positionValue).color;
|
const color = display ? display(positionValue).color : null;
|
||||||
|
|
||||||
if (color) {
|
if (color) {
|
||||||
// if we are past real value the cell is not "on"
|
// if we are past real value the cell is not "on"
|
||||||
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
if (value === null || (positionValue !== null && positionValue > value.numeric)) {
|
||||||
|
|||||||
@@ -363,8 +363,9 @@ export class StackedWithChartLayout extends BigValueLayout {
|
|||||||
|
|
||||||
// make title fontsize it's a bit smaller than valueFontSize
|
// make title fontsize it's a bit smaller than valueFontSize
|
||||||
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
|
this.titleFontSize = Math.min(this.valueFontSize * 0.7, this.titleFontSize);
|
||||||
|
|
||||||
// make chart take up onused space
|
// make chart take up onused space
|
||||||
this.chartHeight = height - this.titleFontSize * LINE_HEIGHT - this.valueFontSize * LINE_HEIGHT + height * 0.05;
|
this.chartHeight = height - this.titleFontSize * LINE_HEIGHT - this.valueFontSize * LINE_HEIGHT;
|
||||||
}
|
}
|
||||||
|
|
||||||
getValueAndTitleContainerStyles() {
|
getValueAndTitleContainerStyles() {
|
||||||
|
|||||||
@@ -29,9 +29,11 @@ interface CascaderState {
|
|||||||
export interface CascaderOption {
|
export interface CascaderOption {
|
||||||
value: any;
|
value: any;
|
||||||
label: string;
|
label: string;
|
||||||
|
// Items will be just flattened into the main list of items recursively.
|
||||||
items?: CascaderOption[];
|
items?: CascaderOption[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
// Children will be shown in a submenu.
|
||||||
children?: CascaderOption[];
|
children?: CascaderOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ const setup = (propOverrides?: Partial<Props>, rowOverrides?: Partial<LogRowMode
|
|||||||
hasAnsi: false,
|
hasAnsi: false,
|
||||||
entry: '',
|
entry: '',
|
||||||
raw: '',
|
raw: '',
|
||||||
timestamp: '',
|
|
||||||
uid: '0',
|
uid: '0',
|
||||||
labels: {},
|
labels: {},
|
||||||
...(rowOverrides || {}),
|
...(rowOverrides || {}),
|
||||||
|
|||||||
@@ -92,17 +92,28 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<tr className={cx(style.logDetailsValue, { [styles.noHoverBackground]: showFieldsStats })}>
|
<tr className={cx(style.logDetailsValue, { [styles.noHoverBackground]: showFieldsStats })}>
|
||||||
{/* Action buttons - show stats/filter results */}
|
{/* Action buttons - show stats/filter results */}
|
||||||
<td title="Ad-hoc statistics" onClick={this.showStats} className={style.logsDetailsIcon}>
|
<td className={style.logsDetailsIcon} colSpan={isLabel ? undefined : 3}>
|
||||||
<i className={`fa fa-signal ${styles.hoverCursor}`} />
|
<i title="Ad-hoc statistics" className={`fa fa-signal ${styles.hoverCursor}`} onClick={this.showStats} />
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td title="Filter for value" onClick={() => isLabel && this.filterLabel()} className={style.logsDetailsIcon}>
|
{isLabel && (
|
||||||
{isLabel && <i className={`fa fa-search-plus ${styles.hoverCursor}`} />}
|
<>
|
||||||
</td>
|
<td className={style.logsDetailsIcon}>
|
||||||
|
<i
|
||||||
<td title="Filter out value" onClick={() => isLabel && this.filterOutLabel()} className={style.logsDetailsIcon}>
|
title="Filter for value"
|
||||||
{isLabel && <i className={`fa fa-search-minus ${styles.hoverCursor}`} />}
|
className={`fa fa-search-plus ${styles.hoverCursor}`}
|
||||||
</td>
|
onClick={this.filterLabel}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className={style.logsDetailsIcon}>
|
||||||
|
<i
|
||||||
|
title="Filter out value"
|
||||||
|
className={`fa fa-search-minus ${styles.hoverCursor}`}
|
||||||
|
onClick={this.filterOutLabel}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Key - value columns */}
|
{/* Key - value columns */}
|
||||||
<td className={style.logDetailsLabel}>{parsedKey}</td>
|
<td className={style.logDetailsLabel}>{parsedKey}</td>
|
||||||
|
|||||||
@@ -3,7 +3,38 @@ import { getRowContexts } from './LogRowContextProvider';
|
|||||||
|
|
||||||
describe('getRowContexts', () => {
|
describe('getRowContexts', () => {
|
||||||
describe('when called with a DataFrame and results are returned', () => {
|
describe('when called with a DataFrame and results are returned', () => {
|
||||||
it('then the result should be in correct format', async () => {
|
it('then the result should be in correct format and filtered', async () => {
|
||||||
|
const firstResult = new MutableDataFrame({
|
||||||
|
refId: 'B',
|
||||||
|
fields: [
|
||||||
|
{ name: 'ts', type: FieldType.time, values: [3, 2, 1] },
|
||||||
|
{ name: 'line', type: FieldType.string, values: ['3', '2', '1'], labels: {} },
|
||||||
|
{ name: 'id', type: FieldType.string, values: ['3', '2', '1'], labels: {} },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const secondResult = new MutableDataFrame({
|
||||||
|
refId: 'B',
|
||||||
|
fields: [
|
||||||
|
{ name: 'ts', type: FieldType.time, values: [6, 5, 4] },
|
||||||
|
{ name: 'line', type: FieldType.string, values: ['6', '5', '4'], labels: {} },
|
||||||
|
{ name: 'id', type: FieldType.string, values: ['6', '5', '4'], labels: {} },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let called = false;
|
||||||
|
const getRowContextMock = (row: LogRowModel, options?: any): Promise<DataQueryResponse> => {
|
||||||
|
if (!called) {
|
||||||
|
called = true;
|
||||||
|
return Promise.resolve({ data: [firstResult] });
|
||||||
|
}
|
||||||
|
return Promise.resolve({ data: [secondResult] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await getRowContexts(getRowContextMock, row, 10);
|
||||||
|
|
||||||
|
expect(result).toEqual({ data: [[['3', '2']], [['6', '5', '4']]], errors: ['', ''] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then the result should be in correct format and filtered without uid', async () => {
|
||||||
const firstResult = new MutableDataFrame({
|
const firstResult = new MutableDataFrame({
|
||||||
refId: 'B',
|
refId: 'B',
|
||||||
fields: [
|
fields: [
|
||||||
@@ -18,23 +49,6 @@ describe('getRowContexts', () => {
|
|||||||
{ name: 'line', type: FieldType.string, values: ['6', '5', '4'], labels: {} },
|
{ name: 'line', type: FieldType.string, values: ['6', '5', '4'], labels: {} },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const row: LogRowModel = {
|
|
||||||
entryFieldIndex: 0,
|
|
||||||
rowIndex: 0,
|
|
||||||
dataFrame: new MutableDataFrame(),
|
|
||||||
entry: '4',
|
|
||||||
labels: (null as any) as Labels,
|
|
||||||
hasAnsi: false,
|
|
||||||
raw: '4',
|
|
||||||
logLevel: LogLevel.info,
|
|
||||||
timeEpochMs: 4,
|
|
||||||
timeFromNow: '',
|
|
||||||
timeLocal: '',
|
|
||||||
timeUtc: '',
|
|
||||||
timestamp: '4',
|
|
||||||
uid: '1',
|
|
||||||
};
|
|
||||||
|
|
||||||
let called = false;
|
let called = false;
|
||||||
const getRowContextMock = (row: LogRowModel, options?: any): Promise<DataQueryResponse> => {
|
const getRowContextMock = (row: LogRowModel, options?: any): Promise<DataQueryResponse> => {
|
||||||
if (!called) {
|
if (!called) {
|
||||||
@@ -46,7 +60,7 @@ describe('getRowContexts', () => {
|
|||||||
|
|
||||||
const result = await getRowContexts(getRowContextMock, row, 10);
|
const result = await getRowContexts(getRowContextMock, row, 10);
|
||||||
|
|
||||||
expect(result).toEqual({ data: [[['3', '2', '1']], [['6', '5', '4']]], errors: ['', ''] });
|
expect(result).toEqual({ data: [[['3', '2', '1']], [['6', '5']]], errors: ['', ''] });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,23 +68,6 @@ describe('getRowContexts', () => {
|
|||||||
it('then the result should be in correct format', async () => {
|
it('then the result should be in correct format', async () => {
|
||||||
const firstError = new Error('Error 1');
|
const firstError = new Error('Error 1');
|
||||||
const secondError = new Error('Error 2');
|
const secondError = new Error('Error 2');
|
||||||
const row: LogRowModel = {
|
|
||||||
entryFieldIndex: 0,
|
|
||||||
rowIndex: 0,
|
|
||||||
dataFrame: new MutableDataFrame(),
|
|
||||||
entry: '4',
|
|
||||||
labels: (null as any) as Labels,
|
|
||||||
hasAnsi: false,
|
|
||||||
raw: '4',
|
|
||||||
logLevel: LogLevel.info,
|
|
||||||
timeEpochMs: 4,
|
|
||||||
timeFromNow: '',
|
|
||||||
timeLocal: '',
|
|
||||||
timeUtc: '',
|
|
||||||
timestamp: '4',
|
|
||||||
uid: '1',
|
|
||||||
};
|
|
||||||
|
|
||||||
let called = false;
|
let called = false;
|
||||||
const getRowContextMock = (row: LogRowModel, options?: any): Promise<DataQueryResponse> => {
|
const getRowContextMock = (row: LogRowModel, options?: any): Promise<DataQueryResponse> => {
|
||||||
if (!called) {
|
if (!called) {
|
||||||
@@ -86,3 +83,19 @@ describe('getRowContexts', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const row: LogRowModel = {
|
||||||
|
entryFieldIndex: 0,
|
||||||
|
rowIndex: 0,
|
||||||
|
dataFrame: new MutableDataFrame(),
|
||||||
|
entry: '4',
|
||||||
|
labels: (null as any) as Labels,
|
||||||
|
hasAnsi: false,
|
||||||
|
raw: '4',
|
||||||
|
logLevel: LogLevel.info,
|
||||||
|
timeEpochMs: 4,
|
||||||
|
timeFromNow: '',
|
||||||
|
timeLocal: '',
|
||||||
|
timeUtc: '',
|
||||||
|
uid: '1',
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LogRowModel, toDataFrame, Field } from '@grafana/data';
|
import { LogRowModel, toDataFrame, Field, FieldCache } from '@grafana/data';
|
||||||
import { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import flatten from 'lodash/flatten';
|
import flatten from 'lodash/flatten';
|
||||||
import useAsync from 'react-use/lib/useAsync';
|
import useAsync from 'react-use/lib/useAsync';
|
||||||
|
|
||||||
@@ -45,7 +45,8 @@ export const getRowContexts = async (
|
|||||||
limit,
|
limit,
|
||||||
}),
|
}),
|
||||||
getRowContext(row, {
|
getRowContext(row, {
|
||||||
limit: limit + 1, // Lets add one more to the limit as we're filtering out one row see comment below
|
// The start time is inclusive so we will get the one row we are using as context entry
|
||||||
|
limit: limit + 1,
|
||||||
direction: 'FORWARD',
|
direction: 'FORWARD',
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -62,16 +63,33 @@ export const getRowContexts = async (
|
|||||||
const data: any[] = [];
|
const data: any[] = [];
|
||||||
for (let index = 0; index < dataResult.data.length; index++) {
|
for (let index = 0; index < dataResult.data.length; index++) {
|
||||||
const dataFrame = toDataFrame(dataResult.data[index]);
|
const dataFrame = toDataFrame(dataResult.data[index]);
|
||||||
const timestampField: Field<string> = dataFrame.fields.filter(field => field.name === 'ts')[0];
|
const fieldCache = new FieldCache(dataFrame);
|
||||||
|
const timestampField: Field<string> = fieldCache.getFieldByName('ts')!;
|
||||||
|
const idField: Field<string> | undefined = fieldCache.getFieldByName('id');
|
||||||
|
|
||||||
for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
|
for (let fieldIndex = 0; fieldIndex < timestampField.values.length; fieldIndex++) {
|
||||||
const timestamp = timestampField.values.get(fieldIndex);
|
// TODO: this filtering is datasource dependant so it will make sense to move it there so the API is
|
||||||
|
// to return correct list of lines handling inclusive ranges or how to filter the correct line on the
|
||||||
|
// datasource.
|
||||||
|
|
||||||
// We need to filter out the row we're basing our search from because of how start/end params work in Loki API
|
// Filter out the row that is the one used as a focal point for the context as we will get it in one of the
|
||||||
// see https://github.com/grafana/loki/issues/597#issuecomment-506408980
|
// requests.
|
||||||
// the alternative to create our own add 1 nanosecond method to the a timestamp string would be quite complex
|
if (idField) {
|
||||||
if (timestamp === row.timestamp) {
|
// For Loki this means we filter only the one row. Issue is we could have other rows logged at the same
|
||||||
continue;
|
// ns which came before but they come in the response that search for logs after. This means right now
|
||||||
|
// we will show those as if they came after. This is not strictly correct but seems better than loosing them
|
||||||
|
// and making this correct would mean quite a bit of complexity to shuffle things around and messing up
|
||||||
|
//counts.
|
||||||
|
if (idField.values.get(fieldIndex) === row.uid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to timestamp. This should not happen right now as this feature is implemented only for loki
|
||||||
|
// and that has ID. Later this branch could be used in other DS but mind that this could also filter out
|
||||||
|
// logs which were logged in the same timestamp and that can be a problem depending on the precision.
|
||||||
|
if (parseInt(timestampField.values.get(fieldIndex), 10) === row.timeEpochMs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const lineField: Field<string> = dataFrame.fields.filter(field => field.name === 'line')[0];
|
const lineField: Field<string> = dataFrame.fields.filter(field => field.name === 'line')[0];
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ const makeLog = (overrides: Partial<LogRowModel>): LogRowModel => {
|
|||||||
hasAnsi: false,
|
hasAnsi: false,
|
||||||
labels: {},
|
labels: {},
|
||||||
raw: entry,
|
raw: entry,
|
||||||
timestamp: '',
|
|
||||||
timeFromNow: '',
|
timeFromNow: '',
|
||||||
timeEpochMs: 1,
|
timeEpochMs: 1,
|
||||||
timeLocal: '',
|
timeLocal: '',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function sharedSingleStatPanelChangedHandler(
|
|||||||
defaults.mappings = mappings;
|
defaults.mappings = mappings;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (panel.gauge) {
|
if (panel.gauge && panel.gauge.show) {
|
||||||
defaults.min = panel.gauge.minValue;
|
defaults.min = panel.gauge.minValue;
|
||||||
defaults.max = panel.gauge.maxValue;
|
defaults.max = panel.gauge.maxValue;
|
||||||
}
|
}
|
||||||
@@ -151,11 +151,10 @@ export function sharedSingleStatMigrationHandler(panel: PanelModel<SingleStatBas
|
|||||||
|
|
||||||
// Migrate color from simple string to a mode
|
// Migrate color from simple string to a mode
|
||||||
const { defaults } = fieldOptions;
|
const { defaults } = fieldOptions;
|
||||||
if (defaults.color) {
|
if (defaults.color && typeof defaults.color === 'string') {
|
||||||
const old = defaults.color;
|
|
||||||
defaults.color = {
|
defaults.color = {
|
||||||
mode: FieldColorMode.Fixed,
|
mode: FieldColorMode.Fixed,
|
||||||
fixedColor: old,
|
fixedColor: defaults.color,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export class UnthemedTimePicker extends PureComponent<Props, State> {
|
|||||||
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
const hasAbsolute = isDateTime(value.raw.from) || isDateTime(value.raw.to);
|
||||||
const syncedTimePicker = timeSyncButton && isSynced;
|
const syncedTimePicker = timeSyncButton && isSynced;
|
||||||
const timePickerIconClass = cx('fa fa-clock-o fa-fw', { ['icon-brand-gradient']: syncedTimePicker });
|
const timePickerIconClass = cx('fa fa-clock-o fa-fw', { ['icon-brand-gradient']: syncedTimePicker });
|
||||||
const timePickerButtonClass = cx('btn navbar-button navbar-button--zoom', {
|
const timePickerButtonClass = cx('btn navbar-button navbar-button--tight', {
|
||||||
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: !!timeSyncButton,
|
[`btn--radius-right-0 ${styles.noRightBorderStyle}`]: !!timeSyncButton,
|
||||||
[`explore-active-button-glow ${styles.syncedTimePicker}`]: syncedTimePicker,
|
[`explore-active-button-glow ${styles.syncedTimePicker}`]: syncedTimePicker,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { memo } from 'react';
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import Calendar from 'react-calendar/dist/entry.nostyle';
|
import Calendar from 'react-calendar/dist/entry.nostyle';
|
||||||
import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data';
|
import { GrafanaTheme, dateTime, TIME_FORMAT } from '@grafana/data';
|
||||||
@@ -84,7 +84,6 @@ const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
title: css`
|
title: css`
|
||||||
color: ${theme.colors.text}
|
color: ${theme.colors.text}
|
||||||
background-color: ${colors.background};
|
background-color: ${colors.background};
|
||||||
line-height: 21px;
|
|
||||||
font-size: ${theme.typography.size.md};
|
font-size: ${theme.typography.size.md};
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
||||||
@@ -129,6 +128,7 @@ const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
.react-calendar__tile--now {
|
.react-calendar__tile--now {
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
height: 26px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-calendar__navigation__label,
|
.react-calendar__navigation__label,
|
||||||
@@ -158,7 +158,8 @@ const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
background-color: ${theme.colors.blue77};
|
background-color: ${theme.colors.blue77};
|
||||||
border-radius: 100px;
|
border-radius: 100px;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 2px 7px 3px;
|
padding-top: 2px;
|
||||||
|
height: 26px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +203,8 @@ interface Props {
|
|||||||
isFullscreen: boolean;
|
isFullscreen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const stopPropagation = (event: React.MouseEvent<HTMLDivElement>) => event.stopPropagation();
|
||||||
|
|
||||||
export const TimePickerCalendar = memo<Props>(props => {
|
export const TimePickerCalendar = memo<Props>(props => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
@@ -214,7 +217,7 @@ export const TimePickerCalendar = memo<Props>(props => {
|
|||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
return (
|
return (
|
||||||
<ClickOutsideWrapper onClick={props.onClose}>
|
<ClickOutsideWrapper onClick={props.onClose}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container} onClick={stopPropagation}>
|
||||||
<Body {...props} />
|
<Body {...props} />
|
||||||
</div>
|
</div>
|
||||||
</ClickOutsideWrapper>
|
</ClickOutsideWrapper>
|
||||||
@@ -223,14 +226,14 @@ export const TimePickerCalendar = memo<Props>(props => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Portal>
|
<Portal>
|
||||||
<div className={styles.modal} onClick={event => event.stopPropagation()}>
|
<div className={styles.modal} onClick={stopPropagation}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<Header {...props} />
|
<Header {...props} />
|
||||||
<Body {...props} />
|
<Body {...props} />
|
||||||
<Footer {...props} />
|
<Footer {...props} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.backdrop} onClick={event => event.stopPropagation()} />
|
<div className={styles.backdrop} onClick={stopPropagation} />
|
||||||
</Portal>
|
</Portal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -247,10 +250,14 @@ const Header = memo<Props>(({ onClose }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const Body = memo<Props>(props => {
|
const Body = memo<Props>(({ onChange, from, to }) => {
|
||||||
|
const [value, setValue] = useState<Date[]>();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getBodyStyles(theme);
|
const styles = getBodyStyles(theme);
|
||||||
const { from, to, onChange } = props;
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue(inputToValue(from, to));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Calendar
|
<Calendar
|
||||||
@@ -259,7 +266,7 @@ const Body = memo<Props>(props => {
|
|||||||
prev2Label={null}
|
prev2Label={null}
|
||||||
className={styles.body}
|
className={styles.body}
|
||||||
tileClassName={styles.title}
|
tileClassName={styles.title}
|
||||||
value={inputToValue(from, to)}
|
value={value}
|
||||||
nextLabel={<span className="fa fa-angle-right" />}
|
nextLabel={<span className="fa fa-angle-right" />}
|
||||||
prevLabel={<span className="fa fa-angle-left" />}
|
prevLabel={<span className="fa fa-angle-left" />}
|
||||||
onChange={value => valueToInput(value, onChange)}
|
onChange={value => valueToInput(value, onChange)}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
TIME_FORMAT,
|
TIME_FORMAT,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { stringToDateTimeType } from '../time';
|
import { stringToDateTimeType } from '../time';
|
||||||
import { isMathString } from '@grafana/data/src/datetime/datemath';
|
|
||||||
|
|
||||||
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
|
export const mapOptionToTimeRange = (option: TimeOption, timeZone?: TimeZone): TimeRange => {
|
||||||
return {
|
return {
|
||||||
@@ -41,7 +40,7 @@ export const mapStringsToTimeRange = (from: string, to: string, roundup?: boolea
|
|||||||
const fromDate = stringToDateTimeType(from, roundup, timeZone);
|
const fromDate = stringToDateTimeType(from, roundup, timeZone);
|
||||||
const toDate = stringToDateTimeType(to, roundup, timeZone);
|
const toDate = stringToDateTimeType(to, roundup, timeZone);
|
||||||
|
|
||||||
if (isMathString(from) || isMathString(to)) {
|
if (dateMath.isMathString(from) || dateMath.isMathString(to)) {
|
||||||
return {
|
return {
|
||||||
from: fromDate,
|
from: fromDate,
|
||||||
to: toDate,
|
to: toDate,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ exports[`TimePicker renders buttons correctly 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="TimePicker Open Button"
|
aria-label="TimePicker Open Button"
|
||||||
className="btn navbar-button navbar-button--zoom"
|
className="btn navbar-button navbar-button--tight"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
@@ -342,7 +342,7 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
aria-label="TimePicker Open Button"
|
aria-label="TimePicker Open Button"
|
||||||
className="btn navbar-button navbar-button--zoom"
|
className="btn navbar-button navbar-button--tight"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
|
|||||||
@@ -14,3 +14,4 @@
|
|||||||
@import 'TimePicker/TimeOfDayPicker';
|
@import 'TimePicker/TimeOfDayPicker';
|
||||||
@import 'Tooltip/Tooltip';
|
@import 'Tooltip/Tooltip';
|
||||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||||
|
@import 'Alert/Alert';
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ docker_build () {
|
|||||||
else
|
else
|
||||||
libc=""
|
libc=""
|
||||||
dockerfile="ubuntu.Dockerfile"
|
dockerfile="ubuntu.Dockerfile"
|
||||||
base_image="${base_arch}ubuntu:18.10"
|
base_image="${base_arch}ubuntu:18.04"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
grafana_tgz="grafana-latest.linux-${arch}${libc}.tar.gz"
|
grafana_tgz="grafana-latest.linux-${arch}${libc}.tar.gz"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG BASE_IMAGE=ubuntu:18.10
|
ARG BASE_IMAGE=ubuntu:18.04
|
||||||
FROM ${BASE_IMAGE} AS grafana-builder
|
FROM ${BASE_IMAGE} AS grafana-builder
|
||||||
|
|
||||||
ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
|
ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/rendering"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
|
||||||
@@ -194,6 +196,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
|
|||||||
"version": setting.BuildVersion,
|
"version": setting.BuildVersion,
|
||||||
"commit": setting.BuildCommit,
|
"commit": setting.BuildCommit,
|
||||||
"buildstamp": setting.BuildStamp,
|
"buildstamp": setting.BuildStamp,
|
||||||
|
"edition": hs.License.Edition(),
|
||||||
"latestVersion": plugins.GrafanaLatestVersion,
|
"latestVersion": plugins.GrafanaLatestVersion,
|
||||||
"hasUpdate": plugins.GrafanaHasUpdate,
|
"hasUpdate": plugins.GrafanaHasUpdate,
|
||||||
"env": setting.Env,
|
"env": setting.Env,
|
||||||
@@ -202,8 +205,11 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
|
|||||||
"licenseInfo": map[string]interface{}{
|
"licenseInfo": map[string]interface{}{
|
||||||
"hasLicense": hs.License.HasLicense(),
|
"hasLicense": hs.License.HasLicense(),
|
||||||
"expiry": hs.License.Expiry(),
|
"expiry": hs.License.Expiry(),
|
||||||
|
"stateInfo": hs.License.StateInfo(),
|
||||||
|
"licenseUrl": hs.License.LicenseURL(c.SignedInUser),
|
||||||
},
|
},
|
||||||
"featureToggles": hs.Cfg.FeatureToggles,
|
"featureToggles": hs.Cfg.FeatureToggles,
|
||||||
|
"phantomJSRenderer": rendering.IsPhantomJSEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonObj, nil
|
return jsonObj, nil
|
||||||
|
|||||||
@@ -190,18 +190,18 @@ func (hs *HTTPServer) configureHttps() error {
|
|||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
||||||
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
|
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
|
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,12 +235,12 @@ func (hs *HTTPServer) configureHttp2() error {
|
|||||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||||
tls.TLS_AES_128_GCM_SHA256,
|
tls.TLS_AES_128_GCM_SHA256,
|
||||||
tls.TLS_AES_256_GCM_SHA384,
|
tls.TLS_AES_256_GCM_SHA384,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||||
},
|
},
|
||||||
NextProtos: []string{"h2", "http/1.1"},
|
NextProtos: []string{"h2", "http/1.1"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.OrgRole == m.ROLE_ADMIN || hs.Cfg.EditorsCanAdmin {
|
if c.OrgRole == m.ROLE_ADMIN || (hs.Cfg.EditorsCanAdmin && c.OrgRole == m.ROLE_EDITOR) {
|
||||||
configNodes = append(configNodes, &dtos.NavLink{
|
configNodes = append(configNodes, &dtos.NavLink{
|
||||||
Text: "Teams",
|
Text: "Teams",
|
||||||
Id: "teams",
|
Id: "teams",
|
||||||
@@ -357,7 +357,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er
|
|||||||
Children: []*dtos.NavLink{},
|
Children: []*dtos.NavLink{},
|
||||||
})
|
})
|
||||||
|
|
||||||
hs.HooksService.RunIndexDataHooks(&data)
|
hs.HooksService.RunIndexDataHooks(&data, c)
|
||||||
|
|
||||||
sort.SliceStable(data.NavTree, func(i, j int) bool {
|
sort.SliceStable(data.NavTree, func(i, j int) bool {
|
||||||
return data.NavTree[i].SortWeight < data.NavTree[j].SortWeight
|
return data.NavTree[i].SortWeight < data.NavTree[j].SortWeight
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ func (server *HTTPServer) PostSyncUserWithLDAP(c *models.ReqContext) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upsertCmd := &models.UpsertUserCommand{
|
upsertCmd := &models.UpsertUserCommand{
|
||||||
|
ReqContext: c,
|
||||||
ExternalUser: user,
|
ExternalUser: user,
|
||||||
SignupAllowed: setting.LDAPAllowSignup,
|
SignupAllowed: setting.LDAPAllowSignup,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ func (hs *HTTPServer) validateRedirectTo(redirectTo string) error {
|
|||||||
if to.IsAbs() {
|
if to.IsAbs() {
|
||||||
return login.ErrAbsoluteRedirectTo
|
return login.ErrAbsoluteRedirectTo
|
||||||
}
|
}
|
||||||
if hs.Cfg.AppSubUrl != "" && !strings.HasPrefix(to.Path, "/"+hs.Cfg.AppSubUrl) {
|
// when using a subUrl, the redirect_to should have a relative or absolute path that includes the subUrl, otherwise the redirect
|
||||||
|
// will send the user to the wrong location
|
||||||
|
if hs.Cfg.AppSubUrl != "" && !strings.HasPrefix(to.Path, hs.Cfg.AppSubUrl) && !strings.HasPrefix(to.Path, "/"+hs.Cfg.AppSubUrl) {
|
||||||
return login.ErrInvalidRedirectTo
|
return login.ErrInvalidRedirectTo
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -177,6 +179,11 @@ func (hs *HTTPServer) LoginPost(c *models.ReqContext, cmd dtos.LoginCommand) Res
|
|||||||
|
|
||||||
if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
if redirectTo, _ := url.QueryUnescape(c.GetCookie("redirect_to")); len(redirectTo) > 0 {
|
||||||
if err := hs.validateRedirectTo(redirectTo); err == nil {
|
if err := hs.validateRedirectTo(redirectTo); err == nil {
|
||||||
|
// remove subpath if it exists at the beginning of the redirect_to
|
||||||
|
// LoginCtrl.tsx is already prepending the redirectUrl with the subpath
|
||||||
|
if setting.AppSubUrl != "" && strings.Index(redirectTo, setting.AppSubUrl) == 0 {
|
||||||
|
redirectTo = strings.Replace(redirectTo, setting.AppSubUrl, "", 1)
|
||||||
|
}
|
||||||
result["redirectUrl"] = redirectTo
|
result["redirectUrl"] = redirectTo
|
||||||
} else {
|
} else {
|
||||||
log.Info("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
log.Info("Ignored invalid redirect_to cookie value: %v", redirectTo)
|
||||||
|
|||||||
@@ -186,7 +186,13 @@ func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
|
|||||||
if userInfo.Role != "" {
|
if userInfo.Role != "" {
|
||||||
rt := m.RoleType(userInfo.Role)
|
rt := m.RoleType(userInfo.Role)
|
||||||
if rt.IsValid() {
|
if rt.IsValid() {
|
||||||
extUser.OrgRoles[1] = rt
|
var orgID int64
|
||||||
|
if setting.AutoAssignOrg && setting.AutoAssignOrgId > 0 {
|
||||||
|
orgID = int64(setting.AutoAssignOrgId)
|
||||||
|
} else {
|
||||||
|
orgID = int64(1)
|
||||||
|
}
|
||||||
|
extUser.OrgRoles[orgID] = rt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ type redirectCase struct {
|
|||||||
err error
|
err error
|
||||||
appURL string
|
appURL string
|
||||||
appSubURL string
|
appSubURL string
|
||||||
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoginErrorCookieApiEndpoint(t *testing.T) {
|
func TestLoginErrorCookieApiEndpoint(t *testing.T) {
|
||||||
@@ -154,6 +155,7 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
desc: "grafana relative url without subpath",
|
desc: "grafana relative url without subpath",
|
||||||
url: "/profile",
|
url: "/profile",
|
||||||
appURL: "http://localhost:3000",
|
appURL: "http://localhost:3000",
|
||||||
|
path: "/",
|
||||||
status: 302,
|
status: 302,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -161,6 +163,15 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
url: "/grafana/profile",
|
url: "/grafana/profile",
|
||||||
appURL: "http://localhost:3000",
|
appURL: "http://localhost:3000",
|
||||||
appSubURL: "grafana",
|
appSubURL: "grafana",
|
||||||
|
path: "grafana/",
|
||||||
|
status: 302,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "grafana slashed relative url with subpath",
|
||||||
|
url: "/grafana/profile",
|
||||||
|
appURL: "http://localhost:3000",
|
||||||
|
appSubURL: "grafana",
|
||||||
|
path: "/grafana/",
|
||||||
status: 302,
|
status: 302,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -168,13 +179,23 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
url: "/profile",
|
url: "/profile",
|
||||||
appURL: "http://localhost:3000",
|
appURL: "http://localhost:3000",
|
||||||
appSubURL: "grafana",
|
appSubURL: "grafana",
|
||||||
|
path: "grafana/",
|
||||||
status: 200,
|
status: 200,
|
||||||
err: login.ErrInvalidRedirectTo,
|
err: login.ErrInvalidRedirectTo,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "grafana subpath absolute url",
|
||||||
|
url: "http://localhost:3000/grafana/profile",
|
||||||
|
appURL: "http://localhost:3000",
|
||||||
|
appSubURL: "grafana",
|
||||||
|
path: "/grafana/profile",
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "grafana absolute url",
|
desc: "grafana absolute url",
|
||||||
url: "http://localhost:3000/profile",
|
url: "http://localhost:3000/profile",
|
||||||
appURL: "http://localhost:3000",
|
appURL: "http://localhost:3000",
|
||||||
|
path: "/",
|
||||||
status: 200,
|
status: 200,
|
||||||
err: login.ErrAbsoluteRedirectTo,
|
err: login.ErrAbsoluteRedirectTo,
|
||||||
},
|
},
|
||||||
@@ -182,6 +203,7 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
desc: "non grafana absolute url",
|
desc: "non grafana absolute url",
|
||||||
url: "http://example.com",
|
url: "http://example.com",
|
||||||
appURL: "http://localhost:3000",
|
appURL: "http://localhost:3000",
|
||||||
|
path: "/",
|
||||||
status: 200,
|
status: 200,
|
||||||
err: login.ErrAbsoluteRedirectTo,
|
err: login.ErrAbsoluteRedirectTo,
|
||||||
},
|
},
|
||||||
@@ -189,6 +211,7 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
desc: "invalid url",
|
desc: "invalid url",
|
||||||
url: ":foo",
|
url: ":foo",
|
||||||
appURL: "http://localhost:3000",
|
appURL: "http://localhost:3000",
|
||||||
|
path: "/",
|
||||||
status: 200,
|
status: 200,
|
||||||
err: login.ErrInvalidRedirectTo,
|
err: login.ErrInvalidRedirectTo,
|
||||||
},
|
},
|
||||||
@@ -203,7 +226,7 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
MaxAge: 60,
|
MaxAge: 60,
|
||||||
Value: c.url,
|
Value: c.url,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Path: hs.Cfg.AppSubUrl + "/",
|
Path: c.path,
|
||||||
Secure: hs.Cfg.CookieSecure,
|
Secure: hs.Cfg.CookieSecure,
|
||||||
SameSite: hs.Cfg.CookieSameSiteMode,
|
SameSite: hs.Cfg.CookieSameSiteMode,
|
||||||
}
|
}
|
||||||
@@ -219,7 +242,7 @@ func TestLoginViewRedirect(t *testing.T) {
|
|||||||
assert.True(t, ok, "Set-Cookie exists")
|
assert.True(t, ok, "Set-Cookie exists")
|
||||||
assert.Greater(t, len(setCookie), 0)
|
assert.Greater(t, len(setCookie), 0)
|
||||||
var redirectToCookieFound bool
|
var redirectToCookieFound bool
|
||||||
expCookieValue := fmt.Sprintf("redirect_to=%v; Path=%v; Max-Age=60; HttpOnly; Secure", c.url, hs.Cfg.AppSubUrl+"/")
|
expCookieValue := fmt.Sprintf("redirect_to=%v; Path=%v; Max-Age=60; HttpOnly; Secure", c.url, c.path)
|
||||||
for _, cookieValue := range setCookie {
|
for _, cookieValue := range setCookie {
|
||||||
if cookieValue == expCookieValue {
|
if cookieValue == expCookieValue {
|
||||||
redirectToCookieFound = true
|
redirectToCookieFound = true
|
||||||
@@ -281,6 +304,12 @@ func TestLoginPostRedirect(t *testing.T) {
|
|||||||
appURL: "https://localhost:3000",
|
appURL: "https://localhost:3000",
|
||||||
appSubURL: "grafana",
|
appSubURL: "grafana",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "grafana no slash relative url with subpath",
|
||||||
|
url: "grafana/profile",
|
||||||
|
appURL: "https://localhost:3000",
|
||||||
|
appSubURL: "grafana",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
desc: "relative url with missing subpath",
|
desc: "relative url with missing subpath",
|
||||||
url: "/profile",
|
url: "/profile",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
@@ -17,7 +18,7 @@ import (
|
|||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
glog "github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/login/social"
|
"github.com/grafana/grafana/pkg/login/social"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
@@ -26,7 +27,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logger = log.New("data-proxy-log")
|
logger = glog.New("data-proxy-log")
|
||||||
client = newHTTPClient()
|
client = newHTTPClient()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,6 +58,18 @@ type httpClient interface {
|
|||||||
Do(req *http.Request) (*http.Response, error)
|
Do(req *http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type logWrapper struct {
|
||||||
|
logger glog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes log messages as bytes from proxy
|
||||||
|
func (lw *logWrapper) Write(p []byte) (n int, err error) {
|
||||||
|
withoutNewline := strings.TrimSuffix(string(p), "\n")
|
||||||
|
lw.logger.Error("Data proxy error", "error", withoutNewline)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDataSourceProxy creates a new Datasource proxy
|
||||||
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string, cfg *setting.Cfg) *DataSourceProxy {
|
func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string, cfg *setting.Cfg) *DataSourceProxy {
|
||||||
targetURL, _ := url.Parse(ds.Url)
|
targetURL, _ := url.Parse(ds.Url)
|
||||||
|
|
||||||
@@ -83,9 +96,12 @@ func (proxy *DataSourceProxy) HandleRequest() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxyErrorLogger := logger.New("userId", proxy.ctx.UserId, "orgId", proxy.ctx.OrgId, "uname", proxy.ctx.Login, "path", proxy.ctx.Req.URL.Path, "remote_addr", proxy.ctx.RemoteAddr(), "referer", proxy.ctx.Req.Referer())
|
||||||
|
|
||||||
reverseProxy := &httputil.ReverseProxy{
|
reverseProxy := &httputil.ReverseProxy{
|
||||||
Director: proxy.getDirector(),
|
Director: proxy.getDirector(),
|
||||||
FlushInterval: time.Millisecond * 200,
|
FlushInterval: time.Millisecond * 200,
|
||||||
|
ErrorLog: log.New(&logWrapper{logger: proxyErrorLogger}, "", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
transport, err := proxy.ds.GetHttpTransport()
|
transport, err := proxy.ds.GetHttpTransport()
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
httpStatusCodes := []string{"200", "404", "500", "unknown"}
|
httpStatusCodes := []string{"200", "404", "500", "unknown"}
|
||||||
|
objectiveMap := map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}
|
||||||
|
|
||||||
MInstanceStart = prometheus.NewCounter(prometheus.CounterOpts{
|
MInstanceStart = prometheus.NewCounter(prometheus.CounterOpts{
|
||||||
Name: "instance_start_total",
|
Name: "instance_start_total",
|
||||||
Help: "counter for started instances",
|
Help: "counter for started instances",
|
||||||
@@ -191,8 +193,9 @@ func init() {
|
|||||||
|
|
||||||
MHttpRequestSummary = prometheus.NewSummaryVec(
|
MHttpRequestSummary = prometheus.NewSummaryVec(
|
||||||
prometheus.SummaryOpts{
|
prometheus.SummaryOpts{
|
||||||
Name: "http_request_duration_milliseconds",
|
Name: "http_request_duration_milliseconds",
|
||||||
Help: "http request summary",
|
Help: "http request summary",
|
||||||
|
Objectives: objectiveMap,
|
||||||
},
|
},
|
||||||
[]string{"handler", "statuscode", "method"},
|
[]string{"handler", "statuscode", "method"},
|
||||||
)
|
)
|
||||||
@@ -216,21 +219,24 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
MApiDashboardSave = prometheus.NewSummary(prometheus.SummaryOpts{
|
MApiDashboardSave = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||||
Name: "api_dashboard_save_milliseconds",
|
Name: "api_dashboard_save_milliseconds",
|
||||||
Help: "summary for dashboard save duration",
|
Help: "summary for dashboard save duration",
|
||||||
Namespace: exporterName,
|
Objectives: objectiveMap,
|
||||||
|
Namespace: exporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
MApiDashboardGet = prometheus.NewSummary(prometheus.SummaryOpts{
|
MApiDashboardGet = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||||
Name: "api_dashboard_get_milliseconds",
|
Name: "api_dashboard_get_milliseconds",
|
||||||
Help: "summary for dashboard get duration",
|
Help: "summary for dashboard get duration",
|
||||||
Namespace: exporterName,
|
Objectives: objectiveMap,
|
||||||
|
Namespace: exporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
MApiDashboardSearch = prometheus.NewSummary(prometheus.SummaryOpts{
|
MApiDashboardSearch = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||||
Name: "api_dashboard_search_milliseconds",
|
Name: "api_dashboard_search_milliseconds",
|
||||||
Help: "summary for dashboard search duration",
|
Help: "summary for dashboard search duration",
|
||||||
Namespace: exporterName,
|
Objectives: objectiveMap,
|
||||||
|
Namespace: exporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
MApiAdminUserCreate = newCounterStartingAtZero(prometheus.CounterOpts{
|
MApiAdminUserCreate = newCounterStartingAtZero(prometheus.CounterOpts{
|
||||||
@@ -330,21 +336,24 @@ func init() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
LDAPUsersSyncExecutionTime = prometheus.NewSummary(prometheus.SummaryOpts{
|
LDAPUsersSyncExecutionTime = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||||
Name: "ldap_users_sync_execution_time",
|
Name: "ldap_users_sync_execution_time",
|
||||||
Help: "summary for LDAP users sync execution duration",
|
Help: "summary for LDAP users sync execution duration",
|
||||||
Namespace: exporterName,
|
Objectives: objectiveMap,
|
||||||
|
Namespace: exporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
MDataSourceProxyReqTimer = prometheus.NewSummary(prometheus.SummaryOpts{
|
MDataSourceProxyReqTimer = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||||
Name: "api_dataproxy_request_all_milliseconds",
|
Name: "api_dataproxy_request_all_milliseconds",
|
||||||
Help: "summary for dataproxy request duration",
|
Help: "summary for dataproxy request duration",
|
||||||
Namespace: exporterName,
|
Objectives: objectiveMap,
|
||||||
|
Namespace: exporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
MAlertingExecutionTime = prometheus.NewSummary(prometheus.SummaryOpts{
|
MAlertingExecutionTime = prometheus.NewSummary(prometheus.SummaryOpts{
|
||||||
Name: "alerting_execution_time_milliseconds",
|
Name: "alerting_execution_time_milliseconds",
|
||||||
Help: "summary of alert exeuction duration",
|
Help: "summary of alert exeuction duration",
|
||||||
Namespace: exporterName,
|
Objectives: objectiveMap,
|
||||||
|
Namespace: exporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
MAlertingActiveAlerts = prometheus.NewGauge(prometheus.GaugeOpts{
|
MAlertingActiveAlerts = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ var loginUsingLDAP = func(query *models.LoginUserQuery) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
upsert := &models.UpsertUserCommand{
|
upsert := &models.UpsertUserCommand{
|
||||||
|
ReqContext: query.ReqContext,
|
||||||
ExternalUser: externalUser,
|
ExternalUser: externalUser,
|
||||||
SignupAllowed: setting.LDAPAllowSignup,
|
SignupAllowed: setting.LDAPAllowSignup,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func notAuthorized(c *m.ReqContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteCookie(c.Resp, "redirect_to", url.QueryEscape(setting.AppSubUrl+c.Req.RequestURI), 0, newCookieOptions)
|
WriteCookie(c.Resp, "redirect_to", url.QueryEscape(c.Req.RequestURI), 0, newCookieOptions)
|
||||||
|
|
||||||
c.Redirect(setting.AppSubUrl + "/login")
|
c.Redirect(setting.AppSubUrl + "/login")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -228,7 +229,24 @@ func initContextWithToken(authTokenService models.UserTokenService, ctx *models.
|
|||||||
|
|
||||||
// Rotate the token just before we write response headers to ensure there is no delay between
|
// Rotate the token just before we write response headers to ensure there is no delay between
|
||||||
// the new token being generated and the client receiving it.
|
// the new token being generated and the client receiving it.
|
||||||
ctx.Resp.Before(func(w macaron.ResponseWriter) {
|
ctx.Resp.Before(rotateEndOfRequestFunc(ctx, authTokenService, token))
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func rotateEndOfRequestFunc(ctx *models.ReqContext, authTokenService models.UserTokenService, token *models.UserToken) macaron.BeforeFunc {
|
||||||
|
return func(w macaron.ResponseWriter) {
|
||||||
|
// if response has already been written, skip.
|
||||||
|
if w.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the request is cancelled by the client we should not try
|
||||||
|
// to rotate the token since the client would not accept any result.
|
||||||
|
if ctx.Context.Req.Context().Err() == context.Canceled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rotated, err := authTokenService.TryRotateToken(ctx.Req.Context(), token, ctx.RemoteAddr(), ctx.Req.UserAgent())
|
rotated, err := authTokenService.TryRotateToken(ctx.Req.Context(), token, ctx.RemoteAddr(), ctx.Req.UserAgent())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Logger.Error("Failed to rotate token", "error", err)
|
ctx.Logger.Error("Failed to rotate token", "error", err)
|
||||||
@@ -238,9 +256,7 @@ func initContextWithToken(authTokenService models.UserTokenService, ctx *models.
|
|||||||
if rotated {
|
if rotated {
|
||||||
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays)
|
WriteSessionCookie(ctx, token.UnhashedToken, setting.LoginMaxLifetimeDays)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays int) {
|
func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays int) {
|
||||||
@@ -262,6 +278,11 @@ func WriteSessionCookie(ctx *models.ReqContext, value string, maxLifetimeDays in
|
|||||||
func AddDefaultResponseHeaders() macaron.Handler {
|
func AddDefaultResponseHeaders() macaron.Handler {
|
||||||
return func(ctx *macaron.Context) {
|
return func(ctx *macaron.Context) {
|
||||||
ctx.Resp.Before(func(w macaron.ResponseWriter) {
|
ctx.Resp.Before(func(w macaron.ResponseWriter) {
|
||||||
|
// if response has already been written, skip.
|
||||||
|
if w.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(ctx.Req.URL.Path, "/api/datasources/proxy/") {
|
if !strings.HasPrefix(ctx.Req.URL.Path, "/api/datasources/proxy/") {
|
||||||
AddNoCacheHeaders(ctx.Resp)
|
AddNoCacheHeaders(ctx.Resp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,19 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/remotecache"
|
"github.com/grafana/grafana/pkg/infra/remotecache"
|
||||||
authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy"
|
authproxy "github.com/grafana/grafana/pkg/middleware/auth_proxy"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
@@ -541,7 +544,8 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
|||||||
|
|
||||||
sc := &scenarioContext{}
|
sc := &scenarioContext{}
|
||||||
|
|
||||||
viewsPath, _ := filepath.Abs("../../public/views")
|
viewsPath, err := filepath.Abs("../../public/views")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
sc.m = macaron.New()
|
sc.m = macaron.New()
|
||||||
sc.m.Use(AddDefaultResponseHeaders())
|
sc.m.Use(AddDefaultResponseHeaders())
|
||||||
@@ -571,3 +575,88 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc) {
|
|||||||
fn(sc)
|
fn(sc)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDontRotateTokensOnCancelledRequests(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
reqContext, _, err := initTokenRotationTest(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tryRotateCallCount := 0
|
||||||
|
uts := &auth.FakeUserAuthTokenService{
|
||||||
|
TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) {
|
||||||
|
tryRotateCallCount++
|
||||||
|
return false, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &models.UserToken{AuthToken: "oldtoken"}
|
||||||
|
|
||||||
|
fn := rotateEndOfRequestFunc(reqContext, uts, token)
|
||||||
|
cancel()
|
||||||
|
fn(reqContext.Resp)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, tryRotateCallCount, "Token rotation was attempted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenRotationAtEndOfRequest(t *testing.T) {
|
||||||
|
reqContext, rr, err := initTokenRotationTest(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
uts := &auth.FakeUserAuthTokenService{
|
||||||
|
TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) {
|
||||||
|
newToken, err := util.RandomHex(16)
|
||||||
|
require.NoError(t, err)
|
||||||
|
token.AuthToken = newToken
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := &models.UserToken{AuthToken: "oldtoken"}
|
||||||
|
|
||||||
|
rotateEndOfRequestFunc(reqContext, uts, token)(reqContext.Resp)
|
||||||
|
|
||||||
|
foundLoginCookie := false
|
||||||
|
for _, c := range rr.Result().Cookies() {
|
||||||
|
if c.Name == "login_token" {
|
||||||
|
foundLoginCookie = true
|
||||||
|
|
||||||
|
require.NotEqual(t, token.AuthToken, c.Value, "Auth token is still the same")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, foundLoginCookie, "Could not find cookie")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initTokenRotationTest(ctx context.Context) (*models.ReqContext, *httptest.ResponseRecorder, error) {
|
||||||
|
setting.LoginCookieName = "login_token"
|
||||||
|
setting.LoginMaxLifetimeDays = 7
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
reqContext := &models.ReqContext{
|
||||||
|
Context: &macaron.Context{
|
||||||
|
Req: macaron.Request{
|
||||||
|
Request: req,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Logger: log.New("testlogger"),
|
||||||
|
}
|
||||||
|
|
||||||
|
mw := mockWriter{rr}
|
||||||
|
reqContext.Resp = mw
|
||||||
|
|
||||||
|
return reqContext, rr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockWriter struct {
|
||||||
|
*httptest.ResponseRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mw mockWriter) Flush() {}
|
||||||
|
func (mw mockWriter) Status() int { return 0 }
|
||||||
|
func (mw mockWriter) Size() int { return 0 }
|
||||||
|
func (mw mockWriter) Written() bool { return false }
|
||||||
|
func (mw mockWriter) Before(macaron.BeforeFunc) {}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
@@ -102,8 +103,6 @@ func Recovery() macaron.Handler {
|
|||||||
return func(c *macaron.Context) {
|
return func(c *macaron.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
stack := stack(3)
|
|
||||||
|
|
||||||
panicLogger := log.Root
|
panicLogger := log.Root
|
||||||
// try to get request logger
|
// try to get request logger
|
||||||
if ctx, ok := c.Data["ctx"]; ok {
|
if ctx, ok := c.Data["ctx"]; ok {
|
||||||
@@ -111,8 +110,22 @@ func Recovery() macaron.Handler {
|
|||||||
panicLogger = ctxTyped.Logger
|
panicLogger = ctxTyped.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// http.ErrAbortHandler is suppressed by default in the http package
|
||||||
|
// and used as a signal for aborting requests. Suppresses stacktrace
|
||||||
|
// since it doesn't add any important information.
|
||||||
|
if err == http.ErrAbortHandler {
|
||||||
|
panicLogger.Error("Request error", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stack := stack(3)
|
||||||
panicLogger.Error("Request error", "error", err, "stack", string(stack))
|
panicLogger.Error("Request error", "error", err, "stack", string(stack))
|
||||||
|
|
||||||
|
// if response has already been written, skip.
|
||||||
|
if c.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.Data["Title"] = "Server Error"
|
c.Data["Title"] = "Server Error"
|
||||||
c.Data["AppSubUrl"] = setting.AppSubUrl
|
c.Data["AppSubUrl"] = setting.AppSubUrl
|
||||||
c.Data["Theme"] = setting.DefaultTheme
|
c.Data["Theme"] = setting.DefaultTheme
|
||||||
|
|||||||
@@ -7,13 +7,32 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/metrics"
|
"github.com/grafana/grafana/pkg/infra/metrics"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"gopkg.in/macaron.v1"
|
"gopkg.in/macaron.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpRequestsInFlight prometheus.Gauge
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
httpRequestsInFlight = prometheus.NewGauge(
|
||||||
|
prometheus.GaugeOpts{
|
||||||
|
Name: "http_request_in_flight",
|
||||||
|
Help: "A gauge of requests currently being served by Grafana.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
prometheus.MustRegister(httpRequestsInFlight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestMetrics is a middleware handler that instruments the request
|
||||||
func RequestMetrics(handler string) macaron.Handler {
|
func RequestMetrics(handler string) macaron.Handler {
|
||||||
return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
|
return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) {
|
||||||
rw := res.(macaron.ResponseWriter)
|
rw := res.(macaron.ResponseWriter)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
httpRequestsInFlight.Inc()
|
||||||
|
defer httpRequestsInFlight.Dec()
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|
||||||
status := rw.Status()
|
status := rw.Status()
|
||||||
|
|||||||
@@ -9,4 +9,11 @@ type Licensing interface {
|
|||||||
|
|
||||||
// Expiry returns the unix epoch timestamp when the license expires, or 0 if no valid license is provided
|
// Expiry returns the unix epoch timestamp when the license expires, or 0 if no valid license is provided
|
||||||
Expiry() int64
|
Expiry() int64
|
||||||
|
|
||||||
|
// Return edition
|
||||||
|
Edition() string
|
||||||
|
|
||||||
|
LicenseURL(user *SignedInUser) string
|
||||||
|
|
||||||
|
StateInfo() string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ package hooks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IndexDataHook func(indexData *dtos.IndexViewData)
|
type IndexDataHook func(indexData *dtos.IndexViewData, req *models.ReqContext)
|
||||||
|
|
||||||
type HooksService struct {
|
type HooksService struct {
|
||||||
indexDataHooks []IndexDataHook
|
indexDataHooks []IndexDataHook
|
||||||
@@ -23,8 +24,8 @@ func (srv *HooksService) AddIndexDataHook(hook IndexDataHook) {
|
|||||||
srv.indexDataHooks = append(srv.indexDataHooks, hook)
|
srv.indexDataHooks = append(srv.indexDataHooks, hook)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData) {
|
func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData, req *models.ReqContext) {
|
||||||
for _, hook := range srv.indexDataHooks {
|
for _, hook := range srv.indexDataHooks {
|
||||||
hook(indexData)
|
hook(indexData, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package licensing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/hooks"
|
"github.com/grafana/grafana/pkg/services/hooks"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
@@ -19,14 +20,30 @@ func (*OSSLicensingService) Expiry() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*OSSLicensingService) Edition() string {
|
||||||
|
return "Open Source"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*OSSLicensingService) StateInfo() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *OSSLicensingService) LicenseURL(user *models.SignedInUser) string {
|
||||||
|
if user.IsGrafanaAdmin {
|
||||||
|
return l.Cfg.AppSubUrl + "/admin/upgrading"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://grafana.com/products/enterprise/?utm_source=grafana_footer"
|
||||||
|
}
|
||||||
|
|
||||||
func (l *OSSLicensingService) Init() error {
|
func (l *OSSLicensingService) Init() error {
|
||||||
l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData) {
|
l.HooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *models.ReqContext) {
|
||||||
for _, node := range indexData.NavTree {
|
for _, node := range indexData.NavTree {
|
||||||
if node.Id == "admin" {
|
if node.Id == "admin" {
|
||||||
node.Children = append(node.Children, &dtos.NavLink{
|
node.Children = append(node.Children, &dtos.NavLink{
|
||||||
Text: "Upgrade",
|
Text: "Upgrade",
|
||||||
Id: "upgrading",
|
Id: "upgrading",
|
||||||
Url: l.Cfg.AppSubUrl + "/admin/upgrading",
|
Url: l.LicenseURL(req.SignedInUser),
|
||||||
Icon: "fa fa-fw fa-unlock-alt",
|
Icon: "fa fa-fw fa-unlock-alt",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func (ls *LoginService) Init() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpsertUser updates an existing user, or if it doesn't exist, inserts a new one.
|
||||||
func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
|
func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
|
||||||
extUser := cmd.ExternalUser
|
extUser := cmd.ExternalUser
|
||||||
|
|
||||||
@@ -38,12 +39,10 @@ func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
|
|||||||
Login: extUser.Login,
|
Login: extUser.Login,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := bus.Dispatch(userQuery)
|
if err := bus.Dispatch(userQuery); err != nil {
|
||||||
if err != models.ErrUserNotFound && err != nil {
|
if err != models.ErrUserNotFound {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if !cmd.SignupAllowed {
|
if !cmd.SignupAllowed {
|
||||||
log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
|
log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule)
|
||||||
return ErrInvalidCredentials
|
return ErrInvalidCredentials
|
||||||
@@ -74,7 +73,6 @@ func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
cmd.Result = userQuery.Result
|
cmd.Result = userQuery.Result
|
||||||
|
|
||||||
@@ -99,9 +97,7 @@ func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = syncOrgRoles(cmd.Result, extUser)
|
if err := syncOrgRoles(cmd.Result, extUser); err != nil {
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,16 +108,15 @@ func (ls *LoginService) UpsertUser(cmd *models.UpsertUserCommand) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ls.Bus.Dispatch(&models.SyncTeamsCommand{
|
err := ls.Bus.Dispatch(&models.SyncTeamsCommand{
|
||||||
User: cmd.Result,
|
User: cmd.Result,
|
||||||
ExternalUser: extUser,
|
ExternalUser: extUser,
|
||||||
})
|
})
|
||||||
|
if err != nil && err != bus.ErrHandlerNotFound {
|
||||||
if err == bus.ErrHandlerNotFound {
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUser(extUser *models.ExternalUserInfo) (*models.User, error) {
|
func createUser(extUser *models.ExternalUserInfo) (*models.User, error) {
|
||||||
|
|||||||
@@ -446,3 +446,76 @@ func TestMultiLDAP(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mockLDAP represents testing struct for ldap testing
|
||||||
|
type mockLDAP struct {
|
||||||
|
dialCalledTimes int
|
||||||
|
loginCalledTimes int
|
||||||
|
closeCalledTimes int
|
||||||
|
usersCalledTimes int
|
||||||
|
bindCalledTimes int
|
||||||
|
|
||||||
|
dialErrReturn error
|
||||||
|
|
||||||
|
loginErrReturn error
|
||||||
|
loginReturn *models.ExternalUserInfo
|
||||||
|
|
||||||
|
bindErrReturn error
|
||||||
|
|
||||||
|
usersErrReturn error
|
||||||
|
usersFirstReturn []*models.ExternalUserInfo
|
||||||
|
usersRestReturn []*models.ExternalUserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login test fn
|
||||||
|
func (mock *mockLDAP) Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) {
|
||||||
|
|
||||||
|
mock.loginCalledTimes = mock.loginCalledTimes + 1
|
||||||
|
return mock.loginReturn, mock.loginErrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users test fn
|
||||||
|
func (mock *mockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) {
|
||||||
|
mock.usersCalledTimes = mock.usersCalledTimes + 1
|
||||||
|
|
||||||
|
if mock.usersCalledTimes == 1 {
|
||||||
|
return mock.usersFirstReturn, mock.usersErrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
return mock.usersRestReturn, mock.usersErrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserBind test fn
|
||||||
|
func (mock *mockLDAP) UserBind(string, string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial test fn
|
||||||
|
func (mock *mockLDAP) Dial() error {
|
||||||
|
mock.dialCalledTimes = mock.dialCalledTimes + 1
|
||||||
|
return mock.dialErrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close test fn
|
||||||
|
func (mock *mockLDAP) Close() {
|
||||||
|
mock.closeCalledTimes = mock.closeCalledTimes + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mock *mockLDAP) Bind() error {
|
||||||
|
mock.bindCalledTimes++
|
||||||
|
return mock.bindErrReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() *mockLDAP {
|
||||||
|
mock := &mockLDAP{}
|
||||||
|
|
||||||
|
newLDAP = func(config *ldap.ServerConfig) ldap.IServer {
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {
|
||||||
|
newLDAP = ldap.New
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
package multildap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/grafana/grafana/pkg/models"
|
|
||||||
"github.com/grafana/grafana/pkg/services/ldap"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockLDAP represents testing struct for ldap testing
|
|
||||||
type MockLDAP struct {
|
|
||||||
dialCalledTimes int
|
|
||||||
loginCalledTimes int
|
|
||||||
closeCalledTimes int
|
|
||||||
usersCalledTimes int
|
|
||||||
bindCalledTimes int
|
|
||||||
|
|
||||||
dialErrReturn error
|
|
||||||
|
|
||||||
loginErrReturn error
|
|
||||||
loginReturn *models.ExternalUserInfo
|
|
||||||
|
|
||||||
bindErrReturn error
|
|
||||||
|
|
||||||
usersErrReturn error
|
|
||||||
usersFirstReturn []*models.ExternalUserInfo
|
|
||||||
usersRestReturn []*models.ExternalUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login test fn
|
|
||||||
func (mock *MockLDAP) Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error) {
|
|
||||||
|
|
||||||
mock.loginCalledTimes = mock.loginCalledTimes + 1
|
|
||||||
return mock.loginReturn, mock.loginErrReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users test fn
|
|
||||||
func (mock *MockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) {
|
|
||||||
mock.usersCalledTimes = mock.usersCalledTimes + 1
|
|
||||||
|
|
||||||
if mock.usersCalledTimes == 1 {
|
|
||||||
return mock.usersFirstReturn, mock.usersErrReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
return mock.usersRestReturn, mock.usersErrReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserBind test fn
|
|
||||||
func (mock *MockLDAP) UserBind(string, string) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial test fn
|
|
||||||
func (mock *MockLDAP) Dial() error {
|
|
||||||
mock.dialCalledTimes = mock.dialCalledTimes + 1
|
|
||||||
return mock.dialErrReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close test fn
|
|
||||||
func (mock *MockLDAP) Close() {
|
|
||||||
mock.closeCalledTimes = mock.closeCalledTimes + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mock *MockLDAP) Bind() error {
|
|
||||||
mock.bindCalledTimes++
|
|
||||||
return mock.bindErrReturn
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockMultiLDAP represents testing struct for multildap testing
|
|
||||||
type MockMultiLDAP struct {
|
|
||||||
LoginCalledTimes int
|
|
||||||
UsersCalledTimes int
|
|
||||||
UserCalledTimes int
|
|
||||||
PingCalledTimes int
|
|
||||||
|
|
||||||
UsersResult []*models.ExternalUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mock *MockMultiLDAP) Ping() ([]*ServerStatus, error) {
|
|
||||||
mock.PingCalledTimes = mock.PingCalledTimes + 1
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login test fn
|
|
||||||
func (mock *MockMultiLDAP) Login(query *models.LoginUserQuery) (
|
|
||||||
*models.ExternalUserInfo, error,
|
|
||||||
) {
|
|
||||||
mock.LoginCalledTimes = mock.LoginCalledTimes + 1
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Users test fn
|
|
||||||
func (mock *MockMultiLDAP) Users(logins []string) (
|
|
||||||
[]*models.ExternalUserInfo, error,
|
|
||||||
) {
|
|
||||||
mock.UsersCalledTimes = mock.UsersCalledTimes + 1
|
|
||||||
return mock.UsersResult, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// User test fn
|
|
||||||
func (mock *MockMultiLDAP) User(login string) (
|
|
||||||
*models.ExternalUserInfo, ldap.ServerConfig, error,
|
|
||||||
) {
|
|
||||||
mock.UserCalledTimes = mock.UserCalledTimes + 1
|
|
||||||
return nil, ldap.ServerConfig{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() *MockLDAP {
|
|
||||||
mock := &MockLDAP{}
|
|
||||||
|
|
||||||
newLDAP = func(config *ldap.ServerConfig) ldap.IServer {
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
func teardown() {
|
|
||||||
newLDAP = ldap.New
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,12 @@ func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, erro
|
|||||||
if !setting.Quota.Enabled {
|
if !setting.Quota.Enabled {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
// No request context means this is a background service, like LDAP Background Sync.
|
||||||
|
// TODO: we should replace the req context with a more limited interface or struct,
|
||||||
|
// something that we could easily provide from background jobs.
|
||||||
|
if c == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
// get the list of scopes that this target is valid for. Org, User, Global
|
// get the list of scopes that this target is valid for. Org, User, Global
|
||||||
scopes, err := m.GetQuotaScopes(target)
|
scopes, err := m.GetQuotaScopes(target)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ func init() {
|
|||||||
registry.RegisterService(&RenderingService{})
|
registry.RegisterService(&RenderingService{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var IsPhantomJSEnabled = false
|
||||||
|
|
||||||
type RenderingService struct {
|
type RenderingService struct {
|
||||||
log log.Logger
|
log log.Logger
|
||||||
pluginInfo *plugins.RendererPlugin
|
pluginInfo *plugins.RendererPlugin
|
||||||
@@ -68,6 +70,7 @@ func (rs *RenderingService) Run(ctx context.Context) error {
|
|||||||
rs.log.Warn("phantomJS is deprecated and will be removed in a future release. " +
|
rs.log.Warn("phantomJS is deprecated and will be removed in a future release. " +
|
||||||
"You should consider migrating from phantomJS to grafana-image-renderer plugin.")
|
"You should consider migrating from phantomJS to grafana-image-renderer plugin.")
|
||||||
rs.renderAction = rs.renderViaPhantomJS
|
rs.renderAction = rs.renderViaPhantomJS
|
||||||
|
IsPhantomJSEnabled = true
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,46 +144,48 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
FROM annotation
|
FROM annotation
|
||||||
LEFT OUTER JOIN ` + dialect.Quote("user") + ` as usr on usr.id = annotation.user_id
|
LEFT OUTER JOIN ` + dialect.Quote("user") + ` as usr on usr.id = annotation.user_id
|
||||||
LEFT OUTER JOIN alert on alert.id = annotation.alert_id
|
LEFT OUTER JOIN alert on alert.id = annotation.alert_id
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT a.id from annotation a
|
||||||
`)
|
`)
|
||||||
|
|
||||||
sql.WriteString(`WHERE annotation.org_id = ?`)
|
sql.WriteString(`WHERE a.org_id = ?`)
|
||||||
params = append(params, query.OrgId)
|
params = append(params, query.OrgId)
|
||||||
|
|
||||||
if query.AnnotationId != 0 {
|
if query.AnnotationId != 0 {
|
||||||
// fmt.Print("annotation query")
|
// fmt.Print("annotation query")
|
||||||
sql.WriteString(` AND annotation.id = ?`)
|
sql.WriteString(` AND a.id = ?`)
|
||||||
params = append(params, query.AnnotationId)
|
params = append(params, query.AnnotationId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.AlertId != 0 {
|
if query.AlertId != 0 {
|
||||||
sql.WriteString(` AND annotation.alert_id = ?`)
|
sql.WriteString(` AND a.alert_id = ?`)
|
||||||
params = append(params, query.AlertId)
|
params = append(params, query.AlertId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.DashboardId != 0 {
|
if query.DashboardId != 0 {
|
||||||
sql.WriteString(` AND annotation.dashboard_id = ?`)
|
sql.WriteString(` AND a.dashboard_id = ?`)
|
||||||
params = append(params, query.DashboardId)
|
params = append(params, query.DashboardId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.PanelId != 0 {
|
if query.PanelId != 0 {
|
||||||
sql.WriteString(` AND annotation.panel_id = ?`)
|
sql.WriteString(` AND a.panel_id = ?`)
|
||||||
params = append(params, query.PanelId)
|
params = append(params, query.PanelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.UserId != 0 {
|
if query.UserId != 0 {
|
||||||
sql.WriteString(` AND annotation.user_id = ?`)
|
sql.WriteString(` AND a.user_id = ?`)
|
||||||
params = append(params, query.UserId)
|
params = append(params, query.UserId)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.From > 0 && query.To > 0 {
|
if query.From > 0 && query.To > 0 {
|
||||||
sql.WriteString(` AND annotation.epoch <= ? AND annotation.epoch_end >= ?`)
|
sql.WriteString(` AND a.epoch <= ? AND a.epoch_end >= ?`)
|
||||||
params = append(params, query.To, query.From)
|
params = append(params, query.To, query.From)
|
||||||
}
|
}
|
||||||
|
|
||||||
if query.Type == "alert" {
|
if query.Type == "alert" {
|
||||||
sql.WriteString(` AND annotation.alert_id > 0`)
|
sql.WriteString(` AND a.alert_id > 0`)
|
||||||
} else if query.Type == "annotation" {
|
} else if query.Type == "annotation" {
|
||||||
sql.WriteString(` AND annotation.alert_id = 0`)
|
sql.WriteString(` AND a.alert_id = 0`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(query.Tags) > 0 {
|
if len(query.Tags) > 0 {
|
||||||
@@ -204,7 +206,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
tagsSubQuery := fmt.Sprintf(`
|
tagsSubQuery := fmt.Sprintf(`
|
||||||
SELECT SUM(1) FROM annotation_tag at
|
SELECT SUM(1) FROM annotation_tag at
|
||||||
INNER JOIN tag on tag.id = at.tag_id
|
INNER JOIN tag on tag.id = at.tag_id
|
||||||
WHERE at.annotation_id = annotation.id
|
WHERE at.annotation_id = a.id
|
||||||
AND (
|
AND (
|
||||||
%s
|
%s
|
||||||
)
|
)
|
||||||
@@ -223,7 +225,8 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
query.Limit = 100
|
query.Limit = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.WriteString(" ORDER BY epoch DESC" + dialect.Limit(query.Limit))
|
// order of ORDER BY arguments match the order of a sql index for performance
|
||||||
|
sql.WriteString(" ORDER BY a.org_id, a.epoch_end DESC, a.epoch DESC" + dialect.Limit(query.Limit) + " ) dt on dt.id = annotation.id")
|
||||||
|
|
||||||
items := make([]*annotations.ItemDTO, 0)
|
items := make([]*annotations.ItemDTO, 0)
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,28 @@ func addAnnotationMig(mg *Migrator) {
|
|||||||
mg.AddMigration("Make epoch_end the same as epoch", NewRawSqlMigration("UPDATE annotation SET epoch_end = epoch"))
|
mg.AddMigration("Make epoch_end the same as epoch", NewRawSqlMigration("UPDATE annotation SET epoch_end = epoch"))
|
||||||
mg.AddMigration("Move region to single row", &AddMakeRegionSingleRowMigration{})
|
mg.AddMigration("Move region to single row", &AddMakeRegionSingleRowMigration{})
|
||||||
|
|
||||||
// TODO! drop region_id column?
|
//
|
||||||
|
// 6.6.1: Optimize annotation queries
|
||||||
|
//
|
||||||
|
mg.AddMigration("Remove index org_id_epoch from annotation table", NewDropIndexMigration(table, &Index{
|
||||||
|
Cols: []string{"org_id", "epoch"}, Type: IndexType,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Remove index org_id_dashboard_id_panel_id_epoch from annotation table", NewDropIndexMigration(table, &Index{
|
||||||
|
Cols: []string{"org_id", "dashboard_id", "panel_id", "epoch"}, Type: IndexType,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Add index for org_id_dashboard_id_epoch_end_epoch on annotation table", NewAddIndexMigration(table, &Index{
|
||||||
|
Cols: []string{"org_id", "dashboard_id", "epoch_end", "epoch"}, Type: IndexType,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Add index for org_id_epoch_end_epoch on annotation table", NewAddIndexMigration(table, &Index{
|
||||||
|
Cols: []string{"org_id", "epoch_end", "epoch"}, Type: IndexType,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Remove index org_id_epoch_epoch_end from annotation table", NewDropIndexMigration(table, &Index{
|
||||||
|
Cols: []string{"org_id", "epoch", "epoch_end"}, Type: IndexType,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddMakeRegionSingleRowMigration struct {
|
type AddMakeRegionSingleRowMigration struct {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/lib/pq"
|
"github.com/lib/pq"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,3 +156,15 @@ func (db *Postgres) IsUniqueConstraintViolation(err error) bool {
|
|||||||
func (db *Postgres) IsDeadlock(err error) bool {
|
func (db *Postgres) IsDeadlock(err error) bool {
|
||||||
return db.isThisError(err, "40P01")
|
return db.isThisError(err, "40P01")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Postgres) PostInsertId(table string, sess *xorm.Session) error {
|
||||||
|
if table != "org" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sync primary key sequence of org table
|
||||||
|
if _, err := sess.Exec("SELECT setval('org_id_seq', (SELECT max(id) FROM org));"); err != nil {
|
||||||
|
return errutil.Wrapf(err, "failed to sync primary key for org table")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,11 +36,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DEV = "development"
|
DEV = "development"
|
||||||
PROD = "production"
|
PROD = "production"
|
||||||
TEST = "test"
|
TEST = "test"
|
||||||
APP_NAME = "Grafana"
|
APP_NAME = "Grafana"
|
||||||
APP_NAME_ENTERPRISE = "Grafana Enterprise"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -619,9 +618,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
Raw = cfg.Raw
|
Raw = cfg.Raw
|
||||||
|
|
||||||
ApplicationName = APP_NAME
|
ApplicationName = APP_NAME
|
||||||
if IsEnterprise {
|
|
||||||
ApplicationName = APP_NAME_ENTERPRISE
|
|
||||||
}
|
|
||||||
|
|
||||||
Env, err = valueAsString(iniFile.Section(""), "app_mode", "development")
|
Env, err = valueAsString(iniFile.Section(""), "app_mode", "development")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ func init() {
|
|||||||
"AWS/DMS": {"CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCIncomingChanges", "CDCLatencySource", "CDCLatencyTarget", "CDCThroughputBandwidthSource", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CPUUtilization", "FreeStorageSpace", "FreeableMemory", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SwapUsage", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
"AWS/DMS": {"CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCIncomingChanges", "CDCLatencySource", "CDCLatencyTarget", "CDCThroughputBandwidthSource", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CPUUtilization", "FreeStorageSpace", "FreeableMemory", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SwapUsage", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||||
"AWS/DocDB": {"BackupRetentionPeriodStorageUsed", "BufferCacheHitRatio", "CPUUtilization", "DatabaseConnections", "DBInstanceReplicaLag", "DBClusterReplicaLagMaximum", "DBClusterReplicaLagMinimum", "DiskQueueDepth", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
"AWS/DocDB": {"BackupRetentionPeriodStorageUsed", "BufferCacheHitRatio", "CPUUtilization", "DatabaseConnections", "DBInstanceReplicaLag", "DBClusterReplicaLagMaximum", "DBClusterReplicaLagMinimum", "DiskQueueDepth", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "ReadIOPS", "ReadLatency", "ReadThroughput", "SnapshotStorageUsed", "SwapUsage", "TotalBackupStorageBilled", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs", "WriteIOPS", "WriteLatency", "WriteThroughput"},
|
||||||
"AWS/DX": {"ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState"},
|
"AWS/DX": {"ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelRx", "ConnectionLightLevelTx", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionState"},
|
||||||
|
"AWS/DAX": {"CPUUtilization", "NetworkPacketsIn", "NetworkPacketsOut", "GetItemRequestCount", "BatchGetItemRequestCount", "BatchWriteItemRequestCount", "DeleteItemRequestCount", "PutItemRequestCount", "UpdateItemRequestCount", "TransactWriteItemsCount", "TransactGetItemsCount", "ItemCacheHits", "ItemCacheMisses", "QueryCacheHits", "QueryCacheMisses", "ScanCacheHits", "ScanCacheMisses", "TotalRequestCount", "ErrorRequestCount", "FaultRequestCount", "FailedRequestCount", "QueryRequestCount", "ScanRequestCount", "ClientConnections", "EstimatedDbSize", "EvictedSize"},
|
||||||
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "TimeToLiveDeletedItemCount", "UserErrors", "WriteThrottleEvents"},
|
"AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "PendingReplicationCount", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReplicationLatency", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "ThrottledRequests", "TimeToLiveDeletedItemCount", "UserErrors", "WriteThrottleEvents"},
|
||||||
"AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"},
|
"AWS/EBS": {"BurstBalance", "VolumeConsumedReadWriteOps", "VolumeIdleTime", "VolumeQueueLength", "VolumeReadBytes", "VolumeReadOps", "VolumeThroughputPercentage", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeWriteBytes", "VolumeWriteOps"},
|
||||||
"AWS/EC2": {"CPUCreditBalance", "CPUCreditUsage", "CPUSurplusCreditBalance", "CPUSurplusCreditsCharged", "CPUUtilization", "DiskReadBytes", "DiskReadOps", "DiskWriteBytes", "DiskWriteOps", "EBSByteBalance%", "EBSIOBalance%", "EBSReadBytes", "EBSReadOps", "EBSWriteBytes", "EBSWriteOps", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
"AWS/EC2": {"CPUCreditBalance", "CPUCreditUsage", "CPUSurplusCreditBalance", "CPUSurplusCreditsCharged", "CPUUtilization", "DiskReadBytes", "DiskReadOps", "DiskWriteBytes", "DiskWriteOps", "EBSByteBalance%", "EBSIOBalance%", "EBSReadBytes", "EBSReadOps", "EBSWriteBytes", "EBSWriteOps", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
|
||||||
@@ -140,6 +141,7 @@ func init() {
|
|||||||
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
"AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"},
|
||||||
"AWS/DocDB": {"DBClusterIdentifier"},
|
"AWS/DocDB": {"DBClusterIdentifier"},
|
||||||
"AWS/DX": {"ConnectionId"},
|
"AWS/DX": {"ConnectionId"},
|
||||||
|
"AWS/DAX": {"Account", "ClusterId", "NodeId"},
|
||||||
"AWS/DynamoDB": {"GlobalSecondaryIndexName", "Operation", "ReceivingRegion", "StreamLabel", "TableName"},
|
"AWS/DynamoDB": {"GlobalSecondaryIndexName", "Operation", "ReceivingRegion", "StreamLabel", "TableName"},
|
||||||
"AWS/EBS": {"VolumeId"},
|
"AWS/EBS": {"VolumeId"},
|
||||||
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
|
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func (e *CloudWatchExecutor) transformQueryResponseToQueryResult(cloudwatchRespo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if partialData {
|
if partialData {
|
||||||
queryResult.ErrorString = "Cloudwatch GetMetricData error: Too many datapoints requested - your search have been limited. Please try to reduce the time range"
|
queryResult.ErrorString = "Cloudwatch GetMetricData error: Too many datapoints requested - your search has been limited. Please try to reduce the time range"
|
||||||
}
|
}
|
||||||
|
|
||||||
queryResult.Series = append(queryResult.Series, timeSeries...)
|
queryResult.Series = append(queryResult.Series, timeSeries...)
|
||||||
|
|||||||
@@ -68,8 +68,15 @@ func parseRequestQuery(model *simplejson.Json, refId string, startTime time.Time
|
|||||||
var period int
|
var period int
|
||||||
if strings.ToLower(p) == "auto" || p == "" {
|
if strings.ToLower(p) == "auto" || p == "" {
|
||||||
deltaInSeconds := endTime.Sub(startTime).Seconds()
|
deltaInSeconds := endTime.Sub(startTime).Seconds()
|
||||||
periods := []int{60, 300, 900, 3600, 21600}
|
periods := []int{60, 300, 900, 3600, 21600, 86400}
|
||||||
period = closest(periods, int(math.Ceil(deltaInSeconds/2000)))
|
datapoints := int(math.Ceil(deltaInSeconds / 2000))
|
||||||
|
period = periods[len(periods)-1]
|
||||||
|
for _, value := range periods {
|
||||||
|
if datapoints <= value {
|
||||||
|
period = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if regexp.MustCompile(`^\d+$`).Match([]byte(p)) {
|
if regexp.MustCompile(`^\d+$`).Match([]byte(p)) {
|
||||||
period, err = strconv.Atoi(p)
|
period, err = strconv.Atoi(p)
|
||||||
@@ -158,25 +165,3 @@ func sortDimensions(dimensions map[string][]string) map[string][]string {
|
|||||||
}
|
}
|
||||||
return sortedDimensions
|
return sortedDimensions
|
||||||
}
|
}
|
||||||
|
|
||||||
func closest(array []int, num int) int {
|
|
||||||
minDiff := array[len(array)-1]
|
|
||||||
var closest int
|
|
||||||
if num <= array[0] {
|
|
||||||
return array[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
if num >= array[len(array)-1] {
|
|
||||||
return array[len(array)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range array {
|
|
||||||
var m = int(math.Abs(float64(num - value)))
|
|
||||||
if m <= minDiff {
|
|
||||||
minDiff = m
|
|
||||||
closest = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return closest
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package cloudwatch
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
@@ -127,66 +128,86 @@ func TestRequestParser(t *testing.T) {
|
|||||||
"period": "auto",
|
"period": "auto",
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("when time range is short", func() {
|
Convey("when time range is 5 minutes", func() {
|
||||||
query.Set("period", "auto")
|
query.Set("period", "auto")
|
||||||
timeRange := tsdb.NewTimeRange("now-2h", "now-1h")
|
to := time.Now()
|
||||||
from, _ := timeRange.ParseFrom()
|
from := to.Local().Add(time.Minute * time.Duration(5))
|
||||||
to, _ := timeRange.ParseTo()
|
|
||||||
|
|
||||||
res, err := parseRequestQuery(query, "ref1", from, to)
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(res.Period, ShouldEqual, 60)
|
So(res.Period, ShouldEqual, 60)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("when time range is 5y", func() {
|
Convey("when time range is 1 day", func() {
|
||||||
timeRange := tsdb.NewTimeRange("now-5y", "now")
|
query.Set("period", "auto")
|
||||||
from, _ := timeRange.ParseFrom()
|
to := time.Now()
|
||||||
to, _ := timeRange.ParseTo()
|
from := to.AddDate(0, 0, -1)
|
||||||
|
|
||||||
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(res.Period, ShouldEqual, 60)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("when time range is 2 days", func() {
|
||||||
|
query.Set("period", "auto")
|
||||||
|
to := time.Now()
|
||||||
|
from := to.AddDate(0, 0, -2)
|
||||||
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(res.Period, ShouldEqual, 300)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("when time range is 7 days", func() {
|
||||||
|
query.Set("period", "auto")
|
||||||
|
to := time.Now()
|
||||||
|
from := to.AddDate(0, 0, -7)
|
||||||
|
|
||||||
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(res.Period, ShouldEqual, 900)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("when time range is 30 days", func() {
|
||||||
|
query.Set("period", "auto")
|
||||||
|
to := time.Now()
|
||||||
|
from := to.AddDate(0, 0, -30)
|
||||||
|
|
||||||
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(res.Period, ShouldEqual, 3600)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("when time range is 90 days", func() {
|
||||||
|
query.Set("period", "auto")
|
||||||
|
to := time.Now()
|
||||||
|
from := to.AddDate(0, 0, -90)
|
||||||
|
|
||||||
res, err := parseRequestQuery(query, "ref1", from, to)
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(res.Period, ShouldEqual, 21600)
|
So(res.Period, ShouldEqual, 21600)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
Convey("closest works as expected", func() {
|
Convey("when time range is 1 year", func() {
|
||||||
periods := []int{60, 300, 900, 3600, 21600}
|
query.Set("period", "auto")
|
||||||
Convey("and input is lower than 60", func() {
|
to := time.Now()
|
||||||
So(closest(periods, 6), ShouldEqual, 60)
|
from := to.AddDate(-1, 0, 0)
|
||||||
|
|
||||||
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(res.Period, ShouldEqual, 21600)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("and input is exactly 60", func() {
|
Convey("when time range is 2 years", func() {
|
||||||
So(closest(periods, 60), ShouldEqual, 60)
|
query.Set("period", "auto")
|
||||||
})
|
to := time.Now()
|
||||||
|
from := to.AddDate(-2, 0, 0)
|
||||||
|
|
||||||
Convey("and input is exactly between two steps", func() {
|
res, err := parseRequestQuery(query, "ref1", from, to)
|
||||||
So(closest(periods, 180), ShouldEqual, 300)
|
So(err, ShouldBeNil)
|
||||||
})
|
So(res.Period, ShouldEqual, 86400)
|
||||||
|
|
||||||
Convey("and input is exactly 2000", func() {
|
|
||||||
So(closest(periods, 2000), ShouldEqual, 900)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("and input is exactly 5000", func() {
|
|
||||||
So(closest(periods, 5000), ShouldEqual, 3600)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("and input is exactly 50000", func() {
|
|
||||||
So(closest(periods, 50000), ShouldEqual, 21600)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("and period isn't shorter than min retension for 15 days", func() {
|
|
||||||
So(closest(periods, (60*60*24*15)+1/2000), ShouldBeGreaterThanOrEqualTo, 300)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("and period isn't shorter than min retension for 63 days", func() {
|
|
||||||
So(closest(periods, (60*60*24*63)+1/2000), ShouldBeGreaterThanOrEqualTo, 3600)
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("and period isn't shorter than min retension for 455 days", func() {
|
|
||||||
So(closest(periods, (60*60*24*455)+1/2000), ShouldBeGreaterThanOrEqualTo, 21600)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,8 +100,10 @@ func parseGetMetricDataTimeSeries(metricDataResults map[string]*cloudwatch.Metri
|
|||||||
series.Tags[key] = values[0]
|
series.Tags[key] = values[0]
|
||||||
} else {
|
} else {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if value == label || value == "*" || strings.Contains(label, value) {
|
if value == label || value == "*" {
|
||||||
series.Tags[key] = label
|
series.Tags[key] = label
|
||||||
|
} else if strings.Contains(label, value) {
|
||||||
|
series.Tags[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
|
|||||||
Namespace: "AWS/ApplicationELB",
|
Namespace: "AWS/ApplicationELB",
|
||||||
MetricName: "TargetResponseTime",
|
MetricName: "TargetResponseTime",
|
||||||
Dimensions: map[string][]string{
|
Dimensions: map[string][]string{
|
||||||
"LoadBalancer": {"lb2"},
|
"LoadBalancer": {"lb1", "lb2"},
|
||||||
"TargetGroup": {"tg"},
|
"TargetGroup": {"tg"},
|
||||||
},
|
},
|
||||||
Stats: "Average",
|
Stats: "Average",
|
||||||
@@ -65,8 +65,12 @@ func TestCloudWatchResponseParser(t *testing.T) {
|
|||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(partialData, ShouldBeFalse)
|
So(partialData, ShouldBeFalse)
|
||||||
So(timeSeries.Name, ShouldEqual, "lb2 Expanded")
|
So(timeSeries.Name, ShouldEqual, "lb1 Expanded")
|
||||||
So(timeSeries.Tags["LoadBalancer"], ShouldEqual, "lb2")
|
So(timeSeries.Tags["LoadBalancer"], ShouldEqual, "lb1")
|
||||||
|
|
||||||
|
timeSeries2 := (*series)[1]
|
||||||
|
So(timeSeries2.Name, ShouldEqual, "lb2 Expanded")
|
||||||
|
So(timeSeries2.Tags["LoadBalancer"], ShouldEqual, "lb2")
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("can expand dimension value using substring", func() {
|
Convey("can expand dimension value using substring", func() {
|
||||||
@@ -110,7 +114,7 @@ func TestCloudWatchResponseParser(t *testing.T) {
|
|||||||
Namespace: "AWS/ApplicationELB",
|
Namespace: "AWS/ApplicationELB",
|
||||||
MetricName: "TargetResponseTime",
|
MetricName: "TargetResponseTime",
|
||||||
Dimensions: map[string][]string{
|
Dimensions: map[string][]string{
|
||||||
"LoadBalancer": {"lb1"},
|
"LoadBalancer": {"lb1", "lb2"},
|
||||||
"TargetGroup": {"tg"},
|
"TargetGroup": {"tg"},
|
||||||
},
|
},
|
||||||
Stats: "Average",
|
Stats: "Average",
|
||||||
@@ -119,11 +123,14 @@ func TestCloudWatchResponseParser(t *testing.T) {
|
|||||||
}
|
}
|
||||||
series, partialData, err := parseGetMetricDataTimeSeries(resp, query)
|
series, partialData, err := parseGetMetricDataTimeSeries(resp, query)
|
||||||
timeSeries := (*series)[0]
|
timeSeries := (*series)[0]
|
||||||
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(partialData, ShouldBeFalse)
|
So(partialData, ShouldBeFalse)
|
||||||
So(timeSeries.Name, ShouldEqual, "lb1 Expanded")
|
So(timeSeries.Name, ShouldEqual, "lb1 Expanded")
|
||||||
So(timeSeries.Tags["LoadBalancer"], ShouldEqual, "lb1")
|
So(timeSeries.Tags["LoadBalancer"], ShouldEqual, "lb1")
|
||||||
|
|
||||||
|
timeSeries2 := (*series)[1]
|
||||||
|
So(timeSeries2.Name, ShouldEqual, "lb2 Expanded")
|
||||||
|
So(timeSeries2.Tags["LoadBalancer"], ShouldEqual, "lb2")
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("can expand dimension value using wildcard", func() {
|
Convey("can expand dimension value using wildcard", func() {
|
||||||
|
|||||||
@@ -542,8 +542,11 @@ func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery, index int) *tsd
|
|||||||
startValue := query.Model.Get("startValue").MustFloat64(rand.Float64() * 100)
|
startValue := query.Model.Get("startValue").MustFloat64(rand.Float64() * 100)
|
||||||
spread := query.Model.Get("spread").MustFloat64(1)
|
spread := query.Model.Get("spread").MustFloat64(1)
|
||||||
noise := query.Model.Get("noise").MustFloat64(0)
|
noise := query.Model.Get("noise").MustFloat64(0)
|
||||||
min, hasMin := query.Model.Get("min").Float64()
|
|
||||||
max, hasMax := query.Model.Get("max").Float64()
|
min, err := query.Model.Get("min").Float64()
|
||||||
|
hasMin := err == nil
|
||||||
|
max, err := query.Model.Get("max").Float64()
|
||||||
|
hasMax := err == nil
|
||||||
|
|
||||||
points := make(tsdb.TimeSeriesPoints, 0)
|
points := make(tsdb.TimeSeriesPoints, 0)
|
||||||
walker := startValue
|
walker := startValue
|
||||||
@@ -551,12 +554,12 @@ func getRandomWalk(query *tsdb.Query, tsdbQuery *tsdb.TsdbQuery, index int) *tsd
|
|||||||
for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
|
for i := int64(0); i < 10000 && timeWalkerMs < to; i++ {
|
||||||
nextValue := walker + (rand.Float64() * noise)
|
nextValue := walker + (rand.Float64() * noise)
|
||||||
|
|
||||||
if hasMin == nil && nextValue < min {
|
if hasMin && nextValue < min {
|
||||||
nextValue = min
|
nextValue = min
|
||||||
walker = min
|
walker = min
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasMax == nil && nextValue > max {
|
if hasMax && nextValue > max {
|
||||||
nextValue = max
|
nextValue = max
|
||||||
walker = max
|
walker = max
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import '@babel/polyfill';
|
import '@babel/polyfill';
|
||||||
|
import 'url-search-params-polyfill'; // fetch polyfill needed for PhantomJs rendering
|
||||||
import 'file-saver';
|
import 'file-saver';
|
||||||
import 'lodash';
|
import 'lodash';
|
||||||
import 'jquery';
|
import 'jquery';
|
||||||
|
|||||||
@@ -7,9 +7,13 @@ export interface BrandComponentProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LoginLogo: FC<BrandComponentProps> = ({ className }) => {
|
export const LoginLogo: FC<BrandComponentProps> = ({ className }) => {
|
||||||
|
const maxSize = css`
|
||||||
|
max-width: 150px;
|
||||||
|
`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<img className={className} src="public/img/grafana_icon.svg" alt="Grafana" />
|
<img className={cx(className, maxSize)} src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||||
<div className="logo-wordmark" />
|
<div className="logo-wordmark" />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export interface FooterLink {
|
|||||||
text: string;
|
text: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
target: string;
|
target?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export let getFooterLinks = (): FooterLink[] => {
|
export let getFooterLinks = (): FooterLink[] => {
|
||||||
@@ -17,7 +17,7 @@ export let getFooterLinks = (): FooterLink[] => {
|
|||||||
target: '_blank',
|
target: '_blank',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Support & Enterprise',
|
text: 'Support',
|
||||||
icon: 'fa fa-support',
|
icon: 'fa fa-support',
|
||||||
url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
|
url: 'https://grafana.com/products/enterprise/?utm_source=grafana_footer',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
@@ -32,15 +32,12 @@ export let getFooterLinks = (): FooterLink[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export let getVersionLinks = (): FooterLink[] => {
|
export let getVersionLinks = (): FooterLink[] => {
|
||||||
const { buildInfo } = config;
|
const { buildInfo, licenseInfo } = config;
|
||||||
|
const links: FooterLink[] = [];
|
||||||
|
const stateInfo = licenseInfo.stateInfo ? ` (${licenseInfo.stateInfo})` : '';
|
||||||
|
|
||||||
const links: FooterLink[] = [
|
links.push({ text: `${buildInfo.edition}${stateInfo}`, url: licenseInfo.licenseUrl });
|
||||||
{
|
links.push({ text: `v${buildInfo.version} (${buildInfo.commit})` });
|
||||||
text: `Grafana v${buildInfo.version} (commit: ${buildInfo.commit})`,
|
|
||||||
url: 'https://grafana.com',
|
|
||||||
target: '_blank',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (buildInfo.hasUpdate) {
|
if (buildInfo.hasUpdate) {
|
||||||
links.push({
|
links.push({
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export class LoginForm extends PureComponent<Props, State> {
|
|||||||
Log In
|
Log In
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button type="submit" className="btn btn-large p-x-2 btn-inverse btn-loading">
|
<button type="submit" disabled className="btn btn-large p-x-2 btn-inverse btn-loading">
|
||||||
Logging In<span>.</span>
|
Logging In<span>.</span>
|
||||||
<span>.</span>
|
<span>.</span>
|
||||||
<span>.</span>
|
<span>.</span>
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import LoginCtrl from './LoginCtrl';
|
|||||||
import { LoginForm } from './LoginForm';
|
import { LoginForm } from './LoginForm';
|
||||||
import { ChangePassword } from './ChangePassword';
|
import { ChangePassword } from './ChangePassword';
|
||||||
import { Branding } from 'app/core/components/Branding/Branding';
|
import { Branding } from 'app/core/components/Branding/Branding';
|
||||||
|
import { Footer } from 'app/core/components/Footer/Footer';
|
||||||
|
|
||||||
export const LoginPage: FC = () => {
|
export const LoginPage: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Branding.LoginBackground className="login container">
|
<Branding.LoginBackground className="login container">
|
||||||
<div className="login-content">
|
<div className="login-content">
|
||||||
<div className="login-branding">
|
<div className="login-branding">
|
||||||
<Branding.LoginLogo className="logo-icon" />
|
<Branding.LoginLogo className="login-logo" />
|
||||||
</div>
|
</div>
|
||||||
<LoginCtrl>
|
<LoginCtrl>
|
||||||
{({
|
{({
|
||||||
@@ -62,6 +63,7 @@ export const LoginPage: FC = () => {
|
|||||||
|
|
||||||
<div className="clearfix" />
|
<div className="clearfix" />
|
||||||
</div>
|
</div>
|
||||||
|
<Footer />
|
||||||
</Branding.LoginBackground>
|
</Branding.LoginBackground>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,13 +60,15 @@ const Navigation = ({ main }: { main: NavModelItem }) => {
|
|||||||
<TabsBar className="page-header__tabs" hideBorder={true}>
|
<TabsBar className="page-header__tabs" hideBorder={true}>
|
||||||
{main.children.map((child, index) => {
|
{main.children.map((child, index) => {
|
||||||
return (
|
return (
|
||||||
<Tab
|
!child.hideFromTabs && (
|
||||||
label={child.text}
|
<Tab
|
||||||
active={child.active}
|
label={child.text}
|
||||||
key={`${child.url}-${index}`}
|
active={child.active}
|
||||||
icon={child.icon}
|
key={`${child.url}-${index}`}
|
||||||
onChangeTab={() => goToUrl(index)}
|
icon={child.icon}
|
||||||
/>
|
onChangeTab={() => goToUrl(index)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TabsBar>
|
</TabsBar>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
import { promiseToDigest } from '../../utils/promiseToDigest';
|
||||||
|
|
||||||
const template = `
|
const template = `
|
||||||
<div class="dropdown cascade-open">
|
<div class="dropdown cascade-open">
|
||||||
@@ -138,9 +139,11 @@ export function queryPartEditorDirective(templateSrv: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$scope.showActionsMenu = () => {
|
$scope.showActionsMenu = () => {
|
||||||
$scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => {
|
promiseToDigest($scope)(
|
||||||
$scope.partActions = res;
|
$scope.handleEvent({ $event: { name: 'get-part-actions' } }).then((res: any) => {
|
||||||
});
|
$scope.partActions = res;
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.triggerPartAction = (action: string) => {
|
$scope.triggerPartAction = (action: string) => {
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export function sqlPartEditorDirective(templateSrv: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
|
const paramValue = templateSrv.highlightVariablesAsHtml(part.params[index]);
|
||||||
const $paramLink = $('<a class="graphite-func-param-link pointer">' + paramValue + '</a>');
|
const $paramLink = $('<a class="query-part__link">' + paramValue + '</a>');
|
||||||
const $input = $(paramTemplate);
|
const $input = $(paramTemplate);
|
||||||
|
|
||||||
$paramLink.appendTo($paramsContainer);
|
$paramLink.appendTo($paramsContainer);
|
||||||
|
|||||||
@@ -223,7 +223,6 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
expect(logsModel.rows).toHaveLength(2);
|
expect(logsModel.rows).toHaveLength(2);
|
||||||
expect(logsModel.rows).toMatchObject([
|
expect(logsModel.rows).toMatchObject([
|
||||||
{
|
{
|
||||||
timestamp: '2019-04-26T09:28:11.352440161Z',
|
|
||||||
entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
|
entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
|
||||||
labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||||
logLevel: 'info',
|
logLevel: 'info',
|
||||||
@@ -231,7 +230,6 @@ describe('dataFrameToLogsModel', () => {
|
|||||||
uid: 'foo',
|
uid: 'foo',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamp: '2019-04-26T14:42:50.991981292Z',
|
|
||||||
entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
|
entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
|
||||||
labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
|
||||||
logLevel: 'error',
|
logLevel: 'error',
|
||||||
|
|||||||
@@ -312,7 +312,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
|
|||||||
const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
|
const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : [];
|
||||||
|
|
||||||
let logLevel = LogLevel.unknown;
|
let logLevel = LogLevel.unknown;
|
||||||
if (logLevelField) {
|
if (logLevelField && logLevelField.values.get(j)) {
|
||||||
logLevel = getLogLevelFromKey(logLevelField.values.get(j));
|
logLevel = getLogLevelFromKey(logLevelField.values.get(j));
|
||||||
} else if (seriesLogLevel) {
|
} else if (seriesLogLevel) {
|
||||||
logLevel = seriesLogLevel;
|
logLevel = seriesLogLevel;
|
||||||
@@ -328,14 +328,13 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
|
|||||||
timeFromNow: time.fromNow(),
|
timeFromNow: time.fromNow(),
|
||||||
timeEpochMs: time.valueOf(),
|
timeEpochMs: time.valueOf(),
|
||||||
timeLocal: time.format(logTimeFormat),
|
timeLocal: time.format(logTimeFormat),
|
||||||
timeUtc: toUtc(ts).format(logTimeFormat),
|
timeUtc: toUtc(time.valueOf()).format(logTimeFormat),
|
||||||
uniqueLabels,
|
uniqueLabels,
|
||||||
hasAnsi,
|
hasAnsi,
|
||||||
searchWords,
|
searchWords,
|
||||||
entry: hasAnsi ? ansicolor.strip(message) : message,
|
entry: hasAnsi ? ansicolor.strip(message) : message,
|
||||||
raw: message,
|
raw: message,
|
||||||
labels: stringField.labels,
|
labels: stringField.labels,
|
||||||
timestamp: ts,
|
|
||||||
uid: idField ? idField.values.get(j) : j.toString(),
|
uid: idField ? idField.values.get(j) : j.toString(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import locationUtil from 'app/core/utils/location_util';
|
|||||||
|
|
||||||
jest.mock('app/core/config', () => {
|
jest.mock('app/core/config', () => {
|
||||||
return {
|
return {
|
||||||
appSubUrl: '/subUrl',
|
getConfig: () => ({ appSubUrl: '/subUrl' }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ import {
|
|||||||
} from './explore';
|
} from './explore';
|
||||||
import { ExploreUrlState, ExploreMode } from 'app/types/explore';
|
import { ExploreUrlState, ExploreMode } from 'app/types/explore';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { DataQueryError, LogsDedupStrategy, LogsModel, LogLevel, dateTime, MutableDataFrame } from '@grafana/data';
|
import {
|
||||||
|
DataQueryError,
|
||||||
|
LogsDedupStrategy,
|
||||||
|
LogsModel,
|
||||||
|
LogLevel,
|
||||||
|
dateTime,
|
||||||
|
MutableDataFrame,
|
||||||
|
LogRowModel,
|
||||||
|
} from '@grafana/data';
|
||||||
import { RefreshPicker } from '@grafana/ui';
|
import { RefreshPicker } from '@grafana/ui';
|
||||||
|
|
||||||
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||||
@@ -372,11 +380,10 @@ describe('refreshIntervalToSortOrder', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('sortLogsResult', () => {
|
describe('sortLogsResult', () => {
|
||||||
const firstRow = {
|
const firstRow: LogRowModel = {
|
||||||
rowIndex: 0,
|
rowIndex: 0,
|
||||||
entryFieldIndex: 0,
|
entryFieldIndex: 0,
|
||||||
dataFrame: new MutableDataFrame(),
|
dataFrame: new MutableDataFrame(),
|
||||||
timestamp: '2019-01-01T21:00:0.0000000Z',
|
|
||||||
entry: '',
|
entry: '',
|
||||||
hasAnsi: false,
|
hasAnsi: false,
|
||||||
labels: {},
|
labels: {},
|
||||||
@@ -389,17 +396,16 @@ describe('sortLogsResult', () => {
|
|||||||
uid: '1',
|
uid: '1',
|
||||||
};
|
};
|
||||||
const sameAsFirstRow = firstRow;
|
const sameAsFirstRow = firstRow;
|
||||||
const secondRow = {
|
const secondRow: LogRowModel = {
|
||||||
rowIndex: 1,
|
rowIndex: 1,
|
||||||
entryFieldIndex: 0,
|
entryFieldIndex: 0,
|
||||||
dataFrame: new MutableDataFrame(),
|
dataFrame: new MutableDataFrame(),
|
||||||
timestamp: '2019-01-01T22:00:0.0000000Z',
|
|
||||||
entry: '',
|
entry: '',
|
||||||
hasAnsi: false,
|
hasAnsi: false,
|
||||||
labels: {},
|
labels: {},
|
||||||
logLevel: LogLevel.info,
|
logLevel: LogLevel.info,
|
||||||
raw: '',
|
raw: '',
|
||||||
timeEpochMs: 0,
|
timeEpochMs: 10,
|
||||||
timeFromNow: '',
|
timeFromNow: '',
|
||||||
timeLocal: '',
|
timeLocal: '',
|
||||||
timeUtc: '',
|
timeUtc: '',
|
||||||
|
|||||||
@@ -88,11 +88,12 @@ export async function getExploreUrl(args: GetExploreUrlArguments) {
|
|||||||
const range = timeSrv.timeRangeForUrl();
|
const range = timeSrv.timeRangeForUrl();
|
||||||
let state: Partial<ExploreUrlState> = { range };
|
let state: Partial<ExploreUrlState> = { range };
|
||||||
if (exploreDatasource.interpolateVariablesInQueries) {
|
if (exploreDatasource.interpolateVariablesInQueries) {
|
||||||
|
const scopedVars = panel.scopedVars || {};
|
||||||
state = {
|
state = {
|
||||||
...state,
|
...state,
|
||||||
datasource: exploreDatasource.name,
|
datasource: exploreDatasource.name,
|
||||||
context: 'explore',
|
context: 'explore',
|
||||||
queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets),
|
queries: exploreDatasource.interpolateVariablesInQueries(exploreTargets, scopedVars),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
state = {
|
state = {
|
||||||
@@ -106,8 +107,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments) {
|
|||||||
const exploreState = JSON.stringify({ ...state, originPanelId: panel.id });
|
const exploreState = JSON.stringify({ ...state, originPanelId: panel.id });
|
||||||
url = renderUrl('/explore', { left: exploreState });
|
url = renderUrl('/explore', { left: exploreState });
|
||||||
}
|
}
|
||||||
const finalUrl = config.appSubUrl + url;
|
return url;
|
||||||
return finalUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildQueryTransaction(
|
export function buildQueryTransaction(
|
||||||
@@ -473,11 +473,11 @@ export const getRefIds = (value: any): string[] => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
export const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
||||||
if (a.timestamp < b.timestamp) {
|
if (a.timeEpochMs < b.timeEpochMs) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.timestamp > b.timestamp) {
|
if (a.timeEpochMs > b.timeEpochMs) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,11 +485,11 @@ export const sortInAscendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const sortInDescendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
const sortInDescendingOrder = (a: LogRowModel, b: LogRowModel) => {
|
||||||
if (a.timestamp > b.timestamp) {
|
if (a.timeEpochMs > b.timeEpochMs) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a.timestamp < b.timestamp) {
|
if (a.timeEpochMs < b.timeEpochMs) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import config from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
|
|
||||||
export const stripBaseFromUrl = (url: string): string => {
|
export const stripBaseFromUrl = (url: string): string => {
|
||||||
const appSubUrl = config.appSubUrl;
|
const appSubUrl = getConfig().appSubUrl;
|
||||||
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0;
|
||||||
const urlWithoutBase =
|
const urlWithoutBase =
|
||||||
url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url;
|
url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url;
|
||||||
@@ -9,4 +9,11 @@ export const stripBaseFromUrl = (url: string): string => {
|
|||||||
return urlWithoutBase;
|
return urlWithoutBase;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { stripBaseFromUrl };
|
export const assureBaseUrl = (url: string) => {
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
return `${getConfig().appSubUrl}${stripBaseFromUrl(url)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default { stripBaseFromUrl, assureBaseUrl };
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ export const LicenseChrome: React.FC<Props> = ({ header, editionNotice, subheade
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
className="logo-icon"
|
|
||||||
src="/public/img/grafana_icon.svg"
|
src="/public/img/grafana_icon.svg"
|
||||||
alt="Grafana"
|
alt="Grafana"
|
||||||
width="80px"
|
width="80px"
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ exports[`ServerStats Should render table with stats 1`] = `
|
|||||||
className="fa fa-support"
|
className="fa fa-support"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Support & Enterprise
|
Support
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -198,13 +198,22 @@ exports[`ServerStats Should render table with stats 1`] = `
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="https://grafana.com"
|
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<i />
|
<i />
|
||||||
|
|
||||||
Grafana vv1.0 (commit: 1)
|
undefined
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
rel="noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<i />
|
||||||
|
|
||||||
|
vv1.0 (1)
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { CoreEvents } from 'app/types';
|
|||||||
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
import { GrafanaRootScope } from 'app/routes/GrafanaCtrl';
|
||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
|
import locationUtil from 'app/core/utils/location_util';
|
||||||
|
|
||||||
export class SettingsCtrl {
|
export class SettingsCtrl {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
@@ -183,7 +184,7 @@ export class SettingsCtrl {
|
|||||||
this.buildSectionList();
|
this.buildSectionList();
|
||||||
|
|
||||||
const currentSection: any = _.find(this.sections, { id: this.viewId } as any);
|
const currentSection: any = _.find(this.sections, { id: this.viewId } as any);
|
||||||
this.$location.url(currentSection.url);
|
this.$location.url(locationUtil.stripBaseFromUrl(currentSection.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteDashboard() {
|
deleteDashboard() {
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import React, { Component } from 'react';
|
|||||||
|
|
||||||
import { renderMarkdown, LinkModelSupplier, ScopedVars } from '@grafana/data';
|
import { renderMarkdown, LinkModelSupplier, ScopedVars } from '@grafana/data';
|
||||||
import { Tooltip, PopoverContent } from '@grafana/ui';
|
import { Tooltip, PopoverContent } from '@grafana/ui';
|
||||||
|
import { getLocationSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { getLocationSrv } from '@grafana/runtime';
|
|
||||||
|
|
||||||
enum InfoMode {
|
enum InfoMode {
|
||||||
Error = 'Error',
|
Error = 'Error',
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const setup = (propOverrides?: object) => {
|
|||||||
loadDataSource: jest.fn(),
|
loadDataSource: jest.fn(),
|
||||||
setDataSourceName,
|
setDataSourceName,
|
||||||
updateDataSource: jest.fn(),
|
updateDataSource: jest.fn(),
|
||||||
|
initDataSourceSettings: jest.fn(),
|
||||||
|
testDataSource: jest.fn(),
|
||||||
setIsDefault,
|
setIsDefault,
|
||||||
dataSourceLoaded,
|
dataSourceLoaded,
|
||||||
query: {},
|
query: {},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { hot } from 'react-hot-loader';
|
import { hot } from 'react-hot-loader';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import isString from 'lodash/isString';
|
import isString from 'lodash/isString';
|
||||||
import { e2e } from '@grafana/e2e';
|
import { e2e } from '@grafana/e2e';
|
||||||
// Components
|
// Components
|
||||||
@@ -11,11 +10,15 @@ import BasicSettings from './BasicSettings';
|
|||||||
import ButtonRow from './ButtonRow';
|
import ButtonRow from './ButtonRow';
|
||||||
// Services & Utils
|
// Services & Utils
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
|
||||||
// Actions & selectors
|
// Actions & selectors
|
||||||
import { getDataSource, getDataSourceMeta } from '../state/selectors';
|
import { getDataSource, getDataSourceMeta } from '../state/selectors';
|
||||||
import { deleteDataSource, loadDataSource, updateDataSource } from '../state/actions';
|
import {
|
||||||
|
deleteDataSource,
|
||||||
|
loadDataSource,
|
||||||
|
updateDataSource,
|
||||||
|
initDataSourceSettings,
|
||||||
|
testDataSource,
|
||||||
|
} from '../state/actions';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getRouteParamsId } from 'app/core/selectors/location';
|
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||||
// Types
|
// Types
|
||||||
@@ -24,8 +27,8 @@ import { UrlQueryMap } from '@grafana/runtime';
|
|||||||
import { DataSourcePluginMeta, DataSourceSettings, NavModel } from '@grafana/data';
|
import { DataSourcePluginMeta, DataSourceSettings, NavModel } from '@grafana/data';
|
||||||
import { getDataSourceLoadingNav } from '../state/navModel';
|
import { getDataSourceLoadingNav } from '../state/navModel';
|
||||||
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
|
||||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
|
||||||
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers';
|
||||||
|
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navModel: NavModel;
|
navModel: NavModel;
|
||||||
@@ -38,55 +41,22 @@ export interface Props {
|
|||||||
updateDataSource: typeof updateDataSource;
|
updateDataSource: typeof updateDataSource;
|
||||||
setIsDefault: typeof setIsDefault;
|
setIsDefault: typeof setIsDefault;
|
||||||
dataSourceLoaded: typeof dataSourceLoaded;
|
dataSourceLoaded: typeof dataSourceLoaded;
|
||||||
|
initDataSourceSettings: typeof initDataSourceSettings;
|
||||||
|
testDataSource: typeof testDataSource;
|
||||||
plugin?: GenericDataSourcePlugin;
|
plugin?: GenericDataSourcePlugin;
|
||||||
query: UrlQueryMap;
|
query: UrlQueryMap;
|
||||||
page?: string;
|
page?: string;
|
||||||
|
testingStatus?: {
|
||||||
|
message?: string;
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
loadError?: Error | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
export class DataSourceSettingsPage extends PureComponent<Props> {
|
||||||
plugin?: GenericDataSourcePlugin;
|
componentDidMount() {
|
||||||
isTesting?: boolean;
|
const { initDataSourceSettings, pageId } = this.props;
|
||||||
testingMessage?: string;
|
initDataSourceSettings(pageId);
|
||||||
testingStatus?: string;
|
|
||||||
loadError?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
plugin: props.plugin,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadPlugin(pluginId?: string) {
|
|
||||||
const { dataSourceMeta } = this.props;
|
|
||||||
let importedPlugin: GenericDataSourcePlugin;
|
|
||||||
|
|
||||||
try {
|
|
||||||
importedPlugin = await importDataSourcePlugin(dataSourceMeta);
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Failed to import plugin module', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ plugin: importedPlugin });
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidMount() {
|
|
||||||
const { loadDataSource, pageId } = this.props;
|
|
||||||
if (isNaN(pageId)) {
|
|
||||||
this.setState({ loadError: 'Invalid ID' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await loadDataSource(pageId);
|
|
||||||
if (!this.state.plugin) {
|
|
||||||
await this.loadPlugin();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ loadError: err });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
|
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -136,40 +106,9 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async testDataSource() {
|
testDataSource() {
|
||||||
const dsApi = await getDatasourceSrv().get(this.props.dataSource.name);
|
const { dataSource, testDataSource } = this.props;
|
||||||
|
testDataSource(dataSource.name);
|
||||||
if (!dsApi.testDatasource) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ isTesting: true, testingMessage: 'Testing...', testingStatus: 'info' });
|
|
||||||
|
|
||||||
getBackendSrv().withNoBackendCache(async () => {
|
|
||||||
try {
|
|
||||||
const result = await dsApi.testDatasource();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isTesting: false,
|
|
||||||
testingStatus: result.status,
|
|
||||||
testingMessage: result.message,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
let message = '';
|
|
||||||
|
|
||||||
if (err.statusText) {
|
|
||||||
message = 'HTTP Error ' + err.statusText;
|
|
||||||
} else {
|
|
||||||
message = err.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
isTesting: false,
|
|
||||||
testingStatus: 'error',
|
|
||||||
testingMessage: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasDataSource() {
|
get hasDataSource() {
|
||||||
@@ -218,7 +157,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderConfigPageBody(page: string) {
|
renderConfigPageBody(page: string) {
|
||||||
const { plugin } = this.state;
|
const { plugin } = this.props;
|
||||||
if (!plugin || !plugin.configPages) {
|
if (!plugin || !plugin.configPages) {
|
||||||
return null; // still loading
|
return null; // still loading
|
||||||
}
|
}
|
||||||
@@ -233,8 +172,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSettings() {
|
renderSettings() {
|
||||||
const { dataSourceMeta, setDataSourceName, setIsDefault, dataSource } = this.props;
|
const { dataSourceMeta, setDataSourceName, setIsDefault, dataSource, testingStatus, plugin } = this.props;
|
||||||
const { testingMessage, testingStatus, plugin } = this.state;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
@@ -265,10 +203,10 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="gf-form-group">
|
<div className="gf-form-group">
|
||||||
{testingMessage && (
|
{testingStatus && testingStatus.message && (
|
||||||
<div className={`alert-${testingStatus} alert`} aria-label={e2e.pages.DataSource.selectors.alert}>
|
<div className={`alert-${testingStatus.status} alert`} aria-label={e2e.pages.DataSource.selectors.alert}>
|
||||||
<div className="alert-icon">
|
<div className="alert-icon">
|
||||||
{testingStatus === 'error' ? (
|
{testingStatus.status === 'error' ? (
|
||||||
<i className="fa fa-exclamation-triangle" />
|
<i className="fa fa-exclamation-triangle" />
|
||||||
) : (
|
) : (
|
||||||
<i className="fa fa-check" />
|
<i className="fa fa-check" />
|
||||||
@@ -276,7 +214,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
<div className="alert-body">
|
<div className="alert-body">
|
||||||
<div className="alert-title" aria-label={e2e.pages.DataSource.selectors.alertMessage}>
|
<div className="alert-title" aria-label={e2e.pages.DataSource.selectors.alertMessage}>
|
||||||
{testingMessage}
|
{testingStatus.message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -294,8 +232,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { navModel, page } = this.props;
|
const { navModel, page, loadError } = this.props;
|
||||||
const { loadError } = this.state;
|
|
||||||
|
|
||||||
if (loadError) {
|
if (loadError) {
|
||||||
return this.renderLoadError(loadError);
|
return this.renderLoadError(loadError);
|
||||||
@@ -315,6 +252,7 @@ function mapStateToProps(state: StoreState) {
|
|||||||
const pageId = getRouteParamsId(state.location);
|
const pageId = getRouteParamsId(state.location);
|
||||||
const dataSource = getDataSource(state.dataSources, pageId);
|
const dataSource = getDataSource(state.dataSources, pageId);
|
||||||
const page = state.location.query.page as string;
|
const page = state.location.query.page as string;
|
||||||
|
const { plugin, loadError, testingStatus } = state.dataSourceSettings;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
navModel: getNavModel(
|
navModel: getNavModel(
|
||||||
@@ -327,6 +265,9 @@ function mapStateToProps(state: StoreState) {
|
|||||||
pageId: pageId,
|
pageId: pageId,
|
||||||
query: state.location.query,
|
query: state.location.query,
|
||||||
page,
|
page,
|
||||||
|
plugin,
|
||||||
|
loadError,
|
||||||
|
testingStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,6 +278,10 @@ const mapDispatchToProps = {
|
|||||||
updateDataSource,
|
updateDataSource,
|
||||||
setIsDefault,
|
setIsDefault,
|
||||||
dataSourceLoaded,
|
dataSourceLoaded,
|
||||||
|
initDataSourceSettings,
|
||||||
|
testDataSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceSettingsPage));
|
export default hot(module)(
|
||||||
|
connectWithCleanUp(mapStateToProps, mapDispatchToProps, state => state.dataSourceSettings)(DataSourceSettingsPage)
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,5 +1,33 @@
|
|||||||
import { findNewName, nameExits } from './actions';
|
import {
|
||||||
|
findNewName,
|
||||||
|
nameExits,
|
||||||
|
InitDataSourceSettingDependencies,
|
||||||
|
testDataSource,
|
||||||
|
TestDataSourceDependencies,
|
||||||
|
} from './actions';
|
||||||
import { getMockPlugin, getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
|
import { getMockPlugin, getMockPlugins } from '../../plugins/__mocks__/pluginMocks';
|
||||||
|
import { thunkTester } from 'test/core/thunk/thunkTester';
|
||||||
|
import {
|
||||||
|
initDataSourceSettingsSucceeded,
|
||||||
|
initDataSourceSettingsFailed,
|
||||||
|
testDataSourceStarting,
|
||||||
|
testDataSourceSucceeded,
|
||||||
|
testDataSourceFailed,
|
||||||
|
} from './reducers';
|
||||||
|
import { initDataSourceSettings } from '../state/actions';
|
||||||
|
import { ThunkResult, ThunkDispatch } from 'app/types';
|
||||||
|
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
|
||||||
|
|
||||||
|
const getBackendSrvMock = () =>
|
||||||
|
({
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
testDatasource: jest.fn().mockReturnValue({
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
withNoBackendCache: jest.fn().mockImplementationOnce(cb => cb()),
|
||||||
|
} as any);
|
||||||
|
|
||||||
describe('Name exists', () => {
|
describe('Name exists', () => {
|
||||||
const plugins = getMockPlugins(5);
|
const plugins = getMockPlugins(5);
|
||||||
@@ -42,3 +70,131 @@ describe('Find new name', () => {
|
|||||||
expect(findNewName(plugins, name)).toEqual('pretty cool plugin-');
|
expect(findNewName(plugins, name)).toEqual('pretty cool plugin-');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('initDataSourceSettings', () => {
|
||||||
|
describe('when pageId is not a number', () => {
|
||||||
|
it('then initDataSourceSettingsFailed should be dispatched', async () => {
|
||||||
|
const dispatchedActions = await thunkTester({})
|
||||||
|
.givenThunk(initDataSourceSettings)
|
||||||
|
.whenThunkIsDispatched('some page');
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Invalid ID'))]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when pageId is a number', () => {
|
||||||
|
it('then initDataSourceSettingsSucceeded should be dispatched', async () => {
|
||||||
|
const thunkMock = (): ThunkResult<void> => (dispatch: ThunkDispatch, getState) => {};
|
||||||
|
const dataSource = { type: 'app' };
|
||||||
|
const dataSourceMeta = { id: 'some id' };
|
||||||
|
const dependencies: InitDataSourceSettingDependencies = {
|
||||||
|
loadDataSource: jest.fn(thunkMock),
|
||||||
|
getDataSource: jest.fn().mockReturnValue(dataSource),
|
||||||
|
getDataSourceMeta: jest.fn().mockReturnValue(dataSourceMeta),
|
||||||
|
importDataSourcePlugin: jest.fn().mockReturnValue({} as GenericDataSourcePlugin),
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
dataSourceSettings: {},
|
||||||
|
dataSources: {},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(initDataSourceSettings)
|
||||||
|
.whenThunkIsDispatched(256, dependencies);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([initDataSourceSettingsSucceeded({} as GenericDataSourcePlugin)]);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledWith(256);
|
||||||
|
|
||||||
|
expect(dependencies.getDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.getDataSource).toHaveBeenCalledWith({}, 256);
|
||||||
|
|
||||||
|
expect(dependencies.getDataSourceMeta).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.getDataSourceMeta).toHaveBeenCalledWith({}, 'app');
|
||||||
|
|
||||||
|
expect(dependencies.importDataSourcePlugin).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.importDataSourcePlugin).toHaveBeenCalledWith(dataSourceMeta);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when plugin loading fails', () => {
|
||||||
|
it('then initDataSourceSettingsFailed should be dispatched', async () => {
|
||||||
|
const dependencies: InitDataSourceSettingDependencies = {
|
||||||
|
loadDataSource: jest.fn().mockImplementation(() => {
|
||||||
|
throw new Error('Error loading plugin');
|
||||||
|
}),
|
||||||
|
getDataSource: jest.fn(),
|
||||||
|
getDataSourceMeta: jest.fn(),
|
||||||
|
importDataSourcePlugin: jest.fn(),
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
dataSourceSettings: {},
|
||||||
|
dataSources: {},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(initDataSourceSettings)
|
||||||
|
.whenThunkIsDispatched(301, dependencies);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Error loading plugin'))]);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(dependencies.loadDataSource).toHaveBeenCalledWith(301);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('testDataSource', () => {
|
||||||
|
describe('when a datasource is tested', () => {
|
||||||
|
it('then testDataSourceStarting and testDataSourceSucceeded should be dispatched', async () => {
|
||||||
|
const dependencies: TestDataSourceDependencies = {
|
||||||
|
getDatasourceSrv: () =>
|
||||||
|
({
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
testDatasource: jest.fn().mockReturnValue({
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as any),
|
||||||
|
getBackendSrv: getBackendSrvMock,
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
testingStatus: {
|
||||||
|
status: '',
|
||||||
|
message: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(testDataSource)
|
||||||
|
.whenThunkIsDispatched('Azure Monitor', dependencies);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceSucceeded(state.testingStatus)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('then testDataSourceFailed should be dispatched', async () => {
|
||||||
|
const dependencies: TestDataSourceDependencies = {
|
||||||
|
getDatasourceSrv: () =>
|
||||||
|
({
|
||||||
|
get: jest.fn().mockReturnValue({
|
||||||
|
testDatasource: jest.fn().mockImplementation(() => {
|
||||||
|
throw new Error('Error testing datasource');
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as any),
|
||||||
|
getBackendSrv: getBackendSrvMock,
|
||||||
|
};
|
||||||
|
const result = {
|
||||||
|
message: 'Error testing datasource',
|
||||||
|
};
|
||||||
|
const state = {
|
||||||
|
testingStatus: {
|
||||||
|
message: '',
|
||||||
|
status: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const dispatchedActions = await thunkTester(state)
|
||||||
|
.givenThunk(testDataSource)
|
||||||
|
.whenThunkIsDispatched('Azure Monitor', dependencies);
|
||||||
|
|
||||||
|
expect(dispatchedActions).toEqual([testDataSourceStarting(), testDataSourceFailed(result)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import config from '../../../core/config';
|
import config from '../../../core/config';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||||
import { updateLocation, updateNavIndex } from 'app/core/actions';
|
import { updateLocation, updateNavIndex } from 'app/core/actions';
|
||||||
import { buildNavModel } from './navModel';
|
import { buildNavModel } from './navModel';
|
||||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||||
import { DataSourcePluginCategory, ThunkResult } from 'app/types';
|
import { DataSourcePluginCategory, ThunkResult, ThunkDispatch } from 'app/types';
|
||||||
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
|
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
|
||||||
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
||||||
import {
|
import {
|
||||||
@@ -13,14 +13,102 @@ import {
|
|||||||
dataSourcePluginsLoad,
|
dataSourcePluginsLoad,
|
||||||
dataSourcePluginsLoaded,
|
dataSourcePluginsLoaded,
|
||||||
dataSourcesLoaded,
|
dataSourcesLoaded,
|
||||||
|
initDataSourceSettingsFailed,
|
||||||
|
initDataSourceSettingsSucceeded,
|
||||||
|
testDataSourceStarting,
|
||||||
|
testDataSourceSucceeded,
|
||||||
|
testDataSourceFailed,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { buildCategories } from './buildCategories';
|
import { buildCategories } from './buildCategories';
|
||||||
|
import { getDataSource, getDataSourceMeta } from './selectors';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
export interface DataSourceTypesLoadedPayload {
|
export interface DataSourceTypesLoadedPayload {
|
||||||
plugins: DataSourcePluginMeta[];
|
plugins: DataSourcePluginMeta[];
|
||||||
categories: DataSourcePluginCategory[];
|
categories: DataSourcePluginCategory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InitDataSourceSettingDependencies {
|
||||||
|
loadDataSource: typeof loadDataSource;
|
||||||
|
getDataSource: typeof getDataSource;
|
||||||
|
getDataSourceMeta: typeof getDataSourceMeta;
|
||||||
|
importDataSourcePlugin: typeof importDataSourcePlugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestDataSourceDependencies {
|
||||||
|
getDatasourceSrv: typeof getDataSourceSrv;
|
||||||
|
getBackendSrv: typeof getBackendSrv;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initDataSourceSettings = (
|
||||||
|
pageId: number,
|
||||||
|
dependencies: InitDataSourceSettingDependencies = {
|
||||||
|
loadDataSource,
|
||||||
|
getDataSource,
|
||||||
|
getDataSourceMeta,
|
||||||
|
importDataSourcePlugin,
|
||||||
|
}
|
||||||
|
): ThunkResult<void> => {
|
||||||
|
return async (dispatch: ThunkDispatch, getState) => {
|
||||||
|
if (isNaN(pageId)) {
|
||||||
|
dispatch(initDataSourceSettingsFailed(new Error('Invalid ID')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await dispatch(dependencies.loadDataSource(pageId));
|
||||||
|
if (getState().dataSourceSettings.plugin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataSource = dependencies.getDataSource(getState().dataSources, pageId);
|
||||||
|
const dataSourceMeta = dependencies.getDataSourceMeta(getState().dataSources, dataSource.type);
|
||||||
|
const importedPlugin = await dependencies.importDataSourcePlugin(dataSourceMeta);
|
||||||
|
|
||||||
|
dispatch(initDataSourceSettingsSucceeded(importedPlugin));
|
||||||
|
} catch (err) {
|
||||||
|
console.log('Failed to import plugin module', err);
|
||||||
|
dispatch(initDataSourceSettingsFailed(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testDataSource = (
|
||||||
|
dataSourceName: string,
|
||||||
|
dependencies: TestDataSourceDependencies = {
|
||||||
|
getDatasourceSrv,
|
||||||
|
getBackendSrv,
|
||||||
|
}
|
||||||
|
): ThunkResult<void> => {
|
||||||
|
return async (dispatch: ThunkDispatch, getState) => {
|
||||||
|
const dsApi = await dependencies.getDatasourceSrv().get(dataSourceName);
|
||||||
|
|
||||||
|
if (!dsApi.testDatasource) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(testDataSourceStarting());
|
||||||
|
|
||||||
|
dependencies.getBackendSrv().withNoBackendCache(async () => {
|
||||||
|
try {
|
||||||
|
const result = await dsApi.testDatasource();
|
||||||
|
|
||||||
|
dispatch(testDataSourceSucceeded(result));
|
||||||
|
} catch (err) {
|
||||||
|
let message = '';
|
||||||
|
|
||||||
|
if (err.statusText) {
|
||||||
|
message = 'HTTP Error ' + err.statusText;
|
||||||
|
} else {
|
||||||
|
message = err.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(testDataSourceFailed({ message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export function loadDataSources(): ThunkResult<void> {
|
export function loadDataSources(): ThunkResult<void> {
|
||||||
return async dispatch => {
|
return async dispatch => {
|
||||||
const response = await getBackendSrv().get('/api/datasources');
|
const response = await getBackendSrv().get('/api/datasources');
|
||||||
@@ -123,7 +211,7 @@ export function findNewName(dataSources: ItemWithName[], name: string) {
|
|||||||
function updateFrontendSettings() {
|
function updateFrontendSettings() {
|
||||||
return getBackendSrv()
|
return getBackendSrv()
|
||||||
.get('/api/frontend/settings')
|
.get('/api/frontend/settings')
|
||||||
.then(settings => {
|
.then((settings: any) => {
|
||||||
config.datasources = settings.datasources;
|
config.datasources = settings.datasources;
|
||||||
config.defaultDatasource = settings.defaultDatasource;
|
config.defaultDatasource = settings.defaultDatasource;
|
||||||
getDatasourceSrv().init();
|
getDatasourceSrv().init();
|
||||||
|
|||||||
@@ -12,11 +12,16 @@ import {
|
|||||||
setDataSourcesSearchQuery,
|
setDataSourcesSearchQuery,
|
||||||
setDataSourceTypeSearchQuery,
|
setDataSourceTypeSearchQuery,
|
||||||
setIsDefault,
|
setIsDefault,
|
||||||
|
dataSourceSettingsReducer,
|
||||||
|
initialDataSourceSettingsState,
|
||||||
|
initDataSourceSettingsSucceeded,
|
||||||
|
initDataSourceSettingsFailed,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import { getMockDataSource, getMockDataSources } from '../__mocks__/dataSourcesMocks';
|
import { getMockDataSource, getMockDataSources } from '../__mocks__/dataSourcesMocks';
|
||||||
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
import { LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
import { DataSourcesState } from 'app/types';
|
import { DataSourcesState, DataSourceSettingsState } from 'app/types';
|
||||||
import { PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
|
import { PluginMeta, PluginMetaInfo, PluginType } from '@grafana/data';
|
||||||
|
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
|
||||||
|
|
||||||
const mockPlugin = () =>
|
const mockPlugin = () =>
|
||||||
({
|
({
|
||||||
@@ -136,3 +141,34 @@ describe('dataSourcesReducer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('dataSourceSettingsReducer', () => {
|
||||||
|
describe('when initDataSourceSettingsSucceeded is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester<DataSourceSettingsState>()
|
||||||
|
.givenReducer(dataSourceSettingsReducer, { ...initialDataSourceSettingsState })
|
||||||
|
.whenActionIsDispatched(initDataSourceSettingsSucceeded({} as GenericDataSourcePlugin))
|
||||||
|
.thenStateShouldEqual({
|
||||||
|
...initialDataSourceSettingsState,
|
||||||
|
plugin: {} as GenericDataSourcePlugin,
|
||||||
|
loadError: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when initDataSourceSettingsFailed is dispatched', () => {
|
||||||
|
it('then state should be correct', () => {
|
||||||
|
reducerTester<DataSourceSettingsState>()
|
||||||
|
.givenReducer(dataSourceSettingsReducer, {
|
||||||
|
...initialDataSourceSettingsState,
|
||||||
|
plugin: {} as GenericDataSourcePlugin,
|
||||||
|
})
|
||||||
|
.whenActionIsDispatched(initDataSourceSettingsFailed(new Error('Some error')))
|
||||||
|
.thenStatePredicateShouldEqual(resultingState => {
|
||||||
|
expect(resultingState.plugin).toEqual(null);
|
||||||
|
expect(resultingState.loadError).toEqual('Some error');
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
import { AnyAction, createAction } from '@reduxjs/toolkit';
|
||||||
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
|
||||||
|
|
||||||
import { DataSourcesState } from 'app/types';
|
import { DataSourcesState, DataSourceSettingsState } from 'app/types';
|
||||||
import { LayoutMode, LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
import { LayoutMode, LayoutModes } from 'app/core/components/LayoutSelector/LayoutSelector';
|
||||||
import { DataSourceTypesLoadedPayload } from './actions';
|
import { DataSourceTypesLoadedPayload } from './actions';
|
||||||
|
import { GenericDataSourcePlugin } from '../settings/PluginSettings';
|
||||||
|
|
||||||
export const initialState: DataSourcesState = {
|
export const initialState: DataSourcesState = {
|
||||||
dataSources: [],
|
dataSources: [],
|
||||||
@@ -94,6 +95,76 @@ export const dataSourcesReducer = (state: DataSourcesState = initialState, actio
|
|||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialDataSourceSettingsState: DataSourceSettingsState = {
|
||||||
|
testingStatus: {
|
||||||
|
status: null,
|
||||||
|
message: null,
|
||||||
|
},
|
||||||
|
loadError: null,
|
||||||
|
plugin: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initDataSourceSettingsSucceeded = createAction<GenericDataSourcePlugin>(
|
||||||
|
'dataSourceSettings/initDataSourceSettingsSucceeded'
|
||||||
|
);
|
||||||
|
|
||||||
|
export const initDataSourceSettingsFailed = createAction<Error>('dataSourceSettings/initDataSourceSettingsFailed');
|
||||||
|
|
||||||
|
export const testDataSourceStarting = createAction<undefined>('dataSourceSettings/testDataSourceStarting');
|
||||||
|
|
||||||
|
export const testDataSourceSucceeded = createAction<{
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
}>('dataSourceSettings/testDataSourceSucceeded');
|
||||||
|
|
||||||
|
export const testDataSourceFailed = createAction<{ message: string }>('dataSourceSettings/testDataSourceFailed');
|
||||||
|
|
||||||
|
export const dataSourceSettingsReducer = (
|
||||||
|
state: DataSourceSettingsState = initialDataSourceSettingsState,
|
||||||
|
action: AnyAction
|
||||||
|
): DataSourceSettingsState => {
|
||||||
|
if (initDataSourceSettingsSucceeded.match(action)) {
|
||||||
|
return { ...state, plugin: action.payload, loadError: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (initDataSourceSettingsFailed.match(action)) {
|
||||||
|
return { ...state, plugin: null, loadError: action.payload.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testDataSourceStarting.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
testingStatus: {
|
||||||
|
message: 'Testing...',
|
||||||
|
status: 'info',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testDataSourceSucceeded.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
testingStatus: {
|
||||||
|
status: action.payload.status,
|
||||||
|
message: action.payload.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testDataSourceFailed.match(action)) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
testingStatus: {
|
||||||
|
status: 'error',
|
||||||
|
message: action.payload.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
dataSources: dataSourcesReducer,
|
dataSources: dataSourcesReducer,
|
||||||
|
dataSourceSettings: dataSourceSettingsReducer,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const makeLog = (overides: Partial<LogRowModel>): LogRowModel => {
|
|||||||
hasAnsi: false,
|
hasAnsi: false,
|
||||||
labels: {},
|
labels: {},
|
||||||
raw: entry,
|
raw: entry,
|
||||||
timestamp: '',
|
|
||||||
timeFromNow: '',
|
timeFromNow: '',
|
||||||
timeEpochMs: 1,
|
timeEpochMs: 1,
|
||||||
timeLocal: '',
|
timeLocal: '',
|
||||||
|
|||||||
@@ -428,9 +428,11 @@ export const runQueries = (exploreId: ExploreId): ThunkResult<void> => {
|
|||||||
|
|
||||||
const queryOptions: QueryOptions = {
|
const queryOptions: QueryOptions = {
|
||||||
minInterval,
|
minInterval,
|
||||||
// This is used for logs streaming for buffer size, with undefined it falls back to datasource config if it
|
// maxDataPoints is used in:
|
||||||
// supports that.
|
// Loki - used for logs streaming for buffer size, with undefined it falls back to datasource config if it supports that.
|
||||||
maxDataPoints: mode === ExploreMode.Logs ? undefined : containerWidth,
|
// Elastic - limits the number of datapoints for the counts query and for logs it has hardcoded limit.
|
||||||
|
// Influx - used to correctly display logs in graph
|
||||||
|
maxDataPoints: mode === ExploreMode.Logs && datasourceInstance.name === 'Loki' ? undefined : containerWidth,
|
||||||
liveStreaming: live,
|
liveStreaming: live,
|
||||||
showingGraph,
|
showingGraph,
|
||||||
showingTable,
|
showingTable,
|
||||||
|
|||||||
@@ -187,7 +187,6 @@ describe('ResultProcessor', () => {
|
|||||||
timeFromNow: 'fromNow() jest mocked',
|
timeFromNow: 'fromNow() jest mocked',
|
||||||
timeLocal: 'format() jest mocked',
|
timeLocal: 'format() jest mocked',
|
||||||
timeUtc: 'format() jest mocked',
|
timeUtc: 'format() jest mocked',
|
||||||
timestamp: 300,
|
|
||||||
uid: '2',
|
uid: '2',
|
||||||
uniqueLabels: {},
|
uniqueLabels: {},
|
||||||
},
|
},
|
||||||
@@ -205,7 +204,6 @@ describe('ResultProcessor', () => {
|
|||||||
timeFromNow: 'fromNow() jest mocked',
|
timeFromNow: 'fromNow() jest mocked',
|
||||||
timeLocal: 'format() jest mocked',
|
timeLocal: 'format() jest mocked',
|
||||||
timeUtc: 'format() jest mocked',
|
timeUtc: 'format() jest mocked',
|
||||||
timestamp: 200,
|
|
||||||
uid: '1',
|
uid: '1',
|
||||||
uniqueLabels: {},
|
uniqueLabels: {},
|
||||||
},
|
},
|
||||||
@@ -223,7 +221,6 @@ describe('ResultProcessor', () => {
|
|||||||
timeFromNow: 'fromNow() jest mocked',
|
timeFromNow: 'fromNow() jest mocked',
|
||||||
timeLocal: 'format() jest mocked',
|
timeLocal: 'format() jest mocked',
|
||||||
timeUtc: 'format() jest mocked',
|
timeUtc: 'format() jest mocked',
|
||||||
timestamp: 100,
|
|
||||||
uid: '0',
|
uid: '0',
|
||||||
uniqueLabels: {},
|
uniqueLabels: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import coreModule from 'app/core/core_module';
|
|||||||
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
|
||||||
import { sanitizeUrl } from 'app/core/utils/text';
|
import { sanitizeUrl } from 'app/core/utils/text';
|
||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
|
import locationUtil from 'app/core/utils/location_util';
|
||||||
import { VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
|
import { VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/ui';
|
||||||
import {
|
import {
|
||||||
DataLink,
|
DataLink,
|
||||||
@@ -216,7 +217,7 @@ export class LinkSrv implements LinkService {
|
|||||||
constructor(private templateSrv: TemplateSrv, private timeSrv: TimeSrv) {}
|
constructor(private templateSrv: TemplateSrv, private timeSrv: TimeSrv) {}
|
||||||
|
|
||||||
getLinkUrl(link: any) {
|
getLinkUrl(link: any) {
|
||||||
const url = this.templateSrv.replace(link.url || '');
|
let url = locationUtil.assureBaseUrl(this.templateSrv.replace(link.url || ''));
|
||||||
const params: { [key: string]: any } = {};
|
const params: { [key: string]: any } = {};
|
||||||
|
|
||||||
if (link.keepTime) {
|
if (link.keepTime) {
|
||||||
@@ -229,7 +230,8 @@ export class LinkSrv implements LinkService {
|
|||||||
this.templateSrv.fillVariableValuesForUrl(params);
|
this.templateSrv.fillVariableValuesForUrl(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
return appendQueryToUrl(url, toUrlParams(params));
|
url = appendQueryToUrl(url, toUrlParams(params));
|
||||||
|
return getConfig().disableSanitizeHtml ? url : sanitizeUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnchorInfo(link: any) {
|
getAnchorInfo(link: any) {
|
||||||
@@ -266,7 +268,7 @@ export class LinkSrv implements LinkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const info: LinkModel<T> = {
|
const info: LinkModel<T> = {
|
||||||
href: href.replace(/\s|\n/g, ''),
|
href: locationUtil.assureBaseUrl(href.replace(/\s|\n/g, '')),
|
||||||
title: this.templateSrv.replace(link.title || '', scopedVars),
|
title: this.templateSrv.replace(link.title || '', scopedVars),
|
||||||
target: link.targetBlank ? '_blank' : '_self',
|
target: link.targetBlank ? '_blank' : '_self',
|
||||||
origin,
|
origin,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user