From 5f515bb3fc450841934b4d74ab4e3cba7e57626c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 19 Oct 2018 15:33:16 +0200 Subject: [PATCH 01/50] using react component --- public/app/core/angular_wrappers.ts | 2 ++ .../app/core/components/Alerts/AlertList.tsx | 36 +++++++++++++++++++ public/app/core/services/alert_srv.ts | 2 ++ public/views/index.template.html | 15 +------- 4 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 public/app/core/components/Alerts/AlertList.tsx diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 6974d40aac8..03d402ce86d 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,10 +5,12 @@ 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 { AlertList } from './components/Alerts/AlertList'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('sidemenu', SideMenu, []); + react2AngularDirective('pageAlertList', AlertList, []); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('searchResult', SearchResult, []); diff --git a/public/app/core/components/Alerts/AlertList.tsx b/public/app/core/components/Alerts/AlertList.tsx new file mode 100644 index 00000000000..bd9fc49a007 --- /dev/null +++ b/public/app/core/components/Alerts/AlertList.tsx @@ -0,0 +1,36 @@ +import React, { PureComponent } from 'react'; + +export interface Props { + alerts: any[]; +} + +export class AlertList extends PureComponent { + onClearAlert = alert => { + console.log('clear alert', alert); + }; + + render() { + const alerts = [{ severity: 'success', icon: 'warning', title: 'test', text: 'test text' }]; + + return ( +
+ {alerts.map((alert, index) => { + return ( +
+
+ +
+
+
{alert.title}
+
{alert.text}
+
+ +
+ ); + })} +
+ ); + } +} diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 2d447651b75..9a4fabb761a 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -20,6 +20,8 @@ export class AlertSrv { this.$rootScope ); + this.list.push({ severity: 'success', icon: 'warning', title: 'test', text: 'test text' }); + this.$rootScope.onAppEvent( 'alert-warning', (e, alert) => { diff --git a/public/views/index.template.html b/public/views/index.template.html index c39d5e08321..597fc8d8f59 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -200,21 +200,8 @@ + -
-
-
- -
-
-
{{alert.title}}
-
-
- -
-
From abb6b135a3f3277203f92642ccf9fb46361d61b0 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 15 Oct 2018 20:39:23 +0200 Subject: [PATCH 02/50] pkg/plugins/plugins.go: remove ineffective break statement. See, $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... pkg/plugins/plugins.go:124:4:warning: ineffective break statement. Did you mean to break out of the outer loop? (SA4011) (megacheck) --- pkg/plugins/plugins.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 67eb0f51d70..4f15441bb2f 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -121,7 +121,6 @@ func (pm *PluginManager) Run(ctx context.Context) error { pm.checkForUpdates() case <-ctx.Done(): run = false - break } } From e673337cb927650bd76ce0ee9407ce3cea892e78 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 15 Oct 2018 21:13:03 +0200 Subject: [PATCH 03/50] pkg/middleware/middleware.go: Fix empty branch warning. See, $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... pkg/middleware/middleware.go:46:3:warning: empty branch (SA9003) (megacheck) --- pkg/middleware/middleware.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 3e83a60f94b..7b29901c1a3 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -43,12 +43,13 @@ func GetContextHandler() macaron.Handler { // then init session and look for userId in session // then look for api key in session (special case for render calls via api) // then test if anonymous access is enabled - if initContextWithRenderAuth(ctx) || - initContextWithApiKey(ctx) || - initContextWithBasicAuth(ctx, orgId) || - initContextWithAuthProxy(ctx, orgId) || - initContextWithUserSessionCookie(ctx, orgId) || - initContextWithAnonymousUser(ctx) { + switch { + case initContextWithRenderAuth(ctx): + case initContextWithApiKey(ctx): + case initContextWithBasicAuth(ctx, orgId): + case initContextWithAuthProxy(ctx, orgId): + case initContextWithUserSessionCookie(ctx, orgId): + case initContextWithAnonymousUser(ctx): } ctx.Logger = log.New("context", "userId", ctx.UserId, "orgId", ctx.OrgId, "uname", ctx.Login) From 68507e88552b0101ae3af273b06c3b1baf70529d Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 15 Oct 2018 21:30:42 +0200 Subject: [PATCH 04/50] pkg/services/alerting/reader.go: Fix should use for range instead of for { select {} }. $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... pkg/services/alerting/reader.go:37:2:warning: should use for range instead of for { select {} } (S1000) (megacheck) --- pkg/services/alerting/reader.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pkg/services/alerting/reader.go b/pkg/services/alerting/reader.go index 627159c286b..2cdbc57b41d 100644 --- a/pkg/services/alerting/reader.go +++ b/pkg/services/alerting/reader.go @@ -34,11 +34,8 @@ func NewRuleReader() *DefaultRuleReader { func (arr *DefaultRuleReader) initReader() { heartbeat := time.NewTicker(time.Second * 10) - for { - select { - case <-heartbeat.C: - arr.heartbeat() - } + for range heartbeat.C { + arr.heartbeat() } } From 9108966fcb40bd7fe974c4215f5b6feedc939fb7 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Mon, 15 Oct 2018 22:13:36 +0200 Subject: [PATCH 05/50] scripts/build/publish.go: Fix warning on err variable. See, $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... scripts/build/publish.go:126:48:warning: argument err is overwritten before first use (SA4009) (megacheck) --- scripts/build/publish.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/build/publish.go b/scripts/build/publish.go index ab9cbebf6bb..d5b19877724 100644 --- a/scripts/build/publish.go +++ b/scripts/build/publish.go @@ -22,13 +22,13 @@ var versionRe = regexp.MustCompile(`grafana-(.*)(\.|_)(arm64|armhfp|aarch64|armv var debVersionRe = regexp.MustCompile(`grafana_(.*)_(arm64|armv7|armhf|amd64)\.deb`) var builds = []build{} var architectureMapping = map[string]string{ - "armv7":"armv7", - "armhfp":"armv7", - "armhf":"armv7", - "arm64":"arm64", - "aarch64":"arm64", - "amd64":"amd64", - "x86_64":"amd64", + "armv7": "armv7", + "armhfp": "armv7", + "armhf": "armv7", + "arm64": "arm64", + "aarch64": "arm64", + "amd64": "amd64", + "x86_64": "amd64", } func main() { @@ -78,7 +78,7 @@ func mapPackage(path string, name string, shaBytes []byte) (build, error) { if len(result) > 0 { version = string(result[1]) log.Printf("Version detected: %v", version) - } else if (len(debResult) > 0) { + } else if len(debResult) > 0 { version = string(debResult[1]) } else { return build{}, fmt.Errorf("Unable to figure out version from '%v'", name) @@ -124,6 +124,9 @@ func mapPackage(path string, name string, shaBytes []byte) (build, error) { } func packageWalker(path string, f os.FileInfo, err error) error { + if err != nil { + log.Printf("error: %v", err) + } if f.Name() == "dist" || strings.Contains(f.Name(), "sha256") || strings.Contains(f.Name(), "latest") { return nil } @@ -134,7 +137,6 @@ func packageWalker(path string, f os.FileInfo, err error) error { } build, err := mapPackage(path, f.Name(), shaBytes) - if err != nil { log.Printf("Could not map metadata from package: %v", err) return nil From b7d821b524c0b671da59f5df670375dcb0582a07 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 22 Oct 2018 14:22:40 +0200 Subject: [PATCH 06/50] component working --- public/app/core/angular_wrappers.ts | 2 +- .../app/core/components/Alerts/AlertList.tsx | 11 ++++- .../core/components/Alerts/state/actions.ts | 23 +++++++++++ .../components/Alerts/state/reducers.test.ts | 41 +++++++++++++++++++ .../core/components/Alerts/state/reducers.ts | 23 +++++++++++ public/app/core/services/alert_srv.ts | 2 - public/app/types/alerts.ts | 10 +++++ public/app/types/index.ts | 4 ++ 8 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 public/app/core/components/Alerts/state/actions.ts create mode 100644 public/app/core/components/Alerts/state/reducers.test.ts create mode 100644 public/app/core/components/Alerts/state/reducers.ts create mode 100644 public/app/types/alerts.ts diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 03d402ce86d..5b14ebe46fa 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,7 @@ 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 { AlertList } from './components/Alerts/AlertList'; +import AlertList from './components/Alerts/AlertList'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); diff --git a/public/app/core/components/Alerts/AlertList.tsx b/public/app/core/components/Alerts/AlertList.tsx index bd9fc49a007..e384924d96f 100644 --- a/public/app/core/components/Alerts/AlertList.tsx +++ b/public/app/core/components/Alerts/AlertList.tsx @@ -1,4 +1,5 @@ import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; export interface Props { alerts: any[]; @@ -10,7 +11,7 @@ export class AlertList extends PureComponent { }; render() { - const alerts = [{ severity: 'success', icon: 'warning', title: 'test', text: 'test text' }]; + const { alerts } = this.props; return (
@@ -34,3 +35,11 @@ export class AlertList extends PureComponent { ); } } + +function mapStateToProps(state) { + return { + alerts: state.alerts.alerts, + }; +} + +export default connect(mapStateToProps)(AlertList); diff --git a/public/app/core/components/Alerts/state/actions.ts b/public/app/core/components/Alerts/state/actions.ts new file mode 100644 index 00000000000..cc82f21a3e7 --- /dev/null +++ b/public/app/core/components/Alerts/state/actions.ts @@ -0,0 +1,23 @@ +import { Alert } from 'app/types'; + +export enum ActionTypes { + AddAlert = 'ADD_ALERT', + ClearAlert = 'CLEAR_ALERT', +} + +interface AddAlertAction { + type: ActionTypes.AddAlert; + payload: Alert; +} + +interface ClearAlertAction { + type: ActionTypes.ClearAlert; + payload: Alert; +} + +export type Action = AddAlertAction | ClearAlertAction; + +export const clearAlert = (alert: Alert) => ({ + type: ActionTypes.ClearAlert, + payload: alert, +}); diff --git a/public/app/core/components/Alerts/state/reducers.test.ts b/public/app/core/components/Alerts/state/reducers.test.ts new file mode 100644 index 00000000000..16848e6b233 --- /dev/null +++ b/public/app/core/components/Alerts/state/reducers.test.ts @@ -0,0 +1,41 @@ +import { alertsReducer } from './reducers'; +import { ActionTypes } from './actions'; + +describe('clear alert', () => { + it('should filter alert', () => { + const initialState = { + alerts: [ + { + severity: 'success', + icon: 'success', + title: 'test', + text: 'test alert', + }, + { + severity: 'fail', + icon: 'warning', + title: 'test2', + text: 'test alert fail 2', + }, + ], + }; + + const result = alertsReducer(initialState, { + type: ActionTypes.ClearAlert, + payload: initialState.alerts[1], + }); + + const expectedResult = { + alerts: [ + { + severity: 'success', + icon: 'success', + title: 'test', + text: 'test alert', + }, + ], + }; + + expect(result).toEqual(expectedResult); + }); +}); diff --git a/public/app/core/components/Alerts/state/reducers.ts b/public/app/core/components/Alerts/state/reducers.ts new file mode 100644 index 00000000000..efbfa96aa2c --- /dev/null +++ b/public/app/core/components/Alerts/state/reducers.ts @@ -0,0 +1,23 @@ +import { Alert, AlertsState } from 'app/types'; +import { Action, ActionTypes } from './actions'; + +export const initialState: AlertsState = { + alerts: [] as Alert[], +}; + +export const alertsReducer = (state = initialState, action: Action): AlertsState => { + switch (action.type) { + case ActionTypes.AddAlert: + return { ...state, alerts: state.alerts.concat([action.payload]) }; + case ActionTypes.ClearAlert: + return { + ...state, + alerts: state.alerts.filter(alert => alert !== action.payload), + }; + } + return state; +}; + +export default { + alerts: alertsReducer, +}; diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 9a4fabb761a..2d447651b75 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -20,8 +20,6 @@ export class AlertSrv { this.$rootScope ); - this.list.push({ severity: 'success', icon: 'warning', title: 'test', text: 'test text' }); - this.$rootScope.onAppEvent( 'alert-warning', (e, alert) => { diff --git a/public/app/types/alerts.ts b/public/app/types/alerts.ts new file mode 100644 index 00000000000..2e744ddc9b7 --- /dev/null +++ b/public/app/types/alerts.ts @@ -0,0 +1,10 @@ +export interface Alert { + severity: string; + icon: string; + title: string; + text: string; +} + +export interface AlertsState { + alerts: Alert[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 7b35f3d6787..af0dacf27ee 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,6 +9,7 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; +import { Alert, AlertsState } from './alerts'; export { Team, @@ -46,6 +47,8 @@ export { User, UsersState, PluginDashboard, + Alert, + AlertsState, }; export interface StoreState { @@ -58,4 +61,5 @@ export interface StoreState { dashboard: DashboardState; dataSources: DataSourcesState; users: UsersState; + alerts: AlertsState; } From bbd02dd616d5a27f2de23db04a4036714f1243ad Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 23 Oct 2018 13:34:27 +0200 Subject: [PATCH 07/50] renaming things --- public/app/core/angular_wrappers.ts | 4 +- .../app/core/components/Alerts/AlertList.tsx | 45 --------- .../core/components/Alerts/state/actions.ts | 23 ----- .../AppNotifications/AppNotificationList.tsx | 96 +++++++++++++++++++ .../AppNotifications/state/actions.ts | 28 ++++++ .../state/reducers.test.ts | 2 +- .../state/reducers.ts | 8 +- public/app/types/alerts.ts | 4 +- public/app/types/index.ts | 4 +- 9 files changed, 135 insertions(+), 79 deletions(-) delete mode 100644 public/app/core/components/Alerts/AlertList.tsx delete mode 100644 public/app/core/components/Alerts/state/actions.ts create mode 100644 public/app/core/components/AppNotifications/AppNotificationList.tsx create mode 100644 public/app/core/components/AppNotifications/state/actions.ts rename public/app/core/components/{Alerts => AppNotifications}/state/reducers.test.ts (94%) rename public/app/core/components/{Alerts => AppNotifications}/state/reducers.ts (72%) diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 5b14ebe46fa..14a0dcdb234 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,12 +5,12 @@ 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 AlertList from './components/Alerts/AlertList'; +import AppNotificationList from './components/AppNotifications/AppNotificationList'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('sidemenu', SideMenu, []); - react2AngularDirective('pageAlertList', AlertList, []); + react2AngularDirective('pageAlertList', AppNotificationList, []); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('searchResult', SearchResult, []); diff --git a/public/app/core/components/Alerts/AlertList.tsx b/public/app/core/components/Alerts/AlertList.tsx deleted file mode 100644 index e384924d96f..00000000000 --- a/public/app/core/components/Alerts/AlertList.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; - -export interface Props { - alerts: any[]; -} - -export class AlertList extends PureComponent { - onClearAlert = alert => { - console.log('clear alert', alert); - }; - - render() { - const { alerts } = this.props; - - return ( -
- {alerts.map((alert, index) => { - return ( -
-
- -
-
-
{alert.title}
-
{alert.text}
-
- -
- ); - })} -
- ); - } -} - -function mapStateToProps(state) { - return { - alerts: state.alerts.alerts, - }; -} - -export default connect(mapStateToProps)(AlertList); diff --git a/public/app/core/components/Alerts/state/actions.ts b/public/app/core/components/Alerts/state/actions.ts deleted file mode 100644 index cc82f21a3e7..00000000000 --- a/public/app/core/components/Alerts/state/actions.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Alert } from 'app/types'; - -export enum ActionTypes { - AddAlert = 'ADD_ALERT', - ClearAlert = 'CLEAR_ALERT', -} - -interface AddAlertAction { - type: ActionTypes.AddAlert; - payload: Alert; -} - -interface ClearAlertAction { - type: ActionTypes.ClearAlert; - payload: Alert; -} - -export type Action = AddAlertAction | ClearAlertAction; - -export const clearAlert = (alert: Alert) => ({ - type: ActionTypes.ClearAlert, - payload: alert, -}); diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx new file mode 100644 index 00000000000..a637d741541 --- /dev/null +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -0,0 +1,96 @@ +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import appEvents from 'app/core/app_events'; +import { addAppNotification, clearAppNotification } from './state/actions'; + +export interface Props { + alerts: any[]; + addAppNotification: typeof addAppNotification; + clearAppNotification: typeof clearAppNotification; +} + +enum AppNotificationSeverity { + Success = 'success', + Warning = 'warning', + Error = 'error', + Info = 'info', +} + +export class AppNotificationList extends PureComponent { + componentDidMount() { + appEvents.on('alert-warning', options => this.addAppNotification(options[0], options[1], 'warning', 5000)); + appEvents.on('alert-success', options => this.addAppNotification(options[0], options[1], 'success', 3000)); + appEvents.on('alert-error', options => this.addAppNotification(options[0], options[1], 'error', 7000)); + } + + addAppNotification(title, text, severity, timeout) { + const newAlert = { + title: title || '', + text: text || '', + severity: severity || AppNotificationSeverity.Info, + icon: this.getIconForSeverity(severity), + remove: this.clearAutomatically(this, timeout), + }; + + this.props.addAppNotification(newAlert); + } + + getIconForSeverity(severity) { + switch (severity) { + case AppNotificationSeverity.Success: + return 'fa fa-check'; + case AppNotificationSeverity.Error: + return 'fa fa-exclamation-triangle'; + default: + return 'fa fa-exclamation'; + } + } + + clearAutomatically = (alert, timeout) => { + setTimeout(() => { + this.props.clearAppNotification(alert); + }, timeout); + }; + + onClearAppNotification = alert => { + this.props.clearAppNotification(alert); + }; + + render() { + const { alerts } = this.props; + + return ( +
+ {alerts.map((alert, index) => { + return ( +
+
+ +
+
+
{alert.title}
+
{alert.text}
+
+ +
+ ); + })} +
+ ); + } +} + +function mapStateToProps(state) { + return { + alerts: state.alerts.alerts, + }; +} + +const mapDispatchToProps = { + addAppNotification, + clearAppNotification, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AppNotificationList); diff --git a/public/app/core/components/AppNotifications/state/actions.ts b/public/app/core/components/AppNotifications/state/actions.ts new file mode 100644 index 00000000000..dfdb066e978 --- /dev/null +++ b/public/app/core/components/AppNotifications/state/actions.ts @@ -0,0 +1,28 @@ +import { AppNotification } from 'app/types'; + +export enum ActionTypes { + AddAppNotification = 'ADD_APP_NOTIFICATION', + ClearAppNotification = 'CLEAR_APP_NOTIFICATION', +} + +interface AddAppNotificationAction { + type: ActionTypes.AddAppNotification; + payload: AppNotification; +} + +interface ClearAppNotificationAction { + type: ActionTypes.ClearAppNotification; + payload: AppNotification; +} + +export type Action = AddAppNotificationAction | ClearAppNotificationAction; + +export const clearAppNotification = (alert: AppNotification) => ({ + type: ActionTypes.ClearAppNotification, + payload: alert, +}); + +export const addAppNotification = (alert: AppNotification) => ({ + type: ActionTypes.AddAppNotification, + payload: alert, +}); diff --git a/public/app/core/components/Alerts/state/reducers.test.ts b/public/app/core/components/AppNotifications/state/reducers.test.ts similarity index 94% rename from public/app/core/components/Alerts/state/reducers.test.ts rename to public/app/core/components/AppNotifications/state/reducers.test.ts index 16848e6b233..e81bbf27967 100644 --- a/public/app/core/components/Alerts/state/reducers.test.ts +++ b/public/app/core/components/AppNotifications/state/reducers.test.ts @@ -21,7 +21,7 @@ describe('clear alert', () => { }; const result = alertsReducer(initialState, { - type: ActionTypes.ClearAlert, + type: ActionTypes.ClearAppNotification, payload: initialState.alerts[1], }); diff --git a/public/app/core/components/Alerts/state/reducers.ts b/public/app/core/components/AppNotifications/state/reducers.ts similarity index 72% rename from public/app/core/components/Alerts/state/reducers.ts rename to public/app/core/components/AppNotifications/state/reducers.ts index efbfa96aa2c..7bb7b2f65bf 100644 --- a/public/app/core/components/Alerts/state/reducers.ts +++ b/public/app/core/components/AppNotifications/state/reducers.ts @@ -1,15 +1,15 @@ -import { Alert, AlertsState } from 'app/types'; +import { AppNotification, AlertsState } from 'app/types'; import { Action, ActionTypes } from './actions'; export const initialState: AlertsState = { - alerts: [] as Alert[], + alerts: [] as AppNotification[], }; export const alertsReducer = (state = initialState, action: Action): AlertsState => { switch (action.type) { - case ActionTypes.AddAlert: + case ActionTypes.AddAppNotification: return { ...state, alerts: state.alerts.concat([action.payload]) }; - case ActionTypes.ClearAlert: + case ActionTypes.ClearAppNotification: return { ...state, alerts: state.alerts.filter(alert => alert !== action.payload), diff --git a/public/app/types/alerts.ts b/public/app/types/alerts.ts index 2e744ddc9b7..96b764243ff 100644 --- a/public/app/types/alerts.ts +++ b/public/app/types/alerts.ts @@ -1,4 +1,4 @@ -export interface Alert { +export interface AppNotification { severity: string; icon: string; title: string; @@ -6,5 +6,5 @@ export interface Alert { } export interface AlertsState { - alerts: Alert[]; + alerts: AppNotification[]; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index af0dacf27ee..fbb3b3d7d65 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,7 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; -import { Alert, AlertsState } from './alerts'; +import { AppNotification, AlertsState } from './alerts'; export { Team, @@ -47,7 +47,7 @@ export { User, UsersState, PluginDashboard, - Alert, + AppNotification, AlertsState, }; From bb6409384e585b4926dd588c66b12d6e8aa870bd Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 23 Oct 2018 16:00:04 +0200 Subject: [PATCH 08/50] connected to store, self remove logic --- .../AppNotifications/AppNotificationList.tsx | 45 ++++++++++--------- .../AppNotifications/state/actions.ts | 6 +-- .../AppNotifications/state/reducers.test.ts | 16 ++++--- .../AppNotifications/state/reducers.ts | 14 +++--- .../app/core/utils/connectWithReduxStore.tsx | 11 +++++ .../permissions/DashboardPermissions.tsx | 10 +---- public/app/store/configureStore.ts | 2 + public/app/types/alerts.ts | 5 ++- public/app/types/index.ts | 6 +-- 9 files changed, 66 insertions(+), 49 deletions(-) create mode 100644 public/app/core/utils/connectWithReduxStore.tsx diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index a637d741541..a5953156aab 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -1,10 +1,11 @@ import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; import appEvents from 'app/core/app_events'; import { addAppNotification, clearAppNotification } from './state/actions'; +import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; +import { AppNotification, StoreState } from '../../../types'; export interface Props { - alerts: any[]; + appNotifications: AppNotification[]; addAppNotification: typeof addAppNotification; clearAppNotification: typeof clearAppNotification; } @@ -24,12 +25,14 @@ export class AppNotificationList extends PureComponent { } addAppNotification(title, text, severity, timeout) { + const id = Date.now(); const newAlert = { + id: id, title: title || '', text: text || '', severity: severity || AppNotificationSeverity.Info, icon: this.getIconForSeverity(severity), - remove: this.clearAutomatically(this, timeout), + remove: this.clearAutomatically(id, timeout), }; this.props.addAppNotification(newAlert); @@ -46,32 +49,36 @@ export class AppNotificationList extends PureComponent { } } - clearAutomatically = (alert, timeout) => { + clearAutomatically = (id, timeout) => { setTimeout(() => { - this.props.clearAppNotification(alert); + this.props.clearAppNotification(id); }, timeout); }; - onClearAppNotification = alert => { - this.props.clearAppNotification(alert); + onClearAppNotification = id => { + this.props.clearAppNotification(id); }; render() { - const { alerts } = this.props; + const { appNotifications } = this.props; return (
- {alerts.map((alert, index) => { + {appNotifications.map((appNotification, index) => { return ( -
+
- +
-
{alert.title}
-
{alert.text}
+
{appNotification.title}
+
{appNotification.text}
-
@@ -82,15 +89,13 @@ export class AppNotificationList extends PureComponent { } } -function mapStateToProps(state) { - return { - alerts: state.alerts.alerts, - }; -} +const mapStateToProps = (state: StoreState) => ({ + appNotifications: state.appNotifications.appNotifications, +}); const mapDispatchToProps = { addAppNotification, clearAppNotification, }; -export default connect(mapStateToProps, mapDispatchToProps)(AppNotificationList); +export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps); diff --git a/public/app/core/components/AppNotifications/state/actions.ts b/public/app/core/components/AppNotifications/state/actions.ts index dfdb066e978..c6cfe9c3e9e 100644 --- a/public/app/core/components/AppNotifications/state/actions.ts +++ b/public/app/core/components/AppNotifications/state/actions.ts @@ -12,14 +12,14 @@ interface AddAppNotificationAction { interface ClearAppNotificationAction { type: ActionTypes.ClearAppNotification; - payload: AppNotification; + payload: number; } export type Action = AddAppNotificationAction | ClearAppNotificationAction; -export const clearAppNotification = (alert: AppNotification) => ({ +export const clearAppNotification = (appNotificationId: number) => ({ type: ActionTypes.ClearAppNotification, - payload: alert, + payload: appNotificationId, }); export const addAppNotification = (alert: AppNotification) => ({ diff --git a/public/app/core/components/AppNotifications/state/reducers.test.ts b/public/app/core/components/AppNotifications/state/reducers.test.ts index e81bbf27967..bdc78b4fe0b 100644 --- a/public/app/core/components/AppNotifications/state/reducers.test.ts +++ b/public/app/core/components/AppNotifications/state/reducers.test.ts @@ -1,17 +1,22 @@ -import { alertsReducer } from './reducers'; +import { appNotificationsReducer } from './reducers'; import { ActionTypes } from './actions'; describe('clear alert', () => { it('should filter alert', () => { + const id1 = 1540301236048; + const id2 = 1540301248293; + const initialState = { - alerts: [ + appNotifications: [ { + id: id1, severity: 'success', icon: 'success', title: 'test', text: 'test alert', }, { + id: id2, severity: 'fail', icon: 'warning', title: 'test2', @@ -20,14 +25,15 @@ describe('clear alert', () => { ], }; - const result = alertsReducer(initialState, { + const result = appNotificationsReducer(initialState, { type: ActionTypes.ClearAppNotification, - payload: initialState.alerts[1], + payload: id2, }); const expectedResult = { - alerts: [ + appNotifications: [ { + id: id1, severity: 'success', icon: 'success', title: 'test', diff --git a/public/app/core/components/AppNotifications/state/reducers.ts b/public/app/core/components/AppNotifications/state/reducers.ts index 7bb7b2f65bf..7887a91a616 100644 --- a/public/app/core/components/AppNotifications/state/reducers.ts +++ b/public/app/core/components/AppNotifications/state/reducers.ts @@ -1,23 +1,23 @@ -import { AppNotification, AlertsState } from 'app/types'; +import { AppNotification, AppNotificationsState } from 'app/types'; import { Action, ActionTypes } from './actions'; -export const initialState: AlertsState = { - alerts: [] as AppNotification[], +export const initialState: AppNotificationsState = { + appNotifications: [] as AppNotification[], }; -export const alertsReducer = (state = initialState, action: Action): AlertsState => { +export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => { switch (action.type) { case ActionTypes.AddAppNotification: - return { ...state, alerts: state.alerts.concat([action.payload]) }; + return { ...state, appNotifications: state.appNotifications.concat([action.payload]) }; case ActionTypes.ClearAppNotification: return { ...state, - alerts: state.alerts.filter(alert => alert !== action.payload), + appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload), }; } return state; }; export default { - alerts: alertsReducer, + appNotifications: appNotificationsReducer, }; diff --git a/public/app/core/utils/connectWithReduxStore.tsx b/public/app/core/utils/connectWithReduxStore.tsx new file mode 100644 index 00000000000..92c61db4e77 --- /dev/null +++ b/public/app/core/utils/connectWithReduxStore.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { store } from '../../store/configureStore'; + +export function connectWithStore(WrappedComponent, ...args) { + const ConnectedWrappedComponent = connect(...args)(WrappedComponent); + + return props => { + return ; + }; +} diff --git a/public/app/features/dashboard/permissions/DashboardPermissions.tsx b/public/app/features/dashboard/permissions/DashboardPermissions.tsx index 5651242a485..c07bef42930 100644 --- a/public/app/features/dashboard/permissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/permissions/DashboardPermissions.tsx @@ -1,5 +1,4 @@ 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'; @@ -13,7 +12,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/store/configureStore'; +import { connectWithStore } from '../../../core/utils/connectWithReduxStore'; export interface Props { dashboardId: number; @@ -95,13 +94,6 @@ export class DashboardPermissions extends PureComponent { } } -function connectWithStore(WrappedComponent, ...args) { - const ConnectedWrappedComponent = connect(...args)(WrappedComponent); - return props => { - return ; - }; -} - const mapStateToProps = (state: StoreState) => ({ permissions: state.dashboard.permissions, }); diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index ccd027a0b6d..c1c6103bab9 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -10,6 +10,7 @@ import dashboardReducers from 'app/features/dashboard/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; +import appNotificationReducers from 'app/core/components/AppNotifications/state/reducers'; const rootReducers = { ...sharedReducers, @@ -21,6 +22,7 @@ const rootReducers = { ...pluginReducers, ...dataSourcesReducers, ...usersReducers, + ...appNotificationReducers, }; export let store; diff --git a/public/app/types/alerts.ts b/public/app/types/alerts.ts index 96b764243ff..3f25fedbf8b 100644 --- a/public/app/types/alerts.ts +++ b/public/app/types/alerts.ts @@ -1,10 +1,11 @@ export interface AppNotification { + id: number; severity: string; icon: string; title: string; text: string; } -export interface AlertsState { - alerts: AppNotification[]; +export interface AppNotificationsState { + appNotifications: AppNotification[]; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index fbb3b3d7d65..bc446bf3e36 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,7 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; -import { AppNotification, AlertsState } from './alerts'; +import { AppNotification, AppNotificationsState } from './alerts'; export { Team, @@ -48,7 +48,7 @@ export { UsersState, PluginDashboard, AppNotification, - AlertsState, + AppNotificationsState, }; export interface StoreState { @@ -61,5 +61,5 @@ export interface StoreState { dashboard: DashboardState; dataSources: DataSourcesState; users: UsersState; - alerts: AlertsState; + appNotifications: AppNotificationsState; } From bd2f9a38d9ba7e7b17a2ff50ec1110f562553e2c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 23 Oct 2018 17:25:16 +0200 Subject: [PATCH 09/50] Added margin and correct border radius --- public/sass/components/_alerts.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/sass/components/_alerts.scss b/public/sass/components/_alerts.scss index 3420dcfdfaf..710c4d1ec0f 100644 --- a/public/sass/components/_alerts.scss +++ b/public/sass/components/_alerts.scss @@ -7,13 +7,13 @@ .alert { padding: 1.25rem 2rem 1.25rem 1.5rem; - margin-bottom: $line-height-base; + margin-bottom: $panel-margin / 2; text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5); background: $alert-error-bg; position: relative; color: $white; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); - border-radius: 2px; + border-radius: $border-radius; display: flex; flex-direction: row; } From 3466969a7c9343f4d6ca15496c8f45aa63e78015 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Thu, 18 Oct 2018 22:37:07 +0200 Subject: [PATCH 10/50] pkg/login/ldap.go: Fix warning comparison to bool constant See, $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... pkg/login/ldap.go:188:40:warning: should omit comparison to bool constant, can be simplified to !*extUser.IsGrafanaAdmin (S1002) (megacheck) --- pkg/login/ldap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 4c71ab3cd5f..d4e81d2bd46 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -185,7 +185,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo if ldapUser.isMemberOf(group.GroupDN) { extUser.OrgRoles[group.OrgId] = group.OrgRole - if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false { + if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin { extUser.IsGrafanaAdmin = group.IsGrafanaAdmin } } From bb12a1bc9948cd0172df526b4b393a016abc4779 Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 19 Oct 2018 19:09:21 +0200 Subject: [PATCH 11/50] pkg/tsdb/graphite/graphite.go: Fix regular expression does not contain any meta characters. I found this article benchmarking Replace vs Regexp, https://medium.com/codezillas/golang-replace-vs-regexp-de4e48482f53 See, $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... pkg/tsdb/graphite/graphite.go:167:28:warning: regular expression does not contain any meta characters (SA6004) (megacheck) pkg/tsdb/graphite/graphite.go:172:28:warning: regular expression does not contain any meta characters (SA6004) (megacheck) --- pkg/tsdb/graphite/graphite.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 2960ba0edc4..ff0ed8d0620 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -164,14 +164,12 @@ func formatTimeRange(input string) string { func fixIntervalFormat(target string) string { rMinute := regexp.MustCompile(`'(\d+)m'`) - rMin := regexp.MustCompile("m") target = rMinute.ReplaceAllStringFunc(target, func(m string) string { - return rMin.ReplaceAllString(m, "min") + return strings.Replace(m, "m", "min", -1) }) rMonth := regexp.MustCompile(`'(\d+)M'`) - rMon := regexp.MustCompile("M") target = rMonth.ReplaceAllStringFunc(target, func(M string) string { - return rMon.ReplaceAllString(M, "mon") + return strings.Replace(M, "M", "mon", -1) }) return target } From 91447dcbf902fb6965c258222b4a9498fa36556f Mon Sep 17 00:00:00 2001 From: Mario Trangoni Date: Fri, 19 Oct 2018 19:47:31 +0200 Subject: [PATCH 12/50] pkg/tsdb/stackdriver/stackdriver.go: Fix regular expression does not contain any meta characters. See, $ gometalinter --vendor --deadline 10m --disable-all --enable=megacheck ./... pkg/tsdb/stackdriver/stackdriver.go:171:26:warning: regular expression does not contain any meta characters (SA6004) (megacheck) --- pkg/tsdb/stackdriver/stackdriver.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 96242dfdec4..5f1bf8f5841 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -168,8 +168,7 @@ func reverse(s string) string { } func interpolateFilterWildcards(value string) string { - re := regexp.MustCompile("[*]") - matches := len(re.FindAllStringIndex(value, -1)) + matches := strings.Count(value, "*") if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") { value = strings.Replace(value, "*", "", -1) value = fmt.Sprintf(`has_substring("%s")`, value) From 3e0a34ceca449ace93f1206e01c270723bc6fb74 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 24 Oct 2018 10:18:28 +0200 Subject: [PATCH 13/50] typing changes --- .../AppNotifications/AppNotificationList.tsx | 9 +-------- .../AppNotifications/state/reducers.test.ts | 7 ++++--- public/app/types/alerts.ts | 11 ----------- public/app/types/appNotifications.ts | 18 ++++++++++++++++++ public/app/types/index.ts | 3 ++- 5 files changed, 25 insertions(+), 23 deletions(-) delete mode 100644 public/app/types/alerts.ts create mode 100644 public/app/types/appNotifications.ts diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index a5953156aab..10b5997ed98 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import appEvents from 'app/core/app_events'; import { addAppNotification, clearAppNotification } from './state/actions'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; -import { AppNotification, StoreState } from '../../../types'; +import { AppNotification, AppNotificationSeverity, StoreState } from 'app/types'; export interface Props { appNotifications: AppNotification[]; @@ -10,13 +10,6 @@ export interface Props { clearAppNotification: typeof clearAppNotification; } -enum AppNotificationSeverity { - Success = 'success', - Warning = 'warning', - Error = 'error', - Info = 'info', -} - export class AppNotificationList extends PureComponent { componentDidMount() { appEvents.on('alert-warning', options => this.addAppNotification(options[0], options[1], 'warning', 5000)); diff --git a/public/app/core/components/AppNotifications/state/reducers.test.ts b/public/app/core/components/AppNotifications/state/reducers.test.ts index bdc78b4fe0b..b46955a087f 100644 --- a/public/app/core/components/AppNotifications/state/reducers.test.ts +++ b/public/app/core/components/AppNotifications/state/reducers.test.ts @@ -1,5 +1,6 @@ import { appNotificationsReducer } from './reducers'; import { ActionTypes } from './actions'; +import { AppNotificationSeverity } from 'app/types'; describe('clear alert', () => { it('should filter alert', () => { @@ -10,14 +11,14 @@ describe('clear alert', () => { appNotifications: [ { id: id1, - severity: 'success', + severity: AppNotificationSeverity.Success, icon: 'success', title: 'test', text: 'test alert', }, { id: id2, - severity: 'fail', + severity: AppNotificationSeverity.Warning, icon: 'warning', title: 'test2', text: 'test alert fail 2', @@ -34,7 +35,7 @@ describe('clear alert', () => { appNotifications: [ { id: id1, - severity: 'success', + severity: AppNotificationSeverity.Success, icon: 'success', title: 'test', text: 'test alert', diff --git a/public/app/types/alerts.ts b/public/app/types/alerts.ts deleted file mode 100644 index 3f25fedbf8b..00000000000 --- a/public/app/types/alerts.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface AppNotification { - id: number; - severity: string; - icon: string; - title: string; - text: string; -} - -export interface AppNotificationsState { - appNotifications: AppNotification[]; -} diff --git a/public/app/types/appNotifications.ts b/public/app/types/appNotifications.ts new file mode 100644 index 00000000000..1bbc66a4baa --- /dev/null +++ b/public/app/types/appNotifications.ts @@ -0,0 +1,18 @@ +export interface AppNotification { + id?: number; + severity: AppNotificationSeverity; + icon: string; + title: string; + text: string; +} + +export enum AppNotificationSeverity { + Success = 'success', + Warning = 'warning', + Error = 'error', + Info = 'info', +} + +export interface AppNotificationsState { + appNotifications: AppNotification[]; +} diff --git a/public/app/types/index.ts b/public/app/types/index.ts index bc446bf3e36..b888ba83877 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,7 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; -import { AppNotification, AppNotificationsState } from './alerts'; +import { AppNotification, AppNotificationSeverity, AppNotificationsState } from './appNotifications'; export { Team, @@ -49,6 +49,7 @@ export { PluginDashboard, AppNotification, AppNotificationsState, + AppNotificationSeverity, }; export interface StoreState { From ed99a543a53afe6700eed3180f4ab4bd9369656a Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 24 Oct 2018 10:23:11 +0200 Subject: [PATCH 14/50] moving things --- .../state/actions.ts => actions/appNotification.ts} | 2 +- public/app/core/angular_wrappers.ts | 2 +- .../components/AppNotifications/AppNotificationList.tsx | 2 +- .../reducers.test.ts => reducers/appNotification.test.ts} | 6 +++--- .../state/reducers.ts => reducers/appNotification.ts} | 4 ++-- public/app/store/configureStore.ts | 2 +- public/views/index.template.html | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) rename public/app/core/{components/AppNotifications/state/actions.ts => actions/appNotification.ts} (93%) rename public/app/core/{components/AppNotifications/state/reducers.test.ts => reducers/appNotification.test.ts} (84%) rename public/app/core/{components/AppNotifications/state/reducers.ts => reducers/appNotification.ts} (90%) diff --git a/public/app/core/components/AppNotifications/state/actions.ts b/public/app/core/actions/appNotification.ts similarity index 93% rename from public/app/core/components/AppNotifications/state/actions.ts rename to public/app/core/actions/appNotification.ts index c6cfe9c3e9e..009e99b1245 100644 --- a/public/app/core/components/AppNotifications/state/actions.ts +++ b/public/app/core/actions/appNotification.ts @@ -1,4 +1,4 @@ -import { AppNotification } from 'app/types'; +import { AppNotification } from 'app/types/'; export enum ActionTypes { AddAppNotification = 'ADD_APP_NOTIFICATION', diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 14a0dcdb234..7be28272f11 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -10,7 +10,7 @@ import AppNotificationList from './components/AppNotifications/AppNotificationLi export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('sidemenu', SideMenu, []); - react2AngularDirective('pageAlertList', AppNotificationList, []); + react2AngularDirective('appNotificationsList', AppNotificationList, []); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); react2AngularDirective('searchResult', SearchResult, []); diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index 10b5997ed98..53c88a41155 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import appEvents from 'app/core/app_events'; -import { addAppNotification, clearAppNotification } from './state/actions'; +import { addAppNotification, clearAppNotification } from '../../actions/appNotification'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { AppNotification, AppNotificationSeverity, StoreState } from 'app/types'; diff --git a/public/app/core/components/AppNotifications/state/reducers.test.ts b/public/app/core/reducers/appNotification.test.ts similarity index 84% rename from public/app/core/components/AppNotifications/state/reducers.test.ts rename to public/app/core/reducers/appNotification.test.ts index b46955a087f..5098abc9e74 100644 --- a/public/app/core/components/AppNotifications/state/reducers.test.ts +++ b/public/app/core/reducers/appNotification.test.ts @@ -1,6 +1,6 @@ -import { appNotificationsReducer } from './reducers'; -import { ActionTypes } from './actions'; -import { AppNotificationSeverity } from 'app/types'; +import { appNotificationsReducer } from './appNotification'; +import { ActionTypes } from '../actions/appNotification'; +import { AppNotificationSeverity } from 'app/types/index'; describe('clear alert', () => { it('should filter alert', () => { diff --git a/public/app/core/components/AppNotifications/state/reducers.ts b/public/app/core/reducers/appNotification.ts similarity index 90% rename from public/app/core/components/AppNotifications/state/reducers.ts rename to public/app/core/reducers/appNotification.ts index 7887a91a616..8812546356a 100644 --- a/public/app/core/components/AppNotifications/state/reducers.ts +++ b/public/app/core/reducers/appNotification.ts @@ -1,5 +1,5 @@ -import { AppNotification, AppNotificationsState } from 'app/types'; -import { Action, ActionTypes } from './actions'; +import { AppNotification, AppNotificationsState } from 'app/types/index'; +import { Action, ActionTypes } from '../actions/appNotification'; export const initialState: AppNotificationsState = { appNotifications: [] as AppNotification[], diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index c1c6103bab9..6b3205dc53e 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -10,7 +10,7 @@ import dashboardReducers from 'app/features/dashboard/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; -import appNotificationReducers from 'app/core/components/AppNotifications/state/reducers'; +import appNotificationReducers from 'app/core/reducers/appNotification'; const rootReducers = { ...sharedReducers, diff --git a/public/views/index.template.html b/public/views/index.template.html index 597fc8d8f59..ced39d9af28 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -200,7 +200,7 @@ - +
From f34cbae2dddbed0d04e5a8323197ea72b8dbe895 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 24 Oct 2018 14:33:53 +0200 Subject: [PATCH 15/50] cleaned up the flow --- public/app/core/actions/appNotification.ts | 4 +- public/app/core/actions/index.ts | 3 +- .../AppNotifications/AppNotificationItem.tsx | 38 ++++++++++ .../AppNotifications/AppNotificationList.tsx | 74 +++++-------------- public/app/core/copy/appNotification.ts | 46 ++++++++++++ .../app/core/reducers/appNotification.test.ts | 5 +- public/app/core/reducers/appNotification.ts | 6 +- public/app/core/reducers/index.ts | 2 + public/app/store/configureStore.ts | 2 - public/app/types/appNotifications.ts | 7 ++ public/app/types/index.ts | 8 +- 11 files changed, 129 insertions(+), 66 deletions(-) create mode 100644 public/app/core/components/AppNotifications/AppNotificationItem.tsx create mode 100644 public/app/core/copy/appNotification.ts diff --git a/public/app/core/actions/appNotification.ts b/public/app/core/actions/appNotification.ts index 009e99b1245..b79b642eef1 100644 --- a/public/app/core/actions/appNotification.ts +++ b/public/app/core/actions/appNotification.ts @@ -22,7 +22,7 @@ export const clearAppNotification = (appNotificationId: number) => ({ payload: appNotificationId, }); -export const addAppNotification = (alert: AppNotification) => ({ +export const notifyApp = (appNotification: AppNotification) => ({ type: ActionTypes.AddAppNotification, - payload: alert, + payload: appNotification, }); diff --git a/public/app/core/actions/index.ts b/public/app/core/actions/index.ts index 451a13dae99..f7ce2dda945 100644 --- a/public/app/core/actions/index.ts +++ b/public/app/core/actions/index.ts @@ -1,4 +1,5 @@ import { updateLocation } from './location'; import { updateNavIndex, UpdateNavIndexAction } from './navModel'; +import { notifyApp, clearAppNotification } from './appNotification'; -export { updateLocation, updateNavIndex, UpdateNavIndexAction }; +export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification }; diff --git a/public/app/core/components/AppNotifications/AppNotificationItem.tsx b/public/app/core/components/AppNotifications/AppNotificationItem.tsx new file mode 100644 index 00000000000..5169c39e7a0 --- /dev/null +++ b/public/app/core/components/AppNotifications/AppNotificationItem.tsx @@ -0,0 +1,38 @@ +import React, { Component } from 'react'; +import { AppNotification } from 'app/types'; + +interface Props { + appNotification: AppNotification; + onClearNotification: (id) => void; +} + +export default class AppNotificationItem extends Component { + shouldComponentUpdate(nextProps) { + return this.props.appNotification.id !== nextProps.appNotification.id; + } + + componentDidMount() { + const { appNotification, onClearNotification } = this.props; + setTimeout(() => { + onClearNotification(appNotification.id); + }, appNotification.timeout); + } + + render() { + const { appNotification, onClearNotification } = this.props; + return ( +
+
+ +
+
+
{appNotification.title}
+
{appNotification.text}
+
+ +
+ ); + } +} diff --git a/public/app/core/components/AppNotifications/AppNotificationList.tsx b/public/app/core/components/AppNotifications/AppNotificationList.tsx index 53c88a41155..c91f8372384 100644 --- a/public/app/core/components/AppNotifications/AppNotificationList.tsx +++ b/public/app/core/components/AppNotifications/AppNotificationList.tsx @@ -1,53 +1,30 @@ import React, { PureComponent } from 'react'; import appEvents from 'app/core/app_events'; -import { addAppNotification, clearAppNotification } from '../../actions/appNotification'; +import AppNotificationItem from './AppNotificationItem'; +import { notifyApp, clearAppNotification } from 'app/core/actions'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; -import { AppNotification, AppNotificationSeverity, StoreState } from 'app/types'; +import { AppNotification, StoreState } from 'app/types'; +import { + createErrorNotification, + createSuccessNotification, + createWarningNotification, +} from '../../copy/appNotification'; export interface Props { appNotifications: AppNotification[]; - addAppNotification: typeof addAppNotification; + notifyApp: typeof notifyApp; clearAppNotification: typeof clearAppNotification; } export class AppNotificationList extends PureComponent { componentDidMount() { - appEvents.on('alert-warning', options => this.addAppNotification(options[0], options[1], 'warning', 5000)); - appEvents.on('alert-success', options => this.addAppNotification(options[0], options[1], 'success', 3000)); - appEvents.on('alert-error', options => this.addAppNotification(options[0], options[1], 'error', 7000)); + const { notifyApp } = this.props; + + appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1]))); + appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1]))); + appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1]))); } - addAppNotification(title, text, severity, timeout) { - const id = Date.now(); - const newAlert = { - id: id, - title: title || '', - text: text || '', - severity: severity || AppNotificationSeverity.Info, - icon: this.getIconForSeverity(severity), - remove: this.clearAutomatically(id, timeout), - }; - - this.props.addAppNotification(newAlert); - } - - getIconForSeverity(severity) { - switch (severity) { - case AppNotificationSeverity.Success: - return 'fa fa-check'; - case AppNotificationSeverity.Error: - return 'fa fa-exclamation-triangle'; - default: - return 'fa fa-exclamation'; - } - } - - clearAutomatically = (id, timeout) => { - setTimeout(() => { - this.props.clearAppNotification(id); - }, timeout); - }; - onClearAppNotification = id => { this.props.clearAppNotification(id); }; @@ -59,22 +36,11 @@ export class AppNotificationList extends PureComponent {
{appNotifications.map((appNotification, index) => { return ( -
-
- -
-
-
{appNotification.title}
-
{appNotification.text}
-
- -
+ this.onClearAppNotification(id)} + /> ); })}
@@ -87,7 +53,7 @@ const mapStateToProps = (state: StoreState) => ({ }); const mapDispatchToProps = { - addAppNotification, + notifyApp, clearAppNotification, }; diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts new file mode 100644 index 00000000000..c34480d7aad --- /dev/null +++ b/public/app/core/copy/appNotification.ts @@ -0,0 +1,46 @@ +import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types'; + +const defaultSuccessNotification: AppNotification = { + title: '', + text: '', + severity: AppNotificationSeverity.Success, + icon: 'fa fa-check', + timeout: AppNotificationTimeout.Success, +}; + +const defaultWarningNotification: AppNotification = { + title: '', + text: '', + severity: AppNotificationSeverity.Warning, + icon: 'fa fa-exclamation', + timeout: AppNotificationTimeout.Warning, +}; + +const defaultErrorNotification: AppNotification = { + title: '', + text: '', + severity: AppNotificationSeverity.Error, + icon: 'fa fa-exclamation-triangle', + timeout: AppNotificationTimeout.Error, +}; + +export const createSuccessNotification = (title: string, text?: string): AppNotification => ({ + ...defaultSuccessNotification, + title: title, + text: text, + id: Date.now(), +}); + +export const createErrorNotification = (title: string, text?: string): AppNotification => ({ + ...defaultErrorNotification, + title: title, + text: text, + id: Date.now(), +}); + +export const createWarningNotification = (title: string, text?: string): AppNotification => ({ + ...defaultWarningNotification, + title: title, + text: text, + id: Date.now(), +}); diff --git a/public/app/core/reducers/appNotification.test.ts b/public/app/core/reducers/appNotification.test.ts index 5098abc9e74..183b699f5fc 100644 --- a/public/app/core/reducers/appNotification.test.ts +++ b/public/app/core/reducers/appNotification.test.ts @@ -1,6 +1,6 @@ import { appNotificationsReducer } from './appNotification'; import { ActionTypes } from '../actions/appNotification'; -import { AppNotificationSeverity } from 'app/types/index'; +import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/'; describe('clear alert', () => { it('should filter alert', () => { @@ -15,6 +15,7 @@ describe('clear alert', () => { icon: 'success', title: 'test', text: 'test alert', + timeout: AppNotificationTimeout.Success, }, { id: id2, @@ -22,6 +23,7 @@ describe('clear alert', () => { icon: 'warning', title: 'test2', text: 'test alert fail 2', + timeout: AppNotificationTimeout.Warning, }, ], }; @@ -39,6 +41,7 @@ describe('clear alert', () => { icon: 'success', title: 'test', text: 'test alert', + timeout: AppNotificationTimeout.Success, }, ], }; diff --git a/public/app/core/reducers/appNotification.ts b/public/app/core/reducers/appNotification.ts index 8812546356a..2c8bbbbd84d 100644 --- a/public/app/core/reducers/appNotification.ts +++ b/public/app/core/reducers/appNotification.ts @@ -1,4 +1,4 @@ -import { AppNotification, AppNotificationsState } from 'app/types/index'; +import { AppNotification, AppNotificationsState } from 'app/types/'; import { Action, ActionTypes } from '../actions/appNotification'; export const initialState: AppNotificationsState = { @@ -17,7 +17,3 @@ export const appNotificationsReducer = (state = initialState, action: Action): A } return state; }; - -export default { - appNotifications: appNotificationsReducer, -}; diff --git a/public/app/core/reducers/index.ts b/public/app/core/reducers/index.ts index be13528c91c..1c8670ed0d6 100644 --- a/public/app/core/reducers/index.ts +++ b/public/app/core/reducers/index.ts @@ -1,7 +1,9 @@ import { navIndexReducer as navIndex } from './navModel'; import { locationReducer as location } from './location'; +import { appNotificationsReducer as appNotifications } from './appNotification'; export default { navIndex, location, + appNotifications, }; diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 6b3205dc53e..ccd027a0b6d 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -10,7 +10,6 @@ import dashboardReducers from 'app/features/dashboard/state/reducers'; import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; -import appNotificationReducers from 'app/core/reducers/appNotification'; const rootReducers = { ...sharedReducers, @@ -22,7 +21,6 @@ const rootReducers = { ...pluginReducers, ...dataSourcesReducers, ...usersReducers, - ...appNotificationReducers, }; export let store; diff --git a/public/app/types/appNotifications.ts b/public/app/types/appNotifications.ts index 1bbc66a4baa..81e6cfd55e1 100644 --- a/public/app/types/appNotifications.ts +++ b/public/app/types/appNotifications.ts @@ -4,6 +4,7 @@ export interface AppNotification { icon: string; title: string; text: string; + timeout: AppNotificationTimeout; } export enum AppNotificationSeverity { @@ -13,6 +14,12 @@ export enum AppNotificationSeverity { Info = 'info', } +export enum AppNotificationTimeout { + Warning = 5000, + Success = 3000, + Error = 7000, +} + export interface AppNotificationsState { appNotifications: AppNotification[]; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index b888ba83877..0cb1b196419 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -9,7 +9,12 @@ import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; import { DataSource, DataSourcesState } from './datasources'; import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; -import { AppNotification, AppNotificationSeverity, AppNotificationsState } from './appNotifications'; +import { + AppNotification, + AppNotificationSeverity, + AppNotificationsState, + AppNotificationTimeout, +} from './appNotifications'; export { Team, @@ -50,6 +55,7 @@ export { AppNotification, AppNotificationsState, AppNotificationSeverity, + AppNotificationTimeout, }; export interface StoreState { From 54a3e2d1d1d710dc9f7be4b831a31a7810c71186 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 24 Oct 2018 14:55:56 +0200 Subject: [PATCH 16/50] Added types to query rows --- public/app/features/explore/QueryRows.tsx | 40 ++++++++++++++++--- .../datasource/prometheus/query_hints.ts | 4 +- public/app/types/explore.ts | 19 ++++++++- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 0b0d7085d2d..4024022851e 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -1,12 +1,12 @@ import React, { PureComponent } from 'react'; -import { QueryTransaction } from 'app/types/explore'; +import { QueryTransaction, HistoryItem, Query, QueryHint } from 'app/types/explore'; // TODO make this datasource-plugin-dependent import QueryField from './PromQueryField'; import QueryTransactions from './QueryTransactions'; -function getFirstHintFromTransactions(transactions: QueryTransaction[]) { +function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); if (transaction) { return transaction.hints[0]; @@ -14,7 +14,30 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]) { return undefined; } -class QueryRow extends PureComponent { +interface QueryRowEventHandlers { + onAddQueryRow: (index: number) => void; + onChangeQuery: (value: string, index: number, override?: boolean) => void; + onClickHintFix: (action: object, index?: number) => void; + onExecuteQuery: () => void; + onRemoveQueryRow: (index: number) => void; +} + +interface QueryRowCommonProps { + className?: string; + history: HistoryItem[]; + request: (url: string) => Promise; + // Temporarily + supportsLogs?: boolean; + transactions: QueryTransaction[]; +} + +type QueryRowProps = QueryRowCommonProps & + QueryRowEventHandlers & { + index: number; + query: string; + }; + +class QueryRow extends PureComponent { onChangeQuery = (value, override?: boolean) => { const { index, onChangeQuery } = this.props; if (onChangeQuery) { @@ -56,7 +79,7 @@ class QueryRow extends PureComponent { render() { const { history, query, request, supportsLogs, transactions } = this.props; - const transactionWithError = transactions.find(t => t.error); + const transactionWithError = transactions.find(t => t.error !== undefined); const hint = getFirstHintFromTransactions(transactions); const queryError = transactionWithError ? transactionWithError.error : null; return ( @@ -93,9 +116,14 @@ class QueryRow extends PureComponent { } } -export default class QueryRows extends PureComponent { +type QueryRowsProps = QueryRowCommonProps & + QueryRowEventHandlers & { + queries: Query[]; + }; + +export default class QueryRows extends PureComponent { render() { - const { className = '', queries, queryHints, transactions, ...handlers } = this.props; + const { className = '', queries, transactions, ...handlers } = this.props; return (
{queries.map((q, index) => ( diff --git a/public/app/plugins/datasource/prometheus/query_hints.ts b/public/app/plugins/datasource/prometheus/query_hints.ts index cfd04c766ba..388a9be48d1 100644 --- a/public/app/plugins/datasource/prometheus/query_hints.ts +++ b/public/app/plugins/datasource/prometheus/query_hints.ts @@ -1,6 +1,8 @@ import _ from 'lodash'; -export function getQueryHints(query: string, series?: any[], datasource?: any): any[] { +import { QueryHint } from 'app/types/explore'; + +export function getQueryHints(query: string, series?: any[], datasource?: any): QueryHint[] { const hints = []; // ..._bucket metric needs a histogram_quantile() diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 918dd4e4483..8746dd2edf6 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -18,11 +18,28 @@ export interface Query { key?: string; } +export interface QueryFix { + type: string; + label: string; + action?: QueryFixAction; +} + +export interface QueryFixAction { + type: string; + query?: string; +} + +export interface QueryHint { + type: string; + label: string; + fix?: QueryFix; +} + export interface QueryTransaction { id: string; done: boolean; error?: string; - hints?: any[]; + hints?: QueryHint[]; latency: number; options: any; query: string; From 08631ea23f972c048fe789698b4e953437658cb8 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 25 Oct 2018 13:03:20 +0900 Subject: [PATCH 17/50] support template variable in stat field --- public/app/plugins/datasource/cloudwatch/datasource.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index b4f739f934c..3eb8eff2d09 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -37,6 +37,9 @@ export default class CloudWatchDatasource { item.namespace = this.templateSrv.replace(item.namespace, options.scopedVars); item.metricName = this.templateSrv.replace(item.metricName, options.scopedVars); item.dimensions = this.convertDimensionFormat(item.dimensions, options.scopedVars); + item.statistics = item.statistics.map(s => { + return this.templateSrv.replace(s, options.scopedVars); + }); item.period = String(this.getPeriod(item, options)); // use string format for period in graph query, and alerting item.id = this.templateSrv.replace(item.id, options.scopedVars); item.expression = this.templateSrv.replace(item.expression, options.scopedVars); From 97b22aa5a90b4869bfa35cdf880feca9b4b9cff3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 25 Oct 2018 10:29:40 +0200 Subject: [PATCH 18/50] mysql: fix timeFilter macro should respect local time zone --- pkg/tsdb/mysql/macros.go | 2 +- pkg/tsdb/mysql/macros_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index a037aa9277a..839f805568e 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -60,7 +60,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", args[0], m.timeRange.GetFromAsSecondsEpoch(), m.timeRange.GetToAsSecondsEpoch()), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index 3c9a5a26c94..24bf18873d5 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -60,7 +60,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix())) }) Convey("interpolate __unixEpochFilter function", func() { @@ -92,7 +92,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix())) }) Convey("interpolate __unixEpochFilter function", func() { @@ -112,7 +112,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix())) }) Convey("interpolate __unixEpochFilter function", func() { From defccb5ab3591414f587b3e3414743fb07065ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 25 Oct 2018 10:32:23 +0200 Subject: [PATCH 19/50] fix panel solo size --- public/sass/pages/_dashboard.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index d9ab29cc91c..795766a22de 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -21,6 +21,9 @@ div.flot-text { height: 100%; &--solo { + position: fixed; + bottom: 0; + right: 0; margin: 0; .panel-container { border: none; From be6f68f341a3b10ae483236d483eafecb09a3bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 25 Oct 2018 11:35:32 +0200 Subject: [PATCH 20/50] fix for annotation promise clearing, bug introduced last week when merging react panels step1 --- .../features/annotations/annotations_srv.ts | 27 ++++++++++++------- .../app/features/dashboard/dashboard_ctrl.ts | 12 +++++++-- public/app/features/panel/solo_panel_ctrl.ts | 2 +- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index 19850da52d9..4fe6e10b2cf 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -1,25 +1,32 @@ -import './editor_ctrl'; - +// Libaries import angular from 'angular'; import _ from 'lodash'; + +// Components +import './editor_ctrl'; import coreModule from 'app/core/core_module'; + +// Utils & Services import { makeRegions, dedupAnnotations } from './events_processing'; +// Types +import { DashboardModel } from '../dashboard/dashboard_model'; + export class AnnotationsSrv { globalAnnotationsPromise: any; alertStatesPromise: any; datasourcePromises: any; /** @ngInject */ - constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) { - $rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope); - $rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope); - } + constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {} - clearCache() { - this.globalAnnotationsPromise = null; - this.alertStatesPromise = null; - this.datasourcePromises = null; + init(dashboard: DashboardModel) { + // clear promises on refresh events + dashboard.on('refresh', () => { + this.globalAnnotationsPromise = null; + this.alertStatesPromise = null; + this.datasourcePromises = null; + }); } getAnnotations(options) { diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index c34b9ddaff2..5871a579f3c 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -1,6 +1,12 @@ +// Utils import config from 'app/core/config'; - +import appEvents from 'app/core/app_events'; import coreModule from 'app/core/core_module'; + +// Services +import { AnnotationsSrv } from '../annotations/annotations_srv'; + +// Types import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; @@ -21,6 +27,7 @@ export class DashboardCtrl { private dashboardSrv, private unsavedChangesSrv, private dashboardViewStateSrv, + private annotationsSrv: AnnotationsSrv, public playlistSrv ) { // temp hack due to way dashboards are loaded @@ -49,6 +56,7 @@ export class DashboardCtrl { // init services this.timeSrv.init(dashboard); this.alertingSrv.init(dashboard, data.alerts); + this.annotationsSrv.init(dashboard); // template values service needs to initialize completely before // the rest of the dashboard can load @@ -72,7 +80,7 @@ export class DashboardCtrl { this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); this.setWindowTitleAndTheme(); - this.$scope.appEvent('dashboard-initialized', dashboard); + appEvents.emit('dashboard-initialized', dashboard); }) .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); } diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts index 15d35188d6d..a8bf5371913 100644 --- a/public/app/features/panel/solo_panel_ctrl.ts +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -14,7 +14,7 @@ export class SoloPanelCtrl { const params = $location.search(); panelId = parseInt(params.panelId, 10); - $scope.onAppEvent('dashboard-initialized', $scope.initPanelScope); + appEvents.on('dashboard-initialized', $scope.initPanelScope); // if no uid, redirect to new route based on slug if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) { From 6f2315d5c585d0a88d274fbf7e8b8c77422821b9 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 25 Oct 2018 12:24:24 +0200 Subject: [PATCH 21/50] Moved prom language features to datasource language provider --- public/app/features/explore/Explore.tsx | 7 +- .../features/explore/PromQueryField.test.tsx | 229 +---------- .../app/features/explore/PromQueryField.tsx | 374 ++---------------- public/app/features/explore/QueryField.tsx | 88 +---- public/app/features/explore/QueryRows.tsx | 6 +- public/app/features/explore/Typeahead.tsx | 20 +- .../datasource/prometheus/datasource.ts | 3 + .../prometheus/language_provider.ts | 334 ++++++++++++++++ .../datasource/prometheus/language_utils.ts} | 3 - .../datasource/prometheus}/promql.ts | 0 .../specs/language_provider.test.ts | 202 ++++++++++ .../prometheus/specs/language_utils.test.ts} | 2 +- public/app/types/explore.ts | 92 +++++ 13 files changed, 683 insertions(+), 677 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/language_provider.ts rename public/app/{features/explore/utils/prometheus.ts => plugins/datasource/prometheus/language_utils.ts} (96%) rename public/app/{features/explore/slate-plugins/prism => plugins/datasource/prometheus}/promql.ts (100%) create mode 100644 public/app/plugins/datasource/prometheus/specs/language_provider.test.ts rename public/app/{features/explore/utils/prometheus.test.ts => plugins/datasource/prometheus/specs/language_utils.test.ts} (97%) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index bac063116f1..680cd1e6685 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -695,11 +695,6 @@ export class Explore extends React.PureComponent { }); } - request = url => { - const { datasource } = this.state; - return datasource.metadataRequest(url); - }; - cloneState(): ExploreState { // Copy state, but copy queries including modifications return { @@ -831,9 +826,9 @@ export class Explore extends React.PureComponent { {datasource && !datasourceError ? (
{ - const defaultProps = { - request: () => ({ data: { data: [] } }), - }; - - it('returns default suggestions on emtpty context', () => { - const instance = shallow().instance() as PromQueryField; - const result = instance.getTypeahead({ text: '', prefix: '', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); - }); - - describe('range suggestions', () => { - it('returns range suggestions in range context', () => { - const instance = shallow().instance() as PromQueryField; - const result = instance.getTypeahead({ text: '1', prefix: '1', wrapperClasses: ['context-range'] }); - expect(result.context).toBe('context-range'); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions).toEqual([ - { - items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }], - label: 'Range vector', - }, - ]); - }); - }); - - describe('metric suggestions', () => { - it('returns metrics suggestions by default', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const result = instance.getTypeahead({ text: 'a', prefix: 'a', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); - }); - - it('returns default suggestions after a binary operator', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const result = instance.getTypeahead({ text: '*', prefix: '', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); - }); - }); - - describe('label suggestions', () => { - it('returns default label suggestions on label context and no metric', () => { - const instance = shallow().instance() as PromQueryField; - const value = Plain.deserialize('{}'); - const range = value.selection.merge({ - anchorOffset: 1, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-labels'); - expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]); - }); - - it('returns label suggestions on label context and metric', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('metric{}'); - const range = value.selection.merge({ - anchorOffset: 7, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-labels'); - expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); - }); - - it('returns label suggestions on label context but leaves out labels that already exist', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}'); - const range = value.selection.merge({ - anchorOffset: 36, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-labels'); - expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); - }); - - it('returns label value suggestions inside a label value context after a negated matching operator', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('{label!=}'); - const range = value.selection.merge({ anchorOffset: 8 }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '!=', - prefix: '', - wrapperClasses: ['context-labels'], - labelKey: 'label', - value: valueWithSelection, - }); - expect(result.context).toBe('context-label-values'); - expect(result.suggestions).toEqual([ - { - items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }], - label: 'Label values for "label"', - }, - ]); - }); - - it('returns a refresher on label context and unavailable metric', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('metric{}'); - const range = value.selection.merge({ - anchorOffset: 7, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeInstanceOf(Promise); - expect(result.suggestions).toEqual([]); - }); - - it('returns label values on label context when given a metric and a label key', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('metric{bar=ba}'); - const range = value.selection.merge({ - anchorOffset: 13, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '=ba', - prefix: 'ba', - wrapperClasses: ['context-labels'], - labelKey: 'bar', - value: valueWithSelection, - }); - expect(result.context).toBe('context-label-values'); - expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]); - }); - - it('returns label suggestions on aggregation context and metric w/ selector', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('sum(metric{foo="xx"}) by ()'); - const range = value.selection.merge({ - anchorOffset: 26, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); - }); - - it('returns label suggestions on aggregation context and metric w/o selector', () => { - const instance = shallow( - - ).instance() as PromQueryField; - const value = Plain.deserialize('sum(metric) by ()'); - const range = value.selection.merge({ - anchorOffset: 16, - }); - const valueWithSelection = value.change().select(range).value; - const result = instance.getTypeahead({ - text: '', - prefix: '', - wrapperClasses: ['context-aggregation'], - value: valueWithSelection, - }); - expect(result.context).toBe('context-aggregation'); - expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); - }); - }); -}); +import { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField'; describe('groupMetricsByPrefix()', () => { it('returns an empty group for no metrics', () => { diff --git a/public/app/features/explore/PromQueryField.tsx b/public/app/features/explore/PromQueryField.tsx index 442e51af987..58856e3116f 100644 --- a/public/app/features/explore/PromQueryField.tsx +++ b/public/app/features/explore/PromQueryField.tsx @@ -1,67 +1,23 @@ import _ from 'lodash'; -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'; +import { TypeaheadOutput } from 'app/types/explore'; + // dom also includes Element polyfills import { getNextCharacter, getPreviousCousin } from './utils/dom'; -import PrismPromql, { FUNCTIONS } from './slate-plugins/prism/promql'; import BracesPlugin from './slate-plugins/braces'; import RunnerPlugin from './slate-plugins/runner'; -import { processLabels, RATE_RANGES, cleanText, parseSelector } from './utils/prometheus'; -import TypeaheadField, { - Suggestion, - SuggestionGroup, - TypeaheadInput, - TypeaheadFieldState, - TypeaheadOutput, -} from './QueryField'; +import TypeaheadField, { TypeaheadInput, TypeaheadFieldState } from './QueryField'; -const DEFAULT_KEYS = ['job', 'instance']; -const EMPTY_SELECTOR = '{}'; const HISTOGRAM_GROUP = '__histograms__'; -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_SYNTAX = 'promql'; export const RECORDING_RULES_GROUP = '__recording_rules__'; -export const wrapLabel = (label: string) => ({ label }); -export const setFunctionMove = (suggestion: Suggestion): Suggestion => { - suggestion.move = -1; - 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); - const count = historyForItem.length; - const recent = historyForItem[0]; - let hint = `Queried ${count} times in the last 24h.`; - if (recent) { - const lastQueried = moment(recent.ts).fromNow(); - hint = `${hint} Last queried ${lastQueried}.`; - } - return { - ...item, - documentation: hint, - }; -} - export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] { // Filter out recording rules and insert as first option const ruleRegex = /:\w+:/; @@ -133,48 +89,36 @@ interface CascaderOption { } interface PromQueryFieldProps { + datasource: any; error?: string; hint?: any; - histogramMetrics?: string[]; history?: any[]; initialQuery?: string | null; - labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] - labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] - metrics?: string[]; metricsByPrefix?: CascaderOption[]; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; onQueryChange?: (value: string, override?: boolean) => void; - portalOrigin?: 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[]; metricsOptions: any[]; metricsByPrefix: CascaderOption[]; syntaxLoaded: boolean; } -interface PromTypeaheadInput { - text: string; - prefix: string; - wrapperClasses: string[]; - labelKey?: string; - value?: Value; -} - class PromQueryField extends React.PureComponent { plugins: any[]; + languageProvider: any; constructor(props: PromQueryFieldProps, context) { super(props, context); + if (props.datasource.languageProvider) { + this.languageProvider = props.datasource.languageProvider; + } + this.plugins = [ BracesPlugin(), RunnerPlugin({ handler: props.onPressEnter }), @@ -185,26 +129,16 @@ class PromQueryField extends React.PureComponent this.onReceiveMetrics()); } } @@ -262,15 +196,19 @@ class PromQueryField extends React.PureComponent { - const { histogramMetrics, metrics, metricsByPrefix } = this.state; + const { histogramMetrics, metrics } = this.languageProvider; if (!metrics) { return; } - // Update global prism config - setPrismTokens(PRISM_SYNTAX, METRIC_MARK, metrics); + Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax(); + Prism.languages[PRISM_SYNTAX][METRIC_MARK] = { + alias: 'variable', + pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`), + }; // Build metrics tree + const metricsByPrefix = groupMetricsByPrefix(metrics); const histogramOptions = histogramMetrics.map(hm => ({ label: hm, value: hm })); const metricsOptions = [ { label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions }, @@ -281,6 +219,11 @@ class PromQueryField extends React.PureComponent { + if (!this.languageProvider) { + return { suggestions: [] }; + } + + const { history } = this.props; const { prefix, text, value, wrapperNode } = typeahead; // Get DOM-dependent context @@ -289,279 +232,20 @@ class PromQueryField extends React.PureComponent 3; - // Determine candidates by CSS context - if (_.includes(wrapperClasses, 'context-range')) { - // Suggestions for metric[|] - return this.getRangeTypeahead(); - } else if (_.includes(wrapperClasses, 'context-labels')) { - // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} - return this.getLabelTypeahead.apply(this, arguments); - } else if (_.includes(wrapperClasses, 'context-aggregation')) { - return this.getAggregationTypeahead.apply(this, arguments); - } else if ( - // Show default suggestions in a couple of scenarios - (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token - (prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace - text.match(/[+\-*/^%]/) // Anything after binary operator - ) { - return this.getEmptyTypeahead(); - } - - return { - suggestions: [], - }; - } - - getEmptyTypeahead(): TypeaheadOutput { - const { history } = this.props; - const { metrics } = this.state; - const suggestions: SuggestionGroup[] = []; - - if (history && history.length > 0) { - const historyItems = _.chain(history) - .uniqBy('query') - .take(HISTORY_ITEM_COUNT) - .map(h => h.query) - .map(wrapLabel) - .map(item => addHistoryMetadata(item, history)) - .value(); - - suggestions.push({ - prefixMatch: true, - skipSort: true, - label: 'History', - items: historyItems, - }); - } - - suggestions.push({ - prefixMatch: true, - label: 'Functions', - items: FUNCTIONS.map(setFunctionMove), - }); - - if (metrics) { - suggestions.push({ - label: 'Metrics', - items: metrics.map(wrapLabel), - }); - } - return { suggestions }; - } - - getRangeTypeahead(): TypeaheadOutput { - return { - context: 'context-range', - suggestions: [ - { - label: 'Range vector', - items: [...RATE_RANGES].map(wrapLabel), - }, - ], - }; - } - - getAggregationTypeahead({ value }: PromTypeaheadInput): TypeaheadOutput { - let refresher: Promise = null; - const suggestions: SuggestionGroup[] = []; - - // sum(foo{bar="1"}) by (|) - const line = value.anchorBlock.getText(); - const cursorOffset: number = value.anchorOffset; - // sum(foo{bar="1"}) by ( - const leftSide = line.slice(0, cursorOffset); - const openParensAggregationIndex = leftSide.lastIndexOf('('); - const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); - const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; - // foo{bar="1"} - const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); - const selector = parseSelector(selectorString, selectorString.length - 2).selector; - - const labelKeys = this.state.labelKeys[selector]; - if (labelKeys) { - suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); - } else { - refresher = this.fetchSeriesLabels(selector); - } - - return { - refresher, - suggestions, - context: 'context-aggregation', - }; - } - - getLabelTypeahead({ text, wrapperClasses, labelKey, value }: PromTypeaheadInput): TypeaheadOutput { - let context: string; - let refresher: Promise = null; - const suggestions: SuggestionGroup[] = []; - const line = value.anchorBlock.getText(); - const cursorOffset: number = value.anchorOffset; - - // Get normalized selector - let selector; - let parsedSelector; - try { - parsedSelector = parseSelector(line, cursorOffset); - selector = parsedSelector.selector; - } catch { - selector = EMPTY_SELECTOR; - } - const containsMetric = selector.indexOf('__name__=') > -1; - const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; - - if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { - // Label values - if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) { - const labelValues = this.state.labelValues[selector][labelKey]; - context = 'context-label-values'; - suggestions.push({ - label: `Label values for "${labelKey}"`, - items: labelValues.map(wrapLabel), - }); - } - } else { - // Label keys - const labelKeys = this.state.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS); - if (labelKeys) { - const possibleKeys = _.difference(labelKeys, existingKeys); - if (possibleKeys.length > 0) { - context = 'context-labels'; - suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) }); - } - } - } - - // Query labels for selector - // Temporarily add skip for logging - if (selector && !this.state.labelValues[selector] && !this.props.supportsLogs) { - if (selector === EMPTY_SELECTOR) { - // Query label values for default labels - refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key))); - } else { - refresher = this.fetchSeriesLabels(selector, !containsMetric); - } - } - - return { context, refresher, suggestions }; - } - - request = url => { - if (this.props.request) { - return this.props.request(url); - } - return fetch(url); - }; - - fetchHistogramMetrics() { - this.fetchSeriesLabels(HISTOGRAM_SELECTOR, true, () => { - const histogramSeries = this.state.labelValues[HISTOGRAM_SELECTOR]; - if (histogramSeries && histogramSeries['__name__']) { - const histogramMetrics = histogramSeries['__name__'].slice().sort(); - this.setState({ histogramMetrics }, this.onReceiveMetrics); - } - }); - } - - // Temporarily here while reusing this field for logging - async fetchLogLabels() { - const url = '/api/prom/label'; - try { - const res = await this.request(url); - const body = await (res.data || res.json()); - const labelKeys = body.data.slice().sort(); - const labelKeysBySelector = { - ...this.state.labelKeys, - [EMPTY_SELECTOR]: labelKeys, - }; - const labelValuesByKey = {}; - const logLabelOptions = []; - for (const key of labelKeys) { - const valuesUrl = `/api/prom/label/${key}/values`; - const res = await this.request(valuesUrl); - const body = await (res.data || res.json()); - const values = body.data.slice().sort(); - labelValuesByKey[key] = values; - logLabelOptions.push({ - label: key, - value: key, - children: values.map(value => ({ 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 { - const res = await this.request(url); - const body = await (res.data || res.json()); - const exisingValues = this.state.labelValues[EMPTY_SELECTOR]; - const values = { - ...exisingValues, - [key]: body.data, - }; - const labelValues = { - ...this.state.labelValues, - [EMPTY_SELECTOR]: values, - }; - this.setState({ labelValues }); - } catch (e) { - console.error(e); - } - } - - async fetchSeriesLabels(name: string, withName?: boolean, callback?: () => void) { - const url = `/api/v1/series?match[]=${name}`; - try { - const res = await this.request(url); - const body = await (res.data || res.json()); - const { keys, values } = processLabels(body.data, withName); - const labelKeys = { - ...this.state.labelKeys, - [name]: keys, - }; - const labelValues = { - ...this.state.labelValues, - [name]: values, - }; - this.setState({ labelKeys, labelValues }, callback); - } catch (e) { - console.error(e); - } - } - - async fetchMetricNames() { - const url = '/api/v1/label/__name__/values'; - try { - const res = await this.request(url); - const body = await (res.data || res.json()); - const metrics = body.data; - const metricsByPrefix = groupMetricsByPrefix(metrics); - this.setState({ metrics, metricsByPrefix }, this.onReceiveMetrics); - } catch (error) { - console.error(error); - } - } - render() { const { error, hint, initialQuery, supportsLogs } = this.props; const { logLabelOptions, metricsOptions, syntaxLoaded } = this.state; + const cleanText = this.languageProvider ? this.languageProvider.cleanText : undefined; return (
diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index ce0bcd71ed0..86daaa43eac 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -5,6 +5,8 @@ import { Change, Value } from 'slate'; import { Editor } from 'slate-react'; import Plain from 'slate-plain-serializer'; +import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore'; + import ClearPlugin from './slate-plugins/clear'; import NewlinePlugin from './slate-plugins/newline'; @@ -13,87 +15,17 @@ import { makeFragment, makeValue } from './Value'; export const TYPEAHEAD_DEBOUNCE = 100; -function getSuggestionByIndex(suggestions: SuggestionGroup[], index: number): Suggestion { +function getSuggestionByIndex(suggestions: CompletionItemGroup[], index: number): CompletionItem { // Flatten suggestion groups const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []); const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length; return flattenedSuggestions[correctedIndex]; } -function hasSuggestions(suggestions: SuggestionGroup[]): boolean { +function hasSuggestions(suggestions: CompletionItemGroup[]): boolean { return suggestions && suggestions.length > 0; } -export interface Suggestion { - /** - * The label of this completion item. By default - * this is also the text that is inserted when selecting - * this completion. - */ - label: string; - /** - * The kind of this completion item. Based on the kind - * an icon is chosen by the editor. - */ - kind?: string; - /** - * A human-readable string with additional information - * about this item, like type or symbol information. - */ - detail?: string; - /** - * A human-readable string, can be Markdown, that represents a doc-comment. - */ - documentation?: string; - /** - * A string that should be used when comparing this item - * with other items. When `falsy` the `label` is used. - */ - sortText?: string; - /** - * A string that should be used when filtering a set of - * completion items. When `falsy` the `label` is used. - */ - filterText?: string; - /** - * A string or snippet that should be inserted in a document when selecting - * this completion. When `falsy` the `label` is used. - */ - insertText?: string; - /** - * Delete number of characters before the caret position, - * by default the letters from the beginning of the word. - */ - deleteBackwards?: number; - /** - * Number of steps to move after the insertion, can be negative. - */ - move?: number; -} - -export interface SuggestionGroup { - /** - * Label that will be displayed for all entries of this group. - */ - label: string; - /** - * List of suggestions of this group. - */ - items: Suggestion[]; - /** - * If true, match only by prefix (and not mid-word). - */ - prefixMatch?: boolean; - /** - * If true, do not filter items in this group based on the search. - */ - skipFilter?: boolean; - /** - * If true, do not sort items. - */ - skipSort?: boolean; -} - interface TypeaheadFieldProps { additionalPlugins?: any[]; cleanText?: (text: string) => string; @@ -110,7 +42,7 @@ interface TypeaheadFieldProps { } export interface TypeaheadFieldState { - suggestions: SuggestionGroup[]; + suggestions: CompletionItemGroup[]; typeaheadContext: string | null; typeaheadIndex: number; typeaheadPrefix: string; @@ -127,12 +59,6 @@ export interface TypeaheadInput { wrapperNode: Element; } -export interface TypeaheadOutput { - context?: string; - refresher?: Promise<{}>; - suggestions: SuggestionGroup[]; -} - class QueryField extends React.PureComponent { menuEl: HTMLElement | null; plugins: any[]; @@ -293,7 +219,7 @@ class QueryField extends React.PureComponent { + onClickMenu = (item: CompletionItem) => { // Manually triggering change const change = this.applyTypeahead(this.state.value.change(), item); this.onChange(change); diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index 4024022851e..aaa7dfcd20b 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -24,8 +24,8 @@ interface QueryRowEventHandlers { interface QueryRowCommonProps { className?: string; + datasource: any; history: HistoryItem[]; - request: (url: string) => Promise; // Temporarily supportsLogs?: boolean; transactions: QueryTransaction[]; @@ -78,7 +78,7 @@ class QueryRow extends PureComponent { }; render() { - const { history, query, request, supportsLogs, transactions } = this.props; + const { datasource, history, query, supportsLogs, transactions } = this.props; const transactionWithError = transactions.find(t => t.error !== undefined); const hint = getFirstHintFromTransactions(transactions); const queryError = transactionWithError ? transactionWithError.error : null; @@ -89,6 +89,7 @@ class QueryRow extends PureComponent {
{ onClickHintFix={this.onClickHintFix} onPressEnter={this.onPressEnter} onQueryChange={this.onChangeQuery} - request={request} supportsLogs={supportsLogs} />
diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 0c01cbe01ba..13882e030f6 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Highlighter from 'react-highlight-words'; -import { Suggestion, SuggestionGroup } from './QueryField'; +import { CompletionItem, CompletionItemGroup } from 'app/types/explore'; function scrollIntoView(el: HTMLElement) { if (!el || !el.offsetParent) { @@ -15,12 +15,12 @@ function scrollIntoView(el: HTMLElement) { interface TypeaheadItemProps { isSelected: boolean; - item: Suggestion; + item: CompletionItem; onClickItem: (Suggestion) => void; prefix?: string; } -class TypeaheadItem extends React.PureComponent { +class TypeaheadItem extends React.PureComponent { el: HTMLElement; componentDidUpdate(prevProps) { @@ -53,14 +53,14 @@ class TypeaheadItem extends React.PureComponent { } interface TypeaheadGroupProps { - items: Suggestion[]; + items: CompletionItem[]; label: string; - onClickItem: (Suggestion) => void; - selected: Suggestion; + onClickItem: (CompletionItem) => void; + selected: CompletionItem; prefix?: string; } -class TypeaheadGroup extends React.PureComponent { +class TypeaheadGroup extends React.PureComponent { render() { const { items, label, selected, onClickItem, prefix } = this.props; return ( @@ -85,13 +85,13 @@ class TypeaheadGroup extends React.PureComponent { } interface TypeaheadProps { - groupedItems: SuggestionGroup[]; + groupedItems: CompletionItemGroup[]; menuRef: any; - selectedItem: Suggestion | null; + selectedItem: CompletionItem | null; onClickItem: (Suggestion) => void; prefix?: string; } -class Typeahead extends React.PureComponent { +class Typeahead extends React.PureComponent { render() { const { groupedItems, menuRef, selectedItem, onClickItem, prefix } = this.props; return ( diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 89f88a946c2..c2740a7c32e 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -5,6 +5,7 @@ import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; import { ResultTransformer } from './result_transformer'; +import PrometheusLanguageProvider from './language_provider'; import { BackendSrv } from 'app/core/services/backend_srv'; import addLabelToQuery from './add_label_to_query'; @@ -60,6 +61,7 @@ export class PrometheusDatasource { interval: string; queryTimeout: string; httpMethod: string; + languageProvider: PrometheusLanguageProvider; resultTransformer: ResultTransformer; /** @ngInject */ @@ -76,6 +78,7 @@ export class PrometheusDatasource { this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; this.resultTransformer = new ResultTransformer(templateSrv); this.ruleMappings = {}; + this.languageProvider = new PrometheusLanguageProvider(this); } init() { diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts new file mode 100644 index 00000000000..3e406a71264 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -0,0 +1,334 @@ +import _ from 'lodash'; +import moment from 'moment'; + +import { + CompletionItem, + CompletionItemGroup, + LanguageProvider, + TypeaheadInput, + TypeaheadOutput, +} from 'app/types/explore'; + +import { parseSelector, processLabels, RATE_RANGES } from './language_utils'; +import PromqlSyntax, { FUNCTIONS } from './promql'; + +const DEFAULT_KEYS = ['job', 'instance']; +const EMPTY_SELECTOR = '{}'; +const HISTOGRAM_SELECTOR = '{le!=""}'; // Returns all timeseries for histograms +const HISTORY_ITEM_COUNT = 5; +const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h + +const wrapLabel = (label: string) => ({ label }); + +const setFunctionMove = (suggestion: CompletionItem): CompletionItem => { + suggestion.move = -1; + return suggestion; +}; + +export function addHistoryMetadata(item: CompletionItem, history: any[]): CompletionItem { + const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; + const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label); + const count = historyForItem.length; + const recent = historyForItem[0]; + let hint = `Queried ${count} times in the last 24h.`; + if (recent) { + const lastQueried = moment(recent.ts).fromNow(); + hint = `${hint} Last queried ${lastQueried}.`; + } + return { + ...item, + documentation: hint, + }; +} + +export default class PromQlLanguageProvider extends LanguageProvider { + histogramMetrics?: string[]; + labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...] + labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] + metrics?: string[]; + logLabelOptions: any[]; + supportsLogs?: boolean; + started: boolean; + + constructor(datasource: any, initialValues?: any) { + super(); + + this.datasource = datasource; + this.histogramMetrics = []; + this.labelKeys = {}; + this.labelValues = {}; + this.metrics = []; + this.supportsLogs = false; + this.started = false; + + Object.assign(this, initialValues); + } + // Strip syntax chars + cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); + + getSyntax() { + return PromqlSyntax; + } + + request = url => { + return this.datasource.metadataRequest(url); + }; + + start = () => { + if (!this.started) { + this.started = true; + return Promise.all([this.fetchMetricNames(), this.fetchHistogramMetrics()]); + } + return Promise.resolve([]); + }; + + // Keep this DOM-free for testing + provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput { + // Syntax spans have 3 classes by default. More indicate a recognized token + const tokenRecognized = wrapperClasses.length > 3; + // Determine candidates by CSS context + if (_.includes(wrapperClasses, 'context-range')) { + // Suggestions for metric[|] + return this.getRangeCompletionItems(); + } else if (_.includes(wrapperClasses, 'context-labels')) { + // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} + return this.getLabelCompletionItems.apply(this, arguments); + } else if (_.includes(wrapperClasses, 'context-aggregation')) { + return this.getAggregationCompletionItems.apply(this, arguments); + } else if ( + // Show default suggestions in a couple of scenarios + (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token + (prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace + text.match(/[+\-*/^%]/) // Anything after binary operator + ) { + return this.getEmptyCompletionItems(context || {}); + } + + return { + suggestions: [], + }; + } + + getEmptyCompletionItems(context: any): TypeaheadOutput { + const { history } = context; + const { metrics } = this; + const suggestions: CompletionItemGroup[] = []; + + if (history && history.length > 0) { + const historyItems = _.chain(history) + .uniqBy('query') + .take(HISTORY_ITEM_COUNT) + .map(h => h.query) + .map(wrapLabel) + .map(item => addHistoryMetadata(item, history)) + .value(); + + suggestions.push({ + prefixMatch: true, + skipSort: true, + label: 'History', + items: historyItems, + }); + } + + suggestions.push({ + prefixMatch: true, + label: 'Functions', + items: FUNCTIONS.map(setFunctionMove), + }); + + if (metrics) { + suggestions.push({ + label: 'Metrics', + items: metrics.map(wrapLabel), + }); + } + return { suggestions }; + } + + getRangeCompletionItems(): TypeaheadOutput { + return { + context: 'context-range', + suggestions: [ + { + label: 'Range vector', + items: [...RATE_RANGES].map(wrapLabel), + }, + ], + }; + } + + getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput { + let refresher: Promise = null; + const suggestions: CompletionItemGroup[] = []; + + // sum(foo{bar="1"}) by (|) + const line = value.anchorBlock.getText(); + const cursorOffset: number = value.anchorOffset; + // sum(foo{bar="1"}) by ( + const leftSide = line.slice(0, cursorOffset); + const openParensAggregationIndex = leftSide.lastIndexOf('('); + const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); + const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; + // foo{bar="1"} + const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + const selector = parseSelector(selectorString, selectorString.length - 2).selector; + + const labelKeys = this.labelKeys[selector]; + if (labelKeys) { + suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) }); + } else { + refresher = this.fetchSeriesLabels(selector); + } + + return { + refresher, + suggestions, + context: 'context-aggregation', + }; + } + + getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput { + let context: string; + let refresher: Promise = null; + const suggestions: CompletionItemGroup[] = []; + const line = value.anchorBlock.getText(); + const cursorOffset: number = value.anchorOffset; + + // Get normalized selector + let selector; + let parsedSelector; + try { + parsedSelector = parseSelector(line, cursorOffset); + selector = parsedSelector.selector; + } catch { + selector = EMPTY_SELECTOR; + } + const containsMetric = selector.indexOf('__name__=') > -1; + const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; + + if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { + // Label values + if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) { + const labelValues = this.labelValues[selector][labelKey]; + context = 'context-label-values'; + suggestions.push({ + label: `Label values for "${labelKey}"`, + items: labelValues.map(wrapLabel), + }); + } + } else { + // Label keys + const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS); + if (labelKeys) { + const possibleKeys = _.difference(labelKeys, existingKeys); + if (possibleKeys.length > 0) { + context = 'context-labels'; + suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) }); + } + } + } + + // Query labels for selector + // Temporarily add skip for logging + if (selector && !this.labelValues[selector] && !this.supportsLogs) { + if (selector === EMPTY_SELECTOR) { + // Query label values for default labels + refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key))); + } else { + refresher = this.fetchSeriesLabels(selector, !containsMetric); + } + } + + return { context, refresher, suggestions }; + } + + async fetchMetricNames() { + const url = '/api/v1/label/__name__/values'; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + this.metrics = body.data; + } catch (error) { + console.error(error); + } + } + + async fetchHistogramMetrics() { + await this.fetchSeriesLabels(HISTOGRAM_SELECTOR, true); + const histogramSeries = this.labelValues[HISTOGRAM_SELECTOR]; + if (histogramSeries && histogramSeries['__name__']) { + this.histogramMetrics = histogramSeries['__name__'].slice().sort(); + } + } + + // Temporarily here while reusing this field for logging + async fetchLogLabels() { + const url = '/api/prom/label'; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const labelKeys = body.data.slice().sort(); + const labelKeysBySelector = { + ...this.labelKeys, + [EMPTY_SELECTOR]: labelKeys, + }; + const labelValuesByKey = {}; + this.logLabelOptions = []; + for (const key of labelKeys) { + const valuesUrl = `/api/prom/label/${key}/values`; + const res = await this.request(valuesUrl); + const body = await (res.data || res.json()); + const values = body.data.slice().sort(); + labelValuesByKey[key] = values; + this.logLabelOptions.push({ + label: key, + value: key, + children: values.map(value => ({ label: value, value })), + }); + } + this.labelValues = { [EMPTY_SELECTOR]: labelValuesByKey }; + this.labelKeys = labelKeysBySelector; + } catch (e) { + console.error(e); + } + } + + async fetchLabelValues(key: string) { + const url = `/api/v1/label/${key}/values`; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const exisingValues = this.labelValues[EMPTY_SELECTOR]; + const values = { + ...exisingValues, + [key]: body.data, + }; + this.labelValues = { + ...this.labelValues, + [EMPTY_SELECTOR]: values, + }; + } catch (e) { + console.error(e); + } + } + + async fetchSeriesLabels(name: string, withName?: boolean) { + const url = `/api/v1/series?match[]=${name}`; + try { + const res = await this.request(url); + const body = await (res.data || res.json()); + const { keys, values } = processLabels(body.data, withName); + this.labelKeys = { + ...this.labelKeys, + [name]: keys, + }; + this.labelValues = { + ...this.labelValues, + [name]: values, + }; + } catch (e) { + console.error(e); + } + } +} diff --git a/public/app/features/explore/utils/prometheus.ts b/public/app/plugins/datasource/prometheus/language_utils.ts similarity index 96% rename from public/app/features/explore/utils/prometheus.ts rename to public/app/plugins/datasource/prometheus/language_utils.ts index 170c5ec8cc5..5995c427cd1 100644 --- a/public/app/features/explore/utils/prometheus.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -23,9 +23,6 @@ export function processLabels(labels, withName = false) { return { values, keys: Object.keys(values) }; } -// Strip syntax chars -export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); - // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; const selectorRegexp = /\{[^}]*?\}/; const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; diff --git a/public/app/features/explore/slate-plugins/prism/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts similarity index 100% rename from public/app/features/explore/slate-plugins/prism/promql.ts rename to public/app/plugins/datasource/prometheus/promql.ts diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts new file mode 100644 index 00000000000..3a46e2efaf3 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -0,0 +1,202 @@ +import Plain from 'slate-plain-serializer'; + +import LanguageProvider from '../language_provider'; + +describe('Language completion provider', () => { + const datasource = { + metadataRequest: () => ({ data: { data: [] } }), + }; + + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(2); + }); + + describe('range suggestions', () => { + it('returns range suggestions in range context', () => { + const instance = new LanguageProvider(datasource); + const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] }); + expect(result.context).toBe('context-range'); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toEqual([ + { + items: [{ label: '1m' }, { label: '5m' }, { label: '10m' }, { label: '30m' }, { label: '1h' }], + label: 'Range vector', + }, + ]); + }); + }); + + describe('metric suggestions', () => { + it('returns metrics suggestions by default', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(2); + }); + + it('returns default suggestions after a binary operator', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(2); + }); + }); + + describe('label suggestions', () => { + it('returns default label suggestions on label context and no metric', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize('{}'); + const range = value.selection.merge({ + anchorOffset: 1, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]); + }); + + it('returns label suggestions on label context and metric', () => { + const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } }); + const value = Plain.deserialize('metric{}'); + const range = value.selection.merge({ + anchorOffset: 7, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + + it('returns label suggestions on label context but leaves out labels that already exist', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] }, + }); + const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}'); + const range = value.selection.merge({ + anchorOffset: 36, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + + it('returns label value suggestions inside a label value context after a negated matching operator', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{}': ['label'] }, + labelValues: { '{}': { label: ['a', 'b', 'c'] } }, + }); + const value = Plain.deserialize('{label!=}'); + const range = value.selection.merge({ anchorOffset: 8 }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '!=', + prefix: '', + wrapperClasses: ['context-labels'], + labelKey: 'label', + value: valueWithSelection, + }); + expect(result.context).toBe('context-label-values'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }], + label: 'Label values for "label"', + }, + ]); + }); + + it('returns a refresher on label context and unavailable metric', () => { + const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } }); + const value = Plain.deserialize('metric{}'); + const range = value.selection.merge({ + anchorOffset: 7, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeInstanceOf(Promise); + expect(result.suggestions).toEqual([]); + }); + + it('returns label values on label context when given a metric and a label key', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{__name__="metric"}': ['bar'] }, + labelValues: { '{__name__="metric"}': { bar: ['baz'] } }, + }); + const value = Plain.deserialize('metric{bar=ba}'); + const range = value.selection.merge({ + anchorOffset: 13, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '=ba', + prefix: 'ba', + wrapperClasses: ['context-labels'], + labelKey: 'bar', + value: valueWithSelection, + }); + expect(result.context).toBe('context-label-values'); + expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]); + }); + + it('returns label suggestions on aggregation context and metric w/ selector', () => { + const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } }); + const value = Plain.deserialize('sum(metric{foo="xx"}) by ()'); + const range = value.selection.merge({ + anchorOffset: 26, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + + it('returns label suggestions on aggregation context and metric w/o selector', () => { + const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } }); + const value = Plain.deserialize('sum(metric) by ()'); + const range = value.selection.merge({ + anchorOffset: 16, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); + }); + }); +}); diff --git a/public/app/features/explore/utils/prometheus.test.ts b/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts similarity index 97% rename from public/app/features/explore/utils/prometheus.test.ts rename to public/app/plugins/datasource/prometheus/specs/language_utils.test.ts index 4e84deaa7e8..748217e21b7 100644 --- a/public/app/features/explore/utils/prometheus.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts @@ -1,4 +1,4 @@ -import { parseSelector } from './prometheus'; +import { parseSelector } from '../language_utils'; describe('parseSelector()', () => { let parsed; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 8746dd2edf6..807769212f3 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,3 +1,75 @@ +import { Value } from 'slate'; + +export interface CompletionItem { + /** + * The label of this completion item. By default + * this is also the text that is inserted when selecting + * this completion. + */ + label: string; + /** + * The kind of this completion item. Based on the kind + * an icon is chosen by the editor. + */ + kind?: string; + /** + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + detail?: string; + /** + * A human-readable string, can be Markdown, that represents a doc-comment. + */ + documentation?: string; + /** + * A string that should be used when comparing this item + * with other items. When `falsy` the `label` is used. + */ + sortText?: string; + /** + * A string that should be used when filtering a set of + * completion items. When `falsy` the `label` is used. + */ + filterText?: string; + /** + * A string or snippet that should be inserted in a document when selecting + * this completion. When `falsy` the `label` is used. + */ + insertText?: string; + /** + * Delete number of characters before the caret position, + * by default the letters from the beginning of the word. + */ + deleteBackwards?: number; + /** + * Number of steps to move after the insertion, can be negative. + */ + move?: number; +} + +export interface CompletionItemGroup { + /** + * Label that will be displayed for all entries of this group. + */ + label: string; + /** + * List of suggestions of this group. + */ + items: CompletionItem[]; + /** + * If true, match only by prefix (and not mid-word). + */ + prefixMatch?: boolean; + /** + * If true, do not filter items in this group based on the search. + */ + skipFilter?: boolean; + /** + * If true, do not sort items. + */ + skipSort?: boolean; +} + interface ExploreDatasource { value: string; label: string; @@ -8,6 +80,26 @@ export interface HistoryItem { query: string; } +export abstract class LanguageProvider { + datasource: any; + request: (url) => Promise; + start: () => Promise; +} + +export interface TypeaheadInput { + text: string; + prefix: string; + wrapperClasses: string[]; + labelKey?: string; + value?: Value; +} + +export interface TypeaheadOutput { + context?: string; + refresher?: Promise<{}>; + suggestions: CompletionItemGroup[]; +} + export interface Range { from: string; to: string; From 6a447a24fb7144ada86a651f5456251297d485e0 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Thu, 25 Oct 2018 14:16:01 +0200 Subject: [PATCH 22/50] stackdriver: don't set project name in query response since default project is now loaded in its own query --- public/app/plugins/datasource/stackdriver/datasource.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 4a81eb8a619..034333cbb86 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -114,7 +114,6 @@ export default class StackdriverDatasource { if (!queryRes.series) { return; } - this.projectName = queryRes.meta.defaultProject; const unit = this.resolvePanelUnitFromTargets(options.targets); queryRes.series.forEach(series => { let timeSerie: any = { From 946ca5477b5e9e4755f23ad2911c76e9294e1cdf Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 24 Oct 2018 14:59:35 +0200 Subject: [PATCH 23/50] changelog: adds note about closing #13723 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 778da9cd499..112e4ec754c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) +* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) ### Breaking changes From d9cd20e43e1b096326add68fb5d0870d12fdd26f Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 25 Oct 2018 14:36:31 +0200 Subject: [PATCH 24/50] docs: improve ES provisioning examples closes #12281 --- docs/sources/administration/provisioning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 8916b2bf6e3..9149aa42130 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -158,7 +158,7 @@ Since not all datasources have the same configuration settings we only have the | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source | | esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | -| interval | string | Elasticsearch | Index date time format | +| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | | assumeRoleArn | string | Cloudwatch | ARN of Assume Role | | defaultRegion | string | Cloudwatch | AWS region | From 30cb28df55a918fe220316aebde9ee5481a85ea6 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 25 Oct 2018 16:55:27 +0200 Subject: [PATCH 25/50] build: correctly adds enterprise to the filename. (#13831) --- Gruntfile.js | 5 +++++ build.go | 4 ++++ scripts/build/build-all.sh | 2 +- scripts/grunt/options/compress.js | 4 ++-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 2d5990b5f58..de3e68d4a92 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -9,12 +9,17 @@ module.exports = function (grunt) { destDir: 'dist', tempDir: 'tmp', platform: process.platform.replace('win32', 'windows'), + enterprise: false, }; if (grunt.option('platform')) { config.platform = grunt.option('platform'); } + if (grunt.option('enterprise')) { + config.enterprise = true; + } + if (grunt.option('arch')) { config.arch = grunt.option('arch'); } else { diff --git a/build.go b/build.go index 69fbf3bada8..6fd55da25b6 100644 --- a/build.go +++ b/build.go @@ -403,6 +403,10 @@ func gruntBuildArg(task string) []string { if phjsToRelease != "" { args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease)) } + if enterprise { + args = append(args, "--enterprise") + } + args = append(args, fmt.Sprintf("--platform=%v", goos)) return args diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 64e51ca6259..f194109ec0d 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -76,7 +76,7 @@ if [ -d '/tmp/phantomjs/windows' ]; then cp /tmp/phantomjs/windows/phantomjs.exe tools/phantomjs/phantomjs.exe rm tools/phantomjs/phantomjs else - echo 'PhantomJS binaries for darwin missing!' + echo 'PhantomJS binaries for Windows missing!' fi go run build.go -goos windows -pkg-arch amd64 ${OPT} package-only diff --git a/scripts/grunt/options/compress.js b/scripts/grunt/options/compress.js index 4dc77ec82f8..943a42bb571 100644 --- a/scripts/grunt/options/compress.js +++ b/scripts/grunt/options/compress.js @@ -4,7 +4,7 @@ module.exports = function(config) { var task = { release: { options: { - archive: '<%= destDir %>/<%= pkg.name %>-<%= pkg.version %>.<%= platform %>-<%= arch %>.tar.gz' + archive: '<%= destDir %>/<%= pkg.name %><%= enterprise ? "-enterprise" : "" %>-<%= pkg.version %>.<%= platform %>-<%= arch %>.tar.gz' }, files : [ { @@ -23,7 +23,7 @@ module.exports = function(config) { }; if (config.platform === 'windows') { - task.release.options.archive = '<%= destDir %>/<%= pkg.name %>-<%= pkg.version %>.<%= platform %>-<%= arch %>.zip'; + task.release.options.archive = '<%= destDir %>/<%= pkg.name %><%= enterprise ? "-enterprise" : "" %>-<%= pkg.version %>.<%= platform %>-<%= arch %>.zip'; } return task; From f1660aa21a86349e411eba49ea0f475af57f37dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 25 Oct 2018 17:05:17 +0200 Subject: [PATCH 26/50] fix: updated backend srv to use appEvents and removed parts of alertsSrv --- public/app/core/components/grafana_app.ts | 4 - public/app/core/services/alert_srv.ts | 96 +---------------------- public/app/core/services/backend_srv.ts | 13 +-- public/app/core/specs/backend_srv.test.ts | 2 +- public/app/features/dashboard/upload.ts | 4 +- 5 files changed, 14 insertions(+), 105 deletions(-) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 2774ab99426..c2b6808d586 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -16,7 +16,6 @@ export class GrafanaCtrl { /** @ngInject */ constructor( $scope, - alertSrv, utilSrv, $rootScope, $controller, @@ -37,11 +36,8 @@ export class GrafanaCtrl { $scope._ = _; profiler.init(config, $rootScope); - alertSrv.init(); utilSrv.init(); bridgeSrv.init(); - - $scope.dashAlerts = alertSrv; }; $rootScope.colors = colors; diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index 2d447651b75..4995b148abd 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -1,100 +1,12 @@ -import angular from 'angular'; -import _ from 'lodash'; import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; export class AlertSrv { - list: any[]; + constructor() {} - /** @ngInject */ - constructor(private $timeout, private $rootScope) { - this.list = []; - } - - init() { - this.$rootScope.onAppEvent( - 'alert-error', - (e, alert) => { - this.set(alert[0], alert[1], 'error', 12000); - }, - this.$rootScope - ); - - this.$rootScope.onAppEvent( - 'alert-warning', - (e, alert) => { - this.set(alert[0], alert[1], 'warning', 5000); - }, - this.$rootScope - ); - - this.$rootScope.onAppEvent( - 'alert-success', - (e, alert) => { - this.set(alert[0], alert[1], 'success', 3000); - }, - this.$rootScope - ); - - appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000)); - appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000)); - appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000)); - } - - getIconForSeverity(severity) { - switch (severity) { - case 'success': - return 'fa fa-check'; - case 'error': - return 'fa fa-exclamation-triangle'; - default: - return 'fa fa-exclamation'; - } - } - - set(title, text, severity, timeout) { - if (_.isObject(text)) { - console.log('alert error', text); - if (text.statusText) { - text = `HTTP Error (${text.status}) ${text.statusText}`; - } - } - - const newAlert = { - title: title || '', - text: text || '', - severity: severity || 'info', - icon: this.getIconForSeverity(severity), - }; - - const newAlertJson = angular.toJson(newAlert); - - // remove same alert if it already exists - _.remove(this.list, value => { - return angular.toJson(value) === newAlertJson; - }); - - this.list.push(newAlert); - if (timeout > 0) { - this.$timeout(() => { - this.list = _.without(this.list, newAlert); - }, timeout); - } - - if (!this.$rootScope.$$phase) { - this.$rootScope.$digest(); - } - - return newAlert; - } - - clear(alert) { - this.list = _.without(this.list, alert); - } - - clearAll() { - this.list = []; + set() { + console.log('old depricated alert srv being used'); } } +// this is just added to not break old plugins that might be using it coreModule.service('alertSrv', AlertSrv); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 3e8132a695b..144567efeb9 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -9,7 +9,7 @@ export class BackendSrv { private noBackendCache: boolean; /** @ngInject */ - constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {} + constructor(private $http, private $q, private $timeout, private contextSrv) {} get(url, params?) { return this.request({ method: 'GET', url: url, params: params }); @@ -49,14 +49,14 @@ export class BackendSrv { } if (err.status === 422) { - this.alertSrv.set('Validation failed', data.message, 'warning', 4000); + appEvents.emit('alert-warning', ['Validation failed', data.message]); throw data; } - data.severity = 'error'; + let severity = 'error'; if (err.status < 500) { - data.severity = 'warning'; + severity = 'warning'; } if (data.message) { @@ -66,7 +66,8 @@ export class BackendSrv { description = message; message = 'Error'; } - this.alertSrv.set(message, description, data.severity, 10000); + + appEvents.emit('alert-' + severity, [message, description]); } throw data; @@ -93,7 +94,7 @@ export class BackendSrv { if (options.method !== 'GET') { if (results && results.data.message) { if (options.showSuccessAlert !== false) { - this.alertSrv.set(results.data.message, '', 'success', 3000); + appEvents.emit('alert-success', [results.data.message]); } } } diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts index 2e35b87deb4..a6cb5a7d331 100644 --- a/public/app/core/specs/backend_srv.test.ts +++ b/public/app/core/specs/backend_srv.test.ts @@ -9,7 +9,7 @@ describe('backend_srv', () => { return Promise.resolve({}); }; - const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); + const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}); describe('when handling errors', () => { it('should return the http status code', async () => { diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 42871327eb6..ec4ad9a03cb 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -11,7 +11,7 @@ const template = ` `; /** @ngInject */ -function uploadDashboardDirective(timer, alertSrv, $location) { +function uploadDashboardDirective(timer, $location) { return { restrict: 'E', template: template, @@ -59,7 +59,7 @@ function uploadDashboardDirective(timer, alertSrv, $location) { // Something elem[0].addEventListener('change', file_selected, false); } else { - alertSrv.set('Oops', 'Sorry, the HTML5 File APIs are not fully supported in this browser.', 'error'); + appEvents.emit('alert-error', ['Oops', 'The HTML5 File APIs are not fully supported in this browser']); } }, }; From c40baa1a2320dc7a1a7c5835a1f40a214a4fd104 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 25 Oct 2018 13:10:56 +0900 Subject: [PATCH 27/50] use default region to call DescribeRegions --- pkg/tsdb/cloudwatch/metric_find_query.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index b74af76f09a..e42ba16b443 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -239,7 +239,8 @@ func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *s "cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1", } - err := e.ensureClientSession("us-east-1") + defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString() + err := e.ensureClientSession(defaultRegion) if err != nil { return nil, err } From 3447b8b299fd6b3ce6a946a3f3889f610f91bba7 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 25 Oct 2018 13:23:26 +0900 Subject: [PATCH 28/50] cache region result --- pkg/tsdb/cloudwatch/metric_find_query.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index e42ba16b443..f117eea4c22 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -35,6 +35,7 @@ type CustomMetricsCache struct { var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache +var regionCache sync.Map func init() { metricsMap = map[string][]string{ @@ -233,14 +234,19 @@ func parseMultiSelectValue(input string) []string { // Whenever this list is updated, frontend list should also be updated. // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { + dsInfo := e.getDsInfo("default") + if cache, ok := regionCache.Load(dsInfo.Profile); ok { + if cache2, ok2 := cache.([]suggestData); ok2 { + return cache2, nil + } + } + regions := []string{ "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1", "eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2", "cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1", } - - defaultRegion := e.DataSource.JsonData.Get("defaultRegion").MustString() - err := e.ensureClientSession(defaultRegion) + err := e.ensureClientSession("default") if err != nil { return nil, err } @@ -270,6 +276,7 @@ func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *s for _, region := range regions { result = append(result, suggestData{Text: region, Value: region}) } + regionCache.Store(dsInfo.Profile, result) return result, nil } From 220c4f4ab46808d364e5690111304382ae4b4163 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 26 Oct 2018 03:10:27 +0900 Subject: [PATCH 29/50] add test --- pkg/tsdb/cloudwatch/metric_find_query.go | 5 +-- pkg/tsdb/cloudwatch/metric_find_query_test.go | 33 ++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index f117eea4c22..718f9e0d253 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -235,7 +235,8 @@ func parseMultiSelectValue(input string) []string { // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { dsInfo := e.getDsInfo("default") - if cache, ok := regionCache.Load(dsInfo.Profile); ok { + profile := dsInfo.Profile + if cache, ok := regionCache.Load(profile); ok { if cache2, ok2 := cache.([]suggestData); ok2 { return cache2, nil } @@ -276,7 +277,7 @@ func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *s for _, region := range regions { result = append(result, suggestData{Text: region, Value: region}) } - regionCache.Store(dsInfo.Profile, result) + regionCache.Store(profile, result) return result, nil } diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go index e3903e8027e..34c3379b4df 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query_test.go +++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go @@ -9,20 +9,26 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/bmizerany/assert" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" ) type mockedEc2 struct { ec2iface.EC2API - Resp ec2.DescribeInstancesOutput + Resp ec2.DescribeInstancesOutput + RespRegions ec2.DescribeRegionsOutput } func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error { fn(&m.Resp, true) return nil } +func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) { + return &m.RespRegions, nil +} func TestCloudWatchMetrics(t *testing.T) { @@ -82,6 +88,31 @@ func TestCloudWatchMetrics(t *testing.T) { }) }) + Convey("When calling handleGetRegions", t, func() { + executor := &CloudWatchExecutor{ + ec2Svc: mockedEc2{RespRegions: ec2.DescribeRegionsOutput{ + Regions: []*ec2.Region{ + { + RegionName: aws.String("ap-northeast-2"), + }, + }, + }}, + } + jsonData := simplejson.New() + jsonData.Set("defaultRegion", "default") + executor.DataSource = &models.DataSource{ + JsonData: jsonData, + SecureJsonData: securejsondata.SecureJsonData{}, + } + + result, _ := executor.handleGetRegions(context.Background(), simplejson.New(), &tsdb.TsdbQuery{}) + + Convey("Should return regions", func() { + So(result[0].Text, ShouldEqual, "ap-northeast-1") + So(result[1].Text, ShouldEqual, "ap-northeast-2") + }) + }) + Convey("When calling handleGetEc2InstanceAttribute", t, func() { executor := &CloudWatchExecutor{ ec2Svc: mockedEc2{Resp: ec2.DescribeInstancesOutput{ From 361864bec689e37e8bb27e594a0accb562a69dad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 26 Oct 2018 09:06:32 +0200 Subject: [PATCH 30/50] changelog: add notes about closing #13769 [skip ci] --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 112e4ec754c..0b14126e5c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited) +# 5.3.3 (unreleased) + +* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) + # 5.3.2 (2018-10-24) * **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm) From e2f74b55d20ebad1faf4e6e2ee47a057271b1667 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 26 Oct 2018 14:23:30 +0200 Subject: [PATCH 31/50] build: grafana enterprise docker. (#13839) --- .circleci/config.yml | 4 ++++ packaging/docker/build-enterprise.sh | 10 ++++++++++ 2 files changed, 14 insertions(+) create mode 100755 packaging/docker/build-enterprise.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index c293ea26a9d..79879b1a386 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -206,6 +206,9 @@ jobs: - run: docker info - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" + - run: cp dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cd packaging/docker && ./build-enterprise.sh "master" + grafana-docker-pr: docker: @@ -409,6 +412,7 @@ workflows: - grafana-docker-master: requires: - build-all + - build-all-enterprise - test-backend - test-frontend - codespell diff --git a/packaging/docker/build-enterprise.sh b/packaging/docker/build-enterprise.sh new file mode 100755 index 00000000000..f716a1f44f1 --- /dev/null +++ b/packaging/docker/build-enterprise.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +_grafana_tag=$1 +_docker_repo=${2:-grafana/grafana-enterprise} + +docker build \ + --tag "${_docker_repo}:${_grafana_tag}"\ + --no-cache=true \ + . From a80e2e1acd8cddfde5af521a5c1fed089e6e2c79 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 26 Oct 2018 14:37:51 +0200 Subject: [PATCH 32/50] build: ge build fix. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 79879b1a386..ad40ad4e72f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -206,7 +206,7 @@ jobs: - run: docker info - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" - - run: cp dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - run: cd packaging/docker && ./build-enterprise.sh "master" From 07cb622729537f6cccc8f31c7ace06dc2e2ddeff Mon Sep 17 00:00:00 2001 From: Jon Ferreira Date: Wed, 10 Oct 2018 13:06:05 -0400 Subject: [PATCH 33/50] Add code to flot that plots any datapoints which to not have neighbors as 0.5 radius points - fixes https://github.com/grafana/grafana/issues/13605 --- public/vendor/flot/jquery.flot.js | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/public/vendor/flot/jquery.flot.js b/public/vendor/flot/jquery.flot.js index 8ee09e25c41..38be4dd8681 100644 --- a/public/vendor/flot/jquery.flot.js +++ b/public/vendor/flot/jquery.flot.js @@ -2271,9 +2271,51 @@ Licensed under the MIT license. }); } + function drawOrphanedPoints(series) { + /* Filters series data for points with no neighbors before or after + * and plots single 0.5 radius points for them so that they are displayed. + */ + var abandonedPoints = []; + var beforeX = null; + var afterX = null; + var datapoints = series.datapoints; + // find any points with no neighbors before or after + var emptyPoints = []; + for (var j = 0; j < datapoints.pointsize - 2; j++) { + emptyPoints.push(0); + } + for (var i = 0; i < datapoints.points.length; i += datapoints.pointsize) { + var x = datapoints.points[i], y = datapoints.points[i + 1]; + if (i === datapoints.points.length - datapoints.pointsize) { + afterX = null; + } else { + afterX = datapoints.points[i + datapoints.pointsize]; + } + if (x !== null && y !== null && beforeX === null && afterX === null) { + abandonedPoints.push(x); + abandonedPoints.push(y); + abandonedPoints.push.apply(abandonedPoints, emptyPoints); + } + beforeX = x; + + } + var olddatapoints = datapoints.points + datapoints.points = abandonedPoints; + + series.points.radius = series.lines.lineWidth/2; + // plot the orphan points with a radius of lineWidth/2 + drawSeriesPoints(series); + // reset old info + datapoints.points = olddatapoints; + } + function drawSeries(series) { if (series.lines.show) drawSeriesLines(series); + if (!series.points.show && !series.bars.show) { + // not necessary if user wants points displayed for everything + drawOrphanedPoints(series); + } if (series.bars.show) drawSeriesBars(series); if (series.points.show) From 58a567173ebd2f5515e1a27d53285672173a60cf Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 26 Oct 2018 15:19:53 +0200 Subject: [PATCH 34/50] build: builds grafana docker for enterprise at release. --- .circleci/config.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index ad40ad4e72f..28400fc6bd6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -206,6 +206,7 @@ jobs: - run: docker info - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" + - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz - run: cd packaging/docker && ./build-enterprise.sh "master" @@ -233,6 +234,9 @@ jobs: - run: docker info - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" + - run: rm packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz + - run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}" build-enterprise: docker: From 2a4a19388f30220fd81586bd70b7e3aa8a1495e5 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sat, 27 Oct 2018 16:54:12 +0800 Subject: [PATCH 35/50] Fix label suggestions for multi-line aggregation queries No label suggestions were being returned for multi-line aggregation contexts because the parsed selector string does not see the full context before a `by` or `without` clause. This solution stitches together all text nodes that comprise the query editor to ensure the selector has sufficient context to generate suggestions. Also, an additional workaround has been included to ensure range vector syntax does not disrupt label suggestions in aggregation contexts. Related: #12890 --- .../prometheus/language_provider.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 3e406a71264..3dd15eb713e 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -162,16 +162,29 @@ export default class PromQlLanguageProvider extends LanguageProvider { let refresher: Promise = null; const suggestions: CompletionItemGroup[] = []; - // sum(foo{bar="1"}) by (|) - const line = value.anchorBlock.getText(); - const cursorOffset: number = value.anchorOffset; - // sum(foo{bar="1"}) by ( - const leftSide = line.slice(0, cursorOffset); + // Stitch all query lines together to support multi-line queries + let queryOffset; + const queryText = value.document.getBlocks().reduce((text, block) => { + const blockText = block.getText(); + if (value.anchorBlock.key === block.key) { + // Newline characters are not accounted for but this is irrelevant + // for the purpose of extracting the selector string + queryOffset = value.anchorOffset + text.length; + } + text += blockText; + return text; + }, ''); + + const leftSide = queryText.slice(0, queryOffset); const openParensAggregationIndex = leftSide.lastIndexOf('('); const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('('); const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex; - // foo{bar="1"} - const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + + let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + + // Range vector syntax not accounted for by subsequent parse so discard it if present + selectorString = selectorString.replace(/\[[^\]]+\]$/, ''); + const selector = parseSelector(selectorString, selectorString.length - 2).selector; const labelKeys = this.labelKeys[selector]; From 61843b58db92fbed130f323a5ec0ab4abff62d92 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sat, 27 Oct 2018 17:02:03 +0800 Subject: [PATCH 36/50] Add tests to cover aggregation context cases This should cover use cases involving multi-line queries and range vector syntax inside aggregation contexts. Related: #12890 --- .../specs/language_provider.test.ts | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 3a46e2efaf3..20e148efd57 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -198,5 +198,76 @@ describe('Language completion provider', () => { expect(result.context).toBe('context-aggregation'); expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); + + it('returns label suggestions inside a multi-line aggregation context', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, + }); + const value = Plain.deserialize('sum(\nmetric\n)\nby ()'); + const aggregationTextBlock = value.document.getBlocksAsArray()[3]; + const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); + + it('returns label suggestions inside an aggregation context with a range vector', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, + }); + const value = Plain.deserialize('sum(rate(metric[1h])) by ()'); + const range = value.selection.merge({ + anchorOffset: 26, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); + + it('returns label suggestions inside an aggregation context with a range vector and label', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] }, + }); + const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()'); + const range = value.selection.merge({ + anchorOffset: 42, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-aggregation'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-aggregation'); + expect(result.suggestions).toEqual([ + { + items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }], + label: 'Labels', + }, + ]); + }); }); }); From 10e5d725bc6afcda1c520972760c99fee71641a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 27 Oct 2018 15:13:15 +0200 Subject: [PATCH 37/50] updated singlestat logo --- .../singlestat/img/icn-singlestat-panel.svg | 102 +++++++++++++----- 1 file changed, 76 insertions(+), 26 deletions(-) diff --git a/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg b/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg index a1e15d4d58d..746687d360f 100644 --- a/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg +++ b/public/app/plugins/panel/singlestat/img/icn-singlestat-panel.svg @@ -1,33 +1,83 @@ - - + + + + - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + From c5f9d8092f110d0f2d01068aeff7b3b0af9bcc0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 27 Oct 2018 16:54:04 +0200 Subject: [PATCH 38/50] Reduce re-renderings when changing view modes --- .../features/dashboard/dashgrid/DashboardGrid.tsx | 12 ++++++------ public/app/plugins/panel/graph2/module.tsx | 6 +++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 1f5fa4cbe12..fe55e64634f 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -21,15 +21,14 @@ function GridWrapper({ className, isResizable, isDraggable, + isFullscreen, }) { - if (size.width === 0) { - console.log('size is zero!'); - } - const width = size.width > 0 ? size.width : lastGridWidth; if (width !== lastGridWidth) { - onWidthChange(); - lastGridWidth = width; + if (!isFullscreen && Math.abs(width - lastGridWidth) > 8) { + onWidthChange(); + lastGridWidth = width; + } } return ( @@ -197,6 +196,7 @@ export class DashboardGrid extends React.Component { onDragStop={this.onDragStop} onResize={this.onResize} onResizeStop={this.onResizeStop} + isFullscreen={this.props.dashboard.meta.fullscreen} > {this.renderPanels()} diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index c2b8c355440..576ece3df61 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -36,7 +36,11 @@ export class Graph2 extends PureComponent { export class TextOptions extends PureComponent { render() { - return

Text2 Options component

; + return ( +
+
Draw Modes
+
+ ); } } From c255b5da1113b1eef115b3acab326e130eb77b88 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sun, 28 Oct 2018 21:03:39 +0800 Subject: [PATCH 39/50] Add sum aggregation query suggestion Implements rudimentary support for placeholder values inside a string with the `PlaceholdersBuffer` class. The latter helps the newly added sum aggregation query suggestion to automatically focus on the label so users can easily choose from the available typeahead options. Related: #13615 --- public/app/features/explore/Explore.tsx | 36 ++++-- .../features/explore/PlaceholdersBuffer.ts | 112 ++++++++++++++++++ public/app/features/explore/QueryField.tsx | 34 ++++-- .../datasource/prometheus/datasource.ts | 3 + .../datasource/prometheus/query_hints.ts | 24 ++++ public/app/types/explore.ts | 1 + 6 files changed, 190 insertions(+), 20 deletions(-) create mode 100644 public/app/features/explore/PlaceholdersBuffer.ts diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 680cd1e6685..f29d38d283a 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -373,9 +373,10 @@ export class Explore extends React.PureComponent { this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue }); }; - onModifyQueries = (action: object, index?: number) => { + onModifyQueries = (action, index?: number) => { const { datasource } = this.state; if (datasource && datasource.modifyQuery) { + const preventSubmit = action.preventSubmit; this.setState( state => { const { queries, queryTransactions } = state; @@ -391,16 +392,26 @@ export class Explore extends React.PureComponent { nextQueryTransactions = []; } else { // Modify query only at index - nextQueries = [ - ...queries.slice(0, index), - { - key: generateQueryKey(index), - query: datasource.modifyQuery(this.queryExpressions[index], action), - }, - ...queries.slice(index + 1), - ]; - // Discard transactions related to row query - nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + nextQueries = queries.map((q, i) => { + // Synchronise all queries with local query cache to ensure consistency + q.query = this.queryExpressions[i]; + return i === index + ? { + key: generateQueryKey(index), + query: datasource.modifyQuery(q.query, action), + } + : q; + }); + nextQueryTransactions = queryTransactions + // Consume the hint corresponding to the action + .map(qt => { + if (qt.hints != null && qt.rowIndex === index) { + qt.hints = qt.hints.filter(hint => hint.fix.action !== action); + } + return qt; + }) + // Preserve previous row query transaction to keep results visible if next query is incomplete + .filter(qt => preventSubmit || qt.rowIndex !== index); } this.queryExpressions = nextQueries.map(q => q.query); return { @@ -408,7 +419,8 @@ export class Explore extends React.PureComponent { queryTransactions: nextQueryTransactions, }; }, - () => this.onSubmit() + // Accepting certain fixes do not result in a well-formed query which should not be submitted + !preventSubmit ? () => this.onSubmit() : null ); } }; diff --git a/public/app/features/explore/PlaceholdersBuffer.ts b/public/app/features/explore/PlaceholdersBuffer.ts new file mode 100644 index 00000000000..9a0db18ef04 --- /dev/null +++ b/public/app/features/explore/PlaceholdersBuffer.ts @@ -0,0 +1,112 @@ +/** + * Provides a stateful means of managing placeholders in text. + * + * Placeholders are numbers prefixed with the `$` character (e.g. `$1`). + * Each number value represents the order in which a placeholder should + * receive focus if multiple placeholders exist. + * + * Example scenario given `sum($3 offset $1) by($2)`: + * 1. `sum( offset |) by()` + * 2. `sum( offset 1h) by(|)` + * 3. `sum(| offset 1h) by (label)` + */ +export default class PlaceholdersBuffer { + private nextMoveOffset: number; + private orders: number[]; + private parts: string[]; + + constructor(text: string) { + const result = this.parse(text); + const nextPlaceholderIndex = result.orders.length ? result.orders[0] : 0; + this.nextMoveOffset = this.getOffsetBetween(result.parts, 0, nextPlaceholderIndex); + this.orders = result.orders; + this.parts = result.parts; + } + + clearPlaceholders() { + this.nextMoveOffset = 0; + this.orders = []; + } + + getNextMoveOffset(): number { + return this.nextMoveOffset; + } + + hasPlaceholders(): boolean { + return this.orders.length > 0; + } + + setNextPlaceholderValue(value: string) { + if (this.orders.length === 0) { + return; + } + const currentPlaceholderIndex = this.orders[0]; + this.parts[currentPlaceholderIndex] = value; + this.orders = this.orders.slice(1); + if (this.orders.length === 0) { + this.nextMoveOffset = 0; + return; + } + const nextPlaceholderIndex = this.orders[0]; + // Case should never happen but handle it gracefully in case + if (currentPlaceholderIndex === nextPlaceholderIndex) { + this.nextMoveOffset = 0; + return; + } + const backwardMove = currentPlaceholderIndex > nextPlaceholderIndex; + const indices = backwardMove + ? { start: nextPlaceholderIndex + 1, end: currentPlaceholderIndex + 1 } + : { start: currentPlaceholderIndex + 1, end: nextPlaceholderIndex }; + this.nextMoveOffset = (backwardMove ? -1 : 1) * this.getOffsetBetween(this.parts, indices.start, indices.end); + } + + toString(): string { + return this.parts.join(''); + } + + private getOffsetBetween(parts: string[], startIndex: number, endIndex: number) { + return parts.slice(startIndex, endIndex).reduce((offset, part) => offset + part.length, 0); + } + + private parse(text: string): ParseResult { + const placeholderRegExp = /\$(\d+)/g; + const parts = []; + const orders = []; + let textOffset = 0; + while (true) { + const match = placeholderRegExp.exec(text); + if (!match) { + break; + } + const part = text.slice(textOffset, match.index); + parts.push(part); + // Accounts for placeholders at text boundaries + if (part !== '') { + parts.push(''); + } + const order = parseInt(match[1], 10); + orders.push({ index: parts.length - 1, order }); + textOffset += part.length + match.length; + } + // Ensures string serialisation still works if no placeholders were parsed + // and also accounts for the remainder of text with placeholders + parts.push(text.slice(textOffset)); + return { + // Placeholder values do not necessarily appear sequentially so sort the + // indices to traverse in priority order + orders: orders.sort((o1, o2) => o1.order - o2.order).map(o => o.index), + parts, + }; + } +} + +type ParseResult = { + /** + * Indices to placeholder items in `parts` in traversal order. + */ + orders: number[]; + /** + * Parts comprising the original text with placeholders occupying distinct items. + */ + parts: string[]; +}; diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 86daaa43eac..9350682f1a0 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -12,6 +12,7 @@ import NewlinePlugin from './slate-plugins/newline'; import Typeahead from './Typeahead'; import { makeFragment, makeValue } from './Value'; +import PlaceholdersBuffer from './PlaceholdersBuffer'; export const TYPEAHEAD_DEBOUNCE = 100; @@ -61,12 +62,15 @@ export interface TypeaheadInput { class QueryField extends React.PureComponent { menuEl: HTMLElement | null; + placeholdersBuffer: PlaceholdersBuffer; plugins: any[]; resetTimer: any; constructor(props, context) { super(props, context); + this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || ''); + // Base plugins this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins]; @@ -76,7 +80,7 @@ class QueryField extends React.PureComponent operation.type === 'insert_text'); + if (insertTextOperation) { + const suggestionText = insertTextOperation.text; + this.placeholdersBuffer.setNextPlaceholderValue(suggestionText); + if (this.placeholdersBuffer.hasPlaceholders()) { + nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus(); + } + } + return true; } break; @@ -336,6 +352,8 @@ class QueryField extends React.PureComponent= SUM_HINT_THRESHOLD_COUNT) { + const simpleMetric = query.trim().match(/^\w+$/); + if (simpleMetric) { + hints.push({ + type: 'ADD_SUM', + label: 'Many time series results returned.', + fix: { + label: 'Consider aggregating with sum().', + action: { + type: 'ADD_SUM', + query: query, + preventSubmit: true, + }, + }, + }); + } + } + return hints.length > 0 ? hints : null; } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 807769212f3..1db37392b0b 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -119,6 +119,7 @@ export interface QueryFix { export interface QueryFixAction { type: string; query?: string; + preventSubmit?: boolean; } export interface QueryHint { From d1d5e9f7d3a81a957f512d7f52704a23e2ef8509 Mon Sep 17 00:00:00 2001 From: Michael Huynh Date: Sun, 28 Oct 2018 21:07:40 +0800 Subject: [PATCH 40/50] Add tests to cover PlaceholdersBuffer and sum hint Related: #13615 --- .../explore/PlaceholdersBuffer.test.ts | 72 +++++++++++++++++++ .../prometheus/specs/query_hints.test.ts | 21 +++++- 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 public/app/features/explore/PlaceholdersBuffer.test.ts diff --git a/public/app/features/explore/PlaceholdersBuffer.test.ts b/public/app/features/explore/PlaceholdersBuffer.test.ts new file mode 100644 index 00000000000..2ce31e79b05 --- /dev/null +++ b/public/app/features/explore/PlaceholdersBuffer.test.ts @@ -0,0 +1,72 @@ +import PlaceholdersBuffer from './PlaceholdersBuffer'; + +describe('PlaceholdersBuffer', () => { + it('does nothing if no placeholders are defined', () => { + const text = 'metric'; + const buffer = new PlaceholdersBuffer(text); + + expect(buffer.hasPlaceholders()).toBe(false); + expect(buffer.toString()).toBe(text); + expect(buffer.getNextMoveOffset()).toBe(0); + }); + + it('respects the traversal order of placeholders', () => { + const text = 'sum($2 offset $1) by ($3)'; + const buffer = new PlaceholdersBuffer(text); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe('sum( offset ) by ()'); + expect(buffer.getNextMoveOffset()).toBe(12); + + buffer.setNextPlaceholderValue('1h'); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe('sum( offset 1h) by ()'); + expect(buffer.getNextMoveOffset()).toBe(-10); + + buffer.setNextPlaceholderValue('metric'); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe('sum(metric offset 1h) by ()'); + expect(buffer.getNextMoveOffset()).toBe(16); + + buffer.setNextPlaceholderValue('label'); + + expect(buffer.hasPlaceholders()).toBe(false); + expect(buffer.toString()).toBe('sum(metric offset 1h) by (label)'); + expect(buffer.getNextMoveOffset()).toBe(0); + }); + + it('respects the traversal order of adjacent placeholders', () => { + const text = '$1$3$2$4'; + const buffer = new PlaceholdersBuffer(text); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe(''); + expect(buffer.getNextMoveOffset()).toBe(0); + + buffer.setNextPlaceholderValue('1'); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe('1'); + expect(buffer.getNextMoveOffset()).toBe(0); + + buffer.setNextPlaceholderValue('2'); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe('12'); + expect(buffer.getNextMoveOffset()).toBe(-1); + + buffer.setNextPlaceholderValue('3'); + + expect(buffer.hasPlaceholders()).toBe(true); + expect(buffer.toString()).toBe('132'); + expect(buffer.getNextMoveOffset()).toBe(1); + + buffer.setNextPlaceholderValue('4'); + + expect(buffer.hasPlaceholders()).toBe(false); + expect(buffer.toString()).toBe('1324'); + expect(buffer.getNextMoveOffset()).toBe(0); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts b/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts index 7eba54536fe..f5435bd5d39 100644 --- a/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts @@ -1,4 +1,4 @@ -import { getQueryHints } from '../query_hints'; +import { getQueryHints, SUM_HINT_THRESHOLD_COUNT } from '../query_hints'; describe('getQueryHints()', () => { it('returns no hints for no series', () => { @@ -79,4 +79,23 @@ describe('getQueryHints()', () => { }, }); }); + + it('returns a sum hint when many time series results are returned for a simple metric', () => { + const seriesCount = SUM_HINT_THRESHOLD_COUNT; + const series = Array.from({ length: seriesCount }, _ => ({ + datapoints: [[0, 0], [0, 0]], + })); + const hints = getQueryHints('metric', series); + expect(hints.length).toBe(seriesCount); + expect(hints[0]).toMatchObject({ + label: 'Many time series results returned.', + index: 0, + fix: { + action: { + type: 'ADD_SUM', + query: 'metric', + }, + }, + }); + }); }); From e44dde3f145ae789c1519affa1a5dc7481d75107 Mon Sep 17 00:00:00 2001 From: Steve Kreitzer Date: Sun, 28 Oct 2018 10:25:42 -0400 Subject: [PATCH 41/50] Fixing issue 13855 --- docs/sources/auth/gitlab.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/auth/gitlab.md b/docs/sources/auth/gitlab.md index e3a450f9fc7..56fc3b131a5 100644 --- a/docs/sources/auth/gitlab.md +++ b/docs/sources/auth/gitlab.md @@ -100,12 +100,12 @@ display name, especially if the display name contains spaces or special characters. Make sure you always use the group or subgroup name as it appears in the URL of the group or subgroup. -Here's a complete example with `alloed_sign_up` enabled, and access limited to +Here's a complete example with `allow_sign_up` enabled, and access limited to the `example` and `foo/bar` groups: ```ini [auth.gitlab] -enabled = false +enabled = true allow_sign_up = true client_id = GITLAB_APPLICATION_ID client_secret = GITLAB_SECRET From 9245dad53ea5faf984bd5cd2039b1038c2cf15e2 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 28 Oct 2018 17:48:17 +0100 Subject: [PATCH 42/50] Fix query hint tests after refactor --- .../plugins/datasource/prometheus/specs/query_hints.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts b/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts index f5435bd5d39..3b782d3dd09 100644 --- a/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/query_hints.test.ts @@ -86,14 +86,16 @@ describe('getQueryHints()', () => { datapoints: [[0, 0], [0, 0]], })); const hints = getQueryHints('metric', series); - expect(hints.length).toBe(seriesCount); + expect(hints.length).toBe(1); expect(hints[0]).toMatchObject({ + type: 'ADD_SUM', label: 'Many time series results returned.', - index: 0, fix: { + label: 'Consider aggregating with sum().', action: { type: 'ADD_SUM', query: 'metric', + preventSubmit: true, }, }, }); From 52669032d059c50e76dbdb9f29e516b0f08ff9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 28 Oct 2018 11:29:55 -0700 Subject: [PATCH 43/50] updated graph tests dashboard --- devenv/dev-dashboards/panel_tests_graph.json | 129 ++++++++++++++++++- 1 file changed, 123 insertions(+), 6 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph.json b/devenv/dev-dashboards/panel_tests_graph.json index 8a1770f0fa6..ba677764a43 100644 --- a/devenv/dev-dashboards/panel_tests_graph.json +++ b/devenv/dev-dashboards/panel_tests_graph.json @@ -927,6 +927,123 @@ "title": "", "type": "text" }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 0, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 44 + }, + "id": 21, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "C-series", + "steppedLine": true + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Null between points", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "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 + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 44 + }, + "id": 22, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, { "aliasColors": {}, "bars": false, @@ -939,7 +1056,7 @@ "h": 7, "w": 24, "x": 0, - "y": 44 + "y": 51 }, "id": 20, "legend": { @@ -1024,7 +1141,7 @@ "h": 7, "w": 12, "x": 0, - "y": 51 + "y": 58 }, "id": 16, "legend": { @@ -1127,7 +1244,7 @@ "h": 7, "w": 12, "x": 12, - "y": 51 + "y": 58 }, "id": 17, "legend": { @@ -1266,7 +1383,7 @@ "h": 7, "w": 12, "x": 0, - "y": 58 + "y": 65 }, "id": 18, "legend": { @@ -1370,7 +1487,7 @@ "h": 7, "w": 12, "x": 12, - "y": 58 + "y": 65 }, "id": 19, "legend": { @@ -1554,5 +1671,5 @@ "timezone": "browser", "title": "Panel Tests - Graph", "uid": "5SdHCadmz", - "version": 3 + "version": 1 } From 9f1f5805ece77aae3b56391a9bea1037e4ce8709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 28 Oct 2018 12:10:49 -0700 Subject: [PATCH 44/50] added switch form component --- .../{Forms/Forms.tsx => Label/Label.tsx} | 1 + public/app/core/components/Switch/Switch.tsx | 46 +++++++++++++++++++ public/app/features/teams/TeamSettings.tsx | 2 +- public/app/plugins/panel/graph2/module.tsx | 4 ++ 4 files changed, 52 insertions(+), 1 deletion(-) rename public/app/core/components/{Forms/Forms.tsx => Label/Label.tsx} (99%) create mode 100644 public/app/core/components/Switch/Switch.tsx diff --git a/public/app/core/components/Forms/Forms.tsx b/public/app/core/components/Label/Label.tsx similarity index 99% rename from public/app/core/components/Forms/Forms.tsx rename to public/app/core/components/Label/Label.tsx index 543e1a1d6df..6d4fd20dbfe 100644 --- a/public/app/core/components/Forms/Forms.tsx +++ b/public/app/core/components/Label/Label.tsx @@ -19,3 +19,4 @@ export const Label: SFC = props => { ); }; + diff --git a/public/app/core/components/Switch/Switch.tsx b/public/app/core/components/Switch/Switch.tsx new file mode 100644 index 00000000000..ba09267ebd2 --- /dev/null +++ b/public/app/core/components/Switch/Switch.tsx @@ -0,0 +1,46 @@ +import React, { PureComponent } from 'react'; +import _ from 'lodash'; + +export interface Props { + label: string; + checked: boolean; + labelClass?: string; + switchClass?: string; + onChange: (event) => any; +} + +export interface State { + id: any; +} + +export class Switch extends PureComponent { + state = { + id: _.uniqueId(), + }; + + internalOnChange = event => { + event.stopPropagation(); + this.props.onChange(event); + }; + + render() { + const { labelClass, switchClass, label, checked } = this.props; + const labelId = `check-${this.state.id}`; + const labelClassName = `gf-form-label ${labelClass} pointer`; + const switchClassName = `gf-form-switch ${switchClass}`; + + return ( +
+ {label && ( + + )} +
+ +
+
+ ); + } +} diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index ef9a5ae0b70..45977de95bf 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Label } from 'app/core/components/Forms/Forms'; +import { Label } from 'app/core/components/Label/Label'; import { Team } from '../../types'; import { updateTeam } from './state/actions'; import { getRouteParamsId } from '../../core/selectors/location'; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 576ece3df61..4011458bea9 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -5,6 +5,7 @@ import React, { PureComponent } from 'react'; // Components import Graph from 'app/viz/Graph'; import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; +import { Switch } from 'app/core/components/Switch/Switch'; // Types import { PanelProps, NullValueMode } from 'app/types'; @@ -35,10 +36,13 @@ export class Graph2 extends PureComponent { } export class TextOptions extends PureComponent { + onChange = () => {}; + render() { return (
Draw Modes
+
); } From e47de5602b69cc57e798522585a87545f511052c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 28 Oct 2018 12:46:18 -0700 Subject: [PATCH 45/50] added missing alpha state prop to graph2 panel --- public/app/plugins/panel/graph2/plugin.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json index b519a57fae4..2e674ab3557 100644 --- a/public/app/plugins/panel/graph2/plugin.json +++ b/public/app/plugins/panel/graph2/plugin.json @@ -3,6 +3,8 @@ "name": "React Graph", "id": "graph2", + "state": "alpha", + "info": { "author": { "name": "Grafana Project", From 0e34a6be0f12ddf1898a81c6660aeb8c226c60c8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 29 Oct 2018 10:35:23 +0100 Subject: [PATCH 46/50] removes old invalid release guide closes #13864 --- packaging/release_process.md | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 packaging/release_process.md diff --git a/packaging/release_process.md b/packaging/release_process.md deleted file mode 100644 index 6037a9c499c..00000000000 --- a/packaging/release_process.md +++ /dev/null @@ -1,29 +0,0 @@ -# New Grafana Release Processes - -## Building release packages - -1) Update package.json so that it has the right version. -2) Create a git tag for the release: `git tag -a v3.0.4 -m "3.0.4 release"` -3) Push branch & tag to github! -2) Packages from master a built automatically by circle CI for this repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer) - -### Non master branch - -When building from non master branch create a new branch in repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer) -and configure circle.yml to deploy that branch as well, https://github.com/grafana/grafana-packer/blob/master/circle.yml#L25, -you also need to update https://github.com/grafana/grafana-packer/blob/v3.1.x/deploy.sh#L7. - -### Windows build - -Sign into ci.appveyor.com and the Grafana project's build history page. Builds for windows take a long time (around 20min) -and fail quite often for random reasons so I usually continue with the release process without a windows build already built. - -1) Click on the green build that has the correct version and tag -2) Click on `DEPLOYMENTS` -3) Click on `NEW DEPLOYMENT` -4) Select GrafanaBuildS3 -4) Select the build you want to deploy. - -The deployment should be quick (just uploads the release zip file to S3) - - From a4ef1d617556c9d7453265154d131edc2b48e8d1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 29 Oct 2018 11:08:30 +0100 Subject: [PATCH 47/50] Makefile: dependency-driven target to build node_modules - added `node_modules` as new target - dependency on `package.json` and `yarn.lock` allows for quick `make node_modules` after a branch change, which noops when the deps have not changed - also added `clean` target --- Makefile | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c9e51d897f3..fcb740d2fac 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,7 @@ all: deps build deps-go: go run build.go setup -deps-js: - yarn install --pure-lockfile --no-progress +deps-js: node_modules deps: deps-js @@ -43,3 +42,10 @@ test: test-go test-js run: ./bin/grafana-server + +clean: + rm -rf node_modules + rm -rf public/build + +node_modules: package.json yarn.lock + yarn install --pure-lockfile --no-progress From d06ad98ec98490504c5520022cbb367396654ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 29 Oct 2018 04:19:45 -0700 Subject: [PATCH 48/50] Revert to sync loading of css, sometimes js loaded before css which caused issues --- public/views/index.template.html | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index ced39d9af28..8b6d6a0775c 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -14,6 +14,9 @@ + + + @@ -253,14 +256,7 @@ navTree: [[.NavTree]] }; - // load css async - var myCSS = document.createElement("link"); - myCSS.rel = "stylesheet"; - myCSS.href = "public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]+[[ .BuildCommit ]]"; - - // 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 + // In case the js files fails to load the code below will show an info message. window.onload = function() { var preloader = document.getElementsByClassName("preloader"); if (preloader.length) { From 2dde2c4f9b4581c61f92f0a93e70844d9796033b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 29 Oct 2018 04:26:44 -0700 Subject: [PATCH 49/50] now that css is loaded sync again I can remove some styles from index html body css --- public/views/index.template.html | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index 8b6d6a0775c..0717908c84c 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -26,13 +26,6 @@