From f121e8309db5a169467177a5b6cc3ee9b78a719e Mon Sep 17 00:00:00 2001 From: Shani Elharrar Date: Thu, 3 Mar 2016 22:34:28 +0200 Subject: [PATCH 01/34] ElasticSearch Terms & Date Histogram: Support 'missing' setting --- .../datasource/elasticsearch/bucket_agg.js | 1 + .../elasticsearch/partials/bucket_agg.html | 36 +++++++++++++++---- .../datasource/elasticsearch/query_builder.js | 8 +++++ .../datasource/elasticsearch/query_def.js | 4 +-- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index 47d88a6ea3c..c397d7936a8 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -27,6 +27,7 @@ function (angular, _, queryDef) { $scope.orderByOptions = []; $scope.bucketAggTypes = queryDef.bucketAggTypes; + $scope.bucketAggTypesHash = _.indexBy(queryDef.bucketAggTypes, 'value'); $scope.orderOptions = queryDef.orderOptions; $scope.sizeOptions = queryDef.sizeOptions; diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 5c1b538cf9a..d222b55d856 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -57,7 +57,7 @@
-
+
  • Trim edges points @@ -71,11 +71,23 @@
+
+
    +
  • + Missing + The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value +
  • +
  • + +
  • +
+
+
    -
  • +
  • Order
  • @@ -86,7 +98,7 @@
    -
  • +
  • Size
  • @@ -95,9 +107,9 @@
-
+
    -
  • +
  • Order By
  • @@ -106,6 +118,18 @@
+
+
    +
  • + Missing + The missing parameter defines how documents that are missing a value should be treated. By default they will be ignored but it is also possible to treat them as if they had a value +
  • +
  • + +
  • +
+
+
@@ -129,5 +153,3 @@
- - diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.js b/public/app/plugins/datasource/elasticsearch/query_builder.js index dd071ba137d..d8c28ed1d82 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.js +++ b/public/app/plugins/datasource/elasticsearch/query_builder.js @@ -48,6 +48,10 @@ function (queryDef) { } } + if (aggDef.settings.missing) { + queryNode.terms.missing = aggDef.settings.missing; + } + return queryNode; }; @@ -67,6 +71,10 @@ function (queryDef) { esAgg.format = "epoch_millis"; } + if (settings.missing) { + esAgg.missing = settings.missing; + } + return esAgg; }; diff --git a/public/app/plugins/datasource/elasticsearch/query_def.js b/public/app/plugins/datasource/elasticsearch/query_def.js index ae44af3e2db..7e76a13731a 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.js +++ b/public/app/plugins/datasource/elasticsearch/query_def.js @@ -20,9 +20,9 @@ function (_) { ], bucketAggTypes: [ - {text: "Terms", value: 'terms' }, + {text: "Terms", value: 'terms', supportsMissing: true }, {text: "Filters", value: 'filters' }, - {text: "Date Histogram", value: 'date_histogram' }, + {text: "Date Histogram", value: 'date_histogram', supportsMissing: true }, ], orderByOptions: [ From 15e369ec02a8392faca072f9f7d3f40f443943df Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Wed, 2 Nov 2016 17:21:11 -0400 Subject: [PATCH 02/34] First pass of getting started panel for home dashboard --- .../plugins/panel/gettingstarted/README.md | 2 + .../plugins/panel/gettingstarted/editor.html | 40 ++ .../gettingstarted/img/icn-dashlist-panel.svg | 119 +++++ .../plugins/panel/gettingstarted/module.html | 33 ++ .../plugins/panel/gettingstarted/module.ts | 15 + .../plugins/panel/gettingstarted/plugin.json | 16 + public/sass/_grafana.scss | 1 + .../components/_panel_gettingstarted.scss | 486 ++++++++++++++++++ 8 files changed, 712 insertions(+) create mode 100644 public/app/plugins/panel/gettingstarted/README.md create mode 100644 public/app/plugins/panel/gettingstarted/editor.html create mode 100644 public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg create mode 100644 public/app/plugins/panel/gettingstarted/module.html create mode 100644 public/app/plugins/panel/gettingstarted/module.ts create mode 100644 public/app/plugins/panel/gettingstarted/plugin.json create mode 100644 public/sass/components/_panel_gettingstarted.scss diff --git a/public/app/plugins/panel/gettingstarted/README.md b/public/app/plugins/panel/gettingstarted/README.md new file mode 100644 index 00000000000..463769dad1f --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/README.md @@ -0,0 +1,2 @@ +# Plugin List Panel - Native Plugin + diff --git a/public/app/plugins/panel/gettingstarted/editor.html b/public/app/plugins/panel/gettingstarted/editor.html new file mode 100644 index 00000000000..c0577578598 --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/editor.html @@ -0,0 +1,40 @@ +
+
+
+ Mode +
+ +
+
+
+ + + +
+
+ +
+
+ Search options + Query + + + +
+ +
+ Tags + + + +
+
+ +
+
+ Limit number to + +
+
+
diff --git a/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg b/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg new file mode 100644 index 00000000000..8bac231bedf --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/img/icn-dashlist-panel.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html new file mode 100644 index 00000000000..ac18a53bd4d --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -0,0 +1,33 @@ +
+
+
+ Getting Started with Grafana + +
+ +
+
diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts new file mode 100644 index 00000000000..178c378f04d --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -0,0 +1,15 @@ +/// + +import {PanelCtrl} from 'app/plugins/sdk'; + +class GettingstartedPanelCtrl extends PanelCtrl { + static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html'; + + /** @ngInject */ + constructor($scope, $injector) { + super($scope, $injector); + } + +} + +export {GettingstartedPanelCtrl, GettingstartedPanelCtrl as PanelCtrl} diff --git a/public/app/plugins/panel/gettingstarted/plugin.json b/public/app/plugins/panel/gettingstarted/plugin.json new file mode 100644 index 00000000000..afcac53b578 --- /dev/null +++ b/public/app/plugins/panel/gettingstarted/plugin.json @@ -0,0 +1,16 @@ +{ + "type": "panel", + "name": "Getting Started", + "id": "gettingstarted", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/icn-dashlist-panel.svg", + "large": "img/icn-dashlist-panel.svg" + } + } +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 936ce0af4d6..c12d8a6e1a0 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -42,6 +42,7 @@ @import "components/panel_graph"; @import "components/submenu"; @import "components/panel_dashlist"; +@import "components/panel_gettingstarted"; @import "components/panel_pluginlist"; @import "components/panel_singlestat"; @import "components/panel_table"; diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss new file mode 100644 index 00000000000..c2b3366d54d --- /dev/null +++ b/public/sass/components/_panel_gettingstarted.scss @@ -0,0 +1,486 @@ +ul.gettingstarted-flex-container { + display: flex; + justify-content: space-around; + flex-direction: row; + padding: 20px; + list-style-type: none; +} + +.gettingstarted-flex-item { + align-items: center; + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.gettingstarted-blurb-copy { + font-size: $font-size-base; + margin-bottom: $spacer/2; + text-align: center; +} + +a.gettingstarted-blurb{ + @extend .gettingstarted-blurb-copy; + color: $text-color; + display: block; +} + +a.gettingstarted-blurb:hover{ + text-decoration: underline; +} + +.gettingstarted-blurb-success { + @extend .gettingstarted-blurb-copy; + color: $text-color-weak; + text-decoration: line-through; +} + +a.gettingstarted-blurb-upcoming { + @extend .gettingstarted-blurb-copy; + color: $text-color-weak; +} + +.gettingstarted-icon-container { + height: 50px; +} + +.gettingstarted-icon-active { + color: $brand-primary; + -webkit-text-fill-color: transparent; + background: $brand-gradient; + -webkit-background-clip: text; + text-decoration:none; + font-size: 35px; + vertical-align: sub; + animation: iconPulse 500ms forwards 1s; + will-change: zoom; +} + +.gettingstarted-icon-upcoming { + color: $text-color-weak; + text-decoration:none; + font-size: 35px; + vertical-align: sub; +} + +.gettingstarted-icon-success { + color: $online; + font-size: 35px; + text-decoration:none; + vertical-align: sub; +} + + +.dashlist-CTA-close-btn { + float: right; + padding: 0; + margin: 0 2px 0 0; + background-color: transparent; + border: none; + i { + font-size: 80%; + } + color: $text-color-weak; + &:hover { + color: $white; + } +} + +@keyframes iconPulse { + from { + zoom: 100%; + } + + 50% { + zoom: 102%; + } + + to { + zoom: 100%; + } +} + + +// ----- Progress Tracker ----- + +// ----- Variables ----- + +// Colours +$progress-color-dark: $panel-bg !default; +$progress-color: $panel-bg !default; +$progress-color-light: $panel-bg !default; +$progress-color-grey-light: $body-bg !default; +$progress-color-grey: $iconContainerBackground !default; +$progress-color-grey-dark: $iconContainerBackground !default; + +// Sizing +$progress-tracker-padding: 5px !default; + +$marker-size: 60px !default; +$marker-size-half: ($marker-size / 2); +$marker-size-third: ($marker-size / 3); +$marker-size-quarter: ($marker-size / 4); +$marker-spacing: 10px !default; + +$path-height: 4px !default; +$path-position: $marker-size-half - ($path-height / 2); + +$text-padding: $marker-size-half !default; +$text-padding-X: $marker-size-third !default; +$text-padding-Y: 5px !default; +$text-padding--vertical: $marker-size + $marker-size-half !default; + +// Only needed for short text version, the word size should be the width of the widest word without padding. +$word-size: 54px !default; +$progress-tracker-word-padding: ($word-size + $text-padding-X + $marker-size-half) / 2; + +// Animations/Transitions +$animation-duration: 0.3s !default; +$ripple-color: rgba(0, 0, 0, 0.3) !default; + +// ----- Elements ----- + +// Container element +.progress-tracker { + display: flex; + margin: 20px auto; + padding: 0; + list-style: none; +} + +// Step container that creates lines between steps + .progress-step { + display: block; + position: relative; + flex: 1 1 0%; + margin: 0; + padding: 0; + min-width: $marker-size; // For a flexbox bug in firefox that wont allow the text overflow on the text + + // Stops the last step growing + &:last-child { + flex-grow: 0; + } + + // Path between markers, this is not created for the last step + &:not(:last-child)::after { + content: ''; + display: block; + position: absolute; + z-index: 1; + top: $path-position; + bottom: $path-position; + right: - $marker-size-half; + width: 100%; + height: $path-height; + transition: background-color $animation-duration; + } + + // Active state + &.is-active { + .progress-title { + font-weight: 400; + } + } + + > a { + display: block; + } + + } + +// Progress marker + .progress-marker { + display: flex; + justify-content: center; + align-items: center; + position: relative; + z-index: 20; + width: $marker-size; + height: $marker-size; + padding-bottom: 2px; // To align text within the marker + color: #fff; + font-weight: 400; + border: 2px solid transparent; + border-radius: 50%; + transition: background-color, border-color; + transition-duration: $animation-duration; + } + + +// Progress text + .progress-text { + display: block; + padding: $text-padding-Y $text-padding-X; + overflow: hidden; + text-overflow: ellipsis; + } + .progress-title { + margin-top: 0; + } + + +// Step state mixin - The first arugment is required and the rest are optional, if you pass in null the value will not be changed. +@mixin progress-state($marker-color-bg, $marker-color-border: null, $marker-color-text: null, $path-color: null, $text-color: null) { + .progress-marker { + color: $marker-color-text; + background-color: $marker-color-bg; + border-color: $marker-color-border; + } + + &::after { + background-color: $path-color; + } + + .progress-text, .progress-step > a .progress-text { + color: $text-color; + } +} + + +// States + .progress-step { + + // Inactive - Default state + @include progress-state($progress-color, null, #fff, $progress-color-grey-light, $progress-color-grey-dark); + + // Active state + &.is-active { + @include progress-state($progress-color); + } + + // Complete state + &.is-complete { + @include progress-state($progress-color-dark, $path-color: $progress-color-grey); + } + + // Hover state + &:hover { + @include progress-state($progress-color-light); + } + + } + + + +// ----- Modifiers ----- + +// Center align markers and text +.progress-tracker--center { + + .progress-step { + text-align: center; + + &:last-child { + flex-grow: 1; + } + + &::after { + right: -50%; + } + } + + .progress-marker { + margin-left: auto; + margin-right: auto; + } +} + + +// Right align markers and text +.progress-tracker--right { + + .progress-step { + text-align: right; + + &:last-child { + flex-grow: 1; + } + + &::after { + right: calc(-100% + #{$marker-size-half}); + } + } + + .progress-marker { + margin-left: auto; + } +} + + +// Border around steps (Only for use without text) +.progress-tracker--border { + padding: $progress-tracker-padding; + border: 2px solid $progress-color-grey; + border-radius: $marker-size + ($progress-tracker-padding * 2); +} + + +// Spaces between markers +.progress-tracker--spaced { + + .progress-step { + + &::after { + width: calc(100% - #{$marker-size + ($marker-spacing * 2)}); + margin-right: ($marker-size-half + $marker-spacing); + } + } + +} + + +// Word below markers +.progress-tracker--word { + padding-right: $progress-tracker-word-padding; + overflow: hidden; + + .progress-text { + display: inline-block; + white-space: nowrap; + } + + .progress-title { + margin: 0; + } + +} + + +.progress-tracker--word-center { + padding-right: $progress-tracker-word-padding; + padding-left: $progress-tracker-word-padding; + + .progress-text { + padding-right: 0; + padding-left: 0; + transform: translateX(calc(-50% + #{$marker-size-half})); + } + +} + + +.progress-tracker--word-right { + padding-right: 0; + padding-left: $progress-tracker-word-padding; + + .progress-text { + padding-left: 0; + transform: translateX(calc(-100% + #{$marker-size})); + } + +} + + +// Text below markers +.progress-tracker--text { + + .progress-step { + + &:last-child { + flex-grow: 1; + } + } + +} + + +// Text above markers +.progress-tracker--text-top { + + .progress-step::after { + top: auto; + } + + .progress-text { + height: 100%; + } + + .progress-marker { + bottom: $marker-size; + } + +} + + +// Text inline with markers +.progress-tracker--text-inline { + + .progress-step { + display: flex; + } + + .progress-text { + position: relative; + z-index: 30; + max-width: 70%; + white-space: nowrap; + padding-top: 0; + padding-bottom: 0; + background-color: #fff; + } + + .progress-title { + margin: 0; + } + +} + + +// Square markers +.progress-tracker--square { + + .progress-step { + padding-top: 0; + } + + .progress-marker { + transform: scaleX(0.33) translateY(- $path-position); + border-radius: 0; + } + +} + + +// Overflow on small screens +@media (max-width: 399px) { + .progress-tracker-mobile { + overflow-x: auto; + + .progress-tracker { + min-width: 200%; + } + } +} + + +// Vertical layout +.progress-tracker--vertical { + flex-direction: column; + + .progress-step { + flex: 1 1 auto; + + &::after { + right: auto; + top: $marker-size-half; + left: $path-position; + width: $path-height; + height: 100%; + } + } + + .progress-marker { + position: absolute; + left: 0; + } + + .progress-text { + padding-top: $marker-size-quarter; + padding-left: $text-padding--vertical; + } + + .progress-step:not(:last-child) .progress-text { + padding-bottom: $text-padding--vertical; + } +} From 4c891e37585ff8c08a2a778c5901f7af97eececf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 8 Nov 2016 15:06:07 +0100 Subject: [PATCH 03/34] docs(): minor spelling fix --- docs/sources/alerting/notifications.md | 55 ++++++++------------------ 1 file changed, 16 insertions(+), 39 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index f274001b9d6..455d61cf535 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -25,31 +25,12 @@ to add and configure a `notification` object. This is done from the Alerting/Not On the notifications list page hit the `New Notification` button to go the the page where you can configure and setup a new notification. -You you specify name and type, and type specific options. You can also test the notification to make +You specify name and type, and type specific options. You can also test the notification to make sure it's working and setup correctly. - - - - - - - - - - - - - - - - - - - ### Send on all alerts -This option will make this notification used for all alert rules, existing and new. +When checked this option will make this notification used for all alert rules, existing and new. ## Supported notification types @@ -61,12 +42,25 @@ To enable email notification you have to setup [SMTP settings](/installation/con in the Grafana config. Email notification will upload an image of the alert graph to an external image destination if available or fallback on attaching the image in the email. +### Slack + +{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}} + +To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how +to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts +in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana. + +Setting | Description +---------- | ----------- +Recipient | allows you to override the slack recipient. +Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel + ### Webhook The webhook notification is a simple way to send information about an state change over HTTP to a custom endpoint. Using this notification you could integrated Grafana into any system you choose, by yourself. -Example json schema: +Example json body: ```json { "title": "My alert", @@ -85,19 +79,6 @@ Example json schema: } ``` -### Slack - -{{< imgbox max-width="40%" img="/img/docs/v4/slack_notification.png" caption="Alerting Slack Notification" >}} - -To set up slack you need to configure an incoming webhook url at slack. You can follow their guide for how -to do that https://api.slack.com/incoming-webhooks If you want to include screenshots of the firing alerts -in the slack messages you have to configure the [external image destination](#external-image-store) in Grafana. - -Setting | Description ----------- | ----------- -Recipient | allows you to override the slack recipient. -Mention | make it possible to include a mention in the slack notification sent by Grafana. Ex @here or @channel - ### PagerDuty To set up PagerDuty, all you have to do is to provide an api key. @@ -117,7 +98,3 @@ config file. This is not an optional requirement, you can get slack and email notifications without setting this up. - - - - From d92bb677dfd09e4f0882631d62310c2a956a07d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 8 Nov 2016 16:15:34 +0100 Subject: [PATCH 04/34] docs(): fix title --- docs/sources/alerting/rules.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 823637941da..a30bf885650 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -1,5 +1,5 @@ +++ -title = "Alerting Engine Rules Guide" +title = "Alerting Engine & Rules Guide" description = "Configuring Alert Rules" keywords = ["grafana", "alerting", "guide", "rules"] type = "docs" From fb57bf77daec82325198ea76f5a95f9054aa3c04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Nov 2016 10:41:39 +0100 Subject: [PATCH 05/34] ux(getting started): progress on getting started panel and persited help flag states, #6466 --- pkg/api/api.go | 3 + pkg/api/dtos/models.go | 27 ++++---- pkg/api/index.go | 1 + pkg/api/user.go | 31 ++++++++++ pkg/models/helpflags.go | 18 ++++++ pkg/models/user.go | 2 + pkg/services/sqlstore/migrations/user_mig.go | 4 ++ pkg/services/sqlstore/user.go | 21 ++++++- public/app/core/services/backend_srv.ts | 4 +- public/app/core/services/context_srv.ts | 1 + .../app/features/dashboard/row/add_panel.ts | 3 - .../app/features/dashboard/row/row_model.ts | 1 - .../plugins/panel/gettingstarted/module.html | 23 +++++-- .../plugins/panel/gettingstarted/module.ts | 39 ++++++++++-- public/dashboards/home.json | 12 +++- .../components/_panel_gettingstarted.scss | 62 +++---------------- 16 files changed, 165 insertions(+), 87 deletions(-) create mode 100644 pkg/models/helpflags.go diff --git a/pkg/api/api.go b/pkg/api/api.go index ed73f2dc76d..6ea5bcd7c95 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -113,6 +113,9 @@ func Register(r *macaron.Macaron) { r.Put("/password", bind(m.ChangeUserPasswordCommand{}), wrap(ChangeUserPassword)) r.Get("/quotas", wrap(GetUserQuotas)) + r.Put("/helpflags/:id", wrap(SetHelpFlag)) + // For dev purpose + r.Get("/helpflags/clear", wrap(ClearHelpFlags)) r.Get("/preferences", wrap(GetUserPreferences)) r.Put("/preferences", bind(dtos.UpdatePrefsCmd{}), wrap(UpdateUserPreferences)) diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 170a5a868fc..7f8d107d9f4 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -22,19 +22,20 @@ type LoginCommand struct { } type CurrentUser struct { - IsSignedIn bool `json:"isSignedIn"` - Id int64 `json:"id"` - Login string `json:"login"` - Email string `json:"email"` - Name string `json:"name"` - LightTheme bool `json:"lightTheme"` - OrgId int64 `json:"orgId"` - OrgName string `json:"orgName"` - OrgRole m.RoleType `json:"orgRole"` - IsGrafanaAdmin bool `json:"isGrafanaAdmin"` - GravatarUrl string `json:"gravatarUrl"` - Timezone string `json:"timezone"` - Locale string `json:"locale"` + IsSignedIn bool `json:"isSignedIn"` + Id int64 `json:"id"` + Login string `json:"login"` + Email string `json:"email"` + Name string `json:"name"` + LightTheme bool `json:"lightTheme"` + OrgId int64 `json:"orgId"` + OrgName string `json:"orgName"` + OrgRole m.RoleType `json:"orgRole"` + IsGrafanaAdmin bool `json:"isGrafanaAdmin"` + GravatarUrl string `json:"gravatarUrl"` + Timezone string `json:"timezone"` + Locale string `json:"locale"` + HelpFlags1 m.HelpFlags1 `json:"helpFlags1"` } type DashboardMeta struct { diff --git a/pkg/api/index.go b/pkg/api/index.go index 99a5f78f9c9..5bc4344a8ba 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -58,6 +58,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { LightTheme: prefs.Theme == "light", Timezone: prefs.Timezone, Locale: locale, + HelpFlags1: c.HelpFlags1, }, Settings: settings, AppUrl: appUrl, diff --git a/pkg/api/user.go b/pkg/api/user.go index f98eec02c40..715103aacbd 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -180,3 +180,34 @@ func SearchUsers(c *middleware.Context) Response { return Json(200, query.Result) } + +func SetHelpFlag(c *middleware.Context) Response { + flag := c.ParamsInt64(":id") + + bitmask := &c.HelpFlags1 + bitmask.AddFlag(m.HelpFlags1(flag)) + + cmd := m.SetUserHelpFlagCommand{ + UserId: c.UserId, + HelpFlags1: *bitmask, + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to update help flag", err) + } + + return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) +} + +func ClearHelpFlags(c *middleware.Context) Response { + cmd := m.SetUserHelpFlagCommand{ + UserId: c.UserId, + HelpFlags1: m.HelpFlags1(0), + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to update help flag", err) + } + + return Json(200, &util.DynMap{"message": "Help flag set", "helpFlags1": cmd.HelpFlags1}) +} diff --git a/pkg/models/helpflags.go b/pkg/models/helpflags.go new file mode 100644 index 00000000000..1bab730e265 --- /dev/null +++ b/pkg/models/helpflags.go @@ -0,0 +1,18 @@ +package models + +type HelpFlags1 uint64 + +const ( + HelpFlagGettingStartedPanelDismissed HelpFlags1 = 1 << iota + HelpFlagDashboardHelp1 +) + +func (f HelpFlags1) HasFlag(flag HelpFlags1) bool { return f&flag != 0 } +func (f *HelpFlags1) AddFlag(flag HelpFlags1) { *f |= flag } +func (f *HelpFlags1) ClearFlag(flag HelpFlags1) { *f &= ^flag } +func (f *HelpFlags1) ToggleFlag(flag HelpFlags1) { *f ^= flag } + +type SetUserHelpFlagCommand struct { + HelpFlags1 HelpFlags1 + UserId int64 +} diff --git a/pkg/models/user.go b/pkg/models/user.go index 1f99f866c86..8bfad7c2fb4 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -22,6 +22,7 @@ type User struct { Company string EmailVerified bool Theme string + HelpFlags1 HelpFlags1 IsAdmin bool OrgId int64 @@ -144,6 +145,7 @@ type SignedInUser struct { Email string ApiKeyId int64 IsGrafanaAdmin bool + HelpFlags1 HelpFlags1 } type UserProfileDTO struct { diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index 51db4d74857..67446f64d1a 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -88,4 +88,8 @@ func addUserMigrations(mg *Migrator) { })) mg.AddMigration("Drop old table user_v1", NewDropTableMigration("user_v1")) + + mg.AddMigration("Add column help_flags1 to user table", NewAddColumnMigration(userV2, &Column{ + Name: "help_flags1", Type: DB_BigInt, Nullable: false, Default: "0", + })) } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index bb21995f54a..b26a9153f55 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -28,6 +28,7 @@ func init() { bus.AddHandler("sql", DeleteUser) bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", UpdateUserPermissions) + bus.AddHandler("sql", SetUserHelpFlag) } func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *session) (int64, error) { @@ -207,7 +208,7 @@ func GetUserByEmail(query *m.GetUserByEmailQuery) error { if err != nil { return err } else if has == false { - return m.ErrUserNotFound + return m.ErrUserNotFound } query.Result = user @@ -308,6 +309,7 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { u.email as email, u.login as login, u.name as name, + u.help_flags1 as help_flags1, org.name as org_name, org_user.role as org_role, org.id as org_id @@ -380,3 +382,20 @@ func UpdateUserPermissions(cmd *m.UpdateUserPermissionsCommand) error { return err }) } + +func SetUserHelpFlag(cmd *m.SetUserHelpFlagCommand) error { + return inTransaction2(func(sess *session) error { + + user := m.User{ + Id: cmd.UserId, + HelpFlags1: cmd.HelpFlags1, + Updated: time.Now(), + } + + if _, err := sess.Id(cmd.UserId).Cols("help_flags1").Update(&user); err != nil { + return err + } + + return nil + }) +} diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 1e620e88216..9a5ec5d219b 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -74,7 +74,9 @@ export class BackendSrv { return this.$http(options).then(results => { if (options.method !== 'GET') { if (results && results.data.message) { - this.alertSrv.set(results.data.message, '', 'success', 3000); + if (options.showSuccessAlert !== false) { + this.alertSrv.set(results.data.message, '', 'success', 3000); + } } } return results.data; diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index ac00528db20..3d048e4f869 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -10,6 +10,7 @@ export class User { isSignedIn: any; orgRole: any; timezone: string; + helpFlags1: number; constructor() { if (config.bootData.user) { diff --git a/public/app/features/dashboard/row/add_panel.ts b/public/app/features/dashboard/row/add_panel.ts index 3e2dc9e31d8..189108d9b4a 100644 --- a/public/app/features/dashboard/row/add_panel.ts +++ b/public/app/features/dashboard/row/add_panel.ts @@ -84,11 +84,8 @@ export class AddPanelCtrl { var panel = { id: null, title: config.new_panel_title, - error: false, span: span < defaultSpan && span > 0 ? span : defaultSpan, - editable: true, type: panelPluginInfo.id, - isNew: true, }; this.rowCtrl.closeDropView(); diff --git a/public/app/features/dashboard/row/row_model.ts b/public/app/features/dashboard/row/row_model.ts index 0729d43bf0a..cbde701ccfe 100644 --- a/public/app/features/dashboard/row/row_model.ts +++ b/public/app/features/dashboard/row/row_model.ts @@ -19,7 +19,6 @@ export class DashboardRow { showTitle: false, titleSize: 'h6', height: 250, - isNew: false, repeat: null, repeatRowId: null, repeatIteration: null, diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html index ac18a53bd4d..0c319a46d81 100644 --- a/public/app/plugins/panel/gettingstarted/module.html +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -2,23 +2,34 @@
Getting Started with Grafana - +
-
    -
  • +
      +
    • Install Grafana
    • -
    • +
    • Create your first data source.
    • -
    • +
    • + + + Create your first data source. + +
    • +
    • - Create your first dashboard. + + Create your first dashboard. + +
    • diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index 178c378f04d..e78203647c0 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -2,14 +2,43 @@ import {PanelCtrl} from 'app/plugins/sdk'; -class GettingstartedPanelCtrl extends PanelCtrl { - static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html'; +import {contextSrv} from 'app/core/core'; - /** @ngInject */ - constructor($scope, $injector) { +class GettingStartedPanelCtrl extends PanelCtrl { + static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html'; + hasDatasources: boolean; + checksDone: boolean; + + /** @ngInject **/ + constructor($scope, $injector, private backendSrv, private datasourceSrv) { super($scope, $injector); + + /* tslint:disable */ + if (contextSrv.user.helpFlags1 & 1) { + this.row.removePanel(this.panel, false); + return; + } + /* tslint:enable */ + + var datasources = datasourceSrv.getMetricSources().filter(item => { + return item.meta.builtIn === false; + }); + + this.hasDatasources = datasources.length > 0; + this.checksDone = true; } + dismiss() { + this.row.removePanel(this.panel, false); + + this.backendSrv.request({ + method: 'PUT', + url: '/api/user/helpflags/1', + showSuccessAlert: false, + }).then(res => { + contextSrv.user.helpFlags1 = res.helpFlags1; + }); + } } -export {GettingstartedPanelCtrl, GettingstartedPanelCtrl as PanelCtrl} +export {GettingStartedPanelCtrl, GettingStartedPanelCtrl as PanelCtrl} diff --git a/public/dashboards/home.json b/public/dashboards/home.json index 393cbc5865c..0ea0af67055 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -9,6 +9,7 @@ "sharedCrosshair": false, "rows": [ { + "title": "Row title", "collapse": false, "editable": true, "height": "25px", @@ -24,9 +25,16 @@ "title": "", "transparent": true, "type": "text" + }, + { + "id": 8, + "links": [], + "span": 12, + "title": "", + "transparent": false, + "type": "gettingstarted" } - ], - "title": "New row" + ] }, { "collapse": false, diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index c2b3366d54d..fd92af59179 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -1,11 +1,3 @@ -ul.gettingstarted-flex-container { - display: flex; - justify-content: space-around; - flex-direction: row; - padding: 20px; - list-style-type: none; -} - .gettingstarted-flex-item { align-items: center; display: flex; @@ -19,14 +11,14 @@ ul.gettingstarted-flex-container { text-align: center; } -a.gettingstarted-blurb{ +.gettingstarted-blurb { @extend .gettingstarted-blurb-copy; color: $text-color; display: block; -} -a.gettingstarted-blurb:hover{ - text-decoration: underline; + &:hover{ + text-decoration: underline; + } } .gettingstarted-blurb-success { @@ -35,27 +27,11 @@ a.gettingstarted-blurb:hover{ text-decoration: line-through; } -a.gettingstarted-blurb-upcoming { +.gettingstarted-blurb-upcoming { @extend .gettingstarted-blurb-copy; color: $text-color-weak; } -.gettingstarted-icon-container { - height: 50px; -} - -.gettingstarted-icon-active { - color: $brand-primary; - -webkit-text-fill-color: transparent; - background: $brand-gradient; - -webkit-background-clip: text; - text-decoration:none; - font-size: 35px; - vertical-align: sub; - animation: iconPulse 500ms forwards 1s; - will-change: zoom; -} - .gettingstarted-icon-upcoming { color: $text-color-weak; text-decoration:none; @@ -70,7 +46,6 @@ a.gettingstarted-blurb-upcoming { vertical-align: sub; } - .dashlist-CTA-close-btn { float: right; padding: 0; @@ -86,25 +61,6 @@ a.gettingstarted-blurb-upcoming { } } -@keyframes iconPulse { - from { - zoom: 100%; - } - - 50% { - zoom: 102%; - } - - to { - zoom: 100%; - } -} - - -// ----- Progress Tracker ----- - -// ----- Variables ----- - // Colours $progress-color-dark: $panel-bg !default; $progress-color: $panel-bg !default; @@ -240,17 +196,16 @@ $ripple-color: rgba(0, 0, 0, 0.3) !default; // States .progress-step { - // Inactive - Default state @include progress-state($progress-color, null, #fff, $progress-color-grey-light, $progress-color-grey-dark); // Active state - &.is-active { + &.active { @include progress-state($progress-color); } // Complete state - &.is-complete { + &.completed { @include progress-state($progress-color-dark, $path-color: $progress-color-grey); } @@ -258,11 +213,8 @@ $ripple-color: rgba(0, 0, 0, 0.3) !default; &:hover { @include progress-state($progress-color-light); } - } - - // ----- Modifiers ----- // Center align markers and text From 18a37a9eef1bd2d772c92a8ab7b148968eef6991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 10 Nov 2016 14:21:37 +0100 Subject: [PATCH 06/34] ux(getting started): struggleing witch cleaning up the css, #6466 --- .../plugins/panel/gettingstarted/module.html | 4 +- .../components/_panel_gettingstarted.scss | 431 ++++-------------- 2 files changed, 97 insertions(+), 338 deletions(-) diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html index 0c319a46d81..2086ffbfc0e 100644 --- a/public/app/plugins/panel/gettingstarted/module.html +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -2,11 +2,11 @@
      Getting Started with Grafana -
      -
        +
        • Install Grafana diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index fd92af59179..baad57157b2 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -1,12 +1,20 @@ -.gettingstarted-flex-item { - align-items: center; - display: flex; - flex-direction: column; - justify-content: flex-start; -} + +// Colours +$progress-color-dark: $panel-bg !default; +$progress-color: $panel-bg !default; +$progress-color-light: $panel-bg !default; +$progress-color-grey-light: $body-bg !default; +$progress-color-grey: $iconContainerBackground !default; +$progress-color-grey-dark: $iconContainerBackground !default; + +// Sizing +$marker-size: 60px !default; +$marker-size-half: ($marker-size / 2); +$path-height: 4px !default; +$path-position: $marker-size-half - ($path-height / 2); + .gettingstarted-blurb-copy { - font-size: $font-size-base; margin-bottom: $spacer/2; text-align: center; } @@ -46,56 +54,23 @@ vertical-align: sub; } -.dashlist-CTA-close-btn { +.dashlist-cta-close-btn { + color: $text-color-weak; float: right; padding: 0; margin: 0 2px 0 0; background-color: transparent; border: none; + i { font-size: 80%; } - color: $text-color-weak; + &:hover { color: $white; } } -// Colours -$progress-color-dark: $panel-bg !default; -$progress-color: $panel-bg !default; -$progress-color-light: $panel-bg !default; -$progress-color-grey-light: $body-bg !default; -$progress-color-grey: $iconContainerBackground !default; -$progress-color-grey-dark: $iconContainerBackground !default; - -// Sizing -$progress-tracker-padding: 5px !default; - -$marker-size: 60px !default; -$marker-size-half: ($marker-size / 2); -$marker-size-third: ($marker-size / 3); -$marker-size-quarter: ($marker-size / 4); -$marker-spacing: 10px !default; - -$path-height: 4px !default; -$path-position: $marker-size-half - ($path-height / 2); - -$text-padding: $marker-size-half !default; -$text-padding-X: $marker-size-third !default; -$text-padding-Y: 5px !default; -$text-padding--vertical: $marker-size + $marker-size-half !default; - -// Only needed for short text version, the word size should be the width of the widest word without padding. -$word-size: 54px !default; -$progress-tracker-word-padding: ($word-size + $text-padding-X + $marker-size-half) / 2; - -// Animations/Transitions -$animation-duration: 0.3s !default; -$ripple-color: rgba(0, 0, 0, 0.3) !default; - -// ----- Elements ----- - // Container element .progress-tracker { display: flex; @@ -105,78 +80,71 @@ $ripple-color: rgba(0, 0, 0, 0.3) !default; } // Step container that creates lines between steps - .progress-step { - display: block; - position: relative; - flex: 1 1 0%; - margin: 0; - padding: 0; - min-width: $marker-size; // For a flexbox bug in firefox that wont allow the text overflow on the text - - // Stops the last step growing - &:last-child { - flex-grow: 0; - } - - // Path between markers, this is not created for the last step - &:not(:last-child)::after { - content: ''; - display: block; - position: absolute; - z-index: 1; - top: $path-position; - bottom: $path-position; - right: - $marker-size-half; - width: 100%; - height: $path-height; - transition: background-color $animation-duration; - } - - // Active state - &.is-active { - .progress-title { - font-weight: 400; - } - } - - > a { - display: block; - } +.progress-step { + display: block; + position: relative; + flex: 1 1 0%; + margin: 0; + padding: 0; + min-width: $marker-size; // For a flexbox bug in firefox that wont allow the text overflow on the text + // Stops the last step growing + &:last-child { + flex-grow: 0; } -// Progress marker - .progress-marker { - display: flex; - justify-content: center; - align-items: center; - position: relative; - z-index: 20; - width: $marker-size; - height: $marker-size; - padding-bottom: 2px; // To align text within the marker - color: #fff; + // Path between markers, this is not created for the last step + &:not(:last-child)::after { + content: ''; + display: block; + position: absolute; + z-index: 1; + top: $path-position; + bottom: $path-position; + right: - $marker-size-half; + width: 100%; + height: $path-height; + } + + // Active state + &.active { + .progress-title { font-weight: 400; - border: 2px solid transparent; - border-radius: 50%; - transition: background-color, border-color; - transition-duration: $animation-duration; } + } + + > a { + display: block; + } + +} + +// Progress marker +.progress-marker { + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: $marker-size; + height: $marker-size; + padding-bottom: 2px; // To align text within the marker + z-index: 20; + background-color: $panel-bg; +} // Progress text - .progress-text { - display: block; - padding: $text-padding-Y $text-padding-X; - overflow: hidden; - text-overflow: ellipsis; - } - .progress-title { - margin-top: 0; - } +.progress-text { + display: block; + padding: $spacer; + overflow: hidden; + text-overflow: ellipsis; +} +.progress-title { + margin-top: 0; +} -// Step state mixin - The first arugment is required and the rest are optional, if you pass in null the value will not be changed. @mixin progress-state($marker-color-bg, $marker-color-border: null, $marker-color-text: null, $path-color: null, $text-color: null) { .progress-marker { color: $marker-color-text; @@ -188,251 +156,42 @@ $ripple-color: rgba(0, 0, 0, 0.3) !default; background-color: $path-color; } - .progress-text, .progress-step > a .progress-text { + .progress-text, + .progress-text a { color: $text-color; } } - // States - .progress-step { - // Inactive - Default state - @include progress-state($progress-color, null, #fff, $progress-color-grey-light, $progress-color-grey-dark); +.progress-step { + text-align: center; - // Active state - &.active { - @include progress-state($progress-color); - } - - // Complete state - &.completed { - @include progress-state($progress-color-dark, $path-color: $progress-color-grey); - } - - // Hover state - &:hover { - @include progress-state($progress-color-light); - } + // Active state + &.active { + @include progress-state($progress-color); } -// ----- Modifiers ----- - -// Center align markers and text -.progress-tracker--center { - - .progress-step { - text-align: center; - - &:last-child { - flex-grow: 1; - } - - &::after { - right: -50%; - } + &.completed { + @include progress-state($progress-color-dark, $path-color: $progress-color-grey); } - .progress-marker { - margin-left: auto; - margin-right: auto; - } -} - - -// Right align markers and text -.progress-tracker--right { - - .progress-step { - text-align: right; - - &:last-child { - flex-grow: 1; - } - - &::after { - right: calc(-100% + #{$marker-size-half}); - } + &:hover { + @include progress-state($progress-color-light); } - .progress-marker { - margin-left: auto; - } -} - - -// Border around steps (Only for use without text) -.progress-tracker--border { - padding: $progress-tracker-padding; - border: 2px solid $progress-color-grey; - border-radius: $marker-size + ($progress-tracker-padding * 2); -} - - -// Spaces between markers -.progress-tracker--spaced { - - .progress-step { - - &::after { - width: calc(100% - #{$marker-size + ($marker-spacing * 2)}); - margin-right: ($marker-size-half + $marker-spacing); - } + &:last-child { + flex-grow: 1; } -} - - -// Word below markers -.progress-tracker--word { - padding-right: $progress-tracker-word-padding; - overflow: hidden; - - .progress-text { - display: inline-block; - white-space: nowrap; - } - - .progress-title { - margin: 0; - } - -} - - -.progress-tracker--word-center { - padding-right: $progress-tracker-word-padding; - padding-left: $progress-tracker-word-padding; - - .progress-text { - padding-right: 0; - padding-left: 0; - transform: translateX(calc(-50% + #{$marker-size-half})); - } - -} - - -.progress-tracker--word-right { - padding-right: 0; - padding-left: $progress-tracker-word-padding; - - .progress-text { - padding-left: 0; - transform: translateX(calc(-100% + #{$marker-size})); - } - -} - - -// Text below markers -.progress-tracker--text { - - .progress-step { - - &:last-child { - flex-grow: 1; - } - } - -} - - -// Text above markers -.progress-tracker--text-top { - - .progress-step::after { - top: auto; - } - - .progress-text { - height: 100%; - } - - .progress-marker { - bottom: $marker-size; - } - -} - - -// Text inline with markers -.progress-tracker--text-inline { - - .progress-step { - display: flex; - } - - .progress-text { - position: relative; - z-index: 30; - max-width: 70%; - white-space: nowrap; - padding-top: 0; - padding-bottom: 0; - background-color: #fff; - } - - .progress-title { - margin: 0; - } - -} - - -// Square markers -.progress-tracker--square { - - .progress-step { - padding-top: 0; - } - - .progress-marker { - transform: scaleX(0.33) translateY(- $path-position); - border-radius: 0; - } - -} - - -// Overflow on small screens -@media (max-width: 399px) { - .progress-tracker-mobile { - overflow-x: auto; - - .progress-tracker { - min-width: 200%; - } + &::after { + right: -50%; } } - -// Vertical layout -.progress-tracker--vertical { - flex-direction: column; - - .progress-step { - flex: 1 1 auto; - - &::after { - right: auto; - top: $marker-size-half; - left: $path-position; - width: $path-height; - height: 100%; - } - } - - .progress-marker { - position: absolute; - left: 0; - } - - .progress-text { - padding-top: $marker-size-quarter; - padding-left: $text-padding--vertical; - } - - .progress-step:not(:last-child) .progress-text { - padding-bottom: $text-padding--vertical; - } +.progress-marker { + margin-left: auto; + margin-right: auto; } + + + From 0c9001c7aebff4c1f22a00a0522fa88fc8d16c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Nov 2016 16:31:19 +0100 Subject: [PATCH 07/34] ux(gettingstarted): progress on getting started panel, #6466 --- public/app/features/plugins/ds_edit_ctrl.ts | 16 ++- .../plugins/panel/gettingstarted/module.html | 44 +++---- .../plugins/panel/gettingstarted/module.ts | 26 +++- .../components/_panel_gettingstarted.scss | 122 +++++++----------- 4 files changed, 102 insertions(+), 106 deletions(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 7ac29227001..69993927470 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -28,6 +28,7 @@ export class DataSourceEditCtrl { tabIndex: number; hasDashboards: boolean; editForm: any; + gettingStarted: boolean; /** @ngInject */ constructor( @@ -46,12 +47,23 @@ export class DataSourceEditCtrl { if (this.$routeParams.id) { this.getDatasourceById(this.$routeParams.id); } else { - this.current = angular.copy(defaults); - this.typeChanged(); + this.initNewDatasourceModel(); } }); } + initNewDatasourceModel() { + this.current = angular.copy(defaults); + + // We are coming from getting started + if (this.$location.search().gettingstarted) { + this.gettingStarted = true; + this.current.isDefault = true; + } + + this.typeChanged(); + } + loadDatasourceTypes() { if (datasourceTypes.length > 0) { this.types = datasourceTypes; diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html index 2086ffbfc0e..1be6f484b03 100644 --- a/public/app/plugins/panel/gettingstarted/module.html +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -1,4 +1,4 @@ -
          +
          Getting Started with Grafana @@ -6,38 +6,30 @@
          -
          diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index e78203647c0..2e99bf29676 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -6,8 +6,8 @@ import {contextSrv} from 'app/core/core'; class GettingStartedPanelCtrl extends PanelCtrl { static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html'; - hasDatasources: boolean; checksDone: boolean; + step: number; /** @ngInject **/ constructor($scope, $injector, private backendSrv, private datasourceSrv) { @@ -24,8 +24,28 @@ class GettingStartedPanelCtrl extends PanelCtrl { return item.meta.builtIn === false; }); - this.hasDatasources = datasources.length > 0; - this.checksDone = true; + this.step = 2; + if (datasources.length === 0) { + this.checksDone = true; + return; + } + + this.step = 3; + this.backendSrv.search({limit: 1}).then(result => { + if (result.length === 0) { + this.checksDone = true; + return; + } + + this.step = 4; + this.checksDone = true; + }); + } + + getStateClass(step) { + if (step === this.step) { return 'active'; } + if (step < this.step) { return 'completed'; } + return ''; } dismiss() { diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index baad57157b2..03f5019ff8e 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -32,7 +32,6 @@ $path-position: $marker-size-half - ($path-height / 2); .gettingstarted-blurb-success { @extend .gettingstarted-blurb-copy; color: $text-color-weak; - text-decoration: line-through; } .gettingstarted-blurb-upcoming { @@ -40,19 +39,6 @@ $path-position: $marker-size-half - ($path-height / 2); color: $text-color-weak; } -.gettingstarted-icon-upcoming { - color: $text-color-weak; - text-decoration:none; - font-size: 35px; - vertical-align: sub; -} - -.gettingstarted-icon-success { - color: $online; - font-size: 35px; - text-decoration:none; - vertical-align: sub; -} .dashlist-cta-close-btn { color: $text-color-weak; @@ -81,20 +67,18 @@ $path-position: $marker-size-half - ($path-height / 2); // Step container that creates lines between steps .progress-step { - display: block; + text-align: center; position: relative; flex: 1 1 0%; margin: 0; padding: 0; - min-width: $marker-size; // For a flexbox bug in firefox that wont allow the text overflow on the text + color: $text-color-weak; - // Stops the last step growing - &:last-child { - flex-grow: 0; - } + // For a flexbox bug in firefox that wont allow the text overflow on the text + min-width: $marker-size; - // Path between markers, this is not created for the last step - &:not(:last-child)::after { + &::after { + right: -50%; content: ''; display: block; position: absolute; @@ -104,19 +88,48 @@ $path-position: $marker-size-half - ($path-height / 2); right: - $marker-size-half; width: 100%; height: $path-height; + background: $progress-color-grey-light; + } + + &:last-child { + &::after { + right: 50%; + } } // Active state &.active { + .progress-step-cta { + display: inline-block; + } .progress-title { font-weight: 400; } } - > a { - display: block; - } + &.completed { + .progress-marker { + color: $online; + // change icon to check + .icon-gf::before { + content: "\e604"; + } + } + .progress-text { + text-decoration: line-through; + } + + &::after { + background: $progress-color-grey; + } + } +} + +.progress-step-cta { + @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius); + @include buttonBackground($btn-success-bg, $btn-success-bg-hl); + display: none; } // Progress marker @@ -130,9 +143,13 @@ $path-position: $marker-size-half - ($path-height / 2); padding-bottom: 2px; // To align text within the marker z-index: 20; background-color: $panel-bg; + margin-left: auto; + margin-right: auto; + color: $text-color-weak; + font-size: 35px; + vertical-align: sub; } - // Progress text .progress-text { display: block; @@ -141,56 +158,11 @@ $path-position: $marker-size-half - ($path-height / 2); text-overflow: ellipsis; } -.progress-title { - margin-top: 0; -} - -@mixin progress-state($marker-color-bg, $marker-color-border: null, $marker-color-text: null, $path-color: null, $text-color: null) { - .progress-marker { - color: $marker-color-text; - background-color: $marker-color-bg; - border-color: $marker-color-border; - } - - &::after { - background-color: $path-color; - } - - .progress-text, - .progress-text a { - color: $text-color; - } -} - -// States -.progress-step { - text-align: center; - - // Active state - &.active { - @include progress-state($progress-color); - } - - &.completed { - @include progress-state($progress-color-dark, $path-color: $progress-color-grey); - } - - &:hover { - @include progress-state($progress-color-light); - } - - &:last-child { - flex-grow: 1; - } - - &::after { - right: -50%; - } -} - .progress-marker { - margin-left: auto; - margin-right: auto; + color: $text-color-weak; + text-decoration:none; + font-size: 35px; + vertical-align: sub; } From 1dc581bdfa0cdfce671349d101592632a0b57a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 18 Nov 2016 17:42:14 +0100 Subject: [PATCH 08/34] ux(gettingstarted): progress on getting started panel, #6466 --- .../app/features/dashboard/row/row_model.ts | 1 + .../plugins/panel/gettingstarted/module.html | 27 +---- .../plugins/panel/gettingstarted/module.ts | 102 ++++++++++++++---- public/app/plugins/panel/pluginlist/module.ts | 3 +- 4 files changed, 87 insertions(+), 46 deletions(-) diff --git a/public/app/features/dashboard/row/row_model.ts b/public/app/features/dashboard/row/row_model.ts index 6fe3b7d4c00..d99e75b3621 100644 --- a/public/app/features/dashboard/row/row_model.ts +++ b/public/app/features/dashboard/row/row_model.ts @@ -19,6 +19,7 @@ export class DashboardRow { showTitle: false, titleSize: 'h6', height: 250, + isNew: false, repeat: null, repeatRowId: null, repeatIteration: null, diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html index 1be6f484b03..ae5f3b9ed54 100644 --- a/public/app/plugins/panel/gettingstarted/module.html +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -7,29 +7,10 @@
          diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index 2e99bf29676..28fa43ec9cb 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -7,10 +7,11 @@ import {contextSrv} from 'app/core/core'; class GettingStartedPanelCtrl extends PanelCtrl { static templateUrl = 'public/app/plugins/panel/gettingstarted/module.html'; checksDone: boolean; - step: number; + stepIndex: number; + steps: any; /** @ngInject **/ - constructor($scope, $injector, private backendSrv, private datasourceSrv) { + constructor($scope, $injector, private backendSrv, private datasourceSrv, private $q) { super($scope, $injector); /* tslint:disable */ @@ -20,32 +21,91 @@ class GettingStartedPanelCtrl extends PanelCtrl { } /* tslint:enable */ - var datasources = datasourceSrv.getMetricSources().filter(item => { - return item.meta.builtIn === false; + this.stepIndex = 0; + this.steps = []; + + this.steps.push({ + title: 'Install Grafana', + icon: 'icon-gf icon-gf-check', + check: () => $q.when(true), }); - this.step = 2; - if (datasources.length === 0) { - this.checksDone = true; - return; - } - - this.step = 3; - this.backendSrv.search({limit: 1}).then(result => { - if (result.length === 0) { - this.checksDone = true; - return; + this.steps.push({ + title: 'Create your first data source', + cta: 'Add data source', + icon: 'icon-gf icon-gf-datasources', + href: 'datasources/new?gettingstarted', + check: () => { + return $q.when( + datasourceSrv.getMetricSources().filter(item => { + return item.meta.builtIn === false; + }).length > 0 + ); } + }); - this.step = 4; - this.checksDone = true; + this.steps.push({ + title: 'Create your first dashboard', + cta: 'New dashboard', + icon: 'icon-gf icon-gf-dashboard', + href: 'dashboard/new?gettingstarted', + check: () => { + return this.backendSrv.search({limit: 1}).then(result => { + return result.length > 0; + }); + } + }); + + this.steps.push({ + title: 'Invite your team', + cta: 'Add Users', + icon: 'icon-gf icon-gf-users', + href: 'org/users?gettingstarted', + check: () => { + return this.backendSrv.get('api/org/users').then(res => { + return res.length > 1; + }); + } + }); + + + this.steps.push({ + title: 'Install apps & plugins', + cta: 'Explore plugin repository', + icon: 'icon-gf icon-gf-apps', + href: 'https://grafana.net/plugins?utm_source=grafana_getting_started', + check: () => { + return this.backendSrv.get('api/plugins', {embedded: 0, core: 0}).then(plugins => { + return plugins.length > 0; + }); + } }); } - getStateClass(step) { - if (step === this.step) { return 'active'; } - if (step < this.step) { return 'completed'; } - return ''; + $onInit() { + this.stepIndex = -1; + return this.nextStep().then(res => { + this.checksDone = true; + console.log(this.steps); + }); + } + + nextStep() { + if (this.stepIndex === this.steps.length - 1) { + return this.$q.when(); + } + + this.stepIndex += 1; + var currentStep = this.steps[this.stepIndex]; + return currentStep.check().then(passed => { + if (passed) { + currentStep.cssClass = 'completed'; + return this.nextStep(); + } + + currentStep.cssClass = 'active'; + return this.$q.when(); + }); } dismiss() { diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index 5ef1fb6fb0c..8a47061feb0 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -11,8 +11,7 @@ class PluginListCtrl extends PanelCtrl { viewModel: any; // Set and populate defaults - panelDefaults = { - }; + panelDefaults = {}; /** @ngInject */ constructor($scope, $injector, private backendSrv, private $location) { From 2f354ed926d0bea96f198d7bf42a1f528643021d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 24 Nov 2016 13:58:51 +0100 Subject: [PATCH 09/34] fix(getting started panel): some more work on this, #6466 --- .../plugins/panel/gettingstarted/module.ts | 28 +++++++++++++ .../components/_panel_gettingstarted.scss | 41 +++++++------------ 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index 28fa43ec9cb..4a490259c31 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -24,6 +24,34 @@ class GettingStartedPanelCtrl extends PanelCtrl { this.stepIndex = 0; this.steps = []; + if (!contextSrv.hasRole('Admin')) { + this.steps.push({ + cta: 'Basic Concepts Guide', + icon: 'fa fa-file-text-o', + href: 'http://docs.grafana.org/guides/basic_concepts/', + check: () => $q.when(false), + cssClass: 'active', + }); + + this.steps.push({ + cta: 'Getting Started Guide', + icon: 'fa fa-file-text-o', + href: 'http://docs.grafana.org/guides/getting_started/', + check: () => $q.when(false), + cssClass: 'active', + }); + + this.steps.push({ + cta: 'Building a dashboard', + icon: 'fa fa-film', + href: 'http://docs.grafana.org/tutorials/screencasts/', + check: () => $q.when(false), + cssClass: 'active', + }); + + return; + } + this.steps.push({ title: 'Install Grafana', icon: 'icon-gf icon-gf-check', diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 03f5019ff8e..b3967c0b421 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -14,32 +14,6 @@ $path-height: 4px !default; $path-position: $marker-size-half - ($path-height / 2); -.gettingstarted-blurb-copy { - margin-bottom: $spacer/2; - text-align: center; -} - -.gettingstarted-blurb { - @extend .gettingstarted-blurb-copy; - color: $text-color; - display: block; - - &:hover{ - text-decoration: underline; - } -} - -.gettingstarted-blurb-success { - @extend .gettingstarted-blurb-copy; - color: $text-color-weak; -} - -.gettingstarted-blurb-upcoming { - @extend .gettingstarted-blurb-copy; - color: $text-color-weak; -} - - .dashlist-cta-close-btn { color: $text-color-weak; float: right; @@ -105,6 +79,19 @@ $path-position: $marker-size-half - ($path-height / 2); .progress-title { font-weight: 400; } + .progress-text { + display: none; + } + + .progress-marker { + .icon-gf { + color: $brand-primary; + -webkit-text-fill-color: transparent; + background: $brand-gradient; + -webkit-background-clip: text; + text-decoration:none; + } + } } &.completed { @@ -145,6 +132,7 @@ $path-position: $marker-size-half - ($path-height / 2); background-color: $panel-bg; margin-left: auto; margin-right: auto; + margin-bottom: $spacer; color: $text-color-weak; font-size: 35px; vertical-align: sub; @@ -153,7 +141,6 @@ $path-position: $marker-size-half - ($path-height / 2); // Progress text .progress-text { display: block; - padding: $spacer; overflow: hidden; text-overflow: ellipsis; } From e6c8e75d621b0fae58afe801053874ac8692066d Mon Sep 17 00:00:00 2001 From: Axel Pirek Date: Fri, 14 Oct 2016 13:14:43 +0200 Subject: [PATCH 10/34] Make tooltip shareable like crosshair --- public/app/features/dashboard/model.ts | 2 ++ .../features/dashboard/partials/settings.html | 6 ++++ public/app/plugins/panel/graph/graph.ts | 20 ++++++++--- .../app/plugins/panel/graph/graph_tooltip.js | 36 ++++++++++++++++--- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index fa15acf823f..a324995df7a 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -19,6 +19,7 @@ export class DashboardModel { timezone: any; editable: any; sharedCrosshair: any; + sharedTooltip: any; rows: DashboardRow[]; time: any; timepicker: any; @@ -52,6 +53,7 @@ export class DashboardModel { this.timezone = data.timezone || ''; this.editable = data.editable !== false; this.sharedCrosshair = data.sharedCrosshair || false; + this.sharedTooltip = data.sharedTooltip || false; this.hideControls = data.hideControls || false; this.time = data.time || { from: 'now-6h', to: 'now' }; this.timepicker = data.timepicker || {}; diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 79cc7d12157..9a136b683e2 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -67,6 +67,12 @@ checked="dashboard.sharedCrosshair" label-class="width-11"> + +
      diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 73fe37d5cad..961d46d0085 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -34,6 +34,9 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { var rootScope = scope.$root; var panelWidth = 0; var thresholdManager = new ThresholdManager(ctrl); + var tooltip = new GraphTooltip(elem, dashboard, scope, function() { + return sortedSeries; + }); var plot; ctrl.events.on('panel-teardown', () => { @@ -64,6 +67,19 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { } }, scope); + rootScope.onAppEvent('setTooltip', function(event, info) { + // do not need to to this if event is from this panel + // or another panel is in fullscreen mode + if (info.scope === scope || ctrl.otherPanelInFullscreenMode()) { + return; + } + tooltip.setTooltip(info.pos); + }, scope); + + rootScope.onAppEvent('clearTooltip', function() { + tooltip.clearTooltip(); + }, scope); + // Receive render events ctrl.events.on('render', function(renderData) { data = renderData || data; @@ -555,10 +571,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { return "%H:%M"; } - var tooltip = new GraphTooltip(elem, dashboard, scope, function() { - return sortedSeries; - }); - elem.bind("plotselected", function (event, ranges) { scope.$apply(function() { timeSrv.setTime({ diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 5ef6c65a400..8259e9adc5c 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -10,7 +10,7 @@ function ($) { var ctrl = scope.ctrl; var panel = ctrl.panel; - var $tooltip = $('
      '); + var $tooltip = $('
      '); this.destroy = function() { $tooltip.remove(); @@ -141,12 +141,32 @@ function ($) { } } + if (dashboard.sharedTooltip) { + ctrl.publishAppEvent('clearTooltip'); + } + if (dashboard.sharedCrosshair) { ctrl.publishAppEvent('clearCrosshair'); } }); elem.bind("plothover", function (event, pos, item) { + if (dashboard.sharedCrosshair) { + ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope}); + } + + if (dashboard.sharedTooltip) { + pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height(); + ctrl.publishAppEvent('setTooltip', {pos: pos, scope: scope}); + } + self.setTooltip(pos, item); + }); + + this.clearTooltip = function() { + $tooltip.detach(); + }; + + this.setTooltip = function(pos, item) { var plot = elem.data().plot; var plotData = plot.getData(); var xAxes = plot.getXAxes(); @@ -154,8 +174,16 @@ function ($) { var seriesList = getSeriesFn(); var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; - if (dashboard.sharedCrosshair) { - ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope}); + // if panelRelY is defined another panel wants us to show a tooltip + // get pageX from position on x axis and pageY from relative position in original panel + if (pos.panelRelY !== undefined) { + var pointOffset = plot.pointOffset({x: pos.x}); + if (Number.isNaN(pointOffset.left) || pointOffset.left < 0) { + $tooltip.detach(); + return; + } + pos.pageX = elem.offset().left + pointOffset.left; + pos.pageY = elem.offset().top + elem.height() * pos.panelRelY; } if (seriesList.length === 0) { @@ -238,7 +266,7 @@ function ($) { else { $tooltip.detach(); } - }); + }; } return GraphTooltip; From fa393c282a6277c538695ddb6fb898320d07dc7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 6 Dec 2016 11:55:20 +0100 Subject: [PATCH 11/34] feat(graph): refactoring shared tooltip PR #6274 --- public/app/core/services/keybindingSrv.ts | 1 + .../dashboard/import/dash_import.html | 8 +-- public/app/features/dashboard/model.ts | 2 - .../features/dashboard/partials/settings.html | 12 +--- public/app/plugins/panel/graph/graph.ts | 69 ++++++++----------- .../app/plugins/panel/graph/graph_tooltip.js | 45 ++++++------ public/app/plugins/panel/graph/module.ts | 1 - .../plugins/panel/graph/specs/graph_specs.ts | 2 +- .../app/plugins/panel/graph/tab_display.html | 2 +- 9 files changed, 59 insertions(+), 83 deletions(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index c1109fa2b2e..2b509db822a 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -89,6 +89,7 @@ export class KeybindingSrv { this.bind('mod+o', () => { dashboard.sharedCrosshair = !dashboard.sharedCrosshair; + appEvents.emit('graph-hover-clear'); scope.broadcastRefresh(); }); diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html index 3e914245627..5e19a02c28c 100644 --- a/public/app/features/dashboard/import/dash_import.html +++ b/public/app/features/dashboard/import/dash_import.html @@ -123,11 +123,11 @@
      - - Cancel Back diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index a324995df7a..fa15acf823f 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -19,7 +19,6 @@ export class DashboardModel { timezone: any; editable: any; sharedCrosshair: any; - sharedTooltip: any; rows: DashboardRow[]; time: any; timepicker: any; @@ -53,7 +52,6 @@ export class DashboardModel { this.timezone = data.timezone || ''; this.editable = data.editable !== false; this.sharedCrosshair = data.sharedCrosshair || false; - this.sharedTooltip = data.sharedTooltip || false; this.hideControls = data.hideControls || false; this.time = data.time || { from: 'now-6h', to: 'now' }; this.timepicker = data.timepicker || {}; diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 9a136b683e2..efa174acdb9 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -57,20 +57,14 @@ - -
      diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 61e95f3ec61..41154fa00d3 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -9,18 +9,17 @@ import 'jquery.flot.fillbelow'; import 'jquery.flot.crosshair'; import './jquery.flot.events'; -import angular from 'angular'; import $ from 'jquery'; -import moment from 'moment'; import _ from 'lodash'; +import moment from 'moment'; import kbn from 'app/core/utils/kbn'; +import {appEvents, coreModule} from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import {ThresholdManager} from './threshold_manager'; -var module = angular.module('grafana.directives'); var labelWidthCache = {}; -module.directive('grafanaGraph', function($rootScope, timeSrv) { +coreModule.directive('grafanaGraph', function($rootScope, timeSrv) { return { restrict: 'A', template: '', @@ -28,7 +27,9 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { var ctrl = scope.ctrl; var dashboard = ctrl.dashboard; var panel = ctrl.panel; - var data, annotations; + var data; + var annotations; + var plot; var sortedSeries; var legendSideLastValue = null; var rootScope = scope.$root; @@ -37,8 +38,8 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { var tooltip = new GraphTooltip(elem, dashboard, scope, function() { return sortedSeries; }); - var plot; + // panel events ctrl.events.on('panel-teardown', () => { thresholdManager = null; @@ -48,39 +49,6 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { } }); - rootScope.onAppEvent('setCrosshair', function(event, info) { - // do not need to to this if event is from this panel - if (info.scope === scope) { - return; - } - - if (dashboard.sharedCrosshair) { - if (plot) { - plot.setCrosshair({ x: info.pos.x, y: info.pos.y }); - } - } - }, scope); - - rootScope.onAppEvent('clearCrosshair', function() { - if (plot) { - plot.clearCrosshair(); - } - }, scope); - - rootScope.onAppEvent('setTooltip', function(event, info) { - // do not need to to this if event is from this panel - // or another panel is in fullscreen mode - if (info.scope === scope || ctrl.otherPanelInFullscreenMode()) { - return; - } - tooltip.setTooltip(info.pos); - }, scope); - - rootScope.onAppEvent('clearTooltip', function() { - tooltip.clearTooltip(); - }, scope); - - // Receive render events ctrl.events.on('render', function(renderData) { data = renderData || data; if (!data) { @@ -90,6 +58,27 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { render_panel(); }); + // global events + appEvents.on('graph-hover', function(evt) { + // ignore other graph hover events if shared tooltip is disabled + if (!dashboard.sharedCrosshair) { + return; + } + + // ignore if we are the emitter + if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) { + return; + } + + tooltip.show(evt.pos); + }, scope); + + appEvents.on('graph-hover-clear', function(event, info) { + if (plot) { + tooltip.clear(plot); + } + }, scope); + function getLegendHeight(panelHeight) { if (!panel.legend.show || panel.legend.rightSide) { return 0; @@ -288,7 +277,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { color: '#666' }, crosshair: { - mode: panel.tooltip.shared || dashboard.sharedCrosshair ? "x" : null + mode: 'x' } }; diff --git a/public/app/plugins/panel/graph/graph_tooltip.js b/public/app/plugins/panel/graph/graph_tooltip.js index 8259e9adc5c..4705d5d5a13 100644 --- a/public/app/plugins/panel/graph/graph_tooltip.js +++ b/public/app/plugins/panel/graph/graph_tooltip.js @@ -1,10 +1,12 @@ define([ 'jquery', - 'lodash' + 'app/core/core', ], -function ($) { +function ($, core) { 'use strict'; + var appEvents = core.appEvents; + function GraphTooltip(elem, dashboard, scope, getSeriesFn) { var self = this; var ctrl = scope.ctrl; @@ -41,7 +43,7 @@ function ($) { return j - 1; }; - this.showTooltip = function(absoluteTime, innerHtml, pos, xMode) { + this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) { if (xMode === 'time') { innerHtml = '
      '+ absoluteTime + '
      ' + innerHtml; } @@ -140,43 +142,34 @@ function ($) { plot.unhighlight(); } } - - if (dashboard.sharedTooltip) { - ctrl.publishAppEvent('clearTooltip'); - } - - if (dashboard.sharedCrosshair) { - ctrl.publishAppEvent('clearCrosshair'); - } + appEvents.emit('graph-hover-clear'); }); elem.bind("plothover", function (event, pos, item) { - if (dashboard.sharedCrosshair) { - ctrl.publishAppEvent('setCrosshair', {pos: pos, scope: scope}); - } + self.show(pos, item); - if (dashboard.sharedTooltip) { - pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height(); - ctrl.publishAppEvent('setTooltip', {pos: pos, scope: scope}); - } - self.setTooltip(pos, item); + // broadcast to other graph panels that we are hovering! + pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height(); + appEvents.emit('graph-hover', {pos: pos, panel: panel}); }); - this.clearTooltip = function() { + this.clear = function(plot) { $tooltip.detach(); + plot.clearCrosshair(); }; - this.setTooltip = function(pos, item) { + this.show = function(pos, item) { var plot = elem.data().plot; var plotData = plot.getData(); var xAxes = plot.getXAxes(); var xMode = xAxes[0].options.mode; var seriesList = getSeriesFn(); + var allSeriesMode = panel.tooltip.shared; var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat; // if panelRelY is defined another panel wants us to show a tooltip // get pageX from position on x axis and pageY from relative position in original panel - if (pos.panelRelY !== undefined) { + if (pos.panelRelY) { var pointOffset = plot.pointOffset({x: pos.x}); if (Number.isNaN(pointOffset.left) || pointOffset.left < 0) { $tooltip.detach(); @@ -184,6 +177,8 @@ function ($) { } pos.pageX = elem.offset().left + pointOffset.left; pos.pageY = elem.offset().top + elem.height() * pos.panelRelY; + plot.setCrosshair(pos); + allSeriesMode = true; } if (seriesList.length === 0) { @@ -196,7 +191,7 @@ function ($) { tooltipFormat = 'YYYY-MM-DD HH:mm:ss'; } - if (panel.tooltip.shared) { + if (allSeriesMode) { plot.unhighlight(); var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos); @@ -239,7 +234,7 @@ function ($) { plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); } - self.showTooltip(absoluteTime, seriesHtml, pos, xMode); + self.renderAndShow(absoluteTime, seriesHtml, pos, xMode); } // single series tooltip else if (item) { @@ -260,7 +255,7 @@ function ($) { group += '
      ' + value + '
      '; - self.showTooltip(absoluteTime, group, pos, xMode); + self.renderAndShow(absoluteTime, group, pos, xMode); } // no hit else { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index e546a33c30b..12639a2fd6d 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -96,7 +96,6 @@ class GraphCtrl extends MetricsPanelCtrl { value_type: 'individual', shared: true, sort: 0, - msResolution: false, }, // time overrides timeFrom: null, diff --git a/public/app/plugins/panel/graph/specs/graph_specs.ts b/public/app/plugins/panel/graph/specs/graph_specs.ts index 046409b77b2..5578b3a361e 100644 --- a/public/app/plugins/panel/graph/specs/graph_specs.ts +++ b/public/app/plugins/panel/graph/specs/graph_specs.ts @@ -12,7 +12,7 @@ import {Emitter} from 'app/core/core'; describe('grafanaGraph', function() { - beforeEach(angularMocks.module('grafana.directives')); + beforeEach(angularMocks.module('grafana.core')); function graphScenario(desc, func, elementWidth = 500) { describe(desc, function() { diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index 6845181b8b2..8ace16c6078 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -48,7 +48,7 @@
-
Hover info
+
Hover tooltip
From e1b25b39994d4c6e0db1eca38282a198e545ffc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 6 Dec 2016 11:58:39 +0100 Subject: [PATCH 12/34] changelog(): updated changelog with info on shared tooltip feature merged via PR #6274, closes #1578 --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 151e0cbcbbd..1a5956986c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,16 @@ # 4.1-beta (unreleased) -### Bugfixes -* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679) -* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443) - ### Enhancements * **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655) * **Victorops**: Add VictorOps Notification Integration [#6411](https://github.com/grafana/grafana/issues/6411) * **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740) * **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697) * **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677) +* **Graph**: Shared crosshair option renamed to shared tooltip, shows tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274) + +### Bugfixes +* **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679) +* **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443) # 4.0.2 (unreleased) From 4ef940482c621a14c1b8649de771129deaae15e0 Mon Sep 17 00:00:00 2001 From: Kyle McCullough Date: Tue, 6 Dec 2016 14:48:13 -0600 Subject: [PATCH 13/34] alerting: add support for OpsGenie --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/opsgenie.go | 118 ++++++++++++++++++ .../alerting/notifiers/opsgenie_test.go | 52 ++++++++ .../app/features/alerting/alert_tab_ctrl.ts | 1 + .../alerting/partials/notification_edit.html | 19 ++- 5 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 pkg/services/alerting/notifiers/opsgenie.go create mode 100644 pkg/services/alerting/notifiers/opsgenie_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 8eb4eb4baea..3c6c362f949 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -46,6 +46,7 @@ var ( M_Alerting_Notification_Sent_Webhook Counter M_Alerting_Notification_Sent_PagerDuty Counter M_Alerting_Notification_Sent_Victorops Counter + M_Alerting_Notification_Sent_OpsGenie Counter // Timers M_DataSource_ProxyReq_Timer Timer @@ -110,6 +111,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook") M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty") M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") + M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") // Timers M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all") diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go new file mode 100644 index 00000000000..e16cda4bc5b --- /dev/null +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -0,0 +1,118 @@ +package notifiers + +import ( + "fmt" + "strconv" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +func init() { + alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier) +} + +var ( + opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert" + opsgenieCloseAlertURL string = "https://api.opsgenie.com/v1/json/alert/close" +) + +func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + autoClose := model.Settings.Get("autoClose").MustBool(true) + apiKey := model.Settings.Get("apiKey").MustString() + if apiKey == "" { + return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"} + } + + return &OpsGenieNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + ApiKey: apiKey, + AutoClose: autoClose, + log: log.New("alerting.notifier.opsgenie"), + }, nil +} + +type OpsGenieNotifier struct { + NotifierBase + ApiKey string + AutoClose bool + log log.Logger +} + +func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error { + metrics.M_Alerting_Notification_Sent_OpsGenie.Inc(1) + + var err error + switch evalContext.Rule.State { + case m.AlertStateOK: + if this.AutoClose { + err = this.closeAlert(evalContext) + } + case m.AlertStateAlerting: + err = this.createAlert(evalContext) + } + return err +} + +func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error { + this.log.Info("Creating OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + bodyJSON := simplejson.New() + bodyJSON.Set("apiKey", this.ApiKey) + bodyJSON.Set("message", evalContext.Rule.Name) + bodyJSON.Set("source", "Grafana") + bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)) + + details := simplejson.New() + details.Set("url", ruleUrl) + if evalContext.ImagePublicUrl != "" { + details.Set("image", evalContext.ImagePublicUrl) + } + + bodyJSON.Set("details", details) + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: opsgenieCreateAlertURL, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) + } + + return nil +} + +func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error { + this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + bodyJSON := simplejson.New() + bodyJSON.Set("apiKey", this.ApiKey) + bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: opsgenieCloseAlertURL, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go new file mode 100644 index 00000000000..9dcb9f3c600 --- /dev/null +++ b/pkg/services/alerting/notifiers/opsgenie_test.go @@ -0,0 +1,52 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestOpsGenieNotifier(t *testing.T) { + Convey("OpsGenie notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "opsgenie_testing", + Type: "opsgenie", + Settings: settingsJSON, + } + + _, err := NewOpsGenieNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should trigger incident", func() { + json := ` + { + "apiKey": "abcdefgh0123456789" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "opsgenie_testing", + Type: "opsgenie", + Settings: settingsJSON, + } + + not, err := NewOpsGenieNotifier(model) + opsgenieNotifier := not.(*OpsGenieNotifier) + + So(err, ShouldBeNil) + So(opsgenieNotifier.Name, ShouldEqual, "opsgenie_testing") + So(opsgenieNotifier.Type, ShouldEqual, "opsgenie") + So(opsgenieNotifier.ApiKey, ShouldEqual, "abcdefgh0123456789") + }) + }) + }) +} diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 426ff62382f..c6683556745 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -94,6 +94,7 @@ export class AlertTabCtrl { case "victorops": return "fa fa-pagelines"; case "webhook": return "fa fa-cubes"; case "pagerduty": return "fa fa-bullhorn"; + case "opsgenie": return "fa fa-bell"; } } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index ac4afbf56fc..b5076efd792 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -19,7 +19,7 @@
Type
-
@@ -122,6 +122,23 @@
+
+

OpsGenie settings

+
+ API Key + +
+
+ + +
+
+
From c5b29228ee1cfb2c7c56562b7af6ff10a064e4a1 Mon Sep 17 00:00:00 2001 From: Mike Rostermund Date: Tue, 6 Dec 2016 22:26:43 +0100 Subject: [PATCH 14/34] Add missing char/typo --- docs/sources/alerting/notifications.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index dfe84f142ec..1411bcf756f 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -91,7 +91,7 @@ Auto resolve incidents | Resolve incidents in pagerduty once the alert goes back # Enable images in notifications {#external-image-store} -Grafan can render the panel associated with the alert rule and include that in the notification. Some types +Grafana can render the panel associated with the alert rule and include that in the notification. Some types of notifications require that this image be publicly accessable (Slack for example). In order to support images in notifications like Slack Grafana can upload the image to an image store. It currently supports Amazon S3 for this and Webdav. So to set that up you need to configure the From d28726be06813b45f01888b3eb8b2f9b129c78aa Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Wed, 7 Dec 2016 04:45:23 -0500 Subject: [PATCH 15/34] Moved ADD ROW to left to consolidate row actions along the same vertical area. (#6852) --- public/app/partials/dashboard.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index c4df2b6adf9..c7cc6fb440d 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -15,8 +15,8 @@
-
- +
+ ADD ROW
From 24172fca01dd596c9c154355da90d3ae4c3e16e4 Mon Sep 17 00:00:00 2001 From: vaibhavinbayarea Date: Wed, 7 Dec 2016 01:47:46 -0800 Subject: [PATCH 16/34] Added feature request "predict value" in moving averages pipeline agg (#5689) issue-id: #5688 --- .../datasource/elasticsearch/partials/metric_agg.html | 5 +++++ public/app/plugins/datasource/elasticsearch/query_def.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html index faa12b5693d..74cf23516cd 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/metric_agg.html @@ -53,6 +53,11 @@
+
+ + +
+
diff --git a/public/app/plugins/datasource/elasticsearch/query_def.js b/public/app/plugins/datasource/elasticsearch/query_def.js index 187e5c2f4fa..9fbfb3495d6 100644 --- a/public/app/plugins/datasource/elasticsearch/query_def.js +++ b/public/app/plugins/datasource/elasticsearch/query_def.js @@ -72,7 +72,8 @@ function (_) { pipelineOptions: { 'moving_avg' : [ {text: 'window', default: 5}, - {text: 'model', default: 'simple'} + {text: 'model', default: 'simple'}, + {text: 'predict', default: 0} ], 'derivative': [ {text: 'unit', default: undefined}, From 2c7adccf12c8620d37dd4e9cef7d31b2d0de175d Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Wed, 7 Dec 2016 11:10:42 +0100 Subject: [PATCH 17/34] Use cache for http.client in tsdb package. (#6833) * datasource: move caching closer to datasource struct * tsdb: use cached version of datasource http transport closes #6825 --- pkg/api/dataproxy.go | 75 +---------- pkg/api/dataproxy_test.go | 153 +-------------------- pkg/api/datasources.go | 3 +- pkg/api/metrics.go | 7 +- pkg/models/datasource_cache.go | 95 +++++++++++++ pkg/models/datasource_cache_test.go | 157 ++++++++++++++++++++++ pkg/services/alerting/conditions/query.go | 18 +-- pkg/tsdb/batch.go | 11 +- pkg/tsdb/executor.go | 22 ++- pkg/tsdb/fake_test.go | 10 +- pkg/tsdb/graphite/graphite.go | 24 ++-- pkg/tsdb/http.go | 29 ---- pkg/tsdb/influxdb/influxdb.go | 26 ++-- pkg/tsdb/influxdb/model_parser.go | 4 +- pkg/tsdb/influxdb/model_parser_test.go | 4 +- pkg/tsdb/models.go | 17 +-- pkg/tsdb/opentsdb/opentsdb.go | 24 ++-- pkg/tsdb/prometheus/prometheus.go | 21 ++- pkg/tsdb/testdata/testdata.go | 11 +- pkg/tsdb/tsdb_test.go | 37 ++--- 20 files changed, 385 insertions(+), 363 deletions(-) create mode 100644 pkg/models/datasource_cache.go create mode 100644 pkg/models/datasource_cache_test.go delete mode 100644 pkg/tsdb/http.go diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 39de6911008..db4c5166feb 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,13 +1,9 @@ package api import ( - "crypto/tls" - "crypto/x509" - "net" "net/http" "net/http/httputil" "net/url" - "sync" "time" "github.com/grafana/grafana/pkg/api/cloudwatch" @@ -19,75 +15,6 @@ import ( "github.com/grafana/grafana/pkg/util" ) -type proxyTransportCache struct { - cache map[int64]cachedTransport - sync.Mutex -} - -type cachedTransport struct { - updated time.Time - - *http.Transport -} - -var ptc = proxyTransportCache{ - cache: make(map[int64]cachedTransport), -} - -func DataProxyTransport(ds *m.DataSource) (*http.Transport, error) { - ptc.Lock() - defer ptc.Unlock() - - if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) { - return t.Transport, nil - } - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - Proxy: http.ProxyFromEnvironment, - Dial: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).Dial, - TLSHandshakeTimeout: 10 * time.Second, - } - - var tlsAuth, tlsAuthWithCACert bool - if ds.JsonData != nil { - tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false) - tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) - } - - if tlsAuth { - transport.TLSClientConfig.InsecureSkipVerify = false - - decrypted := ds.SecureJsonData.Decrypt() - - if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { - caPool := x509.NewCertPool() - ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"])) - if ok { - transport.TLSClientConfig.RootCAs = caPool - } - } - - cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) - if err != nil { - return nil, err - } - transport.TLSClientConfig.Certificates = []tls.Certificate{cert} - } - - ptc.cache[ds.Id] = cachedTransport{ - Transport: transport, - updated: ds.Updated, - } - - return transport, nil -} - func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *httputil.ReverseProxy { director := func(req *http.Request) { req.URL.Scheme = targetUrl.Scheme @@ -189,7 +116,7 @@ func ProxyDataSourceRequest(c *middleware.Context) { } proxy := NewReverseProxy(ds, proxyPath, targetUrl) - proxy.Transport, err = DataProxyTransport(ds) + proxy.Transport, err = ds.GetHttpTransport() if err != nil { c.JsonApiErr(400, "Unable to load TLS certificate", err) return diff --git a/pkg/api/dataproxy_test.go b/pkg/api/dataproxy_test.go index 50abf317e8a..f3ed6994cff 100644 --- a/pkg/api/dataproxy_test.go +++ b/pkg/api/dataproxy_test.go @@ -4,24 +4,18 @@ import ( "net/http" "net/url" "testing" - "time" . "github.com/smartystreets/goconvey/convey" - "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) func TestDataSourceProxy(t *testing.T) { - Convey("When getting graphite datasource proxy", t, func() { - clearCache() ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} targetUrl, err := url.Parse(ds.Url) proxy := NewReverseProxy(&ds, "/render", targetUrl) - proxy.Transport, err = DataProxyTransport(&ds) + proxy.Transport, err = ds.GetHttpTransport() So(err, ShouldBeNil) transport, ok := proxy.Transport.(*http.Transport) @@ -40,7 +34,6 @@ func TestDataSourceProxy(t *testing.T) { }) Convey("When getting influxdb datasource proxy", t, func() { - clearCache() ds := m.DataSource{ Type: m.DS_INFLUXDB_08, Url: "http://influxdb:8083", @@ -67,148 +60,4 @@ func TestDataSourceProxy(t *testing.T) { So(queryVals["p"][0], ShouldEqual, "password") }) }) - - Convey("When caching a datasource proxy", t, func() { - clearCache() - ds := m.DataSource{ - Id: 1, - Url: "http://k8s:8001", - Type: "Kubernetes", - } - - t1, err := DataProxyTransport(&ds) - So(err, ShouldBeNil) - - t2, err := DataProxyTransport(&ds) - So(err, ShouldBeNil) - - Convey("Should be using the cached proxy", func() { - So(t2, ShouldEqual, t1) - }) - }) - - Convey("When getting kubernetes datasource proxy", t, func() { - clearCache() - setting.SecretKey = "password" - - json := simplejson.New() - json.Set("tlsAuth", true) - json.Set("tlsAuthWithCACert", true) - - t := time.Now() - ds := m.DataSource{ - Url: "http://k8s:8001", - Type: "Kubernetes", - Updated: t.Add(-2 * time.Minute), - } - - transport, err := DataProxyTransport(&ds) - So(err, ShouldBeNil) - - Convey("Should have no cert", func() { - So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) - }) - - ds.JsonData = json - ds.SecureJsonData = map[string][]byte{ - "tlsCACert": util.Encrypt([]byte(caCert), "password"), - "tlsClientCert": util.Encrypt([]byte(clientCert), "password"), - "tlsClientKey": util.Encrypt([]byte(clientKey), "password"), - } - ds.Updated = t.Add(-1 * time.Minute) - - transport, err = DataProxyTransport(&ds) - So(err, ShouldBeNil) - - Convey("Should add cert", func() { - So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) - So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1) - }) - - ds.JsonData = nil - ds.SecureJsonData = map[string][]byte{} - ds.Updated = t - - transport, err = DataProxyTransport(&ds) - So(err, ShouldBeNil) - - Convey("Should remove cert", func() { - So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) - So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0) - }) - }) - } - -func clearCache() { - ptc.Lock() - defer ptc.Unlock() - - ptc.cache = make(map[int64]cachedTransport) -} - -const caCert string = `-----BEGIN CERTIFICATE----- -MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV -BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda -MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg -QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4 -4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d -1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN -iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ -0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx -2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5 -VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9 -I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB -CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7 -adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k -MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh -FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n -3lb92xM= ------END CERTIFICATE-----` - -const clientCert string = `-----BEGIN CERTIFICATE----- -MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj -YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w -GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD -ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2 -FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b -Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo -GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8 -SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4 -YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF -AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP -ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw -AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q -4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe -58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5 -llG/Sw5+FquFuChaA6l5KWy7F3bQyA== ------END CERTIFICATE-----` - -const clientKey string = `-----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV -u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn -Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega -0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI -LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi -dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs -Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk -CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x -64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM -8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh -WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf -vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz -k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs -DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35 -aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ -Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo -jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01 -hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0 -M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8 -v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX -xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL -Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0 -Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD -FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD -+VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg= ------END RSA PRIVATE KEY-----` diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index cc305221817..a7254eeec6c 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -5,10 +5,9 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/plugins" - //"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 5516bdd52a5..dad3b159ad6 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb/testdata" "github.com/grafana/grafana/pkg/util" @@ -25,9 +26,9 @@ func QueryMetrics(c *middleware.Context, reqDto dtos.MetricRequest) Response { MaxDataPoints: query.Get("maxDataPoints").MustInt64(100), IntervalMs: query.Get("intervalMs").MustInt64(1000), Model: query, - DataSource: &tsdb.DataSourceInfo{ - Name: "Grafana TestDataDB", - PluginId: "grafana-testdata-datasource", + DataSource: &models.DataSource{ + Name: "Grafana TestDataDB", + Type: "grafana-testdata-datasource", }, }) } diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go new file mode 100644 index 00000000000..e32c0ac9e7c --- /dev/null +++ b/pkg/models/datasource_cache.go @@ -0,0 +1,95 @@ +package models + +import ( + "crypto/tls" + "crypto/x509" + "net" + "net/http" + "sync" + "time" +) + +type proxyTransportCache struct { + cache map[int64]cachedTransport + sync.Mutex +} + +type cachedTransport struct { + updated time.Time + + *http.Transport +} + +var ptc = proxyTransportCache{ + cache: make(map[int64]cachedTransport), +} + +func (ds *DataSource) GetHttpClient() (*http.Client, error) { + transport, err := ds.GetHttpTransport() + + if err != nil { + return nil, err + } + + return &http.Client{ + Timeout: time.Duration(30 * time.Second), + Transport: transport, + }, nil +} + +func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { + ptc.Lock() + defer ptc.Unlock() + + if t, present := ptc.cache[ds.Id]; present && ds.Updated.Equal(t.updated) { + return t.Transport, nil + } + + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + } + + var tlsAuth, tlsAuthWithCACert bool + if ds.JsonData != nil { + tlsAuth = ds.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) + } + + if tlsAuth { + transport.TLSClientConfig.InsecureSkipVerify = false + + decrypted := ds.SecureJsonData.Decrypt() + + if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { + caPool := x509.NewCertPool() + ok := caPool.AppendCertsFromPEM([]byte(decrypted["tlsCACert"])) + if ok { + transport.TLSClientConfig.RootCAs = caPool + } + } + + cert, err := tls.X509KeyPair([]byte(decrypted["tlsClientCert"]), []byte(decrypted["tlsClientKey"])) + if err != nil { + return nil, err + } + transport.TLSClientConfig.Certificates = []tls.Certificate{cert} + } + + ptc.cache[ds.Id] = cachedTransport{ + Transport: transport, + updated: ds.Updated, + } + + return transport, nil +} diff --git a/pkg/models/datasource_cache_test.go b/pkg/models/datasource_cache_test.go new file mode 100644 index 00000000000..25fee55529d --- /dev/null +++ b/pkg/models/datasource_cache_test.go @@ -0,0 +1,157 @@ +package models + +import ( + "testing" + "time" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +func TestDataSourceCache(t *testing.T) { + Convey("When caching a datasource proxy", t, func() { + clearCache() + ds := DataSource{ + Id: 1, + Url: "http://k8s:8001", + Type: "Kubernetes", + } + + t1, err := ds.GetHttpTransport() + So(err, ShouldBeNil) + + t2, err := ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should be using the cached proxy", func() { + So(t2, ShouldEqual, t1) + }) + }) + + Convey("When getting kubernetes datasource proxy", t, func() { + clearCache() + setting.SecretKey = "password" + + json := simplejson.New() + json.Set("tlsAuth", true) + json.Set("tlsAuthWithCACert", true) + + t := time.Now() + ds := DataSource{ + Url: "http://k8s:8001", + Type: "Kubernetes", + Updated: t.Add(-2 * time.Minute), + } + + transport, err := ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should have no cert", func() { + So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) + }) + + ds.JsonData = json + ds.SecureJsonData = map[string][]byte{ + "tlsCACert": util.Encrypt([]byte(caCert), "password"), + "tlsClientCert": util.Encrypt([]byte(clientCert), "password"), + "tlsClientKey": util.Encrypt([]byte(clientKey), "password"), + } + ds.Updated = t.Add(-1 * time.Minute) + + transport, err = ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should add cert", func() { + So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, false) + So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 1) + }) + + ds.JsonData = nil + ds.SecureJsonData = map[string][]byte{} + ds.Updated = t + + transport, err = ds.GetHttpTransport() + So(err, ShouldBeNil) + + Convey("Should remove cert", func() { + So(transport.TLSClientConfig.InsecureSkipVerify, ShouldEqual, true) + So(len(transport.TLSClientConfig.Certificates), ShouldEqual, 0) + }) + }) +} + +func clearCache() { + ptc.Lock() + defer ptc.Unlock() + + ptc.cache = make(map[int64]cachedTransport) +} + +const caCert string = `-----BEGIN CERTIFICATE----- +MIIDATCCAemgAwIBAgIJAMQ5hC3CPDTeMA0GCSqGSIb3DQEBCwUAMBcxFTATBgNV +BAMMDGNhLWs4cy1zdGhsbTAeFw0xNjEwMjcwODQyMjdaFw00NDAzMTQwODQyMjda +MBcxFTATBgNVBAMMDGNhLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAMLe2AmJ6IleeUt69vgNchOjjmxIIxz5sp1vFu94m1vUip7CqnOg +QkpUsHeBPrGYv8UGloARCL1xEWS+9FVZeXWQoDmbC0SxXhFwRIESNCET7Q8KMi/4 +4YPvnMLGZi3Fjwxa8BdUBCN1cx4WEooMVTWXm7RFMtZgDfuOAn3TNXla732sfT/d +1HNFrh48b0wA+HhmA3nXoBnBEblA665hCeo7lIAdRr0zJxJpnFnWXkyTClsAUTMN +iL905LdBiiIRenojipfKXvMz88XSaWTI7JjZYU3BvhyXndkT6f12cef3I96NY3WJ +0uIK4k04WrbzdYXMU3rN6NqlvbHqnI+E7aMCAwEAAaNQME4wHQYDVR0OBBYEFHHx +2+vSPw9bECHj3O51KNo5VdWOMB8GA1UdIwQYMBaAFHHx2+vSPw9bECHj3O51KNo5 +VdWOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH2eV5NcV3LBJHs9 +I+adbiTPg2vyumrGWwy73T0X8Dtchgt8wU7Q9b9Ucg2fOTmSSyS0iMqEu1Yb2ORB +CknM9mixHC9PwEBbkGCom3VVkqdLwSP6gdILZgyLoH4i8sTUz+S1yGPepi+Vzhs7 +adOXtryjcGnwft6HdfKPNklMOHFnjw6uqpho54oj/z55jUpicY/8glDHdrr1bh3k +MHuiWLGewHXPvxfG6UoUx1te65IhifVcJGFZDQwfEmhBflfCmtAJlZEsgTLlBBCh +FHoXIyGOdq1chmRVocdGBCF8fUoGIbuF14r53rpvcbEKtKnnP8+96luKAZLq0a4n +3lb92xM= +-----END CERTIFICATE-----` + +const clientCert string = `-----BEGIN CERTIFICATE----- +MIICsjCCAZoCCQCcd8sOfstQLzANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj +YS1rOHMtc3RobG0wHhcNMTYxMTAyMDkyNTE1WhcNMTcxMTAyMDkyNTE1WjAfMR0w +GwYDVQQDDBRhZG0tZGFuaWVsLWs4cy1zdGhsbTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAOMliaWyNEUJKM37vWCl5bGub3lMicyRAqGQyY/qxD9yKKM2 +FbucVcmWmg5vvTqQVl5rlQ+c7GI8OD6ptmFl8a26coEki7bFr8bkpSyBSEc5p27b +Z0ORFSqBHWHQbr9PkxPLYW6T3gZYUtRYv3OQgGxLXlvUh85n/mQfuR3N1FgmShHo +GtAFi/ht6leXa0Ms+jNSDLCmXpJm1GIEqgyKX7K3+g3vzo9coYqXq4XTa8Efs2v8 +SCwqWfBC3rHfgs/5DLB8WT4Kul8QzxkytzcaBQfRfzhSV6bkgm7oTzt2/1eRRsf4 +YnXzLE9YkCC9sAn+Owzqf+TYC1KRluWDfqqBTJUCAwEAATANBgkqhkiG9w0BAQsF +AAOCAQEAdMsZg6edWGC+xngizn0uamrUg1ViaDqUsz0vpzY5NWLA4MsBc4EtxWRP +ueQvjUimZ3U3+AX0YWNLIrH1FCVos2jdij/xkTUmHcwzr8rQy+B17cFi+a8jtpgw +AU6WWoaAIEhhbWQfth/Diz3mivl1ARB+YqiWca2mjRPLTPcKJEURDVddQ423el0Q +4JNxS5icu7T2zYTYHAo/cT9zVdLZl0xuLxYm3asK1IONJ/evxyVZima3il6MPvhe +58Hwz+m+HdqHxi24b/1J/VKYbISG4huOQCdLzeNXgvwFlGPUmHSnnKo1/KbQDAR5 +llG/Sw5+FquFuChaA6l5KWy7F3bQyA== +-----END CERTIFICATE-----` + +const clientKey string = `-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA4yWJpbI0RQkozfu9YKXlsa5veUyJzJECoZDJj+rEP3IoozYV +u5xVyZaaDm+9OpBWXmuVD5zsYjw4Pqm2YWXxrbpygSSLtsWvxuSlLIFIRzmnbttn +Q5EVKoEdYdBuv0+TE8thbpPeBlhS1Fi/c5CAbEteW9SHzmf+ZB+5Hc3UWCZKEega +0AWL+G3qV5drQyz6M1IMsKZekmbUYgSqDIpfsrf6De/Oj1yhiperhdNrwR+za/xI +LCpZ8ELesd+Cz/kMsHxZPgq6XxDPGTK3NxoFB9F/OFJXpuSCbuhPO3b/V5FGx/hi +dfMsT1iQIL2wCf47DOp/5NgLUpGW5YN+qoFMlQIDAQABAoIBAQCzy4u312XeW1Cs +Mx6EuOwmh59/ESFmBkZh4rxZKYgrfE5EWlQ7i5SwG4BX+wR6rbNfy6JSmHDXlTkk +CKvvToVNcW6fYHEivDnVojhIERFIJ4+rhQmpBtcNLOQ3/4cZ8X/GxE6b+3lb5l+x +64mnjPLKRaIr5/+TVuebEy0xNTJmjnJ7yiB2HRz7uXEQaVSk/P7KAkkyl/9J3/LM +8N9AX1w6qDaNQZ4/P0++1H4SQenosM/b/GqGTomarEk/GE0NcB9rzmR9VCXa7FRh +WV5jyt9vUrwIEiK/6nUnOkGO8Ei3kB7Y+e+2m6WdaNoU5RAfqXmXa0Q/a0lLRruf +vTMo2WrBAoGBAPRaK4cx76Q+3SJ/wfznaPsMM06OSR8A3ctKdV+ip/lyKtb1W8Pz +k8MYQDH7GwPtSu5QD8doL00pPjugZL/ba7X9nAsI+pinyEErfnB9y7ORNEjIYYzs +DiqDKup7ANgw1gZvznWvb9Ge0WUSXvWS0pFkgootQAf+RmnnbWGH6l6RAoGBAO35 +aGUrLro5u9RD24uSXNU3NmojINIQFK5dHAT3yl0BBYstL43AEsye9lX95uMPTvOQ +Cqcn42Hjp/bSe3n0ObyOZeXVrWcDFAfE0wwB1BkvL1lpgnFO9+VQORlH4w3Ppnpo +jcPkR2TFeDaAYtvckhxe/Bk3OnuFmnsQ3VzM75fFAoGBAI6PvS2XeNU+yA3EtA01 +hg5SQ+zlHswz2TMuMeSmJZJnhY78f5mHlwIQOAPxGQXlf/4iP9J7en1uPpzTK3S0 +M9duK4hUqMA/w5oiIhbHjf0qDnMYVbG+V1V+SZ+cPBXmCDihKreGr5qBKnHpkfV8 +v9WL6o1rcRw4wiQvnaV1gsvBAoGBALtzVTczr6gDKCAIn5wuWy+cQSGTsBunjRLX +xuVm5iEiV+KMYkPvAx/pKzMLP96lRVR3ptyKgAKwl7LFk3u50+zh4gQLr35QH2wL +Lw7rNc3srAhrItPsFzqrWX6/cGuFoKYVS239l/sZzRppQPXcpb7xVvTp2whHcir0 +Wtnpl+TdAoGAGqKqo2KU3JoY3IuTDUk1dsNAm8jd9EWDh+s1x4aG4N79mwcss5GD +FF8MbFPneK7xQd8L6HisKUDAUi2NOyynM81LAftPkvN6ZuUVeFDfCL4vCA0HUXLD ++VrOhtUZkNNJlLMiVRJuQKUOGlg8PpObqYbstQAf/0/yFJMRHG82Tcg= +-----END RSA PRIVATE KEY-----` diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index e58dbf1d583..d7e0a0c29e3 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -119,21 +119,9 @@ func (c *QueryCondition) getRequestForAlertRule(datasource *m.DataSource, timeRa TimeRange: timeRange, Queries: []*tsdb.Query{ { - RefId: "A", - Model: c.Query.Model, - DataSource: &tsdb.DataSourceInfo{ - Id: datasource.Id, - Name: datasource.Name, - PluginId: datasource.Type, - Url: datasource.Url, - User: datasource.User, - Password: datasource.Password, - Database: datasource.Database, - BasicAuth: datasource.BasicAuth, - BasicAuthUser: datasource.BasicAuthUser, - BasicAuthPassword: datasource.BasicAuthPassword, - JsonData: datasource.JsonData, - }, + RefId: "A", + Model: c.Query.Model, + DataSource: datasource, }, }, } diff --git a/pkg/tsdb/batch.go b/pkg/tsdb/batch.go index 284a158bf5f..8130ac94b0d 100644 --- a/pkg/tsdb/batch.go +++ b/pkg/tsdb/batch.go @@ -1,9 +1,6 @@ package tsdb -import ( - "context" - "errors" -) +import "context" type Batch struct { DataSourceId int64 @@ -24,12 +21,12 @@ func newBatch(dsId int64, queries QuerySlice) *Batch { } func (bg *Batch) process(ctx context.Context, queryContext *QueryContext) { - executor := getExecutorFor(bg.Queries[0].DataSource) + executor, err := getExecutorFor(bg.Queries[0].DataSource) - if executor == nil { + if err != nil { bg.Done = true result := &BatchResult{ - Error: errors.New("Could not find executor for data source type: " + bg.Queries[0].DataSource.PluginId), + Error: err, QueryResults: make(map[string]*QueryResult), } for _, query := range bg.Queries { diff --git a/pkg/tsdb/executor.go b/pkg/tsdb/executor.go index cc1d592dcd5..251b3dc947a 100644 --- a/pkg/tsdb/executor.go +++ b/pkg/tsdb/executor.go @@ -1,6 +1,11 @@ package tsdb -import "context" +import ( + "context" + "fmt" + + "github.com/grafana/grafana/pkg/models" +) type Executor interface { Execute(ctx context.Context, queries QuerySlice, query *QueryContext) *BatchResult @@ -8,17 +13,22 @@ type Executor interface { var registry map[string]GetExecutorFn -type GetExecutorFn func(dsInfo *DataSourceInfo) Executor +type GetExecutorFn func(dsInfo *models.DataSource) (Executor, error) func init() { registry = make(map[string]GetExecutorFn) } -func getExecutorFor(dsInfo *DataSourceInfo) Executor { - if fn, exists := registry[dsInfo.PluginId]; exists { - return fn(dsInfo) +func getExecutorFor(dsInfo *models.DataSource) (Executor, error) { + if fn, exists := registry[dsInfo.Type]; exists { + executor, err := fn(dsInfo) + if err != nil { + return nil, err + } + + return executor, nil } - return nil + return nil, fmt.Errorf("Could not find executor for data source type: %s", dsInfo.Type) } func RegisterExecutor(pluginId string, fn GetExecutorFn) { diff --git a/pkg/tsdb/fake_test.go b/pkg/tsdb/fake_test.go index c403fdba4fb..3c773971240 100644 --- a/pkg/tsdb/fake_test.go +++ b/pkg/tsdb/fake_test.go @@ -1,6 +1,10 @@ package tsdb -import "context" +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) type FakeExecutor struct { results map[string]*QueryResult @@ -9,11 +13,11 @@ type FakeExecutor struct { type ResultsFn func(context *QueryContext) *QueryResult -func NewFakeExecutor(dsInfo *DataSourceInfo) *FakeExecutor { +func NewFakeExecutor(dsInfo *models.DataSource) (*FakeExecutor, error) { return &FakeExecutor{ results: make(map[string]*QueryResult), resultsFn: make(map[string]ResultsFn), - } + }, nil } func (e *FakeExecutor) Execute(ctx context.Context, queries QuerySlice, context *QueryContext) *BatchResult { diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 3d6af5beb5b..a467e839b82 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -14,28 +14,36 @@ import ( "golang.org/x/net/context/ctxhttp" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" ) type GraphiteExecutor struct { - *tsdb.DataSourceInfo + *models.DataSource + HttpClient *http.Client } -func NewGraphiteExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { - return &GraphiteExecutor{dsInfo} +func NewGraphiteExecutor(datasource *models.DataSource) (tsdb.Executor, error) { + httpClient, err := datasource.GetHttpClient() + + if err != nil { + return nil, err + } + + return &GraphiteExecutor{ + DataSource: datasource, + HttpClient: httpClient, + }, nil } var ( - glog log.Logger - HttpClient *http.Client + glog log.Logger ) func init() { glog = log.New("tsdb.graphite") tsdb.RegisterExecutor("graphite", NewGraphiteExecutor) - - HttpClient = tsdb.GetDefaultClient() } func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { @@ -66,7 +74,7 @@ func (e *GraphiteExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result } - res, err := ctxhttp.Do(ctx, HttpClient, req) + res, err := ctxhttp.Do(ctx, e.HttpClient, req) if err != nil { result.Error = err return result diff --git a/pkg/tsdb/http.go b/pkg/tsdb/http.go deleted file mode 100644 index f5de146d470..00000000000 --- a/pkg/tsdb/http.go +++ /dev/null @@ -1,29 +0,0 @@ -package tsdb - -import ( - "crypto/tls" - "net" - "net/http" - "time" -) - -func GetDefaultClient() *http.Client { - tr := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - } - - return &http.Client{ - Timeout: time.Duration(30 * time.Second), - Transport: tr, - } -} diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 658fbcff023..21d359d24bf 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -11,34 +11,40 @@ import ( "golang.org/x/net/context/ctxhttp" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" ) type InfluxDBExecutor struct { - *tsdb.DataSourceInfo + *models.DataSource QueryParser *InfluxdbQueryParser ResponseParser *ResponseParser + HttpClient *http.Client } -func NewInfluxDBExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { +func NewInfluxDBExecutor(datasource *models.DataSource) (tsdb.Executor, error) { + httpClient, err := datasource.GetHttpClient() + + if err != nil { + return nil, err + } + return &InfluxDBExecutor{ - DataSourceInfo: dsInfo, + DataSource: datasource, QueryParser: &InfluxdbQueryParser{}, ResponseParser: &ResponseParser{}, - } + HttpClient: httpClient, + }, nil } var ( - glog log.Logger - HttpClient *http.Client + glog log.Logger ) func init() { glog = log.New("tsdb.influxdb") tsdb.RegisterExecutor("influxdb", NewInfluxDBExecutor) - - HttpClient = tsdb.GetDefaultClient() } func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, context *tsdb.QueryContext) *tsdb.BatchResult { @@ -63,7 +69,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result.WithError(err) } - resp, err := ctxhttp.Do(ctx, HttpClient, req) + resp, err := ctxhttp.Do(ctx, e.HttpClient, req) if err != nil { return result.WithError(err) } @@ -95,7 +101,7 @@ func (e *InfluxDBExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, func (e *InfluxDBExecutor) getQuery(queries tsdb.QuerySlice, context *tsdb.QueryContext) (*Query, error) { for _, v := range queries { - query, err := e.QueryParser.Parse(v.Model, e.DataSourceInfo) + query, err := e.QueryParser.Parse(v.Model, e.DataSource) if err != nil { return nil, err } diff --git a/pkg/tsdb/influxdb/model_parser.go b/pkg/tsdb/influxdb/model_parser.go index 410bf8f6e82..f3d87739e5b 100644 --- a/pkg/tsdb/influxdb/model_parser.go +++ b/pkg/tsdb/influxdb/model_parser.go @@ -4,12 +4,12 @@ import ( "strconv" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/tsdb" + "github.com/grafana/grafana/pkg/models" ) type InfluxdbQueryParser struct{} -func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *tsdb.DataSourceInfo) (*Query, error) { +func (qp *InfluxdbQueryParser) Parse(model *simplejson.Json, dsInfo *models.DataSource) (*Query, error) { policy := model.Get("policy").MustString("default") rawQuery := model.Get("query").MustString("") useRawQuery := model.Get("rawQuery").MustBool(false) diff --git a/pkg/tsdb/influxdb/model_parser_test.go b/pkg/tsdb/influxdb/model_parser_test.go index eae6e163cc0..f7049efb9a7 100644 --- a/pkg/tsdb/influxdb/model_parser_test.go +++ b/pkg/tsdb/influxdb/model_parser_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/tsdb" + "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -12,7 +12,7 @@ func TestInfluxdbQueryParser(t *testing.T) { Convey("Influxdb query parser", t, func() { parser := &InfluxdbQueryParser{} - dsInfo := &tsdb.DataSourceInfo{ + dsInfo := &models.DataSource{ JsonData: simplejson.New(), } diff --git a/pkg/tsdb/models.go b/pkg/tsdb/models.go index 366709cfba7..e95713e7077 100644 --- a/pkg/tsdb/models.go +++ b/pkg/tsdb/models.go @@ -2,6 +2,7 @@ package tsdb import ( "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" "gopkg.in/guregu/null.v3" ) @@ -9,7 +10,7 @@ type Query struct { RefId string Model *simplejson.Json Depends []string - DataSource *DataSourceInfo + DataSource *models.DataSource Results []*TimeSeries Exclude bool MaxDataPoints int64 @@ -28,20 +29,6 @@ type Response struct { Results map[string]*QueryResult `json:"results"` } -type DataSourceInfo struct { - Id int64 - Name string - PluginId string - Url string - Password string - User string - Database string - BasicAuth bool - BasicAuthUser string - BasicAuthPassword string - JsonData *simplejson.Json -} - type BatchTiming struct { TimeElapsed int64 } diff --git a/pkg/tsdb/opentsdb/opentsdb.go b/pkg/tsdb/opentsdb/opentsdb.go index c5e5b0020d1..f5b6ffda7d2 100644 --- a/pkg/tsdb/opentsdb/opentsdb.go +++ b/pkg/tsdb/opentsdb/opentsdb.go @@ -17,28 +17,36 @@ import ( "gopkg.in/guregu/null.v3" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" ) type OpenTsdbExecutor struct { - *tsdb.DataSourceInfo + *models.DataSource + httpClient *http.Client } -func NewOpenTsdbExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { - return &OpenTsdbExecutor{dsInfo} +func NewOpenTsdbExecutor(datasource *models.DataSource) (tsdb.Executor, error) { + httpClient, err := datasource.GetHttpClient() + + if err != nil { + return nil, err + } + + return &OpenTsdbExecutor{ + DataSource: datasource, + httpClient: httpClient, + }, nil } var ( - plog log.Logger - HttpClient *http.Client + plog log.Logger ) func init() { plog = log.New("tsdb.opentsdb") tsdb.RegisterExecutor("opentsdb", NewOpenTsdbExecutor) - - HttpClient = tsdb.GetDefaultClient() } func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult { @@ -64,7 +72,7 @@ func (e *OpenTsdbExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, return result } - res, err := ctxhttp.Do(ctx, HttpClient, req) + res, err := ctxhttp.Do(ctx, e.httpClient, req) if err != nil { result.Error = err return result diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 21e1dfdef24..c391f00920a 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -9,18 +9,30 @@ import ( "gopkg.in/guregu/null.v3" + "net/http" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" "github.com/prometheus/client_golang/api/prometheus" pmodel "github.com/prometheus/common/model" ) type PrometheusExecutor struct { - *tsdb.DataSourceInfo + *models.DataSource + Transport *http.Transport } -func NewPrometheusExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { - return &PrometheusExecutor{dsInfo} +func NewPrometheusExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { + transport, err := dsInfo.GetHttpTransport() + if err != nil { + return nil, err + } + + return &PrometheusExecutor{ + DataSource: dsInfo, + Transport: transport, + }, nil } var ( @@ -36,7 +48,8 @@ func init() { func (e *PrometheusExecutor) getClient() (prometheus.QueryAPI, error) { cfg := prometheus.Config{ - Address: e.DataSourceInfo.Url, + Address: e.DataSource.Url, + Transport: e.Transport, } client, err := prometheus.New(cfg) diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go index cf2dcc0f898..6aefd8686d8 100644 --- a/pkg/tsdb/testdata/testdata.go +++ b/pkg/tsdb/testdata/testdata.go @@ -4,19 +4,20 @@ import ( "context" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) type TestDataExecutor struct { - *tsdb.DataSourceInfo + *models.DataSource log log.Logger } -func NewTestDataExecutor(dsInfo *tsdb.DataSourceInfo) tsdb.Executor { +func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) { return &TestDataExecutor{ - DataSourceInfo: dsInfo, - log: log.New("tsdb.testdata"), - } + DataSource: dsInfo, + log: log.New("tsdb.testdata"), + }, nil } func init() { diff --git a/pkg/tsdb/tsdb_test.go b/pkg/tsdb/tsdb_test.go index 998f59a6b9d..2b1a2372cd6 100644 --- a/pkg/tsdb/tsdb_test.go +++ b/pkg/tsdb/tsdb_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/grafana/grafana/pkg/models" . "github.com/smartystreets/goconvey/convey" ) @@ -15,9 +16,9 @@ func TestMetricQuery(t *testing.T) { Convey("Given 3 queries for 2 data sources", func() { request := &Request{ Queries: QuerySlice{ - {RefId: "A", DataSource: &DataSourceInfo{Id: 1}}, - {RefId: "B", DataSource: &DataSourceInfo{Id: 1}}, - {RefId: "C", DataSource: &DataSourceInfo{Id: 2}}, + {RefId: "A", DataSource: &models.DataSource{Id: 1}}, + {RefId: "B", DataSource: &models.DataSource{Id: 1}}, + {RefId: "C", DataSource: &models.DataSource{Id: 2}}, }, } @@ -32,9 +33,9 @@ func TestMetricQuery(t *testing.T) { Convey("Given query 2 depends on query 1", func() { request := &Request{ Queries: QuerySlice{ - {RefId: "A", DataSource: &DataSourceInfo{Id: 1}}, - {RefId: "B", DataSource: &DataSourceInfo{Id: 2}}, - {RefId: "C", DataSource: &DataSourceInfo{Id: 3}, Depends: []string{"A", "B"}}, + {RefId: "A", DataSource: &models.DataSource{Id: 1}}, + {RefId: "B", DataSource: &models.DataSource{Id: 2}}, + {RefId: "C", DataSource: &models.DataSource{Id: 3}, Depends: []string{"A", "B"}}, }, } @@ -56,7 +57,7 @@ func TestMetricQuery(t *testing.T) { Convey("When executing request with one query", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}}, }, } @@ -75,8 +76,8 @@ func TestMetricQuery(t *testing.T) { Convey("When executing one request with two queries from same data source", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, - {RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, + {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}}, + {RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}}, }, } @@ -101,9 +102,9 @@ func TestMetricQuery(t *testing.T) { Convey("When executing one request with three queries from different datasources", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, - {RefId: "B", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}}, - {RefId: "C", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}}, + {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}}, + {RefId: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}}, + {RefId: "C", DataSource: &models.DataSource{Id: 2, Type: "test"}}, }, } @@ -118,7 +119,7 @@ func TestMetricQuery(t *testing.T) { Convey("When query uses data source of unknown type", t, func() { req := &Request{ Queries: QuerySlice{ - {RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "asdasdas"}}, + {RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "asdasdas"}}, }, } @@ -130,10 +131,10 @@ func TestMetricQuery(t *testing.T) { req := &Request{ Queries: QuerySlice{ { - RefId: "A", DataSource: &DataSourceInfo{Id: 1, PluginId: "test"}, + RefId: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}, }, { - RefId: "B", DataSource: &DataSourceInfo{Id: 2, PluginId: "test"}, Depends: []string{"A"}, + RefId: "B", DataSource: &models.DataSource{Id: 2, Type: "test"}, Depends: []string{"A"}, }, }, } @@ -167,9 +168,9 @@ func TestMetricQuery(t *testing.T) { } func registerFakeExecutor() *FakeExecutor { - executor := NewFakeExecutor(nil) - RegisterExecutor("test", func(dsInfo *DataSourceInfo) Executor { - return executor + executor, _ := NewFakeExecutor(nil) + RegisterExecutor("test", func(dsInfo *models.DataSource) (Executor, error) { + return executor, nil }) return executor From dd432e671e5da9061e19ce962e9ba4809e706fdb Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 7 Dec 2016 11:28:18 +0100 Subject: [PATCH 18/34] docs(changelog): note about closing #6687 --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 651a44052a1..9cea61561d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ### Enhancements * **Postgres**: Add support for Certs for Postgres database [#6655](https://github.com/grafana/grafana/issues/6655) -* **Victorops**: Add VictorOps Notification Integration [#6411](https://github.com/grafana/grafana/issues/6411) +* **Victorops**: Add VictorOps notification integration [#6411](https://github.com/grafana/grafana/issues/6411) +* **Opsgenie**: Add OpsGenie notification integratiion (by [@kylemcc](https://github.com/kylemcc)) [#6687](https://github.com/grafana/grafana/issues/6687) * **Singlestat**: New aggregation on singlestat panel [#6740](https://github.com/grafana/grafana/pull/6740) * **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697) * **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677) From e082d786972c2f7b92efc36860d454f0a4676c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 7 Dec 2016 12:07:56 +0100 Subject: [PATCH 19/34] fix(elasticsearch): removed unused code --- CHANGELOG.md | 1 + public/app/plugins/datasource/elasticsearch/bucket_agg.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cea61561d4..9d41b15f12c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * **Cloudwatch**: Make it possible to specify access and secret key on the data source config page [#6697](https://github.com/grafana/grafana/issues/6697) * **Table**: Added Hidden Column Style for Table Panel [#5677](https://github.com/grafana/grafana/pull/5677) * **Graph**: Shared crosshair option renamed to shared tooltip, shows tooltip on all graphs as you hover over one graph. [#1578](https://github.com/grafana/grafana/pull/1578), [#6274](https://github.com/grafana/grafana/pull/6274) +* **Elasticsearch**: Added support for Missing option (bucket) for terms aggregation [#4244](https://github.com/grafana/grafana/pull/4244), thx @shanielh ### Bugfixes * **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679) diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.js b/public/app/plugins/datasource/elasticsearch/bucket_agg.js index 1100f642d50..43ef4f91ab7 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.js +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.js @@ -27,7 +27,6 @@ function (angular, _, queryDef) { $scope.orderByOptions = []; $scope.bucketAggTypes = queryDef.bucketAggTypes; - $scope.bucketAggTypesHash = _.indexBy(queryDef.bucketAggTypes, 'value'); $scope.orderOptions = queryDef.orderOptions; $scope.sizeOptions = queryDef.sizeOptions; From 2bdd2a59cb8ff29cc2c0ef83d906314b08325e44 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 7 Dec 2016 15:19:58 +0100 Subject: [PATCH 20/34] test(alerting): improve naming for test scenario --- .../alerting/conditions/reducer_test.go | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index badf868e8fc..47a13a5a33d 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -11,25 +11,6 @@ import ( func TestSimpleReducer(t *testing.T) { Convey("Test simple reducer by calculating", t, func() { - Convey("avg", func() { - result := testReducer("avg", 1, 2, 3) - So(result, ShouldEqual, float64(2)) - }) - - Convey("avg of none null data", func() { - reducer := NewSimpleReducer("avg") - series := &tsdb.TimeSeries{ - Name: "test time serie", - } - - series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1)) - series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) - series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3)) - series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4)) - - So(reducer.Reduce(series).Float64, ShouldEqual, float64(3)) - }) - Convey("sum", func() { result := testReducer("sum", 1, 2, 3) So(result, ShouldEqual, float64(6)) @@ -69,6 +50,25 @@ func TestSimpleReducer(t *testing.T) { result := testReducer("median", 1) So(result, ShouldEqual, float64(1)) }) + + Convey("avg", func() { + result := testReducer("avg", 1, 2, 3) + So(result, ShouldEqual, float64(2)) + }) + + Convey("avg of number values and null values should ignore nulls", func() { + reducer := NewSimpleReducer("avg") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(3), 4)) + + So(reducer.Reduce(series).Float64, ShouldEqual, float64(3)) + }) }) } From 924535c6d0904cfc38facb5e24999f61261d7433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 7 Dec 2016 15:27:20 +0100 Subject: [PATCH 21/34] change(graph): changed null value graph option to default to null, closes #6868 --- public/app/plugins/panel/graph/module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 12639a2fd6d..d86212d33e6 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -88,7 +88,7 @@ class GraphCtrl extends MetricsPanelCtrl { avg: false }, // how null points should be handled - nullPointMode : 'connected', + nullPointMode : 'null', // staircase line mode steppedLine: false, // tooltip options From c871212d6c324c14871d808e7934b1eed6dc6f47 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 7 Dec 2016 15:58:03 +0100 Subject: [PATCH 22/34] docs(changelog): adds note about fixing avg() reducer bug --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d41b15f12c..f918779ebd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### Bugfixes * **Alerting**: Add alert message to webhook notifications [#6807](https://github.com/grafana/grafana/issues/6807) +* **Alerting**: Fixes a bug where avg() reducer treated null as zero. [6c9cf87](https://github.com/grafana/grafana/commit/6c9cf87080e52966846a48d04209d90c166ca42e) * **PNG Rendering**: Fix for server side rendering when using non default http addr bind and domain setting [#6813](https://github.com/grafana/grafana/issues/6813) * **PNG Rendering**: Fix for server side rendering when setting enforce_domain to true [#6769](https://github.com/grafana/grafana/issues/6769) * **Webhooks**: Add content type json to outgoing webhooks [#6822](https://github.com/grafana/grafana/issues/6822) From 8ee85626f21826a20264206f044ba7b4f83a3a25 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 7 Dec 2016 18:09:17 +0100 Subject: [PATCH 23/34] fix(api): return correct json info after updating datasource closes #6869 --- pkg/api/api.go | 2 +- pkg/api/datasources.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 4fa28f799b0..0fc5acb958e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -193,7 +193,7 @@ func Register(r *macaron.Macaron) { r.Group("/datasources", func() { r.Get("/", GetDataSources) r.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), AddDataSource) - r.Put("/:id", bind(m.UpdateDataSourceCommand{}), UpdateDataSource) + r.Put("/:id", bind(m.UpdateDataSourceCommand{}), wrap(UpdateDataSource)) r.Delete("/:id", DeleteDataSource) r.Get("/:id", wrap(GetDataSourceById)) r.Get("/name/:name", wrap(GetDataSourceByName)) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index a7254eeec6c..f0c022c1622 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -117,7 +117,7 @@ func UpdateDataSource(c *middleware.Context, cmd m.UpdateDataSourceCommand) Resp return ApiError(500, "Failed to update datasource", err) } - return Json(200, "Datasource updated") + return Json(200, util.DynMap{"message": "Datasource updated"}) } func fillWithSecureJsonData(cmd *m.UpdateDataSourceCommand) error { From 6ae6a5d312d6fc2ac1fda25a0819db78dab6b7a2 Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Wed, 7 Dec 2016 14:51:00 -0500 Subject: [PATCH 24/34] Polished some styles, updated issue 6466 --- .../plugins/panel/gettingstarted/module.html | 4 +-- .../components/_panel_gettingstarted.scss | 26 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html index ae5f3b9ed54..86605c52fb8 100644 --- a/public/app/plugins/panel/gettingstarted/module.html +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -8,8 +8,8 @@ diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index b3967c0b421..9703f8086d0 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -4,13 +4,14 @@ $progress-color-dark: $panel-bg !default; $progress-color: $panel-bg !default; $progress-color-light: $panel-bg !default; $progress-color-grey-light: $body-bg !default; +$progress-color-shadow: $panel-border !default; $progress-color-grey: $iconContainerBackground !default; $progress-color-grey-dark: $iconContainerBackground !default; // Sizing $marker-size: 60px !default; $marker-size-half: ($marker-size / 2); -$path-height: 4px !default; +$path-height: 2px !default; $path-position: $marker-size-half - ($path-height / 2); @@ -62,9 +63,16 @@ $path-position: $marker-size-half - ($path-height / 2); right: - $marker-size-half; width: 100%; height: $path-height; + border-top: 2px solid $progress-color-grey-light; + border-bottom: $progress-color-shadow; background: $progress-color-grey-light; } + &:first-child { + &::after { + left: 50%; + } + } &:last-child { &::after { right: 50%; @@ -105,10 +113,12 @@ $path-position: $marker-size-half - ($path-height / 2); } .progress-text { text-decoration: line-through; + &:hover { + color: $text-color-weak; + } } - &::after { - background: $progress-color-grey; + background: $progress-color-grey-light; } } } @@ -136,6 +146,9 @@ $path-position: $marker-size-half - ($path-height / 2); color: $text-color-weak; font-size: 35px; vertical-align: sub; + &:hover { + color: $link-hover-color; + } } // Progress text @@ -143,6 +156,10 @@ $path-position: $marker-size-half - ($path-height / 2); display: block; overflow: hidden; text-overflow: ellipsis; + color: $text-muted; + &:hover { + color: $link-hover-color; + } } .progress-marker { @@ -151,6 +168,3 @@ $path-position: $marker-size-half - ($path-height / 2); font-size: 35px; vertical-align: sub; } - - - From c94805d4c2d3c128a13e97fd37d642045f8fcea3 Mon Sep 17 00:00:00 2001 From: Andrey Artemov Date: Thu, 8 Dec 2016 10:12:41 +0200 Subject: [PATCH 25/34] Fix typo in Singlestat page title --- docs/sources/reference/singlestat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/reference/singlestat.md b/docs/sources/reference/singlestat.md index 1fb0c254c01..ed0a956b7eb 100644 --- a/docs/sources/reference/singlestat.md +++ b/docs/sources/reference/singlestat.md @@ -1,5 +1,5 @@ +++ -title = "Singletat Panel" +title = "Singlestat Panel" keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"] type = "docs" [menu.docs] From c97e95227abde9f5adb9871088f58e0f834bbd37 Mon Sep 17 00:00:00 2001 From: Andrey Artemov Date: Thu, 8 Dec 2016 10:14:58 +0200 Subject: [PATCH 26/34] Fix some typos in "Export & Import" reference --- docs/sources/reference/export_import.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/reference/export_import.md b/docs/sources/reference/export_import.md index 7430f4891a6..bc05795c030 100644 --- a/docs/sources/reference/export_import.md +++ b/docs/sources/reference/export_import.md @@ -9,7 +9,7 @@ weight = 8 # Export and Import -Grafana Dashboads can easily be exported and imported, either from the UI or from the HTTP API. +Grafana Dashboards can easily be exported and imported, either from the UI or from the HTTP API. ## Exporting a dashboard @@ -22,9 +22,9 @@ The export feature is accessed from the share menu. ### Making a dashboard portable If you want to export a dashboard for others to use then it could be a good idea to -add template variables for things like a metric prefix (use contant variable) and server name. +add template variables for things like a metric prefix (use constant variable) and server name. -A template varible of the type `Constant` will automatically be hidden in +A template variable of the type `Constant` will automatically be hidden in the dashboard, and will also be added as an required input when the dashboard is imported. ## Importing a dashboard @@ -43,7 +43,7 @@ data source you want the dashboard to use and specify any metric prefixes (if th ## Discover dashboards on Grafana.net -Find dashboads for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards). +Find dashboards for common server applications at [Grafana.net/dashboards](https://grafana.net/dashboards). From 05772b30d74ee409b0b5459b3ae98779ec4d82c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 8 Dec 2016 10:25:05 +0100 Subject: [PATCH 27/34] feat(ux): completed work on getting started panel, #6466 --- pkg/api/dashboard.go | 20 +++++++++++ pkg/api/frontendsettings.go | 13 +++---- pkg/middleware/middleware.go | 4 +++ pkg/plugins/models.go | 1 + .../app/features/dashboard/row/add_panel.ts | 10 ++++-- .../plugins/panel/gettingstarted/module.ts | 36 ------------------- .../plugins/panel/gettingstarted/plugin.json | 2 ++ public/dashboards/home.json | 10 +----- .../components/_panel_gettingstarted.scss | 4 --- 9 files changed, 43 insertions(+), 57 deletions(-) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 0ed77fe982e..55925c4faf6 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/middleware" @@ -216,9 +217,28 @@ func GetHomeDashboard(c *middleware.Context) Response { return ApiError(500, "Failed to load home dashboard", err) } + if c.HasUserRole(m.ROLE_ADMIN) && !c.HasHelpFlag(m.HelpFlagGettingStartedPanelDismissed) { + addGettingStartedPanelToHomeDashboard(dash.Dashboard) + } + return Json(200, &dash) } +func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) { + rows := dash.Get("rows").MustArray() + row := simplejson.NewFromAny(rows[0]) + + newpanel := simplejson.NewFromAny(map[string]interface{}{ + "type": "gettingstarted", + "id": 123123, + "span": 12, + }) + + panels := row.Get("panels").MustArray() + panels = append(panels, newpanel) + row.Set("panels", panels) +} + func GetDashboardFromJsonFile(c *middleware.Context) { file := c.Params(":file") diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 384dd7f0ea1..a3dd9f9ce61 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -122,12 +122,13 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro panels := map[string]interface{}{} for _, panel := range enabledPlugins.Panels { panels[panel.Id] = map[string]interface{}{ - "module": panel.Module, - "baseUrl": panel.BaseUrl, - "name": panel.Name, - "id": panel.Id, - "info": panel.Info, - "sort": getPanelSort(panel.Id), + "module": panel.Module, + "baseUrl": panel.BaseUrl, + "name": panel.Name, + "id": panel.Id, + "info": panel.Info, + "hideFromList": panel.HideFromList, + "sort": getPanelSort(panel.Id), } } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index a546d7e76fc..7a64656b0ee 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -229,6 +229,10 @@ func (ctx *Context) HasUserRole(role m.RoleType) bool { return ctx.OrgRole.Includes(role) } +func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool { + return ctx.HelpFlags1.HasFlag(flag) +} + func (ctx *Context) TimeRequest(timer metrics.Timer) { ctx.Data["perfmon.timer"] = timer } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index ca60662ade8..7de12b4036c 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -38,6 +38,7 @@ type PluginBase struct { Includes []*PluginInclude `json:"includes"` Module string `json:"module"` BaseUrl string `json:"baseUrl"` + HideFromList bool `json:"hideFromList"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` diff --git a/public/app/features/dashboard/row/add_panel.ts b/public/app/features/dashboard/row/add_panel.ts index e43d3d6c521..1ea0cc5159e 100644 --- a/public/app/features/dashboard/row/add_panel.ts +++ b/public/app/features/dashboard/row/add_panel.ts @@ -18,9 +18,15 @@ export class AddPanelCtrl { constructor(private $scope, private $timeout, private $rootScope) { this.row = this.rowCtrl.row; this.dashboard = this.rowCtrl.dashboard; - this.allPanels = _.orderBy(_.map(config.panels, item => item), 'sort'); - this.panelHits = this.allPanels; this.activeIndex = 0; + + this.allPanels = _.chain(config.panels) + .filter({hideFromList: false}) + .map(item => item) + .orderBy('sort') + .value(); + + this.panelHits = this.allPanels; } keyDown(evt) { diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index 4a490259c31..7200f9023de 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -14,44 +14,9 @@ class GettingStartedPanelCtrl extends PanelCtrl { constructor($scope, $injector, private backendSrv, private datasourceSrv, private $q) { super($scope, $injector); - /* tslint:disable */ - if (contextSrv.user.helpFlags1 & 1) { - this.row.removePanel(this.panel, false); - return; - } - /* tslint:enable */ - this.stepIndex = 0; this.steps = []; - if (!contextSrv.hasRole('Admin')) { - this.steps.push({ - cta: 'Basic Concepts Guide', - icon: 'fa fa-file-text-o', - href: 'http://docs.grafana.org/guides/basic_concepts/', - check: () => $q.when(false), - cssClass: 'active', - }); - - this.steps.push({ - cta: 'Getting Started Guide', - icon: 'fa fa-file-text-o', - href: 'http://docs.grafana.org/guides/getting_started/', - check: () => $q.when(false), - cssClass: 'active', - }); - - this.steps.push({ - cta: 'Building a dashboard', - icon: 'fa fa-film', - href: 'http://docs.grafana.org/tutorials/screencasts/', - check: () => $q.when(false), - cssClass: 'active', - }); - - return; - } - this.steps.push({ title: 'Install Grafana', icon: 'icon-gf icon-gf-check', @@ -114,7 +79,6 @@ class GettingStartedPanelCtrl extends PanelCtrl { this.stepIndex = -1; return this.nextStep().then(res => { this.checksDone = true; - console.log(this.steps); }); } diff --git a/public/app/plugins/panel/gettingstarted/plugin.json b/public/app/plugins/panel/gettingstarted/plugin.json index afcac53b578..106f509281f 100644 --- a/public/app/plugins/panel/gettingstarted/plugin.json +++ b/public/app/plugins/panel/gettingstarted/plugin.json @@ -3,6 +3,8 @@ "name": "Getting Started", "id": "gettingstarted", + "hideFromList": true, + "info": { "author": { "name": "Grafana Project", diff --git a/public/dashboards/home.json b/public/dashboards/home.json index 0ea0af67055..d2a41a63c43 100644 --- a/public/dashboards/home.json +++ b/public/dashboards/home.json @@ -9,7 +9,7 @@ "sharedCrosshair": false, "rows": [ { - "title": "Row title", + "title": "Home Dashboard", "collapse": false, "editable": true, "height": "25px", @@ -25,14 +25,6 @@ "title": "", "transparent": true, "type": "text" - }, - { - "id": 8, - "links": [], - "span": 12, - "title": "", - "transparent": false, - "type": "gettingstarted" } ] }, diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 9703f8086d0..54433b589d1 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -90,7 +90,6 @@ $path-position: $marker-size-half - ($path-height / 2); .progress-text { display: none; } - .progress-marker { .icon-gf { color: $brand-primary; @@ -113,9 +112,6 @@ $path-position: $marker-size-half - ($path-height / 2); } .progress-text { text-decoration: line-through; - &:hover { - color: $text-color-weak; - } } &::after { background: $progress-color-grey-light; From f68350acbf24e3aed27c2392e181d19165af9d1b Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Dec 2016 14:01:59 +0100 Subject: [PATCH 28/34] docs(alerting): link fix to issue rather then commit --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f918779ebd7..5d6c5b1e911 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ ### Bugfixes * **Alerting**: Add alert message to webhook notifications [#6807](https://github.com/grafana/grafana/issues/6807) -* **Alerting**: Fixes a bug where avg() reducer treated null as zero. [6c9cf87](https://github.com/grafana/grafana/commit/6c9cf87080e52966846a48d04209d90c166ca42e) +* **Alerting**: Fixes a bug where avg() reducer treated null as zero. [#6879](https://github.com/grafana/grafana/issues/6879) * **PNG Rendering**: Fix for server side rendering when using non default http addr bind and domain setting [#6813](https://github.com/grafana/grafana/issues/6813) * **PNG Rendering**: Fix for server side rendering when setting enforce_domain to true [#6769](https://github.com/grafana/grafana/issues/6769) * **Webhooks**: Add content type json to outgoing webhooks [#6822](https://github.com/grafana/grafana/issues/6822) From 49827de2784c1539d48e722720b3e413dab669d0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Dec 2016 15:05:37 +0100 Subject: [PATCH 29/34] docs: release 4.0.2 --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 8 ++++---- docs/sources/installation/windows.md | 2 +- packaging/publish/publish.sh | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index ab51e0ac124..903e03228ed 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -14,14 +14,14 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [4.0.1 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.1-1480694114_amd64.deb) +Stable for Debian-based Linux | [4.0.2 (x86-64 deb)](https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb) ## Install Stable ``` -$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.1-1480694114_amd64.deb +$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_4.0.2-1481203731_amd64.deb $ sudo apt-get install -y adduser libfontconfig -$ sudo dpkg -i grafana_4.0.1-1480694114_amd64.deb +$ sudo dpkg -i grafana_4.0.2-1481203731_amd64.deb ``` ## APT Repository diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 370bac44cf2..8ccaa43bc79 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -14,24 +14,24 @@ weight = 2 Description | Download ------------ | ------------- -Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.1 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1-1480694114.x86_64.rpm) +Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.0.2 (x86-64 rpm)](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm) ## Install Stable You can install Grafana using Yum directly. - $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1-1480694114.x86_64.rpm + $ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm Or install manually using `rpm`. #### On CentOS / Fedora / Redhat: $ sudo yum install initscripts fontconfig - $ sudo rpm -Uvh grafana-4.0.1-1480694114.x86_64.rpm + $ sudo rpm -Uvh grafana-4.0.2-1481203731.x86_64.rpm #### On OpenSuse: - $ sudo rpm -i --nodeps grafana-4.0.1-1480694114.x86_64.rpm + $ sudo rpm -i --nodeps grafana-4.0.2-1481203731.x86_64.rpm ## Install via YUM Repository diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index fc1495ec311..5217181f1ee 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -13,7 +13,7 @@ weight = 3 Description | Download ------------ | ------------- -Latest stable package for Windows | [grafana.4.0.1.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.1.windows-x64.zip) +Latest stable package for Windows | [grafana.4.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2.windows-x64.zip) ## Configure diff --git a/packaging/publish/publish.sh b/packaging/publish/publish.sh index 7619dd2e0f4..57adcbd3481 100755 --- a/packaging/publish/publish.sh +++ b/packaging/publish/publish.sh @@ -1,6 +1,6 @@ #! /usr/bin/env bash -deb_ver=4.0.0-1480439068 -rpm_ver=4.0.0-1480439068 +deb_ver=4.0.2-1481203731 +rpm_ver=4.0.2-1481203731 wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb From 7c3c8bc4c91241cad2b0a6f65ab49fdebe9bcf5a Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Dec 2016 15:14:44 +0100 Subject: [PATCH 30/34] docs(changelog): add date for 4.0.2 release --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6c5b1e911..bd7c3fad139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ * **API**: HTTP API for deleting org returning incorrect message for a non-existing org [#6679](https://github.com/grafana/grafana/issues/6679) * **Dashboard**: Posting empty dashboard result in corrupted dashboard [#5443](https://github.com/grafana/grafana/issues/5443) -# 4.0.2 (unreleased) +# 4.0.2 (2016-12-08) ### Enhancements * **Playlist**: Add support for kiosk mode [#6727](https://github.com/grafana/grafana/issues/6727) From 6265afd8b614b844c4b5b57d372510d510240aaa Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Dec 2016 15:17:13 +0100 Subject: [PATCH 31/34] docs: updat latest to 4.0.2 --- latest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/latest.json b/latest.json index dd8e0ce33da..78fd48769e8 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "4.0.1", - "testing": "4.0.1" + "stable": "4.0.2", + "testing": "4.0.2" } From db3ac79015d111fb65f3b6daf35c849cf27ccec3 Mon Sep 17 00:00:00 2001 From: Utkarsh Bhatnagar Date: Thu, 8 Dec 2016 12:08:04 -0800 Subject: [PATCH 32/34] Fixed Create org API in docs --- docs/sources/http_api/org.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/http_api/org.md b/docs/sources/http_api/org.md index 340d97438ae..72c995adedf 100644 --- a/docs/sources/http_api/org.md +++ b/docs/sources/http_api/org.md @@ -93,11 +93,11 @@ parent = "http_api" ## Create Organisation -`POST /api/org` +`POST /api/orgs` **Example Request**: - POST /api/org HTTP/1.1 + POST /api/orgs HTTP/1.1 Accept: application/json Content-Type: application/json Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk From f39f77692bf1eccbbb3235ab5d7d7bf5910a9139 Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Fri, 9 Dec 2016 03:41:54 -0500 Subject: [PATCH 33/34] Adjusted the active state to fix the faint line showing. Probably related to the font change going into 4.0 (#6886) --- public/sass/components/_tabs.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 08467126ba2..2426f70afee 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -65,6 +65,6 @@ border-bottom: 2px solid $panel-bg; color: $link-color; position: relative; - top: 2px; + top: 1px; } } From e58b6989c2767067e9d78575722dcafecfaeb26d Mon Sep 17 00:00:00 2001 From: Matt Toback Date: Fri, 9 Dec 2016 03:42:24 -0500 Subject: [PATCH 34/34] Small refactor to make hover highlighting proper, added a title tag for the completed grafana step. (#6882) --- .../plugins/panel/gettingstarted/module.html | 8 +++++--- .../app/plugins/panel/gettingstarted/module.ts | 3 +++ .../sass/components/_panel_gettingstarted.scss | 17 +++++++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/panel/gettingstarted/module.html b/public/app/plugins/panel/gettingstarted/module.html index 86605c52fb8..71aeb07dd9a 100644 --- a/public/app/plugins/panel/gettingstarted/module.html +++ b/public/app/plugins/panel/gettingstarted/module.html @@ -8,9 +8,11 @@
diff --git a/public/app/plugins/panel/gettingstarted/module.ts b/public/app/plugins/panel/gettingstarted/module.ts index 7200f9023de..9e19cce1063 100644 --- a/public/app/plugins/panel/gettingstarted/module.ts +++ b/public/app/plugins/panel/gettingstarted/module.ts @@ -20,6 +20,9 @@ class GettingStartedPanelCtrl extends PanelCtrl { this.steps.push({ title: 'Install Grafana', icon: 'icon-gf icon-gf-check', + href: 'http://docs.grafana.org/', + target: '_blank', + note: 'Review the installation docs', check: () => $q.when(true), }); diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 54433b589d1..d5a231a220c 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -142,9 +142,6 @@ $path-position: $marker-size-half - ($path-height / 2); color: $text-color-weak; font-size: 35px; vertical-align: sub; - &:hover { - color: $link-hover-color; - } } // Progress text @@ -153,9 +150,6 @@ $path-position: $marker-size-half - ($path-height / 2); overflow: hidden; text-overflow: ellipsis; color: $text-muted; - &:hover { - color: $link-hover-color; - } } .progress-marker { @@ -164,3 +158,14 @@ $path-position: $marker-size-half - ($path-height / 2); font-size: 35px; vertical-align: sub; } + +a.progress-link { + &:hover { + .progress-marker, .progress-text { + color: $link-hover-color; + } + &:hover .progress-marker.completed { + color: $online; + } + } +}