From 3a3272e225c986cdeb762197a82f84b84b9e769f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 12 Dec 2017 09:35:57 +0100 Subject: [PATCH 001/127] annotations: allows template variables to be used in tag filter When filtering built in annotations by tag, interpolates the tag with template variables. Fixes #9587 --- .../plugins/datasource/grafana/datasource.ts | 7 +- .../grafana/specs/datasource.jest.ts | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 5ca3c433476..9eb9862094a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; class GrafanaDatasource { /** @ngInject */ - constructor(private backendSrv, private $q) {} + constructor(private backendSrv, private $q, private templateSrv) {} query(options) { return this.backendSrv @@ -58,6 +58,11 @@ class GrafanaDatasource { if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) { return this.$q.when([]); } + const tags = []; + for (let t of params.tags) { + tags.push(this.templateSrv.replace(t)); + } + params.tags = tags; } return this.backendSrv.get('/api/annotations', params); diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts new file mode 100644 index 00000000000..544b04056ac --- /dev/null +++ b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts @@ -0,0 +1,65 @@ +import {GrafanaDatasource} from "../datasource"; +import q from 'q'; +import moment from 'moment'; + +describe('grafana data source', () => { + describe('when executing an annotations query', () => { + let calledBackendSrvParams; + const backendSrvStub = { + get: (url, options) => { + calledBackendSrvParams = options; + return q.resolve([]); + } + }; + + const templateSrvStub = { + replace: val => val.replace('$var', 'replaced') + }; + + const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); + + describe('with tags that have template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['tag1:$var']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('tag1:replaced'); + }); + }); + + describe('with type dashboard', () => { + const options = setupAnnotationQueryOptions( + { + type: 'dashboard', + tags: ['tag1'] + }, + {id: 1} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should remove tags from query options', () => { + expect(calledBackendSrvParams.tags).toBe(undefined); + }); + }); + }); +}); + +function setupAnnotationQueryOptions(annotation, dashboard?) { + return { + annotation: annotation, + dashboard: dashboard, + range: { + from: moment(1432288354), + to: moment(1432288401) + }, + rangeRaw: {from: "now-24h", to: "now"} + }; +} From 18e4271abdabaa111feabf656d3f1bc0f8bf0355 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 10:34:57 +0200 Subject: [PATCH 002/127] added span with folder title that is shown for recently and starred, created a new class for folder title --- public/app/core/components/search/search_results.html | 2 +- public/sass/components/_search.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 7435f8d0b7e..9f266ed3a6b 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -33,7 +33,7 @@ -
{{::item.title}}
+
{{::item.title}} {{::item.folderTitle}}
diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 8338a5d72ae..b00168505fa 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -208,6 +208,12 @@ color: $list-item-link-color; } +.search-item__body-folder-title { + color: $text-color-weak; + font-style: italic; + padding-left: 0.25rem; +} + .search-item__icon { padding: 5px; flex: 0 0 auto; From 83a73327cfb42ed5a3bea73497a5b4c7303a020e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 15:16:22 +0200 Subject: [PATCH 003/127] removed italic --- public/sass/components/_search.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index b00168505fa..3b6c1fbcce6 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -210,7 +210,6 @@ .search-item__body-folder-title { color: $text-color-weak; - font-style: italic; padding-left: 0.25rem; } From 8419cc05531a8db0bd3d3ce0a809096189ab3f33 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 4 Jun 2018 13:32:19 +0200 Subject: [PATCH 004/127] made folder text smaller --- public/sass/components/_search.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 3b6c1fbcce6..e2e3336db05 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -211,6 +211,7 @@ .search-item__body-folder-title { color: $text-color-weak; padding-left: 0.25rem; + font-size: $font-size-xs; } .search-item__icon { From c4308fedea8ee48973d39284e60562db1228fe6b Mon Sep 17 00:00:00 2001 From: Josh Dadak Date: Wed, 25 Jul 2018 14:02:36 +0100 Subject: [PATCH 005/127] Update Configuration.md Perhaps not worded as best it could be, however it would be good to include some information here about the importance of having your Grafana SERVER_ROOT_URL being the same URL listed in your Return URLs in Azure Application. Otherwise Azure Active Directory Auth will not work correctly resulting in an error page being displayed. --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2a799b044b3..8eee32bd616 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -629,7 +629,7 @@ allowed_organizations = team_ids = allowed_organizations = ``` - +Note: It's important to ensure that the SERVER_ROOT_URL in Grafana is set in your Azure Application Return URLs
## [auth.basic] From 87745e6e447f0f4acfd01d9d0984a03477c88c76 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 16:41:21 +0200 Subject: [PATCH 006/127] Explore: label selector for logging - query all available label keys for logs - query all values for each key - build cascader options with label values by key - lots of temporarily added conditions to reuse the promquery field --- public/app/containers/Explore/Explore.tsx | 1 + .../app/containers/Explore/PromQueryField.tsx | 82 +++++++++++++++++-- public/app/containers/Explore/QueryRows.tsx | 3 +- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 9620ac4f91b..bd52cd5ba05 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -564,6 +564,7 @@ export class Explore extends React.Component { onClickHintFix={this.onModifyQueries} onExecuteQuery={this.onSubmit} onRemoveQueryRow={this.onRemoveQueryRow} + supportsLogs={supportsLogs} />
{supportsGraph ? ( diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 1b3ff33971d..ee9496fb024 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -137,12 +137,14 @@ interface PromQueryFieldProps { onQueryChange?: (value: string, override?: boolean) => void; portalPrefix?: string; request?: (url: string) => any; + supportsLogs?: boolean; // To be removed after Logging gets its own query field } interface PromQueryFieldState { histogramMetrics: string[]; labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...] labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] + logLabelOptions: any[]; metrics: string[]; metricsByPrefix: CascaderOption[]; } @@ -171,16 +173,41 @@ class PromQueryField extends React.Component { + let query; + if (selectedOptions.length === 1) { + if (selectedOptions[0].children.length === 0) { + query = selectedOptions[0].value; + } else { + // Ignore click on group + return; + } + } else { + const key = selectedOptions[0].value; + const value = selectedOptions[1].value; + query = `{${key}="${value}"}`; + } + this.onChangeQuery(query, true); + }; + onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => { let query; if (selectedOptions.length === 1) { @@ -380,7 +407,8 @@ class PromQueryField extends React.Component this.fetchLabelValues(key))); @@ -409,6 +437,38 @@ class PromQueryField extends React.Component ({ label: value, value })), + }); + } + const labelValues = { [EMPTY_SELECTOR]: labelValuesByKey }; + this.setState({ labelKeys: labelKeysBySelector, labelValues, logLabelOptions }); + } catch (e) { + console.error(e); + } + } + async fetchLabelValues(key: string) { const url = `/api/v1/label/${key}/values`; try { @@ -463,8 +523,8 @@ class PromQueryField extends React.Component ({ label: hm, value: hm })); const metricsOptions = [ { label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions }, @@ -474,9 +534,15 @@ class PromQueryField extends React.Component
- - - + {supportsLogs ? ( + + + + ) : ( + + + + )}
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a7d91d59033..51adfa81c68 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -44,7 +44,7 @@ class QueryRow extends PureComponent { }; render() { - const { edited, history, query, queryError, queryHint, request } = this.props; + const { edited, history, query, queryError, queryHint, request, supportsLogs } = this.props; return (
@@ -58,6 +58,7 @@ class QueryRow extends PureComponent { onPressEnter={this.onPressEnter} onQueryChange={this.onChangeQuery} request={request} + supportsLogs={supportsLogs} />
From e62c083cf0da76d995ef71e51b5b1e68364ef6e2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Jun 2018 14:03:52 +0900 Subject: [PATCH 007/127] use series matchers to get label name/value --- .../datasource/prometheus/completer.ts | 18 +++++++++------- .../datasource/prometheus/datasource.ts | 8 +++++++ .../prometheus/specs/completer.test.ts | 21 ++++--------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 396a5fc1cd7..3cf4505e16a 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -113,7 +113,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -151,7 +151,7 @@ export class PromCompleter { var labelValues = this.transformToCompletions( _.uniq( result.map(r => { - return r.metric[labelName]; + return r[labelName]; }) ), 'label value' @@ -191,7 +191,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -233,7 +233,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -249,7 +249,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -276,9 +276,11 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - return this.datasource.performInstantQuery({ expr: query }, new Date().getTime() / 1000).then(response => { - this.labelQueryCache[expr] = response.data.data.result; - return response.data.data.result; + let range = this.datasource.getTimeRange(); + let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + return this.datasource.metadataRequest(url).then(response => { + this.labelQueryCache[expr] = response.data.data; + return response.data.data; }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 057bb55b3c3..c019fdc4aab 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,6 +629,14 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } + getTimeRange() { + let range = this.timeSrv.timeRange(); + return { + from: this.getPrometheusTime(range.from, false), + to: this.getPrometheusTime(range.to, true) + }; + } + getOriginalMetricName(labelData) { return this.resultTransformer.getOriginalMetricName(labelData); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 59fcc6592fb..201c8fcb0d7 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function() { +describe('Prometheus editor completer', function () { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -18,22 +18,9 @@ describe('Prometheus editor completer', function() { const backendSrv = {}; const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); - datasourceStub.performInstantQuery = jest.fn(() => - Promise.resolve({ - data: { - data: { - result: [ - { - metric: { - job: 'node', - instance: 'localhost:9100', - }, - }, - ], - }, - }, - }) - ); + datasourceStub.metadataRequest = jest.fn(() => + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); + datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From bf8840255c1e1236ddddfbcf00318e7a85229689 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 27 Jul 2018 11:39:00 +0900 Subject: [PATCH 008/127] Review feedback. --- public/app/plugins/datasource/prometheus/completer.ts | 6 +++--- public/app/plugins/datasource/prometheus/datasource.ts | 6 +++--- .../datasource/prometheus/specs/completer.test.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 3cf4505e16a..5719eeb5ac3 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -264,7 +264,7 @@ export class PromCompleter { return Promise.resolve([]); } - getLabelNameAndValueForExpression(expr, type) { + getLabelNameAndValueForExpression(expr: string, type: string): Promise { if (this.labelQueryCache[expr]) { return Promise.resolve(this.labelQueryCache[expr]); } @@ -276,8 +276,8 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - let range = this.datasource.getTimeRange(); - let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + const { start, end } = this.datasource.getTimeRange(); + const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; return this.datasource.metadataRequest(url).then(response => { this.labelQueryCache[expr] = response.data.data; return response.data.data; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c019fdc4aab..7f4b2fb1c98 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,11 +629,11 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } - getTimeRange() { + getTimeRange(): { start: number; end: number } { let range = this.timeSrv.timeRange(); return { - from: this.getPrometheusTime(range.from, false), - to: this.getPrometheusTime(range.to, true) + start: this.getPrometheusTime(range.from, false), + end: this.getPrometheusTime(range.to, true), }; } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 201c8fcb0d7..7a616c80c74 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function () { +describe('Prometheus editor completer', function() { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -19,8 +19,11 @@ describe('Prometheus editor completer', function () { const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); datasourceStub.metadataRequest = jest.fn(() => - Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); - datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100' } }] } }) + ); + datasourceStub.getTimeRange = jest.fn(() => { + return { start: 1514732400, end: 1514818800 }; + }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From 306c3e6c10fe755e58ddfe622749f4bd0f2c11bd Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 12:34:32 +0200 Subject: [PATCH 009/127] creating types, actions, reducer --- .../teams}/TeamGroupSync.tsx | 0 .../Teams => features/teams}/TeamList.tsx | 17 ++++++++- .../Teams => features/teams}/TeamMembers.tsx | 0 .../Teams => features/teams}/TeamPages.tsx | 0 .../Teams => features/teams}/TeamSettings.tsx | 0 public/app/features/teams/state/actions.ts | 28 +++++++++++++++ public/app/features/teams/state/reducers.ts | 14 ++++++++ public/app/features/teams/state/selectors.ts | 1 + public/app/routes/routes.ts | 4 +-- public/app/types/index.ts | 36 +++++++++++++++++++ 10 files changed, 97 insertions(+), 3 deletions(-) rename public/app/{containers/Teams => features/teams}/TeamGroupSync.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamList.tsx (89%) rename public/app/{containers/Teams => features/teams}/TeamMembers.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamPages.tsx (100%) rename public/app/{containers/Teams => features/teams}/TeamSettings.tsx (100%) create mode 100644 public/app/features/teams/state/actions.ts create mode 100644 public/app/features/teams/state/reducers.ts create mode 100644 public/app/features/teams/state/selectors.ts diff --git a/public/app/containers/Teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx similarity index 100% rename from public/app/containers/Teams/TeamGroupSync.tsx rename to public/app/features/teams/TeamGroupSync.tsx diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx similarity index 89% rename from public/app/containers/Teams/TeamList.tsx rename to public/app/features/teams/TeamList.tsx index d0feee75184..79d71c33596 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; @@ -6,6 +7,8 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { loadTeams } from './state/actions'; +import { getTeams } from './state/selectors'; interface Props { nav: typeof NavStore.Type; @@ -108,4 +111,16 @@ export class TeamList extends React.Component { } } -export default hot(module)(TeamList); +function mapStateToProps(state) { + return { + teams: getTeams(state), + }; +} + +function mapDispatchToProps() { + return { + loadTeams, + }; +} + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx similarity index 100% rename from public/app/containers/Teams/TeamMembers.tsx rename to public/app/features/teams/TeamMembers.tsx diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx similarity index 100% rename from public/app/containers/Teams/TeamPages.tsx rename to public/app/features/teams/TeamPages.tsx diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx similarity index 100% rename from public/app/containers/Teams/TeamSettings.tsx rename to public/app/features/teams/TeamSettings.tsx diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts new file mode 100644 index 00000000000..fafd2091217 --- /dev/null +++ b/public/app/features/teams/state/actions.ts @@ -0,0 +1,28 @@ +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState, Team } from '../../../types'; + +export enum ActionTypes { + LoadTeams = 'LOAD_TEAMS', +} + +export interface LoadTeamsAction { + type: ActionTypes.LoadTeams; + payload: Team[]; +} + +export type Action = LoadTeamsAction; + +type ThunkResult = ThunkAction; + +const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ + type: ActionTypes.LoadTeams, + payload: teams, +}); + +export function loadTeams(): ThunkResult { + return async dispatch => { + const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); + dispatch(teamsLoaded(teams)); + }; +} diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts new file mode 100644 index 00000000000..a104ae2e21c --- /dev/null +++ b/public/app/features/teams/state/reducers.ts @@ -0,0 +1,14 @@ +import { TeamsState } from '../../../types'; +import { Action } from './actions'; + +const initialState: TeamsState = { teams: [] }; + +export const teamsReducer = (state = initialState, action: Action): TeamsState => { + switch (action.type) { + } + return state; +}; + +export default { + teams: teamsReducer, +}; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts new file mode 100644 index 00000000000..f1f66695e65 --- /dev/null +++ b/public/app/features/teams/state/selectors.ts @@ -0,0 +1 @@ +export const getTeams = state => state.teams; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index dfd215f7056..1fd1a474cd3 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/containers/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import TeamPages from 'app/containers/Teams/TeamPages'; -import TeamList from 'app/containers/Teams/TeamList'; +import TeamPages from 'app/features/teams/TeamPages'; +import TeamList from 'app/features/teams/TeamList'; /** @ngInject **/ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..73cb05c26c1 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,9 @@ // Location // +import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; +import { types } from 'mobx-state-tree'; + export interface LocationUpdate { path?: string; query?: UrlQueryMap; @@ -53,6 +56,34 @@ export interface AlertRule { evalData?: { noData: boolean }; } +// +// Teams +// + +export interface Team { + id: number; + name: string; + avatarUrl: string; + email: string; + memberCount: number; + search?: string; + members?: TeamMember[]; + groups?: TeamGroup[]; +} + +export interface TeamMember { + userId: number; + teamId: number; + avatarUrl: string; + email: string; + login: string; +} + +export interface TeamGroup { + groupId: string; + teamId: number; +} + // // NavModel // @@ -89,8 +120,13 @@ export interface AlertRulesState { searchQuery: string; } +export interface TeamsState { + teams: Team[]; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; alertRules: AlertRulesState; + teams: TeamsState; } From 167f0098193475a0ece4c280d26d1dfa4fee5d1a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:13:21 +0200 Subject: [PATCH 010/127] load teams and store in redux --- .../app/features/alerting/AlertRuleList.tsx | 4 +- public/app/features/teams/TeamList.test.tsx | 61 ++++++ public/app/features/teams/TeamList.tsx | 65 +++--- .../__snapshots__/TeamList.test.tsx.snap | 204 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 4 +- public/app/features/teams/state/reducers.ts | 4 +- public/app/stores/configureStore.ts | 2 + public/app/types/index.ts | 3 - 8 files changed, 305 insertions(+), 42 deletions(-) create mode 100644 public/app/features/teams/TeamList.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamList.test.tsx.snap diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 4b48da47256..d30ba0ba802 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -115,7 +115,9 @@ export class AlertRuleList extends PureComponent {
    - {alertRules.map(rule => )} + {alertRules.map(rule => ( + {}} /> + ))}
diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx new file mode 100644 index 00000000000..e7db3edfd3d --- /dev/null +++ b/public/app/features/teams/TeamList.test.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamList, Props } from './TeamList'; +import { NavModel, Team } from '../../types'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teams: [] as Team[], + loadTeams: jest.fn(), + search: '', + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamList; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should render teams table', () => { + const { wrapper } = setup({ + teams: [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ], + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Life cycle', () => { + it('should call loadTeams', () => { + const { instance } = setup(); + + instance.componentDidMount(); + + expect(instance.props.loadTeams).toHaveBeenCalled(); + }); +}); + +describe('Functions', () => {}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 79d71c33596..801b66d24fc 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -1,44 +1,38 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { NavModel, Team } from '../../types'; import { loadTeams } from './state/actions'; import { getTeams } from './state/selectors'; +import { getNavModel } from 'app/core/selectors/navModel'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - backendSrv: BackendSrv; +export interface Props { + navModel: NavModel; + teams: Team[]; + loadTeams: typeof loadTeams; + search: string; } -@inject('nav', 'teams') -@observer -export class TeamList extends React.Component { - constructor(props) { - super(props); - - this.props.nav.load('cfg', 'teams'); +export class TeamList extends PureComponent { + componentDidMount() { this.fetchTeams(); } - fetchTeams() { - this.props.teams.loadTeams(); + async fetchTeams() { + await this.props.loadTeams(); } - deleteTeam(team: Team) { - this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); - } - - onSearchQueryChange = evt => { - this.props.teams.setSearchQuery(evt.target.value); + deleteTeam = (team: Team) => { + console.log('delete team', team); }; - renderTeamMember(team: Team): JSX.Element { + onSearchQueryChange = event => { + console.log('set search', event.target.value); + }; + + renderTeamMember(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -65,10 +59,11 @@ export class TeamList extends React.Component { } render() { - const { nav, teams } = this.props; + const { navModel, teams, search } = this.props; + return (
- +
@@ -77,7 +72,7 @@ export class TeamList extends React.Component { type="text" className="gf-form-input" placeholder="Search teams" - value={teams.search} + value={search} onChange={this.onSearchQueryChange} /> @@ -102,7 +97,7 @@ export class TeamList extends React.Component { - {teams.filteredTeams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeamMember(team))}
@@ -113,14 +108,14 @@ export class TeamList extends React.Component { function mapStateToProps(state) { return { - teams: getTeams(state), + navModel: getNavModel(state.navIndex, 'teams'), + teams: getTeams(state.teams), + search: '', }; } -function mapDispatchToProps() { - return { - loadTeams, - }; -} +const mapDispatchToProps = { + loadTeams, +}; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap new file mode 100644 index 00000000000..c93dafde1c6 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + +
+ + Name + + Email + + Members + +
+
+
+
+`; + +exports[`Render should render teams table 1`] = ` +
+ +
+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
+ + Name + + Email + + Members + +
+ + + + + + test + + + + test@test.com + + + + 1 + + + +
+
+
+
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index fafd2091217..35853bd73e8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -22,7 +22,7 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ export function loadTeams(): ThunkResult { return async dispatch => { - const teams = await getBackendSrv().get('/api/teams/search/', { perpage: 50, page: 1 }); - dispatch(teamsLoaded(teams)); + const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); + dispatch(teamsLoaded(response.teams)); }; } diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index a104ae2e21c..968c69d862c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,10 +1,12 @@ import { TeamsState } from '../../../types'; -import { Action } from './actions'; +import { Action, ActionTypes } from './actions'; const initialState: TeamsState = { teams: [] }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { + case ActionTypes.LoadTeams: + return { teams: action.payload }; } return state; }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 232f2e30cb8..a79c59a5fc1 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -3,10 +3,12 @@ import thunk from 'redux-thunk'; import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; +import teamsReducers from 'app/features/teams/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, + ...teamsReducers, }); export let store; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 73cb05c26c1..beb3253787a 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,9 +2,6 @@ // Location // -import { TeamGroupModel, TeamMemberModel } from '../stores/TeamsStore/TeamsStore'; -import { types } from 'mobx-state-tree'; - export interface LocationUpdate { path?: string; query?: UrlQueryMap; From 7e340b7aa5c1016c934a68d33597672e38e36584 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 15:32:51 +0200 Subject: [PATCH 011/127] delete team --- public/app/features/teams/TeamList.test.tsx | 38 +++++++++++++-------- public/app/features/teams/TeamList.tsx | 6 ++-- public/app/features/teams/state/actions.ts | 10 ++++++ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index e7db3edfd3d..7e4caff80ae 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TeamList, Props } from './TeamList'; +import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; const setup = (propOverrides?: object) => { @@ -8,6 +8,7 @@ const setup = (propOverrides?: object) => { navModel: {} as NavModel, teams: [] as Team[], loadTeams: jest.fn(), + deleteTeam: jest.fn(), search: '', }; @@ -22,6 +23,17 @@ const setup = (propOverrides?: object) => { }; }; +const mockTeam: Team = { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], +}; + describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -30,18 +42,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ], + teams: [mockTeam], }); expect(wrapper).toMatchSnapshot(); @@ -58,4 +59,13 @@ describe('Life cycle', () => { }); }); -describe('Functions', () => {}); +describe('Functions', () => { + describe('Delete team', () => { + it('should call delete team', () => { + const { instance } = setup(); + instance.deleteTeam(mockTeam); + + expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index 801b66d24fc..df8776920d7 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,7 +4,7 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams } from './state/actions'; +import { loadTeams, deleteTeam } from './state/actions'; import { getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; @@ -12,6 +12,7 @@ export interface Props { navModel: NavModel; teams: Team[]; loadTeams: typeof loadTeams; + deleteTeam: typeof deleteTeam; search: string; } @@ -25,7 +26,7 @@ export class TeamList extends PureComponent { } deleteTeam = (team: Team) => { - console.log('delete team', team); + this.props.deleteTeam(team.id); }; onSearchQueryChange = event => { @@ -116,6 +117,7 @@ function mapStateToProps(state) { const mapDispatchToProps = { loadTeams, + deleteTeam, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35853bd73e8..6afed1828c1 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -26,3 +26,13 @@ export function loadTeams(): ThunkResult { dispatch(teamsLoaded(response.teams)); }; } + +export function deleteTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .delete(`/api/teams/${id}`) + .then(() => { + dispatch(loadTeams()); + }); + }; +} From f68ac2021873bcc827de00423a5af90daab34faa Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 5 Sep 2018 16:49:36 +0200 Subject: [PATCH 012/127] set search query action and tests --- public/app/features/teams/TeamList.test.tsx | 30 +-- public/app/features/teams/TeamList.tsx | 20 +- .../app/features/teams/__mocks__/teamMocks.ts | 32 +++ .../__snapshots__/TeamList.test.tsx.snap | 204 +++++++++++++++++- public/app/features/teams/state/actions.ts | 13 +- .../app/features/teams/state/reducers.test.ts | 41 ++++ public/app/features/teams/state/reducers.ts | 7 +- .../features/teams/state/selectors.test.ts | 25 +++ public/app/features/teams/state/selectors.ts | 10 +- public/app/types/index.ts | 1 + 10 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 public/app/features/teams/__mocks__/teamMocks.ts create mode 100644 public/app/features/teams/state/reducers.test.ts create mode 100644 public/app/features/teams/state/selectors.test.ts diff --git a/public/app/features/teams/TeamList.test.tsx b/public/app/features/teams/TeamList.test.tsx index 7e4caff80ae..6c12f1357e5 100644 --- a/public/app/features/teams/TeamList.test.tsx +++ b/public/app/features/teams/TeamList.test.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Props, TeamList } from './TeamList'; import { NavModel, Team } from '../../types'; +import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks'; const setup = (propOverrides?: object) => { const props: Props = { @@ -9,7 +10,8 @@ const setup = (propOverrides?: object) => { teams: [] as Team[], loadTeams: jest.fn(), deleteTeam: jest.fn(), - search: '', + setSearchQuery: jest.fn(), + searchQuery: '', }; Object.assign(props, propOverrides); @@ -23,17 +25,6 @@ const setup = (propOverrides?: object) => { }; }; -const mockTeam: Team = { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], -}; - describe('Render', () => { it('should render component', () => { const { wrapper } = setup(); @@ -42,7 +33,7 @@ describe('Render', () => { it('should render teams table', () => { const { wrapper } = setup({ - teams: [mockTeam], + teams: getMultipleMockTeams(5), }); expect(wrapper).toMatchSnapshot(); @@ -63,9 +54,20 @@ describe('Functions', () => { describe('Delete team', () => { it('should call delete team', () => { const { instance } = setup(); - instance.deleteTeam(mockTeam); + instance.deleteTeam(getMockTeam()); expect(instance.props.deleteTeam).toHaveBeenCalledWith(1); }); }); + + describe('on search query change', () => { + it('should call setSearchQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'test' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test'); + }); + }); }); diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index df8776920d7..a95f0f17a27 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -4,8 +4,8 @@ import { hot } from 'react-hot-loader'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { NavModel, Team } from '../../types'; -import { loadTeams, deleteTeam } from './state/actions'; -import { getTeams } from './state/selectors'; +import { loadTeams, deleteTeam, setSearchQuery } from './state/actions'; +import { getSearchQuery, getTeams } from './state/selectors'; import { getNavModel } from 'app/core/selectors/navModel'; export interface Props { @@ -13,7 +13,8 @@ export interface Props { teams: Team[]; loadTeams: typeof loadTeams; deleteTeam: typeof deleteTeam; - search: string; + setSearchQuery: typeof setSearchQuery; + searchQuery: string; } export class TeamList extends PureComponent { @@ -30,10 +31,10 @@ export class TeamList extends PureComponent { }; onSearchQueryChange = event => { - console.log('set search', event.target.value); + this.props.setSearchQuery(event.target.value); }; - renderTeamMember(team: Team) { + renderTeam(team: Team) { const teamUrl = `org/teams/edit/${team.id}`; return ( @@ -60,7 +61,7 @@ export class TeamList extends PureComponent { } render() { - const { navModel, teams, search } = this.props; + const { navModel, teams, searchQuery } = this.props; return (
@@ -73,7 +74,7 @@ export class TeamList extends PureComponent { type="text" className="gf-form-input" placeholder="Search teams" - value={search} + value={searchQuery} onChange={this.onSearchQueryChange} /> @@ -98,7 +99,7 @@ export class TeamList extends PureComponent { - {teams.map(team => this.renderTeamMember(team))} + {teams.map(team => this.renderTeam(team))}
@@ -111,13 +112,14 @@ function mapStateToProps(state) { return { navModel: getNavModel(state.navIndex, 'teams'), teams: getTeams(state.teams), - search: '', + searchQuery: getSearchQuery(state.teams), }; } const mapDispatchToProps = { loadTeams, deleteTeam, + setSearchQuery, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamList)); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts new file mode 100644 index 00000000000..34405d2ce91 --- /dev/null +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -0,0 +1,32 @@ +import { Team } from '../../../types'; + +export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { + let teams: Team[] = []; + for (let i = 1; i <= numberOfTeams; i++) { + teams.push({ + id: i, + name: `test-${i}`, + avatarUrl: 'some/url/', + email: `test-${i}@test.com`, + memberCount: i, + search: '', + members: [], + groups: [], + }); + } + + return teams; +}; + +export const getMockTeam = (): Team => { + return { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index c93dafde1c6..6ea189f5dbd 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -167,7 +167,7 @@ exports[`Render should render teams table 1`] = ` - test + test-1 - test@test.com + test-1@test.com + + + + + + + + + test-2 + + + + + test-2@test.com + + + + + 2 + + + + + + + + + + + + + + + test-3 + + + + + test-3@test.com + + + + + 3 + + + + + + + + + + + + + + + test-4 + + + + + test-4@test.com + + + + + 4 + + + + + + + + + + + + + + + test-5 + + + + + test-5@test.com + + + + + 5 + + + + + +
diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 6afed1828c1..5914a932ad0 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -4,6 +4,7 @@ import { StoreState, Team } from '../../../types'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + SetSearchQuery = 'SET_SEARCH_QUERY', } export interface LoadTeamsAction { @@ -11,7 +12,12 @@ export interface LoadTeamsAction { payload: Team[]; } -export type Action = LoadTeamsAction; +export interface SetSearchQueryAction { + type: ActionTypes.SetSearchQuery; + payload: string; +} + +export type Action = LoadTeamsAction | SetSearchQueryAction; type ThunkResult = ThunkAction; @@ -20,6 +26,11 @@ const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ payload: teams, }); +export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ + type: ActionTypes.SetSearchQuery, + payload: searchQuery, +}); + export function loadTeams(): ThunkResult { return async dispatch => { const response = await getBackendSrv().get('/api/teams/search', { perpage: 1000, page: 1 }); diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts new file mode 100644 index 00000000000..e115d311e37 --- /dev/null +++ b/public/app/features/teams/state/reducers.test.ts @@ -0,0 +1,41 @@ +import { Action, ActionTypes } from './actions'; +import { initialState, teamsReducer } from './reducers'; + +describe('teams reducer', () => { + it('should set teams', () => { + const payload = [ + { + id: 1, + name: 'test', + avatarUrl: 'some/url/', + email: 'test@test.com', + memberCount: 1, + search: '', + members: [], + groups: [], + }, + ]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.teams).toEqual(payload); + }); + + it('should set search query', () => { + const payload = 'test'; + + const action: Action = { + type: ActionTypes.SetSearchQuery, + payload, + }; + + const result = teamsReducer(initialState, action); + + expect(result.searchQuery).toEqual('test'); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 968c69d862c..673fd240668 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,12 +1,15 @@ import { TeamsState } from '../../../types'; import { Action, ActionTypes } from './actions'; -const initialState: TeamsState = { teams: [] }; +export const initialState: TeamsState = { teams: [], searchQuery: '' }; export const teamsReducer = (state = initialState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: - return { teams: action.payload }; + return { ...state, teams: action.payload }; + + case ActionTypes.SetSearchQuery: + return { ...state, searchQuery: action.payload }; } return state; }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts new file mode 100644 index 00000000000..66fd07444ce --- /dev/null +++ b/public/app/features/teams/state/selectors.test.ts @@ -0,0 +1,25 @@ +import { getTeams } from './selectors'; +import { getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState } from '../../../types'; + +describe('Team selectors', () => { + describe('Get teams', () => { + const mockTeams = getMultipleMockTeams(5); + + it('should return teams if no search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '' }; + + const teams = getTeams(mockState); + + expect(teams).toEqual(mockTeams); + }); + + it('Should filter teams if search query', () => { + const mockState: TeamsState = { teams: mockTeams, searchQuery: '5' }; + + const teams = getTeams(mockState); + + expect(teams.length).toEqual(1); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index f1f66695e65..632bb2cd02a 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1 +1,9 @@ -export const getTeams = state => state.teams; +export const getSearchQuery = state => state.searchQuery; + +export const getTeams = state => { + const regex = RegExp(state.searchQuery, 'i'); + + return state.teams.filter(team => { + return regex.test(team.name); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index beb3253787a..b867a8f6989 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -119,6 +119,7 @@ export interface AlertRulesState { export interface TeamsState { teams: Team[]; + searchQuery: string; } export interface StoreState { From 05bfc3651626b82845626820e23c73e4fd57b2ce Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 14:32:09 +0200 Subject: [PATCH 013/127] Teampages page --- public/app/core/actions/index.ts | 3 +- public/app/core/actions/navModel.ts | 14 ++- public/app/core/reducers/navModel.ts | 18 ++- public/app/core/selectors/location.ts | 3 + public/app/core/selectors/navModel.ts | 2 +- public/app/features/teams/TeamGroupSync.tsx | 12 +- public/app/features/teams/TeamMembers.tsx | 27 ++--- public/app/features/teams/TeamPages.test.tsx | 63 +++++++++++ public/app/features/teams/TeamPages.tsx | 107 +++++++++++------- public/app/features/teams/TeamSettings.tsx | 10 +- .../features/teams/__mocks__/navModelMock.ts | 59 ++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 87 ++++++++++++++ public/app/features/teams/state/actions.ts | 57 +++++++++- .../app/features/teams/state/reducers.test.ts | 6 +- public/app/features/teams/state/reducers.ts | 17 ++- public/app/features/teams/state/selectors.ts | 2 + public/app/types/index.ts | 7 +- 17 files changed, 410 insertions(+), 84 deletions(-) create mode 100644 public/app/core/selectors/location.ts create mode 100644 public/app/features/teams/TeamPages.test.tsx create mode 100644 public/app/features/teams/__mocks__/navModelMock.ts create mode 100644 public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 74b61f845c0..b4b9b21126e 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,3 +1,4 @@ import { updateLocation } from './location'; +import { updateNavIndex } from './navModel'; -export { updateLocation }; +export { updateLocation, updateNavIndex }; diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index 56d129fd263..96465c6ef60 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,3 +1,9 @@ +import { NavModelItem } from '../../types'; + +export enum ActionTypes { + UpdateNavIndex = 'UPDATE_NAV_INDEX', +} + export type Action = UpdateNavIndexAction; // this action is not used yet @@ -5,9 +11,11 @@ export type Action = UpdateNavIndexAction; // like datasource edit, teams edit page export interface UpdateNavIndexAction { - type: 'UPDATE_NAV_INDEX'; + type: ActionTypes.UpdateNavIndex; + payload: NavModelItem; } -export const updateNavIndex = (): UpdateNavIndexAction => ({ - type: 'UPDATE_NAV_INDEX', +export const updateNavIndex = (item: NavModelItem): UpdateNavIndexAction => ({ + type: ActionTypes.UpdateNavIndex, + payload: item, }); diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 26acdb39a3d..ac0e51854e7 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,5 +1,5 @@ -import { Action } from 'app/core/actions/navModel'; -import { NavModelItem, NavIndex } from 'app/types'; +import { Action, ActionTypes } from 'app/core/actions/navModel'; +import { NavIndex, NavModelItem } from 'app/types'; import config from 'app/core/config'; export function buildInitialState(): NavIndex { @@ -25,5 +25,19 @@ function buildNavIndex(navIndex: NavIndex, children: NavModelItem[], parentItem? export const initialState: NavIndex = buildInitialState(); export const navIndexReducer = (state = initialState, action: Action): NavIndex => { + switch (action.type) { + case ActionTypes.UpdateNavIndex: + const newPages = {}; + const payload = action.payload; + + for (const node of payload.children) { + newPages[node.id] = { + ...node, + parentItem: payload, + }; + } + + return { ...state, ...newPages }; + } return state; }; diff --git a/public/app/core/selectors/location.ts b/public/app/core/selectors/location.ts new file mode 100644 index 00000000000..adc31f47e89 --- /dev/null +++ b/public/app/core/selectors/location.ts @@ -0,0 +1,3 @@ +export const getRouteParamsId = state => state.routeParams.id; + +export const getRouteParamsPage = state => state.routeParams.page; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index a7e1c3330bd..8b3a3edd84e 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,7 +1,7 @@ import { NavModel, NavModelItem, NavIndex } from 'app/types'; function getNotFoundModel(): NavModel { - var node: NavModelItem = { + const node: NavModelItem = { id: 'not-found', text: 'Page not found', icon: 'fa fa-fw fa-warning', diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index a3b2e4aed14..6562820d717 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamGroup } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import { Team, TeamGroup } from '../../types'; interface Props { team: Team; @@ -16,7 +15,6 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -@observer export class TeamGroupSync extends React.Component { constructor(props) { super(props); @@ -24,7 +22,7 @@ export class TeamGroupSync extends React.Component { } componentDidMount() { - this.props.team.loadGroups(); + // this.props.team.loadGroups(); } renderGroup(group: TeamGroup) { @@ -49,12 +47,12 @@ export class TeamGroupSync extends React.Component { }; onAddGroup = () => { - this.props.team.addGroup(this.state.newGroupId); + // this.props.team.addGroup(this.state.newGroupId); this.setState({ isAdding: false, newGroupId: '' }); }; onRemoveGroup = (group: TeamGroup) => { - this.props.team.removeGroup(group.groupId); + // this.props.team.removeGroup(group.groupId); }; isNewGroupValid() { @@ -63,7 +61,7 @@ export class TeamGroupSync extends React.Component { render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups.values(); + const groups = this.props.team.groups; return (
diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index b06a547063a..32eb0d09b63 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,10 +1,9 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { Team, TeamMember } from '../../types'; interface Props { team: Team; @@ -15,27 +14,26 @@ interface State { newTeamMember?: User; } -@observer -export class TeamMembers extends React.Component { +export class TeamMembers extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newTeamMember: null }; } componentDidMount() { - this.props.team.loadMembers(); + // this.props.team.loadMembers(); } onSearchQueryChange = evt => { - this.props.team.setSearchQuery(evt.target.value); + // this.props.team.setSearchQuery(evt.target.value); }; removeMember(member: TeamMember) { - this.props.team.removeMember(member); + // this.props.team.removeMember(member); } removeMemberConfirmed(member: TeamMember) { - this.props.team.removeMember(member); + // this.props.team.removeMember(member); } renderMember(member: TeamMember) { @@ -62,16 +60,15 @@ export class TeamMembers extends React.Component { }; onAddUserToTeam = async () => { - await this.props.team.addMember(this.state.newTeamMember.id); - await this.props.team.loadMembers(); - this.setState({ newTeamMember: null }); + // await this.props.team.addMember(this.state.newTeamMember.id); + // await this.props.team.loadMembers(); + // this.setState({ newTeamMember: null }); }; render() { const { newTeamMember, isAdding } = this.state; - const members = this.props.team.filteredMembers; - const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); const { team } = this.props; + const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return (
@@ -124,7 +121,7 @@ export class TeamMembers extends React.Component { - {members.map(member => this.renderMember(member))} + {team.members && team.members.map(member => this.renderMember(member))}
diff --git a/public/app/features/teams/TeamPages.test.tsx b/public/app/features/teams/TeamPages.test.tsx new file mode 100644 index 00000000000..65084d0dc47 --- /dev/null +++ b/public/app/features/teams/TeamPages.test.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamPages, Props } from './TeamPages'; +import { NavModel, Team } from '../../types'; +import { getMockTeam } from './__mocks__/teamMocks'; + +jest.mock('app/core/config', () => ({ + buildInfo: { isEnterprise: true }, +})); + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + teamId: 1, + loadTeam: jest.fn(), + pageName: 'members', + team: {} as Team, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance(); + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render member page if team not empty', () => { + const { wrapper } = setup({ + team: getMockTeam(), + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render settings page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'settings', + }); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render group sync page', () => { + const { wrapper } = setup({ + team: getMockTeam(), + pageName: 'groupsync', + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2abc9c51535..606e254e7ec 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -1,77 +1,106 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import _ from 'lodash'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import config from 'app/core/config'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { NavStore } from 'app/stores/NavStore/NavStore'; -import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; -import { ViewStore } from 'app/stores/ViewStore/ViewStore'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; +import { NavModel, Team } from '../../types'; +import { loadTeam } from './state/actions'; +import { getTeam } from './state/selectors'; +import { getNavModel } from '../../core/selectors/navModel'; +import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; -interface Props { - nav: typeof NavStore.Type; - teams: typeof TeamsStore.Type; - view: typeof ViewStore.Type; +export interface Props { + team: Team; + loadTeam: typeof loadTeam; + teamId: number; + pageName: string; + navModel: NavModel; } -@inject('nav', 'teams', 'view') -@observer -export class TeamPages extends React.Component { +interface State { isSyncEnabled: boolean; - currentPage: string; +} +enum PageTypes { + Members = 'members', + Settings = 'settings', + GroupSync = 'groupsync', +} + +export class TeamPages extends PureComponent { constructor(props) { super(props); - this.isSyncEnabled = config.buildInfo.isEnterprise; - this.currentPage = this.getCurrentPage(); + this.state = { + isSyncEnabled: config.buildInfo.isEnterprise, + }; + } + componentDidMount() { this.loadTeam(); } async loadTeam() { - const { teams, nav, view } = this.props; + const { loadTeam, teamId } = this.props; - await teams.loadById(view.routeParams.get('id')); - - nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); - } - - getCurrentTeam(): Team { - const { teams, view } = this.props; - return teams.map.get(view.routeParams.get('id')); + await loadTeam(teamId); } getCurrentPage() { const pages = ['members', 'settings', 'groupsync']; - const currentPage = this.props.view.routeParams.get('page'); + const currentPage = this.props.pageName; return _.includes(pages, currentPage) ? currentPage : pages[0]; } - render() { - const { nav } = this.props; - const currentTeam = this.getCurrentTeam(); + renderPage() { + const { team } = this.props; + const { isSyncEnabled } = this.state; + const currentPage = this.getCurrentPage(); - if (!nav.main) { - return null; + switch (currentPage) { + case PageTypes.Members: + return ; + + case PageTypes.Settings: + return ; + + case PageTypes.GroupSync: + return isSyncEnabled && ; } + return null; + } + + render() { + const { team, navModel } = this.props; + return (
- - {currentTeam && ( -
- {this.currentPage === 'members' && } - {this.currentPage === 'settings' && } - {this.currentPage === 'groupsync' && this.isSyncEnabled && } -
- )} + + {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
}
); } } -export default hot(module)(TeamPages); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + const pageName = getRouteParamsPage(state.location) || 'members'; + + return { + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + teamId: teamId, + pageName: pageName, + team: getTeam(state.team), + }; +} + +const mapDispatchToProps = { + loadTeam, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamPages)); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 0de60a0b16c..6e3c90d93f9 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,30 +1,28 @@ import React from 'react'; import { hot } from 'react-hot-loader'; -import { observer } from 'mobx-react'; -import { Team } from 'app/stores/TeamsStore/TeamsStore'; import { Label } from 'app/core/components/Forms/Forms'; +import { Team } from '../../types'; interface Props { team: Team; } -@observer export class TeamSettings extends React.Component { constructor(props) { super(props); } onChangeName = evt => { - this.props.team.setName(evt.target.value); + // this.props.team.setName(evt.target.value); }; onChangeEmail = evt => { - this.props.team.setEmail(evt.target.value); + // this.props.team.setEmail(evt.target.value); }; onUpdate = evt => { evt.preventDefault(); - this.props.team.update(); + // this.props.team.update(); }; render() { diff --git a/public/app/features/teams/__mocks__/navModelMock.ts b/public/app/features/teams/__mocks__/navModelMock.ts new file mode 100644 index 00000000000..7aa8515ee13 --- /dev/null +++ b/public/app/features/teams/__mocks__/navModelMock.ts @@ -0,0 +1,59 @@ +export const getMockNavModel = (pageName: string) => { + return { + node: { + active: false, + icon: 'gicon gicon-team', + id: `team-${pageName}-2`, + text: `${pageName}`, + url: 'org/teams/edit/2/members', + parentItem: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }, + main: { + img: '/avatar/b5695b61c91d13e7fa2fe71cfb95de9b', + id: 'team-2', + subTitle: 'Manage members & settings', + url: '', + text: 'test1', + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: true, + icon: 'gicon gicon-team', + id: 'team-members-2', + text: 'Members', + url: 'org/teams/edit/2/members', + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: 'team-settings-2', + text: 'Settings', + url: 'org/teams/edit/2/settings', + }, + ], + }, + }; +}; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap new file mode 100644 index 00000000000..3c19d726e41 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ +
+`; + +exports[`Render should render group sync page 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render member page if team not empty 1`] = ` +
+ +
+ +
+
+`; + +exports[`Render should render settings page 1`] = ` +
+ +
+ +
+
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5914a932ad0..35d07157dec 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,9 +1,12 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team } from '../../../types'; +import { updateNavIndex } from '../../../core/actions'; +import { UpdateNavIndexAction } from '../../../core/actions/navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', + LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', } @@ -12,20 +15,30 @@ export interface LoadTeamsAction { payload: Team[]; } +export interface LoadTeamAction { + type: ActionTypes.LoadTeam; + payload: Team; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction; +export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; const teamsLoaded = (teams: Team[]): LoadTeamsAction => ({ type: ActionTypes.LoadTeams, payload: teams, }); +const teamLoaded = (team: Team): LoadTeamAction => ({ + type: ActionTypes.LoadTeam, + payload: team, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -38,6 +51,44 @@ export function loadTeams(): ThunkResult { }; } +function buildNavModel(team: Team): NavModelItem { + return { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; +} + +export function loadTeam(id: number): ThunkResult { + return async dispatch => { + await getBackendSrv() + .get(`/api/teams/${id}`) + .then(response => { + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index e115d311e37..0ab64a78e41 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from './actions'; -import { initialState, teamsReducer } from './reducers'; +import { initialTeamsState, teamsReducer } from './reducers'; describe('teams reducer', () => { it('should set teams', () => { @@ -21,7 +21,7 @@ describe('teams reducer', () => { payload, }; - const result = teamsReducer(initialState, action); + const result = teamsReducer(initialTeamsState, action); expect(result.teams).toEqual(payload); }); @@ -34,7 +34,7 @@ describe('teams reducer', () => { payload, }; - const result = teamsReducer(initialState, action); + const result = teamsReducer(initialTeamsState, action); expect(result.searchQuery).toEqual('test'); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 673fd240668..56a2f83cd8d 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,9 +1,10 @@ -import { TeamsState } from '../../../types'; +import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; -export const initialState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; -export const teamsReducer = (state = initialState, action: Action): TeamsState => { +export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { case ActionTypes.LoadTeams: return { ...state, teams: action.payload }; @@ -14,6 +15,16 @@ export const teamsReducer = (state = initialState, action: Action): TeamsState = return state; }; +export const teamReducer = (state = initialTeamState, action: Action): TeamState => { + switch (action.type) { + case ActionTypes.LoadTeam: + return { ...state, team: action.payload }; + } + + return state; +}; + export default { teams: teamsReducer, + team: teamReducer, }; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 632bb2cd02a..40940cbae52 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,7 @@ export const getSearchQuery = state => state.searchQuery; +export const getTeam = state => state.team; + export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b867a8f6989..8b0010b6561 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -96,7 +96,7 @@ export interface NavModelItem { hideFromTabs?: boolean; divider?: boolean; children?: NavModelItem[]; - breadcrumbs?: NavModelItem[]; + breadcrumbs?: { title: string; url: string }[]; target?: string; parentItem?: NavModelItem; } @@ -122,6 +122,11 @@ export interface TeamsState { searchQuery: string; } +export interface TeamState { + team: Team; + searchQuery: string; +} + export interface StoreState { navIndex: NavIndex; location: LocationState; From 59b3bfd34293e46ef19c091e45f6be7d16fd309b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 7 Sep 2018 18:01:59 +0200 Subject: [PATCH 014/127] team members, bug in fetching team --- public/app/features/teams/TeamMembers.tsx | 68 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 10 +++ public/app/features/teams/state/actions.ts | 67 +++++++++++++++++- .../app/features/teams/state/reducers.test.ts | 37 ++++++---- public/app/features/teams/state/reducers.ts | 8 ++- public/app/features/teams/state/selectors.ts | 8 ++- public/app/types/index.ts | 3 +- 8 files changed, 161 insertions(+), 44 deletions(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 32eb0d09b63..115fb40e184 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,12 +1,21 @@ import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; import { Team, TeamMember } from '../../types'; +import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; +import { getSearchMemberQuery, getTeam } from './state/selectors'; +import { getRouteParamsId } from '../../core/selectors/location'; interface Props { team: Team; + searchMemberQuery: string; + loadTeamMembers: typeof loadTeamMembers; + addTeamMember: typeof addTeamMember; + removeTeamMember: typeof removeTeamMember; + setSearchMemberQuery: typeof setSearchMemberQuery; } interface State { @@ -21,20 +30,29 @@ export class TeamMembers extends PureComponent { } componentDidMount() { - // this.props.team.loadMembers(); + this.props.loadTeamMembers(); } - onSearchQueryChange = evt => { - // this.props.team.setSearchQuery(evt.target.value); + onSearchQueryChange = event => { + this.props.setSearchMemberQuery(event.target.value); }; removeMember(member: TeamMember) { - // this.props.team.removeMember(member); + this.props.removeTeamMember(member.userId); } - removeMemberConfirmed(member: TeamMember) { - // this.props.team.removeMember(member); - } + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onUserSelected = (user: User) => { + this.setState({ newTeamMember: user }); + }; + + onAddUserToTeam = async () => { + this.props.addTeamMember(this.state.newTeamMember.id); + this.setState({ newTeamMember: null }); + }; renderMember(member: TeamMember) { return ( @@ -51,23 +69,9 @@ export class TeamMembers extends PureComponent { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onUserSelected = (user: User) => { - this.setState({ newTeamMember: user }); - }; - - onAddUserToTeam = async () => { - // await this.props.team.addMember(this.state.newTeamMember.id); - // await this.props.team.loadMembers(); - // this.setState({ newTeamMember: null }); - }; - render() { const { newTeamMember, isAdding } = this.state; - const { team } = this.props; + const { team, searchMemberQuery } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -79,7 +83,7 @@ export class TeamMembers extends PureComponent { type="text" className="gf-form-input" placeholder="Search members" - value={team.search} + value={searchMemberQuery} onChange={this.onSearchQueryChange} /> @@ -129,4 +133,20 @@ export class TeamMembers extends PureComponent { } } -export default hot(module)(TeamMembers); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + searchMemberQuery: getSearchMemberQuery(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamMembers, + addTeamMember, + removeTeamMember, + setSearchMemberQuery, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 606e254e7ec..4395c0bfbef 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -63,7 +63,7 @@ export class TeamPages extends PureComponent { switch (currentPage) { case PageTypes.Members: - return ; + return ; case PageTypes.Settings: return ; @@ -95,7 +95,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), teamId: teamId, pageName: pageName, - team: getTeam(state.team), + team: getTeam(state.team, teamId), }; } diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 34405d2ce91..21c0cf012f0 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -30,3 +30,13 @@ export const getMockTeam = (): Team => { groups: [], }; }; + +export const getMockTeamMember = () => { + return { + userId: 1, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: 'testUser', + }; +}; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 35d07157dec..e407737bb20 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,6 +1,6 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; @@ -8,6 +8,8 @@ export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', LoadTeam = 'LOAD_TEAM', SetSearchQuery = 'SET_SEARCH_QUERY', + SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', + LoadTeamMembers = 'TEAM_MEMBERS_LOADED', } export interface LoadTeamsAction { @@ -20,12 +22,27 @@ export interface LoadTeamAction { payload: Team; } +export interface LoadTeamMembersAction { + type: ActionTypes.LoadTeamMembers; + payload: TeamMember[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; } -export type Action = LoadTeamsAction | SetSearchQueryAction | LoadTeamAction; +export interface SetSearchMemberQueryAction { + type: ActionTypes.SetSearchMemberQuery; + payload: string; +} + +export type Action = + | LoadTeamsAction + | SetSearchQueryAction + | LoadTeamAction + | LoadTeamMembersAction + | SetSearchMemberQueryAction; type ThunkResult = ThunkAction; @@ -39,6 +56,16 @@ const teamLoaded = (team: Team): LoadTeamAction => ({ payload: team, }); +const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => ({ + type: ActionTypes.LoadTeamMembers, + payload: teamMembers, +}); + +export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ + type: ActionTypes.SetSearchMemberQuery, + payload: searchQuery, +}); + export const setSearchQuery = (searchQuery: string): SetSearchQueryAction => ({ type: ActionTypes.SetSearchQuery, payload: searchQuery, @@ -89,6 +116,42 @@ export function loadTeam(id: number): ThunkResult { }; } +export function loadTeamMembers(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/members`) + .then(response => { + dispatch(teamMembersLoaded(response)); + }); + }; +} + +export function addTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/members`, { userId: id }) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + +export function removeTeamMember(id: number): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/members/${id}`) + .then(() => { + dispatch(loadTeamMembers()); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 0ab64a78e41..492ec71ba4b 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -1,20 +1,10 @@ import { Action, ActionTypes } from './actions'; -import { initialTeamsState, teamsReducer } from './reducers'; +import { initialTeamsState, initialTeamState, teamReducer, teamsReducer } from './reducers'; +import { getMockTeam, getMockTeamMember } from '../__mocks__/teamMocks'; describe('teams reducer', () => { it('should set teams', () => { - const payload = [ - { - id: 1, - name: 'test', - avatarUrl: 'some/url/', - email: 'test@test.com', - memberCount: 1, - search: '', - members: [], - groups: [], - }, - ]; + const payload = [getMockTeam()]; const action: Action = { type: ActionTypes.LoadTeams, @@ -39,3 +29,24 @@ describe('teams reducer', () => { expect(result.searchQuery).toEqual('test'); }); }); + +describe('team reducer', () => { + it('should set team members', () => { + const mockTeamMember = getMockTeamMember(); + const mockTeam = getMockTeam(); + const state = { + ...initialTeamState, + team: mockTeam, + }; + + const action: Action = { + type: ActionTypes.LoadTeamMembers, + payload: [mockTeamMember], + }; + + const result = teamReducer(state, action); + const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + + expect(result).toEqual(expectedState); + }); +}); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index 56a2f83cd8d..e30fddb22a5 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -2,7 +2,7 @@ import { Team, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchQuery: '' }; +export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -19,6 +19,12 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState switch (action.type) { case ActionTypes.LoadTeam: return { ...state, team: action.payload }; + + case ActionTypes.LoadTeamMembers: + return { ...state, team: { ...state.team, members: action.payload } }; + + case ActionTypes.SetSearchMemberQuery: + return { ...state, searchMemberQuery: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 40940cbae52..d6142adf157 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,6 +1,12 @@ export const getSearchQuery = state => state.searchQuery; +export const getSearchMemberQuery = state => state.searchMemberQuery; -export const getTeam = state => state.team; +export const getTeam = (state, currentTeamId) => { + if (state.team.id === currentTeamId) { + console.log('yes'); + return state.team; + } +}; export const getTeams = state => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 8b0010b6561..27ae3dbe19b 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -124,7 +124,7 @@ export interface TeamsState { export interface TeamState { team: Team; - searchQuery: string; + searchMemberQuery: string; } export interface StoreState { @@ -132,4 +132,5 @@ export interface StoreState { location: LocationState; alertRules: AlertRulesState; teams: TeamsState; + team: TeamState; } From d494ebc7309b363a8d131b1d719131864196e04c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:19:27 +0200 Subject: [PATCH 015/127] flattened team state, tests for TeamMembers --- .../app/features/teams/TeamMembers.test.tsx | 79 +++++ public/app/features/teams/TeamMembers.tsx | 24 +- public/app/features/teams/TeamPages.tsx | 4 +- .../app/features/teams/__mocks__/teamMocks.ts | 26 +- .../__snapshots__/TeamMembers.test.tsx.snap | 317 ++++++++++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 21 +- public/app/features/teams/state/actions.ts | 1 + .../app/features/teams/state/reducers.test.ts | 36 +- public/app/features/teams/state/reducers.ts | 11 +- .../features/teams/state/selectors.test.ts | 22 +- public/app/features/teams/state/selectors.ts | 11 +- public/app/types/index.ts | 5 +- 12 files changed, 493 insertions(+), 64 deletions(-) create mode 100644 public/app/features/teams/TeamMembers.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx new file mode 100644 index 00000000000..cae37e184fb --- /dev/null +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { TeamMembers, Props } from './TeamMembers'; +import { TeamMember } from '../../types'; +import { getMockTeamMember, getMockTeamMembers } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + members: [] as TeamMember[], + searchMemberQuery: '', + setSearchMemberQuery: jest.fn(), + loadTeamMembers: jest.fn(), + addTeamMember: jest.fn(), + removeTeamMember: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamMembers; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); + + it('should render team members', () => { + const { wrapper } = setup({ + members: getMockTeamMembers(5), + }); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + describe('on search member query change', () => { + it('it should call setSearchMemberQuery', () => { + const { instance } = setup(); + const mockEvent = { target: { value: 'member' } }; + + instance.onSearchQueryChange(mockEvent); + + expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member'); + }); + }); + + describe('on remove member', () => { + const { instance } = setup(); + const mockTeamMember = getMockTeamMember(); + + instance.onRemoveMember(mockTeamMember); + + expect(instance.props.removeTeamMember).toHaveBeenCalledWith(1); + }); + + describe('on add user to team', () => { + const { wrapper, instance } = setup(); + + wrapper.state().newTeamMember = { + id: 1, + label: '', + avatarUrl: '', + login: '', + }; + + instance.onAddUserToTeam(); + + expect(instance.props.addTeamMember).toHaveBeenCalledWith(1); + }); +}); diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 115fb40e184..5ad688aabf8 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -1,16 +1,14 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; -import { hot } from 'react-hot-loader'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; -import { Team, TeamMember } from '../../types'; +import { TeamMember } from '../../types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; -import { getSearchMemberQuery, getTeam } from './state/selectors'; -import { getRouteParamsId } from '../../core/selectors/location'; +import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + members: TeamMember[]; searchMemberQuery: string; loadTeamMembers: typeof loadTeamMembers; addTeamMember: typeof addTeamMember; @@ -37,7 +35,7 @@ export class TeamMembers extends PureComponent { this.props.setSearchMemberQuery(event.target.value); }; - removeMember(member: TeamMember) { + onRemoveMember(member: TeamMember) { this.props.removeTeamMember(member.userId); } @@ -63,7 +61,7 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} - this.removeMember(member)} /> + this.onRemoveMember(member)} /> ); @@ -71,7 +69,7 @@ export class TeamMembers extends PureComponent { render() { const { newTeamMember, isAdding } = this.state; - const { team, searchMemberQuery } = this.props; + const { searchMemberQuery, members } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -125,7 +123,7 @@ export class TeamMembers extends PureComponent { - {team.members && team.members.map(member => this.renderMember(member))} + {members && members.map(member => this.renderMember(member))}
@@ -134,10 +132,8 @@ export class TeamMembers extends PureComponent { } function mapStateToProps(state) { - const teamId = getRouteParamsId(state.location); - return { - team: getTeam(state.team, teamId), + members: getTeamMembers(state.team), searchMemberQuery: getSearchMemberQuery(state.team), }; } @@ -149,4 +145,4 @@ const mapDispatchToProps = { setSearchMemberQuery, }; -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(TeamMembers)); +export default connect(mapStateToProps, mapDispatchToProps)(TeamMembers); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 4395c0bfbef..2528c3c87b8 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -41,10 +41,10 @@ export class TeamPages extends PureComponent { } componentDidMount() { - this.loadTeam(); + this.fetchTeam(); } - async loadTeam() { + async fetchTeam() { const { loadTeam, teamId } = this.props; await loadTeam(teamId); diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index 21c0cf012f0..7050997c387 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -1,4 +1,4 @@ -import { Team } from '../../../types'; +import { Team, TeamMember } from '../../../types'; export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { let teams: Team[] = []; @@ -9,9 +9,6 @@ export const getMultipleMockTeams = (numberOfTeams: number): Team[] => { avatarUrl: 'some/url/', email: `test-${i}@test.com`, memberCount: i, - search: '', - members: [], - groups: [], }); } @@ -25,13 +22,26 @@ export const getMockTeam = (): Team => { avatarUrl: 'some/url/', email: 'test@test.com', memberCount: 1, - search: '', - members: [], - groups: [], }; }; -export const getMockTeamMember = () => { +export const getMockTeamMembers = (amount: number): TeamMember[] => { + let teamMembers: TeamMember[] = []; + + for (let i = 1; i <= amount; i++) { + teamMembers.push({ + userId: i, + teamId: 1, + avatarUrl: 'some/url/', + email: 'test@test.com', + login: `testUser-${i}`, + }); + } + + return teamMembers; +}; + +export const getMockTeamMember = (): TeamMember => { return { userId: 1, teamId: 1, diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap new file mode 100644 index 00000000000..2a42897e2b9 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -0,0 +1,317 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + +
+ + Name + + Email + +
+
+
+`; + +exports[`Render should render team members 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + +
+ + + testUser-1 + + test@test.com + + +
+ + + testUser-2 + + test@test.com + + +
+ + + testUser-3 + + test@test.com + + +
+ + + testUser-4 + + test@test.com + + +
+ + + testUser-5 + + test@test.com + + +
+
+
+`; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 3c19d726e41..563d3d3bb99 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -21,12 +21,9 @@ exports[`Render should render group sync page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> @@ -42,20 +39,7 @@ exports[`Render should render member page if team not empty 1`] = `
- +
`; @@ -73,12 +57,9 @@ exports[`Render should render settings page 1`] = ` Object { "avatarUrl": "some/url/", "email": "test@test.com", - "groups": Array [], "id": 1, "memberCount": 1, - "members": Array [], "name": "test", - "search": "", } } /> diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index e407737bb20..4786edf60a8 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -117,6 +117,7 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { + console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; diff --git a/public/app/features/teams/state/reducers.test.ts b/public/app/features/teams/state/reducers.test.ts index 492ec71ba4b..7f7a33d60ac 100644 --- a/public/app/features/teams/state/reducers.test.ts +++ b/public/app/features/teams/state/reducers.test.ts @@ -31,22 +31,42 @@ describe('teams reducer', () => { }); describe('team reducer', () => { + it('should set team', () => { + const payload = getMockTeam(); + + const action: Action = { + type: ActionTypes.LoadTeam, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.team).toEqual(payload); + }); + it('should set team members', () => { const mockTeamMember = getMockTeamMember(); - const mockTeam = getMockTeam(); - const state = { - ...initialTeamState, - team: mockTeam, - }; const action: Action = { type: ActionTypes.LoadTeamMembers, payload: [mockTeamMember], }; - const result = teamReducer(state, action); - const expectedState = { team: { ...mockTeam, members: [mockTeamMember] }, searchQuery: '' }; + const result = teamReducer(initialTeamState, action); - expect(result).toEqual(expectedState); + expect(result.members).toEqual([mockTeamMember]); + }); + + it('should set member search query', () => { + const payload = 'member'; + + const action: Action = { + type: ActionTypes.SetSearchMemberQuery, + payload, + }; + + const result = teamReducer(initialTeamState, action); + + expect(result.searchMemberQuery).toEqual('member'); }); }); diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index e30fddb22a5..f02ade60923 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -1,8 +1,13 @@ -import { Team, TeamsState, TeamState } from '../../../types'; +import { Team, TeamGroup, TeamMember, TeamsState, TeamState } from '../../../types'; import { Action, ActionTypes } from './actions'; export const initialTeamsState: TeamsState = { teams: [], searchQuery: '' }; -export const initialTeamState: TeamState = { team: {} as Team, searchMemberQuery: '' }; +export const initialTeamState: TeamState = { + team: {} as Team, + members: [] as TeamMember[], + groups: [] as TeamGroup[], + searchMemberQuery: '', +}; export const teamsReducer = (state = initialTeamsState, action: Action): TeamsState => { switch (action.type) { @@ -21,7 +26,7 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState return { ...state, team: action.payload }; case ActionTypes.LoadTeamMembers: - return { ...state, team: { ...state.team, members: action.payload } }; + return { ...state, members: action.payload }; case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index 66fd07444ce..e1b11cf288b 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,8 +1,8 @@ -import { getTeams } from './selectors'; -import { getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState } from '../../../types'; +import { getTeam, getTeams } from './selectors'; +import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { TeamsState, TeamState } from '../../../types'; -describe('Team selectors', () => { +describe('Teams selectors', () => { describe('Get teams', () => { const mockTeams = getMultipleMockTeams(5); @@ -23,3 +23,17 @@ describe('Team selectors', () => { }); }); }); + +describe('Team selectors', () => { + describe('Get team', () => { + const mockTeam = getMockTeam(); + + it('should return team if matching with location team', () => { + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + + const team = getTeam(mockState, '1'); + + expect(team).toEqual(mockTeam); + }); + }); +}); diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index d6142adf157..5e22f96eaf7 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -2,8 +2,7 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; export const getTeam = (state, currentTeamId) => { - if (state.team.id === currentTeamId) { - console.log('yes'); + if (state.team.id === parseInt(currentTeamId)) { return state.team; } }; @@ -15,3 +14,11 @@ export const getTeams = state => { return regex.test(team.name); }); }; + +export const getTeamMembers = state => { + const regex = RegExp(state.searchMemberQuery, 'i'); + + return state.members.filter(member => { + return regex.test(member.login) || regex.test(member.email); + }); +}; diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 27ae3dbe19b..35cd9a41f4e 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -63,9 +63,6 @@ export interface Team { avatarUrl: string; email: string; memberCount: number; - search?: string; - members?: TeamMember[]; - groups?: TeamGroup[]; } export interface TeamMember { @@ -124,6 +121,8 @@ export interface TeamsState { export interface TeamState { team: Team; + members: TeamMember[]; + groups: TeamGroup[]; searchMemberQuery: string; } From 841bd5817de3102c7310625aff94ed6d58d03bb2 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 14:27:33 +0200 Subject: [PATCH 016/127] test for team member selector --- .../features/teams/state/selectors.test.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/public/app/features/teams/state/selectors.test.ts b/public/app/features/teams/state/selectors.test.ts index e1b11cf288b..5f338069bbb 100644 --- a/public/app/features/teams/state/selectors.test.ts +++ b/public/app/features/teams/state/selectors.test.ts @@ -1,6 +1,6 @@ -import { getTeam, getTeams } from './selectors'; -import { getMockTeam, getMultipleMockTeams } from '../__mocks__/teamMocks'; -import { TeamsState, TeamState } from '../../../types'; +import { getTeam, getTeamMembers, getTeams } from './selectors'; +import { getMockTeam, getMockTeamMembers, getMultipleMockTeams } from '../__mocks__/teamMocks'; +import { Team, TeamGroup, TeamsState, TeamState } from '../../../types'; describe('Teams selectors', () => { describe('Get teams', () => { @@ -29,11 +29,28 @@ describe('Team selectors', () => { const mockTeam = getMockTeam(); it('should return team if matching with location team', () => { - const mockState: TeamState = { team: mockTeam, searchMemberQuery: '' }; + const mockState: TeamState = { team: mockTeam, searchMemberQuery: '', members: [], groups: [] }; const team = getTeam(mockState, '1'); expect(team).toEqual(mockTeam); }); }); + + describe('Get members', () => { + const mockTeamMembers = getMockTeamMembers(5); + + it('should return team members', () => { + const mockState: TeamState = { + team: {} as Team, + searchMemberQuery: '', + members: mockTeamMembers, + groups: [] as TeamGroup[], + }; + + const members = getTeamMembers(mockState); + + expect(members).toEqual(mockTeamMembers); + }); + }); }); From 59b5b146daaa7655a0a77594206a6c637db7041a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 14:12:38 +0200 Subject: [PATCH 017/127] wip: began folder to redux migration --- .../ManageDashboards/FolderSettings.tsx | 160 ---------------- .../FolderSettingsPage.test.tsx} | 0 .../manage-dashboards/FolderSettingsPage.tsx | 180 ++++++++++++++++++ .../manage-dashboards/state/actions.ts | 29 +++ .../manage-dashboards/state/reducers.ts | 0 public/app/routes/routes.ts | 4 +- public/app/types/dashboard.ts | 7 + public/app/types/index.ts | 4 + 8 files changed, 222 insertions(+), 162 deletions(-) delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx rename public/app/{containers/ManageDashboards/FolderSettings.test.tsx => features/manage-dashboards/FolderSettingsPage.test.tsx} (100%) create mode 100644 public/app/features/manage-dashboards/FolderSettingsPage.tsx create mode 100644 public/app/features/manage-dashboards/state/actions.ts create mode 100644 public/app/features/manage-dashboards/state/reducers.ts create mode 100644 public/app/types/dashboard.ts diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

Loading

; - } - - return ( -
- -
-

Folder Settings

- -
-
-
- - -
-
- - -
- -
-
-
- ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx similarity index 100% rename from public/app/containers/ManageDashboards/FolderSettings.test.tsx rename to public/app/features/manage-dashboards/FolderSettingsPage.test.tsx diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx new file mode 100644 index 00000000000..4ed6743a8dc --- /dev/null +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -0,0 +1,180 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState } from 'app/types'; +import { getFolderByUid } from './state/actions'; + +export interface Props { + navModel: NavModel; + folderUid: string; + getFolderByUid: typeof getFolderByUid; +} + +export class FolderSettingsPage extends PureComponent { + // formSnapshot: any; + // + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + // + // loadStore() { + // const { nav, folder, view } = this.props; + // + // return folder.load(view.routeParams.get('uid') as string).then(res => { + // this.formSnapshot = getSnapshot(folder); + // view.updatePathAndQuery(`${res.url}/settings`, {}, {}); + // + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }); + // } + + // onTitleChange(evt) { + // this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); + // } + // + // getFormSnapshot() { + // if (!this.formSnapshot) { + // this.formSnapshot = getSnapshot(this.props.folder); + // } + // + // return this.formSnapshot; + // } + // + // save(evt) { + // if (evt) { + // evt.stopPropagation(); + // evt.preventDefault(); + // } + // + // const { nav, folder, view } = this.props; + // + // folder + // .saveFolder({ overwrite: false }) + // .then(newUrl => { + // view.updatePathAndQuery(newUrl, {}, {}); + // + // appEvents.emit('dashboard-saved'); + // appEvents.emit('alert-success', ['Folder saved']); + // }) + // .then(() => { + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }) + // .catch(this.handleSaveFolderError.bind(this)); + // } + // + // delete(evt) { + // if (evt) { + // evt.stopPropagation(); + // evt.preventDefault(); + // } + // + // const { folder, view } = this.props; + // const title = folder.folder.title; + // + // appEvents.emit('confirm-modal', { + // title: 'Delete', + // text: `Do you want to delete this folder and all its dashboards?`, + // icon: 'fa-trash', + // yesText: 'Delete', + // onConfirm: () => { + // return folder.deleteFolder().then(() => { + // appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); + // view.updatePathAndQuery('dashboards', '', ''); + // }); + // }, + // }); + // } + // + // handleSaveFolderError(err) { + // if (err.data && err.data.status === 'version-mismatch') { + // err.isHandled = true; + // + // const { nav, folder, view } = this.props; + // + // appEvents.emit('confirm-modal', { + // title: 'Conflict', + // text: 'Someone else has updated this folder.', + // text2: 'Would you still like to save this folder?', + // yesText: 'Save & Overwrite', + // icon: 'fa-warning', + // onConfirm: () => { + // folder + // .saveFolder({ overwrite: true }) + // .then(newUrl => { + // view.updatePathAndQuery(newUrl, {}, {}); + // + // appEvents.emit('dashboard-saved'); + // appEvents.emit('alert-success', ['Folder saved']); + // }) + // .then(() => { + // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); + // }); + // }, + // }); + // } + // } + + render() { + const { navModel } = this.props; + + // if (!folder.folder || !nav.main) { + // return

Loading

; + // } + + return ( +
+ +
+

Folder Settings

+
+
+ ); + } + + // asd() { + //
+ //
+ //
+ // + // + //
+ //
+ // + // + //
+ // + //
+ // + // } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), + folderUid: uid, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts new file mode 100644 index 00000000000..ab5e1212d5f --- /dev/null +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -0,0 +1,29 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO } from 'app/types'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export type Action = LoadFolderAction; + +type ThunkResult = ThunkAction; + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index a0b070cbcb4..4c50bd65de9 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -4,7 +4,7 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; import TeamPages from 'app/containers/Teams/TeamPages'; import TeamList from 'app/containers/Teams/TeamList'; @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts new file mode 100644 index 00000000000..3ec82842934 --- /dev/null +++ b/public/app/types/dashboard.ts @@ -0,0 +1,7 @@ +export interface FolderDTO { + id: number; + title: string; + url: string; + version: number; + hasAcl: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index debfcf58ac8..e2d9cf8933f 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,3 +1,7 @@ +import { FolderDTO } from './dashboard'; + +export { FolderDTO }; + // // Location // From b1fe0c4c7e015b657fffe917638303950b2661c0 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 15:53:58 +0200 Subject: [PATCH 018/127] team settings --- public/app/features/teams/TeamPages.tsx | 2 +- .../app/features/teams/TeamSettings.test.tsx | 44 ++++++++++++++ public/app/features/teams/TeamSettings.tsx | 59 ++++++++++++++----- .../__snapshots__/TeamPages.test.tsx.snap | 12 +--- .../__snapshots__/TeamSettings.test.tsx.snap | 57 ++++++++++++++++++ public/app/features/teams/state/actions.ts | 14 +++++ 6 files changed, 161 insertions(+), 27 deletions(-) create mode 100644 public/app/features/teams/TeamSettings.test.tsx create mode 100644 public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index 2528c3c87b8..a4ab4a06d4d 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -66,7 +66,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.Settings: - return ; + return ; case PageTypes.GroupSync: return isSyncEnabled && ; diff --git a/public/app/features/teams/TeamSettings.test.tsx b/public/app/features/teams/TeamSettings.test.tsx new file mode 100644 index 00000000000..2e40a0e3c44 --- /dev/null +++ b/public/app/features/teams/TeamSettings.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Props, TeamSettings } from './TeamSettings'; +import { getMockTeam } from './__mocks__/teamMocks'; + +const setup = (propOverrides?: object) => { + const props: Props = { + team: getMockTeam(), + updateTeam: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as TeamSettings; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe('Functions', () => { + it('should update team', () => { + const { instance } = setup(); + const mockEvent = { preventDefault: jest.fn() }; + + instance.setState({ + name: 'test11', + }); + + instance.onUpdate(mockEvent); + + expect(instance.props.updateTeam).toHaveBeenCalledWith('test11', 'test@test.com'); + }); +}); diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 6e3c90d93f9..ef9a5ae0b70 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,41 +1,58 @@ import React from 'react'; -import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; import { Label } from 'app/core/components/Forms/Forms'; import { Team } from '../../types'; +import { updateTeam } from './state/actions'; +import { getRouteParamsId } from '../../core/selectors/location'; +import { getTeam } from './state/selectors'; -interface Props { +export interface Props { team: Team; + updateTeam: typeof updateTeam; } -export class TeamSettings extends React.Component { +interface State { + name: string; + email: string; +} + +export class TeamSettings extends React.Component { constructor(props) { super(props); + + this.state = { + name: props.team.name, + email: props.team.email, + }; } - onChangeName = evt => { - // this.props.team.setName(evt.target.value); + onChangeName = event => { + this.setState({ name: event.target.value }); }; - onChangeEmail = evt => { - // this.props.team.setEmail(evt.target.value); + onChangeEmail = event => { + this.setState({ email: event.target.value }); }; - onUpdate = evt => { - evt.preventDefault(); - // this.props.team.update(); + onUpdate = event => { + const { name, email } = this.state; + event.preventDefault(); + this.props.updateTeam(name, email); }; render() { + const { name, email } = this.state; + return (

Team Settings

-
+
@@ -47,14 +64,14 @@ export class TeamSettings extends React.Component {
-
@@ -64,4 +81,16 @@ export class TeamSettings extends React.Component { } } -export default hot(module)(TeamSettings); +function mapStateToProps(state) { + const teamId = getRouteParamsId(state.location); + + return { + team: getTeam(state.team, teamId), + }; +} + +const mapDispatchToProps = { + updateTeam, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamSettings); diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 563d3d3bb99..73f3fde4093 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -52,17 +52,7 @@ exports[`Render should render settings page 1`] = `
- +
`; diff --git a/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap new file mode 100644 index 00000000000..0f6573ccf90 --- /dev/null +++ b/public/app/features/teams/__snapshots__/TeamSettings.test.tsx.snap @@ -0,0 +1,57 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+

+ Team Settings +

+ +
+ + Name + + +
+
+ + Email + + +
+
+ +
+ +
+`; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 4786edf60a8..5b203d0a502 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -153,6 +153,20 @@ export function removeTeamMember(id: number): ThunkResult { }; } +export function updateTeam(name: string, email: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + await getBackendSrv() + .put(`/api/teams/${team.id}`, { + name, + email, + }) + .then(() => { + dispatch(loadTeam(team.id)); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() From 0cfcf2685e66af76895664c86f562952d63ca812 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 10 Sep 2018 16:58:17 +0200 Subject: [PATCH 019/127] actions for group sync --- .../app/features/teams/TeamGroupSync.test.tsx | 0 public/app/features/teams/TeamGroupSync.tsx | 81 ++++++++++++------- public/app/features/teams/TeamPages.tsx | 3 +- public/app/features/teams/state/actions.ts | 68 +++++++++++++++- public/app/features/teams/state/reducers.ts | 3 + public/app/features/teams/state/selectors.ts | 1 + 6 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 public/app/features/teams/TeamGroupSync.test.tsx diff --git a/public/app/features/teams/TeamGroupSync.test.tsx b/public/app/features/teams/TeamGroupSync.test.tsx new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 6562820d717..39fdd8d413e 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -1,11 +1,16 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import { Team, TeamGroup } from '../../types'; +import { TeamGroup } from '../../types'; +import { addTeamGroup, loadTeamGroups, removeTeamGroup } from './state/actions'; +import { getTeamGroups } from './state/selectors'; -interface Props { - team: Team; +export interface Props { + groups: TeamGroup[]; + loadTeamGroups: typeof loadTeamGroups; + addTeamGroup: typeof addTeamGroup; + removeTeamGroup: typeof removeTeamGroup; } interface State { @@ -15,14 +20,39 @@ interface State { const headerTooltip = `Sync LDAP or OAuth groups with your Grafana teams.`; -export class TeamGroupSync extends React.Component { +export class TeamGroupSync extends PureComponent { constructor(props) { super(props); this.state = { isAdding: false, newGroupId: '' }; } componentDidMount() { - // this.props.team.loadGroups(); + this.fetchTeamGroups(); + } + + async fetchTeamGroups() { + await this.props.loadTeamGroups(); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onNewGroupIdChanged = evt => { + this.setState({ newGroupId: evt.target.value }); + }; + + onAddGroup = () => { + this.props.addTeamGroup(this.state.newGroupId); + this.setState({ isAdding: false, newGroupId: '' }); + }; + + onRemoveGroup = (group: TeamGroup) => { + this.props.removeTeamGroup(group.groupId); + }; + + isNewGroupValid() { + return this.state.newGroupId.length > 1; } renderGroup(group: TeamGroup) { @@ -38,30 +68,9 @@ export class TeamGroupSync extends React.Component { ); } - onToggleAdding = () => { - this.setState({ isAdding: !this.state.isAdding }); - }; - - onNewGroupIdChanged = evt => { - this.setState({ newGroupId: evt.target.value }); - }; - - onAddGroup = () => { - // this.props.team.addGroup(this.state.newGroupId); - this.setState({ isAdding: false, newGroupId: '' }); - }; - - onRemoveGroup = (group: TeamGroup) => { - // this.props.team.removeGroup(group.groupId); - }; - - isNewGroupValid() { - return this.state.newGroupId.length > 1; - } - render() { const { isAdding, newGroupId } = this.state; - const groups = this.props.team.groups; + const groups = this.props.groups; return (
@@ -144,4 +153,16 @@ export class TeamGroupSync extends React.Component { } } -export default hot(module)(TeamGroupSync); +function mapStateToProps(state) { + return { + groups: getTeamGroups(state.team), + }; +} + +const mapDispatchToProps = { + loadTeamGroups, + addTeamGroup, + removeTeamGroup, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(TeamGroupSync); diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index a4ab4a06d4d..f28bde518d2 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -57,7 +57,6 @@ export class TeamPages extends PureComponent { } renderPage() { - const { team } = this.props; const { isSyncEnabled } = this.state; const currentPage = this.getCurrentPage(); @@ -69,7 +68,7 @@ export class TeamPages extends PureComponent { return ; case PageTypes.GroupSync: - return isSyncEnabled && ; + return isSyncEnabled && ; } return null; diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 5b203d0a502..9b3ab3a8177 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,9 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamMember } from '../../../types'; +import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from '../../../types'; import { updateNavIndex } from '../../../core/actions'; import { UpdateNavIndexAction } from '../../../core/actions/navModel'; +import config from 'app/core/config'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -10,6 +11,7 @@ export enum ActionTypes { SetSearchQuery = 'SET_SEARCH_QUERY', SetSearchMemberQuery = 'SET_SEARCH_MEMBER_QUERY', LoadTeamMembers = 'TEAM_MEMBERS_LOADED', + LoadTeamGroups = 'TEAM_GROUPS_LOADED', } export interface LoadTeamsAction { @@ -27,6 +29,11 @@ export interface LoadTeamMembersAction { payload: TeamMember[]; } +export interface LoadTeamGroupsAction { + type: ActionTypes.LoadTeamGroups; + payload: TeamGroup[]; +} + export interface SetSearchQueryAction { type: ActionTypes.SetSearchQuery; payload: string; @@ -42,7 +49,8 @@ export type Action = | SetSearchQueryAction | LoadTeamAction | LoadTeamMembersAction - | SetSearchMemberQueryAction; + | SetSearchMemberQueryAction + | LoadTeamGroupsAction; type ThunkResult = ThunkAction; @@ -61,6 +69,11 @@ const teamMembersLoaded = (teamMembers: TeamMember[]): LoadTeamMembersAction => payload: teamMembers, }); +const teamGroupsLoaded = (teamGroups: TeamGroup[]): LoadTeamGroupsAction => ({ + type: ActionTypes.LoadTeamGroups, + payload: teamGroups, +}); + export const setSearchMemberQuery = (searchQuery: string): SetSearchMemberQueryAction => ({ type: ActionTypes.SetSearchMemberQuery, payload: searchQuery, @@ -79,7 +92,7 @@ export function loadTeams(): ThunkResult { } function buildNavModel(team: Team): NavModelItem { - return { + const navModel = { img: team.avatarUrl, id: 'team-' + team.id, subTitle: 'Manage members & settings', @@ -103,6 +116,18 @@ function buildNavModel(team: Team): NavModelItem { }, ], }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: 'team-settings', + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; } export function loadTeam(id: number): ThunkResult { @@ -117,7 +142,6 @@ export function loadTeam(id: number): ThunkResult { } export function loadTeamMembers(): ThunkResult { - console.log('loading team members'); return async (dispatch, getStore) => { const team = getStore().team.team; @@ -167,6 +191,42 @@ export function updateTeam(name: string, email: string): ThunkResult { }; } +export function loadTeamGroups(): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .get(`/api/teams/${team.id}/groups`) + .then(response => { + dispatch(teamGroupsLoaded(response)); + }); + }; +} + +export function addTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + +export function removeTeamGroup(groupId: string): ThunkResult { + return async (dispatch, getStore) => { + const team = getStore().team.team; + + await getBackendSrv() + .delete(`/api/teams/${team.id}/groups/${groupId}`) + .then(() => { + dispatch(loadTeamGroups()); + }); + }; +} + export function deleteTeam(id: number): ThunkResult { return async dispatch => { await getBackendSrv() diff --git a/public/app/features/teams/state/reducers.ts b/public/app/features/teams/state/reducers.ts index f02ade60923..4af36f2e01c 100644 --- a/public/app/features/teams/state/reducers.ts +++ b/public/app/features/teams/state/reducers.ts @@ -30,6 +30,9 @@ export const teamReducer = (state = initialTeamState, action: Action): TeamState case ActionTypes.SetSearchMemberQuery: return { ...state, searchMemberQuery: action.payload }; + + case ActionTypes.LoadTeamGroups: + return { ...state, groups: action.payload }; } return state; diff --git a/public/app/features/teams/state/selectors.ts b/public/app/features/teams/state/selectors.ts index 5e22f96eaf7..416e293ec78 100644 --- a/public/app/features/teams/state/selectors.ts +++ b/public/app/features/teams/state/selectors.ts @@ -1,5 +1,6 @@ export const getSearchQuery = state => state.searchQuery; export const getSearchMemberQuery = state => state.searchMemberQuery; +export const getTeamGroups = state => state.groups; export const getTeam = (state, currentTeamId) => { if (state.team.id === parseInt(currentTeamId)) { From 679ffbfd8320490c20bb02acc1648557764734df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 10 Sep 2018 21:49:04 +0200 Subject: [PATCH 020/127] wip: progress on redux folder store --- public/app/core/actions/index.ts | 4 +- .../manage-dashboards/state/actions.ts | 39 ++++++++++++++++++- .../manage-dashboards/state/reducers.ts | 20 ++++++++++ public/app/stores/configureStore.ts | 2 + public/app/types/dashboard.ts | 12 +++++- public/app/types/index.ts | 5 ++- 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index b4b9b21126e..451a13dae99 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,4 @@ import { updateLocation } from './location'; -import { updateNavIndex } from './navModel'; +import { updateNavIndex, UpdateNavIndexAction } from './navModel'; -export { updateLocation, updateNavIndex }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction }; diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index ab5e1212d5f..b3243c7bf2b 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,7 +1,8 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO } from 'app/types'; +import { FolderDTO, NavModelItem } from 'app/types'; +import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -19,11 +20,45 @@ export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ export type Action = LoadFolderAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; +function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { const folder = await getBackendSrv().getFolderByUid(uid); dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); }; } diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index e69de29bb2d..1eb873f5bd0 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -0,0 +1,20 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = null; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + canSave: false, + hasChanged: false, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..5aa5ccc5f41 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; +import manageDashboardsReducers from 'app/features/manage-dashboards/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...manageDashboardsReducers, }); export let store; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 3ec82842934..576432d413e 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,7 +1,17 @@ export interface FolderDTO { id: number; + uid: string; title: string; url: string; version: number; - hasAcl: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 221a64b48d4..bc54cea35cb 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -1,6 +1,6 @@ -import { FolderDTO } from './dashboard'; +import { FolderDTO, FolderState } from './dashboard'; -export { FolderDTO }; +export { FolderDTO, FolderState }; // // Location @@ -136,4 +136,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } From 61112d93d8caa8a00b6ef2d1745d18706baf22f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 10:36:55 +0200 Subject: [PATCH 021/127] wip: folder to redux --- .../manage-dashboards/FolderSettingsPage.tsx | 60 +++++++++---------- .../manage-dashboards/state/reducers.ts | 8 ++- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx index 4ed6743a8dc..90528a8798d 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -120,48 +120,41 @@ export class FolderSettingsPage extends PureComponent { render() { const { navModel } = this.props; - // if (!folder.folder || !nav.main) { - // return

Loading

; - // } - return (

Folder Settings

+ +
+
+
+ + +
+
+ + +
+ +
); } - - // asd() { - //
- //
- //
- // - // - //
- //
- // - // - //
- // - //
- // - // } } const mapStateToProps = (state: StoreState) => { @@ -170,6 +163,7 @@ const mapStateToProps = (state: StoreState) => { return { navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), folderUid: uid, + folder: state.folder, }; }; diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 1eb873f5bd0..ee837acc9db 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -1,7 +1,13 @@ import { FolderState } from 'app/types'; import { Action, ActionTypes } from './actions'; -export const inititalState: FolderState = null; +export const inititalState: FolderState = { + uid: 'loading', + id: -1, + title: 'loading', + canSave: false, + hasChanged: false, +}; export const folderReducer = (state = inititalState, action: Action): FolderState => { switch (action.type) { From 6ba5550f5f3115aa0cf23e958dd460677b81dc90 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 14:09:08 +0200 Subject: [PATCH 022/127] renames jest files to match new convention --- ...{datasource.jest.ts => datasource.test.ts} | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) rename public/app/plugins/datasource/grafana/specs/{datasource.jest.ts => datasource.test.ts} (72%) diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.test.ts similarity index 72% rename from public/app/plugins/datasource/grafana/specs/datasource.jest.ts rename to public/app/plugins/datasource/grafana/specs/datasource.test.ts index 544b04056ac..b3afe7207f2 100644 --- a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts +++ b/public/app/plugins/datasource/grafana/specs/datasource.test.ts @@ -13,7 +13,11 @@ describe('grafana data source', () => { }; const templateSrvStub = { - replace: val => val.replace('$var', 'replaced') + replace: val => { + return val + .replace('$var2', 'replaced|replaced2') + .replace('$var', 'replaced'); + } }; const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); @@ -32,6 +36,21 @@ describe('grafana data source', () => { }); }); + describe('with tags that have multi value template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['$var2']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('replaced'); + expect(calledBackendSrvParams.tags[1]).toBe('replaced2'); + }); + }); + describe('with type dashboard', () => { const options = setupAnnotationQueryOptions( { From 19c7dd9834f88b2e8aa6623ad3d758c41fdeee69 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 14:25:25 +0200 Subject: [PATCH 023/127] support template variables with multiple values --- public/app/plugins/datasource/grafana/datasource.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 3d788378045..b3de9a9c85a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -57,8 +57,11 @@ class GrafanaDatasource { return this.$q.when([]); } const tags = []; - for (let t of params.tags) { - tags.push(this.templateSrv.replace(t)); + for (const t of params.tags) { + const renderedValues = this.templateSrv.replace(t, {}, 'pipe'); + for (const tt of renderedValues.split('|')) { + tags.push(tt); + } } params.tags = tags; } From 953bdc4dc063c85ac00e0e8536f1565e1c236144 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 11 Sep 2018 14:53:38 +0200 Subject: [PATCH 024/127] put folder name under dashboard name, tweaked aliginments in search results --- public/app/core/components/search/search_results.html | 3 ++- public/sass/components/_search.scss | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 9f266ed3a6b..45258ded652 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -33,7 +33,8 @@ -
{{::item.title}} {{::item.folderTitle}}
+
{{::item.title}}
+ {{::item.folderTitle}}
diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 1589cc1e52c..b1211bcbdee 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -210,18 +210,20 @@ .search-item__body-title { color: $list-item-link-color; + line-height: 14px; } .search-item__body-folder-title { color: $text-color-weak; - padding-left: 0.25rem; font-size: $font-size-xs; + line-height: 11px; } .search-item__icon { padding: 5px; flex: 0 0 auto; font-size: 19px; + line-height: 22px; padding: 5px 2px 5px 10px; } From 1638c6bea11f196b69611b28890eddebc91d933e Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 11 Sep 2018 15:50:04 +0200 Subject: [PATCH 025/127] enable partial tag matches for annotations --- pkg/api/annotations.go | 21 +++++----- pkg/services/annotations/annotations.go | 1 + pkg/services/sqlstore/annotation.go | 7 +++- pkg/services/sqlstore/annotation_test.go | 41 ++++++++++++++++++- .../plugins/datasource/grafana/datasource.ts | 1 + .../grafana/partials/annotations.editor.html | 27 ++++++++---- 6 files changed, 76 insertions(+), 22 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 55c9c954940..eec07bb9f81 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -14,16 +14,17 @@ import ( func GetAnnotations(c *m.ReqContext) Response { query := &annotations.ItemQuery{ - From: c.QueryInt64("from"), - To: c.QueryInt64("to"), - OrgId: c.OrgId, - UserId: c.QueryInt64("userId"), - AlertId: c.QueryInt64("alertId"), - DashboardId: c.QueryInt64("dashboardId"), - PanelId: c.QueryInt64("panelId"), - Limit: c.QueryInt64("limit"), - Tags: c.QueryStrings("tags"), - Type: c.Query("type"), + From: c.QueryInt64("from"), + To: c.QueryInt64("to"), + OrgId: c.OrgId, + UserId: c.QueryInt64("userId"), + AlertId: c.QueryInt64("alertId"), + DashboardId: c.QueryInt64("dashboardId"), + PanelId: c.QueryInt64("panelId"), + Limit: c.QueryInt64("limit"), + Tags: c.QueryStrings("tags"), + Type: c.Query("type"), + PartialMatch: c.QueryBool("partialMatch"), } repo := annotations.GetRepository() diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index 9b490169d3b..daea43863f4 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -21,6 +21,7 @@ type ItemQuery struct { RegionId int64 `json:"regionId"` Tags []string `json:"tags"` Type string `json:"type"` + PartialMatch bool `json:"partialMatch"` Limit int64 `json:"limit"` } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index a65bc136554..6e25ce432f3 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -211,7 +211,12 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ) `, strings.Join(keyValueFilters, " OR ")) - sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) + if query.PartialMatch { + sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery)) + } else { + sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) + } + } } diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index c0d267f2578..4c31e0442c8 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -78,7 +78,31 @@ func TestAnnotations(t *testing.T) { So(err, ShouldBeNil) So(annotation2.Id, ShouldBeGreaterThan, 0) - Convey("Can query for annotation", func() { + globalAnnotation1 := &annotations.Item{ + OrgId: 1, + UserId: 1, + Text: "deploy", + Type: "", + Epoch: 15, + Tags: []string{"deploy"}, + } + err = repo.Save(globalAnnotation1) + So(err, ShouldBeNil) + So(globalAnnotation1.Id, ShouldBeGreaterThan, 0) + + globalAnnotation2 := &annotations.Item{ + OrgId: 1, + UserId: 1, + Text: "rollback", + Type: "", + Epoch: 17, + Tags: []string{"rollback"}, + } + err = repo.Save(globalAnnotation2) + So(err, ShouldBeNil) + So(globalAnnotation2.Id, ShouldBeGreaterThan, 0) + + Convey("Can query for annotation by dashboard id", func() { items, err := repo.Find(&annotations.ItemQuery{ OrgId: 1, DashboardId: 1, @@ -165,7 +189,7 @@ func TestAnnotations(t *testing.T) { OrgId: 1, DashboardId: 1, From: 1, - To: 15, + To: 15, //this will exclude the second test annotation Tags: []string{"outage", "error"}, }) @@ -173,6 +197,19 @@ func TestAnnotations(t *testing.T) { So(items, ShouldHaveLength, 1) }) + Convey("Should find two annotations using partial match", func() { + items, err := repo.Find(&annotations.ItemQuery{ + OrgId: 1, + From: 1, + To: 25, + PartialMatch: true, + Tags: []string{"rollback", "deploy"}, + }) + + So(err, ShouldBeNil) + So(items, ShouldHaveLength, 2) + }) + Convey("Should find one when all key value tag filters does match", func() { items, err := repo.Find(&annotations.ItemQuery{ OrgId: 1, diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index b3de9a9c85a..4ddfa8df40d 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -40,6 +40,7 @@ class GrafanaDatasource { to: options.range.to.valueOf(), limit: options.annotation.limit, tags: options.annotation.tags, + partialMatch: options.annotation.partialMatch, }; if (options.annotation.type === 'dashboard') { diff --git a/public/app/plugins/datasource/grafana/partials/annotations.editor.html b/public/app/plugins/datasource/grafana/partials/annotations.editor.html index 4289a58e5cb..ba68a08cefd 100644 --- a/public/app/plugins/datasource/grafana/partials/annotations.editor.html +++ b/public/app/plugins/datasource/grafana/partials/annotations.editor.html @@ -2,7 +2,7 @@
- + Filter by
    @@ -11,18 +11,11 @@
-
+
- -
- Tags - - -
-
Max limit
@@ -31,6 +24,22 @@
+
+
+ +
+
+ Tags + + +
+
From 19cbff658bb53bc33ccfcaa84cc5d01fd7d76705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 17:36:23 +0200 Subject: [PATCH 026/127] wip: folder settings page to redux progress --- public/app/core/reducers/location.ts | 4 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ----------- .../manage-dashboards/FolderSettingsPage.tsx | 152 +++++------------- .../manage-dashboards/state/actions.ts | 24 ++- .../manage-dashboards/state/reducers.ts | 7 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/types/dashboard.ts | 1 + public/app/types/index.ts | 1 + 10 files changed, 72 insertions(+), 283 deletions(-) delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.tsx index 90528a8798d..a23e495fd3c 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.tsx @@ -4,121 +4,53 @@ import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState } from 'app/types'; -import { getFolderByUid } from './state/actions'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; export interface Props { navModel: NavModel; folderUid: string; + folder: FolderState; getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; } export class FolderSettingsPage extends PureComponent { - // formSnapshot: any; - // componentDidMount() { this.props.getFolderByUid(this.props.folderUid); } - // - // loadStore() { - // const { nav, folder, view } = this.props; - // - // return folder.load(view.routeParams.get('uid') as string).then(res => { - // this.formSnapshot = getSnapshot(folder); - // view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - // - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }); - // } - // onTitleChange(evt) { - // this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - // } - // - // getFormSnapshot() { - // if (!this.formSnapshot) { - // this.formSnapshot = getSnapshot(this.props.folder); - // } - // - // return this.formSnapshot; - // } - // - // save(evt) { - // if (evt) { - // evt.stopPropagation(); - // evt.preventDefault(); - // } - // - // const { nav, folder, view } = this.props; - // - // folder - // .saveFolder({ overwrite: false }) - // .then(newUrl => { - // view.updatePathAndQuery(newUrl, {}, {}); - // - // appEvents.emit('dashboard-saved'); - // appEvents.emit('alert-success', ['Folder saved']); - // }) - // .then(() => { - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }) - // .catch(this.handleSaveFolderError.bind(this)); - // } - // - // delete(evt) { - // if (evt) { - // evt.stopPropagation(); - // evt.preventDefault(); - // } - // - // const { folder, view } = this.props; - // const title = folder.folder.title; - // - // appEvents.emit('confirm-modal', { - // title: 'Delete', - // text: `Do you want to delete this folder and all its dashboards?`, - // icon: 'fa-trash', - // yesText: 'Delete', - // onConfirm: () => { - // return folder.deleteFolder().then(() => { - // appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - // view.updatePathAndQuery('dashboards', '', ''); - // }); - // }, - // }); - // } - // - // handleSaveFolderError(err) { - // if (err.data && err.data.status === 'version-mismatch') { - // err.isHandled = true; - // - // const { nav, folder, view } = this.props; - // - // appEvents.emit('confirm-modal', { - // title: 'Conflict', - // text: 'Someone else has updated this folder.', - // text2: 'Would you still like to save this folder?', - // yesText: 'Save & Overwrite', - // icon: 'fa-warning', - // onConfirm: () => { - // folder - // .saveFolder({ overwrite: true }) - // .then(newUrl => { - // view.updatePathAndQuery(newUrl, {}, {}); - // - // appEvents.emit('dashboard-saved'); - // appEvents.emit('alert-success', ['Folder saved']); - // }) - // .then(() => { - // return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - // }); - // }, - // }); - // } - // } + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + appEvents.emit('alert-success', ['Folder saved']); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; render() { - const { navModel } = this.props; + const { navModel, folder } = this.props; return (
@@ -127,25 +59,21 @@ export class FolderSettingsPage extends PureComponent {

Folder Settings

-
+
- -
@@ -159,7 +87,6 @@ export class FolderSettingsPage extends PureComponent { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; - return { navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), folderUid: uid, @@ -169,6 +96,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index acd5571aa95..84fe97f22ab 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,8 +1,8 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, NavModelItem } from 'app/types'; -import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; +import { FolderDTO, FolderState, NavModelItem } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -32,7 +32,7 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ export type Action = LoadFolderAction | SetFolderTitleAction; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; function buildNavModel(folder: FolderDTO): NavModelItem { return { @@ -67,6 +67,7 @@ function buildNavModel(folder: FolderDTO): NavModelItem { ], }; } + export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { const folder = await getBackendSrv().getFolderByUid(uid); @@ -74,3 +75,20 @@ export function getFolderByUid(uid: string): ThunkResult { dispatch(updateNavIndex(buildNavModel(folder))); }; } + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index 4844b465dfb..ada5b1812ad 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -16,9 +16,14 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat case ActionTypes.LoadFolder: return { ...action.payload, - canSave: false, hasChanged: false, }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: true, + }; } return state; }; diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 576432d413e..6fbe79cce8c 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -4,6 +4,7 @@ export interface FolderDTO { title: string; url: string; version: number; + canSave: boolean; } export interface FolderState { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 0ddb8f7cd0f..b1096c4827c 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -30,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } From ec41d7608089ab65f78954a5218d7eef4bc578ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 11 Sep 2018 19:00:37 +0200 Subject: [PATCH 027/127] mobx -> redux: major progress on folder migration --- public/app/containers/ContainerProps.ts | 2 - .../manage-dashboards}/FolderPermissions.tsx | 49 ++++--- .../FolderSettingsPage.test.tsx | 118 ++++++---------- .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++++++ .../manage-dashboards/state/reducers.ts | 4 +- public/app/routes/routes.ts | 2 +- public/app/stores/RootStore/RootStore.ts | 2 - public/app/types/{dashboard.ts => folder.ts} | 0 public/app/types/index.ts | 2 +- 9 files changed, 211 insertions(+), 99 deletions(-) rename public/app/{containers/ManageDashboards => features/manage-dashboards}/FolderPermissions.tsx (65%) create mode 100644 public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap rename public/app/types/{dashboard.ts => folder.ts} (100%) diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts index ce09b992f80..84c395413b6 100644 --- a/public/app/containers/ContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -1,13 +1,11 @@ import { NavStore } from './../stores/NavStore/NavStore'; import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; interface ContainerProps { nav: typeof NavStore.Type; permissions: typeof PermissionsStore.Type; view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; backendSrv: any; } diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/manage-dashboards/FolderPermissions.tsx similarity index 65% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/manage-dashboards/FolderPermissions.tsx index 072908d2b8e..00b229801f3 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/manage-dashboards/FolderPermissions.tsx @@ -2,24 +2,34 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +37,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { + if (folder.id === 0) { return

Loading

; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
- +

Folder Permissions

@@ -77,4 +79,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx index bed3d569bcc..defec5e6a57 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx +++ b/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx @@ -1,84 +1,54 @@ import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel, FolderState } from '../../types'; import { shallow } from 'enzyme'; -describe('FolderSettings', () => { - let wrapper; - let page; +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', + uid: '1234', + title: 'loading', canSave: true, + hasChanged: true, version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); + }); + expect(wrapper).toMatchSnapshot(); }); }); diff --git a/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
+ +
+

+ Folder Settings +

+
+ +
+ + +
+
+ + +
+ +
+
+
+`; + +exports[`Render should render component 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/manage-dashboards/state/reducers.ts index ada5b1812ad..41ae10d19e5 100644 --- a/public/app/features/manage-dashboards/state/reducers.ts +++ b/public/app/features/manage-dashboards/state/reducers.ts @@ -2,8 +2,8 @@ import { FolderState } from 'app/types'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { + id: 0, uid: 'loading', - id: -1, title: 'loading', url: '', canSave: false, @@ -22,7 +22,7 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat return { ...state, title: action.payload, - hasChanged: true, + hasChanged: action.payload.trim().length > 0, }; } return state; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 93d83a3b7db..45e72e68c38 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; -import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; +import FolderPermissions from 'app/features/manage-dashboards/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/types/dashboard.ts b/public/app/types/folder.ts similarity index 100% rename from public/app/types/dashboard.ts rename to public/app/types/folder.ts diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b1096c4827c..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,7 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; -import { FolderDTO, FolderState } from './dashboard'; +import { FolderDTO, FolderState } from './folder'; export { Team, From a83beac565e55f14089b26ed025ed5a7b66e2cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:15:18 +0200 Subject: [PATCH 028/127] redux: moved folders to it's own features folder --- public/app/containers/ContainerProps.ts | 12 --- .../FolderPermissions.tsx | 6 +- .../FolderSettingsPage.test.tsx | 3 +- .../FolderSettingsPage.tsx | 1 - .../FolderSettingsPage.test.tsx.snap | 0 .../state/actions.ts | 51 +++---------- public/app/features/folders/state/navModel.ts | 35 +++++++++ .../state/reducers.ts | 0 public/app/features/teams/state/actions.ts | 74 +++++-------------- public/app/routes/routes.ts | 4 +- public/app/stores/configureStore.ts | 4 +- yarn.lock | 30 +------- 12 files changed, 80 insertions(+), 140 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts rename public/app/features/{manage-dashboards => folders}/FolderPermissions.tsx (93%) rename public/app/features/{manage-dashboards => folders}/FolderSettingsPage.test.tsx (95%) rename public/app/features/{manage-dashboards => folders}/FolderSettingsPage.tsx (98%) rename public/app/features/{manage-dashboards => folders}/__snapshots__/FolderSettingsPage.test.tsx.snap (100%) rename public/app/features/{manage-dashboards => folders}/state/actions.ts (64%) create mode 100644 public/app/features/folders/state/navModel.ts rename public/app/features/{manage-dashboards => folders}/state/reducers.ts (100%) diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index 84c395413b6..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/features/manage-dashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 93% rename from public/app/features/manage-dashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 00b229801f3..1dc34aaba1e 100644 --- a/public/app/features/manage-dashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,7 +1,6 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; @@ -11,13 +10,16 @@ import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; -import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; export interface Props { navModel: NavModel; getFolderByUid: typeof getFolderByUid; folderUid: string; folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; } @inject('permissions') diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx similarity index 95% rename from public/app/features/manage-dashboards/FolderSettingsPage.test.tsx rename to public/app/features/folders/FolderSettingsPage.test.tsx index defec5e6a57..3680fa9a197 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.test.tsx +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { FolderSettingsPage, Props } from './FolderSettingsPage'; -import { NavModel, FolderState } from '../../types'; +import { NavModel } from 'app/types'; import { shallow } from 'enzyme'; const setup = (propOverrides?: object) => { @@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => { uid: '1234', title: 'loading', canSave: true, + url: 'url', hasChanged: false, version: 1, }, diff --git a/public/app/features/manage-dashboards/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx similarity index 98% rename from public/app/features/manage-dashboards/FolderSettingsPage.tsx rename to public/app/features/folders/FolderSettingsPage.tsx index a23e495fd3c..2aff0e3e1c4 100644 --- a/public/app/features/manage-dashboards/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -31,7 +31,6 @@ export class FolderSettingsPage extends PureComponent { evt.stopPropagation(); await this.props.saveFolder(this.props.folder); - appEvents.emit('alert-success', ['Folder saved']); }; onDelete = evt => { diff --git a/public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap similarity index 100% rename from public/app/features/manage-dashboards/__snapshots__/FolderSettingsPage.test.tsx.snap rename to public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/folders/state/actions.ts similarity index 64% rename from public/app/features/manage-dashboards/state/actions.ts rename to public/app/features/folders/state/actions.ts index 84fe97f22ab..5d153b2fb8a 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,8 +1,10 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, FolderState, NavModelItem } from 'app/types'; +import { FolderDTO, FolderState } from 'app/types'; import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', @@ -15,16 +17,16 @@ export interface LoadFolderAction { payload: FolderDTO; } -export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ - type: ActionTypes.LoadFolder, - payload: folder, -}); - export interface SetFolderTitleAction { type: ActionTypes.SetFolderTitle; payload: string; } +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ type: ActionTypes.SetFolderTitle, payload: newTitle, @@ -34,39 +36,6 @@ export type Action = LoadFolderAction | SetFolderTitleAction; type ThunkResult = ThunkAction; -function buildNavModel(folder: FolderDTO): NavModelItem { - return { - icon: 'fa fa-folder-open', - id: 'manage-folder', - subTitle: 'Manage folder dashboards & permissions', - url: '', - text: folder.title, - breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], - children: [ - { - active: false, - icon: 'fa fa-fw fa-th-large', - id: `folder-dashboards-${folder.uid}`, - text: 'Dashboards', - url: folder.url, - }, - { - active: false, - icon: 'fa fa-fw fa-lock', - id: `folder-permissions-${folder.uid}`, - text: 'Permissions', - url: `${folder.url}/permissions`, - }, - { - active: false, - icon: 'fa fa-fw fa-cog', - id: `folder-settings-${folder.uid}`, - text: 'Settings', - url: `${folder.url}/settings`, - }, - ], - }; -} export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -82,6 +51,10 @@ export function saveFolder(folder: FolderState): ThunkResult { title: folder.title, version: folder.version, }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + dispatch(updateLocation({ path: `${res.url}/settings` })); }; } diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..614bb30f2d8 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,35 @@ +import { FolderDTO, NavModelItem } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} diff --git a/public/app/features/manage-dashboards/state/reducers.ts b/public/app/features/folders/state/reducers.ts similarity index 100% rename from public/app/features/manage-dashboards/state/reducers.ts rename to public/app/features/folders/state/reducers.ts diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..63bea743607 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -131,107 +131,71 @@ function buildNavModel(team: Team): NavModelItem { export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); }; } export function loadTeamMembers(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/members`) - .then(response => { - dispatch(teamMembersLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + dispatch(teamMembersLoaded(response)); }; } export function addTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/members`, { userId: id }) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + dispatch(loadTeamMembers()); }; } export function removeTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/members/${id}`) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + dispatch(loadTeamMembers()); }; } export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv() - .put(`/api/teams/${team.id}`, { - name, - email, - }) - .then(() => { - dispatch(loadTeam(team.id)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + dispatch(loadTeam(team.id)); }; } export function loadTeamGroups(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/groups`) - .then(response => { - dispatch(teamGroupsLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + dispatch(teamGroupsLoaded(response)); }; } export function addTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups()); }; } export function removeTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/groups/${groupId}`) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 45e72e68c38..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -5,8 +5,8 @@ import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettingsPage from 'app/features/manage-dashboards/FolderSettingsPage'; -import FolderPermissions from 'app/features/manage-dashboards/FolderPermissions'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 5aa5ccc5f41..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,13 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; -import manageDashboardsReducers from 'app/features/manage-dashboards/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, - ...manageDashboardsReducers, + ...foldersReducers, }); export let store; diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: From 0705bf570d403ed4233d4152b3e14a97b71cffc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:47:23 +0200 Subject: [PATCH 029/127] fix: added loading nav states --- public/app/core/selectors/navModel.ts | 10 ++- .../features/folders/FolderPermissions.tsx | 5 +- .../features/folders/FolderSettingsPage.tsx | 4 +- public/app/features/folders/state/navModel.ts | 20 +++++- public/app/features/teams/TeamPages.tsx | 8 ++- public/app/features/teams/state/actions.ts | 43 +----------- public/app/features/teams/state/navModel.ts | 67 +++++++++++++++++++ 7 files changed, 106 insertions(+), 51 deletions(-) create mode 100644 public/app/features/teams/state/navModel.ts diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 1dc34aaba1e..512927c24e6 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -12,6 +12,7 @@ import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { getFolderByUid } from './state/actions'; import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; export interface Props { navModel: NavModel; @@ -48,7 +49,7 @@ export class FolderPermissions extends Component { const { navModel, permissions, backendSrv, folder } = this.props; if (folder.id === 0) { - return

Loading

; + return ; } const dashboardId = folder.id; @@ -84,7 +85,7 @@ export class FolderPermissions extends Component { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; return { - navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`), + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), folderUid: uid, folder: state.folder, }; diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx index 2aff0e3e1c4..1eb7ccafc65 100644 --- a/public/app/features/folders/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -6,6 +6,7 @@ import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; import { NavModel, StoreState, FolderState } from 'app/types'; import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; export interface Props { navModel: NavModel; @@ -86,8 +87,9 @@ export class FolderSettingsPage extends PureComponent { const mapStateToProps = (state: StoreState) => { const uid = state.location.routeParams.uid; + return { - navModel: getNavModel(state.navIndex, `folder-settings-${uid}`), + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), folderUid: uid, folder: state.folder, }; diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts index 614bb30f2d8..e6ef763d019 100644 --- a/public/app/features/folders/state/navModel.ts +++ b/public/app/features/folders/state/navModel.ts @@ -1,4 +1,4 @@ -import { FolderDTO, NavModelItem } from 'app/types'; +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; export function buildNavModel(folder: FolderDTO): NavModelItem { return { @@ -33,3 +33,21 @@ export function buildNavModel(folder: FolderDTO): NavModelItem { ], }; } + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 63bea743607..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,45 +90,6 @@ export function loadTeams(): ThunkResult { }; } -function buildNavModel(team: Team): NavModelItem { - const navModel = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: false, - icon: 'gicon gicon-team', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `team-settings-${team.id}`, - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (config.buildInfo.isEnterprise) { - navModel.children.push({ - active: false, - icon: 'fa fa-fw fa-refresh', - id: `team-groupsync-${team.id}`, - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - return navModel; -} - export function loadTeam(id: number): ThunkResult { return async dispatch => { const response = await getBackendSrv().get(`/api/teams/${id}`); diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} From 78d36f784f3e17a0da13ba0ab007e287eb6f3034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 09:55:22 +0200 Subject: [PATCH 030/127] fix: gofmt issues --- pkg/models/datasource.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b7e3e3eaa17..cbdd0136f4d 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -59,22 +59,22 @@ type DataSource struct { } var knownDatasourcePlugins = map[string]bool{ - DS_ES: true, - DS_GRAPHITE: true, - DS_INFLUXDB: true, - DS_INFLUXDB_08: true, - DS_KAIROSDB: true, - DS_CLOUDWATCH: true, - DS_PROMETHEUS: true, - DS_OPENTSDB: true, - DS_POSTGRES: true, - DS_MYSQL: true, - DS_MSSQL: true, - "opennms": true, - "abhisant-druid-datasource": true, - "dalmatinerdb-datasource": true, - "gnocci": true, - "zabbix": true, + DS_ES: true, + DS_GRAPHITE: true, + DS_INFLUXDB: true, + DS_INFLUXDB_08: true, + DS_KAIROSDB: true, + DS_CLOUDWATCH: true, + DS_PROMETHEUS: true, + DS_OPENTSDB: true, + DS_POSTGRES: true, + DS_MYSQL: true, + DS_MSSQL: true, + "opennms": true, + "abhisant-druid-datasource": true, + "dalmatinerdb-datasource": true, + "gnocci": true, + "zabbix": true, "alexanderzobnin-zabbix-datasource": true, "newrelic-app": true, "grafana-datadog-datasource": true, From a317158b72c7841dcef935452ecc7a316eb7c8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 12 Sep 2018 12:18:24 +0200 Subject: [PATCH 031/127] wip: working on reducer test --- .../app/features/folders/state/reducers.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 public/app/features/folders/state/reducers.test.ts diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..1a7f4310f76 --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,17 @@ +import { Action, ActionTypes } from './actions'; +import { inititalState, folderReducer } from './reducers'; + +describe('folder reducer', () => { + it('should set teams', () => { + const payload = [getMockTeam()]; + + const action: Action = { + type: ActionTypes.LoadTeams, + payload, + }; + + const result = teamsReducer(initialTeamsState, action); + + expect(result.teams).toEqual(payload); + }); +}); From c56ca57df55a5ff9f6519115735de04016d5800b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 12 Sep 2018 17:54:47 +0200 Subject: [PATCH 032/127] docs: include active directory ldap example and restructure --- docs/sources/auth/ldap.md | 200 ++++++++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 52 deletions(-) diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index f63a44e1750..8e95e24a0b0 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -1,7 +1,7 @@ +++ title = "LDAP Authentication" description = "Grafana LDAP Authentication Guide " -keywords = ["grafana", "configuration", "documentation", "ldap"] +keywords = ["grafana", "configuration", "documentation", "ldap", "active directory"] type = "docs" [menu.docs] name = "LDAP" @@ -10,35 +10,42 @@ parent = "authentication" weight = 2 +++ -# LDAP +# LDAP Authentication The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP -group memberships and Grafana Organization user roles. Below we detail grafana.ini config file -settings and ldap.toml config file options. +group memberships and Grafana Organization user roles. + +## Supported LDAP Servers + +Grafana uses a [third-party LDAP library](https://github.com/go-ldap/ldap) under the hood that supports basic LDAP v3 functionality. +This means that you should be able to configure LDAP integration using any compliant LDAPv3 server, for example [OpenLDAP](#openldap) or +[Active Directory](#active-directory) among [others](https://en.wikipedia.org/wiki/Directory_service#LDAP_implementations). ## Enable LDAP -You turn on LDAP in the [main config file]({{< relref "installation/configuration.md" >}}) as well as specify the path to the LDAP +In order to use LDAP integration you'll first need to enable LDAP in the [main config file]({{< relref "installation/configuration.md" >}}) as well as specify the path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`). ```bash [auth.ldap] # Set to `true` to enable LDAP integration (default: `false`) enabled = true + # Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) -config_file = /etc/grafana/ldap.toml` +config_file = /etc/grafana/ldap.toml + # Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to # false only pre-existing Grafana users will be able to login (if ldap authentication is ok). allow_sign_up = true ``` -## LDAP Configuration +## Grafana LDAP Configuration +Depending on which LDAP server you're using and how that's configured your Grafana LDAP configuration may vary. +See [configuration examples](#configuration-examples) for more information. + +**LDAP specific configuration file (ldap.toml) example:** ```bash -# To troubleshoot and get more log info enable ldap debug logging in grafana.ini -# [log] -# filters = ldap:debug - [[servers]] # Ldap server host (specify multiple hosts space separated) host = "127.0.0.1" @@ -69,13 +76,8 @@ search_filter = "(cn=%s)" # An array of base dns to search through search_base_dns = ["dc=grafana,dc=org"] -# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups. -# This is done by enabling group_search_filter below. You must also set member_of= "cn" -# in [servers.attributes] below. - -## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" -## An array of the base DNs to search through for groups. Typically uses ou=groups +# group_search_filter_user_attribute = "distinguishedName" # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] # Specify names of the ldap attributes your ldap uses @@ -85,28 +87,11 @@ surname = "sn" username = "cn" member_of = "memberOf" email = "email" - -# Map ldap groups to grafana org roles -[[servers.group_mappings]] -group_dn = "cn=admins,dc=grafana,dc=org" -org_role = "Admin" -# To make user an instance admin (Grafana Admin) uncomment line below -# grafana_admin = true -# The Grafana organization database id, optional, if left out the default org (id 1) will be used. Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs -# org_id = 1 - -[[servers.group_mappings]] -group_dn = "cn=users,dc=grafana,dc=org" -org_role = "Editor" - -[[servers.group_mappings]] -# If you want to match all (or no ldap groups) then you can use wildcard -group_dn = "*" -org_role = "Viewer" - ``` -## Bind & Bind Password +### Bind + +#### Bind & Bind Password By default the configuration expects you to specify a bind DN and bind password. This should be a read only user that can perform LDAP searches. When the user DN is found a second bind is performed with the user provided username & password (in the normal Grafana login form). @@ -116,7 +101,7 @@ bind_dn = "cn=admin,dc=grafana,dc=org" bind_password = "grafana" ``` -### Single Bind Example +#### Single Bind Example If you can provide a single bind expression that matches all possible users, you can skip the second bind and bind against the user DN directly. This allows you to not specify a bind_password in the configuration file. @@ -128,7 +113,7 @@ bind_dn = "cn=%s,o=users,dc=grafana,dc=org" In this case you skip providing a `bind_password` and instead provide a `bind_dn` value with a `%s` somewhere. This will be replaced with the username entered in on the Grafana login page. The search filter and search bases settings are still needed to perform the LDAP search to retrieve the other LDAP information (like LDAP groups and email). -## POSIX schema (no memberOf attribute) +### POSIX schema If your ldap server does not support the memberOf attribute add these options: ```bash @@ -140,23 +125,134 @@ group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] Also change set `member_of = "cn"` in the `[servers.attributes]` section. +### Group Mappings -## LDAP to Grafana Org Role Sync +In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization and role. These will be synced every time the user logs in, with LDAP being +the authoritative source. So, if you change a user's role in the Grafana Org. Users page, this change will be reset the next time the user logs in. If you +change the LDAP groups of a user, the change will take effect the next time the user logs in. -### Mappings -In `[[servers.group_mappings]]` you can map an LDAP group to a Grafana organization -and role. These will be synced every time the user logs in, with LDAP being -the authoritative source. So, if you change a user's role in the Grafana Org. -Users page, this change will be reset the next time the user logs in. If you -change the LDAP groups of a user, the change will take effect the next -time the user logs in. +The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the +TOML config will be used. -### Grafana Admin -with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs & -users. +**LDAP specific configuration file (ldap.toml) example:** +```bash +[[servers]] +# other settings omitted for clarity -### Priority -The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used. +[[servers.group_mappings]] +group_dn = "cn=superadmins,dc=grafana,dc=org" +org_role = "Admin" +grafana_admin = true # Available in Grafana v5.3 and above + +[[servers.group_mappings]] +group_dn = "cn=admins,dc=grafana,dc=org" +org_role = "Admin" + +[[servers.group_mappings]] +group_dn = "cn=users,dc=grafana,dc=org" +org_role = "Editor" + +[[servers.group_mappings]] +group_dn = "*" +org_role = "Viewer" +``` + +Setting | Required | Description | Default +------------ | ------------ | ------------- | ------------- +`group_dn` | Yes | LDAP distinguished name (DN) of LDAP group. If you want to match all (or no LDAP groups) then you can use wildcard (`"*"`) | +`org_role` | Yes | Assign users of `group_dn` the organisation role `"Admin"`, `"Editor"` or `"Viewer"` | +`org_id` | No | The Grafana organization database id. Setting this allows for multiple group_dn's to be assigned to the same `org_role` provided the `org_id` differs | `1` (default org id) +`grafana_admin` | No | When `true` makes user of `group_dn` Grafana server admin. A Grafana server admin has admin access over all organisations and users. Available in Grafana v5.3 and above | `false` + +### Nested/recursive group membership + +Users with nested/recursive group membership must have an LDAP server that supports `LDAP_MATCHING_RULE_IN_CHAIN` +and configure `group_search_filter` in a way that it returns the groups the submitted username is a member of. + +**Active Directory example:** + +Active Directory groups store the Distinguished Names (DNs) of members, so your filter will need to know the DN for the user based only on the submitted username. +Multiple DN templates can be searched by combining filters with the LDAP OR-operator. Examples: + +```bash +group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])" +group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))" +``` + +For troubleshooting, by changing `member_of` in `[servers.attributes]` to "distinguishedName" it will show you more accurate group memberships when [debug is enabled](#troubleshooting). +## Configuration examples +### OpenLDAP + +[OpenLDAP](http://www.openldap.org/) is an open source directory service. + +**LDAP specific configuration file (ldap.toml):** +```bash +[[servers]] +host = "127.0.0.1" +port = 389 +use_ssl = false +start_tls = false +ssl_skip_verify = false +bind_dn = "cn=admin,dc=grafana,dc=org" +bind_password = 'grafana' +search_filter = "(cn=%s)" +search_base_dns = ["dc=grafana,dc=org"] + +[servers.attributes] +name = "givenName" +surname = "sn" +username = "cn" +member_of = "memberOf" +email = "email" + +# [[servers.group_mappings]] omitted for clarity +``` + +### Active Directory + +[Active Directory](https://technet.microsoft.com/en-us/library/hh831484(v=ws.11).aspx) is a directory service which is commonly used in Windows environments. + +Assuming the following Active Directory server setup: + +* IP address: `10.0.0.1` +* Domain: `CORP` +* DNS name: `corp.local` + +**LDAP specific configuration file (ldap.toml):** +```bash +[[servers]] +host = "10.0.0.1" +port = 3269 +use_ssl = true +start_tls = false +ssl_skip_verify = true +bind_dn = "CORP\\%s" +search_filter = "(sAMAccountName=%s)" +search_base_dns = ["dc=corp,dc=local"] + +[servers.attributes] +name = "givenName" +surname = "sn" +username = "sAMAccountName" +member_of = "memberOf" +email = "mail" + +# [[servers.group_mappings]] omitted for clarity +``` + +#### Port requirements + +In above example SSL is enabled and an encrypted port have been configured. If your Active Directory don't support SSL please change `enable_ssl = false` and `port = 389`. +Please inspect your Active Directory configuration and documentation to find the correct settings. For more information about Active Directory and port requirements see [link](https://technet.microsoft.com/en-us/library/dd772723(v=ws.10)). + +## Troubleshooting + +To troubleshoot and get more log info enable ldap debug logging in the [main config file]({{< relref "installation/configuration.md" >}}). + +```bash +[log] +filters = ldap:debug +``` From 8096cd8f3374a174e113ffe53276af0acd8cf434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 07:30:27 +0200 Subject: [PATCH 033/127] fix: added reducer test --- .../features/folders/state/reducers.test.ts | 37 ++++++++++++++++--- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index 1a7f4310f76..ff37f13f97f 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,17 +1,42 @@ import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; import { inititalState, folderReducer } from './reducers'; +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + describe('folder reducer', () => { - it('should set teams', () => { - const payload = [getMockTeam()]; + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); const action: Action = { - type: ActionTypes.LoadTeams, - payload, + type: ActionTypes.LoadFolder, + payload: folder, }; - const result = teamsReducer(initialTeamsState, action); + const state = folderReducer(inititalState, action); - expect(result.teams).toEqual(payload); + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); }); }); From f360b6186b0d0726762382caec2a787c493cb386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 10:52:29 +0200 Subject: [PATCH 034/127] wip: first couple of things starting to work --- public/app/core/actions/permissions.ts | 24 +++++ .../DisabledPermissionListItem.tsx | 43 ++++++++ .../PermissionList/PermissionList.tsx | 61 +++++++++++ .../PermissionList/PermissionListItem.tsx | 100 ++++++++++++++++++ .../features/folders/FolderPermissions.tsx | 70 +++++++----- public/app/features/folders/state/actions.ts | 85 ++++++++++++++- public/app/features/folders/state/reducers.ts | 41 ++++++- public/app/types/acl.ts | 60 +++++++++++ public/app/types/folder.ts | 11 +- public/app/types/index.ts | 8 +- 10 files changed, 465 insertions(+), 38 deletions(-) create mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/components/PermissionList/DisabledPermissionListItem.tsx create mode 100644 public/app/core/components/PermissionList/PermissionList.tsx create mode 100644 public/app/core/components/PermissionList/PermissionListItem.tsx create mode 100644 public/app/types/acl.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts new file mode 100644 index 00000000000..2b07b7145dd --- /dev/null +++ b/public/app/core/actions/permissions.ts @@ -0,0 +1,24 @@ +import { DashboardAcl } from '../../types'; + +export enum ActionTypes { + LoadFolderPermissions = 'LoadFolderPermissions', +} + +export interface LoadFolderPermissionsAction { + type: ActionTypes.LoadFolderPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadFolderPermissions; + +export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); + +export function getFolderPermissions(uid: string): ThunkResult { + return async dispatch => { + const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx new file mode 100644 index 00000000000..d65595dae66 --- /dev/null +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -0,0 +1,43 @@ +import React, { Component } from 'react'; +import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; +import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; + +export interface Props { + item: any; +} + +export default class DisabledPermissionListItem extends Component { + render() { + const { item } = this.props; + + return ( + + + + + + {item.name} + (Role) + + + Can + +
+ {}} + value={item.permission} + disabled={true} + className={'gf-form-input--form-dropdown-right'} + /> +
+ + + + + + ); + } +} diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx new file mode 100644 index 00000000000..29f810a4358 --- /dev/null +++ b/public/app/core/components/PermissionList/PermissionList.tsx @@ -0,0 +1,61 @@ +import React, { PureComponent } from 'react'; +import PermissionsListItem from './PermissionListItem'; +import DisabledPermissionsListItem from './DisabledPermissionListItem'; +import { DashboardAcl, FolderInfo } from 'app/types'; + +export interface Props { + items: DashboardAcl[]; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: any; + isFetching: boolean; + folderInfo?: FolderInfo; +} + +class PermissionList extends PureComponent { + render() { + const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props; + + return ( + + + + {items.map((item, idx) => { + return ( + + ); + })} + {isFetching === true && items.length < 1 ? ( + + + + ) : null} + + {isFetching === false && items.length < 1 ? ( + + + + ) : null} + +
+ Loading permissions... +
+ No permissions are set. Will only be accessible by admins. +
+ ); + } +} + +export default PermissionList; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx new file mode 100644 index 00000000000..3e5aaf3ab2f --- /dev/null +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -0,0 +1,100 @@ +import React, { PureComponent } from 'react'; +import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; +import { dashboardPermissionLevels } from 'app/types/acl'; +import { DashboardAcl, FolderInfo, PermissionLevel } from 'app/types'; + +const setClassNameHelper = inherited => { + return inherited ? 'gf-form-disabled' : ''; +}; + +function ItemAvatar({ item }) { + if (item.userAvatarUrl) { + return ; + } + if (item.teamAvatarUrl) { + return ; + } + if (item.role === 'Editor') { + return ; + } + + return ; +} + +function ItemDescription({ item }) { + if (item.userId) { + return (User); + } + if (item.teamId) { + return (Team); + } + return (Role); +} + +interface Props { + item: DashboardAcl; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void; + folderInfo?: FolderInfo; +} + +export default class PermissionsListItem extends PureComponent { + onPermissionChanged = option => { + this.props.onPermissionChanged(this.props.item, option.value as PermissionLevel); + }; + + onRemoveItem = () => { + this.props.onRemoveItem(this.props.item); + }; + + render() { + const { item, folderInfo } = this.props; + const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; + + return ( + + + + + + {item.name} + + + {item.inherited && + folderInfo && ( + + Inherited from folder{' '} + + {folderInfo.title} + {' '} + + )} + {inheritedFromRoot && Default Permission} + + Can + +
+ +
+ + + {!item.inherited ? ( + + + + ) : ( + + )} + + + ); + } +} diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 512927c24e6..25de5f8be16 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; @@ -9,50 +8,61 @@ import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, FolderState } from 'app/types'; -import { getFolderByUid } from './state/actions'; -import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types'; +import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions'; import { getLoadingNav } from './state/navModel'; +import PermissionList from 'app/core/components/PermissionList/PermissionList'; export interface Props { navModel: NavModel; - getFolderByUid: typeof getFolderByUid; folderUid: string; folder: FolderState; - permissions: typeof PermissionsStore.Type; - backendSrv: any; + getFolderByUid: typeof getFolderByUid; + getFolderPermissions: typeof getFolderPermissions; + updateFolderPermission: typeof updateFolderPermission; + removeFolderPermission: typeof removeFolderPermission; } -@inject('permissions') -@observer -export class FolderPermissions extends Component { +export interface State { + isAdding: boolean; +} + +export class FolderPermissions extends Component { constructor(props) { super(props); - this.handleAddPermission = this.handleAddPermission.bind(this); + + this.state = { + isAdding: false, + }; } componentDidMount() { this.props.getFolderByUid(this.props.folderUid); + this.props.getFolderPermissions(this.props.folderUid); } - componentWillUnmount() { - const { permissions } = this.props; - permissions.hideAddPermissions(); - } + onOpenAddPermissions = () => { + this.setState({ isAdding: true }); + }; - handleAddPermission() { - const { permissions } = this.props; - permissions.toggleAddPermissions(); - } + onRemoveItem = (item: DashboardAcl) => { + this.props.removeFolderPermission(item); + }; + + onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { + this.props.updateFolderPermission(item, level); + }; render() { - const { navModel, permissions, backendSrv, folder } = this.props; + const { navModel, folder } = this.props; + const { isAdding } = this.state; if (folder.id === 0) { return ; } const dashboardId = folder.id; + const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; return (
@@ -64,18 +74,17 @@ export class FolderPermissions extends Component {
-
- - - - +
); @@ -93,6 +102,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 5d153b2fb8a..29940cc7a31 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,7 +1,14 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; -import { FolderDTO, FolderState } from 'app/types'; +import { + FolderDTO, + FolderState, + DashboardAcl, + DashboardAclDTO, + PermissionLevel, + DashboardAclUpdateDTO, +} from 'app/types'; import { updateNavIndex, updateLocation } from 'app/core/actions'; import { buildNavModel } from './navModel'; import appEvents from 'app/core/app_events'; @@ -10,6 +17,7 @@ export enum ActionTypes { LoadFolder = 'LOAD_FOLDER', SetFolderTitle = 'SET_FOLDER_TITLE', SaveFolder = 'SAVE_FOLDER', + LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS', } export interface LoadFolderAction { @@ -22,6 +30,15 @@ export interface SetFolderTitleAction { payload: string; } +export interface LoadFolderPermissionsAction { + type: ActionTypes.LoadFolderPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadFolderAction | SetFolderTitleAction | LoadFolderPermissionsAction; + +type ThunkResult = ThunkAction; + export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ type: ActionTypes.LoadFolder, payload: folder, @@ -32,10 +49,10 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ payload: newTitle, }); -export type Action = LoadFolderAction | SetFolderTitleAction; - -type ThunkResult = ThunkAction; - +export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -65,3 +82,61 @@ export function deleteFolder(uid: string): ThunkResult { dispatch(updateLocation({ path: `dashboards` })); }; } + +export function getFolderPermissions(uid: string): ThunkResult { + return async dispatch => { + const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} + +function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { + return { + userId: item.userId, + teamId: item.teamId, + role: item.role, + permission: item.permission, + }; +} + +export function updateFolderPermission(itemToUpdate: DashboardAcl, level: PermissionLevel): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited) { + continue; + } + + const updated = toUpdateItem(itemToUpdate); + + // if this is the item we want to update, update it's permisssion + if (itemToUpdate === item) { + updated.permission = level; + } + + itemsToUpdate.push(updated); + } + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} + +export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited || item === itemToDelete) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 41ae10d19e5..6e6a671685a 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,4 +1,4 @@ -import { FolderState } from 'app/types'; +import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { @@ -8,13 +8,15 @@ export const inititalState: FolderState = { url: '', canSave: false, hasChanged: false, - version: 0, + version: 1, + permissions: [], }; export const folderReducer = (state = inititalState, action: Action): FolderState => { switch (action.type) { case ActionTypes.LoadFolder: return { + ...state, ...action.payload, hasChanged: false, }; @@ -24,10 +26,45 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat title: action.payload, hasChanged: action.payload.trim().length > 0, }; + case ActionTypes.LoadFolderPermissions: + return { + ...state, + permissions: processAclItems(action.payload), + }; } return state; }; +function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} + export default { folder: folderReducer, }; diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts new file mode 100644 index 00000000000..d77fc4793fc --- /dev/null +++ b/public/app/types/acl.ts @@ -0,0 +1,60 @@ +export interface DashboardAclDTO { + id?: number; + dashboardId?: number; + userId?: number; + userLogin?: string; + userEmail?: string; + teamId?: number; + team?: string; + permission?: PermissionLevel; + permissionName?: string; + role?: string; + icon?: string; + inherited?: boolean; +} + +export interface DashboardAclUpdateDTO { + userId: number; + teamId: number; + role: string; + permission: PermissionLevel; +} + +export interface DashboardAcl { + id?: number; + dashboardId?: number; + userId?: number; + userLogin?: string; + userEmail?: string; + teamId?: number; + team?: string; + permission?: PermissionLevel; + permissionName?: string; + role?: string; + icon?: string; + name?: string; + inherited?: boolean; + sortRank?: number; +} + +export interface DashboardPermissionInfo { + value: PermissionLevel; + label: string; + description: string; +} + +export enum PermissionLevel { + View = 1, + Edit = 2, + Admin = 4, +} + +export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ + { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' }, + { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, + { + value: PermissionLevel.Admin, + label: 'Admin', + description: 'Can add/remove permissions and can add, edit and delete dashboards.', + }, +]; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts index 6fbe79cce8c..bbcae01fe59 100644 --- a/public/app/types/folder.ts +++ b/public/app/types/folder.ts @@ -1,3 +1,5 @@ +import { DashboardAcl } from './acl'; + export interface FolderDTO { id: number; uid: string; @@ -12,7 +14,14 @@ export interface FolderState { uid: string; title: string; url: string; - version: number; canSave: boolean; hasChanged: boolean; + version: number; + permissions: DashboardAcl[]; +} + +export interface FolderInfo { + id: number; + title: string; + url: string; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 52d1ba592c5..49f7fdb0f28 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,7 +2,8 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; -import { FolderDTO, FolderState } from './folder'; +import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl'; export { Team, @@ -22,6 +23,11 @@ export { UrlQueryValue, FolderDTO, FolderState, + FolderInfo, + DashboardAcl, + DashboardAclDTO, + DashboardAclUpdateDTO, + PermissionLevel, }; export interface StoreState { From 2926725bab128b7bced9ce4f03b8ae368107423c Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 1 May 2018 12:49:18 +0900 Subject: [PATCH 035/127] add annotation option to treat series value as timestamp --- .../datasource/prometheus/datasource.ts | 10 ++++- .../partials/annotations.editor.html | 11 +++++- .../prometheus/specs/datasource.test.ts | 39 +++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 9332a73caca..a60882e0470 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -487,15 +487,21 @@ export class PrometheusDatasource { .value(); for (const value of series.values) { - if (value[1] === '1') { + const valueIsTrue = value[1] === '1'; // e.g. ALERTS + if (valueIsTrue || annotation.useValueForTime) { const event = { annotation: annotation, - time: Math.floor(parseFloat(value[0])) * 1000, title: self.resultTransformer.renderTemplate(titleFormat, series.metric), tags: tags, text: self.resultTransformer.renderTemplate(textFormat, series.metric), }; + if (annotation.useValueForTime) { + event['time'] = Math.floor(parseFloat(value[1])); + } else { + event['time'] = Math.floor(parseFloat(value[0])) * 1000; + } + eventList.push(event); } } diff --git a/public/app/plugins/datasource/prometheus/partials/annotations.editor.html b/public/app/plugins/datasource/prometheus/partials/annotations.editor.html index 09ee52bda45..6e5982123fd 100644 --- a/public/app/plugins/datasource/prometheus/partials/annotations.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/annotations.editor.html @@ -10,7 +10,7 @@
-
Field formats
+
Field formats
Title @@ -27,4 +27,13 @@
+ +
Other options
+
+
+ + +
+
diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index ae91e6647e0..980574624ad 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -630,6 +630,45 @@ describe('PrometheusDatasource', () => { expect(results[0].text).toBe('testinstance'); expect(results[0].time).toBe(123 * 1000); }); + + it('should return annotation list with seriesValueAsTiemstamp', () => { + const options = { + annotation: { + expr: 'timestamp_seconds', + tagKeys: 'job', + titleFormat: '{{job}}', + textFormat: '{{instance}}', + useValueForTime: true, + }, + range: { + from: new Date('2014-04-10T05:20:10Z'), + to: new Date('2014-05-20T03:10:22Z'), + }, + }; + ctx.backendSrvMock.datasourceRequest.mockReturnValue( + Promise.resolve({ + status: 'success', + data: { + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'timestamp_milliseconds', + instance: 'testinstance', + job: 'testjob', + }, + values: [[1443454528, '1500000000000']], + }, + ], + }, + }) + ); + ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); + ctx.ds.annotationQuery(options).then(function (results) { + expect(results[0].time).toEqual(1500000000000); + ctx.backendSrvMock.datasourceRequest.mockReset(); + }); + }); }); describe('When resultFormat is table and instant = true', () => { From 3031c2e6fc1f907c0baa54756fe3aa8fa6935991 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 28 Aug 2018 01:58:53 +0900 Subject: [PATCH 036/127] fix test --- .../prometheus/specs/datasource.test.ts | 113 ++++++++---------- 1 file changed, 47 insertions(+), 66 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 980574624ad..1fa96d03fe7 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -581,7 +581,7 @@ describe('PrometheusDatasource', () => { describe('When performing annotationQuery', () => { let results; - const options = { + const options: any = { annotation: { expr: 'ALERTS{alertstate="firing"}', tagKeys: 'job', @@ -594,79 +594,60 @@ describe('PrometheusDatasource', () => { }, }; - beforeEach(async () => { - const response = { - status: 'success', + const response = { + status: 'success', + data: { data: { - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'ALERTS', - alertname: 'InstanceDown', - alertstate: 'firing', - instance: 'testinstance', - job: 'testjob', - }, - values: [[123, '1']], + resultType: 'matrix', + result: [ + { + metric: { + __name__: 'ALERTS', + alertname: 'InstanceDown', + alertstate: 'firing', + instance: 'testinstance', + job: 'testjob', }, - ], - }, + values: [[123, '1']], + }, + ], }, - }; + }, + }; - backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); + describe('not use useValueForTime', () => { + beforeEach(async () => { + options.annotation.useValueForTime = false; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); - await ctx.ds.annotationQuery(options).then(data => { - results = data; + await ctx.ds.annotationQuery(options).then(function (data) { + results = data; + }); + }); + + it('should return annotation list', () => { + expect(results.length).toBe(1); + expect(results[0].tags).toContain('testjob'); + expect(results[0].title).toBe('InstanceDown'); + expect(results[0].text).toBe('testinstance'); + expect(results[0].time).toBe(123 * 1000); }); }); - it('should return annotation list', () => { - expect(results.length).toBe(1); - expect(results[0].tags).toContain('testjob'); - expect(results[0].title).toBe('InstanceDown'); - expect(results[0].text).toBe('testinstance'); - expect(results[0].time).toBe(123 * 1000); - }); - it('should return annotation list with seriesValueAsTiemstamp', () => { - const options = { - annotation: { - expr: 'timestamp_seconds', - tagKeys: 'job', - titleFormat: '{{job}}', - textFormat: '{{instance}}', - useValueForTime: true, - }, - range: { - from: new Date('2014-04-10T05:20:10Z'), - to: new Date('2014-05-20T03:10:22Z'), - }, - }; - ctx.backendSrvMock.datasourceRequest.mockReturnValue( - Promise.resolve({ - status: 'success', - data: { - resultType: 'matrix', - result: [ - { - metric: { - __name__: 'timestamp_milliseconds', - instance: 'testinstance', - job: 'testjob', - }, - values: [[1443454528, '1500000000000']], - }, - ], - }, - }) - ); - ctx.ds = new PrometheusDatasource(instanceSettings, q, ctx.backendSrvMock, ctx.templateSrvMock, ctx.timeSrvMock); - ctx.ds.annotationQuery(options).then(function (results) { - expect(results[0].time).toEqual(1500000000000); - ctx.backendSrvMock.datasourceRequest.mockReset(); + describe('use useValueForTime', () => { + beforeEach(async () => { + options.annotation.useValueForTime = true; + backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + + await ctx.ds.annotationQuery(options).then(function (data) { + results = data; + }); + }); + + it('should return annotation list', () => { + expect(results[0].time).toEqual(1); }); }); }); From dc08093f6c8077735fb78d24ca3791aa382ede28 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 13 Sep 2018 20:15:33 +0900 Subject: [PATCH 037/127] minor fix --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- .../datasource/prometheus/specs/datasource.test.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index a60882e0470..ca80b3760a7 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -585,7 +585,7 @@ export class PrometheusDatasource { } getTimeRange(): { start: number; end: number } { - let range = this.timeSrv.timeRange(); + const range = this.timeSrv.timeRange(); return { start: this.getPrometheusTime(range.from, false), end: this.getPrometheusTime(range.to, true), diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index 1fa96d03fe7..eef2bbd56b6 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -619,9 +619,9 @@ describe('PrometheusDatasource', () => { beforeEach(async () => { options.annotation.useValueForTime = false; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.annotationQuery(options).then(function (data) { + await ctx.ds.annotationQuery(options).then(data => { results = data; }); }); @@ -639,9 +639,9 @@ describe('PrometheusDatasource', () => { beforeEach(async () => { options.annotation.useValueForTime = true; backendSrv.datasourceRequest = jest.fn(() => Promise.resolve(response)); - ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv, templateSrv, timeSrv); + ctx.ds = new PrometheusDatasource(instanceSettings, q, backendSrv as any, templateSrv, timeSrv); - await ctx.ds.annotationQuery(options).then(function (data) { + await ctx.ds.annotationQuery(options).then(data => { results = data; }); }); From d35eca333feb144d841693552842848402973644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 13:56:09 +0200 Subject: [PATCH 038/127] folder permissions in redux --- .../PermissionList/AddPermission.tsx | 142 ++++++++++++++++++ .../PermissionList/PermissionList.tsx | 3 +- .../PermissionList/PermissionListItem.tsx | 4 +- .../features/folders/FolderPermissions.tsx | 36 +++-- .../folders/FolderSettingsPage.test.tsx | 1 + public/app/features/folders/state/actions.ts | 31 +++- public/app/features/folders/state/reducers.ts | 3 +- public/app/types/acl.ts | 27 ++++ public/app/types/index.ts | 5 - 9 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 public/app/core/components/PermissionList/AddPermission.tsx diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx new file mode 100644 index 00000000000..76bcfac4780 --- /dev/null +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -0,0 +1,142 @@ +import React, { Component } from 'react'; +import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; +import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; +import { + dashboardPermissionLevels, + dashboardAclTargets, + AclTarget, + PermissionLevel, + NewDashboardAclItem, +} from 'app/types/acl'; + +export interface Props { + onAddPermission: (item: NewDashboardAclItem) => void; + onCancel: () => void; +} + +class AddPermissions extends Component { + constructor(props) { + super(props); + this.state = this.getCleanState(); + } + + getCleanState() { + return { + userId: 0, + teamId: 0, + role: '', + type: AclTarget.Team, + permission: PermissionLevel.View, + }; + } + + onTypeChanged = evt => { + this.setState({ type: evt.target.value as AclTarget }); + }; + + onUserSelected = (user: User) => { + this.setState({ + userId: user ? user.id : 0, + teamId: 0, + }); + }; + + onTeamSelected = (team: Team) => { + this.setState({ + userId: 0, + teamId: team ? team.id : 0, + }); + }; + + onPermissionChanged = (permission: OptionWithDescription) => { + this.setState({ permission: permission.value }); + }; + + onSubmit = async evt => { + evt.preventDefault(); + await this.props.onAddPermission(this.state); + this.setState(this.getCleanState()); + }; + + isValid() { + switch (this.state.type) { + case AclTarget.Team: + return this.state.teamId > 0; + case AclTarget.User: + return this.state.userId > 0; + } + return true; + } + + render() { + const { onCancel } = this.props; + const newItem = this.state; + const pickerClassName = 'width-20'; + const isValid = this.isValid(); + + return ( +
+ +
+
Add Permission For
+
+
+
+ +
+
+ + {newItem.type === AclTarget.User ? ( +
+ +
+ ) : null} + + {newItem.type === AclTarget.Team ? ( +
+ +
+ ) : null} + +
+ +
+ +
+ +
+
+
+
+ ); + } +} + +export default AddPermissions; diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx index 29f810a4358..772baa0c274 100644 --- a/public/app/core/components/PermissionList/PermissionList.tsx +++ b/public/app/core/components/PermissionList/PermissionList.tsx @@ -1,7 +1,8 @@ import React, { PureComponent } from 'react'; import PermissionsListItem from './PermissionListItem'; import DisabledPermissionsListItem from './DisabledPermissionListItem'; -import { DashboardAcl, FolderInfo } from 'app/types'; +import { FolderInfo } from 'app/types'; +import { DashboardAcl } from 'app/types/acl'; export interface Props { items: DashboardAcl[]; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx index 3e5aaf3ab2f..b846f98a063 100644 --- a/public/app/core/components/PermissionList/PermissionListItem.tsx +++ b/public/app/core/components/PermissionList/PermissionListItem.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { dashboardPermissionLevels } from 'app/types/acl'; -import { DashboardAcl, FolderInfo, PermissionLevel } from 'app/types'; +import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl'; +import { FolderInfo } from 'app/types'; const setClassNameHelper = inherited => { return inherited ? 'gf-form-disabled' : ''; diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 25de5f8be16..c86137a55ce 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,17 +1,23 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; -import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types'; -import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; +import { + getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, + addFolderPermission, +} from './state/actions'; import { getLoadingNav } from './state/navModel'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; +import AddPermission from 'app/core/components/PermissionList/AddPermission'; +import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; export interface Props { navModel: NavModel; @@ -21,13 +27,14 @@ export interface Props { getFolderPermissions: typeof getFolderPermissions; updateFolderPermission: typeof updateFolderPermission; removeFolderPermission: typeof removeFolderPermission; + addFolderPermission: typeof addFolderPermission; } export interface State { isAdding: boolean; } -export class FolderPermissions extends Component { +export class FolderPermissions extends PureComponent { constructor(props) { super(props); @@ -53,6 +60,14 @@ export class FolderPermissions extends Component { this.props.updateFolderPermission(item, level); }; + onAddPermission = (newItem: NewDashboardAclItem) => { + return this.props.addFolderPermission(newItem); + }; + + onCancelAddPermission = () => { + this.setState({ isAdding: false }); + }; + render() { const { navModel, folder } = this.props; const { isAdding } = this.state; @@ -61,8 +76,7 @@ export class FolderPermissions extends Component { return ; } - const dashboardId = folder.id; - const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; + const folderInfo = { title: folder.title, url: folder.url, id: folder.id }; return (
@@ -78,6 +92,9 @@ export class FolderPermissions extends Component { Add Permission
+ + + { url: 'url', hasChanged: false, version: 1, + permissions: [], }, getFolderByUid: jest.fn(), setFolderTitle: jest.fn(), diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 29940cc7a31..4f15f813a68 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -1,14 +1,15 @@ import { getBackendSrv } from 'app/core/services/backend_srv'; import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; import { - FolderDTO, - FolderState, DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO, -} from 'app/types'; + NewDashboardAclItem, +} from 'app/types/acl'; + import { updateNavIndex, updateLocation } from 'app/core/actions'; import { buildNavModel } from './navModel'; import appEvents from 'app/core/app_events'; @@ -140,3 +141,27 @@ export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult< await dispatch(getFolderPermissions(folder.uid)); }; } + +export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult { + return async (dispatch, getStore) => { + const folder = getStore().folder; + const itemsToUpdate = []; + + for (const item of folder.permissions) { + if (item.inherited) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + itemsToUpdate.push({ + userId: newItem.userId, + teamId: newItem.teamId, + role: item.role, + permission: item.permission, + }); + + await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate }); + await dispatch(getFolderPermissions(folder.uid)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 6e6a671685a..9b73312790c 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,4 +1,5 @@ -import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types'; +import { FolderState } from 'app/types'; +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; import { Action, ActionTypes } from './actions'; export const inititalState: FolderState = { diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index d77fc4793fc..feca062b355 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -43,12 +43,39 @@ export interface DashboardPermissionInfo { description: string; } +export interface NewDashboardAclItem { + teamId: number; + userId: number; + role: string; + permission: PermissionLevel; + type: AclTarget; +} + export enum PermissionLevel { View = 1, Edit = 2, Admin = 4, } +export enum AclTarget { + Team = 'team', + User = 'user', + Viewer = 'viewer', + Editor = 'editor', +} + +export interface AclTargetInfo { + value: AclTarget; + text: string; +} + +export const dashboardAclTargets: AclTargetInfo[] = [ + { value: AclTarget.Team, text: 'Team' }, + { value: AclTarget.User, text: 'User' }, + { value: AclTarget.Viewer, text: 'Everyone With Viewer Role' }, + { value: AclTarget.Editor, text: 'Everyone With Editor Role' }, +]; + export const dashboardPermissionLevels: DashboardPermissionInfo[] = [ { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' }, { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 49f7fdb0f28..6f052c7c503 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -3,7 +3,6 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; -import { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl'; export { Team, @@ -24,10 +23,6 @@ export { FolderDTO, FolderState, FolderInfo, - DashboardAcl, - DashboardAclDTO, - DashboardAclUpdateDTO, - PermissionLevel, }; export interface StoreState { From f2edb82e797d38ec8a53ee2bffd0c044a6571f6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:10:51 +0200 Subject: [PATCH 039/127] Folder pages to redux (#13235) * creating types, actions, reducer * load teams and store in redux * delete team * set search query action and tests * Teampages page * team members, bug in fetching team * flattened team state, tests for TeamMembers * test for team member selector * wip: began folder to redux migration * team settings * actions for group sync * wip: progress on redux folder store * wip: folder to redux * wip: folder settings page to redux progress * mobx -> redux: major progress on folder migration * redux: moved folders to it's own features folder * fix: added loading nav states * fix: gofmt issues * wip: working on reducer test * fix: added reducer test --- public/app/containers/ContainerProps.ts | 14 -- .../ManageDashboards/FolderSettings.test.tsx | 84 --------- .../ManageDashboards/FolderSettings.tsx | 160 ------------------ public/app/core/reducers/location.ts | 4 +- public/app/core/selectors/navModel.ts | 10 +- public/app/core/services/backend_srv.ts | 10 -- public/app/features/dashboard/all.ts | 2 - .../dashboard/folder_settings_ctrl.ts | 94 ---------- .../folders}/FolderPermissions.tsx | 56 +++--- .../folders/FolderSettingsPage.test.tsx | 55 ++++++ .../features/folders/FolderSettingsPage.tsx | 105 ++++++++++++ .../FolderSettingsPage.test.tsx.snap | 131 ++++++++++++++ public/app/features/folders/state/actions.ts | 67 ++++++++ public/app/features/folders/state/navModel.ts | 53 ++++++ .../features/folders/state/reducers.test.ts | 42 +++++ public/app/features/folders/state/reducers.ts | 33 ++++ public/app/features/teams/TeamPages.tsx | 8 +- public/app/features/teams/state/actions.ts | 117 +++---------- public/app/features/teams/state/navModel.ts | 67 ++++++++ public/app/routes/routes.ts | 6 +- public/app/stores/FolderStore/FolderStore.ts | 60 ------- public/app/stores/RootStore/RootStore.ts | 2 - public/app/stores/configureStore.ts | 2 + public/app/types/folder.ts | 18 ++ public/app/types/index.ts | 4 + yarn.lock | 30 +--- 26 files changed, 656 insertions(+), 578 deletions(-) delete mode 100644 public/app/containers/ContainerProps.ts delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.test.tsx delete mode 100644 public/app/containers/ManageDashboards/FolderSettings.tsx delete mode 100644 public/app/features/dashboard/folder_settings_ctrl.ts rename public/app/{containers/ManageDashboards => features/folders}/FolderPermissions.tsx (60%) create mode 100644 public/app/features/folders/FolderSettingsPage.test.tsx create mode 100644 public/app/features/folders/FolderSettingsPage.tsx create mode 100644 public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap create mode 100644 public/app/features/folders/state/actions.ts create mode 100644 public/app/features/folders/state/navModel.ts create mode 100644 public/app/features/folders/state/reducers.test.ts create mode 100644 public/app/features/folders/state/reducers.ts create mode 100644 public/app/features/teams/state/navModel.ts delete mode 100644 public/app/stores/FolderStore/FolderStore.ts create mode 100644 public/app/types/folder.ts diff --git a/public/app/containers/ContainerProps.ts b/public/app/containers/ContainerProps.ts deleted file mode 100644 index ce09b992f80..00000000000 --- a/public/app/containers/ContainerProps.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NavStore } from './../stores/NavStore/NavStore'; -import { PermissionsStore } from './../stores/PermissionsStore/PermissionsStore'; -import { ViewStore } from './../stores/ViewStore/ViewStore'; -import { FolderStore } from './../stores/FolderStore/FolderStore'; - -interface ContainerProps { - nav: typeof NavStore.Type; - permissions: typeof PermissionsStore.Type; - view: typeof ViewStore.Type; - folder: typeof FolderStore.Type; - backendSrv: any; -} - -export default ContainerProps; diff --git a/public/app/containers/ManageDashboards/FolderSettings.test.tsx b/public/app/containers/ManageDashboards/FolderSettings.test.tsx deleted file mode 100644 index bed3d569bcc..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.test.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import { FolderSettings } from './FolderSettings'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; - -describe('FolderSettings', () => { - let wrapper; - let page; - - beforeAll(() => { - backendSrv.getFolderByUid.mockReturnValue( - Promise.resolve({ - id: 1, - uid: 'uid', - title: 'Folder Name', - url: '/dashboards/f/uid/folder-name', - canSave: true, - version: 1, - }) - ); - - const store = RootStore.create( - { - view: { - path: 'asd', - query: {}, - routeParams: { - uid: 'uid-str', - }, - }, - }, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); - page = wrapper.dive(); - return page - .instance() - .loadStore() - .then(() => { - page.update(); - }); - }); - - it('should set the title input field', () => { - const titleInput = page.find('.gf-form-input'); - expect(titleInput).toHaveLength(1); - expect(titleInput.prop('value')).toBe('Folder Name'); - }); - - it('should update title and enable save button when changed', () => { - const titleInput = page.find('.gf-form-input'); - const disabledSubmitButton = page.find('button[type="submit"]'); - expect(disabledSubmitButton.prop('disabled')).toBe(true); - - titleInput.simulate('change', { target: { value: 'New Title' } }); - - const updatedTitleInput = page.find('.gf-form-input'); - expect(updatedTitleInput.prop('value')).toBe('New Title'); - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(false); - }); - - it('should disable save button if title is changed back to old title', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: 'Folder Name' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); - - it('should disable save button if title is changed to empty string', () => { - const titleInput = page.find('.gf-form-input'); - - titleInput.simulate('change', { target: { value: '' } }); - - const enabledSubmitButton = page.find('button[type="submit"]'); - expect(enabledSubmitButton.prop('disabled')).toBe(true); - }); -}); diff --git a/public/app/containers/ManageDashboards/FolderSettings.tsx b/public/app/containers/ManageDashboards/FolderSettings.tsx deleted file mode 100644 index 88830356563..00000000000 --- a/public/app/containers/ManageDashboards/FolderSettings.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { hot } from 'react-hot-loader'; -import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import ContainerProps from 'app/containers/ContainerProps'; -import { getSnapshot } from 'mobx-state-tree'; -import appEvents from 'app/core/app_events'; - -@inject('nav', 'folder', 'view') -@observer -export class FolderSettings extends React.Component { - formSnapshot: any; - - componentDidMount() { - this.loadStore(); - } - - loadStore() { - const { nav, folder, view } = this.props; - - return folder.load(view.routeParams.get('uid') as string).then(res => { - this.formSnapshot = getSnapshot(folder); - view.updatePathAndQuery(`${res.url}/settings`, {}, {}); - - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - } - - onTitleChange(evt) { - this.props.folder.setTitle(this.getFormSnapshot().folder.title, evt.target.value); - } - - getFormSnapshot() { - if (!this.formSnapshot) { - this.formSnapshot = getSnapshot(this.props.folder); - } - - return this.formSnapshot; - } - - save(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { nav, folder, view } = this.props; - - folder - .saveFolder({ overwrite: false }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }) - .catch(this.handleSaveFolderError.bind(this)); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - const { folder, view } = this.props; - const title = folder.folder.title; - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return folder.deleteFolder().then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]); - view.updatePathAndQuery('dashboards', '', ''); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - const { nav, folder, view } = this.props; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - folder - .saveFolder({ overwrite: true }) - .then(newUrl => { - view.updatePathAndQuery(newUrl, {}, {}); - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .then(() => { - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings'); - }); - }, - }); - } - } - - render() { - const { nav, folder } = this.props; - - if (!folder.folder || !nav.main) { - return

Loading

; - } - - return ( -
- -
-

Folder Settings

- -
-
-
- - -
-
- - -
- -
-
-
- ); - } -} - -export default hot(module)(FolderSettings); diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 4591448d082..6a356c4ea5a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -9,8 +9,8 @@ export const initialState: LocationState = { routeParams: {}, }; -function renderUrl(path: string, query: UrlQueryMap): string { - if (Object.keys(query).length > 0) { +function renderUrl(path: string, query: UrlQueryMap | undefined): string { + if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 8b3a3edd84e..aa508616962 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -15,7 +15,7 @@ function getNotFoundModel(): NavModel { }; } -export function getNavModel(navIndex: NavIndex, id: string): NavModel { +export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel): NavModel { if (navIndex[id]) { const node = navIndex[id]; const main = { @@ -33,7 +33,11 @@ export function getNavModel(navIndex: NavIndex, id: string): NavModel { node: node, main: main, }; - } else { - return getNotFoundModel(); } + + if (fallback) { + return fallback; + } + + return getNotFoundModel(); } diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 2a50a1b1f12..3e8132a695b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -252,16 +252,6 @@ export class BackendSrv { return this.post('/api/folders', payload); } - updateFolder(folder, options) { - options = options || {}; - - return this.put(`/api/folders/${folder.uid}`, { - title: folder.title, - version: folder.version, - overwrite: options.overwrite === true, - }); - } - deleteFolder(uid: string, showSuccessAlert) { return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true }); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 1e28a3c9a80..adb665c47b5 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,11 +32,9 @@ import './dashlinks/module'; import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; -import { FolderSettingsCtrl } from './folder_settings_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; import { CreateFolderCtrl } from './create_folder_ctrl'; coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); -coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/folder_settings_ctrl.ts b/public/app/features/dashboard/folder_settings_ctrl.ts deleted file mode 100644 index a847c29ac56..00000000000 --- a/public/app/features/dashboard/folder_settings_ctrl.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { FolderPageLoader } from './folder_page_loader'; -import appEvents from 'app/core/app_events'; - -export class FolderSettingsCtrl { - folderPageLoader: FolderPageLoader; - navModel: any; - folderId: number; - uid: string; - canSave = false; - folder: any; - title: string; - hasChanged: boolean; - - /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $routeParams, private $location) { - if (this.$routeParams.uid) { - this.uid = $routeParams.uid; - - this.folderPageLoader = new FolderPageLoader(this.backendSrv); - this.folderPageLoader.load(this, this.uid, 'manage-folder-settings').then(folder => { - if ($location.path() !== folder.meta.url) { - $location.path(`${folder.meta.url}/settings`).replace(); - } - - this.folder = folder; - this.canSave = this.folder.canSave; - this.title = this.folder.title; - }); - } - } - - save() { - this.titleChanged(); - - if (!this.hasChanged) { - return; - } - - this.folder.title = this.title.trim(); - - return this.backendSrv - .updateFolder(this.folder) - .then(result => { - if (result.url !== this.$location.path()) { - this.$location.url(result.url + '/settings'); - } - - appEvents.emit('dashboard-saved'); - appEvents.emit('alert-success', ['Folder saved']); - }) - .catch(this.handleSaveFolderError); - } - - titleChanged() { - this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase(); - } - - delete(evt) { - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete this folder and all its dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - return this.backendSrv.deleteFolder(this.uid).then(() => { - appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]); - this.$location.url('dashboards'); - }); - }, - }); - } - - handleSaveFolderError(err) { - if (err.data && err.data.status === 'version-mismatch') { - err.isHandled = true; - - appEvents.emit('confirm-modal', { - title: 'Conflict', - text: 'Someone else has updated this folder.', - text2: 'Would you still like to save this folder?', - yesText: 'Save & Overwrite', - icon: 'fa-warning', - onConfirm: () => { - this.backendSrv.updateFolder(this.folder, { overwrite: true }); - }, - }); - } - } -} diff --git a/public/app/containers/ManageDashboards/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx similarity index 60% rename from public/app/containers/ManageDashboards/FolderPermissions.tsx rename to public/app/features/folders/FolderPermissions.tsx index 072908d2b8e..512927c24e6 100644 --- a/public/app/containers/ManageDashboards/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -1,25 +1,38 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; -import { toJS } from 'mobx'; -import ContainerProps from 'app/containers/ContainerProps'; +import { connect } from 'react-redux'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import Permissions from 'app/core/components/Permissions/Permissions'; import Tooltip from 'app/core/components/Tooltip/Tooltip'; import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; import AddPermissions from 'app/core/components/Permissions/AddPermissions'; import SlideDown from 'app/core/components/Animations/SlideDown'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid } from './state/actions'; +import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore'; +import { getLoadingNav } from './state/navModel'; -@inject('nav', 'folder', 'view', 'permissions') +export interface Props { + navModel: NavModel; + getFolderByUid: typeof getFolderByUid; + folderUid: string; + folder: FolderState; + permissions: typeof PermissionsStore.Type; + backendSrv: any; +} + +@inject('permissions') @observer -export class FolderPermissions extends Component { +export class FolderPermissions extends Component { constructor(props) { super(props); this.handleAddPermission = this.handleAddPermission.bind(this); } componentDidMount() { - this.loadStore(); + this.props.getFolderByUid(this.props.folderUid); } componentWillUnmount() { @@ -27,31 +40,23 @@ export class FolderPermissions extends Component { permissions.hideAddPermissions(); } - loadStore() { - const { nav, folder, view } = this.props; - return folder.load(view.routeParams.get('uid') as string).then(res => { - view.updatePathAndQuery(`${res.url}/permissions`, {}, {}); - return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions'); - }); - } - handleAddPermission() { const { permissions } = this.props; permissions.toggleAddPermissions(); } render() { - const { nav, folder, permissions, backendSrv } = this.props; + const { navModel, permissions, backendSrv, folder } = this.props; - if (!folder.folder || !nav.main) { - return

Loading

; + if (folder.id === 0) { + return ; } - const dashboardId = folder.folder.id; + const dashboardId = folder.id; return (
- +

Folder Permissions

@@ -77,4 +82,17 @@ export class FolderPermissions extends Component { } } -export default hot(module)(FolderPermissions); +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + return { + navModel: getNavModel(state.navIndex, `folder-permissions-${uid}`, getLoadingNav(1)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/FolderSettingsPage.test.tsx b/public/app/features/folders/FolderSettingsPage.test.tsx new file mode 100644 index 00000000000..3680fa9a197 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.test.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { FolderSettingsPage, Props } from './FolderSettingsPage'; +import { NavModel } from 'app/types'; +import { shallow } from 'enzyme'; + +const setup = (propOverrides?: object) => { + const props: Props = { + navModel: {} as NavModel, + folderUid: '1234', + folder: { + id: 0, + uid: '1234', + title: 'loading', + canSave: true, + url: 'url', + hasChanged: false, + version: 1, + }, + getFolderByUid: jest.fn(), + setFolderTitle: jest.fn(), + saveFolder: jest.fn(), + deleteFolder: jest.fn(), + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as FolderSettingsPage; + + return { + wrapper, + instance, + }; +}; + +describe('Render', () => { + it('should render component', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); + + it('should enable save button', () => { + const { wrapper } = setup({ + folder: { + id: 1, + uid: '1234', + title: 'loading', + canSave: true, + hasChanged: true, + version: 1, + }, + }); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx new file mode 100644 index 00000000000..1eb7ccafc65 --- /dev/null +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -0,0 +1,105 @@ +import React, { PureComponent } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import appEvents from 'app/core/app_events'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { NavModel, StoreState, FolderState } from 'app/types'; +import { getFolderByUid, setFolderTitle, saveFolder, deleteFolder } from './state/actions'; +import { getLoadingNav } from './state/navModel'; + +export interface Props { + navModel: NavModel; + folderUid: string; + folder: FolderState; + getFolderByUid: typeof getFolderByUid; + setFolderTitle: typeof setFolderTitle; + saveFolder: typeof saveFolder; + deleteFolder: typeof deleteFolder; +} + +export class FolderSettingsPage extends PureComponent { + componentDidMount() { + this.props.getFolderByUid(this.props.folderUid); + } + + onTitleChange = evt => { + this.props.setFolderTitle(evt.target.value); + }; + + onSave = async evt => { + evt.preventDefault(); + evt.stopPropagation(); + + await this.props.saveFolder(this.props.folder); + }; + + onDelete = evt => { + evt.stopPropagation(); + evt.preventDefault(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete this folder and all its dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.props.deleteFolder(this.props.folder.uid); + }, + }); + }; + + render() { + const { navModel, folder } = this.props; + + return ( +
+ +
+

Folder Settings

+ +
+
+
+ + +
+
+ + +
+ +
+
+
+ ); + } +} + +const mapStateToProps = (state: StoreState) => { + const uid = state.location.routeParams.uid; + + return { + navModel: getNavModel(state.navIndex, `folder-settings-${uid}`, getLoadingNav(2)), + folderUid: uid, + folder: state.folder, + }; +}; + +const mapDispatchToProps = { + getFolderByUid, + saveFolder, + setFolderTitle, + deleteFolder, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderSettingsPage)); diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap new file mode 100644 index 00000000000..2de0c193d27 --- /dev/null +++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap @@ -0,0 +1,131 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should enable save button 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; + +exports[`Render should render component 1`] = ` +
+ +
+

+ Folder Settings +

+
+
+
+ + +
+
+ + +
+ +
+
+
+`; diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts new file mode 100644 index 00000000000..5d153b2fb8a --- /dev/null +++ b/public/app/features/folders/state/actions.ts @@ -0,0 +1,67 @@ +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { FolderDTO, FolderState } from 'app/types'; +import { updateNavIndex, updateLocation } from 'app/core/actions'; +import { buildNavModel } from './navModel'; +import appEvents from 'app/core/app_events'; + +export enum ActionTypes { + LoadFolder = 'LOAD_FOLDER', + SetFolderTitle = 'SET_FOLDER_TITLE', + SaveFolder = 'SAVE_FOLDER', +} + +export interface LoadFolderAction { + type: ActionTypes.LoadFolder; + payload: FolderDTO; +} + +export interface SetFolderTitleAction { + type: ActionTypes.SetFolderTitle; + payload: string; +} + +export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({ + type: ActionTypes.LoadFolder, + payload: folder, +}); + +export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({ + type: ActionTypes.SetFolderTitle, + payload: newTitle, +}); + +export type Action = LoadFolderAction | SetFolderTitleAction; + +type ThunkResult = ThunkAction; + + +export function getFolderByUid(uid: string): ThunkResult { + return async dispatch => { + const folder = await getBackendSrv().getFolderByUid(uid); + dispatch(loadFolder(folder)); + dispatch(updateNavIndex(buildNavModel(folder))); + }; +} + +export function saveFolder(folder: FolderState): ThunkResult { + return async dispatch => { + const res = await getBackendSrv().put(`/api/folders/${folder.uid}`, { + title: folder.title, + version: folder.version, + }); + + // this should be redux action at some point + appEvents.emit('alert-success', ['Folder saved']); + + dispatch(updateLocation({ path: `${res.url}/settings` })); + }; +} + +export function deleteFolder(uid: string): ThunkResult { + return async dispatch => { + await getBackendSrv().deleteFolder(uid, true); + dispatch(updateLocation({ path: `dashboards` })); + }; +} diff --git a/public/app/features/folders/state/navModel.ts b/public/app/features/folders/state/navModel.ts new file mode 100644 index 00000000000..e6ef763d019 --- /dev/null +++ b/public/app/features/folders/state/navModel.ts @@ -0,0 +1,53 @@ +import { FolderDTO, NavModelItem, NavModel } from 'app/types'; + +export function buildNavModel(folder: FolderDTO): NavModelItem { + return { + icon: 'fa fa-folder-open', + id: 'manage-folder', + subTitle: 'Manage folder dashboards & permissions', + url: '', + text: folder.title, + breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], + children: [ + { + active: false, + icon: 'fa fa-fw fa-th-large', + id: `folder-dashboards-${folder.uid}`, + text: 'Dashboards', + url: folder.url, + }, + { + active: false, + icon: 'fa fa-fw fa-lock', + id: `folder-permissions-${folder.uid}`, + text: 'Permissions', + url: `${folder.url}/permissions`, + }, + { + active: false, + icon: 'fa fa-fw fa-cog', + id: `folder-settings-${folder.uid}`, + text: 'Settings', + url: `${folder.url}/settings`, + }, + ], + }; +} + +export function getLoadingNav(tabIndex: number): NavModel { + const main = buildNavModel({ + id: 1, + uid: 'loading', + title: 'Loading', + url: 'url', + canSave: false, + version: 0, + }); + + main.children[tabIndex].active = true; + + return { + main: main, + node: main.children[tabIndex], + }; +} diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts new file mode 100644 index 00000000000..ff37f13f97f --- /dev/null +++ b/public/app/features/folders/state/reducers.test.ts @@ -0,0 +1,42 @@ +import { Action, ActionTypes } from './actions'; +import { FolderDTO } from 'app/types'; +import { inititalState, folderReducer } from './reducers'; + +function getTestFolder(): FolderDTO { + return { + id: 1, + title: 'test folder', + uid: 'asd', + url: 'url', + canSave: true, + version: 0, + }; +} + +describe('folder reducer', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); + + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); + + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; + + const state = folderReducer(inititalState, action); + + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); +}); diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts new file mode 100644 index 00000000000..41ae10d19e5 --- /dev/null +++ b/public/app/features/folders/state/reducers.ts @@ -0,0 +1,33 @@ +import { FolderState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const inititalState: FolderState = { + id: 0, + uid: 'loading', + title: 'loading', + url: '', + canSave: false, + hasChanged: false, + version: 0, +}; + +export const folderReducer = (state = inititalState, action: Action): FolderState => { + switch (action.type) { + case ActionTypes.LoadFolder: + return { + ...action.payload, + hasChanged: false, + }; + case ActionTypes.SetFolderTitle: + return { + ...state, + title: action.payload, + hasChanged: action.payload.trim().length > 0, + }; + } + return state; +}; + +export default { + folder: folderReducer, +}; diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index f28bde518d2..bbc8b7013ca 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -7,10 +7,11 @@ import PageHeader from 'app/core/components/PageHeader/PageHeader'; import TeamMembers from './TeamMembers'; import TeamSettings from './TeamSettings'; import TeamGroupSync from './TeamGroupSync'; -import { NavModel, Team } from '../../types'; +import { NavModel, Team } from 'app/types'; import { loadTeam } from './state/actions'; import { getTeam } from './state/selectors'; -import { getNavModel } from '../../core/selectors/navModel'; +import { getTeamLoadingNav } from './state/navModel'; +import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; export interface Props { @@ -89,9 +90,10 @@ export class TeamPages extends PureComponent { function mapStateToProps(state) { const teamId = getRouteParamsId(state.location); const pageName = getRouteParamsPage(state.location) || 'members'; + const teamLoadingNav = getTeamLoadingNav(pageName); return { - navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`), + navModel: getNavModel(state.navIndex, `team-${pageName}-${teamId}`, teamLoadingNav), teamId: teamId, pageName: pageName, team: getTeam(state.team, teamId), diff --git a/public/app/features/teams/state/actions.ts b/public/app/features/teams/state/actions.ts index 91aa899e171..d948dc1c5a3 100644 --- a/public/app/features/teams/state/actions.ts +++ b/public/app/features/teams/state/actions.ts @@ -1,8 +1,8 @@ import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; -import { NavModelItem, StoreState, Team, TeamGroup, TeamMember } from 'app/types'; +import { StoreState, Team, TeamGroup, TeamMember } from 'app/types'; import { updateNavIndex, UpdateNavIndexAction } from 'app/core/actions'; -import config from 'app/core/config'; +import { buildNavModel } from './navModel'; export enum ActionTypes { LoadTeams = 'LOAD_TEAMS', @@ -90,148 +90,73 @@ export function loadTeams(): ThunkResult { }; } -function buildNavModel(team: Team): NavModelItem { - const navModel = { - img: team.avatarUrl, - id: 'team-' + team.id, - subTitle: 'Manage members & settings', - url: '', - text: team.name, - breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], - children: [ - { - active: false, - icon: 'gicon gicon-team', - id: `team-members-${team.id}`, - text: 'Members', - url: `org/teams/edit/${team.id}/members`, - }, - { - active: false, - icon: 'fa fa-fw fa-sliders', - id: `team-settings-${team.id}`, - text: 'Settings', - url: `org/teams/edit/${team.id}/settings`, - }, - ], - }; - - if (config.buildInfo.isEnterprise) { - navModel.children.push({ - active: false, - icon: 'fa fa-fw fa-refresh', - id: `team-groupsync-${team.id}`, - text: 'External group sync', - url: `org/teams/edit/${team.id}/groupsync`, - }); - } - - return navModel; -} - export function loadTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .get(`/api/teams/${id}`) - .then(response => { - dispatch(teamLoaded(response)); - dispatch(updateNavIndex(buildNavModel(response))); - }); + const response = await getBackendSrv().get(`/api/teams/${id}`); + dispatch(teamLoaded(response)); + dispatch(updateNavIndex(buildNavModel(response))); }; } export function loadTeamMembers(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/members`) - .then(response => { - dispatch(teamMembersLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/members`); + dispatch(teamMembersLoaded(response)); }; } export function addTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/members`, { userId: id }) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/members`, { userId: id }); + dispatch(loadTeamMembers()); }; } export function removeTeamMember(id: number): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/members/${id}`) - .then(() => { - dispatch(loadTeamMembers()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/members/${id}`); + dispatch(loadTeamMembers()); }; } export function updateTeam(name: string, email: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - await getBackendSrv() - .put(`/api/teams/${team.id}`, { - name, - email, - }) - .then(() => { - dispatch(loadTeam(team.id)); - }); + await getBackendSrv().put(`/api/teams/${team.id}`, { name, email }); + dispatch(loadTeam(team.id)); }; } export function loadTeamGroups(): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .get(`/api/teams/${team.id}/groups`) - .then(response => { - dispatch(teamGroupsLoaded(response)); - }); + const response = await getBackendSrv().get(`/api/teams/${team.id}/groups`); + dispatch(teamGroupsLoaded(response)); }; } export function addTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .post(`/api/teams/${team.id}/groups`, { groupId: groupId }) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().post(`/api/teams/${team.id}/groups`, { groupId: groupId }); + dispatch(loadTeamGroups()); }; } export function removeTeamGroup(groupId: string): ThunkResult { return async (dispatch, getStore) => { const team = getStore().team.team; - - await getBackendSrv() - .delete(`/api/teams/${team.id}/groups/${groupId}`) - .then(() => { - dispatch(loadTeamGroups()); - }); + await getBackendSrv().delete(`/api/teams/${team.id}/groups/${groupId}`); + dispatch(loadTeamGroups()); }; } export function deleteTeam(id: number): ThunkResult { return async dispatch => { - await getBackendSrv() - .delete(`/api/teams/${id}`) - .then(() => { - dispatch(loadTeams()); - }); + await getBackendSrv().delete(`/api/teams/${id}`); + dispatch(loadTeams()); }; } diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts new file mode 100644 index 00000000000..2fd5a68e680 --- /dev/null +++ b/public/app/features/teams/state/navModel.ts @@ -0,0 +1,67 @@ +import { Team, NavModelItem, NavModel } from 'app/types'; +import config from 'app/core/config'; + +export function buildNavModel(team: Team): NavModelItem { + const navModel = { + img: team.avatarUrl, + id: 'team-' + team.id, + subTitle: 'Manage members & settings', + url: '', + text: team.name, + breadcrumbs: [{ title: 'Teams', url: 'org/teams' }], + children: [ + { + active: false, + icon: 'gicon gicon-team', + id: `team-members-${team.id}`, + text: 'Members', + url: `org/teams/edit/${team.id}/members`, + }, + { + active: false, + icon: 'fa fa-fw fa-sliders', + id: `team-settings-${team.id}`, + text: 'Settings', + url: `org/teams/edit/${team.id}/settings`, + }, + ], + }; + + if (config.buildInfo.isEnterprise) { + navModel.children.push({ + active: false, + icon: 'fa fa-fw fa-refresh', + id: `team-groupsync-${team.id}`, + text: 'External group sync', + url: `org/teams/edit/${team.id}/groupsync`, + }); + } + + return navModel; +} + +export function getTeamLoadingNav(pageName: string): NavModel { + const main = buildNavModel({ + avatarUrl: 'public/img/user_profile.png', + id: 1, + name: 'Loading', + email: 'loading', + memberCount: 0, + }); + + let node: NavModelItem; + + // find active page + for (const child of main.children) { + if (child.id.indexOf(pageName) > 0) { + child.active = true; + node = child; + break; + } + } + + return { + main: main, + node: node, + }; +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 519008d70f5..160250dce96 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,10 +3,10 @@ import './ReactContainer'; import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; -import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; import TeamPages from 'app/features/teams/TeamPages'; import TeamList from 'app/features/teams/TeamList'; -import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; +import FolderSettingsPage from 'app/features/folders/FolderSettingsPage'; +import FolderPermissions from 'app/features/folders/FolderPermissions'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -99,7 +99,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboards/f/:uid/:slug/settings', { template: '', resolve: { - component: () => FolderSettings, + component: () => FolderSettingsPage, }, }) .when('/dashboards/f/:uid/:slug', { diff --git a/public/app/stores/FolderStore/FolderStore.ts b/public/app/stores/FolderStore/FolderStore.ts deleted file mode 100644 index 90932cbe46f..00000000000 --- a/public/app/stores/FolderStore/FolderStore.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; - -export const Folder = types.model('Folder', { - id: types.identifier(types.number), - uid: types.string, - title: types.string, - url: types.string, - canSave: types.boolean, - hasChanged: types.boolean, - version: types.number, -}); - -export const FolderStore = types - .model('FolderStore', { - folder: types.maybe(Folder), - }) - .actions(self => ({ - load: flow(function* load(uid: string) { - // clear folder state - if (self.folder && self.folder.uid !== uid) { - self.folder = null; - } - - const backendSrv = getEnv(self).backendSrv; - const res = yield backendSrv.getFolderByUid(uid); - self.folder = Folder.create({ - id: res.id, - uid: res.uid, - title: res.title, - url: res.url, - canSave: res.canSave, - hasChanged: false, - version: res.version, - }); - - return res; - }), - - setTitle: (originalTitle: string, title: string) => { - self.folder.title = title; - self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0; - }, - - saveFolder: flow(function* saveFolder(options: any) { - const backendSrv = getEnv(self).backendSrv; - self.folder.title = self.folder.title.trim(); - - const res = yield backendSrv.updateFolder(self.folder, options); - self.folder.url = res.url; - self.folder.version = res.version; - - return `${self.folder.url}/settings`; - }), - - deleteFolder: flow(function* deleteFolder() { - const backendSrv = getEnv(self).backendSrv; - - return backendSrv.deleteFolder(self.folder.uid); - }), - })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts index 37c13f48c61..68125fd1f4c 100644 --- a/public/app/stores/RootStore/RootStore.ts +++ b/public/app/stores/RootStore/RootStore.ts @@ -1,7 +1,6 @@ import { types } from 'mobx-state-tree'; import { NavStore } from './../NavStore/NavStore'; import { ViewStore } from './../ViewStore/ViewStore'; -import { FolderStore } from './../FolderStore/FolderStore'; import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; export const RootStore = types.model({ @@ -15,7 +14,6 @@ export const RootStore = types.model({ query: {}, routeParams: {}, }), - folder: types.optional(FolderStore, {}), }); type RootStoreType = typeof RootStore.Type; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index 0cdc07fd31a..e06317853f8 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -4,11 +4,13 @@ import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; +import foldersReducers from 'app/features/folders/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, + ...foldersReducers, }); export let store; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts new file mode 100644 index 00000000000..6fbe79cce8c --- /dev/null +++ b/public/app/types/folder.ts @@ -0,0 +1,18 @@ +export interface FolderDTO { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; +} + +export interface FolderState { + id: number; + uid: string; + title: string; + url: string; + version: number; + canSave: boolean; + hasChanged: boolean; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 92bcdb32836..52d1ba592c5 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,6 +2,7 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; +import { FolderDTO, FolderState } from './folder'; export { Team, @@ -19,6 +20,8 @@ export { NavIndex, UrlQueryMap, UrlQueryValue, + FolderDTO, + FolderState, }; export interface StoreState { @@ -27,4 +30,5 @@ export interface StoreState { alertRules: AlertRulesState; teams: TeamsState; team: TeamState; + folder: FolderState; } diff --git a/yarn.lock b/yarn.lock index fa079d15b72..2b98ff32766 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3182,7 +3182,7 @@ debug@^3.1.0: dependencies: ms "^2.1.1" -debuglog@*, debuglog@^1.0.1: +debuglog@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" @@ -5553,7 +5553,7 @@ import-local@^2.0.0: pkg-dir "^3.0.0" resolve-cwd "^2.0.0" -imurmurhash@*, imurmurhash@^0.1.4: +imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -6990,10 +6990,6 @@ lodash-es@^4.17.5: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" -lodash._baseindexof@*: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c" - lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7001,25 +6997,11 @@ lodash._baseuniq@~4.6.0: lodash._createset "~4.0.0" lodash._root "~3.0.0" -lodash._bindcallback@*: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._cacheindexof@*: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92" - -lodash._createcache@*: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093" - dependencies: - lodash._getnative "^3.0.0" - lodash._createset@~4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" -lodash._getnative@*, lodash._getnative@^3.0.0: +lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -7103,10 +7085,6 @@ lodash.mergewith@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" -lodash.restparam@*: - version "3.6.1" - resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" - lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -9902,7 +9880,7 @@ readable-stream@~1.1.10: isarray "0.0.1" string_decoder "~0.10.x" -readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0: +readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" dependencies: From bae560717d6f1c7694b0817a997ec278c83ee14e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:23:43 +0200 Subject: [PATCH 040/127] fix: fixed tslint issue introduced in recent prometheus PR merge --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 9332a73caca..5674e1353a1 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -579,7 +579,7 @@ export class PrometheusDatasource { } getTimeRange(): { start: number; end: number } { - let range = this.timeSrv.timeRange(); + const range = this.timeSrv.timeRange(); return { start: this.getPrometheusTime(range.from, false), end: this.getPrometheusTime(range.to, true), From e1a1da9064b7ffd7b22764a99a800b04f7cbc747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 14:31:12 +0200 Subject: [PATCH 041/127] fix: add folder permission fix --- public/app/features/folders/state/actions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 4f15f813a68..df81acb91eb 100644 --- a/public/app/features/folders/state/actions.ts +++ b/public/app/features/folders/state/actions.ts @@ -157,8 +157,8 @@ export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult Date: Thu, 13 Sep 2018 15:15:42 +0200 Subject: [PATCH 042/127] renames PartialMatch to MatchAny --- pkg/api/annotations.go | 22 +++++++++---------- pkg/services/annotations/annotations.go | 2 +- pkg/services/sqlstore/annotation.go | 2 +- pkg/services/sqlstore/annotation_test.go | 10 ++++----- .../plugins/datasource/grafana/datasource.ts | 2 +- .../grafana/partials/annotations.editor.html | 10 ++++----- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index eec07bb9f81..242b5531f51 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -14,17 +14,17 @@ import ( func GetAnnotations(c *m.ReqContext) Response { query := &annotations.ItemQuery{ - From: c.QueryInt64("from"), - To: c.QueryInt64("to"), - OrgId: c.OrgId, - UserId: c.QueryInt64("userId"), - AlertId: c.QueryInt64("alertId"), - DashboardId: c.QueryInt64("dashboardId"), - PanelId: c.QueryInt64("panelId"), - Limit: c.QueryInt64("limit"), - Tags: c.QueryStrings("tags"), - Type: c.Query("type"), - PartialMatch: c.QueryBool("partialMatch"), + From: c.QueryInt64("from"), + To: c.QueryInt64("to"), + OrgId: c.OrgId, + UserId: c.QueryInt64("userId"), + AlertId: c.QueryInt64("alertId"), + DashboardId: c.QueryInt64("dashboardId"), + PanelId: c.QueryInt64("panelId"), + Limit: c.QueryInt64("limit"), + Tags: c.QueryStrings("tags"), + Type: c.Query("type"), + MatchAny: c.QueryBool("matchAny"), } repo := annotations.GetRepository() diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index daea43863f4..60a92aa897a 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -21,7 +21,7 @@ type ItemQuery struct { RegionId int64 `json:"regionId"` Tags []string `json:"tags"` Type string `json:"type"` - PartialMatch bool `json:"partialMatch"` + MatchAny bool `json:"matchAny"` Limit int64 `json:"limit"` } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 6e25ce432f3..ceafaaad0e3 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -211,7 +211,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ) `, strings.Join(keyValueFilters, " OR ")) - if query.PartialMatch { + if query.MatchAny { sql.WriteString(fmt.Sprintf(" AND (%s) > 0 ", tagsSubQuery)) } else { sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags))) diff --git a/pkg/services/sqlstore/annotation_test.go b/pkg/services/sqlstore/annotation_test.go index 4c31e0442c8..d3459527e7d 100644 --- a/pkg/services/sqlstore/annotation_test.go +++ b/pkg/services/sqlstore/annotation_test.go @@ -199,11 +199,11 @@ func TestAnnotations(t *testing.T) { Convey("Should find two annotations using partial match", func() { items, err := repo.Find(&annotations.ItemQuery{ - OrgId: 1, - From: 1, - To: 25, - PartialMatch: true, - Tags: []string{"rollback", "deploy"}, + OrgId: 1, + From: 1, + To: 25, + MatchAny: true, + Tags: []string{"rollback", "deploy"}, }) So(err, ShouldBeNil) diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 4ddfa8df40d..3bf772d160c 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -40,7 +40,7 @@ class GrafanaDatasource { to: options.range.to.valueOf(), limit: options.annotation.limit, tags: options.annotation.tags, - partialMatch: options.annotation.partialMatch, + matchAny: options.annotation.matchAny, }; if (options.annotation.type === 'dashboard') { diff --git a/public/app/plugins/datasource/grafana/partials/annotations.editor.html b/public/app/plugins/datasource/grafana/partials/annotations.editor.html index ba68a08cefd..e5a67d6a7dc 100644 --- a/public/app/plugins/datasource/grafana/partials/annotations.editor.html +++ b/public/app/plugins/datasource/grafana/partials/annotations.editor.html @@ -26,11 +26,11 @@
-
From c7fdea1dfb3e245a031023c1b440c52f9af78092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 16:00:02 +0200 Subject: [PATCH 043/127] wip: dashboard permissions to redux --- public/app/core/actions/permissions.ts | 24 ---- public/app/core/angular_wrappers.ts | 2 - public/app/core/reducers/processsAclItems.ts | 31 +++++ public/app/core/utils/acl.ts | 31 +++++ .../DashboardPermissions.tsx | 106 ++++++++++++++++ public/app/features/dashboard/all.ts | 6 + .../app/features/dashboard/state/actions.ts | 115 ++++++++++++++++++ .../app/features/dashboard/state/reducers.ts | 22 ++++ public/app/features/folders/state/reducers.ts | 32 +---- public/app/stores/configureStore.ts | 2 + public/app/types/dashboard.ts | 5 + public/app/types/index.ts | 2 + 12 files changed, 321 insertions(+), 57 deletions(-) delete mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/reducers/processsAclItems.ts create mode 100644 public/app/core/utils/acl.ts create mode 100644 public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx create mode 100644 public/app/features/dashboard/state/actions.ts create mode 100644 public/app/features/dashboard/state/reducers.ts create mode 100644 public/app/types/dashboard.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts deleted file mode 100644 index 2b07b7145dd..00000000000 --- a/public/app/core/actions/permissions.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { DashboardAcl } from '../../types'; - -export enum ActionTypes { - LoadFolderPermissions = 'LoadFolderPermissions', -} - -export interface LoadFolderPermissionsAction { - type: ActionTypes.LoadFolderPermissions; - payload: DashboardAcl[]; -} - -export type Action = LoadFolderPermissions; - -export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({ - type: ActionTypes.LoadFolderPermissions, - payload: items, -}); - -export function getFolderPermissions(uid: string): ThunkResult { - return async dispatch => { - const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); - dispatch(loadFolderPermissions(permissions)); - }; -} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 18e9d8dbd84..6974d40aac8 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; import { SideMenu } from './components/sidemenu/SideMenu'; -import DashboardPermissions from './components/Permissions/DashboardPermissions'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); @@ -18,5 +17,4 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); } diff --git a/public/app/core/reducers/processsAclItems.ts b/public/app/core/reducers/processsAclItems.ts new file mode 100644 index 00000000000..57578d6b2d8 --- /dev/null +++ b/public/app/core/reducers/processsAclItems.ts @@ -0,0 +1,31 @@ +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; + +export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} diff --git a/public/app/core/utils/acl.ts b/public/app/core/utils/acl.ts new file mode 100644 index 00000000000..57578d6b2d8 --- /dev/null +++ b/public/app/core/utils/acl.ts @@ -0,0 +1,31 @@ +import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; + +export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { + return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); +} + +function processAclItem(dto: DashboardAclDTO): DashboardAcl { + const item = dto as DashboardAcl; + + item.sortRank = 0; + if (item.userId > 0) { + item.name = item.userLogin; + item.sortRank = 10; + } else if (item.teamId > 0) { + item.name = item.team; + item.sortRank = 20; + } else if (item.role) { + item.icon = 'fa fa-fw fa-street-view'; + item.name = item.role; + item.sortRank = 30; + if (item.role === 'Editor') { + item.sortRank += 1; + } + } + + if (item.inherited) { + item.sortRank += 100; + } + + return item; +} diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx new file mode 100644 index 00000000000..ad7d9c7f504 --- /dev/null +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -0,0 +1,106 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import Tooltip from 'app/core/components/Tooltip/Tooltip'; +import SlideDown from 'app/core/components/Animations/SlideDown'; +import { StoreState, FolderInfo } from 'app/types'; +import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; +import { getDashboardPermissions } from '../state/actions'; +import PermissionList from 'app/core/components/PermissionList/PermissionList'; +import AddPermission from 'app/core/components/PermissionList/AddPermission'; +import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import { store } from 'app/stores/configureStore'; + +export interface Props { + dashboardId: number; + folder?: FolderInfo; + getDashboardPermissions: typeof getDashboardPermissions; + permissions: DashboardAcl[]; +} + +export interface State { + isAdding: boolean; +} + +export class DashboardPermissions extends PureComponent { + constructor(props) { + super(props); + + this.state = { + isAdding: false, + }; + } + + componentDidMount() { + this.props.getDashboardPermissions(this.props.dashboardId); + } + + onOpenAddPermissions = () => { + this.setState({ isAdding: true }); + }; + + onRemoveItem = (item: DashboardAcl) => { + // this.props.removeFolderPermission(item); + }; + + onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { + // this.props.updateFolderPermission(item, level); + }; + + onAddPermission = (newItem: NewDashboardAclItem) => { + // return this.props.addFolderPermission(newItem); + }; + + onCancelAddPermission = () => { + this.setState({ isAdding: false }); + }; + + render() { + const { permissions, folder } = this.props; + const { isAdding } = this.state; + console.log('DashboardPermissions', this.props); + + return ( +
+
+
+

Permissions

+ + + +
+ +
+
+ + + + +
+ ); + } +} + +function connectWithStore(WrappedComponent, ...args) { + const ConnectedWrappedComponent = connect(...args)(WrappedComponent); + return props => { + return ; + }; +} + +const mapStateToProps = (state: StoreState) => ({ + permissions: state.dashboard.permissions, +}); + +const mapDispatchToProps = { + getDashboardPermissions, +}; + +export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index adb665c47b5..817ed83aaa0 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -30,6 +30,12 @@ import './settings/settings'; import './panellinks/module'; import './dashlinks/module'; +// angular wrappers +import { react2AngularDirective } from 'app/core/utils/react2angular'; +import DashboardPermissions from './DashboardPermissions/DashboardPermissions'; + +react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']); + import coreModule from 'app/core/core_module'; import { FolderDashboardsCtrl } from './folder_dashboards_ctrl'; import { DashboardImportCtrl } from './dashboard_import_ctrl'; diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts new file mode 100644 index 00000000000..b1d25d1f57f --- /dev/null +++ b/public/app/features/dashboard/state/actions.ts @@ -0,0 +1,115 @@ +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +import { getBackendSrv } from 'app/core/services/backend_srv'; + +import { + DashboardAcl, + DashboardAclDTO, + PermissionLevel, + DashboardAclUpdateDTO, + NewDashboardAclItem, +} from 'app/types/acl'; + +export enum ActionTypes { + LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS', +} + +export interface LoadDashboardPermissionsAction { + type: ActionTypes.LoadDashboardPermissions; + payload: DashboardAcl[]; +} + +export type Action = LoadDashboardPermissionsAction; + +type ThunkResult = ThunkAction; + +export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({ + type: ActionTypes.LoadDashboardPermissions, + payload: items, +}); + +export function getDashboardPermissions(id: number): ThunkResult { + return async dispatch => { + const permissions = await getBackendSrv().get(`/api/dashboards/id/${id}/permissions`); + dispatch(loadDashboardPermissions(permissions)); + }; +} + +function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO { + return { + userId: item.userId, + teamId: item.teamId, + role: item.role, + permission: item.permission, + }; +} + +export function updateDashboardPermission( + dashboardId: number, + itemToUpdate: DashboardAcl, + level: PermissionLevel +): ThunkResult { + return async (dispatch, getStore) => { + const { dashboard } = getStore(); + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited) { + continue; + } + + const updated = toUpdateItem(itemToUpdate); + + // if this is the item we want to update, update it's permisssion + if (itemToUpdate === item) { + updated.permission = level; + } + + itemsToUpdate.push(updated); + } + + await getBackendSrv().post(`/api/dashboard/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} + +export function removeDashboardPermission(dashboardId: number, itemToDelete: DashboardAcl): ThunkResult { + return async (dispatch, getStore) => { + const dashboard = getStore().dashboard; + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited || item === itemToDelete) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} + +export function addDashboardPermission(dashboardId: number, newItem: NewDashboardAclItem): ThunkResult { + return async (dispatch, getStore) => { + const { dashboard } = getStore(); + const itemsToUpdate = []; + + for (const item of dashboard.permissions) { + if (item.inherited) { + continue; + } + itemsToUpdate.push(toUpdateItem(item)); + } + + itemsToUpdate.push({ + userId: newItem.userId, + teamId: newItem.teamId, + role: newItem.role, + permission: newItem.permission, + }); + + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await dispatch(getDashboardPermissions(dashboardId)); + }; +} diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts new file mode 100644 index 00000000000..5100529d973 --- /dev/null +++ b/public/app/features/dashboard/state/reducers.ts @@ -0,0 +1,22 @@ +import { DashboardState } from 'app/types'; +import { Action, ActionTypes } from './actions'; +import { processAclItems } from 'app/core/utils/acl'; + +export const inititalState: DashboardState = { + permissions: [], +}; + +export const dashboardReducer = (state = inititalState, action: Action): DashboardState => { + switch (action.type) { + case ActionTypes.LoadDashboardPermissions: + return { + ...state, + permissions: processAclItems(action.payload), + }; + } + return state; +}; + +export default { + dashboard: dashboardReducer, +}; diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 9b73312790c..4560c999659 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/public/app/features/folders/state/reducers.ts @@ -1,6 +1,6 @@ import { FolderState } from 'app/types'; -import { DashboardAcl, DashboardAclDTO } from 'app/types/acl'; import { Action, ActionTypes } from './actions'; +import { processAclItems } from 'app/core/utils/acl'; export const inititalState: FolderState = { id: 0, @@ -36,36 +36,6 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat return state; }; -function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] { - return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); -} - -function processAclItem(dto: DashboardAclDTO): DashboardAcl { - const item = dto as DashboardAcl; - - item.sortRank = 0; - if (item.userId > 0) { - item.name = item.userLogin; - item.sortRank = 10; - } else if (item.teamId > 0) { - item.name = item.team; - item.sortRank = 20; - } else if (item.role) { - item.icon = 'fa fa-fw fa-street-view'; - item.name = item.role; - item.sortRank = 30; - if (item.role === 'Editor') { - item.sortRank += 1; - } - } - - if (item.inherited) { - item.sortRank += 100; - } - - return item; -} - export default { folder: folderReducer, }; diff --git a/public/app/stores/configureStore.ts b/public/app/stores/configureStore.ts index e06317853f8..8f6cf25043d 100644 --- a/public/app/stores/configureStore.ts +++ b/public/app/stores/configureStore.ts @@ -5,12 +5,14 @@ import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; import foldersReducers from 'app/features/folders/state/reducers'; +import dashboardReducers from 'app/features/dashboard/state/reducers'; const rootReducer = combineReducers({ ...sharedReducers, ...alertingReducers, ...teamsReducers, ...foldersReducers, + ...dashboardReducers, }); export let store; diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts new file mode 100644 index 00000000000..d33405c985e --- /dev/null +++ b/public/app/types/dashboard.ts @@ -0,0 +1,5 @@ +import { DashboardAcl } from './acl'; + +export interface DashboardState { + permissions: DashboardAcl[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 6f052c7c503..f2fe165a863 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -3,6 +3,7 @@ import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { DashboardState } from './dashboard'; export { Team, @@ -32,4 +33,5 @@ export interface StoreState { teams: TeamsState; team: TeamState; folder: FolderState; + dashboard: DashboardState; } From bff350166ef1f37e59625eb6b0d0dc287f36b6ef Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 13 Sep 2018 14:36:16 +0200 Subject: [PATCH 044/127] disabling internal metrics disables /metric endpoint but we will still keep sending metrics to graphite closes #10638 --- pkg/api/http_server.go | 4 ++++ pkg/metrics/service.go | 1 - pkg/metrics/settings.go | 5 ----- pkg/setting/setting.go | 3 +++ 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 0de63ce5e08..432d6a18369 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -233,6 +233,10 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { } func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { + if !hs.Cfg.MetricsEndpointEnabled { + return + } + if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/metrics" { return } diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go index ec38e0acfec..3d7fa6a1269 100644 --- a/pkg/metrics/service.go +++ b/pkg/metrics/service.go @@ -28,7 +28,6 @@ func init() { type InternalMetricsService struct { Cfg *setting.Cfg `inject:""` - enabled bool intervalSeconds int64 graphiteCfg *graphitebridge.Config } diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index 58b84a7192f..048e4134690 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -16,13 +16,8 @@ func (im *InternalMetricsService) readSettings() error { return fmt.Errorf("Unable to find metrics config section %v", err) } - im.enabled = section.Key("enabled").MustBool(false) im.intervalSeconds = section.Key("interval_seconds").MustInt64(10) - if !im.enabled { - return nil - } - if err := im.parseGraphiteSettings(); err != nil { return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index fb23a192a85..1a253b9b238 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -203,6 +203,8 @@ type Cfg struct { DisableBruteForceLoginProtection bool TempDataLifetime time.Duration + + MetricsEndpointEnabled bool } type CommandLineArgs struct { @@ -659,6 +661,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.ImagesDir = filepath.Join(DataPath, "png") cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) + cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) analytics := iniFile.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) From 1d66f9a42c697e829b71e8d468c5e6d233a9d912 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 12 Sep 2018 19:27:21 +0200 Subject: [PATCH 045/127] anonymous usage stats for authentication types --- pkg/metrics/metrics.go | 20 +++++++++++++++++++- pkg/metrics/metrics_test.go | 29 ++++++++++++++++++++++++++--- pkg/metrics/service.go | 3 ++- pkg/metrics/settings.go | 4 ++++ pkg/social/social.go | 26 ++++++++++++++++++++++++-- 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index dcdfbf124e1..e2cdb5656b0 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -350,7 +350,7 @@ func getEdition() string { } } -func sendUsageStats() { +func sendUsageStats(oauthProviders map[string]bool) { if !setting.ReportingEnabled { return } @@ -450,6 +450,24 @@ func sendUsageStats() { metrics["stats.alert_notifiers."+stats.Type+".count"] = stats.Count } + authTypes := map[string]bool{} + authTypes["anonymous"] = setting.AnonymousEnabled + authTypes["basic_auth"] = setting.BasicAuthEnabled + authTypes["ldap"] = setting.LdapEnabled + authTypes["auth_proxy"] = setting.AuthProxyEnabled + + for provider, enabled := range oauthProviders { + authTypes["oauth_"+provider] = enabled + } + + for authType, enabled := range authTypes { + enabledValue := 0 + if enabled { + enabledValue = 1 + } + metrics["stats.auth_enabled."+authType+".count"] = enabledValue + } + out, _ := json.MarshalIndent(report, "", " ") data := bytes.NewBuffer(out) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 9fbfd0c26a2..43739221f1e 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -147,11 +147,19 @@ func TestMetrics(t *testing.T) { })) usageStatsURL = ts.URL - sendUsageStats() + oauthProviders := map[string]bool{ + "github": true, + "gitlab": true, + "google": true, + "generic_oauth": true, + "grafana_com": true, + } + + sendUsageStats(oauthProviders) Convey("Given reporting not enabled and sending usage stats", func() { setting.ReportingEnabled = false - sendUsageStats() + sendUsageStats(oauthProviders) Convey("Should not gather stats or call http endpoint", func() { So(getSystemStatsQuery, ShouldBeNil) @@ -164,8 +172,13 @@ func TestMetrics(t *testing.T) { Convey("Given reporting enabled and sending usage stats", func() { setting.ReportingEnabled = true setting.BuildVersion = "5.0.0" + setting.AnonymousEnabled = true + setting.BasicAuthEnabled = true + setting.LdapEnabled = true + setting.AuthProxyEnabled = true + wg.Add(1) - sendUsageStats() + sendUsageStats(oauthProviders) Convey("Should gather stats and call http endpoint", func() { if waitTimeout(&wg, 2*time.Second) { @@ -220,6 +233,16 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.alert_notifiers.slack.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.alert_notifiers.webhook.count").MustInt(), ShouldEqual, 2) + + So(metrics.Get("stats.auth_enabled.anonymous.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.basic_auth.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.ldap.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.auth_proxy.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_github.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_gitlab.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1) + So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1) }) }) diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go index ec38e0acfec..3e66f8686c1 100644 --- a/pkg/metrics/service.go +++ b/pkg/metrics/service.go @@ -31,6 +31,7 @@ type InternalMetricsService struct { enabled bool intervalSeconds int64 graphiteCfg *graphitebridge.Config + oauthProviders map[string]bool } func (im *InternalMetricsService) Init() error { @@ -61,7 +62,7 @@ func (im *InternalMetricsService) Run(ctx context.Context) error { for { select { case <-onceEveryDayTick.C: - sendUsageStats() + sendUsageStats(im.oauthProviders) case <-everyMinuteTicker.C: updateTotalStats() case <-ctx.Done(): diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index 58b84a7192f..ed4b9fe09de 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -5,6 +5,8 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/social" + "github.com/grafana/grafana/pkg/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" @@ -27,6 +29,8 @@ func (im *InternalMetricsService) readSettings() error { return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } + im.oauthProviders = social.GetOAuthProviders(im.Cfg) + return nil } diff --git a/pkg/social/social.go b/pkg/social/social.go index e96b67fe031..721070ab789 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -49,14 +49,13 @@ func (e *Error) Error() string { var ( SocialBaseUrl = "/login/" SocialMap = make(map[string]SocialConnector) + allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} ) func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} - for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) info := &setting.OAuthInfo{ @@ -184,3 +183,26 @@ func NewOAuthService() { } } } + +// GetOAuthProviders returns available oauth providers and if they're enabled or not +var GetOAuthProviders = func(cfg *setting.Cfg) map[string]bool { + result := map[string]bool{} + + if cfg == nil || cfg.Raw == nil { + return result + } + + for _, name := range allOauthes { + if name == "grafananet" { + name = "grafana_com" + } + + sec := cfg.Raw.Section("auth." + name) + if sec == nil { + continue + } + result[name] = sec.Key("enabled").MustBool() + } + + return result +} From 0254a29e35e9404ccc0ccfdc06af8dcac0cd508f Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Thu, 13 Sep 2018 16:51:00 +0200 Subject: [PATCH 046/127] Interpolate $__interval in backend for alerting with sql datasources (#13156) add support for interpolate $__interval and $__interval_ms in sql datasources --- pkg/tsdb/elasticsearch/client/client.go | 2 +- pkg/tsdb/influxdb/query.go | 3 +- pkg/tsdb/interval.go | 4 ++ pkg/tsdb/mssql/macros.go | 22 ++--------- pkg/tsdb/mssql/mssql_test.go | 40 ++++++++++++++++++++ pkg/tsdb/mysql/macros.go | 23 ++---------- pkg/tsdb/mysql/mysql_test.go | 40 ++++++++++++++++++++ pkg/tsdb/postgres/macros.go | 26 +++---------- pkg/tsdb/postgres/postgres_test.go | 40 ++++++++++++++++++++ pkg/tsdb/sql_engine.go | 50 ++++++++++++++++++++++++- pkg/tsdb/sql_engine_test.go | 31 +++++++++++++++ 11 files changed, 218 insertions(+), 63 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index dff626a79eb..78973b3faa6 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -138,7 +138,7 @@ func (c *baseClientImpl) encodeBatchRequests(requests []*multiRequest) ([]byte, } body := string(reqBody) - body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) + body = strings.Replace(body, "$__interval_ms", strconv.FormatInt(r.interval.Milliseconds(), 10), -1) body = strings.Replace(body, "$__interval", r.interval.Text, -1) payload.WriteString(body + "\n") diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 0637a5bbb44..7cb8f0ecd82 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -4,7 +4,6 @@ import ( "fmt" "strconv" "strings" - "time" "regexp" @@ -34,7 +33,7 @@ func (query *Query) Build(queryContext *tsdb.TsdbQuery) (string, error) { res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), -1) res = strings.Replace(res, "$interval", interval.Text, -1) - res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) + res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1) res = strings.Replace(res, "$__interval", interval.Text, -1) return res, nil } diff --git a/pkg/tsdb/interval.go b/pkg/tsdb/interval.go index 49904f27a37..fd6adee39d7 100644 --- a/pkg/tsdb/interval.go +++ b/pkg/tsdb/interval.go @@ -49,6 +49,10 @@ func NewIntervalCalculator(opt *IntervalOptions) *intervalCalculator { return calc } +func (i *Interval) Milliseconds() int64 { + return i.Value.Nanoseconds() / int64(time.Millisecond) +} + func (ic *intervalCalculator) Calculate(timerange *TimeRange, minInterval time.Duration) Interval { to := timerange.MustGetTo().UnixNano() from := timerange.MustGetFrom().UnixNano() diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index caba043e7b6..9303712a480 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -13,12 +13,13 @@ const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type msSqlMacroEngine struct { + *tsdb.SqlMacroEngineBase timeRange *tsdb.TimeRange query *tsdb.Query } func newMssqlMacroEngine() tsdb.SqlMacroEngine { - return &msSqlMacroEngine{} + return &msSqlMacroEngine{SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase()} } func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -27,7 +28,7 @@ func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa rExp, _ := regexp.Compile(sExpr) var macroError error - sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") @@ -47,23 +48,6 @@ func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa return sql, nil } -func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + repl(groups) - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 30d1da3bda1..f9525fc37ac 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -35,6 +35,11 @@ func TestMSSQL(t *testing.T) { return x, nil } + origInterpolate := tsdb.Interpolate + tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + return sql, nil + } + endpoint, err := newMssqlQueryEndpoint(&models.DataSource{ JsonData: simplejson.New(), SecureJsonData: securejsondata.SecureJsonData{}, @@ -47,6 +52,7 @@ func TestMSSQL(t *testing.T) { Reset(func() { sess.Close() tsdb.NewXormEngine = origXormEngine + tsdb.Interpolate = origInterpolate }) Convey("Given a table with different native data types", func() { @@ -295,6 +301,40 @@ func TestMSSQL(t *testing.T) { }) + Convey("When doing a metric query using timeGroup and $__interval", func() { + mockInterpolate := tsdb.Interpolate + tsdb.Interpolate = origInterpolate + + Reset(func() { + tsdb.Interpolate = mockInterpolate + }) + + Convey("Should replace $__interval", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + DataSource: &models.DataSource{}, + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY $__timeGroup(time, $__interval) ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time)/60)*60 ORDER BY 1") + }) + }) + Convey("When doing a metric query using timeGroup with float fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 0dabdd7c283..0f1c4fcaf2c 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -9,17 +9,17 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -//const rsString = `(?:"([^"]*)")`; const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type mySqlMacroEngine struct { + *tsdb.SqlMacroEngineBase timeRange *tsdb.TimeRange query *tsdb.Query } func newMysqlMacroEngine() tsdb.SqlMacroEngine { - return &mySqlMacroEngine{} + return &mySqlMacroEngine{SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase()} } func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -28,7 +28,7 @@ func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa rExp, _ := regexp.Compile(sExpr) var macroError error - sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") @@ -48,23 +48,6 @@ func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRa return sql, nil } -func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + repl(groups) - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__timeEpoch", "__time": diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index ca6df8e360e..13d9040a738 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -42,6 +42,11 @@ func TestMySQL(t *testing.T) { return x, nil } + origInterpolate := tsdb.Interpolate + tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + return sql, nil + } + endpoint, err := newMysqlQueryEndpoint(&models.DataSource{ JsonData: simplejson.New(), SecureJsonData: securejsondata.SecureJsonData{}, @@ -54,6 +59,7 @@ func TestMySQL(t *testing.T) { Reset(func() { sess.Close() tsdb.NewXormEngine = origXormEngine + tsdb.Interpolate = origInterpolate }) Convey("Given a table with different native data types", func() { @@ -295,6 +301,40 @@ func TestMySQL(t *testing.T) { }) + Convey("When doing a metric query using timeGroup and $__interval", func() { + mockInterpolate := tsdb.Interpolate + tsdb.Interpolate = origInterpolate + + Reset(func() { + tsdb.Interpolate = mockInterpolate + }) + + Convey("Should replace $__interval", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + DataSource: &models.DataSource{}, + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT UNIX_TIMESTAMP(time) DIV 60 * 60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1") + }) + }) + Convey("When doing a metric query using timeGroup with value fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 0a2ea1d2af6..16f4adb68a6 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -9,18 +9,21 @@ import ( "github.com/grafana/grafana/pkg/tsdb" ) -//const rsString = `(?:"([^"]*)")`; const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` type postgresMacroEngine struct { + *tsdb.SqlMacroEngineBase timeRange *tsdb.TimeRange query *tsdb.Query timescaledb bool } func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine { - return &postgresMacroEngine{timescaledb: timescaledb} + return &postgresMacroEngine{ + SqlMacroEngineBase: tsdb.NewSqlMacroEngineBase(), + timescaledb: timescaledb, + } } func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { @@ -29,7 +32,7 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim rExp, _ := regexp.Compile(sExpr) var macroError error - sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + sql = m.ReplaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { // detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility // if there is a ',' directly after the macro call $__timeGroup is probably used @@ -66,23 +69,6 @@ func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.Tim return sql, nil } -func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { - result := "" - lastIndex := 0 - - for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { - groups := []string{} - for i := 0; i < len(v); i += 2 { - groups = append(groups, str[v[i]:v[i+1]]) - } - - result += str[lastIndex:v[0]] + repl(groups) - lastIndex = v[1] - } - - return result + str[lastIndex:] -} - func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 4e05f676682..fc1a5f34253 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -43,6 +43,11 @@ func TestPostgres(t *testing.T) { return x, nil } + origInterpolate := tsdb.Interpolate + tsdb.Interpolate = func(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + return sql, nil + } + endpoint, err := newPostgresQueryEndpoint(&models.DataSource{ JsonData: simplejson.New(), SecureJsonData: securejsondata.SecureJsonData{}, @@ -55,6 +60,7 @@ func TestPostgres(t *testing.T) { Reset(func() { sess.Close() tsdb.NewXormEngine = origXormEngine + tsdb.Interpolate = origInterpolate }) Convey("Given a table with different native data types", func() { @@ -222,6 +228,40 @@ func TestPostgres(t *testing.T) { } }) + Convey("When doing a metric query using timeGroup and $__interval", func() { + mockInterpolate := tsdb.Interpolate + tsdb.Interpolate = origInterpolate + + Reset(func() { + tsdb.Interpolate = mockInterpolate + }) + + Convey("Should replace $__interval", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + DataSource: &models.DataSource{}, + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, $__interval) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(30*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + So(queryResult.Meta.Get("sql").MustString(), ShouldEqual, "SELECT floor(extract(epoch from time)/60)*60 AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1") + }) + }) + Convey("When doing a metric query using timeGroup with NULL fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 454853c7cc8..18e02e328d1 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "math" + "regexp" "strconv" "strings" "sync" @@ -43,6 +44,8 @@ var engineCache = engineCacheType{ versions: make(map[int64]int), } +var sqlIntervalCalculator = NewIntervalCalculator(nil) + var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) { return xorm.NewEngine(driverName, connectionString) } @@ -126,7 +129,15 @@ func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, queryResult := &QueryResult{Meta: simplejson.New(), RefId: query.RefId} result.Results[query.RefId] = queryResult - rawSQL, err := e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL) + // global substitutions + rawSQL, err := Interpolate(query, tsdbQuery.TimeRange, rawSQL) + if err != nil { + queryResult.Error = err + continue + } + + // datasource specific substitutions + rawSQL, err = e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL) if err != nil { queryResult.Error = err continue @@ -163,6 +174,20 @@ func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, return result, nil } +// global macros/substitutions for all sql datasources +var Interpolate = func(query *Query, timeRange *TimeRange, sql string) (string, error) { + minInterval, err := GetIntervalFrom(query.DataSource, query.Model, time.Second*60) + if err != nil { + return sql, nil + } + interval := sqlIntervalCalculator.Calculate(timeRange, minInterval) + + sql = strings.Replace(sql, "$__interval_ms", strconv.FormatInt(interval.Milliseconds(), 10), -1) + sql = strings.Replace(sql, "$__interval", interval.Text, -1) + + return sql, nil +} + func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error { columnNames, err := rows.Columns() columnCount := len(columnNames) @@ -589,3 +614,26 @@ func SetupFillmode(query *Query, interval time.Duration, fillmode string) error return nil } + +type SqlMacroEngineBase struct{} + +func NewSqlMacroEngineBase() *SqlMacroEngineBase { + return &SqlMacroEngineBase{} +} + +func (m *SqlMacroEngineBase) ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string { + result := "" + lastIndex := 0 + + for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) { + groups := []string{} + for i := 0; i < len(v); i += 2 { + groups = append(groups, str[v[i]:v[i+1]]) + } + + result += str[lastIndex:v[0]] + repl(groups) + lastIndex = v[1] + } + + return result + str[lastIndex:] +} diff --git a/pkg/tsdb/sql_engine_test.go b/pkg/tsdb/sql_engine_test.go index 854734fac31..05b8a51ae6f 100644 --- a/pkg/tsdb/sql_engine_test.go +++ b/pkg/tsdb/sql_engine_test.go @@ -5,6 +5,8 @@ import ( "time" "github.com/grafana/grafana/pkg/components/null" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -14,6 +16,35 @@ func TestSqlEngine(t *testing.T) { dt := time.Date(2018, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC) earlyDt := time.Date(1970, 3, 14, 21, 20, 6, int(527345*time.Microsecond), time.UTC) + Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { + from := time.Date(2018, 4, 12, 18, 0, 0, 0, time.UTC) + to := from.Add(5 * time.Minute) + timeRange := NewFakeTimeRange("5m", "now", to) + query := &Query{DataSource: &models.DataSource{}, Model: simplejson.New()} + + Convey("interpolate $__interval", func() { + sql, err := Interpolate(query, timeRange, "select $__interval ") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 1m ") + }) + + Convey("interpolate $__interval in $__timeGroup", func() { + sql, err := Interpolate(query, timeRange, "select $__timeGroupAlias(time,$__interval)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select $__timeGroupAlias(time,1m)") + }) + + Convey("interpolate $__interval_ms", func() { + sql, err := Interpolate(query, timeRange, "select $__interval_ms ") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "select 60000 ") + }) + + }) + Convey("Given row values with time.Time as time columns", func() { var nilPointer *time.Time From e33b2d5fce9a3184a9f33fcd7df219d9c011f478 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 13 Sep 2018 17:29:37 +0200 Subject: [PATCH 047/127] changelog: add notes about closing #11555 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a9ffbec9c..939d75f3a53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Minor * **Alerting**: Link to view full size image in Microsoft Teams alert notifier [#13121](https://github.com/grafana/grafana/issues/13121), thx [@holiiveira](https://github.com/holiiveira) +* **Postgres/MySQL/MSSQL**: Add support for replacing $__interval and $__interval_ms in alert queries [#11555](https://github.com/grafana/grafana/issues/11555), thx [@svenklemm](https://github.com/svenklemm) # 5.3.0-beta1 (2018-09-06) From 379227c75d23f2dfb6280dbc775b6f2d1e45af37 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 13 Sep 2018 17:52:28 +0200 Subject: [PATCH 048/127] Updated CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 939d75f3a53..fd2357fbfa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ * **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon) * **Tags**: Default sort order for GetDashboardTags [#11681](https://github.com/grafana/grafana/pull/11681), thx [@Jonnymcc](https://github.com/Jonnymcc) +* **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda) +* **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212) # 5.3.0 (unreleased) From fbfcc622698a951b39a557f6be96d4aace2027c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 21:08:41 +0200 Subject: [PATCH 049/127] fix: added loading screen error scenario (#13256) --- public/views/index.template.html | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/public/views/index.template.html b/public/views/index.template.html index ad55f810d0e..606db2c769e 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -145,6 +145,29 @@ } } + /* Fail info */ + .preloader__text--fail { + display: none; + } + + /* stop logo animation */ + .preloader--done .preloader__bounce, + .preloader--done .preloader__logo { + animation-name: none; + display: none; + } + + .preloader--done .preloader__logo, + .preloader--done .preloader__text { + display: none; + color: #ff5705 !important; + font-size: 15px; + } + + .preloader--done .preloader__text--fail { + display: block; + } + [ng\:cloak], [ng-cloak], .ng-cloak { @@ -159,6 +182,20 @@
Loading Grafana
+
+

+ If your seeing this Grafana has failed to load its application files +
+
+

+

+ 1. This could be caused by your reverse proxy settings.

+ 2. If you host grafana under subpath make sure your grafana.ini root_path setting includes subpath

+ 3. If you have a local dev build make sure you build frontend using: npm run dev, npm run watch, or npm run + build

+ 4. Sometimes restarting grafana-server can help
+

+
@@ -236,6 +273,10 @@ // insert it at the end of the head in a legacy-friendly manner document.head.insertBefore(myCSS, document.head.childNodes[document.head.childNodes.length - 1].nextSibling); + // switch loader to show all has loaded + window.onload = function() { + document.getElementsByClassName("preloader")[0].className = "preloader preloader--done"; + }; [[if .GoogleTagManagerId]] From ec5aa332ac8780b0b89084298b3dc4b86ea4d1df Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 13 Sep 2018 21:09:21 +0200 Subject: [PATCH 050/127] removes old unused examples (#13260) Just wanted to reduce the amount of files/folders in root --- examples/README.md | 5 - examples/alerting-dashboard.json | 800 --------- examples/alerting-multiple-alerts.json | 2216 ------------------------ 3 files changed, 3021 deletions(-) delete mode 100644 examples/README.md delete mode 100644 examples/alerting-dashboard.json delete mode 100644 examples/alerting-multiple-alerts.json diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 75f1f9a9a86..00000000000 --- a/examples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -## Example plugin implementations - -datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource) -app: [example-app](https://github.com/grafana/example-app) -panel: [grafana-piechart-panel](https://github.com/grafana/piechart-panel) diff --git a/examples/alerting-dashboard.json b/examples/alerting-dashboard.json deleted file mode 100644 index 744460d7847..00000000000 --- a/examples/alerting-dashboard.json +++ /dev/null @@ -1,800 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_GRAPHITE", - "label": "graphite", - "description": "", - "type": "datasource", - "pluginId": "graphite", - "pluginName": "Graphite" - } - ], - "__requires": [ - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "3.1.0" - }, - { - "type": "datasource", - "id": "graphite", - "name": "Graphite", - "version": "1.0.0" - } - ], - "id": null, - "title": "Alerting example", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 355 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 1, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - }, - { - "refId": "B", - "target": "aliasByNode(scale(statsd.$apa.counters.session_start.*.count, 10), 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 355 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 2, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 1 - ], - "type": "lt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "count" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "No datapoints", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 20, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 4, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 1, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Count datapoints", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "Row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "lt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Alert below value", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 17, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "lt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Alert below value", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 10, - 80 - ], - "type": "outside_range" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Alert is outside range", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 18, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "lt", - "value": 10 - }, - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 80 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Alert is outside range", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 60, - 80 - ], - "type": "within_range" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Alert is within range", - "notifications": [], - "severity": "critical" - }, - "alerting": {}, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 19, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 60 - }, - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "lt", - "value": 80 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Alert is within range", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - } - ], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [ - { - "current": { - "text": "fakesite", - "value": "fakesite" - }, - "datasource": null, - "hide": 0, - "includeAll": false, - "multi": false, - "name": "apa", - "options": [ - { - "selected": true, - "text": "fakesite", - "value": "fakesite" - } - ], - "query": "fakesite", - "refresh": 0, - "type": "custom" - } - ] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 15, - "links": [], - "gnetId": null -} \ No newline at end of file diff --git a/examples/alerting-multiple-alerts.json b/examples/alerting-multiple-alerts.json deleted file mode 100644 index e6e729ecc06..00000000000 --- a/examples/alerting-multiple-alerts.json +++ /dev/null @@ -1,2216 +0,0 @@ -{ - "__inputs": [ - { - "name": "DS_GRAPHITE", - "label": "graphite", - "description": "", - "type": "datasource", - "pluginId": "graphite", - "pluginName": "Graphite" - } - ], - "__requires": [ - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "3.1.0" - }, - { - "type": "datasource", - "id": "graphite", - "name": "Graphite", - "version": "1.0.0" - } - ], - "id": null, - "title": "Dashboard with many alerts", - "tags": [], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 1, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 5, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 6, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 30 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "sum" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Critical alert panel", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "grid": {}, - "id": 8, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 30 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "Row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 2, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 3, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 4, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 20 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "Warning panel alert", - "notifications": [], - "severity": "warning" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 7, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "warning", - "fill": true, - "fillColor": "rgba(235, 138, 14, 0.12)", - "line": true, - "lineColor": "rgba(247, 149, 32, 0.60)", - "op": "gt", - "value": 20 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 9, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 10, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 11, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 50 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "10s", - "handler": 1, - "name": "Fast Critical panel alert", - "notifications": [], - "severity": "critical" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 12, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "fillColor": "rgba(234, 112, 112, 0.12)", - "line": true, - "lineColor": "rgba(237, 46, 24, 0.60)", - "op": "gt", - "value": 50 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Critical panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - }, - { - "collapse": false, - "editable": true, - "height": "250px", - "panels": [ - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 13, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 14, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 15, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "enabled": true, - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "gt", - "params": [ - 10 - ] - } - } - ], - "severity": "warning", - "frequency": "1s", - "handler": 1, - "notifications": [], - "name": "Fast Warning panel alert" - }, - "aliasColors": {}, - "bars": false, - "datasource": "${DS_GRAPHITE}", - "editable": true, - "error": false, - "fill": 1, - "id": 16, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 3, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)" - } - ], - "thresholds": [ - { - "value": 10, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "warning", - "fillColor": "rgba(235, 138, 14, 0.12)", - "lineColor": "rgba(247, 149, 32, 0.60)" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Fast Warning panel", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "show": true - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - }, - { - "title": "New row", - "height": "250px", - "editable": true, - "collapse": false, - "panels": [ - { - "title": "Alert below value", - "error": false, - "span": 3, - "editable": true, - "type": "graph", - "isNew": true, - "id": 17, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "refId": "A" - } - ], - "datasource": "${DS_GRAPHITE}", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "alert": { - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "lt", - "params": [ - 20 - ] - } - } - ], - "severity": "critical", - "frequency": "60s", - "handler": 1, - "notifications": [], - "name": "Alert below value", - "enabled": true - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - "thresholds": [ - { - "value": 20, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - } - ], - "links": [] - }, - { - "title": "Alert is outside range", - "error": false, - "span": 3, - "editable": true, - "type": "graph", - "isNew": true, - "id": 18, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "refId": "A" - } - ], - "datasource": "${DS_GRAPHITE}", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "alert": { - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "outside_range", - "params": [ - 10, - 80 - ] - } - } - ], - "severity": "critical", - "frequency": "10s", - "handler": 1, - "notifications": [], - "name": "Alert is outside range", - "enabled": true - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - "thresholds": [ - { - "value": 10, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - }, - { - "value": 80, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - } - ], - "links": [] - }, - { - "title": "Alert is within range", - "error": false, - "span": 3, - "editable": true, - "type": "graph", - "isNew": true, - "id": 19, - "targets": [ - { - "target": "aliasByNode(statsd.fakesite.counters.session_start.*.count, 4)", - "refId": "A" - } - ], - "datasource": "${DS_GRAPHITE}", - "renderer": "flot", - "yaxes": [ - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - }, - { - "label": null, - "show": true, - "logBase": 1, - "min": null, - "max": null, - "format": "short" - } - ], - "xaxis": { - "show": true - }, - "alert": { - "conditions": [ - { - "type": "query", - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "type": "avg", - "params": [] - }, - "evaluator": { - "type": "within_range", - "params": [ - 60, - 80 - ] - } - } - ], - "severity": "critical", - "frequency": "10s", - "handler": 1, - "notifications": [], - "name": "Alert is within range", - "enabled": true - }, - "lines": true, - "fill": 1, - "linewidth": 2, - "points": false, - "pointradius": 5, - "bars": false, - "stack": false, - "percentage": false, - "legend": { - "show": true, - "values": false, - "min": false, - "max": false, - "current": false, - "total": false, - "avg": false - }, - "nullPointMode": "connected", - "steppedLine": false, - "tooltip": { - "value_type": "cumulative", - "shared": true, - "sort": 0, - "msResolution": false - }, - "timeFrom": null, - "timeShift": null, - "aliasColors": {}, - "seriesOverrides": [], - "thresholds": [ - { - "value": 60, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - }, - { - "value": 80, - "op": "lt", - "fill": true, - "line": true, - "colorMode": "critical", - "fillColor": "rgba(234, 112, 112, 0.12)", - "lineColor": "rgba(237, 46, 24, 0.60)" - } - ], - "links": [] - } - ] - } - ], - "time": { - "from": "now-6h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 50, - "links": [], - "gnetId": null -} \ No newline at end of file From 124b21a6aa3da3c184e8d634422756fa85beb7e3 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Thu, 13 Sep 2018 17:19:51 -0400 Subject: [PATCH 051/127] use pluginName consistently when upgrading plugins --- pkg/cmd/grafana-cli/commands/upgrade_command.go | 6 +++--- pkg/cmd/grafana-cli/services/services.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index 355ccab3d1c..396371d3577 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -16,7 +16,7 @@ func upgradeCommand(c CommandLine) error { return err } - v, err2 := s.GetPlugin(localPlugin.Id, c.RepoDirectory()) + v, err2 := s.GetPlugin(pluginName, c.RepoDirectory()) if err2 != nil { return err2 @@ -24,9 +24,9 @@ func upgradeCommand(c CommandLine) error { if ShouldUpgrade(localPlugin.Info.Version, v) { s.RemoveInstalledPlugin(pluginsDir, pluginName) - return InstallPlugin(localPlugin.Id, "", c) + return InstallPlugin(pluginName, "", c) } - logger.Infof("%s %s is up to date \n", color.GreenString("✔"), localPlugin.Id) + logger.Infof("%s %s is up to date \n", color.GreenString("✔"), pluginName) return nil } diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index b4e50ac84df..338975bc130 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -63,7 +63,7 @@ func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { var data m.PluginRepo err = json.Unmarshal(body, &data) if err != nil { - logger.Info("Failed to unmarshal graphite response error:", err) + logger.Info("Failed to unmarshal plugin repo response error:", err) return m.PluginRepo{}, err } @@ -140,7 +140,7 @@ func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { var data m.Plugin err = json.Unmarshal(body, &data) if err != nil { - logger.Info("Failed to unmarshal graphite response error:", err) + logger.Info("Failed to unmarshal plugin repo response error:", err) return m.Plugin{}, err } From 74912dca8d48298eb5783d46bd4c0de641d39567 Mon Sep 17 00:00:00 2001 From: Tianon Gravi Date: Thu, 13 Sep 2018 15:41:49 -0700 Subject: [PATCH 052/127] Fix gauge display accuracy for "percent (0.0-1.0)" The "Decimals" value was incorrectly applied to the metric value used to calculate the gauge display in addition to the text value (so a "Decimals" value of "1" turns "45.1%" into "50%" in the gauge display even though the label still correctly says "45.1%"). --- public/app/plugins/panel/singlestat/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c44b09449be..eafa3cf23f4 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -544,7 +544,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { elem.append(plotCanvas); const plotSeries = { - data: [[0, data.valueRounded]], + data: [[0, data.value]], }; $.plot(plotCanvas, [plotSeries], options); From 7bb010926196cc9c5f25a0c850baac13834d89c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 07:47:33 +0200 Subject: [PATCH 053/127] feat: dashboard permissions are working --- .../PermissionList/AddPermission.tsx | 3 +- .../PermissionsInfo.tsx | 0 .../Permissions/AddPermissions.test.tsx | 90 ------------ .../components/Permissions/AddPermissions.tsx | 128 ------------------ .../Permissions/DashboardPermissions.tsx | 71 ---------- .../DisabledPermissionsListItem.tsx | 43 ------ .../core/components/Permissions/FolderInfo.ts | 5 - .../components/Permissions/Permissions.tsx | 91 ------------- .../Permissions/PermissionsList.tsx | 64 --------- .../Permissions/PermissionsListItem.tsx | 91 ------------- .../DashboardPermissions.tsx | 23 +++- .../app/features/dashboard/state/actions.ts | 2 +- .../features/folders/FolderPermissions.tsx | 2 +- .../features/folders/state/reducers.test.ts | 92 ++++++++++--- public/app/types/acl.ts | 24 ++-- public/app/types/index.ts | 5 + scripts/webpack/webpack.common.js | 3 + 17 files changed, 117 insertions(+), 620 deletions(-) rename public/app/core/components/{Permissions => PermissionList}/PermissionsInfo.tsx (100%) delete mode 100644 public/app/core/components/Permissions/AddPermissions.test.tsx delete mode 100644 public/app/core/components/Permissions/AddPermissions.tsx delete mode 100644 public/app/core/components/Permissions/DashboardPermissions.tsx delete mode 100644 public/app/core/components/Permissions/DisabledPermissionsListItem.tsx delete mode 100644 public/app/core/components/Permissions/FolderInfo.ts delete mode 100644 public/app/core/components/Permissions/Permissions.tsx delete mode 100644 public/app/core/components/Permissions/PermissionsList.tsx delete mode 100644 public/app/core/components/Permissions/PermissionsListItem.tsx diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 76bcfac4780..73bffdaf97b 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -8,6 +8,7 @@ import { AclTarget, PermissionLevel, NewDashboardAclItem, + OrgRole, } from 'app/types/acl'; export interface Props { @@ -25,7 +26,7 @@ class AddPermissions extends Component { return { userId: 0, teamId: 0, - role: '', + role: OrgRole.Viewer, type: AclTarget.Team, permission: PermissionLevel.View, }; diff --git a/public/app/core/components/Permissions/PermissionsInfo.tsx b/public/app/core/components/PermissionList/PermissionsInfo.tsx similarity index 100% rename from public/app/core/components/Permissions/PermissionsInfo.tsx rename to public/app/core/components/PermissionList/PermissionsInfo.tsx diff --git a/public/app/core/components/Permissions/AddPermissions.test.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx deleted file mode 100644 index c6d1ab381b8..00000000000 --- a/public/app/core/components/Permissions/AddPermissions.test.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import AddPermissions from './AddPermissions'; -import { RootStore } from 'app/stores/RootStore/RootStore'; -import { getBackendSrv } from 'app/core/services/backend_srv'; - -jest.mock('app/core/services/backend_srv', () => ({ - getBackendSrv: () => { - return { - get: () => { - return Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - ]); - }, - post: jest.fn(() => Promise.resolve({})), - }; - }, -})); - -describe('AddPermissions', () => { - let wrapper; - let store; - let instance; - const backendSrv: any = getBackendSrv(); - - beforeAll(() => { - store = RootStore.create({}, { backendSrv: backendSrv }); - wrapper = shallow(); - instance = wrapper.instance(); - return store.permissions.load(1, true, false); - }); - - describe('when permission for a user is added', () => { - it('should save permission to db', () => { - const evt = { - target: { - value: 'User', - }, - }; - const userItem = { - id: 2, - login: 'user2', - }; - - instance.onTypeChanged(evt); - instance.onUserSelected(userItem); - - wrapper.update(); - - expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false); - - wrapper.find('form').simulate('submit', { preventDefault() {} }); - - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - }); - - describe('when permission for team is added', () => { - it('should save permission to db', () => { - const evt = { - target: { - value: 'Group', - }, - }; - - const teamItem = { - id: 2, - name: 'ug1', - }; - - instance.onTypeChanged(evt); - instance.onTeamSelected(teamItem); - - wrapper.update(); - - expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false); - - wrapper.find('form').simulate('submit', { preventDefault() {} }); - - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - }); - - afterEach(() => { - backendSrv.post.mockClear(); - }); -}); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx deleted file mode 100644 index 289e27aa731..00000000000 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; -import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; -import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; -import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -export interface Props { - permissions: any; -} - -@observer -class AddPermissions extends Component { - constructor(props) { - super(props); - } - - componentWillMount() { - const { permissions } = this.props; - permissions.resetNewType(); - } - - onTypeChanged = evt => { - const { value } = evt.target; - const { permissions } = this.props; - - permissions.setNewType(value); - }; - - onUserSelected = (user: User) => { - const { permissions } = this.props; - if (!user) { - permissions.newItem.setUser(null, null); - return; - } - return permissions.newItem.setUser(user.id, user.login, user.avatarUrl); - }; - - onTeamSelected = (team: Team) => { - const { permissions } = this.props; - if (!team) { - permissions.newItem.setTeam(null, null); - return; - } - return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl); - }; - - onPermissionChanged = (permission: OptionWithDescription) => { - const { permissions } = this.props; - return permissions.newItem.setPermission(permission.value); - }; - - resetNewType() { - const { permissions } = this.props; - return permissions.resetNewType(); - } - - onSubmit = evt => { - evt.preventDefault(); - const { permissions } = this.props; - permissions.addStoreItem(); - }; - - render() { - const { permissions } = this.props; - const newItem = permissions.newItem; - const pickerClassName = 'width-20'; - - const isValid = newItem.isValid(); - - return ( -
- -
-
Add Permission For
-
-
-
- -
-
- - {newItem.type === 'User' ? ( -
- -
- ) : null} - - {newItem.type === 'Group' ? ( -
- -
- ) : null} - -
- -
- -
- -
-
-
-
- ); - } -} - -export default AddPermissions; diff --git a/public/app/core/components/Permissions/DashboardPermissions.tsx b/public/app/core/components/Permissions/DashboardPermissions.tsx deleted file mode 100644 index 38a646b2473..00000000000 --- a/public/app/core/components/Permissions/DashboardPermissions.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { Component } from 'react'; -import { observer } from 'mobx-react'; -import { store } from 'app/stores/store'; -import Permissions from 'app/core/components/Permissions/Permissions'; -import Tooltip from 'app/core/components/Tooltip/Tooltip'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; -import AddPermissions from 'app/core/components/Permissions/AddPermissions'; -import SlideDown from 'app/core/components/Animations/SlideDown'; -import { FolderInfo } from './FolderInfo'; - -export interface Props { - dashboardId: number; - folder?: FolderInfo; - backendSrv: any; -} - -@observer -class DashboardPermissions extends Component { - permissions: any; - - constructor(props) { - super(props); - this.handleAddPermission = this.handleAddPermission.bind(this); - this.permissions = store.permissions; - } - - handleAddPermission() { - this.permissions.toggleAddPermissions(); - } - - componentWillUnmount() { - this.permissions.hideAddPermissions(); - } - - render() { - const { dashboardId, folder, backendSrv } = this.props; - - return ( -
-
-
-

Permissions

- - - -
- -
-
- - - - -
- ); - } -} - -export default DashboardPermissions; diff --git a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx b/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx deleted file mode 100644 index d65595dae66..00000000000 --- a/public/app/core/components/Permissions/DisabledPermissionsListItem.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { Component } from 'react'; -import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -export interface Props { - item: any; -} - -export default class DisabledPermissionListItem extends Component { - render() { - const { item } = this.props; - - return ( - - - - - - {item.name} - (Role) - - - Can - -
- {}} - value={item.permission} - disabled={true} - className={'gf-form-input--form-dropdown-right'} - /> -
- - - - - - ); - } -} diff --git a/public/app/core/components/Permissions/FolderInfo.ts b/public/app/core/components/Permissions/FolderInfo.ts deleted file mode 100644 index d4a6020bb71..00000000000 --- a/public/app/core/components/Permissions/FolderInfo.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface FolderInfo { - id: number; - title: string; - url: string; -} diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx deleted file mode 100644 index d17899c891f..00000000000 --- a/public/app/core/components/Permissions/Permissions.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React, { Component } from 'react'; -import PermissionsList from './PermissionsList'; -import { observer } from 'mobx-react'; -import { FolderInfo } from './FolderInfo'; - -export interface DashboardAcl { - id?: number; - dashboardId?: number; - userId?: number; - userLogin?: string; - userEmail?: string; - teamId?: number; - team?: string; - permission?: number; - permissionName?: string; - role?: string; - icon?: string; - name?: string; - inherited?: boolean; - sortRank?: number; -} - -export interface Props { - dashboardId: number; - folderInfo?: FolderInfo; - permissions?: any; - isFolder: boolean; - backendSrv: any; -} - -@observer -class Permissions extends Component { - constructor(props) { - super(props); - const { dashboardId, isFolder, folderInfo } = this.props; - this.permissionChanged = this.permissionChanged.bind(this); - this.typeChanged = this.typeChanged.bind(this); - this.removeItem = this.removeItem.bind(this); - this.loadStore(dashboardId, isFolder, folderInfo && folderInfo.id === 0); - } - - loadStore(dashboardId, isFolder, isInRoot = false) { - return this.props.permissions.load(dashboardId, isFolder, isInRoot); - } - - permissionChanged(index: number, permission: number, permissionName: string) { - const { permissions } = this.props; - permissions.updatePermissionOnIndex(index, permission, permissionName); - } - - removeItem(index: number) { - const { permissions } = this.props; - permissions.removeStoreItem(index); - } - - resetNewType() { - const { permissions } = this.props; - permissions.resetNewType(); - } - - typeChanged(evt) { - const { value } = evt.target; - const { permissions, dashboardId } = this.props; - - if (value === 'Viewer' || value === 'Editor') { - permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId); - this.resetNewType(); - return; - } - - permissions.setNewType(value); - } - - render() { - const { permissions, folderInfo } = this.props; - - return ( -
- -
- ); - } -} - -export default Permissions; diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx deleted file mode 100644 index 7e64de012e4..00000000000 --- a/public/app/core/components/Permissions/PermissionsList.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { Component } from 'react'; -import PermissionsListItem from './PermissionsListItem'; -import DisabledPermissionsListItem from './DisabledPermissionsListItem'; -import { observer } from 'mobx-react'; -import { FolderInfo } from './FolderInfo'; - -export interface Props { - permissions: any[]; - removeItem: any; - permissionChanged: any; - fetching: boolean; - folderInfo?: FolderInfo; -} - -@observer -class PermissionsList extends Component { - render() { - const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props; - - return ( - - - - {permissions.map((item, idx) => { - return ( - - ); - })} - {fetching === true && permissions.length < 1 ? ( - - - - ) : null} - - {fetching === false && permissions.length < 1 ? ( - - - - ) : null} - -
- Loading permissions... -
- No permissions are set. Will only be accessible by admins. -
- ); - } -} - -export default PermissionsList; diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx deleted file mode 100644 index a17aa8c04df..00000000000 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; -import { observer } from 'mobx-react'; -import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; - -const setClassNameHelper = inherited => { - return inherited ? 'gf-form-disabled' : ''; -}; - -function ItemAvatar({ item }) { - if (item.userAvatarUrl) { - return ; - } - if (item.teamAvatarUrl) { - return ; - } - if (item.role === 'Editor') { - return ; - } - - return ; -} - -function ItemDescription({ item }) { - if (item.userId) { - return (User); - } - if (item.teamId) { - return (Team); - } - return (Role); -} - -export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => { - const handleRemoveItem = evt => { - evt.preventDefault(); - removeItem(itemIndex); - }; - - const handleChangePermission = permissionOption => { - permissionChanged(itemIndex, permissionOption.value, permissionOption.label); - }; - - const inheritedFromRoot = item.dashboardId === -1 && !item.inherited; - - return ( - - - - - - {item.name} - - - {item.inherited && - folderInfo && ( - - Inherited from folder{' '} - - {folderInfo.title} - {' '} - - )} - {inheritedFromRoot && Default Permission} - - Can - -
- -
- - - {!item.inherited ? ( - - - - ) : ( - - )} - - - ); -}); diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx index ad7d9c7f504..6ea7ba12721 100644 --- a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -4,17 +4,25 @@ import Tooltip from 'app/core/components/Tooltip/Tooltip'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { StoreState, FolderInfo } from 'app/types'; import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl'; -import { getDashboardPermissions } from '../state/actions'; +import { + getDashboardPermissions, + addDashboardPermission, + removeDashboardPermission, + updateDashboardPermission, +} from '../state/actions'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; import { store } from 'app/stores/configureStore'; export interface Props { dashboardId: number; folder?: FolderInfo; - getDashboardPermissions: typeof getDashboardPermissions; permissions: DashboardAcl[]; + getDashboardPermissions: typeof getDashboardPermissions; + updateDashboardPermission: typeof updateDashboardPermission; + removeDashboardPermission: typeof removeDashboardPermission; + addDashboardPermission: typeof addDashboardPermission; } export interface State { @@ -39,15 +47,15 @@ export class DashboardPermissions extends PureComponent { }; onRemoveItem = (item: DashboardAcl) => { - // this.props.removeFolderPermission(item); + this.props.removeDashboardPermission(this.props.dashboardId, item); }; onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => { - // this.props.updateFolderPermission(item, level); + this.props.updateDashboardPermission(this.props.dashboardId, item, level); }; onAddPermission = (newItem: NewDashboardAclItem) => { - // return this.props.addFolderPermission(newItem); + return this.props.addDashboardPermission(this.props.dashboardId, newItem); }; onCancelAddPermission = () => { @@ -101,6 +109,9 @@ const mapStateToProps = (state: StoreState) => ({ const mapDispatchToProps = { getDashboardPermissions, + addDashboardPermission, + removeDashboardPermission, + updateDashboardPermission, }; export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index b1d25d1f57f..82333817b2b 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -68,7 +68,7 @@ export function updateDashboardPermission( itemsToUpdate.push(updated); } - await getBackendSrv().post(`/api/dashboard/id/${dashboardId}/permissions`, { items: itemsToUpdate }); + await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate }); await dispatch(getDashboardPermissions(dashboardId)); }; } diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index c86137a55ce..176e270038b 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -17,7 +17,7 @@ import { import { getLoadingNav } from './state/navModel'; import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; -import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo'; +import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; export interface Props { navModel: NavModel; diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index ff37f13f97f..be45c643e77 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from './actions'; -import { FolderDTO } from 'app/types'; +import { FolderDTO, OrgRole, PermissionLevel, FolderState } from 'app/types'; import { inititalState, folderReducer } from './reducers'; function getTestFolder(): FolderDTO { @@ -14,29 +14,85 @@ function getTestFolder(): FolderDTO { } describe('folder reducer', () => { - it('should load folder and set hasChanged to false', () => { - const folder = getTestFolder(); + describe('loadFolder', () => { + it('should load folder and set hasChanged to false', () => { + const folder = getTestFolder(); - const action: Action = { - type: ActionTypes.LoadFolder, - payload: folder, - }; + const action: Action = { + type: ActionTypes.LoadFolder, + payload: folder, + }; - const state = folderReducer(inititalState, action); + const state = folderReducer(inititalState, action); - expect(state.hasChanged).toEqual(false); - expect(state.title).toEqual('test folder'); + expect(state.hasChanged).toEqual(false); + expect(state.title).toEqual('test folder'); + }); }); - it('should set title', () => { - const action: Action = { - type: ActionTypes.SetFolderTitle, - payload: 'new title', - }; + describe('detFolderTitle', () => { + it('should set title', () => { + const action: Action = { + type: ActionTypes.SetFolderTitle, + payload: 'new title', + }; - const state = folderReducer(inititalState, action); + const state = folderReducer(inititalState, action); - expect(state.hasChanged).toEqual(true); - expect(state.title).toEqual('new title'); + expect(state.hasChanged).toEqual(true); + expect(state.title).toEqual('new title'); + }); + }); + + describe('loadFolderPermissions', () => { + let state: FolderState; + + beforeEach(() => { + const action: Action = { + type: ActionTypes.LoadFolderPermissions, + payload: [ + { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, + { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, + { + id: 4, + dashboardId: 10, + permission: PermissionLevel.View, + teamId: 1, + team: 'MyTestTeam', + inherited: true, + }, + { + id: 5, + dashboardId: 1, + permission: PermissionLevel.View, + userId: 1, + userLogin: 'MyTestUser', + }, + { + id: 6, + dashboardId: 1, + permission: PermissionLevel.Edit, + teamId: 2, + team: 'MyTestTeam2', + }, + ], + }; + + state = folderReducer(inititalState, action); + }); + + it('should add permissions to state', async () => { + expect(state.permissions.length).toBe(5); + expect(state.permissions.length).toBe(5); + }); + + it('should be sorted by sort rank and alphabetically', async () => { + expect(state.permissions[0].name).toBe('MyTestTeam'); + expect(state.permissions[0].dashboardId).toBe(10); + expect(state.permissions[1].name).toBe('Editor'); + expect(state.permissions[2].name).toBe('Viewer'); + expect(state.permissions[3].name).toBe('MyTestTeam2'); + expect(state.permissions[4].name).toBe('MyTestUser'); + }); }); }); diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index feca062b355..d6589f8bf40 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -1,3 +1,9 @@ +export enum OrgRole { + Viewer = 'Viewer', + Editor = 'Editor', + Admin = 'Admin', +} + export interface DashboardAclDTO { id?: number; dashboardId?: number; @@ -7,8 +13,7 @@ export interface DashboardAclDTO { teamId?: number; team?: string; permission?: PermissionLevel; - permissionName?: string; - role?: string; + role?: OrgRole; icon?: string; inherited?: boolean; } @@ -16,7 +21,7 @@ export interface DashboardAclDTO { export interface DashboardAclUpdateDTO { userId: number; teamId: number; - role: string; + role: OrgRole; permission: PermissionLevel; } @@ -29,8 +34,7 @@ export interface DashboardAcl { teamId?: number; team?: string; permission?: PermissionLevel; - permissionName?: string; - role?: string; + role?: OrgRole; icon?: string; name?: string; inherited?: boolean; @@ -46,7 +50,7 @@ export interface DashboardPermissionInfo { export interface NewDashboardAclItem { teamId: number; userId: number; - role: string; + role: OrgRole; permission: PermissionLevel; type: AclTarget; } @@ -58,10 +62,10 @@ export enum PermissionLevel { } export enum AclTarget { - Team = 'team', - User = 'user', - Viewer = 'viewer', - Editor = 'editor', + Team = 'Team', + User = 'User', + Viewer = 'Viewer', + Editor = 'Editor', } export interface AclTargetInfo { diff --git a/public/app/types/index.ts b/public/app/types/index.ts index f2fe165a863..8fcfcc7e88d 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -4,6 +4,7 @@ import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './loc import { NavModel, NavModelItem, NavIndex } from './navModel'; import { FolderDTO, FolderState, FolderInfo } from './folder'; import { DashboardState } from './dashboard'; +import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; export { Team, @@ -24,6 +25,10 @@ export { FolderDTO, FolderState, FolderInfo, + DashboardState, + DashboardAcl, + OrgRole, + PermissionLevel, }; export interface StoreState { diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index eea3ebbed2d..d367016c4fb 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -24,6 +24,9 @@ module.exports = { path.resolve('node_modules') ], }, + stats: { + warningsFilter: /export .* was not found in/ + }, node: { fs: 'empty', }, From 776d81189f2db5e49577d47b853b1a3b46ca0a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 07:52:17 +0200 Subject: [PATCH 054/127] test: added simple dashboard reducer test --- public/app/features/folders/state/reducers.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/features/folders/state/reducers.test.ts b/public/app/features/folders/state/reducers.test.ts index be45c643e77..72e97f39562 100644 --- a/public/app/features/folders/state/reducers.test.ts +++ b/public/app/features/folders/state/reducers.test.ts @@ -83,7 +83,6 @@ describe('folder reducer', () => { it('should add permissions to state', async () => { expect(state.permissions.length).toBe(5); - expect(state.permissions.length).toBe(5); }); it('should be sorted by sort rank and alphabetically', async () => { From 331be7d47a9c2f252336c189925a35e8cb2a05d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 08:25:35 +0200 Subject: [PATCH 055/127] fix: add permission fixes --- .../PermissionList/AddPermission.tsx | 26 ++++++++++++------- .../features/dashboard/state/reducers.test.ts | 24 +++++++++++++++++ public/app/types/acl.ts | 2 +- 3 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 public/app/features/dashboard/state/reducers.test.ts diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 73bffdaf97b..77ac6953b74 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -26,28 +26,34 @@ class AddPermissions extends Component { return { userId: 0, teamId: 0, - role: OrgRole.Viewer, type: AclTarget.Team, permission: PermissionLevel.View, }; } onTypeChanged = evt => { - this.setState({ type: evt.target.value as AclTarget }); + const type = evt.target.value as AclTarget; + + switch (type) { + case AclTarget.User: + case AclTarget.Team: + this.setState({ type: type, userId: 0, teamId: 0, role: undefined }); + break; + case AclTarget.Editor: + this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Editor }); + break; + case AclTarget.Viewer: + this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Viewer }); + break; + } }; onUserSelected = (user: User) => { - this.setState({ - userId: user ? user.id : 0, - teamId: 0, - }); + this.setState({ userId: user ? user.id : 0 }); }; onTeamSelected = (team: Team) => { - this.setState({ - userId: 0, - teamId: team ? team.id : 0, - }); + this.setState({ teamId: team ? team.id : 0 }); }; onPermissionChanged = (permission: OptionWithDescription) => { diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts new file mode 100644 index 00000000000..c5b67f58ac9 --- /dev/null +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -0,0 +1,24 @@ +import { Action, ActionTypes } from './actions'; +import { OrgRole, PermissionLevel, DashboardState } from 'app/types'; +import { inititalState, dashboardReducer } from './reducers'; + +describe('dashboard reducer', () => { + describe('loadDashboardPermissions', () => { + let state: DashboardState; + + beforeEach(() => { + const action: Action = { + type: ActionTypes.LoadDashboardPermissions, + payload: [ + { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, + { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, + ], + }; + state = dashboardReducer(inititalState, action); + }); + + it('should add permissions to state', async () => { + expect(state.permissions.length).toBe(2); + }); + }); +}); diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts index d6589f8bf40..fa5ace388c4 100644 --- a/public/app/types/acl.ts +++ b/public/app/types/acl.ts @@ -50,7 +50,7 @@ export interface DashboardPermissionInfo { export interface NewDashboardAclItem { teamId: number; userId: number; - role: OrgRole; + role?: OrgRole; permission: PermissionLevel; type: AclTarget; } From 7b0215380f64304713492b73210d977396523e38 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 14 Sep 2018 08:31:41 +0200 Subject: [PATCH 056/127] added underline to links in table --- public/sass/components/_panel_table.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/sass/components/_panel_table.scss b/public/sass/components/_panel_table.scss index 225238b102c..e47e639a65e 100644 --- a/public/sass/components/_panel_table.scss +++ b/public/sass/components/_panel_table.scss @@ -86,6 +86,8 @@ padding: 0.45em 0 0.45em 1.1em; height: 100%; display: inline-block; + text-decoration: underline; + text-underline-position: under; } } From 0e9a6dcedc0ede60b1a6db3d3245b28b92fa4207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 09:30:32 +0200 Subject: [PATCH 057/127] Use datasource cache for backend tsdb/query endpoint (#13266) fix: use datasource cache for backend datasources --- pkg/api/api.go | 2 +- pkg/api/dataproxy.go | 12 +++++------- pkg/api/metrics.go | 14 +++++++------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 906481bbb8a..39b332aeb9f 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -320,7 +320,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/search/", Search) // metrics - apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(QueryMetrics)) + apiRoute.Post("/tsdb/query", bind(dtos.MetricRequest{}), Wrap(hs.QueryMetrics)) apiRoute.Get("/tsdb/testdata/scenarios", Wrap(GetTestDataScenarios)) apiRoute.Get("/tsdb/testdata/gensql", reqGrafanaAdmin, Wrap(GenerateSQLTestData)) apiRoute.Get("/tsdb/testdata/random-walk", Wrap(GetTestDataRandomWalk)) diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 33839ca985d..f455d3dbd29 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -13,19 +13,20 @@ import ( const HeaderNameNoBackendCache = "X-Grafana-NoCache" -func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m.DataSource, error) { +func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) { + nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" cacheKey := fmt.Sprintf("ds-%d", id) if !nocache { if cached, found := hs.cache.Get(cacheKey); found { ds := cached.(*m.DataSource) - if ds.OrgId == orgID { + if ds.OrgId == c.OrgId { return ds, nil } } } - query := m.GetDataSourceByIdQuery{Id: id, OrgId: orgID} + query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId} if err := bus.Dispatch(&query); err != nil { return nil, err } @@ -37,10 +38,7 @@ func (hs *HTTPServer) getDatasourceByID(id int64, orgID int64, nocache bool) (*m func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) - nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - - ds, err := hs.getDatasourceByID(c.ParamsInt64(":id"), c.OrgId, nocache) - + ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c) if err != nil { c.JsonApiErr(500, "Unable to load datasource meta data", err) return diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index f2bc79df7ad..cb80bd346b8 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -13,21 +13,21 @@ import ( ) // POST /api/tsdb/query -func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { +func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { timeRange := tsdb.NewTimeRange(reqDto.From, reqDto.To) if len(reqDto.Queries) == 0 { return Error(400, "No queries found in query", nil) } - dsID, err := reqDto.Queries[0].Get("datasourceId").Int64() + datasourceId, err := reqDto.Queries[0].Get("datasourceId").Int64() if err != nil { return Error(400, "Query missing datasourceId", nil) } - dsQuery := m.GetDataSourceByIdQuery{Id: dsID, OrgId: c.OrgId} - if err := bus.Dispatch(&dsQuery); err != nil { - return Error(500, "failed to fetch data source", err) + ds, err := hs.getDatasourceFromCache(datasourceId, c) + if err != nil { + return Error(500, "Unable to load datasource meta data", err) } request := &tsdb.TsdbQuery{TimeRange: timeRange} @@ -38,11 +38,11 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { MaxDataPoints: query.Get("maxDataPoints").MustInt64(100), IntervalMs: query.Get("intervalMs").MustInt64(1000), Model: query, - DataSource: dsQuery.Result, + DataSource: ds, }) } - resp, err := tsdb.HandleRequest(context.Background(), dsQuery.Result, request) + resp, err := tsdb.HandleRequest(c.Req.Context(), ds, request) if err != nil { return Error(500, "Metric request error", err) } From e58c2ebc1c8255813b04960613ac599855b3f96e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 09:41:37 +0200 Subject: [PATCH 058/127] tech: remove all mobx stuff --- package.json | 4 - .../core/components/PageHeader/PageHeader.tsx | 5 +- .../DisabledPermissionListItem.tsx | 4 +- public/app/core/components/grafana_app.ts | 4 +- public/app/core/services/bridge_srv.ts | 33 +-- .../DashboardPermissions.tsx | 3 +- .../features/plugins/ds_dashboards_ctrl.ts | 15 +- public/app/features/plugins/ds_edit_ctrl.ts | 15 +- public/app/features/plugins/state/navModel.ts | 45 +++ public/app/routes/ReactContainer.tsx | 14 +- .../app/{stores => store}/configureStore.ts | 0 public/app/stores/NavStore/NavItem.ts | 19 -- public/app/stores/NavStore/NavStore.test.ts | 47 ---- public/app/stores/NavStore/NavStore.ts | 118 -------- .../PermissionsStore/PermissionsStore.test.ts | 116 -------- .../PermissionsStore/PermissionsStore.ts | 259 ------------------ .../PermissionsStore/PermissionsStoreItem.ts | 29 -- public/app/stores/RootStore/RootStore.ts | 20 -- public/app/stores/ViewStore/ViewStore.test.ts | 33 --- public/app/stores/ViewStore/ViewStore.ts | 55 ---- public/app/stores/store.ts | 16 -- public/app/types/datasources.ts | 7 + public/app/types/{folder.ts => folders.ts} | 0 public/app/types/index.ts | 6 +- public/app/types/plugins.ts | 19 ++ yarn.lock | 20 +- 26 files changed, 105 insertions(+), 801 deletions(-) create mode 100644 public/app/features/plugins/state/navModel.ts rename public/app/{stores => store}/configureStore.ts (100%) delete mode 100644 public/app/stores/NavStore/NavItem.ts delete mode 100644 public/app/stores/NavStore/NavStore.test.ts delete mode 100644 public/app/stores/NavStore/NavStore.ts delete mode 100644 public/app/stores/PermissionsStore/PermissionsStore.test.ts delete mode 100644 public/app/stores/PermissionsStore/PermissionsStore.ts delete mode 100644 public/app/stores/PermissionsStore/PermissionsStoreItem.ts delete mode 100644 public/app/stores/RootStore/RootStore.ts delete mode 100644 public/app/stores/ViewStore/ViewStore.test.ts delete mode 100644 public/app/stores/ViewStore/ViewStore.ts delete mode 100644 public/app/stores/store.ts create mode 100644 public/app/types/datasources.ts rename public/app/types/{folder.ts => folders.ts} (100%) create mode 100644 public/app/types/plugins.ts diff --git a/package.json b/package.json index 071d32992af..e73c644c5bf 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,6 @@ "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", - "mobx-react-devtools": "^4.2.15", "mocha": "^4.0.1", "ng-annotate-loader": "^0.6.1", "ng-annotate-webpack-plugin": "^0.3.0", @@ -146,9 +145,6 @@ "immutable": "^3.8.2", "jquery": "^3.2.1", "lodash": "^4.17.10", - "mobx": "^3.4.1", - "mobx-react": "^4.3.5", - "mobx-state-tree": "^1.3.1", "moment": "^2.22.2", "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 9feddde68ce..c176095afa4 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,9 +1,7 @@ import React from 'react'; -import { observer } from 'mobx-react'; import { NavModel, NavModelItem } from 'app/types'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; -import { toJS } from 'mobx'; export interface Props { model: NavModel; @@ -81,7 +79,6 @@ const Navigation = ({ main }: { main: NavModelItem }) => { ); }; -@observer export default class PageHeader extends React.Component { constructor(props) { super(props); @@ -148,7 +145,7 @@ export default class PageHeader extends React.Component { return null; } - const main = toJS(model.main); // Convert to JS if its a mobx observable + const main = model.main; return (
diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx index d65595dae66..d648d06e414 100644 --- a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx +++ b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker'; -import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; +import { dashboardPermissionLevels } from 'app/types/acl'; export interface Props { item: any; @@ -24,7 +24,7 @@ export default class DisabledPermissionListItem extends Component {
{}} value={item.permission} disabled={true} diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 926438ffbc9..a0ea0279d30 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -6,11 +6,10 @@ import coreModule from 'app/core/core_module'; import { profiler } from 'app/core/profiler'; import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; -import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { configureStore } from 'app/stores/configureStore'; +import { configureStore } from 'app/store/configureStore'; export class GrafanaCtrl { /** @ngInject */ @@ -28,7 +27,6 @@ export class GrafanaCtrl { // sets singleston instances for angular services so react components can access them configureStore(); setBackendSrv(backendSrv); - createStore({ backendSrv, datasourceSrv }); $scope.init = () => { $scope.contextSrv = contextSrv; diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 29326794ac6..ee184c243ac 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -1,8 +1,6 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; -import { store } from 'app/stores/store'; -import { store as reduxStore } from 'app/stores/configureStore'; -import { reaction } from 'mobx'; +import { store } from 'app/store/configureStore'; import locationUtil from 'app/core/utils/location_util'; import { updateLocation } from 'app/core/actions'; @@ -18,12 +16,9 @@ export class BridgeSrv { init() { this.$rootScope.$on('$routeUpdate', (evt, data) => { const angularUrl = this.$location.url(); - if (store.view.currentUrl !== angularUrl) { - store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); - } - const state = reduxStore.getState(); + const state = store.getState(); if (state.location.url !== angularUrl) { - reduxStore.dispatch( + store.dispatch( updateLocation({ path: this.$location.path(), query: this.$location.search(), @@ -34,8 +29,7 @@ export class BridgeSrv { }); this.$rootScope.$on('$routeChangeSuccess', (evt, data) => { - store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); - reduxStore.dispatch( + store.dispatch( updateLocation({ path: this.$location.path(), query: this.$location.search(), @@ -44,24 +38,9 @@ export class BridgeSrv { ); }); - // listen for mobx store changes and update angular - reaction( - () => store.view.currentUrl, - currentUrl => { - const angularUrl = this.$location.url(); - const url = locationUtil.stripBaseFromUrl(currentUrl); - if (angularUrl !== url) { - this.$timeout(() => { - this.$location.url(url); - }); - console.log('store updating angular $location.url', url); - } - } - ); - // Listen for changes in redux location -> update angular location - reduxStore.subscribe(() => { - const state = reduxStore.getState(); + store.subscribe(() => { + const state = store.getState(); const angularUrl = this.$location.url(); const url = locationUtil.stripBaseFromUrl(state.location.url); if (angularUrl !== url) { diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx index 6ea7ba12721..5651242a485 100644 --- a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx @@ -13,7 +13,7 @@ import { import PermissionList from 'app/core/components/PermissionList/PermissionList'; import AddPermission from 'app/core/components/PermissionList/AddPermission'; import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo'; -import { store } from 'app/stores/configureStore'; +import { store } from 'app/store/configureStore'; export interface Props { dashboardId: number; @@ -65,7 +65,6 @@ export class DashboardPermissions extends PureComponent { render() { const { permissions, folder } = this.props; const { isAdding } = this.state; - console.log('DashboardPermissions', this.props); return (
diff --git a/public/app/features/plugins/ds_dashboards_ctrl.ts b/public/app/features/plugins/ds_dashboards_ctrl.ts index ed7800698b7..a0324215453 100644 --- a/public/app/features/plugins/ds_dashboards_ctrl.ts +++ b/public/app/features/plugins/ds_dashboards_ctrl.ts @@ -1,6 +1,7 @@ -import { toJS } from 'mobx'; import { coreModule } from 'app/core/core'; -import { store } from 'app/stores/store'; +import { store } from 'app/store/configureStore'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { buildNavModel } from './state/navModel'; export class DataSourceDashboardsCtrl { datasourceMeta: any; @@ -9,11 +10,8 @@ export class DataSourceDashboardsCtrl { /** @ngInject */ constructor(private backendSrv, private $routeParams) { - if (store.nav.main === null) { - store.nav.load('cfg', 'datasources'); - } - - this.navModel = toJS(store.nav); + const state = store.getState(); + this.navModel = getNavModel(state.navIndex, 'datasources'); if (this.$routeParams.id) { this.getDatasourceById(this.$routeParams.id); @@ -30,8 +28,7 @@ export class DataSourceDashboardsCtrl { } updateNav() { - store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-dashboards'); - this.navModel = toJS(store.nav); + this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-dashboards'); } getPluginInfo() { diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 19889d3e26e..c223f444ef3 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -1,8 +1,9 @@ import _ from 'lodash'; -import { toJS } from 'mobx'; import config from 'app/core/config'; import { coreModule, appEvents } from 'app/core/core'; -import { store } from 'app/stores/store'; +import { store } from 'app/store/configureStore'; +import { getNavModel } from 'app/core/selectors/navModel'; +import { buildNavModel } from './state/navModel'; let datasourceTypes = []; @@ -31,11 +32,8 @@ export class DataSourceEditCtrl { /** @ngInject */ constructor(private $q, private backendSrv, private $routeParams, private $location, private datasourceSrv) { - if (store.nav.main === null) { - store.nav.load('cfg', 'datasources'); - } - - this.navModel = toJS(store.nav); + const state = store.getState(); + this.navModel = getNavModel(state.navIndex, 'datasources'); this.datasources = []; this.loadDatasourceTypes().then(() => { @@ -101,8 +99,7 @@ export class DataSourceEditCtrl { } updateNav() { - store.nav.initDatasourceEditNav(this.current, this.datasourceMeta, 'datasource-settings'); - this.navModel = toJS(store.nav); + this.navModel = buildNavModel(this.current, this.datasourceMeta, 'datasource-settings'); } typeChanged() { diff --git a/public/app/features/plugins/state/navModel.ts b/public/app/features/plugins/state/navModel.ts new file mode 100644 index 00000000000..852eb2806f9 --- /dev/null +++ b/public/app/features/plugins/state/navModel.ts @@ -0,0 +1,45 @@ +import _ from 'lodash'; +import { DataSource, PluginMeta, NavModel } from 'app/types'; + +export function buildNavModel(ds: DataSource, plugin: PluginMeta, currentPage: string): NavModel { + let title = 'New'; + const subTitle = `Type: ${plugin.name}`; + + if (ds.id) { + title = ds.name; + } + + const main = { + img: plugin.info.logos.large, + id: 'ds-edit-' + plugin.id, + subTitle: subTitle, + url: '', + text: title, + breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }], + children: [ + { + active: currentPage === 'datasource-settings', + icon: 'fa fa-fw fa-sliders', + id: 'datasource-settings', + text: 'Settings', + url: `datasources/edit/${ds.id}`, + }, + ], + }; + + const hasDashboards = _.find(plugin.includes, { type: 'dashboard' }) !== undefined; + if (hasDashboards && ds.id) { + main.children.push({ + active: currentPage === 'datasource-dashboards', + icon: 'fa fa-fw fa-th-large', + id: 'datasource-dashboards', + text: 'Dashboards', + url: `datasources/edit/${ds.id}/dashboards`, + }); + } + + return { + main: main, + node: _.find(main.children, { active: true }), + }; +} diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index 8a3d7e643f9..ed4d2d21827 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -1,22 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Provider } from 'mobx-react'; -import { Provider as ReduxProvider } from 'react-redux'; +import { Provider } from 'react-redux'; import coreModule from 'app/core/core_module'; -import { store } from 'app/stores/store'; -import { store as reduxStore } from 'app/stores/configureStore'; +import { store } from 'app/store/configureStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { ContextSrv } from 'app/core/services/context_srv'; function WrapInProvider(store, Component, props) { return ( - - - - - + + + ); } diff --git a/public/app/stores/configureStore.ts b/public/app/store/configureStore.ts similarity index 100% rename from public/app/stores/configureStore.ts rename to public/app/store/configureStore.ts diff --git a/public/app/stores/NavStore/NavItem.ts b/public/app/stores/NavStore/NavItem.ts deleted file mode 100644 index 3e8a2a837b3..00000000000 --- a/public/app/stores/NavStore/NavItem.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { types } from 'mobx-state-tree'; - -export const NavItem = types.model('NavItem', { - id: types.identifier(types.string), - text: types.string, - url: types.optional(types.string, ''), - subTitle: types.optional(types.string, ''), - icon: types.optional(types.string, ''), - img: types.optional(types.string, ''), - active: types.optional(types.boolean, false), - hideFromTabs: types.optional(types.boolean, false), - breadcrumbs: types.optional(types.array(types.late(() => Breadcrumb)), []), - children: types.optional(types.array(types.late(() => NavItem)), []), -}); - -export const Breadcrumb = types.model('Breadcrumb', { - title: types.string, - url: types.string, -}); diff --git a/public/app/stores/NavStore/NavStore.test.ts b/public/app/stores/NavStore/NavStore.test.ts deleted file mode 100644 index 43d4496c858..00000000000 --- a/public/app/stores/NavStore/NavStore.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { NavStore } from './NavStore'; - -describe('NavStore', () => { - const folderId = 1; - const folderTitle = 'Folder Name'; - const folderUrl = '/dashboards/f/uid/folder-name'; - const canAdmin = true; - - const folder = { - id: folderId, - url: folderUrl, - title: folderTitle, - canAdmin: canAdmin, - }; - - let store; - - beforeEach(() => { - store = NavStore.create(); - store.initFolderNav(folder, 'manage-folder-settings'); - }); - - it('Should set text', () => { - expect(store.main.text).toBe(folderTitle); - }); - - it('Should load nav with tabs', () => { - expect(store.main.children.length).toBe(3); - expect(store.main.children[0].id).toBe('manage-folder-dashboards'); - expect(store.main.children[1].id).toBe('manage-folder-permissions'); - expect(store.main.children[2].id).toBe('manage-folder-settings'); - }); - - it('Should set correct urls for each tab', () => { - expect(store.main.children.length).toBe(3); - expect(store.main.children[0].url).toBe(folderUrl); - expect(store.main.children[1].url).toBe(`${folderUrl}/permissions`); - expect(store.main.children[2].url).toBe(`${folderUrl}/settings`); - }); - - it('Should set active tab', () => { - expect(store.main.children.length).toBe(3); - expect(store.main.children[0].active).toBe(false); - expect(store.main.children[1].active).toBe(false); - expect(store.main.children[2].active).toBe(true); - }); -}); diff --git a/public/app/stores/NavStore/NavStore.ts b/public/app/stores/NavStore/NavStore.ts deleted file mode 100644 index f87cc486b41..00000000000 --- a/public/app/stores/NavStore/NavStore.ts +++ /dev/null @@ -1,118 +0,0 @@ -import _ from 'lodash'; -import { types, getEnv } from 'mobx-state-tree'; -import { NavItem } from './NavItem'; - -export const NavStore = types - .model('NavStore', { - main: types.maybe(NavItem), - node: types.maybe(NavItem), - }) - .actions(self => ({ - load(...args) { - let children = getEnv(self).navTree; - let main, node; - const parents = []; - - for (const id of args) { - node = children.find(el => el.id === id); - - if (!node) { - throw new Error(`NavItem with id ${id} not found`); - } - - children = node.children; - parents.push(node); - } - - main = parents[parents.length - 2]; - - if (main.children) { - for (const item of main.children) { - item.active = false; - - if (item.url === node.url) { - item.active = true; - } - } - } - - self.main = NavItem.create(main); - self.node = NavItem.create(node); - }, - - initFolderNav(folder: any, activeChildId: string) { - const main = { - icon: 'fa fa-folder-open', - id: 'manage-folder', - subTitle: 'Manage folder dashboards & permissions', - url: '', - text: folder.title, - breadcrumbs: [{ title: 'Dashboards', url: 'dashboards' }], - children: [ - { - active: activeChildId === 'manage-folder-dashboards', - icon: 'fa fa-fw fa-th-large', - id: 'manage-folder-dashboards', - text: 'Dashboards', - url: folder.url, - }, - { - active: activeChildId === 'manage-folder-permissions', - icon: 'fa fa-fw fa-lock', - id: 'manage-folder-permissions', - text: 'Permissions', - url: `${folder.url}/permissions`, - }, - { - active: activeChildId === 'manage-folder-settings', - icon: 'fa fa-fw fa-cog', - id: 'manage-folder-settings', - text: 'Settings', - url: `${folder.url}/settings`, - }, - ], - }; - - self.main = NavItem.create(main); - }, - - initDatasourceEditNav(ds: any, plugin: any, currentPage: string) { - let title = 'New'; - const subTitle = `Type: ${plugin.name}`; - - if (ds.id) { - title = ds.name; - } - - const main = { - img: plugin.info.logos.large, - id: 'ds-edit-' + plugin.id, - subTitle: subTitle, - url: '', - text: title, - breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }], - children: [ - { - active: currentPage === 'datasource-settings', - icon: 'fa fa-fw fa-sliders', - id: 'datasource-settings', - text: 'Settings', - url: `datasources/edit/${ds.id}`, - }, - ], - }; - - const hasDashboards = _.find(plugin.includes, { type: 'dashboard' }) !== undefined; - if (hasDashboards && ds.id) { - main.children.push({ - active: currentPage === 'datasource-dashboards', - icon: 'fa fa-fw fa-th-large', - id: 'datasource-dashboards', - text: 'Dashboards', - url: `datasources/edit/${ds.id}/dashboards`, - }); - } - - self.main = NavItem.create(main); - }, - })); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.test.ts b/public/app/stores/PermissionsStore/PermissionsStore.test.ts deleted file mode 100644 index 6d88401e0d6..00000000000 --- a/public/app/stores/PermissionsStore/PermissionsStore.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { PermissionsStore } from './PermissionsStore'; -import { backendSrv } from 'test/mocks/common'; - -describe('PermissionsStore', () => { - let store; - - beforeEach(async () => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - { - id: 4, - dashboardId: 10, - permission: 1, - permissionName: 'View', - teamId: 1, - team: 'MyTestTeam', - inherited: true, - }, - { - id: 5, - dashboardId: 1, - permission: 1, - permissionName: 'View', - userId: 1, - userLogin: 'MyTestUser', - }, - { - id: 6, - dashboardId: 1, - permission: 1, - permissionName: 'Edit', - teamId: 2, - team: 'MyTestTeam2', - }, - ]) - ); - - backendSrv.post = jest.fn(() => Promise.resolve({})); - - store = PermissionsStore.create( - { - fetching: false, - items: [], - }, - { - backendSrv: backendSrv, - } - ); - - await store.load(1, false, false); - }); - - it('should save update on permission change', async () => { - expect(store.items[0].permission).toBe(1); - expect(store.items[0].permissionName).toBe('View'); - - await store.updatePermissionOnIndex(0, 2, 'Edit'); - - expect(store.items[0].permission).toBe(2); - expect(store.items[0].permissionName).toBe('Edit'); - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - - it('should save removed permissions automatically', async () => { - expect(store.items.length).toBe(5); - - await store.removeStoreItem(2); - - expect(store.items.length).toBe(4); - expect(backendSrv.post.mock.calls.length).toBe(1); - expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions'); - }); - - it('should be sorted by sort rank and alphabetically', async () => { - expect(store.items[0].name).toBe('MyTestTeam'); - expect(store.items[0].dashboardId).toBe(10); - expect(store.items[1].name).toBe('Editor'); - expect(store.items[2].name).toBe('Viewer'); - expect(store.items[3].name).toBe('MyTestTeam2'); - expect(store.items[4].name).toBe('MyTestUser'); - }); - - describe('when one inherited and one not inherited team permission are added', () => { - beforeEach(async () => { - const overridingItemForChildDashboard = { - team: 'MyTestTeam', - dashboardId: 1, - teamId: 1, - permission: 2, - }; - - store.resetNewType(); - store.newItem.setTeam(overridingItemForChildDashboard.teamId, overridingItemForChildDashboard.team); - store.newItem.setPermission(overridingItemForChildDashboard.permission); - await store.addStoreItem(); - }); - - it('should add new overriding permission', () => { - expect(store.items.length).toBe(6); - }); - - it('should be sorted by sort rank and alphabetically', async () => { - expect(store.items[0].name).toBe('MyTestTeam'); - expect(store.items[0].dashboardId).toBe(10); - expect(store.items[1].name).toBe('Editor'); - expect(store.items[2].name).toBe('Viewer'); - expect(store.items[3].name).toBe('MyTestTeam'); - expect(store.items[3].dashboardId).toBe(1); - expect(store.items[4].name).toBe('MyTestTeam2'); - expect(store.items[5].name).toBe('MyTestUser'); - }); - }); -}); diff --git a/public/app/stores/PermissionsStore/PermissionsStore.ts b/public/app/stores/PermissionsStore/PermissionsStore.ts deleted file mode 100644 index d778a09443d..00000000000 --- a/public/app/stores/PermissionsStore/PermissionsStore.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { types, getEnv, flow } from 'mobx-state-tree'; -import { PermissionsStoreItem } from './PermissionsStoreItem'; - -export const permissionOptions = [ - { value: 1, label: 'View', description: 'Can view dashboards.' }, - { value: 2, label: 'Edit', description: 'Can add, edit and delete dashboards.' }, - { - value: 4, - label: 'Admin', - description: 'Can add/remove permissions and can add, edit and delete dashboards.', - }, -]; - -export const aclTypeValues = { - GROUP: { value: 'Group', text: 'Team' }, - USER: { value: 'User', text: 'User' }, - VIEWER: { value: 'Viewer', text: 'Everyone With Viewer Role' }, - EDITOR: { value: 'Editor', text: 'Everyone With Editor Role' }, -}; - -export const aclTypes = Object.keys(aclTypeValues).map(item => aclTypeValues[item]); - -const defaultNewType = aclTypes[0].value; - -export const NewPermissionsItem = types - .model('NewPermissionsItem', { - type: types.optional( - types.enumeration(Object.keys(aclTypeValues).map(item => aclTypeValues[item].value)), - defaultNewType - ), - userId: types.maybe(types.number), - userLogin: types.maybe(types.string), - userAvatarUrl: types.maybe(types.string), - teamAvatarUrl: types.maybe(types.string), - teamId: types.maybe(types.number), - team: types.maybe(types.string), - permission: types.optional(types.number, 1), - }) - .views(self => ({ - isValid: () => { - switch (self.type) { - case aclTypeValues.GROUP.value: - return self.teamId && self.team; - case aclTypeValues.USER.value: - return !!self.userId && !!self.userLogin; - case aclTypeValues.VIEWER.value: - case aclTypeValues.EDITOR.value: - return true; - default: - return false; - } - }, - })) - .actions(self => ({ - setUser(userId: number, userLogin: string, userAvatarUrl: string) { - self.userId = userId; - self.userLogin = userLogin; - self.userAvatarUrl = userAvatarUrl; - self.teamId = null; - self.team = null; - }, - setTeam(teamId: number, team: string, teamAvatarUrl: string) { - self.userId = null; - self.userLogin = null; - self.teamId = teamId; - self.team = team; - self.teamAvatarUrl = teamAvatarUrl; - }, - setPermission(permission: number) { - self.permission = permission; - }, - })); - -export const PermissionsStore = types - .model('PermissionsStore', { - fetching: types.boolean, - isFolder: types.maybe(types.boolean), - dashboardId: types.maybe(types.number), - items: types.optional(types.array(PermissionsStoreItem), []), - originalItems: types.optional(types.array(PermissionsStoreItem), []), - newType: types.optional(types.string, defaultNewType), - newItem: types.maybe(NewPermissionsItem), - isAddPermissionsVisible: types.optional(types.boolean, false), - isInRoot: types.maybe(types.boolean), - }) - .views(self => ({ - isValid: item => { - const dupe = self.items.find(it => { - return isDuplicate(it, item); - }); - if (dupe) { - return false; - } - - return true; - }, - })) - .actions(self => { - const resetNewTypeInternal = () => { - self.newItem = NewPermissionsItem.create(); - }; - - return { - load: flow(function* load(dashboardId: number, isFolder: boolean, isInRoot: boolean) { - const backendSrv = getEnv(self).backendSrv; - self.fetching = true; - self.isFolder = isFolder; - self.isInRoot = isInRoot; - self.dashboardId = dashboardId; - self.items.clear(); - - const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/permissions`); - const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot); - self.items = items; - self.originalItems = items; - self.fetching = false; - }), - - addStoreItem: flow(function* addStoreItem() { - const item = { - type: self.newItem.type, - permission: self.newItem.permission, - dashboardId: self.dashboardId, - team: undefined, - teamId: undefined, - userLogin: undefined, - userId: undefined, - userAvatarUrl: undefined, - teamAvatarUrl: undefined, - role: undefined, - }; - switch (self.newItem.type) { - case aclTypeValues.GROUP.value: - item.team = self.newItem.team; - item.teamId = self.newItem.teamId; - item.teamAvatarUrl = self.newItem.teamAvatarUrl; - break; - case aclTypeValues.USER.value: - item.userLogin = self.newItem.userLogin; - item.userId = self.newItem.userId; - item.userAvatarUrl = self.newItem.userAvatarUrl; - break; - case aclTypeValues.VIEWER.value: - case aclTypeValues.EDITOR.value: - item.role = self.newItem.type; - break; - default: - throw Error('Unknown type: ' + self.newItem.type); - } - - const updatedItems = self.items.peek(); - const newItem = prepareItem(item, self.dashboardId, self.isFolder, self.isInRoot); - updatedItems.push(newItem); - - try { - yield updateItems(self, updatedItems); - self.items.push(newItem); - const sortedItems = self.items.sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); - self.items = sortedItems; - resetNewTypeInternal(); - } catch {} - yield Promise.resolve(); - }), - - removeStoreItem: flow(function* removeStoreItem(idx: number) { - self.items.splice(idx, 1); - yield updateItems(self, self.items.peek()); - }), - - updatePermissionOnIndex: flow(function* updatePermissionOnIndex( - idx: number, - permission: number, - permissionName: string - ) { - self.items[idx].updatePermission(permission, permissionName); - yield updateItems(self, self.items.peek()); - }), - - setNewType(newType: string) { - self.newItem = NewPermissionsItem.create({ type: newType }); - }, - - resetNewType() { - resetNewTypeInternal(); - }, - - toggleAddPermissions() { - self.isAddPermissionsVisible = !self.isAddPermissionsVisible; - }, - - hideAddPermissions() { - self.isAddPermissionsVisible = false; - }, - }; - }); - -const updateItems = (self, items) => { - const backendSrv = getEnv(self).backendSrv; - const updated = []; - for (const item of items) { - if (item.inherited) { - continue; - } - updated.push({ - id: item.id, - userId: item.userId, - teamId: item.teamId, - role: item.role, - permission: item.permission, - }); - } - - return backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, { - items: updated, - }); -}; - -const prepareServerResponse = (response, dashboardId: number, isFolder: boolean, isInRoot: boolean) => { - return response - .map(item => { - return prepareItem(item, dashboardId, isFolder, isInRoot); - }) - .sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name)); -}; - -const prepareItem = (item, dashboardId: number, isFolder: boolean, isInRoot: boolean) => { - item.sortRank = 0; - if (item.userId > 0) { - item.name = item.userLogin; - item.sortRank = 10; - } else if (item.teamId > 0) { - item.name = item.team; - item.sortRank = 20; - } else if (item.role) { - item.icon = 'fa fa-fw fa-street-view'; - item.name = item.role; - item.sortRank = 30; - if (item.role === 'Editor') { - item.sortRank += 1; - } - } - - if (item.inherited) { - item.sortRank += 100; - } - return item; -}; - -const isDuplicate = (origItem, newItem) => { - if (origItem.inherited) { - return false; - } - - return ( - (origItem.role && newItem.role && origItem.role === newItem.role) || - (origItem.userId && newItem.userId && origItem.userId === newItem.userId) || - (origItem.teamId && newItem.teamId && origItem.teamId === newItem.teamId) - ); -}; diff --git a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts b/public/app/stores/PermissionsStore/PermissionsStoreItem.ts deleted file mode 100644 index c4873cb9c01..00000000000 --- a/public/app/stores/PermissionsStore/PermissionsStoreItem.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { types } from 'mobx-state-tree'; - -export const PermissionsStoreItem = types - .model('PermissionsStoreItem', { - dashboardId: types.optional(types.number, -1), - permission: types.number, - permissionName: types.maybe(types.string), - role: types.maybe(types.string), - team: types.optional(types.string, ''), - teamId: types.optional(types.number, 0), - userEmail: types.optional(types.string, ''), - userId: types.optional(types.number, 0), - userLogin: types.optional(types.string, ''), - inherited: types.maybe(types.boolean), - sortRank: types.maybe(types.number), - icon: types.maybe(types.string), - name: types.maybe(types.string), - teamAvatarUrl: types.maybe(types.string), - userAvatarUrl: types.maybe(types.string), - }) - .actions(self => ({ - updateRole: role => { - self.role = role; - }, - updatePermission(permission: number, permissionName: string) { - self.permission = permission; - self.permissionName = permissionName; - }, - })); diff --git a/public/app/stores/RootStore/RootStore.ts b/public/app/stores/RootStore/RootStore.ts deleted file mode 100644 index 68125fd1f4c..00000000000 --- a/public/app/stores/RootStore/RootStore.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { types } from 'mobx-state-tree'; -import { NavStore } from './../NavStore/NavStore'; -import { ViewStore } from './../ViewStore/ViewStore'; -import { PermissionsStore } from './../PermissionsStore/PermissionsStore'; - -export const RootStore = types.model({ - nav: types.optional(NavStore, {}), - permissions: types.optional(PermissionsStore, { - fetching: false, - items: [], - }), - view: types.optional(ViewStore, { - path: '', - query: {}, - routeParams: {}, - }), -}); - -type RootStoreType = typeof RootStore.Type; -export interface RootStoreInterface extends RootStoreType {} diff --git a/public/app/stores/ViewStore/ViewStore.test.ts b/public/app/stores/ViewStore/ViewStore.test.ts deleted file mode 100644 index 18fddd20fd6..00000000000 --- a/public/app/stores/ViewStore/ViewStore.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ViewStore } from './ViewStore'; -import { toJS } from 'mobx'; - -describe('ViewStore', () => { - let store; - - beforeAll(() => { - store = ViewStore.create({ - path: '', - query: {}, - routeParams: {}, - }); - }); - - it('Can update path and query', () => { - store.updatePathAndQuery('/hello', { key: 1, otherParam: 'asd' }, { key: 1, otherParam: 'asd' }); - expect(store.path).toBe('/hello'); - expect(store.query.get('key')).toBe(1); - expect(store.currentUrl).toBe('/hello?key=1&otherParam=asd'); - }); - - it('Query can contain arrays', () => { - store.updatePathAndQuery('/hello', { values: ['A', 'B'] }, { key: 1, otherParam: 'asd' }); - expect(toJS(store.query.get('values'))).toMatchObject(['A', 'B']); - expect(store.currentUrl).toBe('/hello?values=A&values=B'); - }); - - it('Query can contain boolean', () => { - store.updatePathAndQuery('/hello', { abool: true }, { abool: true }); - expect(toJS(store.query.get('abool'))).toBe(true); - expect(store.currentUrl).toBe('/hello?abool'); - }); -}); diff --git a/public/app/stores/ViewStore/ViewStore.ts b/public/app/stores/ViewStore/ViewStore.ts deleted file mode 100644 index 3af6737209c..00000000000 --- a/public/app/stores/ViewStore/ViewStore.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { types } from 'mobx-state-tree'; -import { toJS } from 'mobx'; -import { toUrlParams } from 'app/core/utils/url'; - -const QueryInnerValueType = types.union(types.string, types.boolean, types.number); -const QueryValueType = types.union(QueryInnerValueType, types.array(QueryInnerValueType)); - -export const ViewStore = types - .model({ - path: types.string, - query: types.map(QueryValueType), - routeParams: types.map(QueryValueType), - }) - .views(self => ({ - get currentUrl() { - let path = self.path; - - if (self.query.size) { - path += '?' + toUrlParams(toJS(self.query)); - } - return path; - }, - })) - .actions(self => { - // querystring only - function updateQuery(query: any) { - self.query.clear(); - for (const key of Object.keys(query)) { - if (query[key]) { - self.query.set(key, query[key]); - } - } - } - - // needed to get route parameters like slug from the url - function updateRouteParams(routeParams: any) { - self.routeParams.clear(); - for (const key of Object.keys(routeParams)) { - if (routeParams[key]) { - self.routeParams.set(key, routeParams[key]); - } - } - } - - function updatePathAndQuery(path: string, query: any, routeParams: any) { - self.path = path; - updateQuery(query); - updateRouteParams(routeParams); - } - - return { - updateQuery, - updatePathAndQuery, - }; - }); diff --git a/public/app/stores/store.ts b/public/app/stores/store.ts deleted file mode 100644 index 10acbfe4907..00000000000 --- a/public/app/stores/store.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { RootStore, RootStoreInterface } from './RootStore/RootStore'; -import config from 'app/core/config'; - -export let store: RootStoreInterface; - -export function createStore(services) { - store = RootStore.create( - {}, - { - ...services, - navTree: config.bootData.navTree, - } - ); - - return store; -} diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts new file mode 100644 index 00000000000..78ff7b0724c --- /dev/null +++ b/public/app/types/datasources.ts @@ -0,0 +1,7 @@ +export interface DataSource { + id: number; + orgId: number; + name: string; + typeLogoUrl: string; + type: string; +} diff --git a/public/app/types/folder.ts b/public/app/types/folders.ts similarity index 100% rename from public/app/types/folder.ts rename to public/app/types/folders.ts diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 8fcfcc7e88d..778a1b21b55 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -2,9 +2,11 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams'; import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting'; import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location'; import { NavModel, NavModelItem, NavIndex } from './navModel'; -import { FolderDTO, FolderState, FolderInfo } from './folder'; +import { FolderDTO, FolderState, FolderInfo } from './folders'; import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; +import { DataSource } from './datasources'; +import { PluginMeta } from './plugins'; export { Team, @@ -29,6 +31,8 @@ export { DashboardAcl, OrgRole, PermissionLevel, + DataSource, + PluginMeta, }; export interface StoreState { diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts new file mode 100644 index 00000000000..d26085f8e73 --- /dev/null +++ b/public/app/types/plugins.ts @@ -0,0 +1,19 @@ +export interface PluginMeta { + id: string; + name: string; + info: PluginMetaInfo; + includes: PluginInclude[]; +} + +export interface PluginInclude { + type: string; + name: string; + path: string; +} + +export interface PluginMetaInfo { + logos: { + large: string; + small: string; + }; +} diff --git a/yarn.lock b/yarn.lock index 2b98ff32766..008ebc9a625 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5258,7 +5258,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: +hoist-non-react-statics@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -7593,24 +7593,6 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" -mobx-react-devtools@^4.2.15: - version "4.2.15" - resolved "https://registry.yarnpkg.com/mobx-react-devtools/-/mobx-react-devtools-4.2.15.tgz#881c038fb83db4dffd1e72bbaf5374d26b2fdebb" - -mobx-react@^4.3.5: - version "4.4.3" - resolved "http://registry.npmjs.org/mobx-react/-/mobx-react-4.4.3.tgz#baa9ec41165ee35ae7b9df19bca10190f36f117e" - dependencies: - hoist-non-react-statics "^2.3.1" - -mobx-state-tree@^1.3.1: - version "1.4.0" - resolved "http://registry.npmjs.org/mobx-state-tree/-/mobx-state-tree-1.4.0.tgz#c914c855d5ec5c1c16e4ba6d6925679df42c8110" - -mobx@^3.4.1: - version "3.6.2" - resolved "https://registry.yarnpkg.com/mobx/-/mobx-3.6.2.tgz#fb9f5ff5090539a1ad54e75dc4c098b602693320" - mocha@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" From 84a4b641768de6b8bc541446de45c5152262b8b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 09:57:58 +0200 Subject: [PATCH 059/127] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2357fbfa8..1c4a9edcee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * **Prometheus**: Label completion queries respect dashboard time range [#12251](https://github.com/grafana/grafana/pull/12251), thx [@mtanda](https://github.com/mtanda) * **Prometheus**: Allow to display annotations based on Prometheus series value [#10159](https://github.com/grafana/grafana/issues/10159), thx [@mtanda](https://github.com/mtanda) * **Prometheus**: Adhoc-filtering for Prometheus dashboards [#13212](https://github.com/grafana/grafana/issues/13212) +* **Singlestat**: Fix gauge display accuracy for percents [#13270](https://github.com/grafana/grafana/issues/13270), thx [@tianon](https://github.com/tianon) # 5.3.0 (unreleased) From 0bf5a6ad710cc159a598fc7cbbb19031a9ac7741 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 14 Sep 2018 07:46:04 +0200 Subject: [PATCH 060/127] metrics: starts some counters at zero without starting the counter as zero Grafana will not send any metrics to graphite using the bridge. --- pkg/metrics/metrics.go | 66 +++++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index e2cdb5656b0..9a514fdb6f3 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -61,6 +61,23 @@ var ( M_Grafana_Version *prometheus.GaugeVec ) +func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec { + counter := prometheus.NewCounterVec(opts, labels) + + for _, label := range labelValues { + counter.WithLabelValues(label).Add(0) + } + + return counter +} + +func newCounterStartingAtZero(opts prometheus.CounterOpts, labelValues ...string) prometheus.Counter { + counter := prometheus.NewCounter(opts) + counter.Add(0) + + return counter +} + func init() { M_Instance_Start = prometheus.NewCounter(prometheus.CounterOpts{ Name: "instance_start_total", @@ -68,32 +85,27 @@ func init() { Namespace: exporterName, }) - M_Page_Status = prometheus.NewCounterVec( + httpStatusCodes := []string{"200", "404", "500", "unknown"} + M_Page_Status = newCounterVecStartingAtZero( prometheus.CounterOpts{ Name: "page_response_status_total", Help: "page http response status", Namespace: exporterName, - }, - []string{"code"}, - ) + }, []string{"code"}, httpStatusCodes...) - M_Api_Status = prometheus.NewCounterVec( + M_Api_Status = newCounterVecStartingAtZero( prometheus.CounterOpts{ Name: "api_response_status_total", Help: "api http response status", Namespace: exporterName, - }, - []string{"code"}, - ) + }, []string{"code"}, httpStatusCodes...) - M_Proxy_Status = prometheus.NewCounterVec( + M_Proxy_Status = newCounterVecStartingAtZero( prometheus.CounterOpts{ Name: "proxy_response_status_total", Help: "proxy http response status", Namespace: exporterName, - }, - []string{"code"}, - ) + }, []string{"code"}, httpStatusCodes...) M_Http_Request_Total = prometheus.NewCounterVec( prometheus.CounterOpts{ @@ -111,19 +123,19 @@ func init() { []string{"handler", "statuscode", "method"}, ) - M_Api_User_SignUpStarted = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_User_SignUpStarted = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_user_signup_started_total", Help: "amount of users who started the signup flow", Namespace: exporterName, }) - M_Api_User_SignUpCompleted = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_User_SignUpCompleted = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_user_signup_completed_total", Help: "amount of users who completed the signup flow", Namespace: exporterName, }) - M_Api_User_SignUpInvite = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_User_SignUpInvite = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_user_signup_invite_total", Help: "amount of users who have been invited", Namespace: exporterName, @@ -147,49 +159,49 @@ func init() { Namespace: exporterName, }) - M_Api_Admin_User_Create = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Admin_User_Create = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_admin_user_created_total", Help: "api admin user created counter", Namespace: exporterName, }) - M_Api_Login_Post = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Login_Post = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_login_post_total", Help: "api login post counter", Namespace: exporterName, }) - M_Api_Login_OAuth = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Login_OAuth = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_login_oauth_total", Help: "api login oauth counter", Namespace: exporterName, }) - M_Api_Org_Create = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Org_Create = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_org_create_total", Help: "api org created counter", Namespace: exporterName, }) - M_Api_Dashboard_Snapshot_Create = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Dashboard_Snapshot_Create = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_dashboard_snapshot_create_total", Help: "dashboard snapshots created", Namespace: exporterName, }) - M_Api_Dashboard_Snapshot_External = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Dashboard_Snapshot_External = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_dashboard_snapshot_external_total", Help: "external dashboard snapshots created", Namespace: exporterName, }) - M_Api_Dashboard_Snapshot_Get = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Dashboard_Snapshot_Get = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_dashboard_snapshot_get_total", Help: "loaded dashboards", Namespace: exporterName, }) - M_Api_Dashboard_Insert = prometheus.NewCounter(prometheus.CounterOpts{ + M_Api_Dashboard_Insert = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "api_models_dashboard_insert_total", Help: "dashboards inserted ", Namespace: exporterName, @@ -207,25 +219,25 @@ func init() { Namespace: exporterName, }, []string{"type"}) - M_Aws_CloudWatch_GetMetricStatistics = prometheus.NewCounter(prometheus.CounterOpts{ + M_Aws_CloudWatch_GetMetricStatistics = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "aws_cloudwatch_get_metric_statistics_total", Help: "counter for getting metric statistics from aws", Namespace: exporterName, }) - M_Aws_CloudWatch_ListMetrics = prometheus.NewCounter(prometheus.CounterOpts{ + M_Aws_CloudWatch_ListMetrics = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "aws_cloudwatch_list_metrics_total", Help: "counter for getting list of metrics from aws", Namespace: exporterName, }) - M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{ + M_Aws_CloudWatch_GetMetricData = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "aws_cloudwatch_get_metric_data_total", Help: "counter for getting metric data time series from aws", Namespace: exporterName, }) - M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{ + M_DB_DataSource_QueryById = newCounterStartingAtZero(prometheus.CounterOpts{ Name: "db_datasource_query_by_id_total", Help: "counter for getting datasource by id", Namespace: exporterName, From 5fbe8eff4fd5f669b7a25aa11935665a520426ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 11:28:17 +0200 Subject: [PATCH 061/127] ldap: made minor change to group search, and to docs --- conf/ldap.toml | 32 +++----------------------------- docs/sources/auth/ldap.md | 8 +++++--- pkg/login/ldap.go | 16 ++++++++++------ 3 files changed, 18 insertions(+), 38 deletions(-) diff --git a/conf/ldap.toml b/conf/ldap.toml index 9a7088ed823..b684f2556d5 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -31,37 +31,11 @@ search_filter = "(cn=%s)" # An array of base dns to search through search_base_dns = ["dc=grafana,dc=org"] -# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups. -# This is done by enabling group_search_filter below. You must also set member_of= "cn" -# in [servers.attributes] below. - -# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN -# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of -# below in such a way that the user's recursive group membership is considered. -# -# Nested Groups + Active Directory (AD) Example: -# -# AD groups store the Distinguished Names (DNs) of members, so your filter must -# recursively search your groups for the authenticating user's DN. For example: -# -# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)" -# group_search_filter_user_attribute = "distinguishedName" -# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] -# -# [servers.attributes] -# ... -# member_of = "distinguishedName" - -## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) +## For Posix or LDAP setups that does not support member_of attribute you can define the below settings +## Please check grafana LDAP docs for examples # group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" -## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter. -## Defaults to the value of username in [server.attributes] -## Valid options are any of your values in [servers.attributes] -## If you are using nested groups you probably want to set this and member_of in -## [servers.attributes] to "distinguishedName" -# group_search_filter_user_attribute = "distinguishedName" -## An array of the base DNs to search through for groups. Typically uses ou=groups # group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] +# group_search_filter_user_attribute = "uid" # Specify names of the ldap attributes your ldap uses [servers.attributes] diff --git a/docs/sources/auth/ldap.md b/docs/sources/auth/ldap.md index 8e95e24a0b0..82db8214fb7 100644 --- a/docs/sources/auth/ldap.md +++ b/docs/sources/auth/ldap.md @@ -121,9 +121,11 @@ If your ldap server does not support the memberOf attribute add these options: group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" ## An array of the base DNs to search through for groups. Typically uses ou=groups group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] +## the %s in the search filter will be replaced with the attribute defined below +group_search_filter_user_attribute = "uid" ``` -Also change set `member_of = "cn"` in the `[servers.attributes]` section. +Also set `member_of = "dn"` in the `[servers.attributes]` section. ### Group Mappings @@ -177,10 +179,10 @@ Multiple DN templates can be searched by combining filters with the LDAP OR-oper ```bash group_search_filter = "(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])" group_search_filter = "(|(member:1.2.840.113556.1.4.1941:=CN=%s,[user container/OU])(member:1.2.840.113556.1.4.1941:=CN=%s,[another user container/OU]))" +group_search_filter_user_attribute = "cn" ``` -For troubleshooting, by changing `member_of` in `[servers.attributes]` to "distinguishedName" it will show you more accurate group memberships when [debug is enabled](#troubleshooting). - +For troubleshooting, by changing `member_of` in `[servers.attributes]` to "dn" it will show you more accurate group memberships when [debug is enabled](#troubleshooting). ## Configuration examples diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 053778e8deb..43f45f900d9 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -326,15 +326,19 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { a.log.Info("Searching for user's groups", "filter", filter) + // support old way of reading settings + groupIdAttribute := a.server.Attr.MemberOf + // but prefer dn attribute if default settings are used + if groupIdAttribute == "" || groupIdAttribute == "memberOf" { + groupIdAttribute = "dn" + } + groupSearchReq := ldap.SearchRequest{ BaseDN: groupSearchBase, Scope: ldap.ScopeWholeSubtree, DerefAliases: ldap.NeverDerefAliases, - Attributes: []string{ - // Here MemberOf would be the thing that identifies the group, which is normally 'cn' - a.server.Attr.MemberOf, - }, - Filter: filter, + Attributes: []string{groupIdAttribute}, + Filter: filter, } groupSearchResult, err = a.conn.Search(&groupSearchReq) @@ -344,7 +348,7 @@ func (a *ldapAuther) searchForUser(username string) (*LdapUserInfo, error) { if len(groupSearchResult.Entries) > 0 { for i := range groupSearchResult.Entries { - memberOf = append(memberOf, getLdapAttrN(a.server.Attr.MemberOf, groupSearchResult, i)) + memberOf = append(memberOf, getLdapAttrN(groupIdAttribute, groupSearchResult, i)) } break } From 1a38c45dde08335086d0c432a40255e30b4d1607 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 14 Sep 2018 16:09:43 +0200 Subject: [PATCH 062/127] Hotfix for Explore (empty page after running query) Since #13212 adhoc filters are being gathered, in Explore the template service has no variables set and then throws when iterating over them. --- .../app/features/templating/template_srv.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index def9fda1f56..70fd287402f 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -50,18 +50,20 @@ export class TemplateSrv { getAdhocFilters(datasourceName) { let filters = []; - for (let i = 0; i < this.variables.length; i++) { - const variable = this.variables[i]; - if (variable.type !== 'adhoc') { - continue; - } + if (this.variables) { + for (let i = 0; i < this.variables.length; i++) { + const variable = this.variables[i]; + if (variable.type !== 'adhoc') { + continue; + } - // null is the "default" datasource - if (variable.datasource === null || variable.datasource === datasourceName) { - filters = filters.concat(variable.filters); - } else if (variable.datasource.indexOf('$') === 0) { - if (this.replace(variable.datasource) === datasourceName) { + // null is the "default" datasource + if (variable.datasource === null || variable.datasource === datasourceName) { filters = filters.concat(variable.filters); + } else if (variable.datasource.indexOf('$') === 0) { + if (this.replace(variable.datasource) === datasourceName) { + filters = filters.concat(variable.filters); + } } } } From face5b1890d9d89aa988c5267407c1918bff14c6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 14 Sep 2018 16:32:54 +0200 Subject: [PATCH 063/127] Explore: Add multiline syntax highlighting to query field - the non-nested query field schema did not allow for multi-line highlighting - added explicit code schema and a `makeValue` function that enforces the nested structure - replaced vendored prism-slate adapter with official slate-prism package - renamed language to syntax --- package.json | 1 + .../app/containers/Explore/PromQueryField.tsx | 21 ++- public/app/containers/Explore/QueryField.tsx | 28 +--- public/app/containers/Explore/Value.ts | 41 ++++++ .../Explore/slate-plugins/prism/index.tsx | 123 ------------------ yarn.lock | 8 +- 6 files changed, 73 insertions(+), 149 deletions(-) create mode 100644 public/app/containers/Explore/Value.ts delete mode 100644 public/app/containers/Explore/slate-plugins/prism/index.tsx diff --git a/package.json b/package.json index 071d32992af..d3f2518197b 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,7 @@ "rxjs": "^5.4.3", "slate": "^0.33.4", "slate-plain-serializer": "^0.5.10", + "slate-prism": "^0.5.0", "slate-react": "^0.12.4", "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop/tarball/master", diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 0991f08429a..491e7005bd0 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -3,10 +3,11 @@ import moment from 'moment'; import React from 'react'; import { Value } from 'slate'; import Cascader from 'rc-cascader'; +import PluginPrism from 'slate-prism'; +import Prism from 'prismjs'; // dom also includes Element polyfills import { getNextCharacter, getPreviousCousin } from './utils/dom'; -import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index'; import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; import BracesPlugin from './slate-plugins/braces'; import RunnerPlugin from './slate-plugins/runner'; @@ -27,7 +28,7 @@ const HISTOGRAM_SELECTOR = '{le!=""}'; // Returns all timeseries for histograms const HISTORY_ITEM_COUNT = 5; const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const METRIC_MARK = 'metric'; -const PRISM_LANGUAGE = 'promql'; +const PRISM_SYNTAX = 'promql'; export const RECORDING_RULES_GROUP = '__recording_rules__'; export const wrapLabel = (label: string) => ({ label }); @@ -36,6 +37,15 @@ export const setFunctionMove = (suggestion: Suggestion): Suggestion => { return suggestion; }; +// Syntax highlighting +Prism.languages[PRISM_SYNTAX] = PrismPromql; +function setPrismTokens(language, field, values, alias = 'variable') { + Prism.languages[language][field] = { + alias, + pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`), + }; +} + export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion { const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label); @@ -164,7 +174,10 @@ class PromQueryField extends React.Component node.type === 'code_block', + getSyntax: node => 'promql', + }), ]; this.state = { @@ -221,7 +234,7 @@ class PromQueryField extends React.Component { diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 52bfbc7fed4..13364729e7e 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Block, Change, Document, Text, Value } from 'slate'; +import { Change, Value } from 'slate'; import { Editor } from 'slate-react'; import Plain from 'slate-plain-serializer'; @@ -9,6 +9,7 @@ import ClearPlugin from './slate-plugins/clear'; import NewlinePlugin from './slate-plugins/newline'; import Typeahead from './Typeahead'; +import { makeFragment, makeValue } from './Value'; export const TYPEAHEAD_DEBOUNCE = 300; @@ -16,22 +17,6 @@ function flattenSuggestions(s: any[]): any[] { return s ? s.reduce((acc, g) => acc.concat(g.items), []) : []; } -export const makeFragment = (text: string): Document => { - const lines = text.split('\n').map(line => - Block.create({ - type: 'paragraph', - nodes: [Text.create(line)], - }) - ); - - const fragment = Document.create({ - nodes: lines, - }); - return fragment; -}; - -export const getInitialValue = (value: string): Value => Value.create({ document: makeFragment(value) }); - export interface Suggestion { /** * The label of this completion item. By default @@ -113,6 +98,7 @@ interface TypeaheadFieldProps { onWillApplySuggestion?: (suggestion: string, state: TypeaheadFieldState) => string; placeholder?: string; portalPrefix?: string; + syntax?: string; } export interface TypeaheadFieldState { @@ -156,7 +142,7 @@ class QueryField extends React.Component { + const lines = text.split('\n').map(line => + Block.create({ + type: 'code_line', + nodes: [Text.create(line)], + }) + ); + + const block = Block.create({ + data: { + syntax, + }, + type: 'code_block', + nodes: lines, + }); + + return Document.create({ + nodes: [block], + }); +}; + +export const makeValue = (text: string, syntax?: string) => { + const fragment = makeFragment(text, syntax); + + return Value.create({ + document: fragment, + SCHEMA, + }); +}; diff --git a/public/app/containers/Explore/slate-plugins/prism/index.tsx b/public/app/containers/Explore/slate-plugins/prism/index.tsx deleted file mode 100644 index d185518790f..00000000000 --- a/public/app/containers/Explore/slate-plugins/prism/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from 'react'; -import Prism from 'prismjs'; - -const TOKEN_MARK = 'prism-token'; - -export function setPrismTokens(language, field, values, alias = 'variable') { - Prism.languages[language][field] = { - alias, - pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`), - }; -} - -/** - * Code-highlighting plugin based on Prism and - * https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js - * - * (Adapted to handle nested grammar definitions.) - */ - -export default function PrismPlugin({ definition, language }) { - if (definition) { - // Don't override exising modified definitions - Prism.languages[language] = Prism.languages[language] || definition; - } - - return { - /** - * Render a Slate mark with appropiate CSS class names - * - * @param {Object} props - * @return {Element} - */ - - renderMark(props) { - const { children, mark } = props; - // Only apply spans to marks identified by this plugin - if (mark.type !== TOKEN_MARK) { - return undefined; - } - const className = `token ${mark.data.get('types')}`; - return {children}; - }, - - /** - * Decorate code blocks with Prism.js highlighting. - * - * @param {Node} node - * @return {Array} - */ - - decorateNode(node) { - if (node.type !== 'paragraph') { - return []; - } - - const texts = node.getTexts().toArray(); - const tstring = texts.map(t => t.text).join('\n'); - const grammar = Prism.languages[language]; - const tokens = Prism.tokenize(tstring, grammar); - const decorations = []; - let startText = texts.shift(); - let endText = startText; - let startOffset = 0; - let endOffset = 0; - let start = 0; - - function processToken(token, acc?) { - // Accumulate token types down the tree - const types = `${acc || ''} ${token.type || ''} ${token.alias || ''}`; - - // Add mark for token node - if (typeof token === 'string' || typeof token.content === 'string') { - startText = endText; - startOffset = endOffset; - - const content = typeof token === 'string' ? token : token.content; - const newlines = content.split('\n').length - 1; - const length = content.length - newlines; - const end = start + length; - - let available = startText.text.length - startOffset; - let remaining = length; - - endOffset = startOffset + remaining; - - while (available < remaining) { - endText = texts.shift(); - remaining = length - available; - available = endText.text.length; - endOffset = remaining; - } - - // Inject marks from up the tree (acc) as well - if (typeof token !== 'string' || acc) { - const range = { - anchorKey: startText.key, - anchorOffset: startOffset, - focusKey: endText.key, - focusOffset: endOffset, - marks: [{ type: TOKEN_MARK, data: { types } }], - }; - - decorations.push(range); - } - - start = end; - } else if (token.content && token.content.length) { - // Tokens can be nested - for (const subToken of token.content) { - processToken(subToken, types); - } - } - } - - // Process top-level tokens - for (const token of tokens) { - processToken(token); - } - - return decorations; - }, - }; -} diff --git a/yarn.lock b/yarn.lock index 2b98ff32766..c0619897123 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9322,7 +9322,7 @@ pretty-format@^23.6.0: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -prismjs@^1.6.0: +prismjs@^1.13.0, prismjs@^1.6.0: version "1.15.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9" optionalDependencies: @@ -10736,6 +10736,12 @@ slate-plain-serializer@^0.5.10, slate-plain-serializer@^0.5.17: dependencies: slate-dev-logger "^0.1.43" +slate-prism@^0.5.0: + version "0.5.0" + resolved "http://registry.npmjs.org/slate-prism/-/slate-prism-0.5.0.tgz#009eb74fea38ad76c64db67def7ea0884917adec" + dependencies: + prismjs "^1.13.0" + slate-prop-types@^0.4.34: version "0.4.61" resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.61.tgz#141c109bed81b130dd03ab86dd7541b28d6d962a" From 462b5d937cf3ac4bb5aaf083323d034d9b760109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 14 Sep 2018 16:48:31 +0200 Subject: [PATCH 064/127] gdev: added test dashboard for polystat panel --- .../dev-dashboards/panel_tests_polystat.json | 3343 +++++++++++++++++ 1 file changed, 3343 insertions(+) create mode 100644 devenv/dev-dashboards/panel_tests_polystat.json diff --git a/devenv/dev-dashboards/panel_tests_polystat.json b/devenv/dev-dashboards/panel_tests_polystat.json new file mode 100644 index 00000000000..51d3085c438 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_polystat.json @@ -0,0 +1,3343 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_4", + "datasource": "gdev-testdata", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 4, + "links": [], + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "C", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "D", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "E", + "scenarioId": "random_walk" + } + ], + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "title": "Poor use of space", + "type": "grafana-polystat-panel", + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date & time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Farenheit (°F)", + "value": "farenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + }, + { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_5", + "datasource": "gdev-testdata", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 5, + "links": [], + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": "", + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": "", + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [ + { + "compositeName": "comp", + "members": [ + { + "seriesName": "A-series" + }, + { + "seriesName": "B-series" + } + ], + "enabled": true, + "clickThrough": "", + "hideMembers": true, + "showName": true, + "showValue": true, + "animateMode": "all", + "thresholdLevel": 0, + "sanitizeURLEnabled": true, + "sanitizedURL": "" + } + ], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "C", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "D", + "scenarioId": "random_walk" + }, + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "E", + "scenarioId": "random_walk" + } + ], + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "title": "Composite crash", + "type": "grafana-polystat-panel", + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date & time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Farenheit (°F)", + "value": "farenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + }, + { + "animationModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "d3DivId": "d3_svg_2", + "datasource": "gdev-testdata", + "decimals": 2, + "displayModes": [ + { + "text": "Show All", + "value": "all" + }, + { + "text": "Show Triggered", + "value": "triggered" + } + ], + "fontSizes": [ + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 22, + 24, + 26, + 28, + 30, + 32, + 34, + 36, + 38, + 40, + 42, + 44, + 46, + 48, + 50, + 52, + 54, + 56, + 58, + 60, + 62, + 64, + 66, + 68, + 70 + ], + "fontTypes": [ + "Open Sans", + "Arial", + "Avant Garde", + "Bookman", + "Consolas", + "Courier", + "Courier New", + "Futura", + "Garamond", + "Helvetica", + "Palatino", + "Times", + "Times New Roman", + "Verdana" + ], + "format": "none", + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 2, + "links": [], + "notcolors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "operatorName": "avg", + "operatorOptions": [ + { + "text": "Average", + "value": "avg" + }, + { + "text": "Count", + "value": "count" + }, + { + "text": "Current", + "value": "current" + }, + { + "text": "Delta", + "value": "delta" + }, + { + "text": "Difference", + "value": "diff" + }, + { + "text": "First", + "value": "first" + }, + { + "text": "Log Min", + "value": "logmin" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Min", + "value": "min" + }, + { + "text": "Name", + "value": "name" + }, + { + "text": "Time of Last Point", + "value": "last_time" + }, + { + "text": "Time Step", + "value": "time_step" + }, + { + "text": "Total", + "value": "total" + } + ], + "polystat": { + "animationSpeed": 2500, + "columnAutoSize": true, + "columns": 1, + "defaultClickThrough": "", + "defaultClickThroughSanitize": true, + "displayLimit": 100, + "fontAutoScale": true, + "fontSize": 12, + "globalDisplayMode": "all", + "globalOperatorName": "avg", + "gradientEnabled": true, + "hexagonSortByDirection": "asc", + "hexagonSortByField": "name", + "maxMetrics": 0, + "polygonBorderColor": "black", + "polygonBorderSize": 2, + "radius": "", + "radiusAutoSize": true, + "rowAutoSize": true, + "rows": 1, + "shape": "hexagon_pointed_top", + "tooltipDisplayMode": "all", + "tooltipDisplayTextTriggeredEmpty": "OK", + "tooltipFontSize": 12, + "tooltipFontType": "Open Sans", + "tooltipPrimarySortDirection": "desc", + "tooltipPrimarySortField": "thresholdLevel", + "tooltipSecondarySortDirection": "desc", + "tooltipSecondarySortField": "value", + "tooltipTimestampEnabled": true + }, + "savedComposites": [], + "savedOverrides": [], + "shapes": [ + { + "text": "Hexagon Pointed Top", + "value": "hexagon_pointed_top" + }, + { + "text": "Hexagon Flat Top", + "value": "hexagon_flat_top" + }, + { + "text": "Circle", + "value": "circle" + }, + { + "text": "Cross", + "value": "cross" + }, + { + "text": "Diamond", + "value": "diamond" + }, + { + "text": "Square", + "value": "square" + }, + { + "text": "Star", + "value": "star" + }, + { + "text": "Triangle", + "value": "triangle" + }, + { + "text": "Wye", + "value": "wye" + } + ], + "sortDirections": [ + { + "text": "Ascending", + "value": "asc" + }, + { + "text": "Descending", + "value": "desc" + } + ], + "sortFields": [ + { + "text": "Name", + "value": "name" + }, + { + "text": "Threshold Level", + "value": "thresholdLevel" + }, + { + "text": "Value", + "value": "value" + } + ], + "svgContainer": {}, + "targets": [ + { + "alias": "Sensor-A", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + }, + { + "alias": "Sensor-B", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "3433,23432,55" + }, + { + "alias": "Sensor-C", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,2,3,4,5,6" + }, + { + "alias": "Sensor-E", + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "thresholdStates": [ + { + "text": "ok", + "value": 0 + }, + { + "text": "warning", + "value": 1 + }, + { + "text": "critical", + "value": 2 + }, + { + "text": "custom", + "value": 3 + } + ], + "title": "No Value in Sensor-C Bug", + "type": "grafana-polystat-panel", + "unitFormats": [ + { + "submenu": [ + { + "text": "none", + "value": "none" + }, + { + "text": "short", + "value": "short" + }, + { + "text": "percent (0-100)", + "value": "percent" + }, + { + "text": "percent (0.0-1.0)", + "value": "percentunit" + }, + { + "text": "Humidity (%H)", + "value": "humidity" + }, + { + "text": "decibel", + "value": "dB" + }, + { + "text": "hexadecimal (0x)", + "value": "hex0x" + }, + { + "text": "hexadecimal", + "value": "hex" + }, + { + "text": "scientific notation", + "value": "sci" + }, + { + "text": "locale format", + "value": "locale" + } + ], + "text": "none" + }, + { + "submenu": [ + { + "text": "Dollars ($)", + "value": "currencyUSD" + }, + { + "text": "Pounds (£)", + "value": "currencyGBP" + }, + { + "text": "Euro (€)", + "value": "currencyEUR" + }, + { + "text": "Yen (¥)", + "value": "currencyJPY" + }, + { + "text": "Rubles (₽)", + "value": "currencyRUB" + }, + { + "text": "Hryvnias (₴)", + "value": "currencyUAH" + }, + { + "text": "Real (R$)", + "value": "currencyBRL" + }, + { + "text": "Danish Krone (kr)", + "value": "currencyDKK" + }, + { + "text": "Icelandic Króna (kr)", + "value": "currencyISK" + }, + { + "text": "Norwegian Krone (kr)", + "value": "currencyNOK" + }, + { + "text": "Swedish Krona (kr)", + "value": "currencySEK" + }, + { + "text": "Czech koruna (czk)", + "value": "currencyCZK" + }, + { + "text": "Swiss franc (CHF)", + "value": "currencyCHF" + }, + { + "text": "Polish Złoty (PLN)", + "value": "currencyPLN" + }, + { + "text": "Bitcoin (฿)", + "value": "currencyBTC" + } + ], + "text": "currency" + }, + { + "submenu": [ + { + "text": "Hertz (1/s)", + "value": "hertz" + }, + { + "text": "nanoseconds (ns)", + "value": "ns" + }, + { + "text": "microseconds (µs)", + "value": "µs" + }, + { + "text": "milliseconds (ms)", + "value": "ms" + }, + { + "text": "seconds (s)", + "value": "s" + }, + { + "text": "minutes (m)", + "value": "m" + }, + { + "text": "hours (h)", + "value": "h" + }, + { + "text": "days (d)", + "value": "d" + }, + { + "text": "duration (ms)", + "value": "dtdurationms" + }, + { + "text": "duration (s)", + "value": "dtdurations" + }, + { + "text": "duration (hh:mm:ss)", + "value": "dthms" + }, + { + "text": "Timeticks (s/100)", + "value": "timeticks" + } + ], + "text": "time" + }, + { + "submenu": [ + { + "text": "YYYY-MM-DD HH:mm:ss", + "value": "dateTimeAsIso" + }, + { + "text": "DD/MM/YYYY h:mm:ss a", + "value": "dateTimeAsUS" + }, + { + "text": "From Now", + "value": "dateTimeFromNow" + } + ], + "text": "date & time" + }, + { + "submenu": [ + { + "text": "bits", + "value": "bits" + }, + { + "text": "bytes", + "value": "bytes" + }, + { + "text": "kibibytes", + "value": "kbytes" + }, + { + "text": "mebibytes", + "value": "mbytes" + }, + { + "text": "gibibytes", + "value": "gbytes" + } + ], + "text": "data (IEC)" + }, + { + "submenu": [ + { + "text": "bits", + "value": "decbits" + }, + { + "text": "bytes", + "value": "decbytes" + }, + { + "text": "kilobytes", + "value": "deckbytes" + }, + { + "text": "megabytes", + "value": "decmbytes" + }, + { + "text": "gigabytes", + "value": "decgbytes" + } + ], + "text": "data (Metric)" + }, + { + "submenu": [ + { + "text": "packets/sec", + "value": "pps" + }, + { + "text": "bits/sec", + "value": "bps" + }, + { + "text": "bytes/sec", + "value": "Bps" + }, + { + "text": "kilobits/sec", + "value": "Kbits" + }, + { + "text": "kilobytes/sec", + "value": "KBs" + }, + { + "text": "megabits/sec", + "value": "Mbits" + }, + { + "text": "megabytes/sec", + "value": "MBs" + }, + { + "text": "gigabytes/sec", + "value": "GBs" + }, + { + "text": "gigabits/sec", + "value": "Gbits" + } + ], + "text": "data rate" + }, + { + "submenu": [ + { + "text": "hashes/sec", + "value": "Hs" + }, + { + "text": "kilohashes/sec", + "value": "KHs" + }, + { + "text": "megahashes/sec", + "value": "MHs" + }, + { + "text": "gigahashes/sec", + "value": "GHs" + }, + { + "text": "terahashes/sec", + "value": "THs" + }, + { + "text": "petahashes/sec", + "value": "PHs" + }, + { + "text": "exahashes/sec", + "value": "EHs" + } + ], + "text": "hash rate" + }, + { + "submenu": [ + { + "text": "ops/sec (ops)", + "value": "ops" + }, + { + "text": "requests/sec (rps)", + "value": "reqps" + }, + { + "text": "reads/sec (rps)", + "value": "rps" + }, + { + "text": "writes/sec (wps)", + "value": "wps" + }, + { + "text": "I/O ops/sec (iops)", + "value": "iops" + }, + { + "text": "ops/min (opm)", + "value": "opm" + }, + { + "text": "reads/min (rpm)", + "value": "rpm" + }, + { + "text": "writes/min (wpm)", + "value": "wpm" + } + ], + "text": "throughput" + }, + { + "submenu": [ + { + "text": "millimetre (mm)", + "value": "lengthmm" + }, + { + "text": "meter (m)", + "value": "lengthm" + }, + { + "text": "feet (ft)", + "value": "lengthft" + }, + { + "text": "kilometer (km)", + "value": "lengthkm" + }, + { + "text": "mile (mi)", + "value": "lengthmi" + } + ], + "text": "length" + }, + { + "submenu": [ + { + "text": "Square Meters (m²)", + "value": "areaM2" + }, + { + "text": "Square Feet (ft²)", + "value": "areaF2" + }, + { + "text": "Square Miles (mi²)", + "value": "areaMI2" + } + ], + "text": "area" + }, + { + "submenu": [ + { + "text": "milligram (mg)", + "value": "massmg" + }, + { + "text": "gram (g)", + "value": "massg" + }, + { + "text": "kilogram (kg)", + "value": "masskg" + }, + { + "text": "metric ton (t)", + "value": "masst" + } + ], + "text": "mass" + }, + { + "submenu": [ + { + "text": "metres/second (m/s)", + "value": "velocityms" + }, + { + "text": "kilometers/hour (km/h)", + "value": "velocitykmh" + }, + { + "text": "miles/hour (mph)", + "value": "velocitymph" + }, + { + "text": "knot (kn)", + "value": "velocityknot" + } + ], + "text": "velocity" + }, + { + "submenu": [ + { + "text": "millilitre (mL)", + "value": "mlitre" + }, + { + "text": "litre (L)", + "value": "litre" + }, + { + "text": "cubic metre", + "value": "m3" + }, + { + "text": "Normal cubic metre", + "value": "Nm3" + }, + { + "text": "cubic decimetre", + "value": "dm3" + }, + { + "text": "gallons", + "value": "gallons" + } + ], + "text": "volume" + }, + { + "submenu": [ + { + "text": "Watt (W)", + "value": "watt" + }, + { + "text": "Kilowatt (kW)", + "value": "kwatt" + }, + { + "text": "Milliwatt (mW)", + "value": "mwatt" + }, + { + "text": "Watt per square metre (W/m²)", + "value": "Wm2" + }, + { + "text": "Volt-ampere (VA)", + "value": "voltamp" + }, + { + "text": "Kilovolt-ampere (kVA)", + "value": "kvoltamp" + }, + { + "text": "Volt-ampere reactive (var)", + "value": "voltampreact" + }, + { + "text": "Kilovolt-ampere reactive (kvar)", + "value": "kvoltampreact" + }, + { + "text": "Watt-hour (Wh)", + "value": "watth" + }, + { + "text": "Kilowatt-hour (kWh)", + "value": "kwatth" + }, + { + "text": "Kilowatt-min (kWm)", + "value": "kwattm" + }, + { + "text": "Joule (J)", + "value": "joule" + }, + { + "text": "Electron volt (eV)", + "value": "ev" + }, + { + "text": "Ampere (A)", + "value": "amp" + }, + { + "text": "Kiloampere (kA)", + "value": "kamp" + }, + { + "text": "Milliampere (mA)", + "value": "mamp" + }, + { + "text": "Volt (V)", + "value": "volt" + }, + { + "text": "Kilovolt (kV)", + "value": "kvolt" + }, + { + "text": "Millivolt (mV)", + "value": "mvolt" + }, + { + "text": "Decibel-milliwatt (dBm)", + "value": "dBm" + }, + { + "text": "Ohm (Ω)", + "value": "ohm" + }, + { + "text": "Lumens (Lm)", + "value": "lumens" + } + ], + "text": "energy" + }, + { + "submenu": [ + { + "text": "Celsius (°C)", + "value": "celsius" + }, + { + "text": "Farenheit (°F)", + "value": "farenheit" + }, + { + "text": "Kelvin (K)", + "value": "kelvin" + } + ], + "text": "temperature" + }, + { + "submenu": [ + { + "text": "Millibars", + "value": "pressurembar" + }, + { + "text": "Bars", + "value": "pressurebar" + }, + { + "text": "Kilobars", + "value": "pressurekbar" + }, + { + "text": "Hectopascals", + "value": "pressurehpa" + }, + { + "text": "Kilopascals", + "value": "pressurekpa" + }, + { + "text": "Inches of mercury", + "value": "pressurehg" + }, + { + "text": "PSI", + "value": "pressurepsi" + } + ], + "text": "pressure" + }, + { + "submenu": [ + { + "text": "Newton-meters (Nm)", + "value": "forceNm" + }, + { + "text": "Kilonewton-meters (kNm)", + "value": "forcekNm" + }, + { + "text": "Newtons (N)", + "value": "forceN" + }, + { + "text": "Kilonewtons (kN)", + "value": "forcekN" + } + ], + "text": "force" + }, + { + "submenu": [ + { + "text": "Gallons/min (gpm)", + "value": "flowgpm" + }, + { + "text": "Cubic meters/sec (cms)", + "value": "flowcms" + }, + { + "text": "Cubic feet/sec (cfs)", + "value": "flowcfs" + }, + { + "text": "Cubic feet/min (cfm)", + "value": "flowcfm" + }, + { + "text": "Litre/hour", + "value": "litreh" + }, + { + "text": "Litre/min (l/min)", + "value": "flowlpm" + }, + { + "text": "milliLitre/min (mL/min)", + "value": "flowmlpm" + } + ], + "text": "flow" + }, + { + "submenu": [ + { + "text": "Degrees (°)", + "value": "degree" + }, + { + "text": "Radians", + "value": "radian" + }, + { + "text": "Gradian", + "value": "grad" + } + ], + "text": "angle" + }, + { + "submenu": [ + { + "text": "Meters/sec²", + "value": "accMS2" + }, + { + "text": "Feet/sec²", + "value": "accFS2" + }, + { + "text": "G unit", + "value": "accG" + } + ], + "text": "acceleration" + }, + { + "submenu": [ + { + "text": "Becquerel (Bq)", + "value": "radbq" + }, + { + "text": "curie (Ci)", + "value": "radci" + }, + { + "text": "Gray (Gy)", + "value": "radgy" + }, + { + "text": "rad", + "value": "radrad" + }, + { + "text": "Sievert (Sv)", + "value": "radsv" + }, + { + "text": "rem", + "value": "radrem" + }, + { + "text": "Exposure (C/kg)", + "value": "radexpckg" + }, + { + "text": "roentgen (R)", + "value": "radr" + }, + { + "text": "Sievert/hour (Sv/h)", + "value": "radsvh" + } + ], + "text": "radiation" + }, + { + "submenu": [ + { + "text": "parts-per-million (ppm)", + "value": "ppm" + }, + { + "text": "parts-per-billion (ppb)", + "value": "conppb" + }, + { + "text": "nanogram per cubic metre (ng/m³)", + "value": "conngm3" + }, + { + "text": "nanogram per normal cubic metre (ng/Nm³)", + "value": "conngNm3" + }, + { + "text": "microgram per cubic metre (μg/m³)", + "value": "conμgm3" + }, + { + "text": "microgram per normal cubic metre (μg/Nm³)", + "value": "conμgNm3" + }, + { + "text": "milligram per cubic metre (mg/m³)", + "value": "conmgm3" + }, + { + "text": "milligram per normal cubic metre (mg/Nm³)", + "value": "conmgNm3" + }, + { + "text": "gram per cubic metre (g/m³)", + "value": "congm3" + }, + { + "text": "gram per normal cubic metre (g/Nm³)", + "value": "congNm3" + } + ], + "text": "concentration" + } + ] + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [ + "panel-test", + "gdev" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Panel Tests - Polystat", + "uid": "Kp9Z0hTik", + "version": 5 +} From 0f4ee4ce87cd0ccb01b5abdfd794f51818178d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cyril=20Bont=C3=A9?= Date: Fri, 14 Sep 2018 17:22:07 +0200 Subject: [PATCH 065/127] fix hipchat color code used "no data" notifications --- pkg/services/alerting/notifiers/hipchat.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 1c284ec3d2b..388cec79597 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -125,7 +125,7 @@ func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { case models.AlertStateOK: color = "green" case models.AlertStateNoData: - color = "grey" + color = "gray" case models.AlertStateAlerting: color = "red" } From 9a6446c2b5c927e4e6e4525dbf30042f83a6b529 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 14 Sep 2018 17:27:36 +0200 Subject: [PATCH 066/127] new column for team_member table --- pkg/api/team_members.go | 6 +++ pkg/models/team_member.go | 39 +++++++++++--------- pkg/services/sqlstore/migrations/team_mig.go | 3 ++ pkg/services/sqlstore/team.go | 16 +++++--- pkg/services/sqlstore/team_test.go | 16 ++++++++ 5 files changed, 57 insertions(+), 23 deletions(-) diff --git a/pkg/api/team_members.go b/pkg/api/team_members.go index 60a170a8c31..5b5970de6ad 100644 --- a/pkg/api/team_members.go +++ b/pkg/api/team_members.go @@ -4,6 +4,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -17,6 +18,11 @@ func GetTeamMembers(c *m.ReqContext) Response { for _, member := range query.Result { member.AvatarUrl = dtos.GetGravatarUrl(member.Email) + member.Labels = []string{} + + if setting.IsEnterprise && setting.LdapEnabled && member.External { + member.Labels = append(member.Labels, "LDAP") + } } return JSON(200, query.Result) diff --git a/pkg/models/team_member.go b/pkg/models/team_member.go index 9434dad8ecd..dd64787f465 100644 --- a/pkg/models/team_member.go +++ b/pkg/models/team_member.go @@ -12,10 +12,11 @@ var ( // TeamMember model type TeamMember struct { - Id int64 - OrgId int64 - TeamId int64 - UserId int64 + Id int64 + OrgId int64 + TeamId int64 + UserId int64 + External bool Created time.Time Updated time.Time @@ -25,9 +26,10 @@ type TeamMember struct { // COMMANDS type AddTeamMemberCommand struct { - UserId int64 `json:"userId" binding:"Required"` - OrgId int64 `json:"-"` - TeamId int64 `json:"-"` + UserId int64 `json:"userId" binding:"Required"` + OrgId int64 `json:"-"` + TeamId int64 `json:"-"` + External bool `json:"-"` } type RemoveTeamMemberCommand struct { @@ -40,20 +42,23 @@ type RemoveTeamMemberCommand struct { // QUERIES type GetTeamMembersQuery struct { - OrgId int64 - TeamId int64 - UserId int64 - Result []*TeamMemberDTO + OrgId int64 + TeamId int64 + UserId int64 + External bool + Result []*TeamMemberDTO } // ---------------------- // Projections and DTOs type TeamMemberDTO struct { - OrgId int64 `json:"orgId"` - TeamId int64 `json:"teamId"` - UserId int64 `json:"userId"` - Email string `json:"email"` - Login string `json:"login"` - AvatarUrl string `json:"avatarUrl"` + OrgId int64 `json:"orgId"` + TeamId int64 `json:"teamId"` + UserId int64 `json:"userId"` + External bool `json:"-"` + Email string `json:"email"` + Login string `json:"login"` + AvatarUrl string `json:"avatarUrl"` + Labels []string `json:"labels"` } diff --git a/pkg/services/sqlstore/migrations/team_mig.go b/pkg/services/sqlstore/migrations/team_mig.go index 9800d27f8ab..34c46ad13cf 100644 --- a/pkg/services/sqlstore/migrations/team_mig.go +++ b/pkg/services/sqlstore/migrations/team_mig.go @@ -51,4 +51,7 @@ func addTeamMigrations(mg *Migrator) { Name: "email", Type: DB_NVarchar, Nullable: true, Length: 190, })) + mg.AddMigration("Add column external to team_member table", NewAddColumnMigration(teamMemberV1, &Column{ + Name: "external", Type: DB_Bool, Nullable: true, + })) } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 72955df9a6a..e51bcb9b6e9 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -240,11 +240,12 @@ func AddTeamMember(cmd *m.AddTeamMemberCommand) error { } entity := m.TeamMember{ - OrgId: cmd.OrgId, - TeamId: cmd.TeamId, - UserId: cmd.UserId, - Created: time.Now(), - Updated: time.Now(), + OrgId: cmd.OrgId, + TeamId: cmd.TeamId, + UserId: cmd.UserId, + External: cmd.External, + Created: time.Now(), + Updated: time.Now(), } _, err := sess.Insert(&entity) @@ -289,7 +290,10 @@ func GetTeamMembers(query *m.GetTeamMembersQuery) error { if query.UserId != 0 { sess.Where("team_member.user_id=?", query.UserId) } - sess.Cols("user.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login") + if query.External { + sess.Where("team_member.external=?", dialect.BooleanStr(true)) + } + sess.Cols("team_member.org_id", "team_member.team_id", "team_member.user_id", "user.email", "user.login", "team_member.external") sess.Asc("user.login", "user.email") err := sess.Find(&query.Result) diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index abaa973957d..8f243617262 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -50,13 +50,29 @@ func TestTeamCommandsAndQueries(t *testing.T) { err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[0]}) So(err, ShouldBeNil) + err = AddTeamMember(&m.AddTeamMemberCommand{OrgId: testOrgId, TeamId: team1.Id, UserId: userIds[1], External: true}) + So(err, ShouldBeNil) q1 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id} err = GetTeamMembers(q1) So(err, ShouldBeNil) + So(q1.Result, ShouldHaveLength, 2) So(q1.Result[0].TeamId, ShouldEqual, team1.Id) So(q1.Result[0].Login, ShouldEqual, "loginuser0") So(q1.Result[0].OrgId, ShouldEqual, testOrgId) + So(q1.Result[1].TeamId, ShouldEqual, team1.Id) + So(q1.Result[1].Login, ShouldEqual, "loginuser1") + So(q1.Result[1].OrgId, ShouldEqual, testOrgId) + So(q1.Result[1].External, ShouldEqual, true) + + q2 := &m.GetTeamMembersQuery{OrgId: testOrgId, TeamId: team1.Id, External: true} + err = GetTeamMembers(q2) + So(err, ShouldBeNil) + So(q2.Result, ShouldHaveLength, 1) + So(q2.Result[0].TeamId, ShouldEqual, team1.Id) + So(q2.Result[0].Login, ShouldEqual, "loginuser1") + So(q2.Result[0].OrgId, ShouldEqual, testOrgId) + So(q2.Result[0].External, ShouldEqual, true) }) Convey("Should be able to search for teams", func() { From da68b858d79dfa10fd8fec1f03b55c3375f4b622 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 14 Sep 2018 17:28:34 +0200 Subject: [PATCH 067/127] display team member labels --- .../app/features/teams/TeamMembers.test.tsx | 10 + public/app/features/teams/TeamMembers.tsx | 22 +- public/app/features/teams/TeamPages.tsx | 2 +- .../app/features/teams/__mocks__/teamMocks.ts | 2 + .../__snapshots__/TeamMembers.test.tsx.snap | 302 ++++++++++++++++++ .../__snapshots__/TeamPages.test.tsx.snap | 4 +- public/app/types/teams.ts | 1 + 7 files changed, 338 insertions(+), 5 deletions(-) diff --git a/public/app/features/teams/TeamMembers.test.tsx b/public/app/features/teams/TeamMembers.test.tsx index 8584edd86c8..696880bebd9 100644 --- a/public/app/features/teams/TeamMembers.test.tsx +++ b/public/app/features/teams/TeamMembers.test.tsx @@ -12,6 +12,7 @@ const setup = (propOverrides?: object) => { loadTeamMembers: jest.fn(), addTeamMember: jest.fn(), removeTeamMember: jest.fn(), + syncEnabled: false, }; Object.assign(props, propOverrides); @@ -39,6 +40,15 @@ describe('Render', () => { expect(wrapper).toMatchSnapshot(); }); + + it('should render team members when sync enabled', () => { + const { wrapper } = setup({ + members: getMockTeamMembers(5), + syncEnabled: true, + }); + + expect(wrapper).toMatchSnapshot(); + }); }); describe('Functions', () => { diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index 38e8c0a9aa0..cda175f4395 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import SlideDown from 'app/core/components/Animations/SlideDown'; import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TeamMember } from '../../types'; import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions'; import { getSearchMemberQuery, getTeamMembers } from './state/selectors'; @@ -14,6 +15,7 @@ export interface Props { addTeamMember: typeof addTeamMember; removeTeamMember: typeof removeTeamMember; setSearchMemberQuery: typeof setSearchMemberQuery; + syncEnabled: boolean; } export interface State { @@ -52,7 +54,19 @@ export class TeamMembers extends PureComponent { this.setState({ newTeamMember: null }); }; - renderMember(member: TeamMember) { + renderLabels(labels: string[]) { + if (!labels) { + return ; + } + + return ( + + {labels.map(label => {}} />)} + + ); + } + + renderMember(member: TeamMember, syncEnabled: boolean) { return ( @@ -60,6 +74,7 @@ export class TeamMembers extends PureComponent { {member.login} {member.email} + {syncEnabled ? this.renderLabels(member.labels) : ''} this.onRemoveMember(member)} /> @@ -69,7 +84,7 @@ export class TeamMembers extends PureComponent { render() { const { newTeamMember, isAdding } = this.state; - const { searchMemberQuery, members } = this.props; + const { searchMemberQuery, members, syncEnabled } = this.props; const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); return ( @@ -120,10 +135,11 @@ export class TeamMembers extends PureComponent { Name Email + {syncEnabled ? : ''} - {members && members.map(member => this.renderMember(member))} + {members && members.map(member => this.renderMember(member, syncEnabled))}
diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index bbc8b7013ca..3dc5a9f6f15 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -63,7 +63,7 @@ export class TeamPages extends PureComponent { switch (currentPage) { case PageTypes.Members: - return ; + return ; case PageTypes.Settings: return ; diff --git a/public/app/features/teams/__mocks__/teamMocks.ts b/public/app/features/teams/__mocks__/teamMocks.ts index c9e9a27bee0..34fa06b2d09 100644 --- a/public/app/features/teams/__mocks__/teamMocks.ts +++ b/public/app/features/teams/__mocks__/teamMocks.ts @@ -35,6 +35,7 @@ export const getMockTeamMembers = (amount: number): TeamMember[] => { avatarUrl: 'some/url/', email: 'test@test.com', login: `testUser-${i}`, + labels: ['label 1', 'label 2'], }); } @@ -48,6 +49,7 @@ export const getMockTeamMember = (): TeamMember => { avatarUrl: 'some/url/', email: 'test@test.com', login: 'testUser', + labels: [], }; }; diff --git a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap index 2a42897e2b9..93e6f2131aa 100644 --- a/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap @@ -315,3 +315,305 @@ exports[`Render should render team members 1`] = `
`; + +exports[`Render should render team members when sync enabled 1`] = ` +
+
+
+ +
+
+ +
+ +
+ +
+ Add Team Member +
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + Name + + Email + + +
+ + + testUser-1 + + test@test.com + + + + + +
+ + + testUser-2 + + test@test.com + + + + + +
+ + + testUser-3 + + test@test.com + + + + + +
+ + + testUser-4 + + test@test.com + + + + + +
+ + + testUser-5 + + test@test.com + + + + + +
+
+
+`; diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 4ce4df4acb2..f32b8211d2c 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -29,7 +29,9 @@ exports[`Render should render member page if team not empty 1`] = `
- +
`; diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts index bcf752c86c0..b85ff3833d6 100644 --- a/public/app/types/teams.ts +++ b/public/app/types/teams.ts @@ -12,6 +12,7 @@ export interface TeamMember { avatarUrl: string; email: string; login: string; + labels: string[]; } export interface TeamGroup { From 138e7ab26417a4ea9159018afb9062a883fde260 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Fri, 14 Sep 2018 21:58:02 +0200 Subject: [PATCH 068/127] Add documentation for PostgreSQL query builder --- docs/sources/features/datasources/postgres.md | 95 +++++++++++++++---- 1 file changed, 77 insertions(+), 18 deletions(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index d67958814dd..4bb27e7f4f9 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -16,7 +16,7 @@ Grafana ships with a built-in PostgreSQL data source plugin that allows you to q ## Adding the data source 1. Open the side menu by clicking the Grafana icon in the top header. -2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`. +2. In the side menu under the `Configuration` icon you should find a link named `Data Sources`. 3. Click the `+ Add data source` button in the top header. 4. Select *PostgreSQL* from the *Type* dropdown. @@ -57,7 +57,7 @@ Identifier | Description The database user you specify when you add the data source should only be granted SELECT permissions on the specified database & tables you want to query. Grafana does not validate that the query is safe. The query could include any SQL statement. For example, statements like `DELETE FROM user;` and `DROP TABLE user;` would be -executed. To protect against this we **Highly** recommmend you create a specific postgresql user with restricted permissions. +executed. To protect against this we **highly** recommend you create a specific PostgreSQL user with restricted permissions. Example: @@ -69,9 +69,70 @@ Example: Make sure the user does not get any unwanted privileges from the public role. +## Query Editor + +{{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} + +You find the PostgreSQL query editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the +panel title, then edit. + +The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. + +### Select table, time column and metric column (FROM) + +When you enter edit mode for the first time or add a new query Grafana will try to prefill the query builder with the first table that has a timestamp column and a numeric column. + +In the FROM field, Grafana will suggest tables that are in the `search_path` of the database user. To select a table or view not in your `search_path` +you can manually enter a fully qualified name (schema.table) like `public.metrics`. + +The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name. + +The metric column suggestions will only contain columns with a text datatype (char,varchar,text). +If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `ip::text`. +You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like +`hostname || ' ' || container_name`. + +### Columns, Window and Aggregation functions (SELECT) + +In the `SELECT` row you can specify what columns and functions you want to use. +In the column field you may write arbitrary expressions instead of a column name like `column1 * column2 / column3`. + +The available functions in the query editor depend on the PostgreSQL version you selected when configuring the datasource. +If you use aggregate functions you need to group your resultset. The editor will automatically add a `GROUP BY time` if you add an aggregate function. + +The editor tries to simplify and unify this part of the query. For example:
+![](/img/docs/v53/postgres_select_editor.png)
+ +The above will generate the following PostgreSQL `SELECT` clause: + +```sql +avg(tx_bytes) OVER (ORDER BY "time" ROWS 5 PRECEDING) AS "tx_bytes" +``` + +You may add further value columns by clicking the plus button and selecting `Column` from the menu. Multiple value columns will be plotted as separate series in the graph panel. + +### Filter data (WHERE) +To add a filter click the plus icon to the right of the `WHERE` condition. You can remove filters by clicking on +the filter and selecting `Remove`. A filter for the current selected timerange is automatically added to new queries. + +### Group By +To group by time or any other columns click the plus icon at the end of the GROUP BY row. The suggestion dropdown will only show text columns of your currently selected table but you may manually enter any column. +You can remove the group by clicking on the item and then selecting `Remove`. + +If you add any grouping, all selected columns need to have an aggregate function applied. The query builder will automatically add aggregate functions to all columns without aggregate functions when you add groupings. + +#### Gap Filling + +Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with. + +### Text Editor Mode (RAW) +You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query. + +> If you use the raw query editor, be sure your query at minimum has `ORDER BY time` and a filter on the returned time range. + ## Macros -To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros. +Macros can be used within a query to simplify syntax and allow for dynamic parts. Macro example | Description ------------ | ------------- @@ -80,21 +141,19 @@ Macro example | Description *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'* *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* -*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* -*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in a GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by Grafana and 0 will be used as the value. *$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. -*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). -*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). -*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value. If no value has been seen yet, NULL will be used (only available in Grafana 5.3+). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced with an expression identical to $__timeGroup, but with an added column alias (only available in Grafana 5.3+). +*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamps. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* -*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). -*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup, but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above, but also adds a column alias (only available in Grafana 5.3+). We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. -The query editor has a link named `Generated SQL` that shows up after a query as been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. - ## Table queries If the `Format as` query option is set to `Table` then you can basically do any type of SQL query. The table panel will automatically show the results of whatever columns & rows your query returns. @@ -124,8 +183,8 @@ The resulting table panel: ## Time series queries -If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch. -Any column except `time` and `metric` is treated as a value column. +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a SQL datetime or any numeric datatype representing unix epoch. +Any column except `time` and `metric` are treated as a value column. You may return a column named `metric` that is used as metric name for the value column. If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). @@ -206,7 +265,7 @@ Another option is a query that can create a key/value variable. The query should SELECT hostname AS __text, id AS __value FROM host ``` -You can also create nested variables. For example if you had another variable named `region`. Then you could have +You can also create nested variables. Using a variable named `region`, you could have the hosts variable only show hosts from the current selected region with a query like this (if `region` is a multi-value variable then use the `IN` comparison operator rather than `=` to match against multiple values): ```sql @@ -215,7 +274,7 @@ SELECT hostname FROM host WHERE region IN($region) ### Using Variables in Queries -From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically so if it is a string value do not wrap them in quotes in where clauses. +From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically. If your template variables are strings, do not wrap them in quotes in where clauses. From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. @@ -247,7 +306,7 @@ ORDER BY atimestamp ASC #### Disabling Quoting for Multi-value Variables -Grafana automatically creates a quoted, comma-separated string for multi-value variables. For example: if `server01` and `server02` are selected then it will be formatted as: `'server01', 'server02'`. Do disable quoting, use the csv formatting option for variables: +Grafana automatically creates a quoted, comma-separated string for multi-value variables. For example: if `server01` and `server02` are selected then it will be formatted as: `'server01', 'server02'`. To disable quoting, use the csv formatting option for variables: `${servers:csv}` @@ -291,7 +350,7 @@ tags | Optional field name to use for event tags as a comma separated string. ## Alerting -Time series queries should work in alerting conditions. Table formatted queries is not yet supported in alert rule +Time series queries should work in alerting conditions. Table formatted queries are not yet supported in alert rule conditions. ## Configure the Datasource with Provisioning From 3f309ff5dd2430620bc5ce7bd0a5f731a1466fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 15 Sep 2018 04:50:22 -0700 Subject: [PATCH 069/127] rename folder --- public/app/features/dashboard/all.ts | 2 +- .../DashboardPermissions.tsx | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename public/app/features/dashboard/{DashboardPermissions => permissions}/DashboardPermissions.tsx (100%) diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 817ed83aaa0..f75743513f1 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,7 +32,7 @@ import './dashlinks/module'; // angular wrappers import { react2AngularDirective } from 'app/core/utils/react2angular'; -import DashboardPermissions from './DashboardPermissions/DashboardPermissions'; +import DashboardPermissions from './permissions/DashboardPermissions'; react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']); diff --git a/public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx similarity index 100% rename from public/app/features/dashboard/DashboardPermissions/DashboardPermissions.tsx rename to public/app/features/dashboard/permissions/DashboardPermissions.tsx From 66ae7ddc02b0bcbe19f6f747d5c78a571f54ef76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 15 Sep 2018 04:59:17 -0700 Subject: [PATCH 070/127] fix: increased team picker limit to 50, closes #13294 --- public/app/core/components/Picker/TeamPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 04f108ff8da..a177a13c45b 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -39,7 +39,7 @@ export class TeamPicker extends Component { const backendSrv = getBackendSrv(); this.setState({ isLoading: true }); - return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => { + return backendSrv.get(`/api/teams/search?perpage=50&page=1&query=${query}`).then(result => { const teams = result.teams.map(team => { return { id: team.id, From f73236f8f48d43ac0e817e6f255d0f6c5a2e8918 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Sun, 16 Sep 2018 12:26:05 +0200 Subject: [PATCH 071/127] pkg/services/sqlstore: Fix x.Sql is deprecated: use SQL instead. (megacheck) See, $ gometalinter --vendor --disable-all --enable=megacheck --disable=gotype --deadline 6m ./... | grep SQL alert.go:43:9:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) alert_notification.go:122:12:warning: sess.Sql is deprecated: use SQL instead. (SA1019) (megacheck) annotation.go:226:12:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) dashboard.go:228:9:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) dashboard.go:302:10:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) dashboard.go:416:9:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) dashboard.go:635:12:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) migrations/user_mig.go:137:9:warning: sess.Sql is deprecated: use SQL instead. (SA1019) (megacheck) plugin_setting.go:29:10:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) quota.go:41:12:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) quota.go:84:13:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) quota.go:143:12:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) quota.go:186:13:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) quota.go:234:12:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) team.go:172:12:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) team.go:199:17:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) team.go:223:9:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) temp_user.go:99:10:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) temp_user.go:124:10:warning: x.Sql is deprecated: use SQL instead. (SA1019) (megacheck) user.go:375:3:warning: sess.Sql is deprecated: use SQL instead. (SA1019) (megacheck) user.go:377:3:warning: sess.Sql is deprecated: use SQL instead. (SA1019) (megacheck) user.go:379:3:warning: sess.Sql is deprecated: use SQL instead. (SA1019) (megacheck) --- pkg/services/sqlstore/alert.go | 2 +- pkg/services/sqlstore/alert_notification.go | 2 +- pkg/services/sqlstore/annotation.go | 2 +- pkg/services/sqlstore/dashboard.go | 8 ++++---- pkg/services/sqlstore/migrations/user_mig.go | 2 +- pkg/services/sqlstore/plugin_setting.go | 2 +- pkg/services/sqlstore/quota.go | 10 +++++----- pkg/services/sqlstore/team.go | 6 +++--- pkg/services/sqlstore/temp_user.go | 4 ++-- pkg/services/sqlstore/user.go | 6 +++--- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index af911dc22e6..d4ddf42f637 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -40,7 +40,7 @@ func GetAlertById(query *m.GetAlertByIdQuery) error { func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { var alerts []*m.Alert - err := x.Sql("select * from alert").Find(&alerts) + err := x.SQL("select * from alert").Find(&alerts) if err != nil { return err } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8fb1e2212a9..19ed960638e 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -119,7 +119,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS } results := make([]*m.AlertNotification, 0) - if err := sess.Sql(sql.String(), params...).Find(&results); err != nil { + if err := sess.SQL(sql.String(), params...).Find(&results); err != nil { return err } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index a65bc136554..68d6fefc8af 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -223,7 +223,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I items := make([]*annotations.ItemDTO, 0) - if err := x.Sql(sql.String(), params...).Find(&items); err != nil { + if err := x.SQL(sql.String(), params...).Find(&items); err != nil { return nil, err } diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 64d7108675b..e43279208e7 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -225,7 +225,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear var res []DashboardSearchProjection sql, params := sb.ToSql() - err := x.Sql(sql, params...).Find(&res) + err := x.SQL(sql, params...).Find(&res) if err != nil { return nil, err } @@ -299,7 +299,7 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { ORDER BY term` query.Result = make([]*m.DashboardTagCloudItem, 0) - sess := x.Sql(sql, query.OrgId) + sess := x.SQL(sql, query.OrgId) err := sess.Find(&query.Result) return err } @@ -413,7 +413,7 @@ func GetDashboardPermissionsForUser(query *m.GetDashboardPermissionsForUserQuery params = append(params, query.UserId) params = append(params, dialect.BooleanStr(false)) - err := x.Sql(sql, params...).Find(&query.Result) + err := x.SQL(sql, params...).Find(&query.Result) for _, p := range query.Result { p.PermissionName = p.Permission.String() @@ -632,7 +632,7 @@ func HasEditPermissionInFolders(query *m.HasEditPermissionInFoldersQuery) error } resp := make([]*folderCount, 0) - if err := x.Sql(builder.GetSqlString(), builder.params...).Find(&resp); err != nil { + if err := x.SQL(builder.GetSqlString(), builder.params...).Find(&resp); err != nil { return err } diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 400033aaa33..e273cb7d542 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -134,7 +134,7 @@ type TempUserDTO struct { func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error { users := make([]*TempUserDTO, 0) - err := sess.Sql(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users) + err := sess.SQL(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users) if err != nil { return err } diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index 676d26fad56..973e83eab19 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -26,7 +26,7 @@ func GetPluginSettings(query *m.GetPluginSettingsQuery) error { params = append(params, query.OrgId) } - sess := x.Sql(sql, params...) + sess := x.SQL(sql, params...) query.Result = make([]*m.PluginSettingInfoDTO, 0) return sess.Find(&query.Result) } diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 539555ddc50..7b3a17b5661 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -38,7 +38,7 @@ func GetOrgQuotaByTarget(query *m.GetOrgQuotaByTargetQuery) error { //get quota used. rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(query.Target)) resp := make([]*targetCount, 0) - if err := x.Sql(rawSql, query.OrgId).Find(&resp); err != nil { + if err := x.SQL(rawSql, query.OrgId).Find(&resp); err != nil { return err } @@ -81,7 +81,7 @@ func GetOrgQuotas(query *m.GetOrgQuotasQuery) error { //get quota used. rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target)) resp := make([]*targetCount, 0) - if err := x.Sql(rawSql, q.OrgId).Find(&resp); err != nil { + if err := x.SQL(rawSql, q.OrgId).Find(&resp); err != nil { return err } result[i] = &m.OrgQuotaDTO{ @@ -140,7 +140,7 @@ func GetUserQuotaByTarget(query *m.GetUserQuotaByTargetQuery) error { //get quota used. rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target)) resp := make([]*targetCount, 0) - if err := x.Sql(rawSql, query.UserId).Find(&resp); err != nil { + if err := x.SQL(rawSql, query.UserId).Find(&resp); err != nil { return err } @@ -183,7 +183,7 @@ func GetUserQuotas(query *m.GetUserQuotasQuery) error { //get quota used. rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target)) resp := make([]*targetCount, 0) - if err := x.Sql(rawSql, q.UserId).Find(&resp); err != nil { + if err := x.SQL(rawSql, q.UserId).Find(&resp); err != nil { return err } result[i] = &m.UserQuotaDTO{ @@ -231,7 +231,7 @@ func GetGlobalQuotaByTarget(query *m.GetGlobalQuotaByTargetQuery) error { //get quota used. rawSql := fmt.Sprintf("SELECT COUNT(*) as count from %s", dialect.Quote(query.Target)) resp := make([]*targetCount, 0) - if err := x.Sql(rawSql).Find(&resp); err != nil { + if err := x.SQL(rawSql).Find(&resp); err != nil { return err } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 72955df9a6a..d949ade0174 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -169,7 +169,7 @@ func SearchTeams(query *m.SearchTeamsQuery) error { sql.WriteString(dialect.LimitOffset(int64(query.Limit), int64(offset))) } - if err := x.Sql(sql.String(), params...).Find(&query.Result.Teams); err != nil { + if err := x.SQL(sql.String(), params...).Find(&query.Result.Teams); err != nil { return err } @@ -196,7 +196,7 @@ func GetTeamById(query *m.GetTeamByIdQuery) error { sql.WriteString(` WHERE team.org_id = ? and team.id = ?`) var team m.TeamDTO - exists, err := x.Sql(sql.String(), query.OrgId, query.Id).Get(&team) + exists, err := x.SQL(sql.String(), query.OrgId, query.Id).Get(&team) if err != nil { return err @@ -220,7 +220,7 @@ func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`) sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`) - err := x.Sql(sql.String(), query.OrgId, query.UserId).Find(&query.Result) + err := x.SQL(sql.String(), query.OrgId, query.UserId).Find(&query.Result) return err } diff --git a/pkg/services/sqlstore/temp_user.go b/pkg/services/sqlstore/temp_user.go index e93ba2fd641..f13752f8038 100644 --- a/pkg/services/sqlstore/temp_user.go +++ b/pkg/services/sqlstore/temp_user.go @@ -96,7 +96,7 @@ func GetTempUsersQuery(query *m.GetTempUsersQuery) error { rawSql += " ORDER BY tu.created desc" query.Result = make([]*m.TempUserDTO, 0) - sess := x.Sql(rawSql, params...) + sess := x.SQL(rawSql, params...) err := sess.Find(&query.Result) return err } @@ -121,7 +121,7 @@ func GetTempUserByCode(query *m.GetTempUserByCodeQuery) error { WHERE tu.code=?` var tempUser m.TempUserDTO - sess := x.Sql(rawSql, query.Code) + sess := x.SQL(rawSql, query.Code) has, err := sess.Get(&tempUser) if err != nil { diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 5d1b827e79f..f7bcdbb90c9 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -372,11 +372,11 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { sess := x.Table("user") if query.UserId > 0 { - sess.Sql(rawSql+"WHERE u.id=?", query.UserId) + sess.SQL(rawSql+"WHERE u.id=?", query.UserId) } else if query.Login != "" { - sess.Sql(rawSql+"WHERE u.login=?", query.Login) + sess.SQL(rawSql+"WHERE u.login=?", query.Login) } else if query.Email != "" { - sess.Sql(rawSql+"WHERE u.email=?", query.Email) + sess.SQL(rawSql+"WHERE u.email=?", query.Email) } var user m.SignedInUser From e85d0e8d6b98eb10a9600dd608984f214bf7a7cf Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Sun, 16 Sep 2018 12:37:08 +0200 Subject: [PATCH 072/127] pkg/services/sqlstore: Fix sess.Id is deprecated: use ID instead. (megacheck) See, $ gometalinter --vendor --disable-all --enable=megacheck --disable=gotype --deadline 6m ./... | grep ID alert.go:193:15:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) alert.go:252:18:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) annotation.go:113:12:warning: sess.Table("annotation").Id is deprecated: use ID instead (SA1019) (megacheck) org.go:136:24:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) org.go:169:16:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) org_users.go:24:21:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) org_users.go:88:12:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) org_users.go:141:21:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) plugin_setting.go:103:12:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) preferences.go:97:12:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) quota.go:119:17:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) quota.go:221:17:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) team.go:77:24:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:243:16:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:267:13:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:282:13:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:313:12:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:475:3:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:479:13:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) user.go:493:13:warning: sess.Id is deprecated: use ID instead (SA1019) (megacheck) --- pkg/services/sqlstore/alert.go | 4 ++-- pkg/services/sqlstore/annotation.go | 2 +- pkg/services/sqlstore/org.go | 4 ++-- pkg/services/sqlstore/org_users.go | 6 +++--- pkg/services/sqlstore/plugin_setting.go | 2 +- pkg/services/sqlstore/preferences.go | 2 +- pkg/services/sqlstore/quota.go | 4 ++-- pkg/services/sqlstore/team.go | 2 +- pkg/services/sqlstore/user.go | 14 +++++++------- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index d4ddf42f637..ba898769578 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -190,7 +190,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS alert.Updated = timeNow() alert.State = alertToUpdate.State sess.MustCols("message") - _, err := sess.Id(alert.Id).Update(alert) + _, err := sess.ID(alert.Id).Update(alert) if err != nil { return err } @@ -249,7 +249,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { return inTransaction(func(sess *DBSession) error { alert := m.Alert{} - if has, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { + if has, err := sess.ID(cmd.AlertId).Get(&alert); err != nil { return err } else if !has { return fmt.Errorf("Could not find alert") diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index 68d6fefc8af..019f4787287 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -110,7 +110,7 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error { existing.Tags = item.Tags - _, err = sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing) + _, err = sess.Table("annotation").ID(existing.Id).Cols("epoch", "text", "region_id", "updated", "tags").Update(existing) return err }) } diff --git a/pkg/services/sqlstore/org.go b/pkg/services/sqlstore/org.go index 8931f1cf0f5..e36a80322d8 100644 --- a/pkg/services/sqlstore/org.go +++ b/pkg/services/sqlstore/org.go @@ -133,7 +133,7 @@ func UpdateOrg(cmd *m.UpdateOrgCommand) error { Updated: time.Now(), } - affectedRows, err := sess.Id(cmd.OrgId).Update(&org) + affectedRows, err := sess.ID(cmd.OrgId).Update(&org) if err != nil { return err @@ -166,7 +166,7 @@ func UpdateOrgAddress(cmd *m.UpdateOrgAddressCommand) error { Updated: time.Now(), } - if _, err := sess.Id(cmd.OrgId).Update(&org); err != nil { + if _, err := sess.ID(cmd.OrgId).Update(&org); err != nil { return err } diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index aad72cdacb4..14981cfde64 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -21,7 +21,7 @@ func AddOrgUser(cmd *m.AddOrgUserCommand) error { return inTransaction(func(sess *DBSession) error { // check if user exists var user m.User - if exists, err := sess.Id(cmd.UserId).Get(&user); err != nil { + if exists, err := sess.ID(cmd.UserId).Get(&user); err != nil { return err } else if !exists { return m.ErrUserNotFound @@ -85,7 +85,7 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error { orgUser.Role = cmd.Role orgUser.Updated = time.Now() - _, err = sess.Id(orgUser.Id).Update(&orgUser) + _, err = sess.ID(orgUser.Id).Update(&orgUser) if err != nil { return err } @@ -138,7 +138,7 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error { return inTransaction(func(sess *DBSession) error { // check if user exists var user m.User - if exists, err := sess.Id(cmd.UserId).Get(&user); err != nil { + if exists, err := sess.ID(cmd.UserId).Get(&user); err != nil { return err } else if !exists { return m.ErrUserNotFound diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index 973e83eab19..8fbf1b6be1c 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -100,7 +100,7 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error { pluginSetting.Pinned = cmd.Pinned pluginSetting.PluginVersion = cmd.PluginVersion - _, err = sess.Id(pluginSetting.Id).Update(&pluginSetting) + _, err = sess.ID(pluginSetting.Id).Update(&pluginSetting) return err }) } diff --git a/pkg/services/sqlstore/preferences.go b/pkg/services/sqlstore/preferences.go index 885837764fc..04e787971d9 100644 --- a/pkg/services/sqlstore/preferences.go +++ b/pkg/services/sqlstore/preferences.go @@ -94,7 +94,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { prefs.Theme = cmd.Theme prefs.Updated = time.Now() prefs.Version += 1 - _, err = sess.Id(prefs.Id).AllCols().Update(&prefs) + _, err = sess.ID(prefs.Id).AllCols().Update(&prefs) return err }) } diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 7b3a17b5661..7005b341268 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -116,7 +116,7 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { } } else { //update existing quota entry in the DB. - if _, err := sess.Id(quota.Id).Update("a); err != nil { + if _, err := sess.ID(quota.Id).Update("a); err != nil { return err } } @@ -218,7 +218,7 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { } } else { //update existing quota entry in the DB. - if _, err := sess.Id(quota.Id).Update("a); err != nil { + if _, err := sess.ID(quota.Id).Update("a); err != nil { return err } } diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index d949ade0174..68811cd72f5 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -74,7 +74,7 @@ func UpdateTeam(cmd *m.UpdateTeamCommand) error { sess.MustCols("email") - affectedRows, err := sess.Id(cmd.Id).Update(&team) + affectedRows, err := sess.ID(cmd.Id).Update(&team) if err != nil { return err diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index f7bcdbb90c9..6bd30be1869 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -240,7 +240,7 @@ func UpdateUser(cmd *m.UpdateUserCommand) error { Updated: time.Now(), } - if _, err := sess.Id(cmd.UserId).Update(&user); err != nil { + if _, err := sess.ID(cmd.UserId).Update(&user); err != nil { return err } @@ -264,7 +264,7 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error { Updated: time.Now(), } - _, err := sess.Id(cmd.UserId).Update(&user) + _, err := sess.ID(cmd.UserId).Update(&user) return err }) } @@ -279,7 +279,7 @@ func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error { LastSeenAt: time.Now(), } - _, err := sess.Id(cmd.UserId).Update(&user) + _, err := sess.ID(cmd.UserId).Update(&user) return err }) } @@ -310,7 +310,7 @@ func setUsingOrgInTransaction(sess *DBSession, userID int64, orgID int64) error OrgId: orgID, } - _, err := sess.Id(userID).Update(&user) + _, err := sess.ID(userID).Update(&user) return err } @@ -472,11 +472,11 @@ func DeleteUser(cmd *m.DeleteUserCommand) error { func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error { return inTransaction(func(sess *DBSession) error { user := m.User{} - sess.Id(cmd.UserId).Get(&user) + sess.ID(cmd.UserId).Get(&user) user.IsAdmin = cmd.IsGrafanaAdmin sess.UseBool("is_admin") - _, err := sess.Id(user.Id).Update(&user) + _, err := sess.ID(user.Id).Update(&user) return err }) } @@ -490,7 +490,7 @@ func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error { Updated: time.Now(), } - _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user) + _, err := sess.ID(cmd.UserId).Cols("help_flags1").Update(&user) return err }) } From 02dd27333ec72b40b401cb8c084b018285d55562 Mon Sep 17 00:00:00 2001 From: brian2222 <24463816+brian2222@users.noreply.github.com> Date: Sun, 16 Sep 2018 18:22:09 -0700 Subject: [PATCH 073/127] Update getting_started.md list of datasources updated in section: dashboards, panels, the buildingblocks of grafana... --- docs/sources/guides/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index a27c6ca4c99..ca9793a361a 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -69,7 +69,7 @@ The image above shows you the top header for a Dashboard. ## Dashboards, Panels, the building blocks of Grafana... -Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, Prometheus and Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. +Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently Graphite, Prometheus, Elasticsearch, InfluxDB, OpenTSDB, MySQL, PostgreSQL, Microsoft SQL Server, AWS Cloudwatch, and TestData). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. From 758828328772edb95dc46c294efe8946836ba792 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 13 Sep 2018 17:00:01 +0200 Subject: [PATCH 074/127] docs: template variable support for annotations --- docs/sources/reference/annotations.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/sources/reference/annotations.md b/docs/sources/reference/annotations.md index bfc104ef522..3bb50f4badf 100644 --- a/docs/sources/reference/annotations.md +++ b/docs/sources/reference/annotations.md @@ -45,8 +45,9 @@ can still show them if you add a new **Annotation Query** and filter by tags. Bu ### Query by tag You can create new annotation queries that fetch annotations from the native annotation store via the `-- Grafana --` data source and by setting *Filter by* to `Tags`. Specify at least -one tag. For example create an annotation query name `outages` and specify a tag named `outage`. This query will show all annotations you create (from any dashboard or via API) that -have the `outage` tag. +one tag. For example create an annotation query name `outages` and specify a tag named `outage`. This query will show all annotations you create (from any dashboard or via API) that have the `outage` tag. By default, if you add multiple tags in the annotation query, Grafana will only show annotations that have all the tags you supplied. You can invert the behavior by enabling `Match any` which means that Grafana will show annotations that contains at least one of the tags you supplied. + +In 5.4+ it's possible to use template variables in the tag query. So if you have a dashboard showing stats for different services and an template variable that dictates which services to show, you can now use the same template variable in your annotation query to only show annotations for those services. ## Querying other data sources From 8e5000fb3147903a5bbe76a9b15aaab411396159 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 17 Sep 2018 10:09:49 +0200 Subject: [PATCH 075/127] moves files from /tests to more appropriate folders ref #11433 --- {tests/api => devenv/e2e-api-tests}/clearState.test.ts | 0 {tests/api => devenv/e2e-api-tests}/client.ts | 0 {tests/api => devenv/e2e-api-tests}/dashboard.test.ts | 0 {tests/api => devenv/e2e-api-tests}/folder.test.ts | 0 {tests/api => devenv/e2e-api-tests}/jest.js | 0 {tests/api => devenv/e2e-api-tests}/search.test.ts | 0 {tests/api => devenv/e2e-api-tests}/setup.ts | 0 {tests/api => devenv/e2e-api-tests}/tsconfig.json | 0 {tests/api => devenv/e2e-api-tests}/user.test.ts | 0 pkg/setting/setting_test.go | 8 ++++---- {tests/config-files => pkg/setting/testdata}/override.ini | 0 .../setting/testdata}/override_windows.ini | 0 12 files changed, 4 insertions(+), 4 deletions(-) rename {tests/api => devenv/e2e-api-tests}/clearState.test.ts (100%) rename {tests/api => devenv/e2e-api-tests}/client.ts (100%) rename {tests/api => devenv/e2e-api-tests}/dashboard.test.ts (100%) rename {tests/api => devenv/e2e-api-tests}/folder.test.ts (100%) rename {tests/api => devenv/e2e-api-tests}/jest.js (100%) rename {tests/api => devenv/e2e-api-tests}/search.test.ts (100%) rename {tests/api => devenv/e2e-api-tests}/setup.ts (100%) rename {tests/api => devenv/e2e-api-tests}/tsconfig.json (100%) rename {tests/api => devenv/e2e-api-tests}/user.test.ts (100%) rename {tests/config-files => pkg/setting/testdata}/override.ini (100%) rename {tests/config-files => pkg/setting/testdata}/override_windows.ini (100%) diff --git a/tests/api/clearState.test.ts b/devenv/e2e-api-tests/clearState.test.ts similarity index 100% rename from tests/api/clearState.test.ts rename to devenv/e2e-api-tests/clearState.test.ts diff --git a/tests/api/client.ts b/devenv/e2e-api-tests/client.ts similarity index 100% rename from tests/api/client.ts rename to devenv/e2e-api-tests/client.ts diff --git a/tests/api/dashboard.test.ts b/devenv/e2e-api-tests/dashboard.test.ts similarity index 100% rename from tests/api/dashboard.test.ts rename to devenv/e2e-api-tests/dashboard.test.ts diff --git a/tests/api/folder.test.ts b/devenv/e2e-api-tests/folder.test.ts similarity index 100% rename from tests/api/folder.test.ts rename to devenv/e2e-api-tests/folder.test.ts diff --git a/tests/api/jest.js b/devenv/e2e-api-tests/jest.js similarity index 100% rename from tests/api/jest.js rename to devenv/e2e-api-tests/jest.js diff --git a/tests/api/search.test.ts b/devenv/e2e-api-tests/search.test.ts similarity index 100% rename from tests/api/search.test.ts rename to devenv/e2e-api-tests/search.test.ts diff --git a/tests/api/setup.ts b/devenv/e2e-api-tests/setup.ts similarity index 100% rename from tests/api/setup.ts rename to devenv/e2e-api-tests/setup.ts diff --git a/tests/api/tsconfig.json b/devenv/e2e-api-tests/tsconfig.json similarity index 100% rename from tests/api/tsconfig.json rename to devenv/e2e-api-tests/tsconfig.json diff --git a/tests/api/user.test.ts b/devenv/e2e-api-tests/user.test.ts similarity index 100% rename from tests/api/user.test.ts rename to devenv/e2e-api-tests/user.test.ts diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index affb3c3e7ca..9b19b6c6bfa 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -97,7 +97,7 @@ func TestLoadingSettings(t *testing.T) { Args: []string{ "cfg:default.server.domain=test2", }, - Config: filepath.Join(HomePath, "tests/config-files/override.ini"), + Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"), }) So(Domain, ShouldEqual, "test2") @@ -108,7 +108,7 @@ func TestLoadingSettings(t *testing.T) { cfg := NewCfg() cfg.Load(&CommandLineArgs{ HomePath: "../../", - Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"), + Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"), Args: []string{`cfg:default.paths.data=c:\tmp\data`}, }) @@ -117,7 +117,7 @@ func TestLoadingSettings(t *testing.T) { cfg := NewCfg() cfg.Load(&CommandLineArgs{ HomePath: "../../", - Config: filepath.Join(HomePath, "tests/config-files/override.ini"), + Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"), Args: []string{"cfg:default.paths.data=/tmp/data"}, }) @@ -139,7 +139,7 @@ func TestLoadingSettings(t *testing.T) { cfg := NewCfg() cfg.Load(&CommandLineArgs{ HomePath: "../../", - Config: filepath.Join(HomePath, "tests/config-files/override.ini"), + Config: filepath.Join(HomePath, "pkg/setting/testdata/override.ini"), Args: []string{"cfg:paths.data=/tmp/data"}, }) diff --git a/tests/config-files/override.ini b/pkg/setting/testdata/override.ini similarity index 100% rename from tests/config-files/override.ini rename to pkg/setting/testdata/override.ini diff --git a/tests/config-files/override_windows.ini b/pkg/setting/testdata/override_windows.ini similarity index 100% rename from tests/config-files/override_windows.ini rename to pkg/setting/testdata/override_windows.ini From 6ffca7f1842246d6cf4cd1708dc7bd0752144706 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 17 Sep 2018 10:32:28 +0200 Subject: [PATCH 076/127] docs: add version disclaimer for postgres query editor --- docs/sources/features/datasources/postgres.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 4bb27e7f4f9..7076ff033b3 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -71,6 +71,8 @@ Make sure the user does not get any unwanted privileges from the public role. ## Query Editor +> Only available in Grafana v5.3+. + {{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} You find the PostgreSQL query editor in the metrics tab in Graph or Singlestat panel's edit mode. You enter edit mode by clicking the @@ -85,7 +87,7 @@ When you enter edit mode for the first time or add a new query Grafana will try In the FROM field, Grafana will suggest tables that are in the `search_path` of the database user. To select a table or view not in your `search_path` you can manually enter a fully qualified name (schema.table) like `public.metrics`. -The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name. +The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name. The metric column suggestions will only contain columns with a text datatype (char,varchar,text). If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `ip::text`. @@ -123,7 +125,7 @@ If you add any grouping, all selected columns need to have an aggregate function #### Gap Filling -Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with. +Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with. ### Text Editor Mode (RAW) You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query. @@ -274,7 +276,7 @@ SELECT hostname FROM host WHERE region IN($region) ### Using Variables in Queries -From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically. If your template variables are strings, do not wrap them in quotes in where clauses. +From Grafana 4.3.0 to 4.6.0, template variables are always quoted automatically. If your template variables are strings, do not wrap them in quotes in where clauses. From Grafana 4.7.0, template variable values are only quoted when the template variable is a `multi-value`. From d9ca8b43b7a6d177ada5705c6848af2a5696c2ab Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 17 Sep 2018 10:49:42 +0200 Subject: [PATCH 077/127] changelog: adds note about closing #9735 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4a9edcee6..5d32615ce97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 5.4.0 (unreleased) +### New Features + +* **Annotations**: Enable template variables in tagged annotations queries [#9735](https://github.com/grafana/grafana/issues/9735) + ### Minor * **OAuth**: Allow oauth email attribute name to be configurable [#12986](https://github.com/grafana/grafana/issues/12986), thx [@bobmshannon](https://github.com/bobmshannon) From c1f797ed9089878f5eebc80937955a53446c01a2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 17 Sep 2018 09:53:37 +0200 Subject: [PATCH 078/127] moves docker/ to devenv/docker --- .circleci/config.yml | 4 +-- {docker => devenv}/create_docker_compose.sh | 4 +-- .../docker}/blocks/apache_proxy/Dockerfile | 0 .../docker}/blocks/apache_proxy/ports.conf | 0 .../docker}/blocks/apache_proxy/proxy.conf | 0 .../docker}/blocks/collectd/Dockerfile | 0 .../docker}/blocks/collectd/README.md | 0 .../docker}/blocks/collectd/collectd.conf.tpl | 0 .../docker}/blocks/collectd/etc_mtab | 0 .../docker}/blocks/collectd/start_container | 0 .../docker}/blocks/elastic/elasticsearch.yml | 0 .../docker}/blocks/elastic1/elasticsearch.yml | 0 .../docker}/blocks/elastic5/elasticsearch.yml | 0 .../docker}/blocks/elastic6/elasticsearch.yml | 0 .../docker}/blocks/graphite/Dockerfile | 0 .../docker}/blocks/graphite/files/carbon.conf | 0 .../blocks/graphite/files/events_views.py | 0 .../blocks/graphite/files/initial_data.json | 0 .../blocks/graphite/files/local_settings.py | 0 .../docker}/blocks/graphite/files/my_htpasswd | 0 .../docker}/blocks/graphite/files/nginx.conf | 0 .../blocks/graphite/files/statsd_config.js | 0 .../graphite/files/storage-aggregation.conf | 0 .../graphite/files/storage-schemas.conf | 0 .../blocks/graphite/files/supervisord.conf | 0 .../docker}/blocks/graphite1/Dockerfile | 0 .../blocks/graphite1/big-dashboard.json | 0 .../conf/etc/logrotate.d/graphite-statsd | 0 .../conf/etc/my_init.d/01_conf_init.sh | 0 .../graphite1/conf/etc/nginx/nginx.conf | 0 .../nginx/sites-enabled/graphite-statsd.conf | 0 .../conf/etc/service/carbon-aggregator/run | 0 .../graphite1/conf/etc/service/carbon/run | 0 .../graphite1/conf/etc/service/graphite/run | 0 .../graphite1/conf/etc/service/nginx/run | 0 .../graphite1/conf/etc/service/statsd/run | 0 .../opt/graphite/conf/aggregation-rules.conf | 0 .../conf/opt/graphite/conf/blacklist.conf | 0 .../conf/opt/graphite/conf/carbon.amqp.conf | 0 .../conf/opt/graphite/conf/carbon.conf | 0 .../conf/opt/graphite/conf/dashboard.conf | 0 .../opt/graphite/conf/graphTemplates.conf | 0 .../conf/opt/graphite/conf/relay-rules.conf | 0 .../conf/opt/graphite/conf/rewrite-rules.conf | 0 .../graphite/conf/storage-aggregation.conf | 0 .../opt/graphite/conf/storage-schemas.conf | 0 .../conf/opt/graphite/conf/whitelist.conf | 0 .../graphite/webapp/graphite/app_settings.py | 0 .../webapp/graphite/local_settings.py | 0 .../graphite1/conf/opt/statsd/config.js | 0 .../conf/usr/local/bin/django_admin_init.exp | 0 .../graphite1/conf/usr/local/bin/manage.sh | 0 .../blocks/graphite11/big-dashboard.json | 0 .../docker}/blocks/influxdb/influxdb.conf | 0 .../docker}/blocks/mssql/build/Dockerfile | 0 .../docker}/blocks/mssql/build/entrypoint.sh | 0 .../docker}/blocks/mssql/build/setup.sh | 0 .../blocks/mssql/build/setup.sql.template | 0 {docker => devenv/docker}/blocks/mysql/config | 0 .../docker}/blocks/mysql_opendata/Dockerfile | 0 .../blocks/mysql_opendata/import_csv.sql | 0 .../docker}/blocks/mysql_tests/Dockerfile | 0 .../docker}/blocks/mysql_tests/setup.sql | 0 .../docker}/blocks/nginx_proxy/Dockerfile | 0 .../docker}/blocks/nginx_proxy/htpasswd | 0 .../docker}/blocks/nginx_proxy/nginx.conf | 0 .../docker}/blocks/openldap/Dockerfile | 0 .../docker}/blocks/openldap/entrypoint.sh | 0 .../docker}/blocks/openldap/ldap_dev.toml | 0 .../blocks/openldap/modules/memberof.ldif | 0 .../docker}/blocks/openldap/notes.md | 0 .../docker}/blocks/openldap/prepopulate.sh | 0 .../blocks/openldap/prepopulate/1_units.ldif | 0 .../blocks/openldap/prepopulate/2_users.ldif | 0 .../blocks/openldap/prepopulate/3_groups.ldif | 0 .../docker}/blocks/postgres_tests/Dockerfile | 0 .../docker}/blocks/postgres_tests/setup.sql | 0 .../docker}/blocks/prometheus/Dockerfile | 0 .../docker}/blocks/prometheus/alert.rules | 0 .../docker}/blocks/prometheus/prometheus.yml | 0 .../docker}/blocks/prometheus2/Dockerfile | 0 .../docker}/blocks/prometheus2/alert.rules | 0 .../docker}/blocks/prometheus2/prometheus.yml | 0 .../docker}/blocks/prometheus_mac/Dockerfile | 0 .../docker}/blocks/prometheus_mac/alert.rules | 0 .../blocks/prometheus_mac/prometheus.yml | 0 .../blocks/prometheus_random_data/Dockerfile | 0 .../docker}/blocks/smtp/Dockerfile | 0 .../docker}/blocks/smtp/bootstrap.sh | 0 .../docker}/buildcontainer/Dockerfile | 0 .../docker}/buildcontainer/build.sh | 0 .../docker}/buildcontainer/build_circle.sh | 0 .../docker}/buildcontainer/run_circle.sh | 0 {docker => devenv/docker}/compose_header.yml | 0 {docker => devenv/docker}/debtest/Dockerfile | 0 {docker => devenv/docker}/debtest/build.sh | 0 {docker => devenv/docker}/rpmtest/build.sh | 0 .../blocks/apache_proxy/docker-compose.yaml | 9 ------ docker/blocks/collectd/docker-compose.yaml | 11 ------- docker/blocks/elastic/docker-compose.yaml | 15 --------- docker/blocks/elastic1/docker-compose.yaml | 8 ----- docker/blocks/elastic5/docker-compose.yaml | 15 --------- docker/blocks/elastic6/docker-compose.yaml | 15 --------- docker/blocks/graphite/docker-compose.yaml | 16 ---------- docker/blocks/graphite1/docker-compose.yaml | 21 ------------- docker/blocks/graphite11/docker-compose.yaml | 18 ----------- docker/blocks/influxdb/docker-compose.yaml | 17 ---------- docker/blocks/jaeger/docker-compose.yaml | 6 ---- docker/blocks/memcached/docker-compose.yaml | 5 --- docker/blocks/mssql/docker-compose.yaml | 19 ------------ docker/blocks/mssql_tests/docker-compose.yaml | 12 ------- docker/blocks/mysql/docker-compose.yaml | 18 ----------- .../blocks/mysql_opendata/docker-compose.yaml | 9 ------ docker/blocks/mysql_tests/docker-compose.yaml | 11 ------- docker/blocks/nginx_proxy/docker-compose.yaml | 9 ------ docker/blocks/openldap/docker-compose.yaml | 10 ------ docker/blocks/opentsdb/docker-compose.yaml | 11 ------- docker/blocks/postgres/docker-compose.yaml | 16 ---------- .../blocks/postgres_tests/docker-compose.yaml | 9 ------ docker/blocks/prometheus/docker-compose.yaml | 31 ------------------- docker/blocks/prometheus2/docker-compose.yaml | 31 ------------------- .../blocks/prometheus_mac/docker-compose.yaml | 26 ---------------- docker/blocks/smtp/docker-compose.yaml | 4 --- 123 files changed, 4 insertions(+), 376 deletions(-) rename {docker => devenv}/create_docker_compose.sh (94%) rename {docker => devenv/docker}/blocks/apache_proxy/Dockerfile (100%) rename {docker => devenv/docker}/blocks/apache_proxy/ports.conf (100%) rename {docker => devenv/docker}/blocks/apache_proxy/proxy.conf (100%) rename {docker => devenv/docker}/blocks/collectd/Dockerfile (100%) rename {docker => devenv/docker}/blocks/collectd/README.md (100%) rename {docker => devenv/docker}/blocks/collectd/collectd.conf.tpl (100%) rename {docker => devenv/docker}/blocks/collectd/etc_mtab (100%) rename {docker => devenv/docker}/blocks/collectd/start_container (100%) rename {docker => devenv/docker}/blocks/elastic/elasticsearch.yml (100%) rename {docker => devenv/docker}/blocks/elastic1/elasticsearch.yml (100%) rename {docker => devenv/docker}/blocks/elastic5/elasticsearch.yml (100%) rename {docker => devenv/docker}/blocks/elastic6/elasticsearch.yml (100%) rename {docker => devenv/docker}/blocks/graphite/Dockerfile (100%) rename {docker => devenv/docker}/blocks/graphite/files/carbon.conf (100%) rename {docker => devenv/docker}/blocks/graphite/files/events_views.py (100%) rename {docker => devenv/docker}/blocks/graphite/files/initial_data.json (100%) rename {docker => devenv/docker}/blocks/graphite/files/local_settings.py (100%) rename {docker => devenv/docker}/blocks/graphite/files/my_htpasswd (100%) rename {docker => devenv/docker}/blocks/graphite/files/nginx.conf (100%) rename {docker => devenv/docker}/blocks/graphite/files/statsd_config.js (100%) rename {docker => devenv/docker}/blocks/graphite/files/storage-aggregation.conf (100%) rename {docker => devenv/docker}/blocks/graphite/files/storage-schemas.conf (100%) rename {docker => devenv/docker}/blocks/graphite/files/supervisord.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/Dockerfile (100%) rename {docker => devenv/docker}/blocks/graphite1/big-dashboard.json (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/nginx/nginx.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/service/carbon-aggregator/run (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/service/carbon/run (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/service/graphite/run (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/service/nginx/run (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/etc/service/statsd/run (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/carbon.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/opt/statsd/config.js (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp (100%) rename {docker => devenv/docker}/blocks/graphite1/conf/usr/local/bin/manage.sh (100%) rename {docker => devenv/docker}/blocks/graphite11/big-dashboard.json (100%) rename {docker => devenv/docker}/blocks/influxdb/influxdb.conf (100%) rename {docker => devenv/docker}/blocks/mssql/build/Dockerfile (100%) rename {docker => devenv/docker}/blocks/mssql/build/entrypoint.sh (100%) rename {docker => devenv/docker}/blocks/mssql/build/setup.sh (100%) rename {docker => devenv/docker}/blocks/mssql/build/setup.sql.template (100%) rename {docker => devenv/docker}/blocks/mysql/config (100%) rename {docker => devenv/docker}/blocks/mysql_opendata/Dockerfile (100%) rename {docker => devenv/docker}/blocks/mysql_opendata/import_csv.sql (100%) rename {docker => devenv/docker}/blocks/mysql_tests/Dockerfile (100%) rename {docker => devenv/docker}/blocks/mysql_tests/setup.sql (100%) rename {docker => devenv/docker}/blocks/nginx_proxy/Dockerfile (100%) rename {docker => devenv/docker}/blocks/nginx_proxy/htpasswd (100%) rename {docker => devenv/docker}/blocks/nginx_proxy/nginx.conf (100%) rename {docker => devenv/docker}/blocks/openldap/Dockerfile (100%) rename {docker => devenv/docker}/blocks/openldap/entrypoint.sh (100%) rename {docker => devenv/docker}/blocks/openldap/ldap_dev.toml (100%) rename {docker => devenv/docker}/blocks/openldap/modules/memberof.ldif (100%) rename {docker => devenv/docker}/blocks/openldap/notes.md (100%) rename {docker => devenv/docker}/blocks/openldap/prepopulate.sh (100%) rename {docker => devenv/docker}/blocks/openldap/prepopulate/1_units.ldif (100%) rename {docker => devenv/docker}/blocks/openldap/prepopulate/2_users.ldif (100%) rename {docker => devenv/docker}/blocks/openldap/prepopulate/3_groups.ldif (100%) rename {docker => devenv/docker}/blocks/postgres_tests/Dockerfile (100%) rename {docker => devenv/docker}/blocks/postgres_tests/setup.sql (100%) rename {docker => devenv/docker}/blocks/prometheus/Dockerfile (100%) rename {docker => devenv/docker}/blocks/prometheus/alert.rules (100%) rename {docker => devenv/docker}/blocks/prometheus/prometheus.yml (100%) rename {docker => devenv/docker}/blocks/prometheus2/Dockerfile (100%) rename {docker => devenv/docker}/blocks/prometheus2/alert.rules (100%) rename {docker => devenv/docker}/blocks/prometheus2/prometheus.yml (100%) rename {docker => devenv/docker}/blocks/prometheus_mac/Dockerfile (100%) rename {docker => devenv/docker}/blocks/prometheus_mac/alert.rules (100%) rename {docker => devenv/docker}/blocks/prometheus_mac/prometheus.yml (100%) rename {docker => devenv/docker}/blocks/prometheus_random_data/Dockerfile (100%) rename {docker => devenv/docker}/blocks/smtp/Dockerfile (100%) rename {docker => devenv/docker}/blocks/smtp/bootstrap.sh (100%) rename {docker => devenv/docker}/buildcontainer/Dockerfile (100%) rename {docker => devenv/docker}/buildcontainer/build.sh (100%) rename {docker => devenv/docker}/buildcontainer/build_circle.sh (100%) rename {docker => devenv/docker}/buildcontainer/run_circle.sh (100%) rename {docker => devenv/docker}/compose_header.yml (100%) rename {docker => devenv/docker}/debtest/Dockerfile (100%) rename {docker => devenv/docker}/debtest/build.sh (100%) rename {docker => devenv/docker}/rpmtest/build.sh (100%) delete mode 100644 docker/blocks/apache_proxy/docker-compose.yaml delete mode 100644 docker/blocks/collectd/docker-compose.yaml delete mode 100644 docker/blocks/elastic/docker-compose.yaml delete mode 100644 docker/blocks/elastic1/docker-compose.yaml delete mode 100644 docker/blocks/elastic5/docker-compose.yaml delete mode 100644 docker/blocks/elastic6/docker-compose.yaml delete mode 100644 docker/blocks/graphite/docker-compose.yaml delete mode 100644 docker/blocks/graphite1/docker-compose.yaml delete mode 100644 docker/blocks/graphite11/docker-compose.yaml delete mode 100644 docker/blocks/influxdb/docker-compose.yaml delete mode 100644 docker/blocks/jaeger/docker-compose.yaml delete mode 100644 docker/blocks/memcached/docker-compose.yaml delete mode 100644 docker/blocks/mssql/docker-compose.yaml delete mode 100644 docker/blocks/mssql_tests/docker-compose.yaml delete mode 100644 docker/blocks/mysql/docker-compose.yaml delete mode 100644 docker/blocks/mysql_opendata/docker-compose.yaml delete mode 100644 docker/blocks/mysql_tests/docker-compose.yaml delete mode 100644 docker/blocks/nginx_proxy/docker-compose.yaml delete mode 100644 docker/blocks/openldap/docker-compose.yaml delete mode 100644 docker/blocks/opentsdb/docker-compose.yaml delete mode 100644 docker/blocks/postgres/docker-compose.yaml delete mode 100644 docker/blocks/postgres_tests/docker-compose.yaml delete mode 100644 docker/blocks/prometheus/docker-compose.yaml delete mode 100644 docker/blocks/prometheus2/docker-compose.yaml delete mode 100644 docker/blocks/prometheus_mac/docker-compose.yaml delete mode 100644 docker/blocks/smtp/docker-compose.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml index 186997d0045..e631e0a8d33 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -32,7 +32,7 @@ jobs: - run: sudo apt update - run: sudo apt install -y mysql-client - run: dockerize -wait tcp://127.0.0.1:3306 -timeout 120s - - run: cat docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass + - run: cat devenv/docker/blocks/mysql_tests/setup.sql | mysql -h 127.0.0.1 -P 3306 -u root -prootpass - run: name: mysql integration tests command: 'GRAFANA_TEST_DB=mysql go test ./pkg/services/sqlstore/... ./pkg/tsdb/mysql/... ' @@ -51,7 +51,7 @@ jobs: - run: sudo apt update - run: sudo apt install -y postgresql-client - run: dockerize -wait tcp://127.0.0.1:5432 -timeout 120s - - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f docker/blocks/postgres_tests/setup.sql' + - run: 'PGPASSWORD=grafanatest psql -p 5432 -h 127.0.0.1 -U grafanatest -d grafanatest -f devenv/docker/blocks/postgres_tests/setup.sql' - run: name: postgres integration tests command: 'GRAFANA_TEST_DB=postgres go test ./pkg/services/sqlstore/... ./pkg/tsdb/postgres/...' diff --git a/docker/create_docker_compose.sh b/devenv/create_docker_compose.sh similarity index 94% rename from docker/create_docker_compose.sh rename to devenv/create_docker_compose.sh index 9d28ede8e7e..5da9e8f5c8f 100755 --- a/docker/create_docker_compose.sh +++ b/devenv/create_docker_compose.sh @@ -1,13 +1,13 @@ #!/bin/bash -blocks_dir=blocks +blocks_dir=docker/blocks docker_dir=docker template_dir=templates grafana_config_file=conf.tmp grafana_config=config -compose_header_file=compose_header.yml +compose_header_file=docker/compose_header.yml fig_file=docker-compose.yaml fig_config=docker-compose.yaml diff --git a/docker/blocks/apache_proxy/Dockerfile b/devenv/docker/blocks/apache_proxy/Dockerfile similarity index 100% rename from docker/blocks/apache_proxy/Dockerfile rename to devenv/docker/blocks/apache_proxy/Dockerfile diff --git a/docker/blocks/apache_proxy/ports.conf b/devenv/docker/blocks/apache_proxy/ports.conf similarity index 100% rename from docker/blocks/apache_proxy/ports.conf rename to devenv/docker/blocks/apache_proxy/ports.conf diff --git a/docker/blocks/apache_proxy/proxy.conf b/devenv/docker/blocks/apache_proxy/proxy.conf similarity index 100% rename from docker/blocks/apache_proxy/proxy.conf rename to devenv/docker/blocks/apache_proxy/proxy.conf diff --git a/docker/blocks/collectd/Dockerfile b/devenv/docker/blocks/collectd/Dockerfile similarity index 100% rename from docker/blocks/collectd/Dockerfile rename to devenv/docker/blocks/collectd/Dockerfile diff --git a/docker/blocks/collectd/README.md b/devenv/docker/blocks/collectd/README.md similarity index 100% rename from docker/blocks/collectd/README.md rename to devenv/docker/blocks/collectd/README.md diff --git a/docker/blocks/collectd/collectd.conf.tpl b/devenv/docker/blocks/collectd/collectd.conf.tpl similarity index 100% rename from docker/blocks/collectd/collectd.conf.tpl rename to devenv/docker/blocks/collectd/collectd.conf.tpl diff --git a/docker/blocks/collectd/etc_mtab b/devenv/docker/blocks/collectd/etc_mtab similarity index 100% rename from docker/blocks/collectd/etc_mtab rename to devenv/docker/blocks/collectd/etc_mtab diff --git a/docker/blocks/collectd/start_container b/devenv/docker/blocks/collectd/start_container similarity index 100% rename from docker/blocks/collectd/start_container rename to devenv/docker/blocks/collectd/start_container diff --git a/docker/blocks/elastic/elasticsearch.yml b/devenv/docker/blocks/elastic/elasticsearch.yml similarity index 100% rename from docker/blocks/elastic/elasticsearch.yml rename to devenv/docker/blocks/elastic/elasticsearch.yml diff --git a/docker/blocks/elastic1/elasticsearch.yml b/devenv/docker/blocks/elastic1/elasticsearch.yml similarity index 100% rename from docker/blocks/elastic1/elasticsearch.yml rename to devenv/docker/blocks/elastic1/elasticsearch.yml diff --git a/docker/blocks/elastic5/elasticsearch.yml b/devenv/docker/blocks/elastic5/elasticsearch.yml similarity index 100% rename from docker/blocks/elastic5/elasticsearch.yml rename to devenv/docker/blocks/elastic5/elasticsearch.yml diff --git a/docker/blocks/elastic6/elasticsearch.yml b/devenv/docker/blocks/elastic6/elasticsearch.yml similarity index 100% rename from docker/blocks/elastic6/elasticsearch.yml rename to devenv/docker/blocks/elastic6/elasticsearch.yml diff --git a/docker/blocks/graphite/Dockerfile b/devenv/docker/blocks/graphite/Dockerfile similarity index 100% rename from docker/blocks/graphite/Dockerfile rename to devenv/docker/blocks/graphite/Dockerfile diff --git a/docker/blocks/graphite/files/carbon.conf b/devenv/docker/blocks/graphite/files/carbon.conf similarity index 100% rename from docker/blocks/graphite/files/carbon.conf rename to devenv/docker/blocks/graphite/files/carbon.conf diff --git a/docker/blocks/graphite/files/events_views.py b/devenv/docker/blocks/graphite/files/events_views.py similarity index 100% rename from docker/blocks/graphite/files/events_views.py rename to devenv/docker/blocks/graphite/files/events_views.py diff --git a/docker/blocks/graphite/files/initial_data.json b/devenv/docker/blocks/graphite/files/initial_data.json similarity index 100% rename from docker/blocks/graphite/files/initial_data.json rename to devenv/docker/blocks/graphite/files/initial_data.json diff --git a/docker/blocks/graphite/files/local_settings.py b/devenv/docker/blocks/graphite/files/local_settings.py similarity index 100% rename from docker/blocks/graphite/files/local_settings.py rename to devenv/docker/blocks/graphite/files/local_settings.py diff --git a/docker/blocks/graphite/files/my_htpasswd b/devenv/docker/blocks/graphite/files/my_htpasswd similarity index 100% rename from docker/blocks/graphite/files/my_htpasswd rename to devenv/docker/blocks/graphite/files/my_htpasswd diff --git a/docker/blocks/graphite/files/nginx.conf b/devenv/docker/blocks/graphite/files/nginx.conf similarity index 100% rename from docker/blocks/graphite/files/nginx.conf rename to devenv/docker/blocks/graphite/files/nginx.conf diff --git a/docker/blocks/graphite/files/statsd_config.js b/devenv/docker/blocks/graphite/files/statsd_config.js similarity index 100% rename from docker/blocks/graphite/files/statsd_config.js rename to devenv/docker/blocks/graphite/files/statsd_config.js diff --git a/docker/blocks/graphite/files/storage-aggregation.conf b/devenv/docker/blocks/graphite/files/storage-aggregation.conf similarity index 100% rename from docker/blocks/graphite/files/storage-aggregation.conf rename to devenv/docker/blocks/graphite/files/storage-aggregation.conf diff --git a/docker/blocks/graphite/files/storage-schemas.conf b/devenv/docker/blocks/graphite/files/storage-schemas.conf similarity index 100% rename from docker/blocks/graphite/files/storage-schemas.conf rename to devenv/docker/blocks/graphite/files/storage-schemas.conf diff --git a/docker/blocks/graphite/files/supervisord.conf b/devenv/docker/blocks/graphite/files/supervisord.conf similarity index 100% rename from docker/blocks/graphite/files/supervisord.conf rename to devenv/docker/blocks/graphite/files/supervisord.conf diff --git a/docker/blocks/graphite1/Dockerfile b/devenv/docker/blocks/graphite1/Dockerfile similarity index 100% rename from docker/blocks/graphite1/Dockerfile rename to devenv/docker/blocks/graphite1/Dockerfile diff --git a/docker/blocks/graphite1/big-dashboard.json b/devenv/docker/blocks/graphite1/big-dashboard.json similarity index 100% rename from docker/blocks/graphite1/big-dashboard.json rename to devenv/docker/blocks/graphite1/big-dashboard.json diff --git a/docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd b/devenv/docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd similarity index 100% rename from docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd rename to devenv/docker/blocks/graphite1/conf/etc/logrotate.d/graphite-statsd diff --git a/docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh b/devenv/docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh similarity index 100% rename from docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh rename to devenv/docker/blocks/graphite1/conf/etc/my_init.d/01_conf_init.sh diff --git a/docker/blocks/graphite1/conf/etc/nginx/nginx.conf b/devenv/docker/blocks/graphite1/conf/etc/nginx/nginx.conf similarity index 100% rename from docker/blocks/graphite1/conf/etc/nginx/nginx.conf rename to devenv/docker/blocks/graphite1/conf/etc/nginx/nginx.conf diff --git a/docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf b/devenv/docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf similarity index 100% rename from docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf rename to devenv/docker/blocks/graphite1/conf/etc/nginx/sites-enabled/graphite-statsd.conf diff --git a/docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run b/devenv/docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run similarity index 100% rename from docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run rename to devenv/docker/blocks/graphite1/conf/etc/service/carbon-aggregator/run diff --git a/docker/blocks/graphite1/conf/etc/service/carbon/run b/devenv/docker/blocks/graphite1/conf/etc/service/carbon/run similarity index 100% rename from docker/blocks/graphite1/conf/etc/service/carbon/run rename to devenv/docker/blocks/graphite1/conf/etc/service/carbon/run diff --git a/docker/blocks/graphite1/conf/etc/service/graphite/run b/devenv/docker/blocks/graphite1/conf/etc/service/graphite/run similarity index 100% rename from docker/blocks/graphite1/conf/etc/service/graphite/run rename to devenv/docker/blocks/graphite1/conf/etc/service/graphite/run diff --git a/docker/blocks/graphite1/conf/etc/service/nginx/run b/devenv/docker/blocks/graphite1/conf/etc/service/nginx/run similarity index 100% rename from docker/blocks/graphite1/conf/etc/service/nginx/run rename to devenv/docker/blocks/graphite1/conf/etc/service/nginx/run diff --git a/docker/blocks/graphite1/conf/etc/service/statsd/run b/devenv/docker/blocks/graphite1/conf/etc/service/statsd/run similarity index 100% rename from docker/blocks/graphite1/conf/etc/service/statsd/run rename to devenv/docker/blocks/graphite1/conf/etc/service/statsd/run diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/blacklist.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.amqp.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/carbon.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/dashboard.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/graphTemplates.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/relay-rules.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/rewrite-rules.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-aggregation.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/storage-schemas.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf rename to devenv/docker/blocks/graphite1/conf/opt/graphite/conf/whitelist.conf diff --git a/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py b/devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py rename to devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/app_settings.py diff --git a/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py b/devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py similarity index 100% rename from docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py rename to devenv/docker/blocks/graphite1/conf/opt/graphite/webapp/graphite/local_settings.py diff --git a/docker/blocks/graphite1/conf/opt/statsd/config.js b/devenv/docker/blocks/graphite1/conf/opt/statsd/config.js similarity index 100% rename from docker/blocks/graphite1/conf/opt/statsd/config.js rename to devenv/docker/blocks/graphite1/conf/opt/statsd/config.js diff --git a/docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp b/devenv/docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp similarity index 100% rename from docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp rename to devenv/docker/blocks/graphite1/conf/usr/local/bin/django_admin_init.exp diff --git a/docker/blocks/graphite1/conf/usr/local/bin/manage.sh b/devenv/docker/blocks/graphite1/conf/usr/local/bin/manage.sh similarity index 100% rename from docker/blocks/graphite1/conf/usr/local/bin/manage.sh rename to devenv/docker/blocks/graphite1/conf/usr/local/bin/manage.sh diff --git a/docker/blocks/graphite11/big-dashboard.json b/devenv/docker/blocks/graphite11/big-dashboard.json similarity index 100% rename from docker/blocks/graphite11/big-dashboard.json rename to devenv/docker/blocks/graphite11/big-dashboard.json diff --git a/docker/blocks/influxdb/influxdb.conf b/devenv/docker/blocks/influxdb/influxdb.conf similarity index 100% rename from docker/blocks/influxdb/influxdb.conf rename to devenv/docker/blocks/influxdb/influxdb.conf diff --git a/docker/blocks/mssql/build/Dockerfile b/devenv/docker/blocks/mssql/build/Dockerfile similarity index 100% rename from docker/blocks/mssql/build/Dockerfile rename to devenv/docker/blocks/mssql/build/Dockerfile diff --git a/docker/blocks/mssql/build/entrypoint.sh b/devenv/docker/blocks/mssql/build/entrypoint.sh similarity index 100% rename from docker/blocks/mssql/build/entrypoint.sh rename to devenv/docker/blocks/mssql/build/entrypoint.sh diff --git a/docker/blocks/mssql/build/setup.sh b/devenv/docker/blocks/mssql/build/setup.sh similarity index 100% rename from docker/blocks/mssql/build/setup.sh rename to devenv/docker/blocks/mssql/build/setup.sh diff --git a/docker/blocks/mssql/build/setup.sql.template b/devenv/docker/blocks/mssql/build/setup.sql.template similarity index 100% rename from docker/blocks/mssql/build/setup.sql.template rename to devenv/docker/blocks/mssql/build/setup.sql.template diff --git a/docker/blocks/mysql/config b/devenv/docker/blocks/mysql/config similarity index 100% rename from docker/blocks/mysql/config rename to devenv/docker/blocks/mysql/config diff --git a/docker/blocks/mysql_opendata/Dockerfile b/devenv/docker/blocks/mysql_opendata/Dockerfile similarity index 100% rename from docker/blocks/mysql_opendata/Dockerfile rename to devenv/docker/blocks/mysql_opendata/Dockerfile diff --git a/docker/blocks/mysql_opendata/import_csv.sql b/devenv/docker/blocks/mysql_opendata/import_csv.sql similarity index 100% rename from docker/blocks/mysql_opendata/import_csv.sql rename to devenv/docker/blocks/mysql_opendata/import_csv.sql diff --git a/docker/blocks/mysql_tests/Dockerfile b/devenv/docker/blocks/mysql_tests/Dockerfile similarity index 100% rename from docker/blocks/mysql_tests/Dockerfile rename to devenv/docker/blocks/mysql_tests/Dockerfile diff --git a/docker/blocks/mysql_tests/setup.sql b/devenv/docker/blocks/mysql_tests/setup.sql similarity index 100% rename from docker/blocks/mysql_tests/setup.sql rename to devenv/docker/blocks/mysql_tests/setup.sql diff --git a/docker/blocks/nginx_proxy/Dockerfile b/devenv/docker/blocks/nginx_proxy/Dockerfile similarity index 100% rename from docker/blocks/nginx_proxy/Dockerfile rename to devenv/docker/blocks/nginx_proxy/Dockerfile diff --git a/docker/blocks/nginx_proxy/htpasswd b/devenv/docker/blocks/nginx_proxy/htpasswd similarity index 100% rename from docker/blocks/nginx_proxy/htpasswd rename to devenv/docker/blocks/nginx_proxy/htpasswd diff --git a/docker/blocks/nginx_proxy/nginx.conf b/devenv/docker/blocks/nginx_proxy/nginx.conf similarity index 100% rename from docker/blocks/nginx_proxy/nginx.conf rename to devenv/docker/blocks/nginx_proxy/nginx.conf diff --git a/docker/blocks/openldap/Dockerfile b/devenv/docker/blocks/openldap/Dockerfile similarity index 100% rename from docker/blocks/openldap/Dockerfile rename to devenv/docker/blocks/openldap/Dockerfile diff --git a/docker/blocks/openldap/entrypoint.sh b/devenv/docker/blocks/openldap/entrypoint.sh similarity index 100% rename from docker/blocks/openldap/entrypoint.sh rename to devenv/docker/blocks/openldap/entrypoint.sh diff --git a/docker/blocks/openldap/ldap_dev.toml b/devenv/docker/blocks/openldap/ldap_dev.toml similarity index 100% rename from docker/blocks/openldap/ldap_dev.toml rename to devenv/docker/blocks/openldap/ldap_dev.toml diff --git a/docker/blocks/openldap/modules/memberof.ldif b/devenv/docker/blocks/openldap/modules/memberof.ldif similarity index 100% rename from docker/blocks/openldap/modules/memberof.ldif rename to devenv/docker/blocks/openldap/modules/memberof.ldif diff --git a/docker/blocks/openldap/notes.md b/devenv/docker/blocks/openldap/notes.md similarity index 100% rename from docker/blocks/openldap/notes.md rename to devenv/docker/blocks/openldap/notes.md diff --git a/docker/blocks/openldap/prepopulate.sh b/devenv/docker/blocks/openldap/prepopulate.sh similarity index 100% rename from docker/blocks/openldap/prepopulate.sh rename to devenv/docker/blocks/openldap/prepopulate.sh diff --git a/docker/blocks/openldap/prepopulate/1_units.ldif b/devenv/docker/blocks/openldap/prepopulate/1_units.ldif similarity index 100% rename from docker/blocks/openldap/prepopulate/1_units.ldif rename to devenv/docker/blocks/openldap/prepopulate/1_units.ldif diff --git a/docker/blocks/openldap/prepopulate/2_users.ldif b/devenv/docker/blocks/openldap/prepopulate/2_users.ldif similarity index 100% rename from docker/blocks/openldap/prepopulate/2_users.ldif rename to devenv/docker/blocks/openldap/prepopulate/2_users.ldif diff --git a/docker/blocks/openldap/prepopulate/3_groups.ldif b/devenv/docker/blocks/openldap/prepopulate/3_groups.ldif similarity index 100% rename from docker/blocks/openldap/prepopulate/3_groups.ldif rename to devenv/docker/blocks/openldap/prepopulate/3_groups.ldif diff --git a/docker/blocks/postgres_tests/Dockerfile b/devenv/docker/blocks/postgres_tests/Dockerfile similarity index 100% rename from docker/blocks/postgres_tests/Dockerfile rename to devenv/docker/blocks/postgres_tests/Dockerfile diff --git a/docker/blocks/postgres_tests/setup.sql b/devenv/docker/blocks/postgres_tests/setup.sql similarity index 100% rename from docker/blocks/postgres_tests/setup.sql rename to devenv/docker/blocks/postgres_tests/setup.sql diff --git a/docker/blocks/prometheus/Dockerfile b/devenv/docker/blocks/prometheus/Dockerfile similarity index 100% rename from docker/blocks/prometheus/Dockerfile rename to devenv/docker/blocks/prometheus/Dockerfile diff --git a/docker/blocks/prometheus/alert.rules b/devenv/docker/blocks/prometheus/alert.rules similarity index 100% rename from docker/blocks/prometheus/alert.rules rename to devenv/docker/blocks/prometheus/alert.rules diff --git a/docker/blocks/prometheus/prometheus.yml b/devenv/docker/blocks/prometheus/prometheus.yml similarity index 100% rename from docker/blocks/prometheus/prometheus.yml rename to devenv/docker/blocks/prometheus/prometheus.yml diff --git a/docker/blocks/prometheus2/Dockerfile b/devenv/docker/blocks/prometheus2/Dockerfile similarity index 100% rename from docker/blocks/prometheus2/Dockerfile rename to devenv/docker/blocks/prometheus2/Dockerfile diff --git a/docker/blocks/prometheus2/alert.rules b/devenv/docker/blocks/prometheus2/alert.rules similarity index 100% rename from docker/blocks/prometheus2/alert.rules rename to devenv/docker/blocks/prometheus2/alert.rules diff --git a/docker/blocks/prometheus2/prometheus.yml b/devenv/docker/blocks/prometheus2/prometheus.yml similarity index 100% rename from docker/blocks/prometheus2/prometheus.yml rename to devenv/docker/blocks/prometheus2/prometheus.yml diff --git a/docker/blocks/prometheus_mac/Dockerfile b/devenv/docker/blocks/prometheus_mac/Dockerfile similarity index 100% rename from docker/blocks/prometheus_mac/Dockerfile rename to devenv/docker/blocks/prometheus_mac/Dockerfile diff --git a/docker/blocks/prometheus_mac/alert.rules b/devenv/docker/blocks/prometheus_mac/alert.rules similarity index 100% rename from docker/blocks/prometheus_mac/alert.rules rename to devenv/docker/blocks/prometheus_mac/alert.rules diff --git a/docker/blocks/prometheus_mac/prometheus.yml b/devenv/docker/blocks/prometheus_mac/prometheus.yml similarity index 100% rename from docker/blocks/prometheus_mac/prometheus.yml rename to devenv/docker/blocks/prometheus_mac/prometheus.yml diff --git a/docker/blocks/prometheus_random_data/Dockerfile b/devenv/docker/blocks/prometheus_random_data/Dockerfile similarity index 100% rename from docker/blocks/prometheus_random_data/Dockerfile rename to devenv/docker/blocks/prometheus_random_data/Dockerfile diff --git a/docker/blocks/smtp/Dockerfile b/devenv/docker/blocks/smtp/Dockerfile similarity index 100% rename from docker/blocks/smtp/Dockerfile rename to devenv/docker/blocks/smtp/Dockerfile diff --git a/docker/blocks/smtp/bootstrap.sh b/devenv/docker/blocks/smtp/bootstrap.sh similarity index 100% rename from docker/blocks/smtp/bootstrap.sh rename to devenv/docker/blocks/smtp/bootstrap.sh diff --git a/docker/buildcontainer/Dockerfile b/devenv/docker/buildcontainer/Dockerfile similarity index 100% rename from docker/buildcontainer/Dockerfile rename to devenv/docker/buildcontainer/Dockerfile diff --git a/docker/buildcontainer/build.sh b/devenv/docker/buildcontainer/build.sh similarity index 100% rename from docker/buildcontainer/build.sh rename to devenv/docker/buildcontainer/build.sh diff --git a/docker/buildcontainer/build_circle.sh b/devenv/docker/buildcontainer/build_circle.sh similarity index 100% rename from docker/buildcontainer/build_circle.sh rename to devenv/docker/buildcontainer/build_circle.sh diff --git a/docker/buildcontainer/run_circle.sh b/devenv/docker/buildcontainer/run_circle.sh similarity index 100% rename from docker/buildcontainer/run_circle.sh rename to devenv/docker/buildcontainer/run_circle.sh diff --git a/docker/compose_header.yml b/devenv/docker/compose_header.yml similarity index 100% rename from docker/compose_header.yml rename to devenv/docker/compose_header.yml diff --git a/docker/debtest/Dockerfile b/devenv/docker/debtest/Dockerfile similarity index 100% rename from docker/debtest/Dockerfile rename to devenv/docker/debtest/Dockerfile diff --git a/docker/debtest/build.sh b/devenv/docker/debtest/build.sh similarity index 100% rename from docker/debtest/build.sh rename to devenv/docker/debtest/build.sh diff --git a/docker/rpmtest/build.sh b/devenv/docker/rpmtest/build.sh similarity index 100% rename from docker/rpmtest/build.sh rename to devenv/docker/rpmtest/build.sh diff --git a/docker/blocks/apache_proxy/docker-compose.yaml b/docker/blocks/apache_proxy/docker-compose.yaml deleted file mode 100644 index 86d4befadd6..00000000000 --- a/docker/blocks/apache_proxy/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# This will proxy all requests for http://localhost:10081/grafana/ to -# http://localhost:3000 (Grafana running locally) -# -# Please note that you'll need to change the root_url in the Grafana configuration: -# root_url = %(protocol)s://%(domain)s:10081/grafana/ - - apacheproxy: - build: blocks/apache_proxy - network_mode: host diff --git a/docker/blocks/collectd/docker-compose.yaml b/docker/blocks/collectd/docker-compose.yaml deleted file mode 100644 index c95827f7928..00000000000 --- a/docker/blocks/collectd/docker-compose.yaml +++ /dev/null @@ -1,11 +0,0 @@ - collectd: - build: blocks/collectd - environment: - HOST_NAME: myserver - GRAPHITE_HOST: graphite - GRAPHITE_PORT: 2003 - GRAPHITE_PREFIX: collectd. - REPORT_BY_CPU: 'false' - COLLECT_INTERVAL: 10 - links: - - graphite diff --git a/docker/blocks/elastic/docker-compose.yaml b/docker/blocks/elastic/docker-compose.yaml deleted file mode 100644 index 2eba60f38be..00000000000 --- a/docker/blocks/elastic/docker-compose.yaml +++ /dev/null @@ -1,15 +0,0 @@ - elasticsearch: - image: elasticsearch:2.4.1 - command: elasticsearch -Des.network.host=0.0.0.0 - ports: - - "9200:9200" - - "9300:9300" - volumes: - - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml - - fake-elastic-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: elasticsearch - FD_PORT: 9200 diff --git a/docker/blocks/elastic1/docker-compose.yaml b/docker/blocks/elastic1/docker-compose.yaml deleted file mode 100644 index 518ae76e6ee..00000000000 --- a/docker/blocks/elastic1/docker-compose.yaml +++ /dev/null @@ -1,8 +0,0 @@ - elasticsearch1: - image: elasticsearch:1.7.6 - command: elasticsearch -Des.network.host=0.0.0.0 - ports: - - "11200:9200" - - "11300:9300" - volumes: - - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml diff --git a/docker/blocks/elastic5/docker-compose.yaml b/docker/blocks/elastic5/docker-compose.yaml deleted file mode 100644 index 7148aa18c42..00000000000 --- a/docker/blocks/elastic5/docker-compose.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine - - elasticsearch5: - image: elasticsearch:5 - command: elasticsearch - ports: - - "10200:9200" - - "10300:9300" - - fake-elastic5-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: elasticsearch - FD_PORT: 10200 diff --git a/docker/blocks/elastic6/docker-compose.yaml b/docker/blocks/elastic6/docker-compose.yaml deleted file mode 100644 index dd2439f88e4..00000000000 --- a/docker/blocks/elastic6/docker-compose.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine - - elasticsearch6: - image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4 - command: elasticsearch - ports: - - "11200:9200" - - "11300:9300" - - fake-elastic6-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: elasticsearch6 - FD_PORT: 11200 diff --git a/docker/blocks/graphite/docker-compose.yaml b/docker/blocks/graphite/docker-compose.yaml deleted file mode 100644 index 606e28638f7..00000000000 --- a/docker/blocks/graphite/docker-compose.yaml +++ /dev/null @@ -1,16 +0,0 @@ - graphite09: - build: blocks/graphite - ports: - - "8080:80" - - "2003:2003" - volumes: - - /etc/localtime:/etc/localtime:ro - - /etc/timezone:/etc/timezone:ro - - fake-graphite-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: graphite - FD_PORT: 2003 - diff --git a/docker/blocks/graphite1/docker-compose.yaml b/docker/blocks/graphite1/docker-compose.yaml deleted file mode 100644 index cd10593f423..00000000000 --- a/docker/blocks/graphite1/docker-compose.yaml +++ /dev/null @@ -1,21 +0,0 @@ - graphite: - build: - context: blocks/graphite1 - args: - version: master - ports: - - "8080:80" - - "2003:2003" - - "8125:8125/udp" - - "8126:8126" - volumes: - - /etc/localtime:/etc/localtime:ro - - /etc/timezone:/etc/timezone:ro - - fake-graphite-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: graphite - FD_PORT: 2003 - diff --git a/docker/blocks/graphite11/docker-compose.yaml b/docker/blocks/graphite11/docker-compose.yaml deleted file mode 100644 index 4b0d837a619..00000000000 --- a/docker/blocks/graphite11/docker-compose.yaml +++ /dev/null @@ -1,18 +0,0 @@ - graphite11: - image: graphiteapp/graphite-statsd - ports: - - "8180:80" - - "2103-2104:2003-2004" - - "2123-2124:2023-2024" - - "8225:8125/udp" - - "8226:8126" - - fake-graphite11-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: graphite - FD_PORT: 2103 - FD_GRAPHITE_VERSION: 1.1 - depends_on: - - graphite11 \ No newline at end of file diff --git a/docker/blocks/influxdb/docker-compose.yaml b/docker/blocks/influxdb/docker-compose.yaml deleted file mode 100644 index 3434f5d09b9..00000000000 --- a/docker/blocks/influxdb/docker-compose.yaml +++ /dev/null @@ -1,17 +0,0 @@ - influxdb: - image: influxdb:latest - container_name: influxdb - ports: - - "2004:2004" - - "8083:8083" - - "8086:8086" - volumes: - - ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf - - fake-influxdb-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: influxdb - FD_PORT: 8086 - diff --git a/docker/blocks/jaeger/docker-compose.yaml b/docker/blocks/jaeger/docker-compose.yaml deleted file mode 100644 index 2b57c863425..00000000000 --- a/docker/blocks/jaeger/docker-compose.yaml +++ /dev/null @@ -1,6 +0,0 @@ - jaeger: - image: jaegertracing/all-in-one:latest - ports: - - "127.0.0.1:6831:6831/udp" - - "16686:16686" - diff --git a/docker/blocks/memcached/docker-compose.yaml b/docker/blocks/memcached/docker-compose.yaml deleted file mode 100644 index b3201da0f95..00000000000 --- a/docker/blocks/memcached/docker-compose.yaml +++ /dev/null @@ -1,5 +0,0 @@ - memcached: - image: memcached:latest - ports: - - "11211:11211" - diff --git a/docker/blocks/mssql/docker-compose.yaml b/docker/blocks/mssql/docker-compose.yaml deleted file mode 100644 index a346fb791f7..00000000000 --- a/docker/blocks/mssql/docker-compose.yaml +++ /dev/null @@ -1,19 +0,0 @@ - mssql: - build: - context: blocks/mssql/build - environment: - ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: Password! - MSSQL_PID: Developer - MSSQL_DATABASE: grafana - MSSQL_USER: grafana - MSSQL_PASSWORD: Password! - ports: - - "1433:1433" - - fake-mssql-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: mssql - FD_PORT: 1433 \ No newline at end of file diff --git a/docker/blocks/mssql_tests/docker-compose.yaml b/docker/blocks/mssql_tests/docker-compose.yaml deleted file mode 100644 index 5da6aad82af..00000000000 --- a/docker/blocks/mssql_tests/docker-compose.yaml +++ /dev/null @@ -1,12 +0,0 @@ - mssqltests: - build: - context: blocks/mssql/build - environment: - ACCEPT_EULA: Y - MSSQL_SA_PASSWORD: Password! - MSSQL_PID: Express - MSSQL_DATABASE: grafanatest - MSSQL_USER: grafana - MSSQL_PASSWORD: Password! - ports: - - "1433:1433" \ No newline at end of file diff --git a/docker/blocks/mysql/docker-compose.yaml b/docker/blocks/mysql/docker-compose.yaml deleted file mode 100644 index 381b04a53c8..00000000000 --- a/docker/blocks/mysql/docker-compose.yaml +++ /dev/null @@ -1,18 +0,0 @@ - mysql: - image: mysql:5.6 - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: grafana - MYSQL_USER: grafana - MYSQL_PASSWORD: password - ports: - - "3306:3306" - command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all] - - fake-mysql-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: mysql - FD_PORT: 3306 - diff --git a/docker/blocks/mysql_opendata/docker-compose.yaml b/docker/blocks/mysql_opendata/docker-compose.yaml deleted file mode 100644 index 594eeed284a..00000000000 --- a/docker/blocks/mysql_opendata/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ - mysql_opendata: - build: blocks/mysql_opendata - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: testdata - MYSQL_USER: grafana - MYSQL_PASSWORD: password - ports: - - "3307:3306" diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/docker/blocks/mysql_tests/docker-compose.yaml deleted file mode 100644 index 035a6167017..00000000000 --- a/docker/blocks/mysql_tests/docker-compose.yaml +++ /dev/null @@ -1,11 +0,0 @@ - mysqltests: - build: - context: blocks/mysql_tests - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: grafana_tests - MYSQL_USER: grafana - MYSQL_PASSWORD: password - ports: - - "3306:3306" - tmpfs: /var/lib/mysql:rw diff --git a/docker/blocks/nginx_proxy/docker-compose.yaml b/docker/blocks/nginx_proxy/docker-compose.yaml deleted file mode 100644 index a0ceceb83ac..00000000000 --- a/docker/blocks/nginx_proxy/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# This will proxy all requests for http://localhost:10080/grafana/ to -# http://localhost:3000 (Grafana running locally) -# -# Please note that you'll need to change the root_url in the Grafana configuration: -# root_url = %(protocol)s://%(domain)s:10080/grafana/ - - nginxproxy: - build: blocks/nginx_proxy - network_mode: host diff --git a/docker/blocks/openldap/docker-compose.yaml b/docker/blocks/openldap/docker-compose.yaml deleted file mode 100644 index be06524a57d..00000000000 --- a/docker/blocks/openldap/docker-compose.yaml +++ /dev/null @@ -1,10 +0,0 @@ - openldap: - build: blocks/openldap - environment: - SLAPD_PASSWORD: grafana - SLAPD_DOMAIN: grafana.org - SLAPD_ADDITIONAL_MODULES: memberof - ports: - - "389:389" - - diff --git a/docker/blocks/opentsdb/docker-compose.yaml b/docker/blocks/opentsdb/docker-compose.yaml deleted file mode 100644 index ee064bb107d..00000000000 --- a/docker/blocks/opentsdb/docker-compose.yaml +++ /dev/null @@ -1,11 +0,0 @@ - opentsdb: - image: opower/opentsdb:latest - ports: - - "4242:4242" - - fake-opentsdb-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: opentsdb - diff --git a/docker/blocks/postgres/docker-compose.yaml b/docker/blocks/postgres/docker-compose.yaml deleted file mode 100644 index 27736042f7b..00000000000 --- a/docker/blocks/postgres/docker-compose.yaml +++ /dev/null @@ -1,16 +0,0 @@ - postgrestest: - image: postgres:9.3 - environment: - POSTGRES_USER: grafana - POSTGRES_PASSWORD: password - POSTGRES_DATABASE: grafana - ports: - - "5432:5432" - command: postgres -c log_connections=on -c logging_collector=on -c log_destination=stderr -c log_directory=/var/log/postgresql - - fake-postgres-data: - image: grafana/fake-data-gen - network_mode: bridge - environment: - FD_DATASOURCE: postgres - FD_PORT: 5432 diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/docker/blocks/postgres_tests/docker-compose.yaml deleted file mode 100644 index f5ce0a5a3d3..00000000000 --- a/docker/blocks/postgres_tests/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ - postgrestest: - build: - context: blocks/postgres_tests - environment: - POSTGRES_USER: grafanatest - POSTGRES_PASSWORD: grafanatest - ports: - - "5432:5432" - tmpfs: /var/lib/postgresql/data:rw \ No newline at end of file diff --git a/docker/blocks/prometheus/docker-compose.yaml b/docker/blocks/prometheus/docker-compose.yaml deleted file mode 100644 index 3c304cc74ad..00000000000 --- a/docker/blocks/prometheus/docker-compose.yaml +++ /dev/null @@ -1,31 +0,0 @@ - prometheus: - build: blocks/prometheus - network_mode: host - ports: - - "9090:9090" - - node_exporter: - image: prom/node-exporter - network_mode: host - ports: - - "9100:9100" - - fake-prometheus-data: - image: grafana/fake-data-gen - network_mode: host - ports: - - "9091:9091" - environment: - FD_DATASOURCE: prom - - alertmanager: - image: quay.io/prometheus/alertmanager - network_mode: host - ports: - - "9093:9093" - - prometheus-random-data: - build: blocks/prometheus_random_data - network_mode: host - ports: - - "8081:8080" diff --git a/docker/blocks/prometheus2/docker-compose.yaml b/docker/blocks/prometheus2/docker-compose.yaml deleted file mode 100644 index 589df868084..00000000000 --- a/docker/blocks/prometheus2/docker-compose.yaml +++ /dev/null @@ -1,31 +0,0 @@ - prometheus: - build: blocks/prometheus2 - network_mode: host - ports: - - "9090:9090" - - node_exporter: - image: prom/node-exporter - network_mode: host - ports: - - "9100:9100" - - fake-prometheus-data: - image: grafana/fake-data-gen - network_mode: host - ports: - - "9091:9091" - environment: - FD_DATASOURCE: prom - - alertmanager: - image: quay.io/prometheus/alertmanager - network_mode: host - ports: - - "9093:9093" - - prometheus-random-data: - build: blocks/prometheus_random_data - network_mode: host - ports: - - "8081:8080" diff --git a/docker/blocks/prometheus_mac/docker-compose.yaml b/docker/blocks/prometheus_mac/docker-compose.yaml deleted file mode 100644 index ef53b07418a..00000000000 --- a/docker/blocks/prometheus_mac/docker-compose.yaml +++ /dev/null @@ -1,26 +0,0 @@ - prometheus: - build: blocks/prometheus_mac - ports: - - "9090:9090" - - node_exporter: - image: prom/node-exporter - ports: - - "9100:9100" - - fake-prometheus-data: - image: grafana/fake-data-gen - ports: - - "9091:9091" - environment: - FD_DATASOURCE: prom - - alertmanager: - image: quay.io/prometheus/alertmanager - ports: - - "9093:9093" - - prometheus-random-data: - build: blocks/prometheus_random_data - ports: - - "8081:8080" diff --git a/docker/blocks/smtp/docker-compose.yaml b/docker/blocks/smtp/docker-compose.yaml deleted file mode 100644 index 85d598b6167..00000000000 --- a/docker/blocks/smtp/docker-compose.yaml +++ /dev/null @@ -1,4 +0,0 @@ - snmpd: - image: namshi/smtp - ports: - - "25:25" From 8e69d7731a445683c42a5e2ca29c585b6f90a587 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 17 Sep 2018 11:15:46 +0200 Subject: [PATCH 079/127] moves benchmark script to devenv ref #11433 --- {scripts => devenv}/benchmarks/ab/ab_test.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {scripts => devenv}/benchmarks/ab/ab_test.sh (100%) diff --git a/scripts/benchmarks/ab/ab_test.sh b/devenv/benchmarks/ab/ab_test.sh similarity index 100% rename from scripts/benchmarks/ab/ab_test.sh rename to devenv/benchmarks/ab/ab_test.sh From d84a0ec3e75a937e86967e9ebc4b0a622282bd3e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 17 Sep 2018 11:50:22 +0200 Subject: [PATCH 080/127] removes testdata from getting started --- docs/sources/guides/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/guides/getting_started.md b/docs/sources/guides/getting_started.md index ca9793a361a..27957990265 100644 --- a/docs/sources/guides/getting_started.md +++ b/docs/sources/guides/getting_started.md @@ -69,7 +69,7 @@ The image above shows you the top header for a Dashboard. ## Dashboards, Panels, the building blocks of Grafana... -Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently Graphite, Prometheus, Elasticsearch, InfluxDB, OpenTSDB, MySQL, PostgreSQL, Microsoft SQL Server, AWS Cloudwatch, and TestData). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. +Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a grid. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently Graphite, Prometheus, Elasticsearch, InfluxDB, OpenTSDB, MySQL, PostgreSQL, Microsoft SQL Server and AWS Cloudwatch). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. From 8dbba467f158703948103c7d9c0a5dd4cbb04e0e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 17 Sep 2018 17:29:11 +0200 Subject: [PATCH 081/127] moves /tests to /pkg/plugins ref #11433 --- pkg/plugins/dashboard_importer_test.go | 4 ++-- pkg/plugins/dashboards_test.go | 2 +- pkg/plugins/plugins_test.go | 2 +- {tests => pkg/plugins/testdata}/datasource-test/module.js | 0 {tests => pkg/plugins/testdata}/datasource-test/plugin.json | 0 .../plugins/testdata}/test-app/dashboards/connections.json | 0 .../testdata}/test-app/dashboards/connections_result.json | 0 .../plugins/testdata}/test-app/dashboards/memory.json | 0 {tests => pkg/plugins/testdata}/test-app/plugin.json | 0 9 files changed, 4 insertions(+), 4 deletions(-) rename {tests => pkg/plugins/testdata}/datasource-test/module.js (100%) rename {tests => pkg/plugins/testdata}/datasource-test/plugin.json (100%) rename {tests => pkg/plugins/testdata}/test-app/dashboards/connections.json (100%) rename {tests => pkg/plugins/testdata}/test-app/dashboards/connections_result.json (100%) rename {tests => pkg/plugins/testdata}/test-app/dashboards/memory.json (100%) rename {tests => pkg/plugins/testdata}/test-app/plugin.json (100%) diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 6f31b49f99d..ca8dfcd515c 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -35,7 +35,7 @@ func TestDashboardImport(t *testing.T) { So(cmd.Result, ShouldNotBeNil) resultStr, _ := mock.SavedDashboards[0].Dashboard.Data.EncodePretty() - expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json") + expectedBytes, _ := ioutil.ReadFile("testdata/test-app/dashboards/connections_result.json") expectedJson, _ := simplejson.NewJson(expectedBytes) expectedStr, _ := expectedJson.EncodePretty() @@ -89,7 +89,7 @@ func pluginScenario(desc string, t *testing.T, fn func()) { Convey("Given a plugin", t, func() { setting.Raw = ini.Empty() sec, _ := setting.Raw.NewSection("plugin.test-app") - sec.NewKey("path", "../../tests/test-app") + sec.NewKey("path", "testdata/test-app") pm := &PluginManager{} err := pm.Init() diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index c422a1431c0..6fc6ace0e00 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -16,7 +16,7 @@ func TestPluginDashboards(t *testing.T) { Convey("When asking plugin dashboard info", t, func() { setting.Raw = ini.Empty() sec, _ := setting.Raw.NewSection("plugin.test-app") - sec.NewKey("path", "../../tests/test-app") + sec.NewKey("path", "testdata/test-app") pm := &PluginManager{} err := pm.Init() diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index fa68ae4389d..d16e6abb4c7 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -30,7 +30,7 @@ func TestPluginScans(t *testing.T) { Convey("When reading app plugin definition", t, func() { setting.Raw = ini.Empty() sec, _ := setting.Raw.NewSection("plugin.nginx-app") - sec.NewKey("path", "../../tests/test-app") + sec.NewKey("path", "testdata/test-app") pm := &PluginManager{} err := pm.Init() diff --git a/tests/datasource-test/module.js b/pkg/plugins/testdata/datasource-test/module.js similarity index 100% rename from tests/datasource-test/module.js rename to pkg/plugins/testdata/datasource-test/module.js diff --git a/tests/datasource-test/plugin.json b/pkg/plugins/testdata/datasource-test/plugin.json similarity index 100% rename from tests/datasource-test/plugin.json rename to pkg/plugins/testdata/datasource-test/plugin.json diff --git a/tests/test-app/dashboards/connections.json b/pkg/plugins/testdata/test-app/dashboards/connections.json similarity index 100% rename from tests/test-app/dashboards/connections.json rename to pkg/plugins/testdata/test-app/dashboards/connections.json diff --git a/tests/test-app/dashboards/connections_result.json b/pkg/plugins/testdata/test-app/dashboards/connections_result.json similarity index 100% rename from tests/test-app/dashboards/connections_result.json rename to pkg/plugins/testdata/test-app/dashboards/connections_result.json diff --git a/tests/test-app/dashboards/memory.json b/pkg/plugins/testdata/test-app/dashboards/memory.json similarity index 100% rename from tests/test-app/dashboards/memory.json rename to pkg/plugins/testdata/test-app/dashboards/memory.json diff --git a/tests/test-app/plugin.json b/pkg/plugins/testdata/test-app/plugin.json similarity index 100% rename from tests/test-app/plugin.json rename to pkg/plugins/testdata/test-app/plugin.json From f8ca55bb1f444107baad5f16bed9c971ce17cb47 Mon Sep 17 00:00:00 2001 From: Nick Triller Date: Mon, 17 Sep 2018 17:56:52 +0200 Subject: [PATCH 082/127] Fix setting test --- pkg/setting/setting_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 9b19b6c6bfa..6524073e4da 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -130,7 +130,7 @@ func TestLoadingSettings(t *testing.T) { cfg := NewCfg() cfg.Load(&CommandLineArgs{ HomePath: "../../", - Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"), + Config: filepath.Join(HomePath, "pkg/setting/testdata/override_windows.ini"), Args: []string{`cfg:paths.data=c:\tmp\data`}, }) From 1be26ad362a95074a85fdea3b1def47eff12d91d Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 18 Sep 2018 10:47:01 +0200 Subject: [PATCH 083/127] disable codecov --- scripts/circle-test-backend.sh | 12 +----------- scripts/circle-test-frontend.sh | 7 +------ 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/scripts/circle-test-backend.sh b/scripts/circle-test-backend.sh index 4740ef99f1a..959a5812d81 100755 --- a/scripts/circle-test-backend.sh +++ b/scripts/circle-test-backend.sh @@ -17,17 +17,7 @@ echo "building backend with install to cache pkgs" exit_if_fail time go install ./pkg/cmd/grafana-server echo "running go test" - set -e -echo "" > coverage.txt - time for d in $(go list ./pkg/...); do - exit_if_fail go test -coverprofile=profile.out -covermode=atomic $d - if [ -f profile.out ]; then - cat profile.out >> coverage.txt - rm profile.out - fi + exit_if_fail go test -covermode=atomic $d done - -echo "Publishing go code coverage" -bash <(curl -s https://codecov.io/bash) -cF go diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh index 796af82e7d8..3af199d3ff2 100755 --- a/scripts/circle-test-frontend.sh +++ b/scripts/circle-test-frontend.sh @@ -10,9 +10,4 @@ function exit_if_fail { fi } -exit_if_fail npm run test:coverage - -# publish code coverage -echo "Publishing javascript code coverage" -bash <(curl -s https://codecov.io/bash) -cF javascript -rm -rf coverage +exit_if_fail npm run test From 63ed02e626939eb4ff342d324c1863f1ad314fb3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 18 Sep 2018 11:44:31 +0200 Subject: [PATCH 084/127] removes codedov refs --- Gruntfile.js | 1 - codecov.yml | 11 ----------- package.json | 1 - scripts/grunt/options/exec.js | 7 +------ 4 files changed, 1 insertion(+), 19 deletions(-) delete mode 100644 codecov.yml diff --git a/Gruntfile.js b/Gruntfile.js index 8a71fb44148..2d5990b5f58 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -25,7 +25,6 @@ module.exports = function (grunt) { } } - config.coverage = grunt.option('coverage'); config.phjs = grunt.option('phjsToRelease'); config.pkg.version = grunt.option('pkgVer') || config.pkg.version; diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index b2a839365ac..00000000000 --- a/codecov.yml +++ /dev/null @@ -1,11 +0,0 @@ -coverage: - precision: 2 - round: down - range: "50...100" - - status: - project: yes - patch: yes - changes: no - -comment: off diff --git a/package.json b/package.json index f4846f9fc51..1e7ed02c87b 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,6 @@ "watch": "webpack --progress --colors --watch --mode development --config scripts/webpack/webpack.dev.js", "build": "grunt build", "test": "grunt test", - "test:coverage": "grunt test --coverage=true", "lint": "tslint -c tslint.json --project tsconfig.json", "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index 087439f7ea9..2634f2b546b 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -1,14 +1,9 @@ module.exports = function(config, grunt) { 'use strict'; - var coverage = ''; - if (config.coverage) { - coverage = '--coverage --maxWorkers 2'; - } - return { tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json', - jest: 'node ./node_modules/jest-cli/bin/jest.js ' + coverage, + jest: 'node ./node_modules/jest-cli/bin/jest.js', webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js', }; }; From 0f8b9b8ff9e01d7c4cbe705101ee87d21082d4bd Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 18 Sep 2018 12:07:53 +0200 Subject: [PATCH 085/127] set maxworkers 2 for frontend tests --- scripts/circle-test-frontend.sh | 1 + scripts/grunt/options/exec.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/circle-test-frontend.sh b/scripts/circle-test-frontend.sh index 3af199d3ff2..68c044b395c 100755 --- a/scripts/circle-test-frontend.sh +++ b/scripts/circle-test-frontend.sh @@ -11,3 +11,4 @@ function exit_if_fail { } exit_if_fail npm run test +exit_if_fail npm run build diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js index 2634f2b546b..92e530cd5fd 100644 --- a/scripts/grunt/options/exec.js +++ b/scripts/grunt/options/exec.js @@ -3,7 +3,7 @@ module.exports = function(config, grunt) { return { tslint: 'node ./node_modules/tslint/lib/tslintCli.js -c tslint.json --project ./tsconfig.json', - jest: 'node ./node_modules/jest-cli/bin/jest.js', + jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2', webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js', }; }; From bb5aaa2dce70cb78487bc498ffe6de07cca93685 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 18 Sep 2018 23:18:39 +0200 Subject: [PATCH 086/127] pkg/services/sqlstore/alert_notification.go: Simplify err check $ gometalinter --vendor --disable=all --enable=megacheck --deadline=10m ./... pkg/services/sqlstore/alert_notification.go:242:3:warning: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) (megacheck) --- pkg/services/sqlstore/alert_notification.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 19ed960638e..31867910ddb 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -239,11 +239,8 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou Success: cmd.Success, } - if _, err := sess.Insert(journalEntry); err != nil { - return err - } - - return nil + _, err := sess.Insert(journalEntry) + return err }) } From f19fd1a9b0e662bdaf9f7dafd967c16fd91a8196 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 18 Sep 2018 23:27:06 +0200 Subject: [PATCH 087/127] pkg/plugins/dashboards_updater.go: Simplify err check $ gometalinter --vendor --disable=all --enable=megacheck --deadline=10m ./... pkg/plugins/dashboards_updater.go:51:2:warning: 'if err != nil { return err }; return nil' can be simplified to 'return err' (S1013) (megacheck) --- pkg/plugins/dashboards_updater.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/plugins/dashboards_updater.go b/pkg/plugins/dashboards_updater.go index ebe11ed32d4..616d4541bec 100644 --- a/pkg/plugins/dashboards_updater.go +++ b/pkg/plugins/dashboards_updater.go @@ -48,11 +48,7 @@ func autoUpdateAppDashboard(pluginDashInfo *PluginDashboardInfoDTO, orgId int64) Path: pluginDashInfo.Path, } - if err := bus.Dispatch(&updateCmd); err != nil { - return err - } - - return nil + return bus.Dispatch(&updateCmd) } func syncPluginDashboards(pluginDef *PluginBase, orgId int64) { From 13a1d0a026c4cadd61dac8847776e51493bc4255 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Tue, 18 Sep 2018 23:36:02 +0200 Subject: [PATCH 088/127] pkg/tsdb/elasticsearch/client/client.go: use time.Since instead of time.Now().Sub $ gometalinter --vendor --disable=all --enable=megacheck --deadline=10m ./... pkg/tsdb/elasticsearch/client/client.go:147:13:warning: should use time.Since instead of time.Now().Sub (S1012) (megacheck) pkg/tsdb/elasticsearch/client/client.go:190:14:warning: should use time.Since instead of time.Now().Sub (S1012) (megacheck) pkg/tsdb/elasticsearch/client/client.go:218:13:warning: should use time.Since instead of time.Now().Sub (S1012) (megacheck) --- pkg/tsdb/elasticsearch/client/client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index 78973b3faa6..4ebe0db8f89 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -144,7 +144,7 @@ func (c *baseClientImpl) encodeBatchRequests(requests []*multiRequest) ([]byte, payload.WriteString(body + "\n") } - elapsed := time.Now().Sub(start) + elapsed := time.Since(start) clientLog.Debug("Encoded batch requests to json", "took", elapsed) return payload.Bytes(), nil @@ -187,7 +187,7 @@ func (c *baseClientImpl) executeRequest(method, uriPath string, body []byte) (*h start := time.Now() defer func() { - elapsed := time.Now().Sub(start) + elapsed := time.Since(start) clientLog.Debug("Executed request", "took", elapsed) }() return ctxhttp.Do(c.ctx, httpClient, req) @@ -215,7 +215,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch return nil, err } - elapsed := time.Now().Sub(start) + elapsed := time.Since(start) clientLog.Debug("Decoded multisearch json response", "took", elapsed) msr.Status = res.StatusCode From e07513bd65aa337da6357ffda89ee89d49cbe3d7 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 19 Sep 2018 10:58:23 +0200 Subject: [PATCH 089/127] Don't use unnest in queries for redshift compatibility --- .../plugins/datasource/postgres/meta_query.ts | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index 0c54cc26cad..7339a3e3882 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -25,7 +25,7 @@ export class PostgresMetaQuery { findMetricTable() { // query that returns first table found that has a timestamp(tz) column and a float column - const query = ` + let query = ` SELECT quote_ident(table_name) as table_name, ( SELECT @@ -47,11 +47,9 @@ SELECT ORDER BY ordinal_position LIMIT 1 ) AS value_column FROM information_schema.tables t -WHERE - table_schema IN ( - SELECT CASE WHEN trim(unnest) = '"$user"' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting('search_path'),',')) - ) AND +WHERE `; + query += this.buildSchemaConstraint(); + query += ` AND EXISTS ( SELECT 1 FROM information_schema.columns c @@ -76,8 +74,14 @@ LIMIT 1 buildSchemaConstraint() { const query = ` table_schema IN ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) + SELECT + CASE WHEN trim(s[i]) = '"$user"' THEN user ELSE trim(s[i]) END + FROM + generate_series( + array_lower(string_to_array(current_setting('search_path'),','),1), + array_upper(string_to_array(current_setting('search_path'),','),1) + ) as i, + string_to_array(current_setting('search_path'),',') s )`; return query; } @@ -92,11 +96,7 @@ table_schema IN ( query += ' AND table_name = ' + this.quoteIdentAsLiteral(parts[1]); return query; } else { - query = ` -table_schema IN ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) -)`; + query = this.buildSchemaConstraint(); query += ' AND table_name = ' + this.quoteIdentAsLiteral(table); return query; @@ -149,18 +149,8 @@ table_schema IN ( } buildDatatypeQuery(column: string) { - let query = ` -SELECT udt_name -FROM information_schema.columns -WHERE - table_schema IN ( - SELECT schema FROM ( - SELECT CASE WHEN trim(unnest) = \'"$user"\' THEN user ELSE trim(unnest) END as schema - FROM unnest(string_to_array(current_setting(\'search_path\'),\',\')) - ) s - WHERE EXISTS (SELECT 1 FROM information_schema.schemata WHERE schema_name = s.schema) - ) -`; + let query = 'SELECT udt_name FROM information_schema.columns WHERE '; + query += this.buildSchemaConstraint(); query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); return query; From 6aac8610ebe44783bbd38182293d42b2715a6078 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 19 Sep 2018 11:45:55 +0200 Subject: [PATCH 090/127] Explore: Fix click to filter for recording rule expressions - recording rule names contain ':' - include this in the pattern for metric names --- public/app/plugins/datasource/prometheus/add_label_to_query.ts | 2 +- .../datasource/prometheus/specs/add_label_to_query.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/add_label_to_query.ts b/public/app/plugins/datasource/prometheus/add_label_to_query.ts index 9ea01ed755a..0a1c3ea1878 100644 --- a/public/app/plugins/datasource/prometheus/add_label_to_query.ts +++ b/public/app/plugins/datasource/prometheus/add_label_to_query.ts @@ -15,7 +15,7 @@ const builtInWords = [ .join('|') .split('|'); -const metricNameRegexp = /([A-Za-z]\w*)\b(?![\(\]{=!",])/g; +const metricNameRegexp = /([A-Za-z:][\w:]*)\b(?![\(\]{=!",])/g; const selectorRegexp = /{([^{]*)}/g; // addLabelToQuery('foo', 'bar', 'baz') => 'foo{bar="baz"}' diff --git a/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts b/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts index 9c654e8e467..e7c114f7d5f 100644 --- a/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/add_label_to_query.test.ts @@ -28,6 +28,7 @@ describe('addLabelToQuery()', () => { expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( 'foo{bar="baz",instance="my-host.com:9100"}' ); + expect(addLabelToQuery('foo:metric:rate1m', 'bar', 'baz')).toBe('foo:metric:rate1m{bar="baz"}'); expect(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz')).toBe('foo{bar="baz",list="a,b,c"}'); }); From f25538744d850ad29798587d3f10ff6546eecc40 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 19 Sep 2018 12:01:02 +0200 Subject: [PATCH 091/127] Explore: Fix label suggestions for recording rules - parsing of recording rules failed for label suggestor - added ':' to parsing routine --- public/app/containers/Explore/utils/prometheus.test.ts | 3 +++ public/app/containers/Explore/utils/prometheus.ts | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/public/app/containers/Explore/utils/prometheus.test.ts b/public/app/containers/Explore/utils/prometheus.test.ts index d12d28c6bc9..4e84deaa7e8 100644 --- a/public/app/containers/Explore/utils/prometheus.test.ts +++ b/public/app/containers/Explore/utils/prometheus.test.ts @@ -57,5 +57,8 @@ describe('parseSelector()', () => { parsed = parseSelector('baz{foo="bar"}', 12); expect(parsed.selector).toBe('{__name__="baz",foo="bar"}'); + + parsed = parseSelector('bar:metric:1m{}', 14); + expect(parsed.selector).toBe('{__name__="bar:metric:1m"}'); }); }); diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/containers/Explore/utils/prometheus.ts index 19129976282..8c41b94d684 100644 --- a/public/app/containers/Explore/utils/prometheus.ts +++ b/public/app/containers/Explore/utils/prometheus.ts @@ -32,7 +32,7 @@ const labelRegexp = /\b\w+="[^"\n]*?"/g; export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics - if (query.match(/^\w+$/)) { + if (query.match(/^[A-Za-z:][\w:]*$/)) { return { selector: `{__name__="${query}"}`, labelKeys: ['__name__'], @@ -76,7 +76,7 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any // Add metric if there is one before the selector const metricPrefix = query.slice(0, prefixOpen); - const metricMatch = metricPrefix.match(/\w+$/); + const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/); if (metricMatch) { labels['__name__'] = `"${metricMatch[0]}"`; } From 978284bc3ddb3c8b0fdad2d7715cee2c445c439d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 19 Sep 2018 14:35:27 +0200 Subject: [PATCH 092/127] devenv: re-add missing docker-compose files --- .gitignore | 4 +-- .../blocks/apache_proxy/docker-compose.yaml | 9 ++++++ .../blocks/collectd/docker-compose.yaml | 11 +++++++ .../docker/blocks/elastic/docker-compose.yaml | 15 +++++++++ .../blocks/elastic1/docker-compose.yaml | 8 +++++ .../blocks/elastic5/docker-compose.yaml | 15 +++++++++ .../blocks/elastic6/docker-compose.yaml | 15 +++++++++ .../blocks/graphite/docker-compose.yaml | 16 ++++++++++ .../blocks/graphite1/docker-compose.yaml | 21 +++++++++++++ .../blocks/graphite11/docker-compose.yaml | 18 +++++++++++ .../blocks/influxdb/docker-compose.yaml | 17 ++++++++++ .../docker/blocks/jaeger/docker-compose.yaml | 6 ++++ .../blocks/memcached/docker-compose.yaml | 5 +++ .../docker/blocks/mssql/docker-compose.yaml | 19 ++++++++++++ .../blocks/mssql_tests/docker-compose.yaml | 12 +++++++ .../docker/blocks/mysql/docker-compose.yaml | 18 +++++++++++ .../blocks/mysql_opendata/docker-compose.yaml | 9 ++++++ .../blocks/mysql_tests/docker-compose.yaml | 11 +++++++ .../blocks/nginx_proxy/docker-compose.yaml | 9 ++++++ .../blocks/openldap/docker-compose.yaml | 10 ++++++ .../blocks/opentsdb/docker-compose.yaml | 11 +++++++ .../blocks/postgres/docker-compose.yaml | 16 ++++++++++ .../blocks/postgres_tests/docker-compose.yaml | 9 ++++++ .../blocks/prometheus/docker-compose.yaml | 31 +++++++++++++++++++ .../blocks/prometheus2/docker-compose.yaml | 31 +++++++++++++++++++ .../blocks/prometheus_mac/docker-compose.yaml | 26 ++++++++++++++++ devenv/docker/blocks/smtp/docker-compose.yaml | 4 +++ 27 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 devenv/docker/blocks/apache_proxy/docker-compose.yaml create mode 100644 devenv/docker/blocks/collectd/docker-compose.yaml create mode 100644 devenv/docker/blocks/elastic/docker-compose.yaml create mode 100644 devenv/docker/blocks/elastic1/docker-compose.yaml create mode 100644 devenv/docker/blocks/elastic5/docker-compose.yaml create mode 100644 devenv/docker/blocks/elastic6/docker-compose.yaml create mode 100644 devenv/docker/blocks/graphite/docker-compose.yaml create mode 100644 devenv/docker/blocks/graphite1/docker-compose.yaml create mode 100644 devenv/docker/blocks/graphite11/docker-compose.yaml create mode 100644 devenv/docker/blocks/influxdb/docker-compose.yaml create mode 100644 devenv/docker/blocks/jaeger/docker-compose.yaml create mode 100644 devenv/docker/blocks/memcached/docker-compose.yaml create mode 100644 devenv/docker/blocks/mssql/docker-compose.yaml create mode 100644 devenv/docker/blocks/mssql_tests/docker-compose.yaml create mode 100644 devenv/docker/blocks/mysql/docker-compose.yaml create mode 100644 devenv/docker/blocks/mysql_opendata/docker-compose.yaml create mode 100644 devenv/docker/blocks/mysql_tests/docker-compose.yaml create mode 100644 devenv/docker/blocks/nginx_proxy/docker-compose.yaml create mode 100644 devenv/docker/blocks/openldap/docker-compose.yaml create mode 100644 devenv/docker/blocks/opentsdb/docker-compose.yaml create mode 100644 devenv/docker/blocks/postgres/docker-compose.yaml create mode 100644 devenv/docker/blocks/postgres_tests/docker-compose.yaml create mode 100644 devenv/docker/blocks/prometheus/docker-compose.yaml create mode 100644 devenv/docker/blocks/prometheus2/docker-compose.yaml create mode 100644 devenv/docker/blocks/prometheus_mac/docker-compose.yaml create mode 100644 devenv/docker/blocks/smtp/docker-compose.yaml diff --git a/.gitignore b/.gitignore index bf97948d178..78b8d075ef6 100644 --- a/.gitignore +++ b/.gitignore @@ -40,8 +40,8 @@ public/css/*.min.css conf/custom.ini fig.yml -docker-compose.yml -docker-compose.yaml +devenv/docker-compose.yml +devenv/docker-compose.yaml /conf/provisioning/**/custom.yaml /conf/provisioning/**/dev.yaml /conf/ldap_dev.toml diff --git a/devenv/docker/blocks/apache_proxy/docker-compose.yaml b/devenv/docker/blocks/apache_proxy/docker-compose.yaml new file mode 100644 index 00000000000..86d4befadd6 --- /dev/null +++ b/devenv/docker/blocks/apache_proxy/docker-compose.yaml @@ -0,0 +1,9 @@ +# This will proxy all requests for http://localhost:10081/grafana/ to +# http://localhost:3000 (Grafana running locally) +# +# Please note that you'll need to change the root_url in the Grafana configuration: +# root_url = %(protocol)s://%(domain)s:10081/grafana/ + + apacheproxy: + build: blocks/apache_proxy + network_mode: host diff --git a/devenv/docker/blocks/collectd/docker-compose.yaml b/devenv/docker/blocks/collectd/docker-compose.yaml new file mode 100644 index 00000000000..c95827f7928 --- /dev/null +++ b/devenv/docker/blocks/collectd/docker-compose.yaml @@ -0,0 +1,11 @@ + collectd: + build: blocks/collectd + environment: + HOST_NAME: myserver + GRAPHITE_HOST: graphite + GRAPHITE_PORT: 2003 + GRAPHITE_PREFIX: collectd. + REPORT_BY_CPU: 'false' + COLLECT_INTERVAL: 10 + links: + - graphite diff --git a/devenv/docker/blocks/elastic/docker-compose.yaml b/devenv/docker/blocks/elastic/docker-compose.yaml new file mode 100644 index 00000000000..2eba60f38be --- /dev/null +++ b/devenv/docker/blocks/elastic/docker-compose.yaml @@ -0,0 +1,15 @@ + elasticsearch: + image: elasticsearch:2.4.1 + command: elasticsearch -Des.network.host=0.0.0.0 + ports: + - "9200:9200" + - "9300:9300" + volumes: + - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + + fake-elastic-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: elasticsearch + FD_PORT: 9200 diff --git a/devenv/docker/blocks/elastic1/docker-compose.yaml b/devenv/docker/blocks/elastic1/docker-compose.yaml new file mode 100644 index 00000000000..518ae76e6ee --- /dev/null +++ b/devenv/docker/blocks/elastic1/docker-compose.yaml @@ -0,0 +1,8 @@ + elasticsearch1: + image: elasticsearch:1.7.6 + command: elasticsearch -Des.network.host=0.0.0.0 + ports: + - "11200:9200" + - "11300:9300" + volumes: + - ./blocks/elastic/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml diff --git a/devenv/docker/blocks/elastic5/docker-compose.yaml b/devenv/docker/blocks/elastic5/docker-compose.yaml new file mode 100644 index 00000000000..7148aa18c42 --- /dev/null +++ b/devenv/docker/blocks/elastic5/docker-compose.yaml @@ -0,0 +1,15 @@ +# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine + + elasticsearch5: + image: elasticsearch:5 + command: elasticsearch + ports: + - "10200:9200" + - "10300:9300" + + fake-elastic5-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: elasticsearch + FD_PORT: 10200 diff --git a/devenv/docker/blocks/elastic6/docker-compose.yaml b/devenv/docker/blocks/elastic6/docker-compose.yaml new file mode 100644 index 00000000000..dd2439f88e4 --- /dev/null +++ b/devenv/docker/blocks/elastic6/docker-compose.yaml @@ -0,0 +1,15 @@ +# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine + + elasticsearch6: + image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4 + command: elasticsearch + ports: + - "11200:9200" + - "11300:9300" + + fake-elastic6-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: elasticsearch6 + FD_PORT: 11200 diff --git a/devenv/docker/blocks/graphite/docker-compose.yaml b/devenv/docker/blocks/graphite/docker-compose.yaml new file mode 100644 index 00000000000..606e28638f7 --- /dev/null +++ b/devenv/docker/blocks/graphite/docker-compose.yaml @@ -0,0 +1,16 @@ + graphite09: + build: blocks/graphite + ports: + - "8080:80" + - "2003:2003" + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + + fake-graphite-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: graphite + FD_PORT: 2003 + diff --git a/devenv/docker/blocks/graphite1/docker-compose.yaml b/devenv/docker/blocks/graphite1/docker-compose.yaml new file mode 100644 index 00000000000..cd10593f423 --- /dev/null +++ b/devenv/docker/blocks/graphite1/docker-compose.yaml @@ -0,0 +1,21 @@ + graphite: + build: + context: blocks/graphite1 + args: + version: master + ports: + - "8080:80" + - "2003:2003" + - "8125:8125/udp" + - "8126:8126" + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + + fake-graphite-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: graphite + FD_PORT: 2003 + diff --git a/devenv/docker/blocks/graphite11/docker-compose.yaml b/devenv/docker/blocks/graphite11/docker-compose.yaml new file mode 100644 index 00000000000..4b0d837a619 --- /dev/null +++ b/devenv/docker/blocks/graphite11/docker-compose.yaml @@ -0,0 +1,18 @@ + graphite11: + image: graphiteapp/graphite-statsd + ports: + - "8180:80" + - "2103-2104:2003-2004" + - "2123-2124:2023-2024" + - "8225:8125/udp" + - "8226:8126" + + fake-graphite11-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: graphite + FD_PORT: 2103 + FD_GRAPHITE_VERSION: 1.1 + depends_on: + - graphite11 \ No newline at end of file diff --git a/devenv/docker/blocks/influxdb/docker-compose.yaml b/devenv/docker/blocks/influxdb/docker-compose.yaml new file mode 100644 index 00000000000..3434f5d09b9 --- /dev/null +++ b/devenv/docker/blocks/influxdb/docker-compose.yaml @@ -0,0 +1,17 @@ + influxdb: + image: influxdb:latest + container_name: influxdb + ports: + - "2004:2004" + - "8083:8083" + - "8086:8086" + volumes: + - ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf + + fake-influxdb-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: influxdb + FD_PORT: 8086 + diff --git a/devenv/docker/blocks/jaeger/docker-compose.yaml b/devenv/docker/blocks/jaeger/docker-compose.yaml new file mode 100644 index 00000000000..2b57c863425 --- /dev/null +++ b/devenv/docker/blocks/jaeger/docker-compose.yaml @@ -0,0 +1,6 @@ + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - "127.0.0.1:6831:6831/udp" + - "16686:16686" + diff --git a/devenv/docker/blocks/memcached/docker-compose.yaml b/devenv/docker/blocks/memcached/docker-compose.yaml new file mode 100644 index 00000000000..b3201da0f95 --- /dev/null +++ b/devenv/docker/blocks/memcached/docker-compose.yaml @@ -0,0 +1,5 @@ + memcached: + image: memcached:latest + ports: + - "11211:11211" + diff --git a/devenv/docker/blocks/mssql/docker-compose.yaml b/devenv/docker/blocks/mssql/docker-compose.yaml new file mode 100644 index 00000000000..a346fb791f7 --- /dev/null +++ b/devenv/docker/blocks/mssql/docker-compose.yaml @@ -0,0 +1,19 @@ + mssql: + build: + context: blocks/mssql/build + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Password! + MSSQL_PID: Developer + MSSQL_DATABASE: grafana + MSSQL_USER: grafana + MSSQL_PASSWORD: Password! + ports: + - "1433:1433" + + fake-mssql-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: mssql + FD_PORT: 1433 \ No newline at end of file diff --git a/devenv/docker/blocks/mssql_tests/docker-compose.yaml b/devenv/docker/blocks/mssql_tests/docker-compose.yaml new file mode 100644 index 00000000000..5da6aad82af --- /dev/null +++ b/devenv/docker/blocks/mssql_tests/docker-compose.yaml @@ -0,0 +1,12 @@ + mssqltests: + build: + context: blocks/mssql/build + environment: + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Password! + MSSQL_PID: Express + MSSQL_DATABASE: grafanatest + MSSQL_USER: grafana + MSSQL_PASSWORD: Password! + ports: + - "1433:1433" \ No newline at end of file diff --git a/devenv/docker/blocks/mysql/docker-compose.yaml b/devenv/docker/blocks/mysql/docker-compose.yaml new file mode 100644 index 00000000000..381b04a53c8 --- /dev/null +++ b/devenv/docker/blocks/mysql/docker-compose.yaml @@ -0,0 +1,18 @@ + mysql: + image: mysql:5.6 + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: grafana + MYSQL_USER: grafana + MYSQL_PASSWORD: password + ports: + - "3306:3306" + command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all] + + fake-mysql-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: mysql + FD_PORT: 3306 + diff --git a/devenv/docker/blocks/mysql_opendata/docker-compose.yaml b/devenv/docker/blocks/mysql_opendata/docker-compose.yaml new file mode 100644 index 00000000000..594eeed284a --- /dev/null +++ b/devenv/docker/blocks/mysql_opendata/docker-compose.yaml @@ -0,0 +1,9 @@ + mysql_opendata: + build: blocks/mysql_opendata + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: testdata + MYSQL_USER: grafana + MYSQL_PASSWORD: password + ports: + - "3307:3306" diff --git a/devenv/docker/blocks/mysql_tests/docker-compose.yaml b/devenv/docker/blocks/mysql_tests/docker-compose.yaml new file mode 100644 index 00000000000..035a6167017 --- /dev/null +++ b/devenv/docker/blocks/mysql_tests/docker-compose.yaml @@ -0,0 +1,11 @@ + mysqltests: + build: + context: blocks/mysql_tests + environment: + MYSQL_ROOT_PASSWORD: rootpass + MYSQL_DATABASE: grafana_tests + MYSQL_USER: grafana + MYSQL_PASSWORD: password + ports: + - "3306:3306" + tmpfs: /var/lib/mysql:rw diff --git a/devenv/docker/blocks/nginx_proxy/docker-compose.yaml b/devenv/docker/blocks/nginx_proxy/docker-compose.yaml new file mode 100644 index 00000000000..a0ceceb83ac --- /dev/null +++ b/devenv/docker/blocks/nginx_proxy/docker-compose.yaml @@ -0,0 +1,9 @@ +# This will proxy all requests for http://localhost:10080/grafana/ to +# http://localhost:3000 (Grafana running locally) +# +# Please note that you'll need to change the root_url in the Grafana configuration: +# root_url = %(protocol)s://%(domain)s:10080/grafana/ + + nginxproxy: + build: blocks/nginx_proxy + network_mode: host diff --git a/devenv/docker/blocks/openldap/docker-compose.yaml b/devenv/docker/blocks/openldap/docker-compose.yaml new file mode 100644 index 00000000000..be06524a57d --- /dev/null +++ b/devenv/docker/blocks/openldap/docker-compose.yaml @@ -0,0 +1,10 @@ + openldap: + build: blocks/openldap + environment: + SLAPD_PASSWORD: grafana + SLAPD_DOMAIN: grafana.org + SLAPD_ADDITIONAL_MODULES: memberof + ports: + - "389:389" + + diff --git a/devenv/docker/blocks/opentsdb/docker-compose.yaml b/devenv/docker/blocks/opentsdb/docker-compose.yaml new file mode 100644 index 00000000000..ee064bb107d --- /dev/null +++ b/devenv/docker/blocks/opentsdb/docker-compose.yaml @@ -0,0 +1,11 @@ + opentsdb: + image: opower/opentsdb:latest + ports: + - "4242:4242" + + fake-opentsdb-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: opentsdb + diff --git a/devenv/docker/blocks/postgres/docker-compose.yaml b/devenv/docker/blocks/postgres/docker-compose.yaml new file mode 100644 index 00000000000..27736042f7b --- /dev/null +++ b/devenv/docker/blocks/postgres/docker-compose.yaml @@ -0,0 +1,16 @@ + postgrestest: + image: postgres:9.3 + environment: + POSTGRES_USER: grafana + POSTGRES_PASSWORD: password + POSTGRES_DATABASE: grafana + ports: + - "5432:5432" + command: postgres -c log_connections=on -c logging_collector=on -c log_destination=stderr -c log_directory=/var/log/postgresql + + fake-postgres-data: + image: grafana/fake-data-gen + network_mode: bridge + environment: + FD_DATASOURCE: postgres + FD_PORT: 5432 diff --git a/devenv/docker/blocks/postgres_tests/docker-compose.yaml b/devenv/docker/blocks/postgres_tests/docker-compose.yaml new file mode 100644 index 00000000000..f5ce0a5a3d3 --- /dev/null +++ b/devenv/docker/blocks/postgres_tests/docker-compose.yaml @@ -0,0 +1,9 @@ + postgrestest: + build: + context: blocks/postgres_tests + environment: + POSTGRES_USER: grafanatest + POSTGRES_PASSWORD: grafanatest + ports: + - "5432:5432" + tmpfs: /var/lib/postgresql/data:rw \ No newline at end of file diff --git a/devenv/docker/blocks/prometheus/docker-compose.yaml b/devenv/docker/blocks/prometheus/docker-compose.yaml new file mode 100644 index 00000000000..3c304cc74ad --- /dev/null +++ b/devenv/docker/blocks/prometheus/docker-compose.yaml @@ -0,0 +1,31 @@ + prometheus: + build: blocks/prometheus + network_mode: host + ports: + - "9090:9090" + + node_exporter: + image: prom/node-exporter + network_mode: host + ports: + - "9100:9100" + + fake-prometheus-data: + image: grafana/fake-data-gen + network_mode: host + ports: + - "9091:9091" + environment: + FD_DATASOURCE: prom + + alertmanager: + image: quay.io/prometheus/alertmanager + network_mode: host + ports: + - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + network_mode: host + ports: + - "8081:8080" diff --git a/devenv/docker/blocks/prometheus2/docker-compose.yaml b/devenv/docker/blocks/prometheus2/docker-compose.yaml new file mode 100644 index 00000000000..589df868084 --- /dev/null +++ b/devenv/docker/blocks/prometheus2/docker-compose.yaml @@ -0,0 +1,31 @@ + prometheus: + build: blocks/prometheus2 + network_mode: host + ports: + - "9090:9090" + + node_exporter: + image: prom/node-exporter + network_mode: host + ports: + - "9100:9100" + + fake-prometheus-data: + image: grafana/fake-data-gen + network_mode: host + ports: + - "9091:9091" + environment: + FD_DATASOURCE: prom + + alertmanager: + image: quay.io/prometheus/alertmanager + network_mode: host + ports: + - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + network_mode: host + ports: + - "8081:8080" diff --git a/devenv/docker/blocks/prometheus_mac/docker-compose.yaml b/devenv/docker/blocks/prometheus_mac/docker-compose.yaml new file mode 100644 index 00000000000..ef53b07418a --- /dev/null +++ b/devenv/docker/blocks/prometheus_mac/docker-compose.yaml @@ -0,0 +1,26 @@ + prometheus: + build: blocks/prometheus_mac + ports: + - "9090:9090" + + node_exporter: + image: prom/node-exporter + ports: + - "9100:9100" + + fake-prometheus-data: + image: grafana/fake-data-gen + ports: + - "9091:9091" + environment: + FD_DATASOURCE: prom + + alertmanager: + image: quay.io/prometheus/alertmanager + ports: + - "9093:9093" + + prometheus-random-data: + build: blocks/prometheus_random_data + ports: + - "8081:8080" diff --git a/devenv/docker/blocks/smtp/docker-compose.yaml b/devenv/docker/blocks/smtp/docker-compose.yaml new file mode 100644 index 00000000000..85d598b6167 --- /dev/null +++ b/devenv/docker/blocks/smtp/docker-compose.yaml @@ -0,0 +1,4 @@ + snmpd: + image: namshi/smtp + ports: + - "25:25" From 667ca3d54d8fee325b9778cf545187d9d83c85ff Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 19 Sep 2018 17:59:55 +0200 Subject: [PATCH 093/127] devenv: fix docker blocks paths --- devenv/docker/blocks/apache_proxy/docker-compose.yaml | 2 +- devenv/docker/blocks/collectd/docker-compose.yaml | 2 +- devenv/docker/blocks/graphite/docker-compose.yaml | 2 +- devenv/docker/blocks/graphite1/docker-compose.yaml | 2 +- devenv/docker/blocks/mssql/docker-compose.yaml | 2 +- devenv/docker/blocks/mssql_tests/docker-compose.yaml | 2 +- devenv/docker/blocks/mysql_opendata/docker-compose.yaml | 2 +- devenv/docker/blocks/mysql_tests/docker-compose.yaml | 2 +- devenv/docker/blocks/nginx_proxy/docker-compose.yaml | 2 +- devenv/docker/blocks/openldap/docker-compose.yaml | 2 +- devenv/docker/blocks/postgres_tests/docker-compose.yaml | 2 +- devenv/docker/blocks/prometheus/docker-compose.yaml | 4 ++-- devenv/docker/blocks/prometheus2/docker-compose.yaml | 4 ++-- devenv/docker/blocks/prometheus_mac/docker-compose.yaml | 4 ++-- 14 files changed, 17 insertions(+), 17 deletions(-) diff --git a/devenv/docker/blocks/apache_proxy/docker-compose.yaml b/devenv/docker/blocks/apache_proxy/docker-compose.yaml index 86d4befadd6..3791213f05a 100644 --- a/devenv/docker/blocks/apache_proxy/docker-compose.yaml +++ b/devenv/docker/blocks/apache_proxy/docker-compose.yaml @@ -5,5 +5,5 @@ # root_url = %(protocol)s://%(domain)s:10081/grafana/ apacheproxy: - build: blocks/apache_proxy + build: docker/blocks/apache_proxy network_mode: host diff --git a/devenv/docker/blocks/collectd/docker-compose.yaml b/devenv/docker/blocks/collectd/docker-compose.yaml index c95827f7928..c5e189b58d8 100644 --- a/devenv/docker/blocks/collectd/docker-compose.yaml +++ b/devenv/docker/blocks/collectd/docker-compose.yaml @@ -1,5 +1,5 @@ collectd: - build: blocks/collectd + build: docker/blocks/collectd environment: HOST_NAME: myserver GRAPHITE_HOST: graphite diff --git a/devenv/docker/blocks/graphite/docker-compose.yaml b/devenv/docker/blocks/graphite/docker-compose.yaml index 606e28638f7..acebd2bd9c0 100644 --- a/devenv/docker/blocks/graphite/docker-compose.yaml +++ b/devenv/docker/blocks/graphite/docker-compose.yaml @@ -1,5 +1,5 @@ graphite09: - build: blocks/graphite + build: docker/blocks/graphite ports: - "8080:80" - "2003:2003" diff --git a/devenv/docker/blocks/graphite1/docker-compose.yaml b/devenv/docker/blocks/graphite1/docker-compose.yaml index cd10593f423..1fa3e738ba8 100644 --- a/devenv/docker/blocks/graphite1/docker-compose.yaml +++ b/devenv/docker/blocks/graphite1/docker-compose.yaml @@ -1,6 +1,6 @@ graphite: build: - context: blocks/graphite1 + context: docker/blocks/graphite1 args: version: master ports: diff --git a/devenv/docker/blocks/mssql/docker-compose.yaml b/devenv/docker/blocks/mssql/docker-compose.yaml index a346fb791f7..05a93629e73 100644 --- a/devenv/docker/blocks/mssql/docker-compose.yaml +++ b/devenv/docker/blocks/mssql/docker-compose.yaml @@ -1,6 +1,6 @@ mssql: build: - context: blocks/mssql/build + context: docker/blocks/mssql/build environment: ACCEPT_EULA: Y MSSQL_SA_PASSWORD: Password! diff --git a/devenv/docker/blocks/mssql_tests/docker-compose.yaml b/devenv/docker/blocks/mssql_tests/docker-compose.yaml index 5da6aad82af..eea4d1e3561 100644 --- a/devenv/docker/blocks/mssql_tests/docker-compose.yaml +++ b/devenv/docker/blocks/mssql_tests/docker-compose.yaml @@ -1,6 +1,6 @@ mssqltests: build: - context: blocks/mssql/build + context: docker/blocks/mssql/build environment: ACCEPT_EULA: Y MSSQL_SA_PASSWORD: Password! diff --git a/devenv/docker/blocks/mysql_opendata/docker-compose.yaml b/devenv/docker/blocks/mysql_opendata/docker-compose.yaml index 594eeed284a..4d478ee0860 100644 --- a/devenv/docker/blocks/mysql_opendata/docker-compose.yaml +++ b/devenv/docker/blocks/mysql_opendata/docker-compose.yaml @@ -1,5 +1,5 @@ mysql_opendata: - build: blocks/mysql_opendata + build: docker/blocks/mysql_opendata environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: testdata diff --git a/devenv/docker/blocks/mysql_tests/docker-compose.yaml b/devenv/docker/blocks/mysql_tests/docker-compose.yaml index 035a6167017..a7509d47880 100644 --- a/devenv/docker/blocks/mysql_tests/docker-compose.yaml +++ b/devenv/docker/blocks/mysql_tests/docker-compose.yaml @@ -1,6 +1,6 @@ mysqltests: build: - context: blocks/mysql_tests + context: docker/blocks/mysql_tests environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: grafana_tests diff --git a/devenv/docker/blocks/nginx_proxy/docker-compose.yaml b/devenv/docker/blocks/nginx_proxy/docker-compose.yaml index a0ceceb83ac..aefd7226f36 100644 --- a/devenv/docker/blocks/nginx_proxy/docker-compose.yaml +++ b/devenv/docker/blocks/nginx_proxy/docker-compose.yaml @@ -5,5 +5,5 @@ # root_url = %(protocol)s://%(domain)s:10080/grafana/ nginxproxy: - build: blocks/nginx_proxy + build: docker/blocks/nginx_proxy network_mode: host diff --git a/devenv/docker/blocks/openldap/docker-compose.yaml b/devenv/docker/blocks/openldap/docker-compose.yaml index be06524a57d..d11858ccfb9 100644 --- a/devenv/docker/blocks/openldap/docker-compose.yaml +++ b/devenv/docker/blocks/openldap/docker-compose.yaml @@ -1,5 +1,5 @@ openldap: - build: blocks/openldap + build: docker/blocks/openldap environment: SLAPD_PASSWORD: grafana SLAPD_DOMAIN: grafana.org diff --git a/devenv/docker/blocks/postgres_tests/docker-compose.yaml b/devenv/docker/blocks/postgres_tests/docker-compose.yaml index f5ce0a5a3d3..7e6da7d8517 100644 --- a/devenv/docker/blocks/postgres_tests/docker-compose.yaml +++ b/devenv/docker/blocks/postgres_tests/docker-compose.yaml @@ -1,6 +1,6 @@ postgrestest: build: - context: blocks/postgres_tests + context: docker/blocks/postgres_tests environment: POSTGRES_USER: grafanatest POSTGRES_PASSWORD: grafanatest diff --git a/devenv/docker/blocks/prometheus/docker-compose.yaml b/devenv/docker/blocks/prometheus/docker-compose.yaml index 3c304cc74ad..db778060dde 100644 --- a/devenv/docker/blocks/prometheus/docker-compose.yaml +++ b/devenv/docker/blocks/prometheus/docker-compose.yaml @@ -1,5 +1,5 @@ prometheus: - build: blocks/prometheus + build: docker/blocks/prometheus network_mode: host ports: - "9090:9090" @@ -25,7 +25,7 @@ - "9093:9093" prometheus-random-data: - build: blocks/prometheus_random_data + build: docker/blocks/prometheus_random_data network_mode: host ports: - "8081:8080" diff --git a/devenv/docker/blocks/prometheus2/docker-compose.yaml b/devenv/docker/blocks/prometheus2/docker-compose.yaml index 589df868084..d586b4b5742 100644 --- a/devenv/docker/blocks/prometheus2/docker-compose.yaml +++ b/devenv/docker/blocks/prometheus2/docker-compose.yaml @@ -1,5 +1,5 @@ prometheus: - build: blocks/prometheus2 + build: docker/blocks/prometheus2 network_mode: host ports: - "9090:9090" @@ -25,7 +25,7 @@ - "9093:9093" prometheus-random-data: - build: blocks/prometheus_random_data + build: docker/blocks/prometheus_random_data network_mode: host ports: - "8081:8080" diff --git a/devenv/docker/blocks/prometheus_mac/docker-compose.yaml b/devenv/docker/blocks/prometheus_mac/docker-compose.yaml index ef53b07418a..b73d278fae2 100644 --- a/devenv/docker/blocks/prometheus_mac/docker-compose.yaml +++ b/devenv/docker/blocks/prometheus_mac/docker-compose.yaml @@ -1,5 +1,5 @@ prometheus: - build: blocks/prometheus_mac + build: docker/blocks/prometheus_mac ports: - "9090:9090" @@ -21,6 +21,6 @@ - "9093:9093" prometheus-random-data: - build: blocks/prometheus_random_data + build: docker/blocks/prometheus_random_data ports: - "8081:8080" From 7a95791025905a86f38027ff97d3713bf88aa15c Mon Sep 17 00:00:00 2001 From: Ben Doyle Date: Thu, 20 Sep 2018 14:16:43 +0100 Subject: [PATCH 094/127] Fix misspelled authentication in Auth overview doc --- docs/sources/auth/overview.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index 3a38ed83988..20010a9ac09 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -32,11 +32,11 @@ permissions and org memberships. ## Grafana Auth -Grafana of course has a built in user authentication system with password authenticaten enabled by default. You can +Grafana of course has a built in user authentication system with password authentication enabled by default. You can disable authentication by enabling anonymous access. You can also hide login form and only allow login through an auth provider (listed above). There is also options for allowing self sign up. -### Anonymous authenticaten +### Anonymous authentication You can make Grafana accessible without any login required by enabling anonymous access in the configuration file. @@ -84,4 +84,3 @@ Set to the option detailed below to true to hide sign-out menu link. Useful if y [auth] disable_signout_menu = true ``` - From 4a8d80a94077767713af7cc4e6668abb93a7c637 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 20 Sep 2018 16:57:58 +0200 Subject: [PATCH 095/127] Explore: Fix metric suggestions when first letters have been typed --- public/app/containers/Explore/PromQueryField.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 491e7005bd0..8188e516161 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -255,6 +255,8 @@ class PromQueryField extends React.Component 3; // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -266,7 +268,7 @@ class PromQueryField extends React.Component Date: Thu, 20 Sep 2018 16:59:06 +0200 Subject: [PATCH 096/127] Explore: dont rate-hint on rate queries --- public/app/plugins/datasource/prometheus/datasource.ts | 2 +- .../plugins/datasource/prometheus/specs/datasource.test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ca80b3760a7..b53b9eb34c1 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -46,7 +46,7 @@ export function determineQueryHints(series: any[], datasource?: any): any[] { // Check for monotony const datapoints: number[][] = s.datapoints; - if (datapoints.length > 1) { + if (query.indexOf('rate(') === -1 && datapoints.length > 1) { let increasing = false; const monotonic = datapoints.filter(dp => dp[0] !== null).every((dp, index) => { if (index === 0) { diff --git a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts index eef2bbd56b6..692a1827247 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource.test.ts @@ -247,6 +247,12 @@ describe('PrometheusDatasource', () => { }); }); + it('returns no rate hint for a monotonously increasing series that already has a rate', () => { + const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'rate(metric[1m])', responseIndex: 0 }]; + const hints = determineQueryHints(series); + expect(hints).toEqual([null]); + }); + it('returns a rate hint w/o action for a complex monotonously increasing series', () => { const series = [{ datapoints: [[23, 1000], [24, 1001]], query: 'sum(metric)', responseIndex: 0 }]; const hints = determineQueryHints(series); From 0e173918aa575c943ce42ea990f9594c64f86f91 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 20 Sep 2018 17:02:26 +0200 Subject: [PATCH 097/127] Explore: show series title in tooltip of legend item --- public/app/containers/Explore/Legend.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/app/containers/Explore/Legend.tsx b/public/app/containers/Explore/Legend.tsx index e00932fe566..439b6c3e54f 100644 --- a/public/app/containers/Explore/Legend.tsx +++ b/public/app/containers/Explore/Legend.tsx @@ -5,7 +5,9 @@ const LegendItem = ({ series }) => (
- {series.alias} + + {series.alias} +
); From 9e86809ace5e06c7669bb4dc92c718b0421f1909 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 20 Sep 2018 17:22:38 +0200 Subject: [PATCH 098/127] Explore: remove closing brace with opening brace --- .../Explore/slate-plugins/braces.test.ts | 18 ++++++++++++++++++ .../containers/Explore/slate-plugins/braces.ts | 16 ++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/public/app/containers/Explore/slate-plugins/braces.test.ts b/public/app/containers/Explore/slate-plugins/braces.test.ts index dda805c07f7..410334d020f 100644 --- a/public/app/containers/Explore/slate-plugins/braces.test.ts +++ b/public/app/containers/Explore/slate-plugins/braces.test.ts @@ -53,4 +53,22 @@ describe('braces', () => { handler(event, change); expect(Plain.serialize(change.value)).toEqual('sum(rate(metric{namespace="dev", cluster="c1"}[2m]))'); }); + + it('removes closing brace when opening brace is removed', () => { + const change = Plain.deserialize('time()').change(); + let event; + change.move(5); + event = new window.KeyboardEvent('keydown', { key: 'Backspace' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('time'); + }); + + it('keeps closing brace when opening brace is removed and inner values exist', () => { + const change = Plain.deserialize('time(value)').change(); + let event; + change.move(5); + event = new window.KeyboardEvent('keydown', { key: 'Backspace' }); + const handled = handler(event, change); + expect(handled).toBeFalsy(); + }); }); diff --git a/public/app/containers/Explore/slate-plugins/braces.ts b/public/app/containers/Explore/slate-plugins/braces.ts index 2ea58569ef0..f3a76263ad6 100644 --- a/public/app/containers/Explore/slate-plugins/braces.ts +++ b/public/app/containers/Explore/slate-plugins/braces.ts @@ -43,6 +43,22 @@ export default function BracesPlugin() { return true; } + case 'Backspace': { + const text = value.anchorText.text; + const offset = value.anchorOffset; + const previousChar = text[offset - 1]; + const nextChar = text[offset]; + if (BRACES[previousChar] && BRACES[previousChar] === nextChar) { + event.preventDefault(); + // Remove closing brace if directly following + change + .deleteBackward() + .deleteForward() + .focus(); + return true; + } + } + default: { break; } From b609d81194f3e6a79a3798cf00040dd8035ab82e Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Thu, 20 Sep 2018 19:43:19 +0200 Subject: [PATCH 099/127] pkg/tsdb/elasticsearch/client/search_request.go: simplify loop with append. $ gometalinter --vendor --disable-all --enable=megacheck --disable=gotype --deadline=6m ./... pkg/tsdb/elasticsearch/client/search_request.go:59:4:warning: should replace loop with sr.Aggs = append(sr.Aggs, aggArray...) (S1011) (megacheck) pkg/tsdb/elasticsearch/client/search_request.go:303:4:warning: should replace loop with agg.Aggregation.Aggs = append(agg.Aggregation.Aggs, childAggs...) (S1011) (megacheck) --- pkg/tsdb/elasticsearch/client/search_request.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index 2b833ce78d3..9d55fc15ff9 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -56,9 +56,7 @@ func (b *SearchRequestBuilder) Build() (*SearchRequest, error) { if err != nil { return nil, err } - for _, agg := range aggArray { - sr.Aggs = append(sr.Aggs, agg) - } + sr.Aggs = append(sr.Aggs, aggArray...) } } @@ -300,9 +298,7 @@ func (b *aggBuilderImpl) Build() (AggArray, error) { return nil, err } - for _, childAgg := range childAggs { - agg.Aggregation.Aggs = append(agg.Aggregation.Aggs, childAgg) - } + agg.Aggregation.Aggs = append(agg.Aggregation.Aggs, childAggs...) } aggs = append(aggs, agg) From 303e70db25daac93e2a892004fdaa1cbc6772d15 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Thu, 20 Sep 2018 20:05:19 +0200 Subject: [PATCH 100/127] pkg/tsdb/elasticsearch/response_parser.go: simplify redundant code $ gometalinter --vendor --disable-all --enable=megacheck --disable=gotype --deadline=6m ./... pkg/tsdb/elasticsearch/response_parser.go:95:41:warning: should use make(map[string]string) instead (S1019) (megacheck) pkg/tsdb/elasticsearch/response_parser.go:125:41:warning: should use make(map[string]string) instead (S1019) (megacheck) pkg/tsdb/elasticsearch/response_parser.go:317:5:warning: redundant break statement (S1023) (megacheck) pkg/tsdb/elasticsearch/response_parser.go:358:5:warning: redundant break statement (S1023) (megacheck) --- pkg/tsdb/elasticsearch/response_parser.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 7bdab60389c..0090754840a 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -92,7 +92,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu } else { for _, b := range esAgg.Get("buckets").MustArray() { bucket := simplejson.NewFromAny(b) - newProps := make(map[string]string, 0) + newProps := make(map[string]string) for k, v := range props { newProps[k] = v @@ -122,7 +122,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu for _, bucketKey := range bucketKeys { bucket := simplejson.NewFromAny(buckets[bucketKey]) - newProps := make(map[string]string, 0) + newProps := make(map[string]string) for k, v := range props { newProps[k] = v @@ -314,7 +314,6 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef switch metric.Type { case "count": addMetricValue(&values, rp.getMetricName(metric.Type), castToNullFloat(bucket.Get("doc_count"))) - break case "extended_stats": metaKeys := make([]string, 0) meta := metric.Meta.MustMap() @@ -355,7 +354,6 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef } addMetricValue(&values, metricName, castToNullFloat(bucket.GetPath(metric.ID, "value"))) - break } } From 0dea8fe1e07a0f92714f26b77a7314d773debb4e Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Thu, 20 Sep 2018 20:15:22 +0200 Subject: [PATCH 101/127] pkg/services/sqlstore/user.go: empty branch $ gometalinter --vendor --disable-all --enable=megacheck --disable=gotype --deadline=6m ./... pkg/services/sqlstore/user.go:274:3:warning: empty branch (SA9003) (megacheck) --- pkg/services/sqlstore/user.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 6bd30be1869..848a11d81ab 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -271,9 +271,6 @@ func ChangeUserPassword(cmd *m.ChangeUserPasswordCommand) error { func UpdateUserLastSeenAt(cmd *m.UpdateUserLastSeenAtCommand) error { return inTransaction(func(sess *DBSession) error { - if cmd.UserId <= 0 { - } - user := m.User{ Id: cmd.UserId, LastSeenAt: time.Now(), From 03a2a39a2abee974462556c823c7a9d2b9b69ff9 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Thu, 20 Sep 2018 22:31:10 +0200 Subject: [PATCH 102/127] pkg/tracing/tracing.go: replace deprecated cfg.New function $ gometalinter --vendor --disable-all --enable=megacheck --disable=gotype --deadline=6m ./... pkg/tracing/tracing.go:81:25:warning: cfg.New is deprecated: use NewTracer() function (SA1019) (megacheck) --- pkg/tracing/tracing.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/tracing/tracing.go b/pkg/tracing/tracing.go index 61f45af3635..fd7258b7a0a 100644 --- a/pkg/tracing/tracing.go +++ b/pkg/tracing/tracing.go @@ -58,7 +58,8 @@ func (ts *TracingService) parseSettings() { func (ts *TracingService) initGlobalTracer() error { cfg := jaegercfg.Configuration{ - Disabled: !ts.enabled, + ServiceName: "grafana", + Disabled: !ts.enabled, Sampler: &jaegercfg.SamplerConfig{ Type: ts.samplerType, Param: ts.samplerParam, @@ -78,7 +79,7 @@ func (ts *TracingService) initGlobalTracer() error { options = append(options, jaegercfg.Tag(tag, value)) } - tracer, closer, err := cfg.New("grafana", options...) + tracer, closer, err := cfg.NewTracer(options...) if err != nil { return err } From 3689bb778c36b32e9d114520276bccc2cafa7613 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 21 Sep 2018 10:44:53 +0200 Subject: [PATCH 103/127] Fix misspell issues See, $ gometalinter --disable-all --enable misspell --deadline 10m --vendor ./... pkg/api/dtos/alerting_test.go:32:13:warning: "expectes" is a misspelling of "expects" (misspell) pkg/api/static/static.go:2:18:warning: "Unknwon" is a misspelling of "Unknown" (misspell) pkg/components/imguploader/azureblobuploader.go:55:48:warning: "conatiner" is a misspelling of "container" (misspell) pkg/login/ldap_settings.go:51:115:warning: "compatability" is a misspelling of "compatibility" (misspell) pkg/middleware/auth_proxy_test.go:122:22:warning: "Destory" is a misspelling of "Destroy" (misspell) pkg/middleware/logger.go:2:18:warning: "Unknwon" is a misspelling of "Unknown" (misspell) pkg/services/notifications/codes.go:9:13:warning: "Unknwon" is a misspelling of "Unknown" (misspell) pkg/services/session/mysql.go:170:3:warning: "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/mysql.go:171:24:warning: "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/session.go:95:4:warning: "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/session.go:96:1:warning: "Destory" is a misspelling of "Destroy" (misspell) pkg/services/session/session.go:167:25:warning: "Destory" is a misspelling of "Destroy" (misspell) pkg/setting/setting.go:1:18:warning: "Unknwon" is a misspelling of "Unknown" (misspell) pkg/tsdb/cloudwatch/cloudwatch.go:199:14:warning: "resolutin" is a misspelling of "resolutions" (misspell) pkg/tsdb/cloudwatch/cloudwatch.go:270:15:warning: "resolutin" is a misspelling of "resolutions" (misspell) pkg/tsdb/elasticsearch/response_parser.go:531:24:warning: "Unkown" is a misspelling of "Unknown" (misspell) pkg/tsdb/elasticsearch/client/search_request.go:113:7:warning: "initaite" is a misspelling of "initiate" (misspell) Note: Unknwon is a library name, and Destory a mysql typo. --- pkg/api/dtos/alerting_test.go | 2 +- pkg/components/imguploader/azureblobuploader.go | 2 +- pkg/login/ldap_settings.go | 2 +- pkg/tsdb/cloudwatch/cloudwatch.go | 4 ++-- pkg/tsdb/elasticsearch/client/search_request.go | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go index c38f281be9c..f4c09f202cb 100644 --- a/pkg/api/dtos/alerting_test.go +++ b/pkg/api/dtos/alerting_test.go @@ -29,7 +29,7 @@ func TestFormatShort(t *testing.T) { } if parsed != tc.interval { - t.Errorf("expectes the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval) + t.Errorf("expects the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval) } } } diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go index 3c0ac5b8884..d4117b6fc34 100644 --- a/pkg/components/imguploader/azureblobuploader.go +++ b/pkg/components/imguploader/azureblobuploader.go @@ -52,7 +52,7 @@ func (az *AzureBlobUploader) Upload(ctx context.Context, imageDiskPath string) ( } randomFileName := util.GetRandomString(30) + ".png" // upload image - az.log.Debug("Uploading image to azure_blob", "conatiner_name", az.container_name, "blob_name", randomFileName) + az.log.Debug("Uploading image to azure_blob", "container_name", az.container_name, "blob_name", randomFileName) resp, err := blob.FileUpload(az.container_name, randomFileName, file) if err != nil { return "", err diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go index 7ebfbc79ba8..40791a509db 100644 --- a/pkg/login/ldap_settings.go +++ b/pkg/login/ldap_settings.go @@ -48,7 +48,7 @@ type LdapAttributeMap struct { type LdapGroupToOrgRole struct { GroupDN string `toml:"group_dn"` OrgId int64 `toml:"org_id"` - IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability) + IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility) OrgRole m.RoleType `toml:"org_role"` } diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 92352a51315..be14c6f96ec 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -196,7 +196,7 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch params.ExtendedStatistics = query.ExtendedStatistics } - // 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600 + // 1 minutes resolution metrics is stored for 15 days, 15 * 24 * 60 = 21600 if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) { return nil, errors.New("too long query period") } @@ -267,7 +267,7 @@ func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, regi ScanBy: aws.String("TimestampAscending"), } for _, query := range queries { - // 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600 + // 1 minutes resolution metrics is stored for 15 days, 15 * 24 * 60 = 21600 if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) { return nil, errors.New("too long query period") } diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index 9d55fc15ff9..4c577a2c31d 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -110,7 +110,7 @@ func (b *SearchRequestBuilder) Query() *QueryBuilder { return b.queryBuilder } -// Agg initaite and returns a new aggregation builder +// Agg initiate and returns a new aggregation builder func (b *SearchRequestBuilder) Agg() AggBuilder { aggBuilder := newAggBuilder() b.aggBuilders = append(b.aggBuilders, aggBuilder) From 80fa66fcb06c30e8cfa80e8c0f7cfbe0025add6e Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 21 Sep 2018 11:51:26 +0200 Subject: [PATCH 104/127] Fix some typos found by codespell See, $ codespell -S "./.git*,./vendor*,./public*" --- CHANGELOG.md | 4 ++-- build.go | 3 +-- devenv/dev-dashboards/panel_tests_polystat.json | 12 ++++++------ pkg/components/imguploader/azureblobuploader.go | 12 ++++++------ pkg/components/simplejson/simplejson.go | 6 +++--- pkg/services/alerting/extractor.go | 5 +++-- pkg/services/alerting/notifiers/teams.go | 2 +- .../datasources/testdata/broken-yaml/commented.yaml | 2 +- pkg/services/rendering/http_mode.go | 4 ++-- pkg/services/rendering/rendering.go | 2 +- pkg/services/sqlstore/migrations/annotation_mig.go | 2 +- pkg/services/sqlstore/transactions_test.go | 2 +- pkg/tsdb/elasticsearch/client/client_test.go | 2 +- pkg/tsdb/influxdb/query_test.go | 2 +- pkg/tsdb/prometheus/prometheus.go | 4 ++-- pkg/util/md5_test.go | 2 +- 16 files changed, 33 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d32615ce97..ace4348af99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -318,7 +318,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4- * **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572) * **Dashboard**: Add search filter/tabs to new panel control [#10427](https://github.com/grafana/grafana/issues/10427) * **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553) -* **Influxdb**: Dont assume the first column in table response is time. [#11476](https://github.com/grafana/grafana/issues/11476), thx [@hahnjo](https://github.com/hahnjo) +* **Influxdb**: Don't assume the first column in table response is time. [#11476](https://github.com/grafana/grafana/issues/11476), thx [@hahnjo](https://github.com/hahnjo) ### Tech * Backend code simplification [#11613](https://github.com/grafana/grafana/pull/11613), thx [@knweiss](https://github.com/knweiss) @@ -1464,7 +1464,7 @@ Grafana 2.x is fundamentally different from 1.x; it now ships with an integrated **New features** - [Issue #1623](https://github.com/grafana/grafana/issues/1623). Share Dashboard: Dashboard snapshot sharing (dash and data snapshot), save to local or save to public snapshot dashboard snapshots.raintank.io site -- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embedd a single graph on another web site +- [Issue #1622](https://github.com/grafana/grafana/issues/1622). Share Panel: The share modal now has an embed option, gives you an iframe that you can use to embed a single graph on another web site - [Issue #718](https://github.com/grafana/grafana/issues/718). Dashboard: When saving a dashboard and another user has made changes in between the user is prompted with a warning if he really wants to overwrite the other's changes - [Issue #1331](https://github.com/grafana/grafana/issues/1331). Graph & Singlestat: New axis/unit format selector and more units (kbytes, Joule, Watt, eV), and new design for graph axis & grid tab and single stat options tab views - [Issue #1241](https://github.com/grafana/grafana/issues/1242). Timepicker: New option in timepicker (under dashboard settings), to change ``now`` to be for example ``now-1m``, useful when you want to ignore last minute because it contains incomplete data diff --git a/build.go b/build.go index 561dd70df0e..9502f52be11 100644 --- a/build.go +++ b/build.go @@ -120,7 +120,6 @@ func main() { createLinuxPackages() } - case "pkg-rpm": grunt(gruntBuildArg("release")...) createRpmPackages() @@ -417,7 +416,7 @@ func test(pkg string) { func build(binaryName, pkg string, tags []string) { binary := fmt.Sprintf("./bin/%s-%s/%s", goos, goarch, binaryName) if isDev { - //dont include os and arch in output path in dev environment + //don't include os and arch in output path in dev environment binary = fmt.Sprintf("./bin/%s", binaryName) } diff --git a/devenv/dev-dashboards/panel_tests_polystat.json b/devenv/dev-dashboards/panel_tests_polystat.json index 51d3085c438..fc3f4c92b3c 100644 --- a/devenv/dev-dashboards/panel_tests_polystat.json +++ b/devenv/dev-dashboards/panel_tests_polystat.json @@ -884,8 +884,8 @@ "value": "celsius" }, { - "text": "Farenheit (°F)", - "value": "farenheit" + "text": "Fahrenheit (°F)", + "value": "fahrenheit" }, { "text": "Kelvin (K)", @@ -1991,8 +1991,8 @@ "value": "celsius" }, { - "text": "Farenheit (°F)", - "value": "farenheit" + "text": "Fahrenheit (°F)", + "value": "fahrenheit" }, { "text": "Kelvin (K)", @@ -3078,8 +3078,8 @@ "value": "celsius" }, { - "text": "Farenheit (°F)", - "value": "farenheit" + "text": "Fahrenheit (°F)", + "value": "fahrenheit" }, { "text": "Kelvin (K)", diff --git a/pkg/components/imguploader/azureblobuploader.go b/pkg/components/imguploader/azureblobuploader.go index d4117b6fc34..a902807925b 100644 --- a/pkg/components/imguploader/azureblobuploader.go +++ b/pkg/components/imguploader/azureblobuploader.go @@ -274,10 +274,10 @@ func (a *Auth) canonicalizedHeaders(req *http.Request) string { } } - splitted := strings.Split(buffer.String(), "\n") - sort.Strings(splitted) + split := strings.Split(buffer.String(), "\n") + sort.Strings(split) - return strings.Join(splitted, "\n") + return strings.Join(split, "\n") } /* @@ -313,8 +313,8 @@ func (a *Auth) canonicalizedResource(req *http.Request) string { buffer.WriteString(fmt.Sprintf("\n%s:%s", key, strings.Join(values, ","))) } - splitted := strings.Split(buffer.String(), "\n") - sort.Strings(splitted) + split := strings.Split(buffer.String(), "\n") + sort.Strings(split) - return strings.Join(splitted, "\n") + return strings.Join(split, "\n") } diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index 85e2f955943..35e305eb414 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -256,7 +256,7 @@ func (j *Json) StringArray() ([]string, error) { // MustArray guarantees the return of a `[]interface{}` (with optional default) // -// useful when you want to interate over array values in a succinct manner: +// useful when you want to iterate over array values in a succinct manner: // for i, v := range js.Get("results").MustArray() { // fmt.Println(i, v) // } @@ -281,7 +281,7 @@ func (j *Json) MustArray(args ...[]interface{}) []interface{} { // MustMap guarantees the return of a `map[string]interface{}` (with optional default) // -// useful when you want to interate over map values in a succinct manner: +// useful when you want to iterate over map values in a succinct manner: // for k, v := range js.Get("dictionary").MustMap() { // fmt.Println(k, v) // } @@ -329,7 +329,7 @@ func (j *Json) MustString(args ...string) string { // MustStringArray guarantees the return of a `[]string` (with optional default) // -// useful when you want to interate over array values in a succinct manner: +// useful when you want to iterate over array values in a succinct manner: // for i, s := range js.Get("results").MustStringArray() { // fmt.Println(i, s) // } diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e1c1bfacb2e..229092e217b 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -82,12 +82,13 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, if collapsed && collapsedJSON.MustBool() { // extract alerts from sub panels for collapsed panels - als, err := e.getAlertFromPanels(panel, validateAlertFunc) + alertSlice, err := e.getAlertFromPanels(panel, + validateAlertFunc) if err != nil { return nil, err } - alerts = append(alerts, als...) + alerts = append(alerts, alertSlice...) continue } diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 7beb71e5c65..2dad11285b4 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -74,7 +74,7 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { } message := "" - if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok. + if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok. message = evalContext.Rule.Message } diff --git a/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml index 1bb9cb53b45..fc13398d472 100644 --- a/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml +++ b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml @@ -4,7 +4,7 @@ # org_id: 1 # # list of datasources to insert/update depending -# # whats available in the datbase +# # what's available in the datbase #datasources: # # name of the datasource. Required # - name: Graphite diff --git a/pkg/services/rendering/http_mode.go b/pkg/services/rendering/http_mode.go index d47dfaeaae1..40259c44746 100644 --- a/pkg/services/rendering/http_mode.go +++ b/pkg/services/rendering/http_mode.go @@ -70,7 +70,7 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend return nil, ErrTimeout } - // if we didnt get a 200 response, something went wrong. + // if we didn't get a 200 response, something went wrong. if resp.StatusCode != http.StatusOK { rs.log.Error("Remote rendering request failed", "error", resp.Status) return nil, fmt.Errorf("Remote rendering request failed. %d: %s", resp.StatusCode, resp.Status) @@ -83,7 +83,7 @@ func (rs *RenderingService) renderViaHttp(ctx context.Context, opts Opts) (*Rend defer out.Close() _, err = io.Copy(out, resp.Body) if err != nil { - // check that we didnt timeout while receiving the response. + // check that we didn't timeout while receiving the response. if reqContext.Err() == context.DeadlineExceeded { rs.log.Info("Rendering timed out") return nil, ErrTimeout diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index ff4a67cc9b6..ecef83d74d9 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -45,7 +45,7 @@ func (rs *RenderingService) Init() error { // set value used for domain attribute of renderKey cookie if rs.Cfg.RendererUrl != "" { - // RendererCallbackUrl has already been passed, it wont generate an error. + // RendererCallbackUrl has already been passed, it won't generate an error. u, _ := url.Parse(rs.Cfg.RendererCallbackUrl) rs.domain = u.Hostname() } else if setting.HttpAddr != setting.DEFAULT_HTTP_ADDR { diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index d231d3283e2..49920dee490 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -105,7 +105,7 @@ func addAnnotationMig(mg *Migrator) { })) // - // Convert epoch saved as seconds to miliseconds + // Convert epoch saved as seconds to milliseconds // updateEpochSql := "UPDATE annotation SET epoch = (epoch*1000) where epoch < 9999999999" mg.AddMigration("Convert existing annotations from seconds to milliseconds", NewRawSqlMigration(updateEpochSql)) diff --git a/pkg/services/sqlstore/transactions_test.go b/pkg/services/sqlstore/transactions_test.go index 937649921ba..41dedde5db4 100644 --- a/pkg/services/sqlstore/transactions_test.go +++ b/pkg/services/sqlstore/transactions_test.go @@ -39,7 +39,7 @@ func TestTransaction(t *testing.T) { So(err, ShouldEqual, models.ErrInvalidApiKey) }) - Convey("wont update if one handler fails", func() { + Convey("won't update if one handler fails", func() { err := ss.InTransaction(context.Background(), func(ctx context.Context) error { err := DeleteApiKeyCtx(ctx, deleteApiKeyCmd) if err != nil { diff --git a/pkg/tsdb/elasticsearch/client/client_test.go b/pkg/tsdb/elasticsearch/client/client_test.go index 11d1cdb1d71..af9ac0d8fce 100644 --- a/pkg/tsdb/elasticsearch/client/client_test.go +++ b/pkg/tsdb/elasticsearch/client/client_test.go @@ -40,7 +40,7 @@ func TestClient(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("When unspported version set should return error", func() { + Convey("When unsupported version set should return error", func() { ds := &models.DataSource{ JsonData: simplejson.NewFromAny(map[string]interface{}{ "esVersion": 6, diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index f1270560269..cc1358a72d7 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -158,7 +158,7 @@ func TestInfluxdbQueryBuilder(t *testing.T) { So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`) }) - Convey("can render number greather then condition tags", func() { + Convey("can render number greater then condition tags", func() { query := &Query{Tags: []*Tag{{Operator: ">", Value: "10001", Key: "key"}}} So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`) diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index bf9fe9f152c..83bb683fccf 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -92,12 +92,12 @@ func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSourc return nil, err } - querys, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) + queries, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) if err != nil { return nil, err } - for _, query := range querys { + for _, query := range queries { timeRange := apiv1.Range{ Start: query.Start, End: query.End, diff --git a/pkg/util/md5_test.go b/pkg/util/md5_test.go index 1338d42bb51..16ef1ddb4a0 100644 --- a/pkg/util/md5_test.go +++ b/pkg/util/md5_test.go @@ -3,7 +3,7 @@ package util import "testing" func TestMd5Sum(t *testing.T) { - input := "dont hash passwords with md5" + input := "don't hash passwords with md5" have, err := Md5SumString(input) if err != nil { From f0167e17edef3e21abc00d308300d311765159c5 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 21 Sep 2018 13:39:24 +0200 Subject: [PATCH 105/127] Revert Fahrenheit to Farenheit This is a typo in https://github.com/grafana/grafana/blob/master/public/app/core/utils/kbn.ts#L1051 --- devenv/dev-dashboards/panel_tests_polystat.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_polystat.json b/devenv/dev-dashboards/panel_tests_polystat.json index fc3f4c92b3c..51d3085c438 100644 --- a/devenv/dev-dashboards/panel_tests_polystat.json +++ b/devenv/dev-dashboards/panel_tests_polystat.json @@ -884,8 +884,8 @@ "value": "celsius" }, { - "text": "Fahrenheit (°F)", - "value": "fahrenheit" + "text": "Farenheit (°F)", + "value": "farenheit" }, { "text": "Kelvin (K)", @@ -1991,8 +1991,8 @@ "value": "celsius" }, { - "text": "Fahrenheit (°F)", - "value": "fahrenheit" + "text": "Farenheit (°F)", + "value": "farenheit" }, { "text": "Kelvin (K)", @@ -3078,8 +3078,8 @@ "value": "celsius" }, { - "text": "Fahrenheit (°F)", - "value": "fahrenheit" + "text": "Farenheit (°F)", + "value": "farenheit" }, { "text": "Kelvin (K)", From da46cc2fca45b0e5310529ba5b5509e388260347 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 21 Sep 2018 13:41:03 +0200 Subject: [PATCH 106/127] Fix changed want md5 hash --- pkg/util/md5_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/md5_test.go b/pkg/util/md5_test.go index 16ef1ddb4a0..43c685b8763 100644 --- a/pkg/util/md5_test.go +++ b/pkg/util/md5_test.go @@ -10,7 +10,7 @@ func TestMd5Sum(t *testing.T) { t.Fatal("expected err to be nil") } - want := "2d6a56c82d09d374643b926d3417afba" + want := "dd1f7fdb3466c0d09c2e839d1f1530f8" if have != want { t.Fatalf("expected: %s got: %s", want, have) } From 60dfff11a049b583a13c61e2a535eae04c025c46 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 21 Sep 2018 13:41:31 +0200 Subject: [PATCH 107/127] Fix datbase > database --- .../datasources/testdata/broken-yaml/commented.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml index fc13398d472..b532c9012ec 100644 --- a/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml +++ b/pkg/services/provisioning/datasources/testdata/broken-yaml/commented.yaml @@ -4,7 +4,7 @@ # org_id: 1 # # list of datasources to insert/update depending -# # what's available in the datbase +# # what's available in the database #datasources: # # name of the datasource. Required # - name: Graphite From 7641c37dfcdab0b511addab081725af722477507 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 21 Sep 2018 16:57:39 +0200 Subject: [PATCH 108/127] docs: improve oauth generic azure ad instructions --- docs/sources/auth/generic-oauth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/auth/generic-oauth.md b/docs/sources/auth/generic-oauth.md index f89595d3b11..5bb5c4cd753 100644 --- a/docs/sources/auth/generic-oauth.md +++ b/docs/sources/auth/generic-oauth.md @@ -174,7 +174,7 @@ allowed_organizations = allowed_organizations = ``` -Note: It's important to ensure that the SERVER_ROOT_URL in Grafana is set in your Azure Application Return URLs +> Note: It's important to ensure that the [root_url](/installation/configuration/#root-url) in Grafana is set in your Azure Application Reply URLs (App -> Settings -> Reply URLs) ## Set up OAuth2 with Centrify From 5fd24e2435a28e95a88a1a9989f532c4b6809581 Mon Sep 17 00:00:00 2001 From: Jon Ferreira Date: Fri, 21 Sep 2018 17:17:29 -0400 Subject: [PATCH 109/127] Fix https://github.com/grafana/grafana/issues/13387 metric segment options displays after blur --- public/app/core/directives/metric_segment.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index 7759e14f2cc..de904e95fc6 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -118,6 +118,9 @@ export function metricSegment($compile, $sce) { }; $scope.matcher = function(item) { + if (linkMode) { + return false; + } let str = this.query; if (str[0] === '/') { str = str.substring(1); From 98dad530e282f7aed968a4ee62afdfcb36422922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 22 Sep 2018 10:06:57 +0200 Subject: [PATCH 110/127] provisioning: changed provisioning default update interval from 3 to 10 seconds --- docs/sources/administration/provisioning.md | 2 +- pkg/services/provisioning/dashboards/config_reader.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index a026d1ec0cd..16d425d289a 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -200,7 +200,7 @@ providers: folder: '' type: file disableDeletion: false - updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards + updateIntervalSeconds: 10 #how often Grafana will scan for changed dashboards options: path: /var/lib/grafana/dashboards ``` diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 7508550838f..bfef06b558e 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -83,7 +83,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { } if dashboards[i].UpdateIntervalSeconds == 0 { - dashboards[i].UpdateIntervalSeconds = 3 + dashboards[i].UpdateIntervalSeconds = 10 } } From e91729a5683114d4b8729eca6a21b7eb35333184 Mon Sep 17 00:00:00 2001 From: Jon Ferreira Date: Sat, 22 Sep 2018 15:00:36 -0400 Subject: [PATCH 111/127] When stacking graphs, always include the y-offset so that tooltips can render proper values for individual points --- public/vendor/flot/jquery.flot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 8ee09e25c41..4a85b08c8d7 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -1129,7 +1129,7 @@ Licensed under the MIT license. format.push({ x: true, number: true, required: true }); format.push({ y: true, number: true, required: true }); - if (s.bars.show || (s.lines.show && s.lines.fill)) { + if (s.stack || s.bars.show || (s.lines.show && s.lines.fill)) { var autoscale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero)); format.push({ y: true, number: true, required: false, defaultValue: 0, autoscale: autoscale }); if (s.bars.horizontal) { From 0870464ea535a2ad111b856637bc99eefc0b1051 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Sat, 22 Sep 2018 10:50:00 +0200 Subject: [PATCH 112/127] Fix goconst issues See, $ gometalinter --vendor --disable-all --enable=goconst --disable=gotype --deadline=6m ./... build.go:113:15:warning: 2 other occurrence(s) of "linux" found in: build.go:119:15 build.go:491:34 (goconst) build.go:119:15:warning: 2 other occurrence(s) of "linux" found in: build.go:113:15 build.go:491:34 (goconst) build.go:491:34:warning: 2 other occurrence(s) of "linux" found in: build.go:113:15 build.go:119:15 (goconst) build.go:381:21:warning: 2 other occurrence(s) of "windows" found in: build.go:423:13 build.go:487:13 (goconst) build.go:423:13:warning: 2 other occurrence(s) of "windows" found in: build.go:381:21 build.go:487:13 (goconst) build.go:487:13:warning: 2 other occurrence(s) of "windows" found in: build.go:381:21 build.go:423:13 (goconst) pkg/api/dashboard.go:67:22:warning: 5 other occurrence(s) of "Anonymous" found in: pkg/api/dashboard.go:67:35 pkg/api/dashboard.go:131:10 pkg/api/dashboard.go:406:13 pkg/api/folder.go:98:22 pkg/api/folder.go:98:35 (goconst) pkg/api/dashboard.go:67:35:warning: 5 other occurrence(s) of "Anonymous" found in: pkg/api/dashboard.go:67:22 pkg/api/dashboard.go:131:10 pkg/api/dashboard.go:406:13 pkg/api/folder.go:98:22 pkg/api/folder.go:98:35 (goconst) pkg/api/dashboard.go:131:10:warning: 5 other occurrence(s) of "Anonymous" found in: pkg/api/dashboard.go:67:22 pkg/api/dashboard.go:67:35 pkg/api/dashboard.go:406:13 pkg/api/folder.go:98:22 pkg/api/folder.go:98:35 (goconst) pkg/api/dashboard.go:406:13:warning: 5 other occurrence(s) of "Anonymous" found in: pkg/api/dashboard.go:67:22 pkg/api/dashboard.go:67:35 pkg/api/dashboard.go:131:10 pkg/api/folder.go:98:22 pkg/api/folder.go:98:35 (goconst) pkg/api/folder.go:98:22:warning: 5 other occurrence(s) of "Anonymous" found in: pkg/api/dashboard.go:67:22 pkg/api/dashboard.go:67:35 pkg/api/dashboard.go:131:10 pkg/api/dashboard.go:406:13 pkg/api/folder.go:98:35 (goconst) pkg/api/folder.go:98:35:warning: 5 other occurrence(s) of "Anonymous" found in: pkg/api/dashboard.go:67:22 pkg/api/dashboard.go:67:35 pkg/api/dashboard.go:131:10 pkg/api/dashboard.go:406:13 pkg/api/folder.go:98:22 (goconst) pkg/api/index.go:63:47:warning: 2 other occurrence(s) of "light" found in: pkg/api/index.go:91:22 pkg/api/index.go:93:16 (goconst) pkg/api/index.go:91:22:warning: 2 other occurrence(s) of "light" found in: pkg/api/index.go:63:47 pkg/api/index.go:93:16 (goconst) pkg/api/index.go:93:16:warning: 2 other occurrence(s) of "light" found in: pkg/api/index.go:63:47 pkg/api/index.go:91:22 (goconst) pkg/components/null/float.go:71:25:warning: 2 other occurrence(s) of "null" found in: pkg/components/null/float.go:103:10 pkg/components/null/float.go:112:10 (goconst) pkg/components/null/float.go:103:10:warning: 2 other occurrence(s) of "null" found in: pkg/components/null/float.go:71:25 pkg/components/null/float.go:112:10 (goconst) pkg/components/null/float.go:112:10:warning: 2 other occurrence(s) of "null" found in: pkg/components/null/float.go:71:25 pkg/components/null/float.go:103:10 (goconst) pkg/services/alerting/notifiers/pagerduty.go:79:16:warning: 2 other occurrence(s) of "Triggered metrics:\n\n" found in: pkg/services/alerting/notifiers/kafka.go:64:16 pkg/services/alerting/notifiers/opsgenie.go:98:16 (goconst) pkg/services/alerting/notifiers/kafka.go:64:16:warning: 2 other occurrence(s) of "Triggered metrics:\n\n" found in: pkg/services/alerting/notifiers/pagerduty.go:79:16 pkg/services/alerting/notifiers/opsgenie.go:98:16 (goconst) pkg/services/alerting/notifiers/opsgenie.go:98:16:warning: 2 other occurrence(s) of "Triggered metrics:\n\n" found in: pkg/services/alerting/notifiers/pagerduty.go:79:16 pkg/services/alerting/notifiers/kafka.go:64:16 (goconst) pkg/social/social.go:85:11:warning: 2 other occurrence(s) of "grafana_com" found in: pkg/social/social.go:162:14 pkg/social/social.go:197:11 (goconst) pkg/social/social.go:162:14:warning: 2 other occurrence(s) of "grafana_com" found in: pkg/social/social.go:85:11 pkg/social/social.go:197:11 (goconst) pkg/social/social.go:197:11:warning: 2 other occurrence(s) of "grafana_com" found in: pkg/social/social.go:85:11 pkg/social/social.go:162:14 (goconst) pkg/tsdb/elasticsearch/time_series_query.go:92:17:warning: 3 other occurrence(s) of "count" found in: pkg/tsdb/elasticsearch/response_parser.go:152:8 pkg/tsdb/elasticsearch/response_parser.go:167:31 pkg/tsdb/elasticsearch/response_parser.go:315:9 (goconst) pkg/tsdb/elasticsearch/response_parser.go:152:8:warning: 3 other occurrence(s) of "count" found in: pkg/tsdb/elasticsearch/time_series_query.go:92:17 pkg/tsdb/elasticsearch/response_parser.go:167:31 pkg/tsdb/elasticsearch/response_parser.go:315:9 (goconst) pkg/tsdb/elasticsearch/response_parser.go:167:31:warning: 3 other occurrence(s) of "count" found in: pkg/tsdb/elasticsearch/time_series_query.go:92:17 pkg/tsdb/elasticsearch/response_parser.go:152:8 pkg/tsdb/elasticsearch/response_parser.go:315:9 (goconst) pkg/tsdb/elasticsearch/response_parser.go:315:9:warning: 3 other occurrence(s) of "count" found in: pkg/tsdb/elasticsearch/time_series_query.go:92:17 pkg/tsdb/elasticsearch/response_parser.go:152:8 pkg/tsdb/elasticsearch/response_parser.go:167:31 (goconst) pkg/tsdb/elasticsearch/time_series_query.go:78:9:warning: 2 other occurrence(s) of "date_histogram" found in: pkg/tsdb/elasticsearch/response_parser.go:84:22 pkg/tsdb/elasticsearch/response_parser.go:369:24 (goconst) pkg/tsdb/elasticsearch/response_parser.go:84:22:warning: 2 other occurrence(s) of "date_histogram" found in: pkg/tsdb/elasticsearch/time_series_query.go:78:9 pkg/tsdb/elasticsearch/response_parser.go:369:24 (goconst) pkg/tsdb/elasticsearch/response_parser.go:369:24:warning: 2 other occurrence(s) of "date_histogram" found in: pkg/tsdb/elasticsearch/time_series_query.go:78:9 pkg/tsdb/elasticsearch/response_parser.go:84:22 (goconst) --- build.go | 17 ++++++++---- pkg/api/dashboard.go | 10 +++++-- pkg/api/folder.go | 2 +- pkg/api/index.go | 16 +++++++---- pkg/components/null/float.go | 12 +++++--- pkg/services/alerting/notifiers/base.go | 4 +++ pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/social/social.go | 14 ++++++---- pkg/tsdb/elasticsearch/response_parser.go | 29 ++++++++++++++------ pkg/tsdb/elasticsearch/time_series_query.go | 10 +++---- 12 files changed, 80 insertions(+), 40 deletions(-) diff --git a/build.go b/build.go index 9502f52be11..69fbf3bada8 100644 --- a/build.go +++ b/build.go @@ -22,6 +22,11 @@ import ( "time" ) +const ( + windows = "windows" + linux = "linux" +) + var ( //versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`) goarch string @@ -110,13 +115,13 @@ func main() { case "package": grunt(gruntBuildArg("build")...) grunt(gruntBuildArg("package")...) - if goos == "linux" { + if goos == linux { createLinuxPackages() } case "package-only": grunt(gruntBuildArg("package")...) - if goos == "linux" { + if goos == linux { createLinuxPackages() } @@ -378,7 +383,7 @@ func ensureGoPath() { } func grunt(params ...string) { - if runtime.GOOS == "windows" { + if runtime.GOOS == windows { runPrint(`.\node_modules\.bin\grunt`, params...) } else { runPrint("./node_modules/.bin/grunt", params...) @@ -420,7 +425,7 @@ func build(binaryName, pkg string, tags []string) { binary = fmt.Sprintf("./bin/%s", binaryName) } - if goos == "windows" { + if goos == windows { binary += ".exe" } @@ -484,11 +489,11 @@ func clean() { func setBuildEnv() { os.Setenv("GOOS", goos) - if goos == "windows" { + if goos == windows { // require windows >=7 os.Setenv("CGO_CFLAGS", "-D_WIN32_WINNT=0x0601") } - if goarch != "amd64" || goos != "linux" { + if goarch != "amd64" || goos != linux { // needed for all other archs cgo = true } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index c2ab6dd9a1a..d65598f6e5e 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -22,6 +22,10 @@ import ( "github.com/grafana/grafana/pkg/util" ) +const ( + anonString = "Anonymous" +) + func isDashboardStarredByUser(c *m.ReqContext, dashID int64) (bool, error) { if !c.IsSignedIn { return false, nil @@ -64,7 +68,7 @@ func GetDashboard(c *m.ReqContext) Response { } // Finding creator and last updater of the dashboard - updater, creator := "Anonymous", "Anonymous" + updater, creator := anonString, anonString if dash.UpdatedBy > 0 { updater = getUserLogin(dash.UpdatedBy) } @@ -128,7 +132,7 @@ func getUserLogin(userID int64) string { query := m.GetUserByIdQuery{Id: userID} err := bus.Dispatch(&query) if err != nil { - return "Anonymous" + return anonString } return query.Result.Login } @@ -403,7 +407,7 @@ func GetDashboardVersion(c *m.ReqContext) Response { return Error(500, fmt.Sprintf("Dashboard version %d not found for dashboardId %d", query.Version, dashID), err) } - creator := "Anonymous" + creator := anonString if query.Result.CreatedBy > 0 { creator = getUserLogin(query.Result.CreatedBy) } diff --git a/pkg/api/folder.go b/pkg/api/folder.go index f0cdff24d20..0e08343b556 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -95,7 +95,7 @@ func toFolderDto(g guardian.DashboardGuardian, folder *m.Folder) dtos.Folder { canAdmin, _ := g.CanAdmin() // Finding creator and last updater of the folder - updater, creator := "Anonymous", "Anonymous" + updater, creator := anonString, anonString if folder.CreatedBy > 0 { creator = getUserLogin(folder.CreatedBy) } diff --git a/pkg/api/index.go b/pkg/api/index.go index b8101a01fc8..1b73acd8829 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -11,6 +11,12 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +const ( + // Themes + lightName = "light" + darkName = "dark" +) + func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { settings, err := getFrontendSettingsMap(c) if err != nil { @@ -60,7 +66,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { OrgRole: c.OrgRole, GravatarUrl: dtos.GetGravatarUrl(c.Email), IsGrafanaAdmin: c.IsGrafanaAdmin, - LightTheme: prefs.Theme == "light", + LightTheme: prefs.Theme == lightName, Timezone: prefs.Timezone, Locale: locale, HelpFlags1: c.HelpFlags1, @@ -88,12 +94,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { } themeURLParam := c.Query("theme") - if themeURLParam == "light" { + if themeURLParam == lightName { data.User.LightTheme = true - data.Theme = "light" - } else if themeURLParam == "dark" { + data.Theme = lightName + } else if themeURLParam == darkName { data.User.LightTheme = false - data.Theme = "dark" + data.Theme = darkName } if hasEditPermissionInFoldersQuery.Result { diff --git a/pkg/components/null/float.go b/pkg/components/null/float.go index 4f783f2c584..9082c831084 100644 --- a/pkg/components/null/float.go +++ b/pkg/components/null/float.go @@ -8,6 +8,10 @@ import ( "strconv" ) +const ( + nullString = "null" +) + // Float is a nullable float64. // It does not consider zero values to be null. // It will decode to null, not zero, if null. @@ -68,7 +72,7 @@ func (f *Float) UnmarshalJSON(data []byte) error { // It will return an error if the input is not an integer, blank, or "null". func (f *Float) UnmarshalText(text []byte) error { str := string(text) - if str == "" || str == "null" { + if str == "" || str == nullString { f.Valid = false return nil } @@ -82,7 +86,7 @@ func (f *Float) UnmarshalText(text []byte) error { // It will encode null if this Float is null. func (f Float) MarshalJSON() ([]byte, error) { if !f.Valid { - return []byte("null"), nil + return []byte(nullString), nil } return []byte(strconv.FormatFloat(f.Float64, 'f', -1, 64)), nil } @@ -100,7 +104,7 @@ func (f Float) MarshalText() ([]byte, error) { // It will encode a blank string if this Float is null. func (f Float) String() string { if !f.Valid { - return "null" + return nullString } return fmt.Sprintf("%1.3f", f.Float64) @@ -109,7 +113,7 @@ func (f Float) String() string { // FullString returns float as string in full precision func (f Float) FullString() string { if !f.Valid { - return "null" + return nullString } return fmt.Sprintf("%f", f.Float64) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index ca011356247..3cf04719577 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -11,6 +11,10 @@ import ( "github.com/grafana/grafana/pkg/services/alerting" ) +const ( + triggMetrString = "Triggered metrics:\n\n" +) + type NotifierBase struct { Name string Type string diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index d8d19fc5dae..a8a424c87a7 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -61,7 +61,7 @@ func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { state := evalContext.Rule.State - customData := "Triggered metrics:\n\n" + customData := triggMetrString for _, evt := range evalContext.EvalMatches { customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value) } diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 84148a0d99c..629968b5102 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -95,7 +95,7 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err return err } - customData := "Triggered metrics:\n\n" + customData := triggMetrString for _, evt := range evalContext.EvalMatches { customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value) } diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index bf85466388f..9f6ce3c2dc8 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -76,7 +76,7 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Rule.State == m.AlertStateOK { eventType = "resolve" } - customData := "Triggered metrics:\n\n" + customData := triggMetrString for _, evt := range evalContext.EvalMatches { customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value) } diff --git a/pkg/social/social.go b/pkg/social/social.go index 721070ab789..8918507f3b9 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -46,10 +46,14 @@ func (e *Error) Error() string { return e.s } +const ( + grafanaCom = "grafana_com" +) + var ( SocialBaseUrl = "/login/" SocialMap = make(map[string]SocialConnector) - allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} + allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom} ) func NewOAuthService() { @@ -82,7 +86,7 @@ func NewOAuthService() { } if name == "grafananet" { - name = "grafana_com" + name = grafanaCom } setting.OAuthService.OAuthInfos[name] = info @@ -159,7 +163,7 @@ func NewOAuthService() { } } - if name == "grafana_com" { + if name == grafanaCom { config = oauth2.Config{ ClientID: info.ClientId, ClientSecret: info.ClientSecret, @@ -171,7 +175,7 @@ func NewOAuthService() { Scopes: info.Scopes, } - SocialMap["grafana_com"] = &SocialGrafanaCom{ + SocialMap[grafanaCom] = &SocialGrafanaCom{ SocialBase: &SocialBase{ Config: &config, log: logger, @@ -194,7 +198,7 @@ var GetOAuthProviders = func(cfg *setting.Cfg) map[string]bool { for _, name := range allOauthes { if name == "grafananet" { - name = "grafana_com" + name = grafanaCom } sec := cfg.Raw.Section("auth." + name) diff --git a/pkg/tsdb/elasticsearch/response_parser.go b/pkg/tsdb/elasticsearch/response_parser.go index 0090754840a..0837c3dd9d5 100644 --- a/pkg/tsdb/elasticsearch/response_parser.go +++ b/pkg/tsdb/elasticsearch/response_parser.go @@ -13,6 +13,19 @@ import ( "github.com/grafana/grafana/pkg/tsdb/elasticsearch/client" ) +const ( + // Metric types + countType = "count" + percentilesType = "percentiles" + extendedStatsType = "extended_stats" + // Bucket types + dateHistType = "date_histogram" + histogramType = "histogram" + filtersType = "filters" + termsType = "terms" + geohashGridType = "geohash_grid" +) + type responseParser struct { Responses []*es.SearchResponse Targets []*Query @@ -81,7 +94,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu } if depth == maxDepth { - if aggDef.Type == "date_histogram" { + if aggDef.Type == dateHistType { err = rp.processMetrics(esAgg, target, series, props) } else { err = rp.processAggregationDocs(esAgg, aggDef, target, table, props) @@ -149,7 +162,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query, } switch metric.Type { - case "count": + case countType: newSeries := tsdb.TimeSeries{ Tags: make(map[string]string), } @@ -164,10 +177,10 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query, for k, v := range props { newSeries.Tags[k] = v } - newSeries.Tags["metric"] = "count" + newSeries.Tags["metric"] = countType *series = append(*series, &newSeries) - case "percentiles": + case percentilesType: buckets := esAgg.Get("buckets").MustArray() if len(buckets) == 0 { break @@ -198,7 +211,7 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query, } *series = append(*series, &newSeries) } - case "extended_stats": + case extendedStatsType: buckets := esAgg.Get("buckets").MustArray() metaKeys := make([]string, 0) @@ -312,9 +325,9 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef for _, metric := range target.Metrics { switch metric.Type { - case "count": + case countType: addMetricValue(&values, rp.getMetricName(metric.Type), castToNullFloat(bucket.Get("doc_count"))) - case "extended_stats": + case extendedStatsType: metaKeys := make([]string, 0) meta := metric.Meta.MustMap() for k := range meta { @@ -366,7 +379,7 @@ func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef func (rp *responseParser) trimDatapoints(series *tsdb.TimeSeriesSlice, target *Query) { var histogram *BucketAgg for _, bucketAgg := range target.BucketAggs { - if bucketAgg.Type == "date_histogram" { + if bucketAgg.Type == dateHistType { histogram = bucketAgg break } diff --git a/pkg/tsdb/elasticsearch/time_series_query.go b/pkg/tsdb/elasticsearch/time_series_query.go index c9bb05dd09a..fddcf3cb8b3 100644 --- a/pkg/tsdb/elasticsearch/time_series_query.go +++ b/pkg/tsdb/elasticsearch/time_series_query.go @@ -75,15 +75,15 @@ func (e *timeSeriesQuery) execute() (*tsdb.Response, error) { // iterate backwards to create aggregations bottom-down for _, bucketAgg := range q.BucketAggs { switch bucketAgg.Type { - case "date_histogram": + case dateHistType: aggBuilder = addDateHistogramAgg(aggBuilder, bucketAgg, from, to) - case "histogram": + case histogramType: aggBuilder = addHistogramAgg(aggBuilder, bucketAgg) - case "filters": + case filtersType: aggBuilder = addFiltersAgg(aggBuilder, bucketAgg) - case "terms": + case termsType: aggBuilder = addTermsAgg(aggBuilder, bucketAgg, q.Metrics) - case "geohash_grid": + case geohashGridType: aggBuilder = addGeoHashGridAgg(aggBuilder, bucketAgg) } } From 9774ec0ad75516a5b3e5151a07712fad8ae998b4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 24 Sep 2018 09:41:11 +0200 Subject: [PATCH 113/127] devenv: adds script for creating many dashboards with alerts --- .gitignore | 1 + .../bulk_alerting_dashboards.yaml | 9 + .../bulkdash_alerting.jsonnet | 168 ++++++++++++++++++ devenv/setup.sh | 23 ++- 4 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 devenv/bulk_alerting_dashboards/bulk_alerting_dashboards.yaml create mode 100644 devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet diff --git a/.gitignore b/.gitignore index 78b8d075ef6..08525d92519 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ debug.test *.orig /devenv/bulk-dashboards/*.json +/devenv/bulk_alerting_dashboards/*.json diff --git a/devenv/bulk_alerting_dashboards/bulk_alerting_dashboards.yaml b/devenv/bulk_alerting_dashboards/bulk_alerting_dashboards.yaml new file mode 100644 index 00000000000..1ede5dcd30a --- /dev/null +++ b/devenv/bulk_alerting_dashboards/bulk_alerting_dashboards.yaml @@ -0,0 +1,9 @@ +apiVersion: 1 + +providers: + - name: 'Bulk alerting dashboards' + folder: 'Bulk alerting dashboards' + type: file + options: + path: devenv/bulk_alerting_dashboards + diff --git a/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet b/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet new file mode 100644 index 00000000000..daa362b3ced --- /dev/null +++ b/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet @@ -0,0 +1,168 @@ +{ + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "links": [], + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 65 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "frequency": "10s", + "handler": 1, + "name": "bulk alerting", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "Prometheus", + "fill": 1, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "$$hashKey": "object:117", + "expr": "go_goroutines", + "format": "time_series", + "intervalFactor": 1, + "refId": "A" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 50 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ] + } + ], + "schemaVersion": 16, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "New dashboard", + "uid": null, + "version": 0 +} \ No newline at end of file diff --git a/devenv/setup.sh b/devenv/setup.sh index cc71ecc71bf..8b8f2d51284 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -14,6 +14,20 @@ bulkDashboard() { ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml } +bulkAlertingDashboard() { + + requiresJsonnet + + COUNTER=0 + MAX=100 + while [ $COUNTER -lt $MAX ]; do + jsonnet -o "bulk_alerting_dashboards/alerting_dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk_alerting_dashboards/bulkdash_alerting.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'alerting-title-${COUNTER}' }" + let COUNTER=COUNTER+1 + done + + ln -s -f -r ./bulk_alerting_dashboards/bulk_alerting_dashboards.yaml ../conf/provisioning/dashboards/custom.yaml +} + requiresJsonnet() { if ! type "jsonnet" > /dev/null; then echo "you need you install jsonnet to run this script" @@ -36,8 +50,9 @@ devDatasources() { usage() { echo -e "\n" echo "Usage:" - echo " bulk-dashboards - create and provisioning 400 dashboards" - echo " no args - provisiong core datasources and dev dashboards" + echo " bulk-dashboards - create and provisioning 400 dashboards" + echo " bulk-alerting-dashboards - create and provisioning 400 dashboards with alerts" + echo " no args - provisiong core datasources and dev dashboards" } main() { @@ -48,7 +63,9 @@ main() { local cmd=$1 - if [[ $cmd == "bulk-dashboards" ]]; then + if [[ $cmd == "bulk-alerting-dashboards" ]]; then + bulkAlertingDashboard + elif [[ $cmd == "bulk-dashboards" ]]; then bulkDashboard else devDashboards From fd5acdd857fe32915de908a7cf442cd340c354e2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 24 Sep 2018 10:59:39 +0200 Subject: [PATCH 114/127] target gfdev-prometheus datasource --- devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet b/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet index daa362b3ced..a7acd57745d 100644 --- a/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet +++ b/devenv/bulk_alerting_dashboards/bulkdash_alerting.jsonnet @@ -43,7 +43,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "Prometheus", + "datasource": "gdev-prometheus", "fill": 1, "gridPos": { "h": 9, From d07a3a7637fde223bf8878c1158613bc83a8bc9b Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 24 Sep 2018 12:16:06 +0200 Subject: [PATCH 115/127] Explore: moved code to app/features/explore --- .../{containers/Explore => features/explore}/ElapsedTime.tsx | 0 public/app/{containers/Explore => features/explore}/Explore.tsx | 0 public/app/{containers/Explore => features/explore}/Graph.tsx | 0 .../app/{containers/Explore => features/explore}/JSONViewer.tsx | 0 public/app/{containers/Explore => features/explore}/Legend.tsx | 0 public/app/{containers/Explore => features/explore}/Logs.tsx | 0 .../Explore => features/explore}/PromQueryField.test.tsx | 0 .../{containers/Explore => features/explore}/PromQueryField.tsx | 0 .../app/{containers/Explore => features/explore}/QueryField.tsx | 0 .../app/{containers/Explore => features/explore}/QueryRows.tsx | 0 public/app/{containers/Explore => features/explore}/Table.tsx | 0 .../Explore => features/explore}/TimePicker.test.tsx | 0 .../app/{containers/Explore => features/explore}/TimePicker.tsx | 0 .../app/{containers/Explore => features/explore}/Typeahead.tsx | 0 public/app/{containers/Explore => features/explore}/Value.ts | 0 public/app/{containers/Explore => features/explore}/Wrapper.tsx | 0 .../Explore => features/explore}/slate-plugins/braces.test.ts | 0 .../Explore => features/explore}/slate-plugins/braces.ts | 0 .../Explore => features/explore}/slate-plugins/clear.test.ts | 0 .../Explore => features/explore}/slate-plugins/clear.ts | 0 .../Explore => features/explore}/slate-plugins/newline.ts | 0 .../Explore => features/explore}/slate-plugins/prism/promql.ts | 0 .../Explore => features/explore}/slate-plugins/runner.ts | 0 .../{containers/Explore => features/explore}/utils/debounce.ts | 0 .../app/{containers/Explore => features/explore}/utils/dom.ts | 0 .../Explore => features/explore}/utils/prometheus.test.ts | 0 .../Explore => features/explore}/utils/prometheus.ts | 0 .../app/{containers/Explore => features/explore}/utils/query.ts | 0 public/app/routes/routes.ts | 2 +- 29 files changed, 1 insertion(+), 1 deletion(-) rename public/app/{containers/Explore => features/explore}/ElapsedTime.tsx (100%) rename public/app/{containers/Explore => features/explore}/Explore.tsx (100%) rename public/app/{containers/Explore => features/explore}/Graph.tsx (100%) rename public/app/{containers/Explore => features/explore}/JSONViewer.tsx (100%) rename public/app/{containers/Explore => features/explore}/Legend.tsx (100%) rename public/app/{containers/Explore => features/explore}/Logs.tsx (100%) rename public/app/{containers/Explore => features/explore}/PromQueryField.test.tsx (100%) rename public/app/{containers/Explore => features/explore}/PromQueryField.tsx (100%) rename public/app/{containers/Explore => features/explore}/QueryField.tsx (100%) rename public/app/{containers/Explore => features/explore}/QueryRows.tsx (100%) rename public/app/{containers/Explore => features/explore}/Table.tsx (100%) rename public/app/{containers/Explore => features/explore}/TimePicker.test.tsx (100%) rename public/app/{containers/Explore => features/explore}/TimePicker.tsx (100%) rename public/app/{containers/Explore => features/explore}/Typeahead.tsx (100%) rename public/app/{containers/Explore => features/explore}/Value.ts (100%) rename public/app/{containers/Explore => features/explore}/Wrapper.tsx (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/braces.test.ts (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/braces.ts (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/clear.test.ts (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/clear.ts (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/newline.ts (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/prism/promql.ts (100%) rename public/app/{containers/Explore => features/explore}/slate-plugins/runner.ts (100%) rename public/app/{containers/Explore => features/explore}/utils/debounce.ts (100%) rename public/app/{containers/Explore => features/explore}/utils/dom.ts (100%) rename public/app/{containers/Explore => features/explore}/utils/prometheus.test.ts (100%) rename public/app/{containers/Explore => features/explore}/utils/prometheus.ts (100%) rename public/app/{containers/Explore => features/explore}/utils/query.ts (100%) diff --git a/public/app/containers/Explore/ElapsedTime.tsx b/public/app/features/explore/ElapsedTime.tsx similarity index 100% rename from public/app/containers/Explore/ElapsedTime.tsx rename to public/app/features/explore/ElapsedTime.tsx diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/features/explore/Explore.tsx similarity index 100% rename from public/app/containers/Explore/Explore.tsx rename to public/app/features/explore/Explore.tsx diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/features/explore/Graph.tsx similarity index 100% rename from public/app/containers/Explore/Graph.tsx rename to public/app/features/explore/Graph.tsx diff --git a/public/app/containers/Explore/JSONViewer.tsx b/public/app/features/explore/JSONViewer.tsx similarity index 100% rename from public/app/containers/Explore/JSONViewer.tsx rename to public/app/features/explore/JSONViewer.tsx diff --git a/public/app/containers/Explore/Legend.tsx b/public/app/features/explore/Legend.tsx similarity index 100% rename from public/app/containers/Explore/Legend.tsx rename to public/app/features/explore/Legend.tsx diff --git a/public/app/containers/Explore/Logs.tsx b/public/app/features/explore/Logs.tsx similarity index 100% rename from public/app/containers/Explore/Logs.tsx rename to public/app/features/explore/Logs.tsx diff --git a/public/app/containers/Explore/PromQueryField.test.tsx b/public/app/features/explore/PromQueryField.test.tsx similarity index 100% rename from public/app/containers/Explore/PromQueryField.test.tsx rename to public/app/features/explore/PromQueryField.test.tsx diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/features/explore/PromQueryField.tsx similarity index 100% rename from public/app/containers/Explore/PromQueryField.tsx rename to public/app/features/explore/PromQueryField.tsx diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx similarity index 100% rename from public/app/containers/Explore/QueryField.tsx rename to public/app/features/explore/QueryField.tsx diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx similarity index 100% rename from public/app/containers/Explore/QueryRows.tsx rename to public/app/features/explore/QueryRows.tsx diff --git a/public/app/containers/Explore/Table.tsx b/public/app/features/explore/Table.tsx similarity index 100% rename from public/app/containers/Explore/Table.tsx rename to public/app/features/explore/Table.tsx diff --git a/public/app/containers/Explore/TimePicker.test.tsx b/public/app/features/explore/TimePicker.test.tsx similarity index 100% rename from public/app/containers/Explore/TimePicker.test.tsx rename to public/app/features/explore/TimePicker.test.tsx diff --git a/public/app/containers/Explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx similarity index 100% rename from public/app/containers/Explore/TimePicker.tsx rename to public/app/features/explore/TimePicker.tsx diff --git a/public/app/containers/Explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx similarity index 100% rename from public/app/containers/Explore/Typeahead.tsx rename to public/app/features/explore/Typeahead.tsx diff --git a/public/app/containers/Explore/Value.ts b/public/app/features/explore/Value.ts similarity index 100% rename from public/app/containers/Explore/Value.ts rename to public/app/features/explore/Value.ts diff --git a/public/app/containers/Explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx similarity index 100% rename from public/app/containers/Explore/Wrapper.tsx rename to public/app/features/explore/Wrapper.tsx diff --git a/public/app/containers/Explore/slate-plugins/braces.test.ts b/public/app/features/explore/slate-plugins/braces.test.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/braces.test.ts rename to public/app/features/explore/slate-plugins/braces.test.ts diff --git a/public/app/containers/Explore/slate-plugins/braces.ts b/public/app/features/explore/slate-plugins/braces.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/braces.ts rename to public/app/features/explore/slate-plugins/braces.ts diff --git a/public/app/containers/Explore/slate-plugins/clear.test.ts b/public/app/features/explore/slate-plugins/clear.test.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/clear.test.ts rename to public/app/features/explore/slate-plugins/clear.test.ts diff --git a/public/app/containers/Explore/slate-plugins/clear.ts b/public/app/features/explore/slate-plugins/clear.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/clear.ts rename to public/app/features/explore/slate-plugins/clear.ts diff --git a/public/app/containers/Explore/slate-plugins/newline.ts b/public/app/features/explore/slate-plugins/newline.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/newline.ts rename to public/app/features/explore/slate-plugins/newline.ts diff --git a/public/app/containers/Explore/slate-plugins/prism/promql.ts b/public/app/features/explore/slate-plugins/prism/promql.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/prism/promql.ts rename to public/app/features/explore/slate-plugins/prism/promql.ts diff --git a/public/app/containers/Explore/slate-plugins/runner.ts b/public/app/features/explore/slate-plugins/runner.ts similarity index 100% rename from public/app/containers/Explore/slate-plugins/runner.ts rename to public/app/features/explore/slate-plugins/runner.ts diff --git a/public/app/containers/Explore/utils/debounce.ts b/public/app/features/explore/utils/debounce.ts similarity index 100% rename from public/app/containers/Explore/utils/debounce.ts rename to public/app/features/explore/utils/debounce.ts diff --git a/public/app/containers/Explore/utils/dom.ts b/public/app/features/explore/utils/dom.ts similarity index 100% rename from public/app/containers/Explore/utils/dom.ts rename to public/app/features/explore/utils/dom.ts diff --git a/public/app/containers/Explore/utils/prometheus.test.ts b/public/app/features/explore/utils/prometheus.test.ts similarity index 100% rename from public/app/containers/Explore/utils/prometheus.test.ts rename to public/app/features/explore/utils/prometheus.test.ts diff --git a/public/app/containers/Explore/utils/prometheus.ts b/public/app/features/explore/utils/prometheus.ts similarity index 100% rename from public/app/containers/Explore/utils/prometheus.ts rename to public/app/features/explore/utils/prometheus.ts diff --git a/public/app/containers/Explore/utils/query.ts b/public/app/features/explore/utils/query.ts similarity index 100% rename from public/app/containers/Explore/utils/query.ts rename to public/app/features/explore/utils/query.ts diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 160250dce96..015b4ae0b51 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -116,7 +116,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { template: '', resolve: { roles: () => ['Editor', 'Admin'], - component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Wrapper'), + component: () => import(/* webpackChunkName: "explore" */ 'app/features/explore/Wrapper'), }, }) .when('/org', { From 30fe407e8e2ee442a5e75cf9d60c044acd809df9 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 24 Sep 2018 12:44:37 +0200 Subject: [PATCH 116/127] devenv: fix uid for bulk alert dashboards --- devenv/setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devenv/setup.sh b/devenv/setup.sh index 8b8f2d51284..7b5499a9f52 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -21,7 +21,7 @@ bulkAlertingDashboard() { COUNTER=0 MAX=100 while [ $COUNTER -lt $MAX ]; do - jsonnet -o "bulk_alerting_dashboards/alerting_dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk_alerting_dashboards/bulkdash_alerting.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'alerting-title-${COUNTER}' }" + jsonnet -o "bulk_alerting_dashboards/alerting_dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk_alerting_dashboards/bulkdash_alerting.jsonnet'; bulkDash + { uid: 'bd-${COUNTER}', title: 'alerting-title-${COUNTER}' }" let COUNTER=COUNTER+1 done From 4dab595ed78feb67b8e79e8d89ef7bbf63df7eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 24 Sep 2018 15:58:22 +0200 Subject: [PATCH 117/127] rendering: Added concurrent rendering limits --- conf/defaults.ini | 2 ++ conf/sample.ini | 2 ++ pkg/services/alerting/notifier.go | 1 + pkg/services/alerting/result_handler.go | 2 +- pkg/services/rendering/interface.go | 1 + pkg/services/rendering/rendering.go | 33 ++++++++++++++---- pkg/setting/setting.go | 14 +++++--- public/img/rendering_error.png | Bin 0 -> 3161 bytes public/img/rendering_limit.png | Bin 0 -> 3859 bytes public/img/rendering_plugin_not_installed.png | Bin 0 -> 3651 bytes public/img/rendering_timeout.png | Bin 0 -> 3382 bytes 11 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 public/img/rendering_error.png create mode 100644 public/img/rendering_limit.png create mode 100644 public/img/rendering_plugin_not_installed.png create mode 100644 public/img/rendering_timeout.png diff --git a/conf/defaults.ini b/conf/defaults.ini index 15b8927e65a..caccebbd910 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -550,3 +550,5 @@ container_name = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer server_url = callback_url = +concurrent_limit = 10 +concurrent_limit_alerting = 5 diff --git a/conf/sample.ini b/conf/sample.ini index 2ef254f79b9..7a460faca0e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -471,3 +471,5 @@ log_queries = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer ;server_url = ;callback_url = +;concurrent_limit = 10 +;concurrent_limit_alerting = 5 diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 7fbd956f4f9..839893f3444 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -113,6 +113,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { Timeout: alertTimeout / 2, OrgId: context.Rule.OrgId, OrgRole: m.ROLE_ADMIN, + IsAlert: true, } ref, err := context.GetDashboardUID() diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 363d06d1132..893cca948f9 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -100,7 +100,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } } - handler.notifier.SendIfNeeded(evalContext) + handler.notifier.SendIfNeeded(evalContext) return nil } diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 85c139cfc04..856e6e683ff 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -22,6 +22,7 @@ type Opts struct { Path string Encoding string Timezone string + IsAlert bool } type RenderResult struct { diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index ecef83d74d9..2b9d91771e9 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -24,12 +24,13 @@ func init() { } type RenderingService struct { - log log.Logger - pluginClient *plugin.Client - grpcPlugin pluginModel.RendererPlugin - pluginInfo *plugins.RendererPlugin - renderAction renderFunc - domain string + log log.Logger + pluginClient *plugin.Client + grpcPlugin pluginModel.RendererPlugin + pluginInfo *plugins.RendererPlugin + renderAction renderFunc + domain string + inProgressCount int Cfg *setting.Cfg `inject:""` } @@ -89,7 +90,27 @@ func (rs *RenderingService) Run(ctx context.Context) error { return err } +func (rs *RenderingService) getLimit(isAlerting bool) int { + if isAlerting { + return rs.Cfg.RendererLimitAlerting + } else { + return rs.Cfg.RendererLimit + } +} + func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) { + if rs.inProgressCount > rs.getLimit(opts.IsAlert) { + return &RenderResult{ + FilePath: filepath.Join(setting.HomePath, "public/img/rendering_limit.png"), + }, nil + } + + defer func() { + rs.inProgressCount -= 1 + }() + + rs.inProgressCount += 1 + if rs.renderAction != nil { return rs.renderAction(ctx, opts) } else { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 1a253b9b238..71e499f9298 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -196,10 +196,13 @@ type Cfg struct { Smtp SmtpSettings // Rendering - ImagesDir string - PhantomDir string - RendererUrl string - RendererCallbackUrl string + ImagesDir string + PhantomDir string + RendererUrl string + RendererCallbackUrl string + RendererLimit int + RendererLimitAlerting int + DisableBruteForceLoginProtection bool TempDataLifetime time.Duration @@ -645,6 +648,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { // Rendering renderSec := iniFile.Section("rendering") + cfg.RendererLimit = renderSec.Key("concurrent_limit").MustInt(10) + cfg.RendererLimitAlerting = renderSec.Key("concurrent_limit").MustInt(5) + cfg.RendererUrl = renderSec.Key("server_url").String() cfg.RendererCallbackUrl = renderSec.Key("callback_url").String() if cfg.RendererCallbackUrl == "" { diff --git a/public/img/rendering_error.png b/public/img/rendering_error.png new file mode 100644 index 0000000000000000000000000000000000000000..cc327c267be865451f631737dbcbb7eda1af4a71 GIT binary patch literal 3161 zcmZ{mc{J1w7stogVi;zO8DoZq;m0py_bQZ-Fg=wllRe9jElW*yS&9)tmMmE+Yb9br zX~@>Y*qSJ2MoB`XC{a97UcKje|9H-M-}}e++;czokMAG%+N7)c!C4fH)j(8_ow1Wwxa-Bme{efWze-2f%GfxaE$* zMc8miULVgNBe(&Fm;&Gm9I_1;oZL|)Y7q_rlohM7NC2P!%7jU*!VFfyR1P){hk$?< zi6GG>ueySk+mIC2RaR}|@;GgDKv}L10RaF5LjWuyf=mJkC`vaW`HQ6ZdbvC{UTI^; zjBQRL5o95tLABftql!D2Ww>C>sH--tohIL9KeB z<5$b+&H+s697e=Aj>we7o61YLVuiX80!#_W3>;EE!W-do=t*==RiLXfNL5xfCCE%l zBNnB^>(F9UtVlIlgd!_U!wAes@-=gKW25y$t(7YKqX(}$1AudH9>va=%S{Pc)eO|e zU%wVJRvYR~^=Y|meK*)B&uwp(Eh$1#g}GaWftP2=;LGu{ML3ysyfkx^m)gN|yUrq~ zFkkjHmloDVrj@5$yb%_kwcq1PRT@1dihesQCNC-YZdyQjl!LFEgFfJ3uR$>)+a4tw zM_LWHKi^l2_P@Kl>yIFKEKRWYfsHIwRT+ut4AM5QvBz+1%7Va^e%W)mrf^s&nPx@NqAl)H!>*43U$$SFK2d zkvl>yVJH$dJjN7(a|<-d{wQJ6CJ`M{Fh_E5aERb+z6tnTF)%vWq0| zh7_&~l_}WsvH28|B+YjHN5*v1#VUqMjBa9?V?xbdcl?mDMTRm1JP{Ua=v|vHcn>8% z27Tyu^?FOgO8?9X=^?+4&I7Kv)?9eFakb!Gf0!HEgkIC!fLx5`+ugG`#F+K%sOIRi zb0w!*)M{T2F(T~-i#e_d5|+mA0UIQpEz*yr2+iPWj1k=xIEkk;1L z8ucfaAv!{Jg29?!2S`Klr>0aloqW!Mk5)TBtEF3-_V#uZU^08MDFp=0l3(TOTE^_h zwn@gdA^sOfTpk@$LXtCmVngmPtn5{a-~95-pnj6iTkN#<^+#`-;VbRgl(+_87Td6+ z^PY~*+;T{=@fe!AsoORgJ&!CWM;>$GMkUyIa#3HM6Jq;qO6BHs`V9LL<(srbPLCNa zL9qfn;^i}RDt1pqe`dB&VYbaJmtu@7^iVe2^LEp7pTw?2h0LJ9WWAzxC> zNsESUJ&FB(;IOadwV^zh^>6A{KR!9G#!WLOx{p{H-+Dl7@ld)}%AnQ`<(8gX{dD6E zbQ0Vvkvwpm96n4(H^l>ld%1jH9IaA{u6;#!$z&@{5Gl4t0Gg_ps$uTdG~t|IzF_fVvObX4G;z}1ByemI zx7Oo)L8#+K4_i2Ndiz|f4$;o0ICy9aGZfl7|2mfzM|&?|UG__OSpD1m-ol!AhlMEc zP|qCh409qT&Oq~Xgxs1S_aJCxIhlXo@k4mNL%q~r$c_p8$F(KTIz#fNQK3rBcrI$m z$VD+MEy!-ACm_7f@lmnA1G7D4*d;a7J!n7rm+OGQ(yL2@aupw+wp>u=W;S+uEgeZA z{#4gL<-P&?Gj{@Y$qb}r1i2Wo%r=RWY0HgY=FNP^GPUa8RLQ@Q-(zv!5AAOK!JU09 zAf5A4{>g__`f_|cO-J_xobZv!EIU{w4d@l>`Q7|9U;lFE^@#r?M-)jc(@}4W{ERmG zY`LFfL|0#5+*kIoGI8&GCv?Hp&uF$%sFSr%6|%8eL1ZLcD%u@K|wl9=kaI$MR2+d&RivO1>8W z?7LOo&ERg!UTRm+56i?isZP_`i8+jrQsaY;w60U@So>OArHA7%0j{Co>Bs5Km6@%;m)i zwZ8kd)Mb~Z2-~w!#ovDFS;y&D5-O&WNP#-iR!6;7iFHb-bWfuO!T3f zN_lv;m!HJmcjJho1yYWo9}C9W4bw_UyTC@vT^;=w)sdkWc*U5wimobT(Zj1T4>ZLn zeuo(eFWY-7lHN)C?GwvzdIH(A;A<;*=wtEE%eXExzB|DUH(xGzO`2D`ahs#fJa>+( z-)aUAQF!~O=K?c7jH)2GwPCPKS}SYb^*D&yB~KnMS#jK`%S*|>K6$Nz{#Y4i<)aiX zy(Du&Z+6wl@kF$?(P-5@558B`%CK8`dImEdT_4Dd2DKD`;PjmA7l{5+$z(wE3#Mid z9vojHafzm%R~w(7PA2chwxmddv#$aIdOTl@R(rc!{ega}CVYu>=5I?Psc*?(*PrTTjl6`0kJ?yyR6 z98m;=9-o?`dY#N&HB$oSlWmSL8wm#toZd?3lRRG5ws>}3I3+2(s*C?^XXdM8DCYgj z?oT|U<4_w^@5(H2Tn{zlars)ZBbMrE=IN!K;WvFLp~}K*Mi7hLF7#|VU)`RGiC?u}Pk-oNAP9b&`Exnd=da2* z3eBlO;?Tn=>-aOnW+CBol;)b(#l@iizjAnIt>YGsXuueeK29BaYT+`E@6}mE`5#4l zH=gRyDM`+&cX$6+`X#5(2*s4gm?#%-&m{e`{qyIIwUWW5RWIy^Ej10;YSbj3 z^EZ-^$raq&clorb9kydwIPll|oWbtSQkKZC7coTdCtucM2JAg@Hmia~z`la}4}Kmn zYaWNk+~GaRUaefm-<9j>oSpFPp!Kb4UG_DK+H`1VUf1gX?tk7g)FI8=G$)dJZfe4A zaV0&3(R}A@jCn|DdP?@h$b;Pe(;KbJ49Qlh`%whqaG!wFhVLhHlvHrV88y*6c3RVw zZ9|j8HRnw-$n%t*qzeasKmY$H5^nr`RZOmSd=urj|DSD^`de|gX-+(?-;{j#xOu&q H$HjjG5OI== literal 0 HcmV?d00001 diff --git a/public/img/rendering_limit.png b/public/img/rendering_limit.png new file mode 100644 index 0000000000000000000000000000000000000000..f2ba9aad0ba8047d6d770694dc6fb8fff9a4c646 GIT binary patch literal 3859 zcmb_ecTm&I+71K=QhpLj5=a7}{YU^oKrsP11OcfM@gP!_A}AsV7CDx-Lk(wYi;R{%(q3PBgd$0`$e(y}HXVR0|t z8UV2X33*BcCUS2mf^Q6f*ntECkWelw#e>5aQ8-WqyDb6Blj2)JVwpGss3cPkhin6U zoS7FOsO`mg!I@!dDxuZM$g!^NUxpkr|UIyQ&H>F3Nm)xT2LmF7@(ZoiiX zAxR0(P{ecPl=QG-L9!y=D4`^2zEsxi^S&ldng89>+HSkPuu6JL7zRSvO;)H?0#ZCY zJ2)|8O>3VXZPoFu8E9l^o-P6{=yA8=L2d6pPn@J#sy8U8F>$KXXeBNZn>bSa zu*SQ#CLB9#L=Mm)get1!DX1{8iWn@`1c$OwKzd{0Tp5Wdl(-vOEVOPm`CgX`wT#n$ zH-?tmQT9n^(A)}!< zCoe2A?Lo3P{gQ{3t##OOQn01w7+UGFJoXkE!H`3gqa^ZBqD9YMxaSXqG1}=xEvNj` zNbbqCIrN;A@F9jz_IXOO3rRym%Z)_P2MM7V$rZR@p0q$f%?p$3EqzV7^w{{YE8%_l zKHyp8QD`&l=;zN+L-z6(K?1M)@qtKVanfL{BRz9hW12IFCyK`lMHZ2(MME}#1OWqd1S z^=oRb8`SF4z*+=NS@8*d@0gKsw3XN=Sk{jnf%t2;B7O^cI31dd_REKenF6TGUFpzx z%>H-LinNmU0&mnpl`*S!nu!=k{6Rr`*3KSE&I}OhQ{!G1j?(D0-0UO@SKao+!$@$E zIJh>3kP)#%!0;`hD4OCi#o5Rc&rJsQ*fMR;X-M9g${S6)E}nJc`R_QzPs`IuQF zx&Vd6f_c9m-huU$7ADZoDDeHE+I#ter*ltCQ3&?u+=`DsMe?IN|0&x%a@xl{YGm2A z!Rp4Gn@c^v1zUCSTHopPFe@+32gPm*YRKEz!*ouO2n z9jtl9dLw7DM4lsC3}_fTm;G5b6jL&2Z-6ajobEn-V@s&`kNNedXI5@QO=}J;A9D3m z?8zfQ``TIBRjpL&x$Nan;8oIt2|uq_clF!LfTtKYwMG{vOZ{sVaw9P2)!6YvjG?PT zC2szS&d^^c-k2+8Y?>6CIxf|+PAN6?NdSf-u+Lxicd?Q&M>TyyYafkM#wk&JI~RrB zfzcoAtZv2CvcU9Xe8C7suh<)S{4EFBZaP{E!7C+a-^ROv!v;VZJIVH02cRBfA zZjkZngpT;`%ie?IJ>1$S!zV|E*};zdC!jsH>F3{Nf>3vuTR#E0phy-^7#6Lu+ zcbD2jC3+?9LH~a|KBI4ROu!RUd{({HGBhBz(WAL^@`(B8IQ+}waK9H9qV*TdYmxS! z2HEhO8}M=MO*{@wuC>KYf`E{KN+Avl?#Q-j49)GjYUq zdEEv1tIb)R9=s#{+t)WaWq;3#%y+G)+k_nS+IY*jrO2|eO<-p|ykoqvx^|0ZsR~xiV`mr+B#VwC#md#vg2V{At z^{WiEF+aSAM7NV=o0064msFIH(S;_&CC;TP-sHU;-;@@V&O@xvgT-UGeC(dmN`tSq z1}noZ{SMI^^-b5(&?kXbUWqwv+{{=eygkocZbdy~@DT^W)@-b2wd2_^F^YGysU)obgv;X>mV)!+Xfow0 z6JK~?bIrAGb1Wk!Hm$k4D}sy;wELcwdwZelx(Z411Tu2Y?19fSzvXvB1o?s=?+aPB zKQ{e_kM!xr=Z7~m%Wsb~PFD4`5x|Pi)-7opsYT7JJay>vxw-B|Ywl%Oy z{rZQ?+bwU!+>5Jsd?iy z9@NUMFCT4T&pfTaLqdt`<|OVw67gP=s=!a@s3Gl^TS+Qr*1aVQe#S4hs0R-F?JbpS zHIAj=#1>(nzJV@{EBC};Z7vaNjUUoW0J?d|!R~{va#z8f+G2{{S*kd>H$m#m`QA>s zDlyE?@bjkum9hrjtOE}&)=jeF?x7Ru zyCu)$R4ChveBUx8k79U^R#!a~oW8`m6?K-p>_!5XW+&--+HUGf0&G~S%Ue_)4Ew-w(v;5fy_O+Q;RQ;CPKjh zR*b{13gY-hO>bnNHl|~5?tHfs325+oZdo=@{#|c*jegQ|wuYJ*=N25@F&0{&HkE8i z=qxFPKhhZ`4AGyP{k&IU^wQ+0%bkftB8qlxe&?Qc+xC0&pr`QlF8-3MDpT#=(HRVzY zqx5gQ)=7a#Xy@s!v_leS$SfY|3hK9kz@ETcbM@?nyK{OUcD~HMeXzcZ=1$TaZ1y7o(j??Ir5giQ?nN7A+ibn zl^&h!gT3Ky(*jAqG+Hd~_s#l$wF6H6`elRxeSFSl<&D%`{w~zoaafuR{&wnM zo^gHds{Ei_^`>DmbV5eUAFAhD)N9C#Qx`d#;Z{j@y0rv^3LiKO%%g~R8nsa4= zO5%7x^Q+vk%cf#Gf6Yr@LpWpj*`gL0W6MMHxHpG8F&#U`d3KWt+oUSDKY9rkFMQo+n9 z%8&rJw4n*?rixL2Hg~wk h7!E*6p#ObXK&_cRTq4=-$nI7UD|0)uhetea{SS;Mu&Mw6 literal 0 HcmV?d00001 diff --git a/public/img/rendering_plugin_not_installed.png b/public/img/rendering_plugin_not_installed.png new file mode 100644 index 0000000000000000000000000000000000000000..f135ff7cc9f9b3898396404cfe1b510fd81bbc42 GIT binary patch literal 3651 zcmZ{mc{J2*8^>obkzuSegPCD$zcHB-q6n#EDTc&j36CX2mKZyaWtwS(Xk>{jQwmw; z8H^siCX8g?LdGZ|SxZlrN)&HB=l$nB?|JV(KIcC7=Q`IP_qon>zBlZM7UH4`q971R z+{)6-9t0AA{=5&12>xWt;)ub|hiyA+hcf_xSHh~GP^di^cuz)d4+a8M<@aFwwqT$k z7>EGii2(j2fS-ba_F#g53ViKn0RZgD!1pA?EdV^AuFRX@@4-X?b!@E^XdDJwLt+Bt zAkhF`7r+y+5-Tu};mT`0~mk8-OS&4Fl$ebj&8wTnwa0dV*Y> zN`S{}2JGq0is~n^NG?)_jg;t?7I?_x*Y}Pejc5Q)QgH{bv}HLID5>m8%Wg?P#-#)c zWWhXM*F*oq-kNwHH4QdaLstc(rwlJZim~DQc_a6E+zj5sD|U8{0M53=LyMxU9*0%1 zz#_Ym@F}>gBU&r~DM*0}Cc*_v`}x{_)!mO71drlBuH^t0H>i~BfR+yz0}Nr*hfpdE zG$tJ_Q;UE(DhR)r;Ae69m90ZrRUM;!EyK?n0N})R^8?xdK0rkyT3Ov&5lK*jS)id; zk^3IBy=<$xb0>}5lu9%*Hnr9}V6TB6!T@X~wJABJHMl|u22Mf2i5RH@8L$O~bECAm zq@bSHU&?uK^I_U`-s5&=mVRsDUsc%-}*igNDT7^sUgVJ2OfqO<{ z@r@TuQq|2sZNSyq)-1>OBwJ2?1uj1amkzETEv)M%<<|2!6}+x|>yUh27yU-;^%zpT zh2|A+A6Iux*Q$^cS@!tLnm-3gH2@{yhJoeQ>a>DDP$4U`lMXSHANa<0lb&*IYZdO1 zAAC0l=Zh!PquUKGo$Y);n)0Odh!DB8vc*zT=Djpl3PUrkJOB2}``2D3R=&J9J-s@H z)1uF1EIwICNIcyhhD7YfZg4f(*Xpn<|zfc}hh2ALu7$VOehRLd*t@ zcrQD92dL*_oXQ;!COAx|6_=M<;qvXbgO>~H!-Jvb#&k=zdCIYZXCF=1u{ijVgVCTW;rM%xas*kK~bopQh+b-UD^~jiLPSSiI?jqxYXYRnY+Wpsf+UZm0;q;v+DE~4j@_w#}3YX zt<3GF*C5JoLZkh09Ifj#&%y!WipSTzl;>^CZD~``a1HDu$jm{OIyKMEL<;>HlHCSjVS%5!8f3cr=S@b{f1P$kw8$?7`qrvX{x4G{8h;i0k9xN^ zDe@mB+Uz?zY9>irvnqejtYtPc&DoK`^v-;HnfSwb&Vd4K^^>A-8Cgr#DVf+O_hL*b zC=pR7q0eFmoFq48D%YwKg<8plTm2C{zUz^*r|oS@PZxwl?`aKvpZMnFaQ+P3@gHm^ zEqO*0&yUvSjR+RUl6pK!jouoXK7L4Bfz(rp(S+<1<1J5_#;vvC$4CBH8*C-qYl3+Z z4XU>9%POIJt;`?4v!vbJ#`w%qN0tudLe9@CRR@xK*e=I#(?8U4sBr+Jh%_+Q(q3j1=|Dd6Ja}>9`miSfeh`K9ZcR!wgndk+x z4rCiI)QO#B+=_~iQdl*7cJi%(zZ$PgEkwRY;!u3R&ZLpQGDo|r1KGt|e^mEi!SHIK z$8*=+bvV!8h%;lDhkN(-_n1omXN5)=LqpB(cG%|jZ3>xm(bcteX!_2%)L3P9{AWi@ ziN_#Pr|NY{v3HnS_uy5cNmjr!=Yn(htVdh^OcGUAPrbMq<3QhOpb}D}f=RtOkSUyj z-y^5Q*iSw2kuStam9J${7w7#iz0b8wW-q)eXL>EqgO6FgG@A~)Qn}HmtJm~l_E5v$ zfqQnD2QKrZGW!w-<*kShcUEK<=D5e@+_nv6J5M=E*hElsum?7*j+n?m-?=a_YvuQz zvR|;*#i|QEQ#yr}aC)lE5Gbz@f39pl!ptL`YOBzjr_va7pRs!ScH%d{;F$63(Y$&v z4jL}%Sg%x9zRa|;G5Isp8cp|6ZL}==!c+URyO@YDYi=)Ezm(!oOdsp8ubsz{S`9Cu zX$A=$h6`WYziPOfF0#m!#g}*cd-SyP%1B>g^P4ly%W9g-O-53D>Ps{;9_Zg`y88yJ zR>m=de=Xz+G$;2-OmLRwc22JoED4_PKYD8QHFU0PkRj+O!r4o~!Zs zIYIIVpQZMwLDSjfVU|eFJ@LbfHFuSixgO54MuF$KyG_xn2ojJf5!IPtn>8;U3(K!j zeIcLO7sos{LVEtbvu(tR;mERcN1|0T?q59EAq)#fuoehjyfcbJSu@Vukg}Fks{4Rv zDlf1ByFN*X**K~-=jT`BjK!S{~j3a6Y~g zrxRAzbt+EB?fY1ujiK`r6#0=>K+PIZ*ki9uUZ{R%OjV=B4=j(NiY5+x%Z}21?==5$ z^{*AT38&e#=H19cw&91~{fkgJNsp}?{$hw+$uEW0C7nQ8my%LOqorxtz7+|8@qz?~ zVA+&Y;gy9HH#*YfE6NV4<4n$%I*nrDxu{yKQ$?O%K-zPfy zGTtP3m6zc*?l=t>+TB=$c7g~Ii%|a~r#z4F21DHwEhukA=CEWN>7c$vgUQiT3HY-; zk_ffe9^uy?R}?uPEN~5P>1_Sr;B}y0|*M(POiGPQD7INU~ zKZ6b&Ylla-lV9Dg+OJPe@$k-bo}nx5qsFW}7iguK$AnuHd4@NYFJw;G`a*UyV{G!u zGsF#_{cxGdr)k@&Y1UjxV#Y5~gLKI@`rhyM`nL=$8ti;XCXQTy0@?4Bty@F*&g+yp z4_*!QiB*_b;s=mrpChv?b_qc^#dTwGIC7%9Ao79MV#Qe_#uxut^&g`h`iI3Nf`kUP zgKnc|ws)qbsL$08e*1itz-kkK{={zm|73&z7YRlb(bB*2fXMdVBwYWWWbnV3k?rY^ UD64y-2ma-(&JfL*r##9320j&!NB{r; literal 0 HcmV?d00001 diff --git a/public/img/rendering_timeout.png b/public/img/rendering_timeout.png new file mode 100644 index 0000000000000000000000000000000000000000..07a87eb5de3f7d288899b0835a5f81205d15e27c GIT binary patch literal 3382 zcmdT_X*kpi7awHLn6V9GhB3o`46d#v+gMUGrtEtnMO_lY&=65Vm=Ga52@_tGM2;lZ*Vf(W3 ztQj`p07IlN!-Vtu*?PTyXf;eChb7l$vOjFROw;>{A z1;M@uC{3nQy)fzfPJ#oC}?O^Q&CJk(KH zg7tP#*T2FDzy+#n>ZmDfLM5tX1nOqkfwj}A$!UOf8UW~T%PBIULe-P(nyyLxphi|t zF01xRP0$erQk{vwY%3s~Rber3X$A!JJBwY^GWd4t3G4TCtoe0K?JIrx9&WB4t~$5@ zggOGLVu(g8$Vt~Ji1k2(-mn;PrR}Vt3SR(Mhr$AY#)2YB2ZLn5WO@{23{=2P3gB(% z;Z%9y2-u;?8FouQGv*m{a$YK9eSAh4fVD32Ig(D)O2lZA(bygp zU|jiNS;{FYa574%SQ$0|gA~Igs}#iBdxO32(>@Ng?@F$9Wk6UhcXcc%rL4(K6P_G}aPVrKU+y!(uQR z4XS9SGI~J{yeWGy9C5Fd>fy$;fZ#MnRRlr%N`COA;J`~^-X0_)4_7@KdjQvrR7+Gx zE-1n`p<j3>C#>t(v+FnZ1o-ct1&TR)=keaR7Y*7g zmW+?!QHqDNp9O2D&sqskIu-HdSs+SZ5x!iHY!=9k?weh{OP^M<=1#jIu<}nA&pSJ| zO1&7~@)Y_Sl+gEAVgJp~=}}HErW)QP_2uF{;L{!FsgBsjt$zmPGzNdYaP%tB`rA^rR*=*uqd<2CUv&G+)o~0!hlk zml&vx$AxrV@~x!S#UEMHb-pwd6>LVnS!73oWuu?g_10VoYeau`)j~>IX!g4{$Ss5p z#b2h@oCMES=f8-};LzKbkJbyH{w;bjn#WJDyb6g8|H6^7+|3v+&Z8kFxF9{~$MDfP z=BOwC_#YSjE88V@xBO#&X$sZx>AQdn8Qv;<&bfRplHw^8%zY?2BY&fh)_>>|0Z9gH zculqnbc^m9nSSmHhW%}mk2k_gi`_wtMUBSi8n?G-kA-|;oc%lE)7tH9JbaqvHObdA z1)t$qe0?+-+&1~rrCZeIoWP81(C?B_=%Kyg@#}3Fi{A#0bj>AUztT1`mJ;0=<9ibn zPW&QIg{(?aWq#hRz;6{z7@rlUl}~UsA=#W(N^p%Q7x9w2V`4Sor@C}@iZ7?~#o=FTgN*Tr|On_1Q9;F8d%)bW~L{lP+W;TkPiX1mvJcz8Uor z6~=Y@MmHdz`j1vfRD(swppo*iBEHr4Psa}KH|W^Cu5YLZ$DQ6+c38zFZTkj3}77hlcnMcf5=9Eac~gk_sOGx(DNm zj&=VLdqUw~(xuta1LllNv#KGF`gXFt9{BFAHEf7yWYu4-cgU|FT2cv1!N+e#O}uw$ zU-lMnzBFq)0$IIcyhf!!WtH6@CEEX!)cNJ^QCrr^!v1_y`(1oJf?IPl-gH)a*&#VP zT$fPkkMvgQ%V$5@GbwND*^S0Z2jFeU%J@3)+;*8D0=CH+>kkO z?(DjIzvTRwuk%QbtprcXEW-cV!<}f0=p?*Uw*hg|DWxYqRe|{LWaAETzbe5k(-&9QwJQ+9PYnNp?}xjfq9J~JMfoJ4KDfiAuf@3~4b1B=yqT`@=T zr4(6aX2SAHA&JM{HwTYDG6Y4N_q@9u;r*M%VHEGT6Ji0yuP61K8kCN~PJr*rWc$k2 zzM~w^<*P8T51C{x+HPng$8N?u>y~ievtyi--0?dj$yMUpT(g$M6_*0up{bI9^?&vF zrM|59t4s4af0K|<(dU)7c+Q_hC@&om{JCz6Rcn+G7Sv2V(MTiIKe=6e zrnfxrbmWrmUA(aYcYL~NHWRM^vpo}z;~r{F;F?#r%!h)pyTf}cZKq*F-?j(Tncq)k z;=wKKYp---uYeZ%0f)KM{UuvloCTL&Uz)1%(=lFIKhkXqa!P|{wcD7m1ohhDd?$6z zy}ACq;Y%rXJ{SFpwgAe}AV^(*1~iNCguYx&LA*ORSPtovBQL(%D=H>o585pmeYG>2 z;ZuE3#Qe4Aoq_cqcJAYuz1}XvTg=`Qj?=Xt6V0)7ZNn;Qm}*B#p>lhPA{id*Q~1a* z(A^@Z*eTT@DoFRPz5$6FEAy_bN>xo)8Y=6)<)2ybAv?ogxO*YJd0%TNxSt1}D+yzcDQECIS$# zWR#eQnn-|A9cEiU$P`1{nDoGX4PsB;c)u6UB9$#va`(68_cihNxehOk c|9=4X#! Date: Tue, 25 Sep 2018 11:14:44 +0200 Subject: [PATCH 118/127] fix: Legend to the right, as table, should follow the width prop. Removing css conflicting with baron's width calculation. #13312 --- public/sass/components/_panel_graph.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 72f3ca3dbbe..8049a2c3107 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -17,10 +17,6 @@ padding-left: 0px; } - .graph-legend-table { - width: auto; - } - .graph-legend-table .graph-legend-series { display: table-row; } From 46405288570acc4eb02746eeebaabe4ea6c9324b Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 25 Sep 2018 11:17:26 +0200 Subject: [PATCH 119/127] Remove non-existing css prop --- public/sass/components/_panel_graph.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 8049a2c3107..01fcc5a3e64 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -31,7 +31,6 @@ } .datapoints-warning { - pointer: none; position: absolute; top: 50%; left: 50%; From cb96c6d9424decdf217ef42a1dba5484f055e2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 25 Sep 2018 12:17:04 +0200 Subject: [PATCH 120/127] Changed setting to be an alerting setting --- conf/defaults.ini | 6 ++++-- conf/sample.ini | 6 ++++-- docs/sources/installation/configuration.md | 8 ++++++++ pkg/api/render.go | 19 ++++++++++--------- pkg/services/alerting/notifier.go | 13 +++++++------ pkg/services/rendering/interface.go | 20 ++++++++++---------- pkg/services/rendering/rendering.go | 10 +--------- pkg/setting/setting.go | 5 ++--- 8 files changed, 46 insertions(+), 41 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index caccebbd910..eb8debc0094 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -474,6 +474,10 @@ error_or_timeout = alerting # Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) nodata_or_nullvalues = no_data +# Alert notifications can include images, but rendering many images at the same time can overload the server +# This limit will protect the server from render overloading and make sure notifications are sent out quickly +concurrent_render_limit = 5 + #################################### Explore ############################# [explore] # Enable the Explore section @@ -550,5 +554,3 @@ container_name = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer server_url = callback_url = -concurrent_limit = 10 -concurrent_limit_alerting = 5 diff --git a/conf/sample.ini b/conf/sample.ini index 7a460faca0e..f393c66a20e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -393,6 +393,10 @@ log_queries = # Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) ;nodata_or_nullvalues = no_data +# Alert notifications can include images, but rendering many images at the same time can overload the server +# This limit will protect the server from render overloading and make sure notifications are sent out quickly +;concurrent_render_limit = 5 + #################################### Explore ############################# [explore] # Enable the Explore section @@ -471,5 +475,3 @@ log_queries = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer ;server_url = ;callback_url = -;concurrent_limit = 10 -;concurrent_limit_alerting = 5 diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2bf4789257d..5a838e8a321 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -566,3 +566,11 @@ Default setting for new alert rules. Defaults to categorize error and timeouts a > Available in 5.3 and above Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok) + +# concurrent_render_limit + +> Available in 5.3 and above + +Alert notifications can include images, but rendering many images at the same time can overload the server. +This limit will protect the server from render overloading and make sure notifications are sent out quickly. Default +value is `5`. diff --git a/pkg/api/render.go b/pkg/api/render.go index b8ef6cc5cb6..cf672af9bea 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -41,15 +41,16 @@ func (hs *HTTPServer) RenderToPng(c *m.ReqContext) { } result, err := hs.RenderService.Render(c.Req.Context(), rendering.Opts{ - Width: width, - Height: height, - Timeout: time.Duration(timeout) * time.Second, - OrgId: c.OrgId, - UserId: c.UserId, - OrgRole: c.OrgRole, - Path: c.Params("*") + queryParams, - Timezone: queryReader.Get("tz", ""), - Encoding: queryReader.Get("encoding", ""), + Width: width, + Height: height, + Timeout: time.Duration(timeout) * time.Second, + OrgId: c.OrgId, + UserId: c.UserId, + OrgRole: c.OrgRole, + Path: c.Params("*") + queryParams, + Timezone: queryReader.Get("tz", ""), + Encoding: queryReader.Get("encoding", ""), + ConcurrentLimit: 30, }) if err != nil && err == rendering.ErrTimeout { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 839893f3444..353df1938a2 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/setting" m "github.com/grafana/grafana/pkg/models" ) @@ -108,12 +109,12 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { } renderOpts := rendering.Opts{ - Width: 1000, - Height: 500, - Timeout: alertTimeout / 2, - OrgId: context.Rule.OrgId, - OrgRole: m.ROLE_ADMIN, - IsAlert: true, + Width: 1000, + Height: 500, + Timeout: alertTimeout / 2, + OrgId: context.Rule.OrgId, + OrgRole: m.ROLE_ADMIN, + ConcurrentLimit: setting.AlertingRenderLimit, } ref, err := context.GetDashboardUID() diff --git a/pkg/services/rendering/interface.go b/pkg/services/rendering/interface.go index 856e6e683ff..39cb1ada0f5 100644 --- a/pkg/services/rendering/interface.go +++ b/pkg/services/rendering/interface.go @@ -13,16 +13,16 @@ var ErrNoRenderer = errors.New("No renderer plugin found nor is an external rend var ErrPhantomJSNotInstalled = errors.New("PhantomJS executable not found") type Opts struct { - Width int - Height int - Timeout time.Duration - OrgId int64 - UserId int64 - OrgRole models.RoleType - Path string - Encoding string - Timezone string - IsAlert bool + Width int + Height int + Timeout time.Duration + OrgId int64 + UserId int64 + OrgRole models.RoleType + Path string + Encoding string + Timezone string + ConcurrentLimit int } type RenderResult struct { diff --git a/pkg/services/rendering/rendering.go b/pkg/services/rendering/rendering.go index 2b9d91771e9..0b4f23e93b4 100644 --- a/pkg/services/rendering/rendering.go +++ b/pkg/services/rendering/rendering.go @@ -90,16 +90,8 @@ func (rs *RenderingService) Run(ctx context.Context) error { return err } -func (rs *RenderingService) getLimit(isAlerting bool) int { - if isAlerting { - return rs.Cfg.RendererLimitAlerting - } else { - return rs.Cfg.RendererLimit - } -} - func (rs *RenderingService) Render(ctx context.Context, opts Opts) (*RenderResult, error) { - if rs.inProgressCount > rs.getLimit(opts.IsAlert) { + if rs.inProgressCount > opts.ConcurrentLimit { return &RenderResult{ FilePath: filepath.Join(setting.HomePath, "public/img/rendering_limit.png"), }, nil diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 71e499f9298..27df73a9eed 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -166,6 +166,7 @@ var ( // Alerting AlertingEnabled bool ExecuteAlerts bool + AlertingRenderLimit int AlertingErrorOrTimeout string AlertingNoDataOrNullValues string @@ -648,9 +649,6 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { // Rendering renderSec := iniFile.Section("rendering") - cfg.RendererLimit = renderSec.Key("concurrent_limit").MustInt(10) - cfg.RendererLimitAlerting = renderSec.Key("concurrent_limit").MustInt(5) - cfg.RendererUrl = renderSec.Key("server_url").String() cfg.RendererCallbackUrl = renderSec.Key("callback_url").String() if cfg.RendererCallbackUrl == "" { @@ -683,6 +681,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { alerting := iniFile.Section("alerting") AlertingEnabled = alerting.Key("enabled").MustBool(true) ExecuteAlerts = alerting.Key("execute_alerts").MustBool(true) + AlertingRenderLimit = alerting.Key("concurrent_render_limit").MustInt(5) AlertingErrorOrTimeout = alerting.Key("error_or_timeout").MustString("alerting") AlertingNoDataOrNullValues = alerting.Key("nodata_or_nullvalues").MustString("no_data") From d2f2c3f22034f947b4564123bba5fdfc019cc143 Mon Sep 17 00:00:00 2001 From: Axel Pirek Date: Tue, 25 Sep 2018 12:38:02 +0200 Subject: [PATCH 121/127] Fix spelling of your and you're --- CHANGELOG.md | 2 +- .../graphite1/conf/opt/graphite/conf/aggregation-rules.conf | 2 +- docs/README.md | 2 +- docs/sources/guides/whats-new-in-v4-2.md | 2 +- docs/sources/tutorials/ha_setup.md | 2 +- pkg/cmd/grafana-cli/commands/install_command.go | 2 +- public/views/index.template.html | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace4348af99..39479054af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -505,7 +505,7 @@ See [security announcement](https://community.grafana.com/t/grafana-5-2-3-and-4- # 4.6.2 (2017-11-16) ## Important -* **Prometheus**: Fixes bug with new prometheus alerts in Grafana. Make sure to download this version if your using Prometheus for alerting. More details in the issue. [#9777](https://github.com/grafana/grafana/issues/9777) +* **Prometheus**: Fixes bug with new prometheus alerts in Grafana. Make sure to download this version if you're using Prometheus for alerting. More details in the issue. [#9777](https://github.com/grafana/grafana/issues/9777) ## Fixes * **Color picker**: Bug after using textbox input field to change/paste color string [#9769](https://github.com/grafana/grafana/issues/9769) diff --git a/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf index c9520124a2a..792bbfd6857 100644 --- a/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf +++ b/devenv/docker/blocks/graphite1/conf/opt/graphite/conf/aggregation-rules.conf @@ -8,7 +8,7 @@ # 'avg'. The name of the aggregate metric will be derived from # 'output_template' filling in any captured fields from 'input_pattern'. # -# For example, if you're metric naming scheme is: +# For example, if your metric naming scheme is: # # .applications... # diff --git a/docs/README.md b/docs/README.md index ff5ef6a4131..7310f184a60 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,7 +65,7 @@ make docs-build This will rebuild the docs docker container. -To be able to use the image your have to quit (CTRL-C) the `make watch` command (that you run in the same directory as this README). Then simply rerun `make watch`, it will restart the docs server but now with access to your image. +To be able to use the image you have to quit (CTRL-C) the `make watch` command (that you run in the same directory as this README). Then simply rerun `make watch`, it will restart the docs server but now with access to your image. ### Editing content diff --git a/docs/sources/guides/whats-new-in-v4-2.md b/docs/sources/guides/whats-new-in-v4-2.md index e976ed24700..e36e762bb76 100644 --- a/docs/sources/guides/whats-new-in-v4-2.md +++ b/docs/sources/guides/whats-new-in-v4-2.md @@ -67,7 +67,7 @@ Making it possible to have users in multiple groups and have detailed access con ## Upgrade & Breaking changes -If your using https in grafana we now force you to use tls 1.2 and the most secure ciphers. +If you're using https in grafana we now force you to use tls 1.2 and the most secure ciphers. We think its better to be secure by default rather then making it configurable. If you want to run https with lower versions of tls we suggest you put a reserve proxy in front of grafana. diff --git a/docs/sources/tutorials/ha_setup.md b/docs/sources/tutorials/ha_setup.md index 0f138b20a17..5fdb091a348 100644 --- a/docs/sources/tutorials/ha_setup.md +++ b/docs/sources/tutorials/ha_setup.md @@ -22,7 +22,7 @@ Setting up Grafana for high availability is fairly simple. It comes down to two First, you need to do is to setup MySQL or Postgres on another server and configure Grafana to use that database. You can find the configuration for doing that in the [[database]]({{< relref "configuration.md" >}}#database) section in the grafana config. -Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database your using. +Grafana will now persist all long term data in the database. How to configure the database for high availability is out of scope for this guide. We recommend finding an expert on for the database you're using. ## User sessions diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 5d4969e06af..f88bb9bbfff 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -112,7 +112,7 @@ func SelectVersion(plugin m.Plugin, version string) (m.Version, error) { } } - return m.Version{}, errors.New("Could not find the version your looking for") + return m.Version{}, errors.New("Could not find the version you're looking for") } func RemoveGitBuildFromName(pluginName, filename string) string { diff --git a/public/views/index.template.html b/public/views/index.template.html index 606db2c769e..ec51a12d34f 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -184,7 +184,7 @@
Loading Grafana

- If your seeing this Grafana has failed to load its application files + If you're seeing this Grafana has failed to load its application files

From 862ca07f037cf90c03674172954d034b49f3306f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 25 Sep 2018 14:01:38 +0200 Subject: [PATCH 122/127] fix: updated tests --- pkg/services/provisioning/dashboards/config_reader_test.go | 4 ++-- .../test-configs/dashboards-from-disk/dev-dashboards.yaml | 2 +- .../dashboards/testdata/test-configs/version-0/version-0.yaml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/services/provisioning/dashboards/config_reader_test.go b/pkg/services/provisioning/dashboards/config_reader_test.go index df0d2ae038e..d386e42349d 100644 --- a/pkg/services/provisioning/dashboards/config_reader_test.go +++ b/pkg/services/provisioning/dashboards/config_reader_test.go @@ -70,7 +70,7 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) { So(len(ds.Options), ShouldEqual, 1) So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds.DisableDeletion, ShouldBeTrue) - So(ds.UpdateIntervalSeconds, ShouldEqual, 10) + So(ds.UpdateIntervalSeconds, ShouldEqual, 15) ds2 := cfg[1] So(ds2.Name, ShouldEqual, "default") @@ -81,5 +81,5 @@ func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) { So(len(ds2.Options), ShouldEqual, 1) So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards") So(ds2.DisableDeletion, ShouldBeFalse) - So(ds2.UpdateIntervalSeconds, ShouldEqual, 3) + So(ds2.UpdateIntervalSeconds, ShouldEqual, 10) } diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml index e26c329f87c..c43c4a14c53 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/dashboards-from-disk/dev-dashboards.yaml @@ -6,7 +6,7 @@ providers: folder: 'developers' editable: true disableDeletion: true - updateIntervalSeconds: 10 + updateIntervalSeconds: 15 type: file options: path: /var/lib/grafana/dashboards diff --git a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml index 69a317fb396..8b7b8991759 100644 --- a/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml +++ b/pkg/services/provisioning/dashboards/testdata/test-configs/version-0/version-0.yaml @@ -3,7 +3,7 @@ folder: 'developers' editable: true disableDeletion: true - updateIntervalSeconds: 10 + updateIntervalSeconds: 15 type: file options: path: /var/lib/grafana/dashboards From 54f7920f0dff5ceee62fb3e9d1ebac6bce6ede60 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 25 Sep 2018 14:02:55 +0200 Subject: [PATCH 123/127] Remove option r from ln command since its not working everywhere --- devenv/setup.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devenv/setup.sh b/devenv/setup.sh index 7b5499a9f52..c9cc0d47a6f 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -11,7 +11,7 @@ bulkDashboard() { let COUNTER=COUNTER+1 done - ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml + ln -s -f ../../../devenv/bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml } bulkAlertingDashboard() { @@ -25,7 +25,7 @@ bulkAlertingDashboard() { let COUNTER=COUNTER+1 done - ln -s -f -r ./bulk_alerting_dashboards/bulk_alerting_dashboards.yaml ../conf/provisioning/dashboards/custom.yaml + ln -s -f ../../../devenv/bulk_alerting_dashboards/bulk_alerting_dashboards.yaml ../conf/provisioning/dashboards/custom.yaml } requiresJsonnet() { From 499b71c8ff49404eb07aacb67caf2891d2feddbf Mon Sep 17 00:00:00 2001 From: Chris Hicks <29731108+Chris-Hicks@users.noreply.github.com> Date: Tue, 25 Sep 2018 16:12:11 +0100 Subject: [PATCH 124/127] Remove .dropdown-menu-open on body click fixes #13409 --- public/app/core/components/grafana_app.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index a0ea0279d30..4272c8a0b71 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -245,6 +245,9 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop return; } + // ensure dropdown menu doesn't impact on z-index + body.find('.dropdown-menu-open').removeClass('dropdown-menu-open'); + // for stuff that animates, slides out etc, clicking it needs to // hide it right away const clickAutoHide = target.closest('[data-click-hide]'); From 53c7b339269966747ca7c9dda1d72ea247d65122 Mon Sep 17 00:00:00 2001 From: Aidan Rowe Date: Wed, 26 Sep 2018 10:22:17 +1000 Subject: [PATCH 125/127] imguploader: Add support for ECS credential provider for S3 --- pkg/components/imguploader/s3uploader.go | 29 +++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/components/imguploader/s3uploader.go b/pkg/components/imguploader/s3uploader.go index a1e4aed0f47..9c8af21e39e 100644 --- a/pkg/components/imguploader/s3uploader.go +++ b/pkg/components/imguploader/s3uploader.go @@ -2,12 +2,15 @@ package imguploader import ( "context" + "fmt" "os" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" + "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds" + "github.com/aws/aws-sdk-go/aws/defaults" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/session" @@ -50,7 +53,7 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, SecretAccessKey: u.secretKey, }}, &credentials.EnvProvider{}, - &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}, + remoteCredProvider(sess), }) cfg := &aws.Config{ Region: aws.String(u.region), @@ -85,3 +88,27 @@ func (u *S3Uploader) Upload(ctx context.Context, imageDiskPath string) (string, } return image_url, nil } + +func remoteCredProvider(sess *session.Session) credentials.Provider { + ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") + + if len(ecsCredURI) > 0 { + return ecsCredProvider(sess, ecsCredURI) + } + return ec2RoleProvider(sess) +} + +func ecsCredProvider(sess *session.Session, uri string) credentials.Provider { + const host = `169.254.170.2` + + d := defaults.Get() + return endpointcreds.NewProviderClient( + *d.Config, + d.Handlers, + fmt.Sprintf("http://%s%s", host, uri), + func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute }) +} + +func ec2RoleProvider(sess *session.Session) credentials.Provider { + return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute} +} From 9dbdc29118dad87f1a32d88d2697ca09c6d1102a Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 26 Sep 2018 10:56:25 +0200 Subject: [PATCH 126/127] filter NULL values for column value suggestions --- public/app/plugins/datasource/postgres/meta_query.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index 7339a3e3882..fd13f3b4482 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -144,6 +144,7 @@ table_schema IN ( let query = 'SELECT DISTINCT quote_literal(' + column + ')'; query += ' FROM ' + this.target.table; query += ' WHERE $__timeFilter(' + this.target.timeColumn + ')'; + query += ' AND ' + column + ' IS NOT NULL'; query += ' ORDER BY 1 LIMIT 100'; return query; } From b04052f51573d847b907eb3a2aafd309aece4f89 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 24 Sep 2018 16:16:10 +0200 Subject: [PATCH 127/127] alerting: move all notification conditions to defaultShouldNotify --- pkg/models/alert_notifications.go | 2 +- pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifiers/base.go | 21 ++-- pkg/services/alerting/notifiers/base_test.go | 102 +++++++++++++----- pkg/services/sqlstore/alert_notification.go | 18 ++-- .../sqlstore/alert_notification_test.go | 24 +++-- 6 files changed, 110 insertions(+), 59 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 42d33d5ed22..b90b3d36ced 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -98,7 +98,7 @@ type GetLatestNotificationQuery struct { AlertId int64 NotifierId int64 - Result *AlertNotificationJournal + Result []AlertNotificationJournal } type CleanNotificationJournalCommand struct { diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 353df1938a2..cbad5cbfdcf 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -68,7 +68,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi // Verify that we can send the notification again // but this time within the same transaction. - if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { + if !evalContext.IsTestRun && !not.ShouldNotify(ctx, evalContext) { return nil } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index ca011356247..24daa02bce8 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -42,12 +42,21 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { } } -func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, journals []models.AlertNotificationJournal) bool { // Only notify on state change. if context.PrevAlertState == context.Rule.State && !sendReminder { return false } + // get last successfully sent notification + lastNotify := time.Time{} + for _, j := range journals { + if j.Success { + lastNotify = time.Unix(j.SentAt, 0) + break + } + } + // Do not notify if interval has not elapsed if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { return false @@ -75,20 +84,12 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext } err := bus.DispatchCtx(ctx, cmd) - if err == models.ErrJournalingNotFound { - return true - } - if err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } - if !cmd.Result.Success { - return true - } - - return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0)) + return defaultShouldNotify(c, n.SendReminder, n.Frequency, cmd.Result) } func (n *NotifierBase) GetType() string { diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 57b82f32466..9ea4b82fd54 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -15,51 +15,105 @@ import ( ) func TestShouldSendAlertNotification(t *testing.T) { + tnow := time.Now() + tcs := []struct { name string prevState m.AlertStateType newState m.AlertStateType - expected bool sendReminder bool + frequency time.Duration + journals []m.AlertNotificationJournal + + expect bool }{ { - name: "pending -> ok should not trigger an notification", - newState: m.AlertStatePending, - prevState: m.AlertStateOK, - expected: false, + name: "pending -> ok should not trigger an notification", + newState: m.AlertStatePending, + prevState: m.AlertStateOK, + sendReminder: false, + journals: []m.AlertNotificationJournal{}, + + expect: false, }, { - name: "ok -> alerting should trigger an notification", - newState: m.AlertStateOK, - prevState: m.AlertStateAlerting, - expected: true, + name: "ok -> alerting should trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + sendReminder: false, + journals: []m.AlertNotificationJournal{}, + + expect: true, }, { - name: "ok -> pending should not trigger an notification", - newState: m.AlertStateOK, - prevState: m.AlertStatePending, - expected: false, + name: "ok -> pending should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStatePending, + sendReminder: false, + journals: []m.AlertNotificationJournal{}, + + expect: false, }, { name: "ok -> ok should not trigger an notification", newState: m.AlertStateOK, prevState: m.AlertStateOK, - expected: false, sendReminder: false, + journals: []m.AlertNotificationJournal{}, + + expect: false, }, { - name: "ok -> alerting should not trigger an notification", + name: "ok -> alerting should trigger an notification", newState: m.AlertStateOK, prevState: m.AlertStateAlerting, - expected: true, sendReminder: true, + journals: []m.AlertNotificationJournal{}, + + expect: true, }, { name: "ok -> ok with reminder should not trigger an notification", newState: m.AlertStateOK, prevState: m.AlertStateOK, - expected: false, sendReminder: true, + journals: []m.AlertNotificationJournal{}, + + expect: false, + }, + { + name: "alerting -> alerting with reminder and no journaling should trigger", + newState: m.AlertStateAlerting, + prevState: m.AlertStateAlerting, + frequency: time.Minute * 10, + sendReminder: true, + journals: []m.AlertNotificationJournal{}, + + expect: true, + }, + { + name: "alerting -> alerting with reminder and successful recent journal event should not trigger", + newState: m.AlertStateAlerting, + prevState: m.AlertStateAlerting, + frequency: time.Minute * 10, + sendReminder: true, + journals: []m.AlertNotificationJournal{ + {SentAt: tnow.Add(-time.Minute).Unix(), Success: true}, + }, + + expect: false, + }, + { + name: "alerting -> alerting with reminder and failed recent journal event should trigger", + newState: m.AlertStateAlerting, + prevState: m.AlertStateAlerting, + frequency: time.Minute * 10, + sendReminder: true, + expect: true, + journals: []m.AlertNotificationJournal{ + {SentAt: tnow.Add(-time.Minute).Unix(), Success: false}, // recent failed notification + {SentAt: tnow.Add(-time.Hour).Unix(), Success: true}, // old successful notification + }, }, } @@ -69,8 +123,8 @@ func TestShouldSendAlertNotification(t *testing.T) { }) evalContext.Rule.State = tc.prevState - if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected { - t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) + if defaultShouldNotify(evalContext, true, tc.frequency, tc.journals) != tc.expect { + t.Errorf("failed test %s.\n expected \n%+v \nto return: %v", tc.name, tc, tc.expect) } } } @@ -87,16 +141,6 @@ func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { }) evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) - Convey("should notify if no journaling is found", func() { - bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { - return m.ErrJournalingNotFound - }) - - if !notifier.ShouldNotify(context.Background(), evalContext) { - t.Errorf("should send notifications when ErrJournalingNotFound is returned") - } - }) - Convey("should not notify query returns error", func() { bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { return errors.New("some kind of error unknown error") diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 31867910ddb..df247e6891d 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -230,7 +230,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { } func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error { - return inTransactionCtx(ctx, func(sess *DBSession) error { + return withDbSession(ctx, func(sess *DBSession) error { journalEntry := &m.AlertNotificationJournal{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, @@ -245,21 +245,19 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou } func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { - return inTransactionCtx(ctx, func(sess *DBSession) error { - nj := &m.AlertNotificationJournal{} + return withDbSession(ctx, func(sess *DBSession) error { + nj := []m.AlertNotificationJournal{} - _, err := sess.Desc("alert_notification_journal.sent_at"). - Limit(1). - Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj) + err := sess.Desc("alert_notification_journal.sent_at"). + Where("alert_notification_journal.org_id = ?", cmd.OrgId). + Where("alert_notification_journal.alert_id = ?", cmd.AlertId). + Where("alert_notification_journal.notifier_id = ?", cmd.NotifierId). + Find(&nj) if err != nil { return err } - if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 { - return m.ErrJournalingNotFound - } - cmd.Result = nj return nil }) diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 83fb42db9bb..1e3df45b5cf 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -15,16 +15,21 @@ func TestAlertNotificationSQLAccess(t *testing.T) { InitTestDB(t) Convey("Alert notification journal", func() { - var alertId int64 = 5 + var alertId int64 = 7 var orgId int64 = 5 - var notifierId int64 = 5 + var notifierId int64 = 10 Convey("Getting last journal should raise error if no one exists", func() { query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} - err := GetLatestNotification(context.Background(), query) - So(err, ShouldEqual, m.ErrJournalingNotFound) + GetLatestNotification(context.Background(), query) + So(len(query.Result), ShouldEqual, 0) - Convey("shoulbe be able to record two journaling events", func() { + // recording an journal entry in another org to make sure org filter works as expected. + journalInOtherOrg := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: 10, Success: true, SentAt: 1} + err := RecordNotificationJournal(context.Background(), journalInOtherOrg) + So(err, ShouldBeNil) + + Convey("should be able to record two journaling events", func() { createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1} err := RecordNotificationJournal(context.Background(), createCmd) @@ -38,17 +43,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("get last journaling event", func() { err := GetLatestNotification(context.Background(), query) So(err, ShouldBeNil) - So(query.Result.SentAt, ShouldEqual, 1001) + So(len(query.Result), ShouldEqual, 2) + last := query.Result[0] + So(last.SentAt, ShouldEqual, 1001) Convey("be able to clear all journaling for an notifier", func() { cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId} err := CleanNotificationJournal(context.Background(), cmd) So(err, ShouldBeNil) - Convey("querying for last junaling should raise error", func() { + Convey("querying for last journaling should return no journal entries", func() { query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} err := GetLatestNotification(context.Background(), query) - So(err, ShouldEqual, m.ErrJournalingNotFound) + So(err, ShouldBeNil) + So(len(query.Result), ShouldEqual, 0) }) }) })