From 5f3b7eaa459e75f8a6b52e777a95c6ca025a9c3b Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Wed, 22 Mar 2017 23:28:55 +0100 Subject: [PATCH 01/77] Discord integration --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/discord.go | 114 ++++++++++++++++++ .../alerting/notifiers/discord_test.go | 52 ++++++++ 3 files changed, 168 insertions(+) create mode 100644 pkg/services/alerting/notifiers/discord.go create mode 100644 pkg/services/alerting/notifiers/discord_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index c23a53009a9..e00aafa54fd 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -52,6 +52,7 @@ var ( M_Alerting_Notification_Sent_Threema Counter M_Alerting_Notification_Sent_Sensu Counter M_Alerting_Notification_Sent_Pushover Counter + M_Alerting_Notification_Sent_Discord Counter M_Aws_CloudWatch_GetMetricStatistics Counter M_Aws_CloudWatch_ListMetrics Counter @@ -124,6 +125,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu") M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE") M_Alerting_Notification_Sent_Pushover = RegCounter("alerting.notifications_sent", "type", "pushover") + M_Alerting_Notification_Sent_Discord = RegCounter("alerting.notifications_sent", "type", "discord") M_Aws_CloudWatch_GetMetricStatistics = RegCounter("aws.cloudwatch.get_metric_statistics") M_Aws_CloudWatch_ListMetrics = RegCounter("aws.cloudwatch.list_metrics") diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go new file mode 100644 index 00000000000..ce9849bbe6c --- /dev/null +++ b/pkg/services/alerting/notifiers/discord.go @@ -0,0 +1,114 @@ +package notifiers + +import ( + "strconv" + "strings" + + "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" + "github.com/grafana/grafana/pkg/setting" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "discord", + Name: "Discord", + Description: "Sends notifications to Discord", + Factory: NewDiscordNotifier, + OptionsTemplate: ` +

Discord settings

+
+ Webhook URL + +
+ `, + }) +} + +func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + url := model.Settings.Get("url").MustString() + if url == "" { + return nil, alerting.ValidationError{Reason: "Could not find webhook url property in settings"} + } + + return &DiscordNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + WebhookURL: url, + log: log.New("alerting.notifier.discord"), + }, nil +} + +type DiscordNotifier struct { + NotifierBase + WebhookURL string + log log.Logger +} + +func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { + this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL) + metrics.M_Alerting_Notification_Sent_Discord.Inc(1) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + bodyJSON := simplejson.New() + bodyJSON.Set("username", "Grafana") + + fields := make([]map[string]interface{}, 0) + + for _, evt := range evalContext.EvalMatches { + fields = append(fields, map[string]interface{}{ + "name": evt.Metric, + "value": evt.Value, + "inline": true, + }) + } + + footer := map[string]interface{}{ + "text": "Grafana v" + setting.BuildVersion, + "icon_url": "https://grafana.com/assets/img/fav32.png", + } + + color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0) + + image := map[string]interface{}{ + "url": evalContext.ImagePublicUrl, + } + + embed := simplejson.New() + embed.Set("title", evalContext.GetNotificationTitle()) + //Discord takes integer for color + embed.Set("color", color) + embed.Set("url", ruleUrl) + embed.Set("description", evalContext.Rule.Message) + embed.Set("type", "rich") + embed.Set("fields", fields) + embed.Set("footer", footer) + embed.Set("image", image) + + bodyJSON.Set("embeds", []interface{}{embed}) + + body, _ := bodyJSON.MarshalJSON() + + this.log.Info("Message", string(body)) + + cmd := &m.SendWebhookSync{ + Url: this.WebhookURL, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to Discord", "error", err, "body", string(body)) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/discord_test.go b/pkg/services/alerting/notifiers/discord_test.go new file mode 100644 index 00000000000..fe925aab362 --- /dev/null +++ b/pkg/services/alerting/notifiers/discord_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 TestDiscordNotifier(t *testing.T) { + Convey("Telegram 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: "discord_testing", + Type: "discord", + Settings: settingsJSON, + } + + _, err := NewDiscordNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should trigger incident", func() { + json := ` + { + "url": "https://web.hook/" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "discord_testing", + Type: "discord", + Settings: settingsJSON, + } + + not, err := NewDiscordNotifier(model) + discordNotifier := not.(*DiscordNotifier) + + So(err, ShouldBeNil) + So(discordNotifier.Name, ShouldEqual, "discord_testing") + So(discordNotifier.Type, ShouldEqual, "discord") + So(discordNotifier.WebhookURL, ShouldEqual, "https://web.hook/") + }) + }) + }) +} From 1bf982439b3a729f5930936a3be4fb2ab9a8f01a Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Thu, 23 Mar 2017 21:53:54 +0100 Subject: [PATCH 02/77] Sending image --- pkg/components/null/float.go | 9 +++ pkg/models/notifications.go | 13 ++-- pkg/services/alerting/notifiers/discord.go | 83 ++++++++++++++++++--- pkg/services/notifications/notifications.go | 13 ++-- pkg/services/notifications/webhook.go | 21 ++++-- 5 files changed, 107 insertions(+), 32 deletions(-) diff --git a/pkg/components/null/float.go b/pkg/components/null/float.go index 1e78946e878..b7b2f44a741 100644 --- a/pkg/components/null/float.go +++ b/pkg/components/null/float.go @@ -106,6 +106,15 @@ func (f Float) String() string { return fmt.Sprintf("%1.3f", f.Float64) } +// FullString returns float as string in full precision +func (f Float) FullString() string { + if !f.Valid { + return "null" + } + + return fmt.Sprintf("%f", f.Float64) +} + // SetValid changes this Float's value and also sets it to be non-null. func (f *Float) SetValid(n float64) { f.Float64 = n diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index ad7aed3bc50..a3fc917f0f4 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -18,12 +18,13 @@ type SendEmailCommandSync struct { } type SendWebhookSync struct { - Url string - User string - Password string - Body string - HttpMethod string - HttpHeader map[string]string + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string } type SendResetPasswordEmailCommand struct { diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index ce9849bbe6c..17080511656 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -1,6 +1,10 @@ package notifiers import ( + "bytes" + "io" + "mime/multipart" + "os" "strconv" "strings" @@ -64,9 +68,10 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { fields := make([]map[string]interface{}, 0) for _, evt := range evalContext.EvalMatches { + fields = append(fields, map[string]interface{}{ "name": evt.Metric, - "value": evt.Value, + "value": evt.Value.FullString(), "inline": true, }) } @@ -78,10 +83,6 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { color, _ := strconv.ParseInt(strings.TrimLeft(evalContext.GetStateModel().Color, "#"), 16, 0) - image := map[string]interface{}{ - "url": evalContext.ImagePublicUrl, - } - embed := simplejson.New() embed.Set("title", evalContext.GetNotificationTitle()) //Discord takes integer for color @@ -91,22 +92,80 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { embed.Set("type", "rich") embed.Set("fields", fields) embed.Set("footer", footer) - embed.Set("image", image) + + var image = make(map[string]interface{}) + + var embeddedImage = false + + if evalContext.ImagePublicUrl != "" { + image = map[string]interface{}{ + "url": evalContext.ImagePublicUrl, + } + embed.Set("image", image) + } else { + image = map[string]interface{}{ + "url": "attachment://graph.png", + } + embed.Set("image", image) + embeddedImage = true + } bodyJSON.Set("embeds", []interface{}{embed}) - body, _ := bodyJSON.MarshalJSON() + json, _ := bodyJSON.MarshalJSON() - this.log.Info("Message", string(body)) + content_type := "application/json" + + var body []byte + + if embeddedImage { + + var b bytes.Buffer + + w := multipart.NewWriter(&b) + + f, err := os.Open(evalContext.ImageOnDiskPath) + + if err != nil { + this.log.Error("Can't open graph file", err) + return err + } + + defer f.Close() + + fw, err := w.CreateFormField("payload_json") + if err != nil { + return err + } + + if _, err = fw.Write([]byte(string(json))); err != nil { + return err + } + + fw, err = w.CreateFormFile("file", "graph.png") + + if _, err = io.Copy(fw, f); err != nil { + return err + } + + w.Close() + + body = b.Bytes() + content_type = w.FormDataContentType() + + } else { + body = json + } cmd := &m.SendWebhookSync{ - Url: this.WebhookURL, - Body: string(body), - HttpMethod: "POST", + Url: this.WebhookURL, + Body: string(body), + HttpMethod: "POST", + ContentType: content_type, } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { - this.log.Error("Failed to send notification to Discord", "error", err, "body", string(body)) + this.log.Error("Failed to send notification to Discord", "error", err) return err } diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index c765774d062..decb74e8be2 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -60,12 +60,13 @@ func Init() error { func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { return sendWebRequestSync(ctx, &Webhook{ - Url: cmd.Url, - User: cmd.User, - Password: cmd.Password, - Body: cmd.Body, - HttpMethod: cmd.HttpMethod, - HttpHeader: cmd.HttpHeader, + Url: cmd.Url, + User: cmd.User, + Password: cmd.Password, + Body: cmd.Body, + HttpMethod: cmd.HttpMethod, + HttpHeader: cmd.HttpHeader, + ContentType: cmd.ContentType, }) } diff --git a/pkg/services/notifications/webhook.go b/pkg/services/notifications/webhook.go index c74804ab828..f532ffc2f9e 100644 --- a/pkg/services/notifications/webhook.go +++ b/pkg/services/notifications/webhook.go @@ -16,12 +16,13 @@ import ( ) type Webhook struct { - Url string - User string - Password string - Body string - HttpMethod string - HttpHeader map[string]string + Url string + User string + Password string + Body string + HttpMethod string + HttpHeader map[string]string + ContentType string } var netTransport = &http.Transport{ @@ -61,7 +62,7 @@ func processWebhookQueue() { } func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { - webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod) + webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod, "content type", webhook.ContentType) if webhook.HttpMethod == "" { webhook.HttpMethod = http.MethodPost @@ -72,7 +73,11 @@ func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { return err } - request.Header.Add("Content-Type", "application/json") + if webhook.ContentType == "" { + webhook.ContentType = "application/json" + } + + request.Header.Add("Content-Type", webhook.ContentType) request.Header.Add("User-Agent", "Grafana") if webhook.User != "" && webhook.Password != "" { request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password)) From dc11f1da81fb4dfa7758813d4dbc90c8e33ef9cb Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Sun, 8 Apr 2018 20:00:18 +0200 Subject: [PATCH 03/77] Make dashboard JSON editable --- public/app/features/dashboard/dashboard_srv.ts | 6 +++++- public/app/features/dashboard/settings/settings.ts | 8 +++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index 9d766fdfc3f..0ce448b6e88 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -100,7 +100,7 @@ export class DashboardSrv { .catch(this.handleSaveDashboardError.bind(this, clone, options)); } - saveDashboard(options, clone) { + saveDashboard(options?, clone?) { if (clone) { this.setCurrent(this.create(clone, this.dash.meta)); } @@ -120,6 +120,10 @@ export class DashboardSrv { return this.save(this.dash.getSaveModelClone(), options); } + saveJSONDashboard(json: string) { + return this.save(JSON.parse(json), {}); + } + showSaveAsModal() { this.$rootScope.appEvent('show-modal', { templateHtml: '', diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index e9d5c6180be..9949b52dd45 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -134,7 +134,13 @@ export class SettingsCtrl { } saveDashboard() { - this.dashboardSrv.saveDashboard(); + if (this.viewId === 'view_json') { + this.dashboardSrv.saveJSONDashboard(this.json).then(() => { + window.location.reload(); + }); + } else { + this.dashboardSrv.saveDashboard(); + } } onPostSave() { From 697a3eaa4358130224ddbe25c10412177b1a42d5 Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Sun, 8 Apr 2018 20:31:17 +0200 Subject: [PATCH 04/77] Fix after merge --- pkg/services/alerting/notifiers/discord.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 17080511656..b6831c90cfe 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -11,7 +11,6 @@ import ( "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" "github.com/grafana/grafana/pkg/setting" @@ -54,7 +53,6 @@ type DiscordNotifier struct { func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error { this.log.Info("Sending alert notification to", "webhook_url", this.WebhookURL) - metrics.M_Alerting_Notification_Sent_Discord.Inc(1) ruleUrl, err := evalContext.GetRuleUrl() if err != nil { From cfd1f5c072ab1d8282e5396cc79e306f968182d9 Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Sun, 8 Apr 2018 20:38:08 +0200 Subject: [PATCH 05/77] Revert files From c4b57a2562966fc143fb2ad0eb0a90abdca7a308 Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Sun, 8 Apr 2018 20:40:09 +0200 Subject: [PATCH 06/77] Revert files --- .../components/EmptyListCTA/EmptyListCTA.tsx | 53 +- .../core/components/Login/LoginBackground.tsx | 16 +- .../app/core/components/PasswordStrength.tsx | 15 +- .../components/colorpicker/ColorPalette.tsx | 4 +- .../colorpicker/ColorPickerPopover.tsx | 31 +- .../components/colorpicker/SpectrumPicker.tsx | 24 +- .../app/core/specs/PasswordStrength.jest.tsx | 13 +- .../dashboard/dashgrid/DashboardPanel.tsx | 15 +- .../app/features/templating/query_variable.ts | 4 +- public/sass/_variables.scss | 29 +- public/sass/base/_font_awesome.scss | 26 +- public/sass/base/_forms.scss | 44 +- public/sass/base/_grafana_icons.scss | 123 +- public/sass/base/_normalize.scss | 20 +- public/sass/base/_reboot.scss | 8 +- public/sass/base/font-awesome/_core.scss | 3 +- public/sass/base/font-awesome/_mixins.scss | 7 +- public/sass/base/font-awesome/_path.scss | 19 +- public/sass/base/font-awesome/_variables.scss | 1576 ++++++++--------- public/sass/components/_drop.scss | 12 +- public/sass/components/_filter-list.scss | 6 +- public/sass/components/_footer.scss | 4 +- public/sass/components/_json_explorer.scss | 6 +- public/sass/components/_jsontree.scss | 4 +- .../components/_panel_gettingstarted.scss | 4 +- public/sass/components/_row.scss | 2 +- public/sass/components/_shortcuts.scss | 2 +- public/sass/components/_switch.scss | 8 +- public/sass/components/_tabs.scss | 9 +- public/sass/components/_timepicker.scss | 4 +- public/sass/mixins/_drop_element.scss | 52 +- public/sass/mixins/_forms.scss | 3 +- public/sass/mixins/_mixins.scss | 63 +- public/sass/pages/_playlist.scss | 4 +- public/sass/utils/_validation.scss | 2 +- public/test/core/utils/version_specs.ts | 36 +- public/test/index.ts | 4 + public/test/jest-shim.ts | 3 +- public/test/lib/common.ts | 13 +- public/test/mocks/backend_srv.ts | 5 +- public/test/specs/app_specs.ts | 7 +- public/test/specs/helpers.ts | 22 +- 42 files changed, 1201 insertions(+), 1104 deletions(-) diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index c32edee156d..1583303dfa1 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -1,37 +1,34 @@ import React, { Component } from 'react'; export interface IProps { - model: any; + model: any; } class EmptyListCTA extends Component { - render() { - const { - title, - buttonIcon, - buttonLink, - buttonTitle, - proTip, - proTipLink, - proTipLinkTitle, - proTipTarget, - } = this.props.model; - return ( -
-
{title}
- - - {buttonTitle} - -
- ProTip: {proTip} - - {proTipLinkTitle} - -
-
- ); - } + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget + } = this.props.model; + return ( +
+
{title}
+ {buttonTitle} +
+ ProTip: {proTip} + {proTipLinkTitle} +
+
+ ); + } } export default EmptyListCTA; diff --git a/public/app/core/components/Login/LoginBackground.tsx b/public/app/core/components/Login/LoginBackground.tsx index fb554845240..83e228ab6e0 100644 --- a/public/app/core/components/Login/LoginBackground.tsx +++ b/public/app/core/components/Login/LoginBackground.tsx @@ -4,14 +4,10 @@ const xCount = 50; const yCount = 50; function Cell({ x, y, flipIndex }) { - const index = y * xCount + x; + const index = (y * xCount) + x; const bgColor1 = getColor(x, y); return ( -
+
); } @@ -35,7 +31,7 @@ export default class LoginBackground extends Component { } flipElements() { - const elementIndexToFlip = getRandomInt(0, xCount * yCount - 1); + const elementIndexToFlip = getRandomInt(0, (xCount * yCount) - 1); this.setState(prevState => { return { ...prevState, @@ -61,7 +57,9 @@ export default class LoginBackground extends Component { return (
{Array.from(Array(xCount)).map((el2, x) => { - return ; + return ( + + ); })}
); @@ -1238,5 +1236,5 @@ function getColor(x, y) { // let randY = getRandomInt(0, y); // let randIndex = randY * xCount + randX; - return colors[(y * xCount + x) % colors.length]; + return colors[(y*xCount + x) % colors.length]; } diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx index 4830ebdb61b..8f92b18445c 100644 --- a/public/app/core/components/PasswordStrength.tsx +++ b/public/app/core/components/PasswordStrength.tsx @@ -5,27 +5,28 @@ export interface IProps { } export class PasswordStrength extends React.Component { + constructor(props) { super(props); } render() { const { password } = this.props; - let strengthText = 'strength: strong like a bull.'; - let strengthClass = 'password-strength-good'; + let strengthText = "strength: strong like a bull."; + let strengthClass = "password-strength-good"; if (!password) { return null; } if (password.length <= 8) { - strengthText = 'strength: you can do better.'; - strengthClass = 'password-strength-ok'; + strengthText = "strength: you can do better."; + strengthClass = "password-strength-ok"; } if (password.length < 4) { - strengthText = 'strength: weak sauce.'; - strengthClass = 'password-strength-bad'; + strengthText = "strength: weak sauce."; + strengthClass = "password-strength-bad"; } return ( @@ -35,3 +36,5 @@ export class PasswordStrength extends React.Component { ); } } + + diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx index 812827996c7..07b25a32046 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -29,8 +29,7 @@ export class ColorPalette extends React.Component { key={paletteColor} className={'pointer fa ' + cssClass} style={{ color: paletteColor }} - onClick={this.onColorSelect(paletteColor)} - > + onClick={this.onColorSelect(paletteColor)}>  
); @@ -42,3 +41,4 @@ export class ColorPalette extends React.Component { ); } } + diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 4ac4161a160..360c3fdd5c4 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component { this.state = { tab: 'palette', color: this.props.color || DEFAULT_COLOR, - colorString: this.props.color || DEFAULT_COLOR, + colorString: this.props.color || DEFAULT_COLOR }; } @@ -32,7 +32,7 @@ export class ColorPickerPopover extends React.Component { if (newColor.isValid()) { this.setState({ color: newColor.toString(), - colorString: newColor.toString(), + colorString: newColor.toString() }); this.props.onColorSelect(color); } @@ -50,7 +50,7 @@ export class ColorPickerPopover extends React.Component { onColorStringChange(e) { let colorString = e.target.value; this.setState({ - colorString: colorString, + colorString: colorString }); let newColor = tinycolor(colorString); @@ -71,11 +71,11 @@ export class ColorPickerPopover extends React.Component { componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', e => { + this.pickerNavElem.on('show', (e) => { // use href attr (#name => name) let tab = e.target.hash.slice(1); this.setState({ - tab: tab, + tab: tab }); }); } @@ -97,24 +97,19 @@ export class ColorPickerPopover extends React.Component {
-
{currentTab}
+
+ {currentTab} +
- + +
); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index 7242c094cde..eef04545308 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -29,17 +29,14 @@ export class SpectrumPicker extends React.Component { } componentDidMount() { - let spectrumOptions = _.assignIn( - { - flat: true, - showAlpha: true, - showButtons: false, - color: this.props.color, - appendTo: this.elem, - move: this.onSpectrumMove, - }, - this.props.options - ); + let spectrumOptions = _.assignIn({ + flat: true, + showAlpha: true, + showButtons: false, + color: this.props.color, + appendTo: this.elem, + move: this.onSpectrumMove, + }, this.props.options); this.elem.spectrum(spectrumOptions); this.elem.spectrum('show'); @@ -67,6 +64,9 @@ export class SpectrumPicker extends React.Component { } render() { - return
; + return ( +
+ ); } } + diff --git a/public/app/core/specs/PasswordStrength.jest.tsx b/public/app/core/specs/PasswordStrength.jest.tsx index 1bd52ee6d50..a0a2df69029 100644 --- a/public/app/core/specs/PasswordStrength.jest.tsx +++ b/public/app/core/specs/PasswordStrength.jest.tsx @@ -1,21 +1,24 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import {shallow} from 'enzyme'; -import { PasswordStrength } from '../components/PasswordStrength'; +import {PasswordStrength} from '../components/PasswordStrength'; describe('PasswordStrength', () => { + it('should have class bad if length below 4', () => { const wrapper = shallow(); - expect(wrapper.find('.password-strength-bad')).toHaveLength(1); + expect(wrapper.find(".password-strength-bad")).toHaveLength(1); }); it('should have class ok if length below 8', () => { const wrapper = shallow(); - expect(wrapper.find('.password-strength-ok')).toHaveLength(1); + expect(wrapper.find(".password-strength-ok")).toHaveLength(1); }); it('should have class good if length above 8', () => { const wrapper = shallow(); - expect(wrapper.find('.password-strength-good')).toHaveLength(1); + expect(wrapper.find(".password-strength-good")).toHaveLength(1); }); + }); + diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index f135d94431a..27fe64d4660 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; -import { AttachedPanel } from './PanelLoader'; -import { DashboardRow } from './DashboardRow'; -import { AddPanelPanel } from './AddPanelPanel'; +import {PanelModel} from '../panel_model'; +import {PanelContainer} from './PanelContainer'; +import {AttachedPanel} from './PanelLoader'; +import {DashboardRow} from './DashboardRow'; +import {AddPanelPanel} from './AddPanelPanel'; export interface DashboardPanelProps { panel: PanelModel; @@ -46,6 +46,9 @@ export class DashboardPanel extends React.Component { return ; } - return
(this.element = element)} className="panel-height-helper" />; + return ( +
this.element = element} className="panel-height-helper" /> + ); } } + diff --git a/public/app/features/templating/query_variable.ts b/public/app/features/templating/query_variable.ts index 54bd7bb660c..b87167ad646 100644 --- a/public/app/features/templating/query_variable.ts +++ b/public/app/features/templating/query_variable.ts @@ -198,9 +198,7 @@ export class QueryVariable implements Variable { } }); } else if (sortType === 3) { - options = _.sortBy(options, opt => { - return _.toLower(opt.text); - }); + options = _.sortBy(options, opt => { return _.toLower(opt.text); }); } if (reverseSort) { diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 5a5f495470d..f46cacb0dd1 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -53,9 +53,9 @@ $enable-flex: true; // Typography // ------------------------- -$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif; -$font-family-serif: Georgia, 'Times New Roman', Times, serif; -$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace; +$font-family-sans-serif: "Roboto", Helvetica, Arial, sans-serif; +$font-family-serif: Georgia, "Times New Roman", Times, serif; +$font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; $font-family-base: $font-family-sans-serif !default; $font-size-root: 14px !default; @@ -90,7 +90,7 @@ $lead-font-size: 1.25rem !default; $lead-font-weight: 300 !default; $headings-margin-bottom: ($spacer / 2) !default; -$headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; +$headings-font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; $headings-font-weight: 400 !default; $headings-line-height: 1.1 !default; @@ -152,9 +152,16 @@ $input-padding-y-sm: 4px !default; $input-padding-x-lg: 20px !default; $input-padding-y-lg: 10px !default; -$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default; -$input-height-lg: ( ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2)) !default; -$input-height-sm: ( ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2)) !default; +$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) + !default; +$input-height-lg: ( + ($font-size-lg * $line-height-lg) + ($input-padding-y-lg * 2) + ) + !default; +$input-height-sm: ( + ($font-size-sm * $line-height-sm) + ($input-padding-y-sm * 2) + ) + !default; $form-group-margin-bottom: $spacer-y !default; $gf-form-margin: 0.2rem; @@ -214,9 +221,9 @@ $panel-padding: 0px 10px 5px 10px; $tabs-padding: 10px 15px 9px; $external-services: ( - github: (bgColor: #464646, borderColor: #393939, icon: ''), - google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ''), - grafanacom: (bgColor: inherit, borderColor: #393939, icon: ''), - oauth: (bgColor: inherit, borderColor: #393939, icon: '') + github: (bgColor: #464646, borderColor: #393939, icon: ""), + google: (bgColor: #e84d3c, borderColor: #b83e31, icon: ""), + grafanacom: (bgColor: inherit, borderColor: #393939, icon: ""), + oauth: (bgColor: inherit, borderColor: #393939, icon: "") ) !default; diff --git a/public/sass/base/_font_awesome.scss b/public/sass/base/_font_awesome.scss index c3521338143..47398b2e9ba 100644 --- a/public/sass/base/_font_awesome.scss +++ b/public/sass/base/_font_awesome.scss @@ -3,16 +3,16 @@ * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ -@import './font-awesome/variables'; -@import './font-awesome/mixins'; -@import './font-awesome/path'; -@import './font-awesome/core'; -@import './font-awesome/larger'; -@import './font-awesome/fixed-width'; -@import './font-awesome/list'; -@import './font-awesome/bordered-pulled'; -@import './font-awesome/animated'; -@import './font-awesome/rotated-flipped'; -@import './font-awesome/stacked'; -@import './font-awesome/icons'; -@import './font-awesome/screen-reader'; +@import "./font-awesome/variables"; +@import "./font-awesome/mixins"; +@import "./font-awesome/path"; +@import "./font-awesome/core"; +@import "./font-awesome/larger"; +@import "./font-awesome/fixed-width"; +@import "./font-awesome/list"; +@import "./font-awesome/bordered-pulled"; +@import "./font-awesome/animated"; +@import "./font-awesome/rotated-flipped"; +@import "./font-awesome/stacked"; +@import "./font-awesome/icons"; +@import "./font-awesome/screen-reader"; diff --git a/public/sass/base/_forms.scss b/public/sass/base/_forms.scss index 239bb8d4e1e..3197eb57991 100644 --- a/public/sass/base/_forms.scss +++ b/public/sass/base/_forms.scss @@ -58,19 +58,19 @@ textarea { } // Reset width of input images, buttons, radios, checkboxes -input[type='file'], -input[type='image'], -input[type='submit'], -input[type='reset'], -input[type='button'], -input[type='radio'], -input[type='checkbox'] { +input[type="file"], +input[type="image"], +input[type="submit"], +input[type="reset"], +input[type="button"], +input[type="radio"], +input[type="checkbox"] { width: auto; // Override of generic input selector } // Set the height of select and file controls to match text inputs select, -input[type='file'] { +input[type="file"] { height: $input-height; /* In IE7, the height of the select element cannot be changed by height, only font-size */ line-height: $input-height; } @@ -90,19 +90,19 @@ select[size] { // Focus for select, file, radio, and checkbox select:focus, -input[type='file']:focus, -input[type='radio']:focus, -input[type='checkbox']:focus { +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { @include tab-focus(); } // not a big fan of number fields -input[type='number']::-webkit-outer-spin-button, -input[type='number']::-webkit-inner-spin-button { +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } -input[type='number'] { +input[type="number"] { -moz-appearance: textfield; } // Placeholder @@ -155,15 +155,15 @@ textarea[readonly] { } // Explicitly reset the colors here -input[type='radio'][disabled], -input[type='checkbox'][disabled], -input[type='radio'][readonly], -input[type='checkbox'][readonly] { +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"][readonly], +input[type="checkbox"][readonly] { cursor: $cursor-disabled; background-color: transparent; } -input[type='text'].input-fluid { +input[type="text"].input-fluid { width: 100%; box-sizing: border-box; padding: 10px; @@ -172,7 +172,7 @@ input[type='text'].input-fluid { height: 100%; } -input[type='checkbox'].cr1 { +input[type="checkbox"].cr1 { display: none; } @@ -194,7 +194,7 @@ label.cr1 { cursor: pointer; } -input[type='checkbox'].cr1:checked + label { +input[type="checkbox"].cr1:checked + label { background: url($checkboxImageUrl) 0px -18px no-repeat; } @@ -203,7 +203,7 @@ input[type='checkbox'].cr1:checked + label { display: block; overflow: hidden; padding-right: 10px; - input[type='text'] { + input[type="text"] { width: 100%; padding: 5px 6px; height: 100%; diff --git a/public/sass/base/_grafana_icons.scss b/public/sass/base/_grafana_icons.scss index 73574ff9efc..55e57f6d087 100644 --- a/public/sass/base/_grafana_icons.scss +++ b/public/sass/base/_grafana_icons.scss @@ -1,17 +1,18 @@ @font-face { - font-family: 'grafana-icons'; - src: url('../fonts/grafana-icons.eot?okx5td'); - src: url('../fonts/grafana-icons.eot?okx5td#iefix') format('embedded-opentype'), - url('../fonts/grafana-icons.ttf?okx5td') format('truetype'), - url('../fonts/grafana-icons.woff?okx5td') format('woff'), - url('../fonts/grafana-icons.svg?okx5td#grafana-icons') format('svg'); + font-family: "grafana-icons"; + src: url("../fonts/grafana-icons.eot?okx5td"); + src: url("../fonts/grafana-icons.eot?okx5td#iefix") + format("embedded-opentype"), + url("../fonts/grafana-icons.ttf?okx5td") format("truetype"), + url("../fonts/grafana-icons.woff?okx5td") format("woff"), + url("../fonts/grafana-icons.svg?okx5td#grafana-icons") format("svg"); font-weight: normal; font-style: normal; } .icon-gf { /* use !important to prevent issues with browser extensions that change fonts */ - font-family: 'grafana-icons' !important; + font-family: "grafana-icons" !important; speak: none; font-style: normal; font-weight: normal; @@ -36,165 +37,165 @@ } .icon-gf-raintank_wordmark:before { - content: '\e600'; + content: "\e600"; } .micon-gf-raintank_icn:before { - content: '\e601'; + content: "\e601"; } .icon-gf-raintank_r-icn:before { - content: '\e905'; + content: "\e905"; } .icon-gf-check-alt:before { - content: '\e603'; + content: "\e603"; } .icon-gf-check:before { - content: '\e604'; + content: "\e604"; } .icon-gf-collector:before { - content: '\e605'; + content: "\e605"; } .icon-gf-dashboard:before { - content: '\e606'; + content: "\e606"; } .icon-gf-panel:before { - content: '\e904'; + content: "\e904"; } .icon-gf-datasources:before { - content: '\e607'; + content: "\e607"; } .icon-gf-endpoint-tiny:before { - content: '\e608'; + content: "\e608"; } .icon-gf-endpoint:before { - content: '\e609'; + content: "\e609"; } .icon-gf-page:before { - content: '\e908'; + content: "\e908"; } .icon-gf-filter:before { - content: '\e60a'; + content: "\e60a"; } .icon-gf-status:before { - content: '\e60b'; + content: "\e60b"; } .icon-gf-monitoring:before { - content: '\e60c'; + content: "\e60c"; } .icon-gf-monitoring-tiny:before { - content: '\e620'; + content: "\e620"; } .icon-gf-jump-to-dashboard:before { - content: '\e60d'; + content: "\e60d"; } .icon-gf-warn, .icon-gf-warning:before { - content: '\e60e'; + content: "\e60e"; } .icon-gf-nodata:before { - content: '\e60f'; + content: "\e60f"; } .icon-gf-critical:before { - content: '\e610'; + content: "\e610"; } .icon-gf-crit:before { - content: '\e610'; + content: "\e610"; } .icon-gf-online:before { - content: '\e611'; + content: "\e611"; } .icon-gf-event-error:before { - content: '\e623'; + content: "\e623"; } .icon-gf-event:before { - content: '\e624'; + content: "\e624"; } .icon-gf-sadface:before { - content: '\e907'; + content: "\e907"; } .icon-gf-private-collector:before { - content: '\e612'; + content: "\e612"; } .icon-gf-alert:before { - content: '\e61f'; + content: "\e61f"; } .icon-gf-alert-disabled:before { - content: '\e621'; + content: "\e621"; } .icon-gf-refresh:before { - content: '\e613'; + content: "\e613"; } .icon-gf-save:before { - content: '\e614'; + content: "\e614"; } .icon-gf-share:before { - content: '\e616'; + content: "\e616"; } .icon-gf-star:before { - content: '\e617'; + content: "\e617"; } .icon-gf-search:before { - content: '\e618'; + content: "\e618"; } .icon-gf-settings:before { - content: '\e615'; + content: "\e615"; } .icon-gf-add:before { - content: '\e619'; + content: "\e619"; } .icon-gf-remove:before { - content: '\e61a'; + content: "\e61a"; } .icon-gf-video:before { - content: '\e61b'; + content: "\e61b"; } .icon-gf-bulk_action:before { - content: '\e61c'; + content: "\e61c"; } .icon-gf-grabber:before { - content: '\e90b'; + content: "\e90b"; } .icon-gf-users:before { - content: '\e622'; + content: "\e622"; } .icon-gf-globe:before { - content: '\e61d'; + content: "\e61d"; } .icon-gf-snapshot:before { - content: '\e61e'; + content: "\e61e"; } .icon-gf-play-grafana-icon:before { - content: '\e629'; + content: "\e629"; } .icon-gf-grafana-icon:before { - content: '\e625'; + content: "\e625"; } .icon-gf-email:before { - content: '\e628'; + content: "\e628"; } .icon-gf-stopwatch:before { - content: '\e626'; + content: "\e626"; } .icon-gf-skull:before { - content: '\e900'; + content: "\e900"; } .icon-gf-probe:before { - content: '\e901'; + content: "\e901"; } .icon-gf-apps:before { - content: '\e902'; + content: "\e902"; } .icon-gf-scale:before { - content: '\e906'; + content: "\e906"; } .icon-gf-pending:before { - content: '\e909'; + content: "\e909"; } .icon-gf-verified:before { - content: '\e90a'; + content: "\e90a"; } .icon-gf-worldping:before { - content: '\e627'; + content: "\e627"; } .icon-gf-grafana_wordmark:before { - content: '\e903'; + content: "\e903"; } diff --git a/public/sass/base/_normalize.scss b/public/sass/base/_normalize.scss index fff6ad95d7c..057f5c63602 100644 --- a/public/sass/base/_normalize.scss +++ b/public/sass/base/_normalize.scss @@ -291,9 +291,9 @@ select { // button, -html input[type='button'], -// 1 input[type='reset'], -input[type='submit'] { +html input[type="button"], +// 1 input[type="reset"], +input[type="submit"] { -webkit-appearance: button; // 2 cursor: pointer; // 3 } @@ -334,8 +334,8 @@ input { // 2. Remove excess padding in IE 8/9/10. // -input[type='checkbox'], -input[type='radio'] { +input[type="checkbox"], +input[type="radio"] { box-sizing: border-box; // 1 padding: 0; // 2 } @@ -346,8 +346,8 @@ input[type='radio'] { // decrement button to change from `default` to `text`. // -input[type='number']::-webkit-inner-spin-button, -input[type='number']::-webkit-outer-spin-button { +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { height: auto; } @@ -355,7 +355,7 @@ input[type='number']::-webkit-outer-spin-button { // Address `appearance` set to `searchfield` in Safari and Chrome. // -input[type='search'] { +input[type="search"] { -webkit-appearance: textfield; } @@ -365,8 +365,8 @@ input[type='search'] { // padding (and `textfield` appearance). // -input[type='search']::-webkit-search-cancel-button, -input[type='search']::-webkit-search-decoration { +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } diff --git a/public/sass/base/_reboot.scss b/public/sass/base/_reboot.scss index ccbb17a07b6..e34bdfdb9ed 100644 --- a/public/sass/base/_reboot.scss +++ b/public/sass/base/_reboot.scss @@ -87,7 +87,7 @@ body { // might still respond to pointer events. // // Credit: https://github.com/suitcss/base -[tabindex='-1']:focus { +[tabindex="-1"]:focus { outline: none !important; } @@ -214,7 +214,7 @@ img { // for traditionally non-focusable elements with role="button" // see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile -[role='button'] { +[role="button"] { cursor: pointer; } @@ -231,7 +231,7 @@ img { a, area, button, -[role='button'], +[role="button"], input, label, select, @@ -320,7 +320,7 @@ legend { // border: 0; } -input[type='search'] { +input[type="search"] { // This overrides the extra rounded corners on search inputs in iOS so that our // `.form-control` class can properly style them. Note that this cannot simply // be added to `.form-control` as it's not specific enough. For details, see diff --git a/public/sass/base/font-awesome/_core.scss b/public/sass/base/font-awesome/_core.scss index a5411f108af..9940b63463d 100644 --- a/public/sass/base/font-awesome/_core.scss +++ b/public/sass/base/font-awesome/_core.scss @@ -3,7 +3,8 @@ .#{$fa-css-prefix} { display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} + FontAwesome; // shortening font declaration font-size: inherit; // can't have font-size inherit on line above, so need to override text-rendering: auto; // optimizelegibility throws things off #1094 -webkit-font-smoothing: antialiased; diff --git a/public/sass/base/font-awesome/_mixins.scss b/public/sass/base/font-awesome/_mixins.scss index f2a92c1e922..19771009107 100644 --- a/public/sass/base/font-awesome/_mixins.scss +++ b/public/sass/base/font-awesome/_mixins.scss @@ -3,7 +3,8 @@ @mixin fa-icon() { display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} + FontAwesome; // shortening font declaration font-size: inherit; // can't have font-size inherit on line above, so need to override text-rendering: auto; // optimizelegibility throws things off #1094 -webkit-font-smoothing: antialiased; @@ -11,14 +12,14 @@ } @mixin fa-icon-rotate($degrees, $rotation) { - -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})'; + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; -webkit-transform: rotate($degrees); -ms-transform: rotate($degrees); transform: rotate($degrees); } @mixin fa-icon-flip($horiz, $vert, $rotation) { - -ms-filter: 'progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)'; + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; -webkit-transform: scale($horiz, $vert); -ms-transform: scale($horiz, $vert); transform: scale($horiz, $vert); diff --git a/public/sass/base/font-awesome/_path.scss b/public/sass/base/font-awesome/_path.scss index d452de2170a..0316afa161d 100644 --- a/public/sass/base/font-awesome/_path.scss +++ b/public/sass/base/font-awesome/_path.scss @@ -2,13 +2,18 @@ * -------------------------- */ @font-face { - font-family: 'FontAwesome'; - src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); - src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), - url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), - url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), - url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), - url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); + font-family: "FontAwesome"; + src: url("#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}"); + src: url("#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}") + format("embedded-opentype"), + url("#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}") + format("woff2"), + url("#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}") + format("woff"), + url("#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}") + format("truetype"), + url("#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular") + format("svg"); // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts font-weight: normal; font-style: normal; diff --git a/public/sass/base/font-awesome/_variables.scss b/public/sass/base/font-awesome/_variables.scss index 1e91e831e66..a4bd3cd87b9 100644 --- a/public/sass/base/font-awesome/_variables.scss +++ b/public/sass/base/font-awesome/_variables.scss @@ -1,799 +1,799 @@ // Variables // -------------------------- -$fa-font-path: '../fonts' !default; +$fa-font-path: "../fonts" !default; $fa-font-size-base: 14px !default; $fa-line-height-base: 1 !default; //$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly $fa-css-prefix: fa !default; -$fa-version: '4.7.0' !default; +$fa-version: "4.7.0" !default; $fa-border-color: #eee !default; $fa-inverse: #fff !default; $fa-li-width: (30em / 14) !default; -$fa-var-500px: '\f26e'; -$fa-var-address-book: '\f2b9'; -$fa-var-address-book-o: '\f2ba'; -$fa-var-address-card: '\f2bb'; -$fa-var-address-card-o: '\f2bc'; -$fa-var-adjust: '\f042'; -$fa-var-adn: '\f170'; -$fa-var-align-center: '\f037'; -$fa-var-align-justify: '\f039'; -$fa-var-align-left: '\f036'; -$fa-var-align-right: '\f038'; -$fa-var-amazon: '\f270'; -$fa-var-ambulance: '\f0f9'; -$fa-var-american-sign-language-interpreting: '\f2a3'; -$fa-var-anchor: '\f13d'; -$fa-var-android: '\f17b'; -$fa-var-angellist: '\f209'; -$fa-var-angle-double-down: '\f103'; -$fa-var-angle-double-left: '\f100'; -$fa-var-angle-double-right: '\f101'; -$fa-var-angle-double-up: '\f102'; -$fa-var-angle-down: '\f107'; -$fa-var-angle-left: '\f104'; -$fa-var-angle-right: '\f105'; -$fa-var-angle-up: '\f106'; -$fa-var-apple: '\f179'; -$fa-var-archive: '\f187'; -$fa-var-area-chart: '\f1fe'; -$fa-var-arrow-circle-down: '\f0ab'; -$fa-var-arrow-circle-left: '\f0a8'; -$fa-var-arrow-circle-o-down: '\f01a'; -$fa-var-arrow-circle-o-left: '\f190'; -$fa-var-arrow-circle-o-right: '\f18e'; -$fa-var-arrow-circle-o-up: '\f01b'; -$fa-var-arrow-circle-right: '\f0a9'; -$fa-var-arrow-circle-up: '\f0aa'; -$fa-var-arrow-down: '\f063'; -$fa-var-arrow-left: '\f060'; -$fa-var-arrow-right: '\f061'; -$fa-var-arrow-up: '\f062'; -$fa-var-arrows: '\f047'; -$fa-var-arrows-alt: '\f0b2'; -$fa-var-arrows-h: '\f07e'; -$fa-var-arrows-v: '\f07d'; -$fa-var-asl-interpreting: '\f2a3'; -$fa-var-assistive-listening-systems: '\f2a2'; -$fa-var-asterisk: '\f069'; -$fa-var-at: '\f1fa'; -$fa-var-audio-description: '\f29e'; -$fa-var-automobile: '\f1b9'; -$fa-var-backward: '\f04a'; -$fa-var-balance-scale: '\f24e'; -$fa-var-ban: '\f05e'; -$fa-var-bandcamp: '\f2d5'; -$fa-var-bank: '\f19c'; -$fa-var-bar-chart: '\f080'; -$fa-var-bar-chart-o: '\f080'; -$fa-var-barcode: '\f02a'; -$fa-var-bars: '\f0c9'; -$fa-var-bath: '\f2cd'; -$fa-var-bathtub: '\f2cd'; -$fa-var-battery: '\f240'; -$fa-var-battery-0: '\f244'; -$fa-var-battery-1: '\f243'; -$fa-var-battery-2: '\f242'; -$fa-var-battery-3: '\f241'; -$fa-var-battery-4: '\f240'; -$fa-var-battery-empty: '\f244'; -$fa-var-battery-full: '\f240'; -$fa-var-battery-half: '\f242'; -$fa-var-battery-quarter: '\f243'; -$fa-var-battery-three-quarters: '\f241'; -$fa-var-bed: '\f236'; -$fa-var-beer: '\f0fc'; -$fa-var-behance: '\f1b4'; -$fa-var-behance-square: '\f1b5'; -$fa-var-bell: '\f0f3'; -$fa-var-bell-o: '\f0a2'; -$fa-var-bell-slash: '\f1f6'; -$fa-var-bell-slash-o: '\f1f7'; -$fa-var-bicycle: '\f206'; -$fa-var-binoculars: '\f1e5'; -$fa-var-birthday-cake: '\f1fd'; -$fa-var-bitbucket: '\f171'; -$fa-var-bitbucket-square: '\f172'; -$fa-var-bitcoin: '\f15a'; -$fa-var-black-tie: '\f27e'; -$fa-var-blind: '\f29d'; -$fa-var-bluetooth: '\f293'; -$fa-var-bluetooth-b: '\f294'; -$fa-var-bold: '\f032'; -$fa-var-bolt: '\f0e7'; -$fa-var-bomb: '\f1e2'; -$fa-var-book: '\f02d'; -$fa-var-bookmark: '\f02e'; -$fa-var-bookmark-o: '\f097'; -$fa-var-braille: '\f2a1'; -$fa-var-briefcase: '\f0b1'; -$fa-var-btc: '\f15a'; -$fa-var-bug: '\f188'; -$fa-var-building: '\f1ad'; -$fa-var-building-o: '\f0f7'; -$fa-var-bullhorn: '\f0a1'; -$fa-var-bullseye: '\f140'; -$fa-var-bus: '\f207'; -$fa-var-buysellads: '\f20d'; -$fa-var-cab: '\f1ba'; -$fa-var-calculator: '\f1ec'; -$fa-var-calendar: '\f073'; -$fa-var-calendar-check-o: '\f274'; -$fa-var-calendar-minus-o: '\f272'; -$fa-var-calendar-o: '\f133'; -$fa-var-calendar-plus-o: '\f271'; -$fa-var-calendar-times-o: '\f273'; -$fa-var-camera: '\f030'; -$fa-var-camera-retro: '\f083'; -$fa-var-car: '\f1b9'; -$fa-var-caret-down: '\f0d7'; -$fa-var-caret-left: '\f0d9'; -$fa-var-caret-right: '\f0da'; -$fa-var-caret-square-o-down: '\f150'; -$fa-var-caret-square-o-left: '\f191'; -$fa-var-caret-square-o-right: '\f152'; -$fa-var-caret-square-o-up: '\f151'; -$fa-var-caret-up: '\f0d8'; -$fa-var-cart-arrow-down: '\f218'; -$fa-var-cart-plus: '\f217'; -$fa-var-cc: '\f20a'; -$fa-var-cc-amex: '\f1f3'; -$fa-var-cc-diners-club: '\f24c'; -$fa-var-cc-discover: '\f1f2'; -$fa-var-cc-jcb: '\f24b'; -$fa-var-cc-mastercard: '\f1f1'; -$fa-var-cc-paypal: '\f1f4'; -$fa-var-cc-stripe: '\f1f5'; -$fa-var-cc-visa: '\f1f0'; -$fa-var-certificate: '\f0a3'; -$fa-var-chain: '\f0c1'; -$fa-var-chain-broken: '\f127'; -$fa-var-check: '\f00c'; -$fa-var-check-circle: '\f058'; -$fa-var-check-circle-o: '\f05d'; -$fa-var-check-square: '\f14a'; -$fa-var-check-square-o: '\f046'; -$fa-var-chevron-circle-down: '\f13a'; -$fa-var-chevron-circle-left: '\f137'; -$fa-var-chevron-circle-right: '\f138'; -$fa-var-chevron-circle-up: '\f139'; -$fa-var-chevron-down: '\f078'; -$fa-var-chevron-left: '\f053'; -$fa-var-chevron-right: '\f054'; -$fa-var-chevron-up: '\f077'; -$fa-var-child: '\f1ae'; -$fa-var-chrome: '\f268'; -$fa-var-circle: '\f111'; -$fa-var-circle-o: '\f10c'; -$fa-var-circle-o-notch: '\f1ce'; -$fa-var-circle-thin: '\f1db'; -$fa-var-clipboard: '\f0ea'; -$fa-var-clock-o: '\f017'; -$fa-var-clone: '\f24d'; -$fa-var-close: '\f00d'; -$fa-var-cloud: '\f0c2'; -$fa-var-cloud-download: '\f0ed'; -$fa-var-cloud-upload: '\f0ee'; -$fa-var-cny: '\f157'; -$fa-var-code: '\f121'; -$fa-var-code-fork: '\f126'; -$fa-var-codepen: '\f1cb'; -$fa-var-codiepie: '\f284'; -$fa-var-coffee: '\f0f4'; -$fa-var-cog: '\f013'; -$fa-var-cogs: '\f085'; -$fa-var-columns: '\f0db'; -$fa-var-comment: '\f075'; -$fa-var-comment-o: '\f0e5'; -$fa-var-commenting: '\f27a'; -$fa-var-commenting-o: '\f27b'; -$fa-var-comments: '\f086'; -$fa-var-comments-o: '\f0e6'; -$fa-var-compass: '\f14e'; -$fa-var-compress: '\f066'; -$fa-var-connectdevelop: '\f20e'; -$fa-var-contao: '\f26d'; -$fa-var-copy: '\f0c5'; -$fa-var-copyright: '\f1f9'; -$fa-var-creative-commons: '\f25e'; -$fa-var-credit-card: '\f09d'; -$fa-var-credit-card-alt: '\f283'; -$fa-var-crop: '\f125'; -$fa-var-crosshairs: '\f05b'; -$fa-var-css3: '\f13c'; -$fa-var-cube: '\f1b2'; -$fa-var-cubes: '\f1b3'; -$fa-var-cut: '\f0c4'; -$fa-var-cutlery: '\f0f5'; -$fa-var-dashboard: '\f0e4'; -$fa-var-dashcube: '\f210'; -$fa-var-database: '\f1c0'; -$fa-var-deaf: '\f2a4'; -$fa-var-deafness: '\f2a4'; -$fa-var-dedent: '\f03b'; -$fa-var-delicious: '\f1a5'; -$fa-var-desktop: '\f108'; -$fa-var-deviantart: '\f1bd'; -$fa-var-diamond: '\f219'; -$fa-var-digg: '\f1a6'; -$fa-var-dollar: '\f155'; -$fa-var-dot-circle-o: '\f192'; -$fa-var-download: '\f019'; -$fa-var-dribbble: '\f17d'; -$fa-var-drivers-license: '\f2c2'; -$fa-var-drivers-license-o: '\f2c3'; -$fa-var-dropbox: '\f16b'; -$fa-var-drupal: '\f1a9'; -$fa-var-edge: '\f282'; -$fa-var-edit: '\f044'; -$fa-var-eercast: '\f2da'; -$fa-var-eject: '\f052'; -$fa-var-ellipsis-h: '\f141'; -$fa-var-ellipsis-v: '\f142'; -$fa-var-empire: '\f1d1'; -$fa-var-envelope: '\f0e0'; -$fa-var-envelope-o: '\f003'; -$fa-var-envelope-open: '\f2b6'; -$fa-var-envelope-open-o: '\f2b7'; -$fa-var-envelope-square: '\f199'; -$fa-var-envira: '\f299'; -$fa-var-eraser: '\f12d'; -$fa-var-etsy: '\f2d7'; -$fa-var-eur: '\f153'; -$fa-var-euro: '\f153'; -$fa-var-exchange: '\f0ec'; -$fa-var-exclamation: '\f12a'; -$fa-var-exclamation-circle: '\f06a'; -$fa-var-exclamation-triangle: '\f071'; -$fa-var-expand: '\f065'; -$fa-var-expeditedssl: '\f23e'; -$fa-var-external-link: '\f08e'; -$fa-var-external-link-square: '\f14c'; -$fa-var-eye: '\f06e'; -$fa-var-eye-slash: '\f070'; -$fa-var-eyedropper: '\f1fb'; -$fa-var-fa: '\f2b4'; -$fa-var-facebook: '\f09a'; -$fa-var-facebook-f: '\f09a'; -$fa-var-facebook-official: '\f230'; -$fa-var-facebook-square: '\f082'; -$fa-var-fast-backward: '\f049'; -$fa-var-fast-forward: '\f050'; -$fa-var-fax: '\f1ac'; -$fa-var-feed: '\f09e'; -$fa-var-female: '\f182'; -$fa-var-fighter-jet: '\f0fb'; -$fa-var-file: '\f15b'; -$fa-var-file-archive-o: '\f1c6'; -$fa-var-file-audio-o: '\f1c7'; -$fa-var-file-code-o: '\f1c9'; -$fa-var-file-excel-o: '\f1c3'; -$fa-var-file-image-o: '\f1c5'; -$fa-var-file-movie-o: '\f1c8'; -$fa-var-file-o: '\f016'; -$fa-var-file-pdf-o: '\f1c1'; -$fa-var-file-photo-o: '\f1c5'; -$fa-var-file-picture-o: '\f1c5'; -$fa-var-file-powerpoint-o: '\f1c4'; -$fa-var-file-sound-o: '\f1c7'; -$fa-var-file-text: '\f15c'; -$fa-var-file-text-o: '\f0f6'; -$fa-var-file-video-o: '\f1c8'; -$fa-var-file-word-o: '\f1c2'; -$fa-var-file-zip-o: '\f1c6'; -$fa-var-files-o: '\f0c5'; -$fa-var-film: '\f008'; -$fa-var-filter: '\f0b0'; -$fa-var-fire: '\f06d'; -$fa-var-fire-extinguisher: '\f134'; -$fa-var-firefox: '\f269'; -$fa-var-first-order: '\f2b0'; -$fa-var-flag: '\f024'; -$fa-var-flag-checkered: '\f11e'; -$fa-var-flag-o: '\f11d'; -$fa-var-flash: '\f0e7'; -$fa-var-flask: '\f0c3'; -$fa-var-flickr: '\f16e'; -$fa-var-floppy-o: '\f0c7'; -$fa-var-folder: '\f07b'; -$fa-var-folder-o: '\f114'; -$fa-var-folder-open: '\f07c'; -$fa-var-folder-open-o: '\f115'; -$fa-var-font: '\f031'; -$fa-var-font-awesome: '\f2b4'; -$fa-var-fonticons: '\f280'; -$fa-var-fort-awesome: '\f286'; -$fa-var-forumbee: '\f211'; -$fa-var-forward: '\f04e'; -$fa-var-foursquare: '\f180'; -$fa-var-free-code-camp: '\f2c5'; -$fa-var-frown-o: '\f119'; -$fa-var-futbol-o: '\f1e3'; -$fa-var-gamepad: '\f11b'; -$fa-var-gavel: '\f0e3'; -$fa-var-gbp: '\f154'; -$fa-var-ge: '\f1d1'; -$fa-var-gear: '\f013'; -$fa-var-gears: '\f085'; -$fa-var-genderless: '\f22d'; -$fa-var-get-pocket: '\f265'; -$fa-var-gg: '\f260'; -$fa-var-gg-circle: '\f261'; -$fa-var-gift: '\f06b'; -$fa-var-git: '\f1d3'; -$fa-var-git-square: '\f1d2'; -$fa-var-github: '\f09b'; -$fa-var-github-alt: '\f113'; -$fa-var-github-square: '\f092'; -$fa-var-gitlab: '\f296'; -$fa-var-gittip: '\f184'; -$fa-var-glass: '\f000'; -$fa-var-glide: '\f2a5'; -$fa-var-glide-g: '\f2a6'; -$fa-var-globe: '\f0ac'; -$fa-var-google: '\f1a0'; -$fa-var-google-plus: '\f0d5'; -$fa-var-google-plus-circle: '\f2b3'; -$fa-var-google-plus-official: '\f2b3'; -$fa-var-google-plus-square: '\f0d4'; -$fa-var-google-wallet: '\f1ee'; -$fa-var-graduation-cap: '\f19d'; -$fa-var-gratipay: '\f184'; -$fa-var-grav: '\f2d6'; -$fa-var-group: '\f0c0'; -$fa-var-h-square: '\f0fd'; -$fa-var-hacker-news: '\f1d4'; -$fa-var-hand-grab-o: '\f255'; -$fa-var-hand-lizard-o: '\f258'; -$fa-var-hand-o-down: '\f0a7'; -$fa-var-hand-o-left: '\f0a5'; -$fa-var-hand-o-right: '\f0a4'; -$fa-var-hand-o-up: '\f0a6'; -$fa-var-hand-paper-o: '\f256'; -$fa-var-hand-peace-o: '\f25b'; -$fa-var-hand-pointer-o: '\f25a'; -$fa-var-hand-rock-o: '\f255'; -$fa-var-hand-scissors-o: '\f257'; -$fa-var-hand-spock-o: '\f259'; -$fa-var-hand-stop-o: '\f256'; -$fa-var-handshake-o: '\f2b5'; -$fa-var-hard-of-hearing: '\f2a4'; -$fa-var-hashtag: '\f292'; -$fa-var-hdd-o: '\f0a0'; -$fa-var-header: '\f1dc'; -$fa-var-headphones: '\f025'; -$fa-var-heart: '\f004'; -$fa-var-heart-o: '\f08a'; -$fa-var-heartbeat: '\f21e'; -$fa-var-history: '\f1da'; -$fa-var-home: '\f015'; -$fa-var-hospital-o: '\f0f8'; -$fa-var-hotel: '\f236'; -$fa-var-hourglass: '\f254'; -$fa-var-hourglass-1: '\f251'; -$fa-var-hourglass-2: '\f252'; -$fa-var-hourglass-3: '\f253'; -$fa-var-hourglass-end: '\f253'; -$fa-var-hourglass-half: '\f252'; -$fa-var-hourglass-o: '\f250'; -$fa-var-hourglass-start: '\f251'; -$fa-var-houzz: '\f27c'; -$fa-var-html5: '\f13b'; -$fa-var-i-cursor: '\f246'; -$fa-var-id-badge: '\f2c1'; -$fa-var-id-card: '\f2c2'; -$fa-var-id-card-o: '\f2c3'; -$fa-var-ils: '\f20b'; -$fa-var-image: '\f03e'; -$fa-var-imdb: '\f2d8'; -$fa-var-inbox: '\f01c'; -$fa-var-indent: '\f03c'; -$fa-var-industry: '\f275'; -$fa-var-info: '\f129'; -$fa-var-info-circle: '\f05a'; -$fa-var-inr: '\f156'; -$fa-var-instagram: '\f16d'; -$fa-var-institution: '\f19c'; -$fa-var-internet-explorer: '\f26b'; -$fa-var-intersex: '\f224'; -$fa-var-ioxhost: '\f208'; -$fa-var-italic: '\f033'; -$fa-var-joomla: '\f1aa'; -$fa-var-jpy: '\f157'; -$fa-var-jsfiddle: '\f1cc'; -$fa-var-key: '\f084'; -$fa-var-keyboard-o: '\f11c'; -$fa-var-krw: '\f159'; -$fa-var-language: '\f1ab'; -$fa-var-laptop: '\f109'; -$fa-var-lastfm: '\f202'; -$fa-var-lastfm-square: '\f203'; -$fa-var-leaf: '\f06c'; -$fa-var-leanpub: '\f212'; -$fa-var-legal: '\f0e3'; -$fa-var-lemon-o: '\f094'; -$fa-var-level-down: '\f149'; -$fa-var-level-up: '\f148'; -$fa-var-life-bouy: '\f1cd'; -$fa-var-life-buoy: '\f1cd'; -$fa-var-life-ring: '\f1cd'; -$fa-var-life-saver: '\f1cd'; -$fa-var-lightbulb-o: '\f0eb'; -$fa-var-line-chart: '\f201'; -$fa-var-link: '\f0c1'; -$fa-var-linkedin: '\f0e1'; -$fa-var-linkedin-square: '\f08c'; -$fa-var-linode: '\f2b8'; -$fa-var-linux: '\f17c'; -$fa-var-list: '\f03a'; -$fa-var-list-alt: '\f022'; -$fa-var-list-ol: '\f0cb'; -$fa-var-list-ul: '\f0ca'; -$fa-var-location-arrow: '\f124'; -$fa-var-lock: '\f023'; -$fa-var-long-arrow-down: '\f175'; -$fa-var-long-arrow-left: '\f177'; -$fa-var-long-arrow-right: '\f178'; -$fa-var-long-arrow-up: '\f176'; -$fa-var-low-vision: '\f2a8'; -$fa-var-magic: '\f0d0'; -$fa-var-magnet: '\f076'; -$fa-var-mail-forward: '\f064'; -$fa-var-mail-reply: '\f112'; -$fa-var-mail-reply-all: '\f122'; -$fa-var-male: '\f183'; -$fa-var-map: '\f279'; -$fa-var-map-marker: '\f041'; -$fa-var-map-o: '\f278'; -$fa-var-map-pin: '\f276'; -$fa-var-map-signs: '\f277'; -$fa-var-mars: '\f222'; -$fa-var-mars-double: '\f227'; -$fa-var-mars-stroke: '\f229'; -$fa-var-mars-stroke-h: '\f22b'; -$fa-var-mars-stroke-v: '\f22a'; -$fa-var-maxcdn: '\f136'; -$fa-var-meanpath: '\f20c'; -$fa-var-medium: '\f23a'; -$fa-var-medkit: '\f0fa'; -$fa-var-meetup: '\f2e0'; -$fa-var-meh-o: '\f11a'; -$fa-var-mercury: '\f223'; -$fa-var-microchip: '\f2db'; -$fa-var-microphone: '\f130'; -$fa-var-microphone-slash: '\f131'; -$fa-var-minus: '\f068'; -$fa-var-minus-circle: '\f056'; -$fa-var-minus-square: '\f146'; -$fa-var-minus-square-o: '\f147'; -$fa-var-mixcloud: '\f289'; -$fa-var-mobile: '\f10b'; -$fa-var-mobile-phone: '\f10b'; -$fa-var-modx: '\f285'; -$fa-var-money: '\f0d6'; -$fa-var-moon-o: '\f186'; -$fa-var-mortar-board: '\f19d'; -$fa-var-motorcycle: '\f21c'; -$fa-var-mouse-pointer: '\f245'; -$fa-var-music: '\f001'; -$fa-var-navicon: '\f0c9'; -$fa-var-neuter: '\f22c'; -$fa-var-newspaper-o: '\f1ea'; -$fa-var-object-group: '\f247'; -$fa-var-object-ungroup: '\f248'; -$fa-var-odnoklassniki: '\f263'; -$fa-var-odnoklassniki-square: '\f264'; -$fa-var-opencart: '\f23d'; -$fa-var-openid: '\f19b'; -$fa-var-opera: '\f26a'; -$fa-var-optin-monster: '\f23c'; -$fa-var-outdent: '\f03b'; -$fa-var-pagelines: '\f18c'; -$fa-var-paint-brush: '\f1fc'; -$fa-var-paper-plane: '\f1d8'; -$fa-var-paper-plane-o: '\f1d9'; -$fa-var-paperclip: '\f0c6'; -$fa-var-paragraph: '\f1dd'; -$fa-var-paste: '\f0ea'; -$fa-var-pause: '\f04c'; -$fa-var-pause-circle: '\f28b'; -$fa-var-pause-circle-o: '\f28c'; -$fa-var-paw: '\f1b0'; -$fa-var-paypal: '\f1ed'; -$fa-var-pencil: '\f040'; -$fa-var-pencil-square: '\f14b'; -$fa-var-pencil-square-o: '\f044'; -$fa-var-percent: '\f295'; -$fa-var-phone: '\f095'; -$fa-var-phone-square: '\f098'; -$fa-var-photo: '\f03e'; -$fa-var-picture-o: '\f03e'; -$fa-var-pie-chart: '\f200'; -$fa-var-pied-piper: '\f2ae'; -$fa-var-pied-piper-alt: '\f1a8'; -$fa-var-pied-piper-pp: '\f1a7'; -$fa-var-pinterest: '\f0d2'; -$fa-var-pinterest-p: '\f231'; -$fa-var-pinterest-square: '\f0d3'; -$fa-var-plane: '\f072'; -$fa-var-play: '\f04b'; -$fa-var-play-circle: '\f144'; -$fa-var-play-circle-o: '\f01d'; -$fa-var-plug: '\f1e6'; -$fa-var-plus: '\f067'; -$fa-var-plus-circle: '\f055'; -$fa-var-plus-square: '\f0fe'; -$fa-var-plus-square-o: '\f196'; -$fa-var-podcast: '\f2ce'; -$fa-var-power-off: '\f011'; -$fa-var-print: '\f02f'; -$fa-var-product-hunt: '\f288'; -$fa-var-puzzle-piece: '\f12e'; -$fa-var-qq: '\f1d6'; -$fa-var-qrcode: '\f029'; -$fa-var-question: '\f128'; -$fa-var-question-circle: '\f059'; -$fa-var-question-circle-o: '\f29c'; -$fa-var-quora: '\f2c4'; -$fa-var-quote-left: '\f10d'; -$fa-var-quote-right: '\f10e'; -$fa-var-ra: '\f1d0'; -$fa-var-random: '\f074'; -$fa-var-ravelry: '\f2d9'; -$fa-var-rebel: '\f1d0'; -$fa-var-recycle: '\f1b8'; -$fa-var-reddit: '\f1a1'; -$fa-var-reddit-alien: '\f281'; -$fa-var-reddit-square: '\f1a2'; -$fa-var-refresh: '\f021'; -$fa-var-registered: '\f25d'; -$fa-var-remove: '\f00d'; -$fa-var-renren: '\f18b'; -$fa-var-reorder: '\f0c9'; -$fa-var-repeat: '\f01e'; -$fa-var-reply: '\f112'; -$fa-var-reply-all: '\f122'; -$fa-var-resistance: '\f1d0'; -$fa-var-retweet: '\f079'; -$fa-var-rmb: '\f157'; -$fa-var-road: '\f018'; -$fa-var-rocket: '\f135'; -$fa-var-rotate-left: '\f0e2'; -$fa-var-rotate-right: '\f01e'; -$fa-var-rouble: '\f158'; -$fa-var-rss: '\f09e'; -$fa-var-rss-square: '\f143'; -$fa-var-rub: '\f158'; -$fa-var-ruble: '\f158'; -$fa-var-rupee: '\f156'; -$fa-var-s15: '\f2cd'; -$fa-var-safari: '\f267'; -$fa-var-save: '\f0c7'; -$fa-var-scissors: '\f0c4'; -$fa-var-scribd: '\f28a'; -$fa-var-search: '\f002'; -$fa-var-search-minus: '\f010'; -$fa-var-search-plus: '\f00e'; -$fa-var-sellsy: '\f213'; -$fa-var-send: '\f1d8'; -$fa-var-send-o: '\f1d9'; -$fa-var-server: '\f233'; -$fa-var-share: '\f064'; -$fa-var-share-alt: '\f1e0'; -$fa-var-share-alt-square: '\f1e1'; -$fa-var-share-square: '\f14d'; -$fa-var-share-square-o: '\f045'; -$fa-var-shekel: '\f20b'; -$fa-var-sheqel: '\f20b'; -$fa-var-shield: '\f132'; -$fa-var-ship: '\f21a'; -$fa-var-shirtsinbulk: '\f214'; -$fa-var-shopping-bag: '\f290'; -$fa-var-shopping-basket: '\f291'; -$fa-var-shopping-cart: '\f07a'; -$fa-var-shower: '\f2cc'; -$fa-var-sign-in: '\f090'; -$fa-var-sign-language: '\f2a7'; -$fa-var-sign-out: '\f08b'; -$fa-var-signal: '\f012'; -$fa-var-signing: '\f2a7'; -$fa-var-simplybuilt: '\f215'; -$fa-var-sitemap: '\f0e8'; -$fa-var-skyatlas: '\f216'; -$fa-var-skype: '\f17e'; -$fa-var-slack: '\f198'; -$fa-var-sliders: '\f1de'; -$fa-var-slideshare: '\f1e7'; -$fa-var-smile-o: '\f118'; -$fa-var-snapchat: '\f2ab'; -$fa-var-snapchat-ghost: '\f2ac'; -$fa-var-snapchat-square: '\f2ad'; -$fa-var-snowflake-o: '\f2dc'; -$fa-var-soccer-ball-o: '\f1e3'; -$fa-var-sort: '\f0dc'; -$fa-var-sort-alpha-asc: '\f15d'; -$fa-var-sort-alpha-desc: '\f15e'; -$fa-var-sort-amount-asc: '\f160'; -$fa-var-sort-amount-desc: '\f161'; -$fa-var-sort-asc: '\f0de'; -$fa-var-sort-desc: '\f0dd'; -$fa-var-sort-down: '\f0dd'; -$fa-var-sort-numeric-asc: '\f162'; -$fa-var-sort-numeric-desc: '\f163'; -$fa-var-sort-up: '\f0de'; -$fa-var-soundcloud: '\f1be'; -$fa-var-space-shuttle: '\f197'; -$fa-var-spinner: '\f110'; -$fa-var-spoon: '\f1b1'; -$fa-var-spotify: '\f1bc'; -$fa-var-square: '\f0c8'; -$fa-var-square-o: '\f096'; -$fa-var-stack-exchange: '\f18d'; -$fa-var-stack-overflow: '\f16c'; -$fa-var-star: '\f005'; -$fa-var-star-half: '\f089'; -$fa-var-star-half-empty: '\f123'; -$fa-var-star-half-full: '\f123'; -$fa-var-star-half-o: '\f123'; -$fa-var-star-o: '\f006'; -$fa-var-steam: '\f1b6'; -$fa-var-steam-square: '\f1b7'; -$fa-var-step-backward: '\f048'; -$fa-var-step-forward: '\f051'; -$fa-var-stethoscope: '\f0f1'; -$fa-var-sticky-note: '\f249'; -$fa-var-sticky-note-o: '\f24a'; -$fa-var-stop: '\f04d'; -$fa-var-stop-circle: '\f28d'; -$fa-var-stop-circle-o: '\f28e'; -$fa-var-street-view: '\f21d'; -$fa-var-strikethrough: '\f0cc'; -$fa-var-stumbleupon: '\f1a4'; -$fa-var-stumbleupon-circle: '\f1a3'; -$fa-var-subscript: '\f12c'; -$fa-var-subway: '\f239'; -$fa-var-suitcase: '\f0f2'; -$fa-var-sun-o: '\f185'; -$fa-var-superpowers: '\f2dd'; -$fa-var-superscript: '\f12b'; -$fa-var-support: '\f1cd'; -$fa-var-table: '\f0ce'; -$fa-var-tablet: '\f10a'; -$fa-var-tachometer: '\f0e4'; -$fa-var-tag: '\f02b'; -$fa-var-tags: '\f02c'; -$fa-var-tasks: '\f0ae'; -$fa-var-taxi: '\f1ba'; -$fa-var-telegram: '\f2c6'; -$fa-var-television: '\f26c'; -$fa-var-tencent-weibo: '\f1d5'; -$fa-var-terminal: '\f120'; -$fa-var-text-height: '\f034'; -$fa-var-text-width: '\f035'; -$fa-var-th: '\f00a'; -$fa-var-th-large: '\f009'; -$fa-var-th-list: '\f00b'; -$fa-var-themeisle: '\f2b2'; -$fa-var-thermometer: '\f2c7'; -$fa-var-thermometer-0: '\f2cb'; -$fa-var-thermometer-1: '\f2ca'; -$fa-var-thermometer-2: '\f2c9'; -$fa-var-thermometer-3: '\f2c8'; -$fa-var-thermometer-4: '\f2c7'; -$fa-var-thermometer-empty: '\f2cb'; -$fa-var-thermometer-full: '\f2c7'; -$fa-var-thermometer-half: '\f2c9'; -$fa-var-thermometer-quarter: '\f2ca'; -$fa-var-thermometer-three-quarters: '\f2c8'; -$fa-var-thumb-tack: '\f08d'; -$fa-var-thumbs-down: '\f165'; -$fa-var-thumbs-o-down: '\f088'; -$fa-var-thumbs-o-up: '\f087'; -$fa-var-thumbs-up: '\f164'; -$fa-var-ticket: '\f145'; -$fa-var-times: '\f00d'; -$fa-var-times-circle: '\f057'; -$fa-var-times-circle-o: '\f05c'; -$fa-var-times-rectangle: '\f2d3'; -$fa-var-times-rectangle-o: '\f2d4'; -$fa-var-tint: '\f043'; -$fa-var-toggle-down: '\f150'; -$fa-var-toggle-left: '\f191'; -$fa-var-toggle-off: '\f204'; -$fa-var-toggle-on: '\f205'; -$fa-var-toggle-right: '\f152'; -$fa-var-toggle-up: '\f151'; -$fa-var-trademark: '\f25c'; -$fa-var-train: '\f238'; -$fa-var-transgender: '\f224'; -$fa-var-transgender-alt: '\f225'; -$fa-var-trash: '\f1f8'; -$fa-var-trash-o: '\f014'; -$fa-var-tree: '\f1bb'; -$fa-var-trello: '\f181'; -$fa-var-tripadvisor: '\f262'; -$fa-var-trophy: '\f091'; -$fa-var-truck: '\f0d1'; -$fa-var-try: '\f195'; -$fa-var-tty: '\f1e4'; -$fa-var-tumblr: '\f173'; -$fa-var-tumblr-square: '\f174'; -$fa-var-turkish-lira: '\f195'; -$fa-var-tv: '\f26c'; -$fa-var-twitch: '\f1e8'; -$fa-var-twitter: '\f099'; -$fa-var-twitter-square: '\f081'; -$fa-var-umbrella: '\f0e9'; -$fa-var-underline: '\f0cd'; -$fa-var-undo: '\f0e2'; -$fa-var-universal-access: '\f29a'; -$fa-var-university: '\f19c'; -$fa-var-unlink: '\f127'; -$fa-var-unlock: '\f09c'; -$fa-var-unlock-alt: '\f13e'; -$fa-var-unsorted: '\f0dc'; -$fa-var-upload: '\f093'; -$fa-var-usb: '\f287'; -$fa-var-usd: '\f155'; -$fa-var-user: '\f007'; -$fa-var-user-circle: '\f2bd'; -$fa-var-user-circle-o: '\f2be'; -$fa-var-user-md: '\f0f0'; -$fa-var-user-o: '\f2c0'; -$fa-var-user-plus: '\f234'; -$fa-var-user-secret: '\f21b'; -$fa-var-user-times: '\f235'; -$fa-var-users: '\f0c0'; -$fa-var-vcard: '\f2bb'; -$fa-var-vcard-o: '\f2bc'; -$fa-var-venus: '\f221'; -$fa-var-venus-double: '\f226'; -$fa-var-venus-mars: '\f228'; -$fa-var-viacoin: '\f237'; -$fa-var-viadeo: '\f2a9'; -$fa-var-viadeo-square: '\f2aa'; -$fa-var-video-camera: '\f03d'; -$fa-var-vimeo: '\f27d'; -$fa-var-vimeo-square: '\f194'; -$fa-var-vine: '\f1ca'; -$fa-var-vk: '\f189'; -$fa-var-volume-control-phone: '\f2a0'; -$fa-var-volume-down: '\f027'; -$fa-var-volume-off: '\f026'; -$fa-var-volume-up: '\f028'; -$fa-var-warning: '\f071'; -$fa-var-wechat: '\f1d7'; -$fa-var-weibo: '\f18a'; -$fa-var-weixin: '\f1d7'; -$fa-var-whatsapp: '\f232'; -$fa-var-wheelchair: '\f193'; -$fa-var-wheelchair-alt: '\f29b'; -$fa-var-wifi: '\f1eb'; -$fa-var-wikipedia-w: '\f266'; -$fa-var-window-close: '\f2d3'; -$fa-var-window-close-o: '\f2d4'; -$fa-var-window-maximize: '\f2d0'; -$fa-var-window-minimize: '\f2d1'; -$fa-var-window-restore: '\f2d2'; -$fa-var-windows: '\f17a'; -$fa-var-won: '\f159'; -$fa-var-wordpress: '\f19a'; -$fa-var-wpbeginner: '\f297'; -$fa-var-wpexplorer: '\f2de'; -$fa-var-wpforms: '\f298'; -$fa-var-wrench: '\f0ad'; -$fa-var-xing: '\f168'; -$fa-var-xing-square: '\f169'; -$fa-var-y-combinator: '\f23b'; -$fa-var-y-combinator-square: '\f1d4'; -$fa-var-yahoo: '\f19e'; -$fa-var-yc: '\f23b'; -$fa-var-yc-square: '\f1d4'; -$fa-var-yelp: '\f1e9'; -$fa-var-yen: '\f157'; -$fa-var-yoast: '\f2b1'; -$fa-var-youtube: '\f167'; -$fa-var-youtube-play: '\f16a'; -$fa-var-youtube-square: '\f166'; +$fa-var-500px: "\f26e"; +$fa-var-address-book: "\f2b9"; +$fa-var-address-book-o: "\f2ba"; +$fa-var-address-card: "\f2bb"; +$fa-var-address-card-o: "\f2bc"; +$fa-var-adjust: "\f042"; +$fa-var-adn: "\f170"; +$fa-var-align-center: "\f037"; +$fa-var-align-justify: "\f039"; +$fa-var-align-left: "\f036"; +$fa-var-align-right: "\f038"; +$fa-var-amazon: "\f270"; +$fa-var-ambulance: "\f0f9"; +$fa-var-american-sign-language-interpreting: "\f2a3"; +$fa-var-anchor: "\f13d"; +$fa-var-android: "\f17b"; +$fa-var-angellist: "\f209"; +$fa-var-angle-double-down: "\f103"; +$fa-var-angle-double-left: "\f100"; +$fa-var-angle-double-right: "\f101"; +$fa-var-angle-double-up: "\f102"; +$fa-var-angle-down: "\f107"; +$fa-var-angle-left: "\f104"; +$fa-var-angle-right: "\f105"; +$fa-var-angle-up: "\f106"; +$fa-var-apple: "\f179"; +$fa-var-archive: "\f187"; +$fa-var-area-chart: "\f1fe"; +$fa-var-arrow-circle-down: "\f0ab"; +$fa-var-arrow-circle-left: "\f0a8"; +$fa-var-arrow-circle-o-down: "\f01a"; +$fa-var-arrow-circle-o-left: "\f190"; +$fa-var-arrow-circle-o-right: "\f18e"; +$fa-var-arrow-circle-o-up: "\f01b"; +$fa-var-arrow-circle-right: "\f0a9"; +$fa-var-arrow-circle-up: "\f0aa"; +$fa-var-arrow-down: "\f063"; +$fa-var-arrow-left: "\f060"; +$fa-var-arrow-right: "\f061"; +$fa-var-arrow-up: "\f062"; +$fa-var-arrows: "\f047"; +$fa-var-arrows-alt: "\f0b2"; +$fa-var-arrows-h: "\f07e"; +$fa-var-arrows-v: "\f07d"; +$fa-var-asl-interpreting: "\f2a3"; +$fa-var-assistive-listening-systems: "\f2a2"; +$fa-var-asterisk: "\f069"; +$fa-var-at: "\f1fa"; +$fa-var-audio-description: "\f29e"; +$fa-var-automobile: "\f1b9"; +$fa-var-backward: "\f04a"; +$fa-var-balance-scale: "\f24e"; +$fa-var-ban: "\f05e"; +$fa-var-bandcamp: "\f2d5"; +$fa-var-bank: "\f19c"; +$fa-var-bar-chart: "\f080"; +$fa-var-bar-chart-o: "\f080"; +$fa-var-barcode: "\f02a"; +$fa-var-bars: "\f0c9"; +$fa-var-bath: "\f2cd"; +$fa-var-bathtub: "\f2cd"; +$fa-var-battery: "\f240"; +$fa-var-battery-0: "\f244"; +$fa-var-battery-1: "\f243"; +$fa-var-battery-2: "\f242"; +$fa-var-battery-3: "\f241"; +$fa-var-battery-4: "\f240"; +$fa-var-battery-empty: "\f244"; +$fa-var-battery-full: "\f240"; +$fa-var-battery-half: "\f242"; +$fa-var-battery-quarter: "\f243"; +$fa-var-battery-three-quarters: "\f241"; +$fa-var-bed: "\f236"; +$fa-var-beer: "\f0fc"; +$fa-var-behance: "\f1b4"; +$fa-var-behance-square: "\f1b5"; +$fa-var-bell: "\f0f3"; +$fa-var-bell-o: "\f0a2"; +$fa-var-bell-slash: "\f1f6"; +$fa-var-bell-slash-o: "\f1f7"; +$fa-var-bicycle: "\f206"; +$fa-var-binoculars: "\f1e5"; +$fa-var-birthday-cake: "\f1fd"; +$fa-var-bitbucket: "\f171"; +$fa-var-bitbucket-square: "\f172"; +$fa-var-bitcoin: "\f15a"; +$fa-var-black-tie: "\f27e"; +$fa-var-blind: "\f29d"; +$fa-var-bluetooth: "\f293"; +$fa-var-bluetooth-b: "\f294"; +$fa-var-bold: "\f032"; +$fa-var-bolt: "\f0e7"; +$fa-var-bomb: "\f1e2"; +$fa-var-book: "\f02d"; +$fa-var-bookmark: "\f02e"; +$fa-var-bookmark-o: "\f097"; +$fa-var-braille: "\f2a1"; +$fa-var-briefcase: "\f0b1"; +$fa-var-btc: "\f15a"; +$fa-var-bug: "\f188"; +$fa-var-building: "\f1ad"; +$fa-var-building-o: "\f0f7"; +$fa-var-bullhorn: "\f0a1"; +$fa-var-bullseye: "\f140"; +$fa-var-bus: "\f207"; +$fa-var-buysellads: "\f20d"; +$fa-var-cab: "\f1ba"; +$fa-var-calculator: "\f1ec"; +$fa-var-calendar: "\f073"; +$fa-var-calendar-check-o: "\f274"; +$fa-var-calendar-minus-o: "\f272"; +$fa-var-calendar-o: "\f133"; +$fa-var-calendar-plus-o: "\f271"; +$fa-var-calendar-times-o: "\f273"; +$fa-var-camera: "\f030"; +$fa-var-camera-retro: "\f083"; +$fa-var-car: "\f1b9"; +$fa-var-caret-down: "\f0d7"; +$fa-var-caret-left: "\f0d9"; +$fa-var-caret-right: "\f0da"; +$fa-var-caret-square-o-down: "\f150"; +$fa-var-caret-square-o-left: "\f191"; +$fa-var-caret-square-o-right: "\f152"; +$fa-var-caret-square-o-up: "\f151"; +$fa-var-caret-up: "\f0d8"; +$fa-var-cart-arrow-down: "\f218"; +$fa-var-cart-plus: "\f217"; +$fa-var-cc: "\f20a"; +$fa-var-cc-amex: "\f1f3"; +$fa-var-cc-diners-club: "\f24c"; +$fa-var-cc-discover: "\f1f2"; +$fa-var-cc-jcb: "\f24b"; +$fa-var-cc-mastercard: "\f1f1"; +$fa-var-cc-paypal: "\f1f4"; +$fa-var-cc-stripe: "\f1f5"; +$fa-var-cc-visa: "\f1f0"; +$fa-var-certificate: "\f0a3"; +$fa-var-chain: "\f0c1"; +$fa-var-chain-broken: "\f127"; +$fa-var-check: "\f00c"; +$fa-var-check-circle: "\f058"; +$fa-var-check-circle-o: "\f05d"; +$fa-var-check-square: "\f14a"; +$fa-var-check-square-o: "\f046"; +$fa-var-chevron-circle-down: "\f13a"; +$fa-var-chevron-circle-left: "\f137"; +$fa-var-chevron-circle-right: "\f138"; +$fa-var-chevron-circle-up: "\f139"; +$fa-var-chevron-down: "\f078"; +$fa-var-chevron-left: "\f053"; +$fa-var-chevron-right: "\f054"; +$fa-var-chevron-up: "\f077"; +$fa-var-child: "\f1ae"; +$fa-var-chrome: "\f268"; +$fa-var-circle: "\f111"; +$fa-var-circle-o: "\f10c"; +$fa-var-circle-o-notch: "\f1ce"; +$fa-var-circle-thin: "\f1db"; +$fa-var-clipboard: "\f0ea"; +$fa-var-clock-o: "\f017"; +$fa-var-clone: "\f24d"; +$fa-var-close: "\f00d"; +$fa-var-cloud: "\f0c2"; +$fa-var-cloud-download: "\f0ed"; +$fa-var-cloud-upload: "\f0ee"; +$fa-var-cny: "\f157"; +$fa-var-code: "\f121"; +$fa-var-code-fork: "\f126"; +$fa-var-codepen: "\f1cb"; +$fa-var-codiepie: "\f284"; +$fa-var-coffee: "\f0f4"; +$fa-var-cog: "\f013"; +$fa-var-cogs: "\f085"; +$fa-var-columns: "\f0db"; +$fa-var-comment: "\f075"; +$fa-var-comment-o: "\f0e5"; +$fa-var-commenting: "\f27a"; +$fa-var-commenting-o: "\f27b"; +$fa-var-comments: "\f086"; +$fa-var-comments-o: "\f0e6"; +$fa-var-compass: "\f14e"; +$fa-var-compress: "\f066"; +$fa-var-connectdevelop: "\f20e"; +$fa-var-contao: "\f26d"; +$fa-var-copy: "\f0c5"; +$fa-var-copyright: "\f1f9"; +$fa-var-creative-commons: "\f25e"; +$fa-var-credit-card: "\f09d"; +$fa-var-credit-card-alt: "\f283"; +$fa-var-crop: "\f125"; +$fa-var-crosshairs: "\f05b"; +$fa-var-css3: "\f13c"; +$fa-var-cube: "\f1b2"; +$fa-var-cubes: "\f1b3"; +$fa-var-cut: "\f0c4"; +$fa-var-cutlery: "\f0f5"; +$fa-var-dashboard: "\f0e4"; +$fa-var-dashcube: "\f210"; +$fa-var-database: "\f1c0"; +$fa-var-deaf: "\f2a4"; +$fa-var-deafness: "\f2a4"; +$fa-var-dedent: "\f03b"; +$fa-var-delicious: "\f1a5"; +$fa-var-desktop: "\f108"; +$fa-var-deviantart: "\f1bd"; +$fa-var-diamond: "\f219"; +$fa-var-digg: "\f1a6"; +$fa-var-dollar: "\f155"; +$fa-var-dot-circle-o: "\f192"; +$fa-var-download: "\f019"; +$fa-var-dribbble: "\f17d"; +$fa-var-drivers-license: "\f2c2"; +$fa-var-drivers-license-o: "\f2c3"; +$fa-var-dropbox: "\f16b"; +$fa-var-drupal: "\f1a9"; +$fa-var-edge: "\f282"; +$fa-var-edit: "\f044"; +$fa-var-eercast: "\f2da"; +$fa-var-eject: "\f052"; +$fa-var-ellipsis-h: "\f141"; +$fa-var-ellipsis-v: "\f142"; +$fa-var-empire: "\f1d1"; +$fa-var-envelope: "\f0e0"; +$fa-var-envelope-o: "\f003"; +$fa-var-envelope-open: "\f2b6"; +$fa-var-envelope-open-o: "\f2b7"; +$fa-var-envelope-square: "\f199"; +$fa-var-envira: "\f299"; +$fa-var-eraser: "\f12d"; +$fa-var-etsy: "\f2d7"; +$fa-var-eur: "\f153"; +$fa-var-euro: "\f153"; +$fa-var-exchange: "\f0ec"; +$fa-var-exclamation: "\f12a"; +$fa-var-exclamation-circle: "\f06a"; +$fa-var-exclamation-triangle: "\f071"; +$fa-var-expand: "\f065"; +$fa-var-expeditedssl: "\f23e"; +$fa-var-external-link: "\f08e"; +$fa-var-external-link-square: "\f14c"; +$fa-var-eye: "\f06e"; +$fa-var-eye-slash: "\f070"; +$fa-var-eyedropper: "\f1fb"; +$fa-var-fa: "\f2b4"; +$fa-var-facebook: "\f09a"; +$fa-var-facebook-f: "\f09a"; +$fa-var-facebook-official: "\f230"; +$fa-var-facebook-square: "\f082"; +$fa-var-fast-backward: "\f049"; +$fa-var-fast-forward: "\f050"; +$fa-var-fax: "\f1ac"; +$fa-var-feed: "\f09e"; +$fa-var-female: "\f182"; +$fa-var-fighter-jet: "\f0fb"; +$fa-var-file: "\f15b"; +$fa-var-file-archive-o: "\f1c6"; +$fa-var-file-audio-o: "\f1c7"; +$fa-var-file-code-o: "\f1c9"; +$fa-var-file-excel-o: "\f1c3"; +$fa-var-file-image-o: "\f1c5"; +$fa-var-file-movie-o: "\f1c8"; +$fa-var-file-o: "\f016"; +$fa-var-file-pdf-o: "\f1c1"; +$fa-var-file-photo-o: "\f1c5"; +$fa-var-file-picture-o: "\f1c5"; +$fa-var-file-powerpoint-o: "\f1c4"; +$fa-var-file-sound-o: "\f1c7"; +$fa-var-file-text: "\f15c"; +$fa-var-file-text-o: "\f0f6"; +$fa-var-file-video-o: "\f1c8"; +$fa-var-file-word-o: "\f1c2"; +$fa-var-file-zip-o: "\f1c6"; +$fa-var-files-o: "\f0c5"; +$fa-var-film: "\f008"; +$fa-var-filter: "\f0b0"; +$fa-var-fire: "\f06d"; +$fa-var-fire-extinguisher: "\f134"; +$fa-var-firefox: "\f269"; +$fa-var-first-order: "\f2b0"; +$fa-var-flag: "\f024"; +$fa-var-flag-checkered: "\f11e"; +$fa-var-flag-o: "\f11d"; +$fa-var-flash: "\f0e7"; +$fa-var-flask: "\f0c3"; +$fa-var-flickr: "\f16e"; +$fa-var-floppy-o: "\f0c7"; +$fa-var-folder: "\f07b"; +$fa-var-folder-o: "\f114"; +$fa-var-folder-open: "\f07c"; +$fa-var-folder-open-o: "\f115"; +$fa-var-font: "\f031"; +$fa-var-font-awesome: "\f2b4"; +$fa-var-fonticons: "\f280"; +$fa-var-fort-awesome: "\f286"; +$fa-var-forumbee: "\f211"; +$fa-var-forward: "\f04e"; +$fa-var-foursquare: "\f180"; +$fa-var-free-code-camp: "\f2c5"; +$fa-var-frown-o: "\f119"; +$fa-var-futbol-o: "\f1e3"; +$fa-var-gamepad: "\f11b"; +$fa-var-gavel: "\f0e3"; +$fa-var-gbp: "\f154"; +$fa-var-ge: "\f1d1"; +$fa-var-gear: "\f013"; +$fa-var-gears: "\f085"; +$fa-var-genderless: "\f22d"; +$fa-var-get-pocket: "\f265"; +$fa-var-gg: "\f260"; +$fa-var-gg-circle: "\f261"; +$fa-var-gift: "\f06b"; +$fa-var-git: "\f1d3"; +$fa-var-git-square: "\f1d2"; +$fa-var-github: "\f09b"; +$fa-var-github-alt: "\f113"; +$fa-var-github-square: "\f092"; +$fa-var-gitlab: "\f296"; +$fa-var-gittip: "\f184"; +$fa-var-glass: "\f000"; +$fa-var-glide: "\f2a5"; +$fa-var-glide-g: "\f2a6"; +$fa-var-globe: "\f0ac"; +$fa-var-google: "\f1a0"; +$fa-var-google-plus: "\f0d5"; +$fa-var-google-plus-circle: "\f2b3"; +$fa-var-google-plus-official: "\f2b3"; +$fa-var-google-plus-square: "\f0d4"; +$fa-var-google-wallet: "\f1ee"; +$fa-var-graduation-cap: "\f19d"; +$fa-var-gratipay: "\f184"; +$fa-var-grav: "\f2d6"; +$fa-var-group: "\f0c0"; +$fa-var-h-square: "\f0fd"; +$fa-var-hacker-news: "\f1d4"; +$fa-var-hand-grab-o: "\f255"; +$fa-var-hand-lizard-o: "\f258"; +$fa-var-hand-o-down: "\f0a7"; +$fa-var-hand-o-left: "\f0a5"; +$fa-var-hand-o-right: "\f0a4"; +$fa-var-hand-o-up: "\f0a6"; +$fa-var-hand-paper-o: "\f256"; +$fa-var-hand-peace-o: "\f25b"; +$fa-var-hand-pointer-o: "\f25a"; +$fa-var-hand-rock-o: "\f255"; +$fa-var-hand-scissors-o: "\f257"; +$fa-var-hand-spock-o: "\f259"; +$fa-var-hand-stop-o: "\f256"; +$fa-var-handshake-o: "\f2b5"; +$fa-var-hard-of-hearing: "\f2a4"; +$fa-var-hashtag: "\f292"; +$fa-var-hdd-o: "\f0a0"; +$fa-var-header: "\f1dc"; +$fa-var-headphones: "\f025"; +$fa-var-heart: "\f004"; +$fa-var-heart-o: "\f08a"; +$fa-var-heartbeat: "\f21e"; +$fa-var-history: "\f1da"; +$fa-var-home: "\f015"; +$fa-var-hospital-o: "\f0f8"; +$fa-var-hotel: "\f236"; +$fa-var-hourglass: "\f254"; +$fa-var-hourglass-1: "\f251"; +$fa-var-hourglass-2: "\f252"; +$fa-var-hourglass-3: "\f253"; +$fa-var-hourglass-end: "\f253"; +$fa-var-hourglass-half: "\f252"; +$fa-var-hourglass-o: "\f250"; +$fa-var-hourglass-start: "\f251"; +$fa-var-houzz: "\f27c"; +$fa-var-html5: "\f13b"; +$fa-var-i-cursor: "\f246"; +$fa-var-id-badge: "\f2c1"; +$fa-var-id-card: "\f2c2"; +$fa-var-id-card-o: "\f2c3"; +$fa-var-ils: "\f20b"; +$fa-var-image: "\f03e"; +$fa-var-imdb: "\f2d8"; +$fa-var-inbox: "\f01c"; +$fa-var-indent: "\f03c"; +$fa-var-industry: "\f275"; +$fa-var-info: "\f129"; +$fa-var-info-circle: "\f05a"; +$fa-var-inr: "\f156"; +$fa-var-instagram: "\f16d"; +$fa-var-institution: "\f19c"; +$fa-var-internet-explorer: "\f26b"; +$fa-var-intersex: "\f224"; +$fa-var-ioxhost: "\f208"; +$fa-var-italic: "\f033"; +$fa-var-joomla: "\f1aa"; +$fa-var-jpy: "\f157"; +$fa-var-jsfiddle: "\f1cc"; +$fa-var-key: "\f084"; +$fa-var-keyboard-o: "\f11c"; +$fa-var-krw: "\f159"; +$fa-var-language: "\f1ab"; +$fa-var-laptop: "\f109"; +$fa-var-lastfm: "\f202"; +$fa-var-lastfm-square: "\f203"; +$fa-var-leaf: "\f06c"; +$fa-var-leanpub: "\f212"; +$fa-var-legal: "\f0e3"; +$fa-var-lemon-o: "\f094"; +$fa-var-level-down: "\f149"; +$fa-var-level-up: "\f148"; +$fa-var-life-bouy: "\f1cd"; +$fa-var-life-buoy: "\f1cd"; +$fa-var-life-ring: "\f1cd"; +$fa-var-life-saver: "\f1cd"; +$fa-var-lightbulb-o: "\f0eb"; +$fa-var-line-chart: "\f201"; +$fa-var-link: "\f0c1"; +$fa-var-linkedin: "\f0e1"; +$fa-var-linkedin-square: "\f08c"; +$fa-var-linode: "\f2b8"; +$fa-var-linux: "\f17c"; +$fa-var-list: "\f03a"; +$fa-var-list-alt: "\f022"; +$fa-var-list-ol: "\f0cb"; +$fa-var-list-ul: "\f0ca"; +$fa-var-location-arrow: "\f124"; +$fa-var-lock: "\f023"; +$fa-var-long-arrow-down: "\f175"; +$fa-var-long-arrow-left: "\f177"; +$fa-var-long-arrow-right: "\f178"; +$fa-var-long-arrow-up: "\f176"; +$fa-var-low-vision: "\f2a8"; +$fa-var-magic: "\f0d0"; +$fa-var-magnet: "\f076"; +$fa-var-mail-forward: "\f064"; +$fa-var-mail-reply: "\f112"; +$fa-var-mail-reply-all: "\f122"; +$fa-var-male: "\f183"; +$fa-var-map: "\f279"; +$fa-var-map-marker: "\f041"; +$fa-var-map-o: "\f278"; +$fa-var-map-pin: "\f276"; +$fa-var-map-signs: "\f277"; +$fa-var-mars: "\f222"; +$fa-var-mars-double: "\f227"; +$fa-var-mars-stroke: "\f229"; +$fa-var-mars-stroke-h: "\f22b"; +$fa-var-mars-stroke-v: "\f22a"; +$fa-var-maxcdn: "\f136"; +$fa-var-meanpath: "\f20c"; +$fa-var-medium: "\f23a"; +$fa-var-medkit: "\f0fa"; +$fa-var-meetup: "\f2e0"; +$fa-var-meh-o: "\f11a"; +$fa-var-mercury: "\f223"; +$fa-var-microchip: "\f2db"; +$fa-var-microphone: "\f130"; +$fa-var-microphone-slash: "\f131"; +$fa-var-minus: "\f068"; +$fa-var-minus-circle: "\f056"; +$fa-var-minus-square: "\f146"; +$fa-var-minus-square-o: "\f147"; +$fa-var-mixcloud: "\f289"; +$fa-var-mobile: "\f10b"; +$fa-var-mobile-phone: "\f10b"; +$fa-var-modx: "\f285"; +$fa-var-money: "\f0d6"; +$fa-var-moon-o: "\f186"; +$fa-var-mortar-board: "\f19d"; +$fa-var-motorcycle: "\f21c"; +$fa-var-mouse-pointer: "\f245"; +$fa-var-music: "\f001"; +$fa-var-navicon: "\f0c9"; +$fa-var-neuter: "\f22c"; +$fa-var-newspaper-o: "\f1ea"; +$fa-var-object-group: "\f247"; +$fa-var-object-ungroup: "\f248"; +$fa-var-odnoklassniki: "\f263"; +$fa-var-odnoklassniki-square: "\f264"; +$fa-var-opencart: "\f23d"; +$fa-var-openid: "\f19b"; +$fa-var-opera: "\f26a"; +$fa-var-optin-monster: "\f23c"; +$fa-var-outdent: "\f03b"; +$fa-var-pagelines: "\f18c"; +$fa-var-paint-brush: "\f1fc"; +$fa-var-paper-plane: "\f1d8"; +$fa-var-paper-plane-o: "\f1d9"; +$fa-var-paperclip: "\f0c6"; +$fa-var-paragraph: "\f1dd"; +$fa-var-paste: "\f0ea"; +$fa-var-pause: "\f04c"; +$fa-var-pause-circle: "\f28b"; +$fa-var-pause-circle-o: "\f28c"; +$fa-var-paw: "\f1b0"; +$fa-var-paypal: "\f1ed"; +$fa-var-pencil: "\f040"; +$fa-var-pencil-square: "\f14b"; +$fa-var-pencil-square-o: "\f044"; +$fa-var-percent: "\f295"; +$fa-var-phone: "\f095"; +$fa-var-phone-square: "\f098"; +$fa-var-photo: "\f03e"; +$fa-var-picture-o: "\f03e"; +$fa-var-pie-chart: "\f200"; +$fa-var-pied-piper: "\f2ae"; +$fa-var-pied-piper-alt: "\f1a8"; +$fa-var-pied-piper-pp: "\f1a7"; +$fa-var-pinterest: "\f0d2"; +$fa-var-pinterest-p: "\f231"; +$fa-var-pinterest-square: "\f0d3"; +$fa-var-plane: "\f072"; +$fa-var-play: "\f04b"; +$fa-var-play-circle: "\f144"; +$fa-var-play-circle-o: "\f01d"; +$fa-var-plug: "\f1e6"; +$fa-var-plus: "\f067"; +$fa-var-plus-circle: "\f055"; +$fa-var-plus-square: "\f0fe"; +$fa-var-plus-square-o: "\f196"; +$fa-var-podcast: "\f2ce"; +$fa-var-power-off: "\f011"; +$fa-var-print: "\f02f"; +$fa-var-product-hunt: "\f288"; +$fa-var-puzzle-piece: "\f12e"; +$fa-var-qq: "\f1d6"; +$fa-var-qrcode: "\f029"; +$fa-var-question: "\f128"; +$fa-var-question-circle: "\f059"; +$fa-var-question-circle-o: "\f29c"; +$fa-var-quora: "\f2c4"; +$fa-var-quote-left: "\f10d"; +$fa-var-quote-right: "\f10e"; +$fa-var-ra: "\f1d0"; +$fa-var-random: "\f074"; +$fa-var-ravelry: "\f2d9"; +$fa-var-rebel: "\f1d0"; +$fa-var-recycle: "\f1b8"; +$fa-var-reddit: "\f1a1"; +$fa-var-reddit-alien: "\f281"; +$fa-var-reddit-square: "\f1a2"; +$fa-var-refresh: "\f021"; +$fa-var-registered: "\f25d"; +$fa-var-remove: "\f00d"; +$fa-var-renren: "\f18b"; +$fa-var-reorder: "\f0c9"; +$fa-var-repeat: "\f01e"; +$fa-var-reply: "\f112"; +$fa-var-reply-all: "\f122"; +$fa-var-resistance: "\f1d0"; +$fa-var-retweet: "\f079"; +$fa-var-rmb: "\f157"; +$fa-var-road: "\f018"; +$fa-var-rocket: "\f135"; +$fa-var-rotate-left: "\f0e2"; +$fa-var-rotate-right: "\f01e"; +$fa-var-rouble: "\f158"; +$fa-var-rss: "\f09e"; +$fa-var-rss-square: "\f143"; +$fa-var-rub: "\f158"; +$fa-var-ruble: "\f158"; +$fa-var-rupee: "\f156"; +$fa-var-s15: "\f2cd"; +$fa-var-safari: "\f267"; +$fa-var-save: "\f0c7"; +$fa-var-scissors: "\f0c4"; +$fa-var-scribd: "\f28a"; +$fa-var-search: "\f002"; +$fa-var-search-minus: "\f010"; +$fa-var-search-plus: "\f00e"; +$fa-var-sellsy: "\f213"; +$fa-var-send: "\f1d8"; +$fa-var-send-o: "\f1d9"; +$fa-var-server: "\f233"; +$fa-var-share: "\f064"; +$fa-var-share-alt: "\f1e0"; +$fa-var-share-alt-square: "\f1e1"; +$fa-var-share-square: "\f14d"; +$fa-var-share-square-o: "\f045"; +$fa-var-shekel: "\f20b"; +$fa-var-sheqel: "\f20b"; +$fa-var-shield: "\f132"; +$fa-var-ship: "\f21a"; +$fa-var-shirtsinbulk: "\f214"; +$fa-var-shopping-bag: "\f290"; +$fa-var-shopping-basket: "\f291"; +$fa-var-shopping-cart: "\f07a"; +$fa-var-shower: "\f2cc"; +$fa-var-sign-in: "\f090"; +$fa-var-sign-language: "\f2a7"; +$fa-var-sign-out: "\f08b"; +$fa-var-signal: "\f012"; +$fa-var-signing: "\f2a7"; +$fa-var-simplybuilt: "\f215"; +$fa-var-sitemap: "\f0e8"; +$fa-var-skyatlas: "\f216"; +$fa-var-skype: "\f17e"; +$fa-var-slack: "\f198"; +$fa-var-sliders: "\f1de"; +$fa-var-slideshare: "\f1e7"; +$fa-var-smile-o: "\f118"; +$fa-var-snapchat: "\f2ab"; +$fa-var-snapchat-ghost: "\f2ac"; +$fa-var-snapchat-square: "\f2ad"; +$fa-var-snowflake-o: "\f2dc"; +$fa-var-soccer-ball-o: "\f1e3"; +$fa-var-sort: "\f0dc"; +$fa-var-sort-alpha-asc: "\f15d"; +$fa-var-sort-alpha-desc: "\f15e"; +$fa-var-sort-amount-asc: "\f160"; +$fa-var-sort-amount-desc: "\f161"; +$fa-var-sort-asc: "\f0de"; +$fa-var-sort-desc: "\f0dd"; +$fa-var-sort-down: "\f0dd"; +$fa-var-sort-numeric-asc: "\f162"; +$fa-var-sort-numeric-desc: "\f163"; +$fa-var-sort-up: "\f0de"; +$fa-var-soundcloud: "\f1be"; +$fa-var-space-shuttle: "\f197"; +$fa-var-spinner: "\f110"; +$fa-var-spoon: "\f1b1"; +$fa-var-spotify: "\f1bc"; +$fa-var-square: "\f0c8"; +$fa-var-square-o: "\f096"; +$fa-var-stack-exchange: "\f18d"; +$fa-var-stack-overflow: "\f16c"; +$fa-var-star: "\f005"; +$fa-var-star-half: "\f089"; +$fa-var-star-half-empty: "\f123"; +$fa-var-star-half-full: "\f123"; +$fa-var-star-half-o: "\f123"; +$fa-var-star-o: "\f006"; +$fa-var-steam: "\f1b6"; +$fa-var-steam-square: "\f1b7"; +$fa-var-step-backward: "\f048"; +$fa-var-step-forward: "\f051"; +$fa-var-stethoscope: "\f0f1"; +$fa-var-sticky-note: "\f249"; +$fa-var-sticky-note-o: "\f24a"; +$fa-var-stop: "\f04d"; +$fa-var-stop-circle: "\f28d"; +$fa-var-stop-circle-o: "\f28e"; +$fa-var-street-view: "\f21d"; +$fa-var-strikethrough: "\f0cc"; +$fa-var-stumbleupon: "\f1a4"; +$fa-var-stumbleupon-circle: "\f1a3"; +$fa-var-subscript: "\f12c"; +$fa-var-subway: "\f239"; +$fa-var-suitcase: "\f0f2"; +$fa-var-sun-o: "\f185"; +$fa-var-superpowers: "\f2dd"; +$fa-var-superscript: "\f12b"; +$fa-var-support: "\f1cd"; +$fa-var-table: "\f0ce"; +$fa-var-tablet: "\f10a"; +$fa-var-tachometer: "\f0e4"; +$fa-var-tag: "\f02b"; +$fa-var-tags: "\f02c"; +$fa-var-tasks: "\f0ae"; +$fa-var-taxi: "\f1ba"; +$fa-var-telegram: "\f2c6"; +$fa-var-television: "\f26c"; +$fa-var-tencent-weibo: "\f1d5"; +$fa-var-terminal: "\f120"; +$fa-var-text-height: "\f034"; +$fa-var-text-width: "\f035"; +$fa-var-th: "\f00a"; +$fa-var-th-large: "\f009"; +$fa-var-th-list: "\f00b"; +$fa-var-themeisle: "\f2b2"; +$fa-var-thermometer: "\f2c7"; +$fa-var-thermometer-0: "\f2cb"; +$fa-var-thermometer-1: "\f2ca"; +$fa-var-thermometer-2: "\f2c9"; +$fa-var-thermometer-3: "\f2c8"; +$fa-var-thermometer-4: "\f2c7"; +$fa-var-thermometer-empty: "\f2cb"; +$fa-var-thermometer-full: "\f2c7"; +$fa-var-thermometer-half: "\f2c9"; +$fa-var-thermometer-quarter: "\f2ca"; +$fa-var-thermometer-three-quarters: "\f2c8"; +$fa-var-thumb-tack: "\f08d"; +$fa-var-thumbs-down: "\f165"; +$fa-var-thumbs-o-down: "\f088"; +$fa-var-thumbs-o-up: "\f087"; +$fa-var-thumbs-up: "\f164"; +$fa-var-ticket: "\f145"; +$fa-var-times: "\f00d"; +$fa-var-times-circle: "\f057"; +$fa-var-times-circle-o: "\f05c"; +$fa-var-times-rectangle: "\f2d3"; +$fa-var-times-rectangle-o: "\f2d4"; +$fa-var-tint: "\f043"; +$fa-var-toggle-down: "\f150"; +$fa-var-toggle-left: "\f191"; +$fa-var-toggle-off: "\f204"; +$fa-var-toggle-on: "\f205"; +$fa-var-toggle-right: "\f152"; +$fa-var-toggle-up: "\f151"; +$fa-var-trademark: "\f25c"; +$fa-var-train: "\f238"; +$fa-var-transgender: "\f224"; +$fa-var-transgender-alt: "\f225"; +$fa-var-trash: "\f1f8"; +$fa-var-trash-o: "\f014"; +$fa-var-tree: "\f1bb"; +$fa-var-trello: "\f181"; +$fa-var-tripadvisor: "\f262"; +$fa-var-trophy: "\f091"; +$fa-var-truck: "\f0d1"; +$fa-var-try: "\f195"; +$fa-var-tty: "\f1e4"; +$fa-var-tumblr: "\f173"; +$fa-var-tumblr-square: "\f174"; +$fa-var-turkish-lira: "\f195"; +$fa-var-tv: "\f26c"; +$fa-var-twitch: "\f1e8"; +$fa-var-twitter: "\f099"; +$fa-var-twitter-square: "\f081"; +$fa-var-umbrella: "\f0e9"; +$fa-var-underline: "\f0cd"; +$fa-var-undo: "\f0e2"; +$fa-var-universal-access: "\f29a"; +$fa-var-university: "\f19c"; +$fa-var-unlink: "\f127"; +$fa-var-unlock: "\f09c"; +$fa-var-unlock-alt: "\f13e"; +$fa-var-unsorted: "\f0dc"; +$fa-var-upload: "\f093"; +$fa-var-usb: "\f287"; +$fa-var-usd: "\f155"; +$fa-var-user: "\f007"; +$fa-var-user-circle: "\f2bd"; +$fa-var-user-circle-o: "\f2be"; +$fa-var-user-md: "\f0f0"; +$fa-var-user-o: "\f2c0"; +$fa-var-user-plus: "\f234"; +$fa-var-user-secret: "\f21b"; +$fa-var-user-times: "\f235"; +$fa-var-users: "\f0c0"; +$fa-var-vcard: "\f2bb"; +$fa-var-vcard-o: "\f2bc"; +$fa-var-venus: "\f221"; +$fa-var-venus-double: "\f226"; +$fa-var-venus-mars: "\f228"; +$fa-var-viacoin: "\f237"; +$fa-var-viadeo: "\f2a9"; +$fa-var-viadeo-square: "\f2aa"; +$fa-var-video-camera: "\f03d"; +$fa-var-vimeo: "\f27d"; +$fa-var-vimeo-square: "\f194"; +$fa-var-vine: "\f1ca"; +$fa-var-vk: "\f189"; +$fa-var-volume-control-phone: "\f2a0"; +$fa-var-volume-down: "\f027"; +$fa-var-volume-off: "\f026"; +$fa-var-volume-up: "\f028"; +$fa-var-warning: "\f071"; +$fa-var-wechat: "\f1d7"; +$fa-var-weibo: "\f18a"; +$fa-var-weixin: "\f1d7"; +$fa-var-whatsapp: "\f232"; +$fa-var-wheelchair: "\f193"; +$fa-var-wheelchair-alt: "\f29b"; +$fa-var-wifi: "\f1eb"; +$fa-var-wikipedia-w: "\f266"; +$fa-var-window-close: "\f2d3"; +$fa-var-window-close-o: "\f2d4"; +$fa-var-window-maximize: "\f2d0"; +$fa-var-window-minimize: "\f2d1"; +$fa-var-window-restore: "\f2d2"; +$fa-var-windows: "\f17a"; +$fa-var-won: "\f159"; +$fa-var-wordpress: "\f19a"; +$fa-var-wpbeginner: "\f297"; +$fa-var-wpexplorer: "\f2de"; +$fa-var-wpforms: "\f298"; +$fa-var-wrench: "\f0ad"; +$fa-var-xing: "\f168"; +$fa-var-xing-square: "\f169"; +$fa-var-y-combinator: "\f23b"; +$fa-var-y-combinator-square: "\f1d4"; +$fa-var-yahoo: "\f19e"; +$fa-var-yc: "\f23b"; +$fa-var-yc-square: "\f1d4"; +$fa-var-yelp: "\f1e9"; +$fa-var-yen: "\f157"; +$fa-var-yoast: "\f2b1"; +$fa-var-youtube: "\f167"; +$fa-var-youtube-play: "\f16a"; +$fa-var-youtube-square: "\f166"; diff --git a/public/sass/components/_drop.scss b/public/sass/components/_drop.scss index 8d9d4fc6b7d..6568414ed88 100644 --- a/public/sass/components/_drop.scss +++ b/public/sass/components/_drop.scss @@ -5,13 +5,13 @@ $useDropShadow: false; $attachmentOffset: 0%; $easing: cubic-bezier(0, 0, 0.265, 1); -@include drop-theme('error', $popover-error-bg, $popover-color); -@include drop-theme('popover', $popover-bg, $popover-color, $popover-border-color); -@include drop-theme('help', $popover-help-bg, $popover-help-color); +@include drop-theme("error", $popover-error-bg, $popover-color); +@include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color); +@include drop-theme("help", $popover-help-bg, $popover-help-color); -@include drop-animation-scale('drop', 'help', $attachmentOffset: $attachmentOffset, $easing: $easing); -@include drop-animation-scale('drop', 'error', $attachmentOffset: $attachmentOffset, $easing: $easing); -@include drop-animation-scale('drop', 'popover', $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing); +@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing); .drop-element { z-index: 10000; diff --git a/public/sass/components/_filter-list.scss b/public/sass/components/_filter-list.scss index 90d0a21c539..7713aa05ac2 100644 --- a/public/sass/components/_filter-list.scss +++ b/public/sass/components/_filter-list.scss @@ -67,17 +67,17 @@ text-transform: uppercase; &.online { - background-image: url('/img/online.svg'); + background-image: url("/img/online.svg"); color: $online; } &.warn { - background-image: url('/img/warn-tiny.svg'); + background-image: url("/img/warn-tiny.svg"); color: $warn; } &.critical { - background-image: url('/img/critical.svg'); + background-image: url("/img/critical.svg"); color: $critical; } } diff --git a/public/sass/components/_footer.scss b/public/sass/components/_footer.scss index cec6f820118..8b7d64e47fe 100644 --- a/public/sass/components/_footer.scss +++ b/public/sass/components/_footer.scss @@ -25,7 +25,7 @@ display: inline-block; padding-right: 2px; &::after { - content: ' | '; + content: " | "; padding-left: 2px; } } @@ -33,7 +33,7 @@ li:last-child { &::after { padding-left: 0; - content: ''; + content: ""; } } } diff --git a/public/sass/components/_json_explorer.scss b/public/sass/components/_json_explorer.scss index aa212cd2dab..2b1be8bd4f5 100644 --- a/public/sass/components/_json_explorer.scss +++ b/public/sass/components/_json_explorer.scss @@ -21,10 +21,10 @@ display: none; } &.json-formatter-object::after { - content: 'No properties'; + content: "No properties"; } &.json-formatter-array::after { - content: '[]'; + content: "[]"; } } } @@ -87,7 +87,7 @@ &::after { display: inline-block; transition: transform $json-explorer-rotate-time ease-in; - content: '►'; + content: "►"; } } diff --git a/public/sass/components/_jsontree.scss b/public/sass/components/_jsontree.scss index 0a0497a0627..665deda0f12 100644 --- a/public/sass/components/_jsontree.scss +++ b/public/sass/components/_jsontree.scss @@ -35,12 +35,12 @@ json-tree { color: $variable; padding: 5px 10px 5px 15px; &::after { - content: ':'; + content: ":"; } } json-node.expandable { &::before { - content: '\25b6'; + content: "\25b6"; position: absolute; left: 0px; font-size: 8px; diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 7b935e3707d..1fb3eda1834 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -52,7 +52,7 @@ $path-position: $marker-size-half - ($path-height / 2); &::after { right: -50%; - content: ''; + content: ""; display: block; position: absolute; z-index: 1; @@ -105,7 +105,7 @@ $path-position: $marker-size-half - ($path-height / 2); // change icon to check .icon-gf::before { - content: '\e604'; + content: "\e604"; } } .progress-text { diff --git a/public/sass/components/_row.scss b/public/sass/components/_row.scss index 9cd564a4edf..3c1465a30bc 100644 --- a/public/sass/components/_row.scss +++ b/public/sass/components/_row.scss @@ -69,7 +69,7 @@ cursor: move; width: 1rem; height: 100%; - background: url('../img/grab_dark.svg') no-repeat 50% 50%; + background: url("../img/grab_dark.svg") no-repeat 50% 50%; background-size: 8px; visibility: hidden; position: absolute; diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index 83e112648cf..b5f61872585 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -33,7 +33,7 @@ text-align: center; margin-right: 0.3rem; padding: 3px 5px; - font: 11px Consolas, 'Liberation Mono', Menlo, Courier, monospace; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; color: #555; vertical-align: middle; diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 11ab2e3554a..c7eb1914103 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -64,8 +64,8 @@ } input + label::before { - font-family: 'FontAwesome'; - content: '\f096'; // square-o + font-family: "FontAwesome"; + content: "\f096"; // square-o color: $text-color-weak; transition: transform 0.4s; backface-visibility: hidden; @@ -73,11 +73,11 @@ } input + label::after { - content: '\f046'; // check-square-o + content: "\f046"; // check-square-o color: $orange; text-shadow: $text-shadow-strong; - font-family: 'FontAwesome'; + font-family: "FontAwesome"; transition: transform 0.4s; transform: rotateY(180deg); backface-visibility: hidden; diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 44a116cd0a4..197d5892652 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -44,13 +44,18 @@ &::before { display: block; - content: ' '; + content: " "; position: absolute; left: 0; right: 0; height: 2px; top: 0; - background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); + background-image: linear-gradient( + to right, + #ffd500 0%, + #ff4400 99%, + #ff4400 100% + ); } } } diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index b9e39ae9e04..2d7a12c3d01 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -103,10 +103,10 @@ } .fa-chevron-left::before { - content: '\f053'; + content: "\f053"; } .fa-chevron-right::before { - content: '\f054'; + content: "\f054"; } .glyphicon-chevron-right { diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index eb354b219fc..e7e53382e0e 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -15,7 +15,7 @@ border: 1px solid $border-color; &:before { - content: ''; + content: ""; display: block; position: absolute; width: 0; @@ -88,7 +88,8 @@ left: $popover-arrow-size * 2; } - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-middle + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -98,7 +99,8 @@ } } - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-middle + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -108,7 +110,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-middle + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -118,7 +121,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-middle + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -129,7 +133,8 @@ } // Top and bottom corners - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-bottom + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -139,7 +144,8 @@ } } - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-bottom + .drop-content { margin-top: $popover-arrow-size; &:before { @@ -149,7 +155,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-top + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -159,7 +166,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-top + .drop-content { margin-bottom: $popover-arrow-size; &:before { @@ -170,7 +178,8 @@ } // Side corners - &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left .drop-content { + &.drop-element-attached-top.drop-element-attached-right.drop-target-attached-left + .drop-content { margin-right: $popover-arrow-size; &:before { @@ -180,7 +189,8 @@ } } - &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right .drop-content { + &.drop-element-attached-top.drop-element-attached-left.drop-target-attached-right + .drop-content { margin-left: $popover-arrow-size; &:before { @@ -190,7 +200,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left .drop-content { + &.drop-element-attached-bottom.drop-element-attached-right.drop-target-attached-left + .drop-content { margin-right: $popover-arrow-size; &:before { @@ -200,7 +211,8 @@ } } - &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right .drop-content { + &.drop-element-attached-bottom.drop-element-attached-left.drop-target-attached-right + .drop-content { margin-left: $popover-arrow-size; &:before { @@ -212,7 +224,7 @@ } } -@mixin drop-animation-scale($themePrefix: 'drop', $themeName: 'default', $attachmentOffset: 0, $easing: 'linear') { +@mixin drop-animation-scale($themePrefix: "drop", $themeName: "default", $attachmentOffset: 0, $easing: "linear") { .#{$themePrefix}-element.#{$themePrefix}-#{$themeName} { transform: translateZ(0); transition: opacity 100ms; @@ -235,16 +247,20 @@ } } // Centers and middles - &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-bottom.#{$themePrefix}-element-attached-center + .#{$themePrefix}-content { transform-origin: 50% calc(100% + #{$attachmentOffset}); } - &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-top.#{$themePrefix}-element-attached-center + .#{$themePrefix}-content { transform-origin: 50% (-$attachmentOffset); } - &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-right.#{$themePrefix}-element-attached-middle + .#{$themePrefix}-content { transform-origin: calc(100% + #{$attachmentOffset}) 50%; } - &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle .#{$themePrefix}-content { + &.#{$themePrefix}-element-attached-left.#{$themePrefix}-element-attached-middle + .#{$themePrefix}-content { transform-origin: -($attachmentOffset 50%); } // Top and bottom corners diff --git a/public/sass/mixins/_forms.scss b/public/sass/mixins/_forms.scss index 2f163e0f46b..ce488f0f636 100644 --- a/public/sass/mixins/_forms.scss +++ b/public/sass/mixins/_forms.scss @@ -41,7 +41,8 @@ &:focus { border-color: $input-border-focus; outline: none; - $shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 5px $input-box-shadow-focus; + $shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), + 0 0 5px $input-box-shadow-focus; @include box-shadow($shadow); } } diff --git a/public/sass/mixins/_mixins.scss b/public/sass/mixins/_mixins.scss index 2e3c2fdcdd8..f3be6af56ba 100644 --- a/public/sass/mixins/_mixins.scss +++ b/public/sass/mixins/_mixins.scss @@ -1,6 +1,6 @@ @mixin clearfix() { &::after { - content: ''; + content: ""; display: table; clear: both; } @@ -265,10 +265,20 @@ // Add an alphatransparency value to any background or border color (via Elyse Holladay) #translucent { @mixin background($color: $white, $alpha: 1) { - background-color: hsla(hue($color), saturation($color), lightness($color), $alpha); + background-color: hsla( + hue($color), + saturation($color), + lightness($color), + $alpha + ); } @mixin border($color: $white, $alpha: 1) { - border-color: hsla(hue($color), saturation($color), lightness($color), $alpha); + border-color: hsla( + hue($color), + saturation($color), + lightness($color), + $alpha + ); @include background-clip(padding-box); } } @@ -284,37 +294,66 @@ // Gradients @mixin gradient-horizontal($startColor: #555, $endColor: #333) { background-color: $endColor; - background-image: linear-gradient(to right, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + to right, + $startColor, + $endColor + ); // Standard, IE10 background-repeat: repeat-x; } @mixin gradient-vertical($startColor: #555, $endColor: #333) { background-color: mix($startColor, $endColor, 60%); - background-image: linear-gradient(to bottom, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + to bottom, + $startColor, + $endColor + ); // Standard, IE10 background-repeat: repeat-x; } @mixin gradient-directional($startColor: #555, $endColor: #333, $deg: 45deg) { background-color: $endColor; background-repeat: repeat-x; - background-image: linear-gradient($deg, $startColor, $endColor); // Standard, IE10 + background-image: linear-gradient( + $deg, + $startColor, + $endColor + ); // Standard, IE10 } @mixin gradient-horizontal-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { background-color: mix($midColor, $endColor, 80%); - background-image: linear-gradient(to right, $startColor, $midColor $colorStop, $endColor); + background-image: linear-gradient( + to right, + $startColor, + $midColor $colorStop, + $endColor + ); background-repeat: no-repeat; } @mixin gradient-vertical-three-colors($startColor: #00b3ee, $midColor: #7a43b6, $colorStop: 50%, $endColor: #c3325f) { background-color: mix($midColor, $endColor, 80%); - background-image: linear-gradient($startColor, $midColor $colorStop, $endColor); + background-image: linear-gradient( + $startColor, + $midColor $colorStop, + $endColor + ); background-repeat: no-repeat; } @mixin gradient-radial($innerColor: #555, $outerColor: #333) { background-color: $outerColor; - background-image: -webkit-gradient(radial, center center, 0, center center, 460, from($innerColor), to($outerColor)); + background-image: -webkit-gradient( + radial, + center center, + 0, + center center, + 460, + from($innerColor), + to($outerColor) + ); background-image: -webkit-radial-gradient(circle, $innerColor, $outerColor); background-image: -moz-radial-gradient(circle, $innerColor, $outerColor); background-image: -o-radial-gradient(circle, $innerColor, $outerColor); @@ -341,7 +380,11 @@ @mixin left-brand-border-gradient() { border: none; - border-image: linear-gradient(rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); + border-image: linear-gradient( + rgba(255, 213, 0, 1) 0%, + rgba(255, 68, 0, 1) 99%, + rgba(255, 68, 0, 1) 100% + ); border-image-slice: 1; border-style: solid; border-top: 0; diff --git a/public/sass/pages/_playlist.scss b/public/sass/pages/_playlist.scss index 37fa7f17e20..5dd1c92cbd2 100644 --- a/public/sass/pages/_playlist.scss +++ b/public/sass/pages/_playlist.scss @@ -84,11 +84,11 @@ background-color: $list-item-bg; margin-bottom: 4px; .search-result-icon:before { - content: '\f009'; + content: "\f009"; } &.search-item-dash-home .search-result-icon:before { - content: '\f015'; + content: "\f015"; } } diff --git a/public/sass/utils/_validation.scss b/public/sass/utils/_validation.scss index c074d847490..86b7c008bfd 100644 --- a/public/sass/utils/_validation.scss +++ b/public/sass/utils/_validation.scss @@ -1,4 +1,4 @@ -input[type='text'].ng-dirty.ng-invalid { +input[type="text"].ng-dirty.ng-invalid { } input.validation-error, diff --git a/public/test/core/utils/version_specs.ts b/public/test/core/utils/version_specs.ts index 9abc4f326fc..a057c8e16bd 100644 --- a/public/test/core/utils/version_specs.ts +++ b/public/test/core/utils/version_specs.ts @@ -1,8 +1,8 @@ -import { describe, beforeEach, it, expect } from 'test/lib/common'; +import {describe, beforeEach, it, expect} from 'test/lib/common'; -import { SemVersion, isVersionGtOrEq } from 'app/core/utils/version'; +import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version'; -describe('SemVersion', () => { +describe("SemVersion", () => { let version = '1.0.0-alpha.1'; describe('parsing', () => { @@ -23,13 +23,13 @@ describe('SemVersion', () => { it('should detect greater version properly', () => { let semver = new SemVersion(version); let cases = [ - { value: '3.4.5', expected: true }, - { value: '3.4.4', expected: true }, - { value: '3.4.6', expected: false }, - { value: '4', expected: false }, - { value: '3.5', expected: false }, + {value: '3.4.5', expected: true}, + {value: '3.4.4', expected: true}, + {value: '3.4.6', expected: false}, + {value: '4', expected: false}, + {value: '3.5', expected: false}, ]; - cases.forEach(testCase => { + cases.forEach((testCase) => { expect(semver.isGtOrEq(testCase.value)).to.be(testCase.expected); }); }); @@ -38,16 +38,16 @@ describe('SemVersion', () => { describe('isVersionGtOrEq', () => { it('should compare versions properly (a >= b)', () => { let cases = [ - { values: ['3.4.5', '3.4.5'], expected: true }, - { values: ['3.4.5', '3.4.4'], expected: true }, - { values: ['3.4.5', '3.4.6'], expected: false }, - { values: ['3.4', '3.4.0'], expected: true }, - { values: ['3', '3.0.0'], expected: true }, - { values: ['3.1.1-beta1', '3.1'], expected: true }, - { values: ['3.4.5', '4'], expected: false }, - { values: ['3.4.5', '3.5'], expected: false }, + {values: ['3.4.5', '3.4.5'], expected: true}, + {values: ['3.4.5', '3.4.4'] , expected: true}, + {values: ['3.4.5', '3.4.6'], expected: false}, + {values: ['3.4', '3.4.0'], expected: true}, + {values: ['3', '3.0.0'], expected: true}, + {values: ['3.1.1-beta1', '3.1'], expected: true}, + {values: ['3.4.5', '4'], expected: false}, + {values: ['3.4.5', '3.5'], expected: false}, ]; - cases.forEach(testCase => { + cases.forEach((testCase) => { expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).to.be(testCase.expected); }); }); diff --git a/public/test/index.ts b/public/test/index.ts index a9b67d2d81f..33f24331b67 100644 --- a/public/test/index.ts +++ b/public/test/index.ts @@ -25,3 +25,7 @@ const context = (require).context('../', true, /specs\.(tsx?|js)/); for (let key of context.keys()) { context(key); } + + + + diff --git a/public/test/jest-shim.ts b/public/test/jest-shim.ts index f9af0c4a3f3..80c4bb3d21b 100644 --- a/public/test/jest-shim.ts +++ b/public/test/jest-shim.ts @@ -1,5 +1,6 @@ declare var global: NodeJS.Global; -(global).requestAnimationFrame = callback => { +(global).requestAnimationFrame = (callback) => { setTimeout(callback, 0); }; + diff --git a/public/test/lib/common.ts b/public/test/lib/common.ts index ddb4000174b..c6b74fc7ecb 100644 --- a/public/test/lib/common.ts +++ b/public/test/lib/common.ts @@ -1,4 +1,4 @@ -var _global = window; +var _global = (window); var beforeEach = _global.beforeEach; var afterEach = _global.afterEach; var before = _global.before; @@ -12,4 +12,13 @@ var angularMocks = { inject: _global.inject, }; -export { beforeEach, afterEach, before, describe, it, sinon, expect, angularMocks }; +export { + beforeEach, + afterEach, + before, + describe, + it, + sinon, + expect, + angularMocks, +}; diff --git a/public/test/mocks/backend_srv.ts b/public/test/mocks/backend_srv.ts index 32c04866395..666de593722 100644 --- a/public/test/mocks/backend_srv.ts +++ b/public/test/mocks/backend_srv.ts @@ -1,5 +1,8 @@ export class BackendSrvMock { search: any; - constructor() {} + constructor() { + } + } + diff --git a/public/test/specs/app_specs.ts b/public/test/specs/app_specs.ts index 780fef040ba..f82946c20a2 100644 --- a/public/test/specs/app_specs.ts +++ b/public/test/specs/app_specs.ts @@ -1,11 +1,14 @@ -import { describe, it, expect } from 'test/lib/common'; +import {describe, it, expect} from 'test/lib/common'; -import { GrafanaApp } from 'app/app'; +import {GrafanaApp} from 'app/app'; describe('GrafanaApp', () => { + var app = new GrafanaApp(); it('can call inits', () => { expect(app).to.not.be(null); }); }); + + diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index b92e853d8f8..8e83915362f 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -1,8 +1,8 @@ import _ from 'lodash'; import config from 'app/core/config'; import * as dateMath from 'app/core/utils/datemath'; -import { angularMocks, sinon } from '../lib/common'; -import { PanelModel } from 'app/features/dashboard/panel_model'; +import {angularMocks, sinon} from '../lib/common'; +import {PanelModel} from 'app/features/dashboard/panel_model'; export function ControllerTestContext() { var self = this; @@ -42,8 +42,8 @@ export function ControllerTestContext() { self.$location = $location; self.$browser = $browser; self.$q = $q; - self.panel = new PanelModel({ type: 'test' }); - self.dashboard = { meta: {} }; + self.panel = new PanelModel({type: 'test'}); + self.dashboard = {meta: {}}; $rootScope.appEvent = sinon.spy(); $rootScope.onAppEvent = sinon.spy(); @@ -53,14 +53,14 @@ export function ControllerTestContext() { $rootScope.colors.push('#' + i); } - config.panels['test'] = { info: {} }; + config.panels['test'] = {info: {}}; self.ctrl = $controller( Ctrl, - { $scope: self.scope }, + {$scope: self.scope}, { panel: self.panel, dashboard: self.dashboard, - } + }, ); }); }; @@ -72,7 +72,7 @@ export function ControllerTestContext() { self.$browser = $browser; self.scope.contextSrv = {}; self.scope.panel = {}; - self.scope.dashboard = { meta: {} }; + self.scope.dashboard = {meta: {}}; self.scope.dashboardMeta = {}; self.scope.dashboardViewState = new DashboardViewStateStub(); self.scope.appEvent = sinon.spy(); @@ -131,7 +131,7 @@ export function DashboardViewStateStub() { export function TimeSrvStub() { this.init = sinon.spy(); - this.time = { from: 'now-1h', to: 'now' }; + this.time = {from: 'now-1h', to: 'now'}; this.timeRange = function(parse) { if (parse === false) { return this.time; @@ -159,7 +159,7 @@ export function ContextSrvStub() { export function TemplateSrvStub() { this.variables = []; - this.templateSettings = { interpolate: /\[\[([\s\S]+?)\]\]/g }; + this.templateSettings = {interpolate: /\[\[([\s\S]+?)\]\]/g}; this.data = {}; this.replace = function(text) { return _.template(text, this.templateSettings)(this.data); @@ -188,7 +188,7 @@ var allDeps = { TimeSrvStub: TimeSrvStub, ControllerTestContext: ControllerTestContext, ServiceTestContext: ServiceTestContext, - DashboardViewStateStub: DashboardViewStateStub, + DashboardViewStateStub: DashboardViewStateStub }; // for legacy From a101177b5521bfdd8ce67f6a60216fb7fb8d3c8f Mon Sep 17 00:00:00 2001 From: Aleksei Magusev Date: Mon, 29 Jan 2018 16:46:51 +0100 Subject: [PATCH 07/77] Conditionally select a field to return in ResponseParser for InfluxDB This patch also fixes "value[1] || value[0]" to not ignore zeros. --- .../datasource/influxdb/response_parser.ts | 17 +++++++++--- .../influxdb/specs/response_parser.jest.ts | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index 78ce67e7a37..33b883f1d64 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -11,14 +11,23 @@ export default class ResponseParser { return []; } - var influxdb11format = query.toLowerCase().indexOf('show tag values') >= 0; - var res = {}; _.each(influxResults.series, serie => { _.each(serie.values, value => { if (_.isArray(value)) { - if (influxdb11format) { - addUnique(res, value[1] || value[0]); + // In general, there are 2 possible shapes for the returned value. + // The first one is a two-element array, + // where the first element is somewhat a metadata value: + // the tag name for SHOW TAG VALUES queries, + // the time field for SELECT queries, etc. + // The second shape is an one-element array, + // that is containing an immediate value. + // For example, SHOW FIELD KEYS queries return such shape. + // Note, pre-0.11 versions return + // the second shape for SHOW TAG VALUES queries + // (while the newer versions—first). + if (value[1] !== undefined) { + addUnique(res, value[1]); } else { addUnique(res, value[0]); } diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts b/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts index 8ddc0fcdaf1..ba1546c37e9 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts @@ -85,6 +85,32 @@ describe('influxdb response parser', () => { }); }); + describe('SELECT response', () => { + var query = 'SELECT "usage_iowait" FROM "cpu" LIMIT 10'; + var response = { + results: [ + { + series: [ + { + name: 'cpu', + columns: ['time', 'usage_iowait'], + values: [[1488465190006040638, 0.0], [1488465190006040638, 15.0], [1488465190006040638, 20.2]], + }, + ], + }, + ], + }; + + var result = parser.parse(query, response); + + it('should return second column', () => { + expect(_.size(result)).toBe(3); + expect(result[0].text).toBe(0.0); + expect(result[1].text).toBe(15.0); + expect(result[2].text).toBe(20.2); + }); + }); + describe('SHOW FIELD response', () => { var query = 'SHOW FIELD KEYS FROM "cpu"'; describe('response from 0.10.0', () => { From 92e5968f91472abdba17405092e092a532de5eb4 Mon Sep 17 00:00:00 2001 From: Aleksei Magusev Date: Thu, 1 Feb 2018 14:55:03 +0100 Subject: [PATCH 08/77] Fix ResponseParser for InfluxDB to return only string values --- public/app/plugins/datasource/influxdb/response_parser.ts | 2 +- .../datasource/influxdb/specs/response_parser.jest.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/influxdb/response_parser.ts b/public/app/plugins/datasource/influxdb/response_parser.ts index 33b883f1d64..746a8c0e05a 100644 --- a/public/app/plugins/datasource/influxdb/response_parser.ts +++ b/public/app/plugins/datasource/influxdb/response_parser.ts @@ -38,7 +38,7 @@ export default class ResponseParser { }); return _.map(res, value => { - return { text: value }; + return { text: value.toString() }; }); } } diff --git a/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts b/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts index ba1546c37e9..c0652f5fca3 100644 --- a/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts +++ b/public/app/plugins/datasource/influxdb/specs/response_parser.jest.ts @@ -105,9 +105,9 @@ describe('influxdb response parser', () => { it('should return second column', () => { expect(_.size(result)).toBe(3); - expect(result[0].text).toBe(0.0); - expect(result[1].text).toBe(15.0); - expect(result[2].text).toBe(20.2); + expect(result[0].text).toBe('0'); + expect(result[1].text).toBe('15'); + expect(result[2].text).toBe('20.2'); }); }); From bff174dba90a5467ecef62961d49fdd13b53f512 Mon Sep 17 00:00:00 2001 From: Andrzej Ressel Date: Wed, 11 Apr 2018 22:59:29 +0200 Subject: [PATCH 09/77] PR comments --- .../features/dashboard/settings/settings.html | 8 ++++-- .../features/dashboard/settings/settings.ts | 27 ++++++++++++------- .../sass/components/_dashboard_settings.scss | 4 +++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/public/app/features/dashboard/settings/settings.html b/public/app/features/dashboard/settings/settings.html index 5103fab8b75..3e9796239dc 100644 --- a/public/app/features/dashboard/settings/settings.html +++ b/public/app/features/dashboard/settings/settings.html @@ -87,12 +87,16 @@
-
-

View JSON

+
+

Dashboard JSON

+ +
diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 9949b52dd45..0f940c422e1 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -17,7 +17,14 @@ export class SettingsCtrl { hasUnsavedFolderChange: boolean; /** @ngInject */ - constructor(private $scope, private $location, private $rootScope, private backendSrv, private dashboardSrv) { + constructor( + private $scope, + private $route, + private $location, + private $rootScope, + private backendSrv, + private dashboardSrv + ) { // temp hack for annotations and variables editors // that rely on inherited scope $scope.dashboard = this.dashboard; @@ -93,8 +100,8 @@ export class SettingsCtrl { } this.sections.push({ - title: 'View JSON', - id: 'view_json', + title: 'Dashboard JSON', + id: 'dashboard_json', icon: 'gicon gicon-json', }); @@ -134,13 +141,13 @@ export class SettingsCtrl { } saveDashboard() { - if (this.viewId === 'view_json') { - this.dashboardSrv.saveJSONDashboard(this.json).then(() => { - window.location.reload(); - }); - } else { - this.dashboardSrv.saveDashboard(); - } + this.dashboardSrv.saveDashboard(); + } + + saveDashboardJson() { + this.dashboardSrv.saveJSONDashboard(this.json).then(() => { + this.$route.reload(); + }); } onPostSave() { diff --git a/public/sass/components/_dashboard_settings.scss b/public/sass/components/_dashboard_settings.scss index 11d943eb13c..873da7f4935 100644 --- a/public/sass/components/_dashboard_settings.scss +++ b/public/sass/components/_dashboard_settings.scss @@ -80,3 +80,7 @@ margin-bottom: 10px; } } + +.dashboard-settings__json-save-button { + margin-top: $spacer; +} From c2b720835b8f7cf1f58b14ab120a723b6835aed4 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 27 Apr 2018 19:34:10 +0900 Subject: [PATCH 10/77] fix to match table column name and order --- public/app/plugins/datasource/prometheus/datasource.ts | 1 + .../app/plugins/datasource/prometheus/result_transformer.ts | 6 +++--- public/app/plugins/panel/table/module.ts | 5 +++++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2a8b3069a53..6d654438271 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -153,6 +153,7 @@ export class PrometheusDatasource { end: end, responseListLength: responseList.length, responseIndex: index, + refId: activeTargets[index].refId, }; this.resultTransformer.transform(result, response, transformerOptions); diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index 6d97b783983..d5feda7d28c 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -8,7 +8,7 @@ export class ResultTransformer { let prometheusResult = response.data.data.result; if (options.format === 'table') { - result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.responseIndex)); + result.push(this.transformMetricDataToTable(prometheusResult, options.responseListLength, options.refId)); } else if (options.format === 'heatmap') { let seriesList = []; prometheusResult.sort(sortSeriesByLabel); @@ -58,7 +58,7 @@ export class ResultTransformer { return { target: metricLabel, datapoints: dps }; } - transformMetricDataToTable(md, resultCount: number, resultIndex: number) { + transformMetricDataToTable(md, resultCount: number, refId: string) { var table = new TableModel(); var i, j; var metricLabels = {}; @@ -83,7 +83,7 @@ export class ResultTransformer { metricLabels[label] = labelIndex + 1; table.columns.push({ text: label }); }); - let valueText = resultCount > 1 ? `Value #${String.fromCharCode(65 + resultIndex)}` : 'Value'; + let valueText = resultCount > 1 ? `Value #${refId}` : 'Value'; table.columns.push({ text: valueText }); // Populate rows, set value to empty string when label not present. diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts index 27eab205f09..f4728982ad5 100644 --- a/public/app/plugins/panel/table/module.ts +++ b/public/app/plugins/panel/table/module.ts @@ -154,6 +154,11 @@ class TablePanelCtrl extends MetricsPanelCtrl { this.render(); } + moveQuery(target, direction) { + super.moveQuery(target, direction); + super.refresh(); + } + exportCsv() { var scope = this.$scope.$new(true); scope.tableData = this.renderer.render_values(); From 3d9b7a5892f11920c3be744287f0a4b46cfd5464 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 30 Apr 2018 14:41:52 +0200 Subject: [PATCH 11/77] increase length of auth_id column in user_auth table --- pkg/services/sqlstore/migrations/user_auth_mig.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/services/sqlstore/migrations/user_auth_mig.go b/pkg/services/sqlstore/migrations/user_auth_mig.go index 4d8a18ce33e..be4d112f6f6 100644 --- a/pkg/services/sqlstore/migrations/user_auth_mig.go +++ b/pkg/services/sqlstore/migrations/user_auth_mig.go @@ -21,4 +21,9 @@ func addUserAuthMigrations(mg *Migrator) { mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1)) // add indices addTableIndicesMigrations(mg, "v1", userAuthV1) + + mg.AddMigration("alter user_auth.auth_id to length 255", new(RawSqlMigration). + Sqlite("SELECT 0 WHERE 0;"). + Postgres("ALTER TABLE user_auth ALTER COLUMN auth_id TYPE VARCHAR(255);"). + Mysql("ALTER TABLE user_auth MODIFY auth_id VARCHAR(255);")) } From 05b0bfafe4a7b4587d3fcba70c8e8e60e66afaf1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 27 Apr 2018 18:21:20 +0200 Subject: [PATCH 12/77] Explore: Add entry to panel menu to jump to Explore * panel container menu gets new Explore entry (between Edit and Share) * entry only shows if datasource has `supportsExplore` set to true (set for Prometheus only for now) * click on Explore entry changes url to `/explore/state` via location provider * `state` is a JSON representation of the panel queries * datasources implement `getExploreState()` how to turn a panel config into explore initial state * Explore can parse the state and initialize its query expressions * ReactContainer now forwards route parameters as props to component * `pluginlist` and `singlestat` panel subclasses needed to be adapted because `panel_ctrl` now has the location provider as a property already --- public/app/containers/Explore/Explore.tsx | 18 ++++++++++++++++-- public/app/containers/Explore/QueryRows.tsx | 19 +++++++++++++++---- public/app/features/panel/panel_ctrl.ts | 18 ++++++++++++++++++ .../datasource/prometheus/datasource.ts | 10 ++++++++++ public/app/plugins/panel/pluginlist/module.ts | 2 +- public/app/plugins/panel/singlestat/module.ts | 2 +- public/app/routes/ReactContainer.tsx | 1 + public/app/routes/routes.ts | 2 +- 8 files changed, 63 insertions(+), 9 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index eae3f4d0a1f..ba1cb6807a6 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -38,6 +38,19 @@ function makeTimeSeriesList(dataList, options) { }); } +function parseInitialQueries(initial) { + if (!initial) { + return []; + } + try { + const parsed = JSON.parse(initial); + return parsed.queries.map(q => q.query); + } catch (e) { + console.error(e); + return []; + } +} + interface IExploreState { datasource: any; datasourceError: any; @@ -58,6 +71,7 @@ export class Explore extends React.Component { constructor(props) { super(props); + const initialQueries = parseInitialQueries(props.routeParams.initial); this.state = { datasource: null, datasourceError: null, @@ -65,7 +79,7 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, - queries: ensureQueries(), + queries: ensureQueries(initialQueries), requestOptions: null, showingGraph: true, showingTable: true, @@ -77,7 +91,7 @@ export class Explore extends React.Component { const datasource = await this.props.datasourceSrv.get(); const testResult = await datasource.testDatasource(); if (testResult.status === 'success') { - this.setState({ datasource, datasourceError: null, datasourceLoading: false }); + this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit()); } else { this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false }); } diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a0a9981368d..3940d16b2f6 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -6,13 +6,16 @@ class QueryRow extends PureComponent { constructor(props) { super(props); this.state = { - query: '', + edited: false, + query: props.query || '', }; } handleChangeQuery = value => { const { index, onChangeQuery } = this.props; - this.setState({ query: value }); + const { query } = this.state; + const edited = query !== value; + this.setState({ edited, query: value }); if (onChangeQuery) { onChangeQuery(value, index); } @@ -41,6 +44,7 @@ class QueryRow extends PureComponent { render() { const { request } = this.props; + const { edited, query } = this.state; return (
@@ -52,7 +56,12 @@ class QueryRow extends PureComponent {
- +
); @@ -63,7 +72,9 @@ export default class QueryRows extends PureComponent { render() { const { className = '', queries, ...handlers } = this.props; return ( -
{queries.map((q, index) => )}
+
+ {queries.map((q, index) => )} +
); } } diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index da8adc4f908..cdd2bc2be60 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -22,6 +22,7 @@ export class PanelCtrl { editorTabs: any; $scope: any; $injector: any; + $location: any; $timeout: any; fullscreen: boolean; inspector: any; @@ -35,6 +36,7 @@ export class PanelCtrl { constructor($scope, $injector) { this.$injector = $injector; + this.$location = $injector.get('$location'); this.$scope = $scope; this.$timeout = $injector.get('$timeout'); this.editorTabIndex = 0; @@ -97,6 +99,12 @@ export class PanelCtrl { this.changeView(false, false); } + explore() { + // TS hack :< + const initialState = JSON.stringify(this['datasource'].getExploreState(this.panel)); + this.$location.url(`/explore/${initialState}`); + } + initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); @@ -154,6 +162,16 @@ export class PanelCtrl { }); } + // TS hack :< + if ('datasource' in this && this['datasource'].supportsExplore) { + menu.push({ + text: 'Explore', + click: 'ctrl.explore();', + icon: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + menu.push({ text: 'Share', click: 'ctrl.sharePanel();', diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 2a8b3069a53..221bffbda67 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -19,6 +19,7 @@ export class PrometheusDatasource { type: string; editorSrc: string; name: string; + supportsExplore: boolean; supportMetrics: boolean; url: string; directUrl: string; @@ -34,6 +35,7 @@ export class PrometheusDatasource { this.type = 'prometheus'; this.editorSrc = 'app/features/prometheus/partials/query.editor.html'; this.name = instanceSettings.name; + this.supportsExplore = true; this.supportMetrics = true; this.url = instanceSettings.url; this.directUrl = instanceSettings.directUrl; @@ -323,6 +325,14 @@ export class PrometheusDatasource { }); } + getExploreState(panel) { + if (!panel.targets) { + return {}; + } + const queries = panel.targets.map(t => ({ query: t.expr, format: t.format })); + return { queries }; + } + getPrometheusTime(date, roundUp) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index e97b1a8fbf9..acfa69b171c 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -12,7 +12,7 @@ class PluginListCtrl extends PanelCtrl { panelDefaults = {}; /** @ngInject */ - constructor($scope, $injector, private backendSrv, private $location) { + constructor($scope, $injector, private backendSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index c7f523a9591..28d3f308d68 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, private $location, private linkSrv) { + constructor($scope, $injector, private linkSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index d6d34372090..db6938cc878 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -29,6 +29,7 @@ export function reactContainer($route, $location, backendSrv: BackendSrv, dataso const props = { backendSrv: backendSrv, datasourceSrv: datasourceSrv, + routeParams: $route.current.params, }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 49690561728..6a61315f956 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -111,7 +111,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { controller: 'FolderDashboardsCtrl', controllerAs: 'ctrl', }) - .when('/explore', { + .when('/explore/:initial?', { template: '', resolve: { component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'), From 8a53ec610bf0dc6cc5b69e675958b94482331661 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 29 Apr 2018 14:02:32 +0200 Subject: [PATCH 13/77] Fix url encoding, expand template vars, fix TS hacks * moved datasource related functions to panel sub-class * expand panel template vars for url * added keybindings for x -> Explore * url encoding for explore state --- public/app/containers/Explore/Explore.tsx | 3 ++- public/app/core/services/keybindingSrv.ts | 14 ++++++++++- public/app/core/utils/location_util.ts | 11 +++++---- .../app/features/panel/metrics_panel_ctrl.ts | 19 +++++++++++++++ public/app/features/panel/panel_ctrl.ts | 24 +++++++------------ .../datasource/prometheus/datasource.ts | 15 ++++++++---- 6 files changed, 60 insertions(+), 26 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index ba1cb6807a6..40261ee635a 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -10,6 +10,7 @@ import Graph from './Graph'; import Table from './Table'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; +import { decodePathComponent } from 'app/core/utils/location_util'; function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { @@ -43,7 +44,7 @@ function parseInitialQueries(initial) { return []; } try { - const parsed = JSON.parse(initial); + const parsed = JSON.parse(decodePathComponent(initial)); return parsed.queries.map(q => q.query); } catch (e) { console.error(e); diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 55d968fd981..94bf9efb31b 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -3,6 +3,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; +import { encodePathComponent } from 'app/core/utils/location_util'; import Mousetrap from 'mousetrap'; import 'mousetrap-global-bind'; @@ -13,7 +14,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location) { + constructor(private $rootScope, private $location, private datasourceSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -176,6 +177,17 @@ export class KeybindingSrv { } }); + this.bind('x', async () => { + if (dashboard.meta.focusPanelId) { + const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); + const datasource = await this.datasourceSrv.get(panel.datasource); + if (datasource && datasource.supportsExplore) { + const exploreState = encodePathComponent(JSON.stringify(datasource.getExploreState(panel))); + this.$location.url(`/explore/${exploreState}`); + } + } + }); + // delete panel this.bind('p r', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts index f8d6aa4ee5f..735272285ff 100644 --- a/public/app/core/utils/location_util.ts +++ b/public/app/core/utils/location_util.ts @@ -1,6 +1,11 @@ import config from 'app/core/config'; -const _stripBaseFromUrl = url => { +// Slash encoding for angular location provider, see https://github.com/angular/angular.js/issues/10479 +const SLASH = ''; +export const decodePathComponent = (pc: string) => decodeURIComponent(pc).replace(new RegExp(SLASH, 'g'), '/'); +export const encodePathComponent = (pc: string) => encodeURIComponent(pc.replace(/\//g, SLASH)); + +export const stripBaseFromUrl = url => { const appSubUrl = config.appSubUrl; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; const urlWithoutBase = @@ -9,6 +14,4 @@ const _stripBaseFromUrl = url => { return urlWithoutBase; }; -export default { - stripBaseFromUrl: _stripBaseFromUrl, -}; +export default { stripBaseFromUrl }; diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 9e9598e1732..acf46a193e8 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -6,6 +6,7 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; +import { encodePathComponent } from 'app/core/utils/location_util'; import { metricsTabDirective } from './metrics_tab'; @@ -309,6 +310,24 @@ class MetricsPanelCtrl extends PanelCtrl { this.refresh(); } + getAdditionalMenuItems() { + const items = []; + if (this.datasource.supportsExplore) { + items.push({ + text: 'Explore', + click: 'ctrl.explore();', + icon: 'fa fa-fw fa-rocket', + shortcut: 'x', + }); + } + return items; + } + + explore() { + const exploreState = encodePathComponent(JSON.stringify(this.datasource.getExploreState(this.panel))); + this.$location.url(`/explore/${exploreState}`); + } + addQuery(target) { target.refId = this.dashboard.getNextQueryLetter(this.panel); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index cdd2bc2be60..67725ec5fec 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -99,12 +99,6 @@ export class PanelCtrl { this.changeView(false, false); } - explore() { - // TS hack :< - const initialState = JSON.stringify(this['datasource'].getExploreState(this.panel)); - this.$location.url(`/explore/${initialState}`); - } - initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); @@ -162,16 +156,6 @@ export class PanelCtrl { }); } - // TS hack :< - if ('datasource' in this && this['datasource'].supportsExplore) { - menu.push({ - text: 'Explore', - click: 'ctrl.explore();', - icon: 'fa fa-fw fa-rocket', - shortcut: 'x', - }); - } - menu.push({ text: 'Share', click: 'ctrl.sharePanel();', @@ -179,6 +163,9 @@ export class PanelCtrl { shortcut: 'p s', }); + // Additional items from sub-class + menu.push(...this.getAdditionalMenuItems()); + let extendedMenu = this.getExtendedMenu(); menu.push({ text: 'More ...', @@ -227,6 +214,11 @@ export class PanelCtrl { return menu; } + // Override in sub-class to add items before extended menu + getAdditionalMenuItems() { + return []; + } + otherPanelInFullscreenMode() { return this.dashboard.meta.fullscreen && !this.fullscreen; } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 221bffbda67..5d9510b4238 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -326,11 +326,18 @@ export class PrometheusDatasource { } getExploreState(panel) { - if (!panel.targets) { - return {}; + let state = {}; + if (panel.targets) { + const queries = panel.targets.map(t => ({ + query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), + format: t.format, + })); + state = { + ...state, + queries, + }; } - const queries = panel.targets.map(t => ({ query: t.expr, format: t.format })); - return { queries }; + return state; } getPrometheusTime(date, roundUp) { From 253b2cc081dc3fe2f81da2feb22b490f09494537 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 1 May 2018 05:00:56 +0900 Subject: [PATCH 14/77] add test for prometheus table column title --- .../prometheus/specs/result_transformer.jest.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts index abcc46d7ea8..64b983fc8a7 100644 --- a/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts +++ b/public/app/plugins/datasource/prometheus/specs/result_transformer.jest.ts @@ -47,6 +47,18 @@ describe('Prometheus Result Transformer', () => { { text: 'Value' }, ]); }); + + it('should column title include refId if response count is more than 2', () => { + var table = ctx.resultTransformer.transformMetricDataToTable(response.data.result, 2, "B"); + expect(table.type).toBe('table'); + expect(table.columns).toEqual([ + { text: 'Time', type: 'time' }, + { text: '__name__' }, + { text: 'instance' }, + { text: 'job' }, + { text: 'Value #B' }, + ]); + }); }); describe('When resultFormat is table and instant = true', () => { From 3dd073f98d3fc2bc5e4b5639d226aa04cb68db0f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 2 May 2018 09:56:53 +0200 Subject: [PATCH 15/77] fixed so all buttons are styled not just small ones, fixes #11616 --- public/sass/components/_timepicker.scss | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 2d7a12c3d01..9b71e8e7c05 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -71,12 +71,10 @@ td { padding: 1px; } - button.btn-sm { + button { @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl); - font-size: $font-size-sm; background-image: none; border: none; - padding: 5px 11px; color: $text-color; &.active span { color: $blue; @@ -86,6 +84,10 @@ color: $orange; font-weight: bold; } + &.btn-sm { + font-size: $font-size-sm; + padding: 5px 11px; + } } } @@ -103,10 +105,10 @@ } .fa-chevron-left::before { - content: "\f053"; + content: '\f053'; } .fa-chevron-right::before { - content: "\f054"; + content: '\f054'; } .glyphicon-chevron-right { From 64283408ee610e6bd1832abf37a946ca25505c81 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 2 May 2018 12:43:25 +0300 Subject: [PATCH 16/77] scroll: fix scrolling on mobile Chrome (#11710) --- public/sass/pages/_dashboard.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index cf32522df7f..aeb9e28975b 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -45,6 +45,8 @@ div.flot-text { height: calc(100% - 27px); position: relative; overflow: hidden; + // Fixes scrolling on mobile devices + overflow-y: scroll; } .panel-title-container { From 14bb7832af9a43f58cc0bf53eb7e4abe9bf44085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 19:54:07 +0200 Subject: [PATCH 17/77] Metrics package now follows new service interface & registration (#11787) * refactoring: metrics package now follows new service interface & registration * fix: minor fix, make sure metrics service is imported, by grafana-server --- pkg/cmd/grafana-server/server.go | 3 +- pkg/metrics/init.go | 38 ----------------- pkg/metrics/metrics.go | 24 +---------- pkg/metrics/service.go | 71 ++++++++++++++++++++++++++++++++ pkg/metrics/settings.go | 58 +++++++++++--------------- 5 files changed, 96 insertions(+), 98 deletions(-) delete mode 100644 pkg/metrics/init.go create mode 100644 pkg/metrics/service.go diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 30bb0b2003a..f20195b563a 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -24,7 +24,6 @@ import ( "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/metrics" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" @@ -33,6 +32,7 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" + _ "github.com/grafana/grafana/pkg/metrics" _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" @@ -72,7 +72,6 @@ func (g *GrafanaServerImpl) Start() error { sqlstore.NewEngine() // TODO: this should return an error sqlstore.EnsureAdminUser() - metrics.Init(g.cfg.Raw) login.Init() social.NewOAuthService() diff --git a/pkg/metrics/init.go b/pkg/metrics/init.go deleted file mode 100644 index 833b148d319..00000000000 --- a/pkg/metrics/init.go +++ /dev/null @@ -1,38 +0,0 @@ -package metrics - -import ( - "context" - - ini "gopkg.in/ini.v1" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/metrics/graphitebridge" -) - -var metricsLogger log.Logger = log.New("metrics") - -type logWrapper struct { - logger log.Logger -} - -func (lw *logWrapper) Println(v ...interface{}) { - lw.logger.Info("graphite metric bridge", v...) -} - -func Init(file *ini.File) { - cfg := ReadSettings(file) - internalInit(cfg) -} - -func internalInit(settings *MetricSettings) { - initMetricVars(settings) - - if settings.GraphiteBridgeConfig != nil { - bridge, err := graphitebridge.NewBridge(settings.GraphiteBridgeConfig) - if err != nil { - metricsLogger.Error("failed to create graphite bridge", "error", err) - } else { - go bridge.Run(context.Background()) - } - } -} diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index e3640378f7e..83505826910 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -279,7 +279,7 @@ func init() { }, []string{"version"}) } -func initMetricVars(settings *MetricSettings) { +func initMetricVars() { prometheus.MustRegister( M_Instance_Start, M_Page_Status, @@ -316,28 +316,6 @@ func initMetricVars(settings *MetricSettings) { M_StatTotal_Playlists, M_Grafana_Version) - go instrumentationLoop(settings) -} - -func instrumentationLoop(settings *MetricSettings) chan struct{} { - M_Instance_Start.Inc() - - // set the total stats gauges before we publishing metrics - updateTotalStats() - - onceEveryDayTick := time.NewTicker(time.Hour * 24) - everyMinuteTicker := time.NewTicker(time.Minute) - defer onceEveryDayTick.Stop() - defer everyMinuteTicker.Stop() - - for { - select { - case <-onceEveryDayTick.C: - sendUsageStats() - case <-everyMinuteTicker.C: - updateTotalStats() - } - } } func updateTotalStats() { diff --git a/pkg/metrics/service.go b/pkg/metrics/service.go new file mode 100644 index 00000000000..ec38e0acfec --- /dev/null +++ b/pkg/metrics/service.go @@ -0,0 +1,71 @@ +package metrics + +import ( + "context" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics/graphitebridge" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/setting" +) + +var metricsLogger log.Logger = log.New("metrics") + +type logWrapper struct { + logger log.Logger +} + +func (lw *logWrapper) Println(v ...interface{}) { + lw.logger.Info("graphite metric bridge", v...) +} + +func init() { + registry.RegisterService(&InternalMetricsService{}) + initMetricVars() +} + +type InternalMetricsService struct { + Cfg *setting.Cfg `inject:""` + + enabled bool + intervalSeconds int64 + graphiteCfg *graphitebridge.Config +} + +func (im *InternalMetricsService) Init() error { + return im.readSettings() +} + +func (im *InternalMetricsService) Run(ctx context.Context) error { + // Start Graphite Bridge + if im.graphiteCfg != nil { + bridge, err := graphitebridge.NewBridge(im.graphiteCfg) + if err != nil { + metricsLogger.Error("failed to create graphite bridge", "error", err) + } else { + go bridge.Run(ctx) + } + } + + M_Instance_Start.Inc() + + // set the total stats gauges before we publishing metrics + updateTotalStats() + + onceEveryDayTick := time.NewTicker(time.Hour * 24) + everyMinuteTicker := time.NewTicker(time.Minute) + defer onceEveryDayTick.Stop() + defer everyMinuteTicker.Stop() + + for { + select { + case <-onceEveryDayTick.C: + sendUsageStats() + case <-everyMinuteTicker.C: + updateTotalStats() + case <-ctx.Done(): + return ctx.Err() + } + } +} diff --git a/pkg/metrics/settings.go b/pkg/metrics/settings.go index c21e7279b7e..58b84a7192f 100644 --- a/pkg/metrics/settings.go +++ b/pkg/metrics/settings.go @@ -1,67 +1,53 @@ package metrics import ( + "fmt" "strings" "time" "github.com/grafana/grafana/pkg/metrics/graphitebridge" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" - ini "gopkg.in/ini.v1" ) -type MetricSettings struct { - Enabled bool - IntervalSeconds int64 - GraphiteBridgeConfig *graphitebridge.Config -} - -func ReadSettings(file *ini.File) *MetricSettings { - var settings = &MetricSettings{ - Enabled: false, +func (im *InternalMetricsService) readSettings() error { + var section, err = im.Cfg.Raw.GetSection("metrics") + if err != nil { + return fmt.Errorf("Unable to find metrics config section %v", err) } - var section, err = file.GetSection("metrics") - if err != nil { - metricsLogger.Crit("Unable to find metrics config section", "error", err) + im.enabled = section.Key("enabled").MustBool(false) + im.intervalSeconds = section.Key("interval_seconds").MustInt64(10) + + if !im.enabled { return nil } - settings.Enabled = section.Key("enabled").MustBool(false) - settings.IntervalSeconds = section.Key("interval_seconds").MustInt64(10) - - if !settings.Enabled { - return settings + if err := im.parseGraphiteSettings(); err != nil { + return fmt.Errorf("Unable to parse metrics graphite section, %v", err) } - cfg, err := parseGraphiteSettings(settings, file) - if err != nil { - metricsLogger.Crit("Unable to parse metrics graphite section", "error", err) - return nil - } - - settings.GraphiteBridgeConfig = cfg - - return settings + return nil } -func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphitebridge.Config, error) { - graphiteSection, err := setting.Raw.GetSection("metrics.graphite") +func (im *InternalMetricsService) parseGraphiteSettings() error { + graphiteSection, err := im.Cfg.Raw.GetSection("metrics.graphite") + if err != nil { - return nil, nil + return nil } address := graphiteSection.Key("address").String() if address == "" { - return nil, nil + return nil } - cfg := &graphitebridge.Config{ + bridgeCfg := &graphitebridge.Config{ URL: address, Prefix: graphiteSection.Key("prefix").MustString("prod.grafana.%(instance_name)s"), CountersAsDelta: true, Gatherer: prometheus.DefaultGatherer, - Interval: time.Duration(settings.IntervalSeconds) * time.Second, + Interval: time.Duration(im.intervalSeconds) * time.Second, Timeout: 10 * time.Second, Logger: &logWrapper{logger: metricsLogger}, ErrorHandling: graphitebridge.ContinueOnError, @@ -74,6 +60,8 @@ func parseGraphiteSettings(settings *MetricSettings, file *ini.File) (*graphiteb prefix = "prod.grafana.%(instance_name)s." } - cfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1) - return cfg, nil + bridgeCfg.Prefix = strings.Replace(prefix, "%(instance_name)s", safeInstanceName, -1) + + im.graphiteCfg = bridgeCfg + return nil } From 053c2039bb407a33d2af36609279213a6bc4d5f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 1 May 2018 15:51:15 +0200 Subject: [PATCH 18/77] refactor: provisioning service refactoring --- pkg/api/http_server.go | 11 ++++- pkg/cmd/grafana-server/server.go | 8 +--- .../provisioning/dashboards/config_reader.go | 2 +- .../provisioning/dashboards/dashboard.go | 7 +--- pkg/services/provisioning/provisioning.go | 40 ++++++++++++------- pkg/setting/setting.go | 18 +++++---- 6 files changed, 49 insertions(+), 37 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 38858606579..f978a8c220c 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -26,9 +26,14 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/setting" ) +func init() { + registry.RegisterService(&HTTPServer{}) +} + type HTTPServer struct { log log.Logger macaron *macaron.Macaron @@ -41,12 +46,14 @@ type HTTPServer struct { Bus bus.Bus `inject:""` } -func (hs *HTTPServer) Init() { +func (hs *HTTPServer) Init() error { hs.log = log.New("http.server") hs.cache = gocache.New(5*time.Minute, 10*time.Minute) + + return nil } -func (hs *HTTPServer) Start(ctx context.Context) error { +func (hs *HTTPServer) Run(ctx context.Context) error { var err error hs.context = ctx diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index f20195b563a..05d10ea1afd 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/provisioning" "golang.org/x/sync/errgroup" @@ -37,6 +36,7 @@ import ( _ "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/cleanup" _ "github.com/grafana/grafana/pkg/services/notifications" + _ "github.com/grafana/grafana/pkg/services/provisioning" _ "github.com/grafana/grafana/pkg/services/search" ) @@ -75,10 +75,6 @@ func (g *GrafanaServerImpl) Start() error { login.Init() social.NewOAuthService() - if err := provisioning.Init(g.context, setting.HomePath, g.cfg.Raw); err != nil { - return fmt.Errorf("Failed to provision Grafana from config. error: %v", err) - } - tracingCloser, err := tracing.Init(g.cfg.Raw) if err != nil { return fmt.Errorf("Tracing settings is not valid. error: %v", err) @@ -116,7 +112,7 @@ func (g *GrafanaServerImpl) Start() error { g.log.Info("Initializing " + reflect.TypeOf(service).Elem().Name()) if err := service.Init(); err != nil { - return fmt.Errorf("Service init failed %v", err) + return fmt.Errorf("Service init failed: %v", err) } } diff --git a/pkg/services/provisioning/dashboards/config_reader.go b/pkg/services/provisioning/dashboards/config_reader.go index 8ac79df0fac..4f9577f82db 100644 --- a/pkg/services/provisioning/dashboards/config_reader.go +++ b/pkg/services/provisioning/dashboards/config_reader.go @@ -69,7 +69,7 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) { parsedDashboards, err := cr.parseConfigs(file) if err != nil { - + return nil, err } if len(parsedDashboards) > 0 { diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index a5349517bbe..a856565bf01 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -10,19 +10,16 @@ import ( type DashboardProvisioner struct { cfgReader *configReader log log.Logger - ctx context.Context } -func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) { +func NewDashboardProvisioner(configDirectory string) *DashboardProvisioner { log := log.New("provisioning.dashboard") d := &DashboardProvisioner{ cfgReader: &configReader{path: configDirectory, log: log}, log: log, - ctx: ctx, } - err := d.Provision(ctx) - return d, err + return d } func (provider *DashboardProvisioner) Provision(ctx context.Context) error { diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 71385994bb5..9044ae97389 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -2,30 +2,40 @@ package provisioning import ( "context" + "fmt" "path" - "path/filepath" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" - ini "gopkg.in/ini.v1" + "github.com/grafana/grafana/pkg/setting" ) -func Init(ctx context.Context, homePath string, cfg *ini.File) error { - provisioningPath := makeAbsolute(cfg.Section("paths").Key("provisioning").String(), homePath) +func init() { + registry.RegisterService(&ProvisioningService{}) +} - datasourcePath := path.Join(provisioningPath, "datasources") +type ProvisioningService struct { + Cfg *setting.Cfg `inject:""` +} + +func (ps *ProvisioningService) Init() error { + datasourcePath := path.Join(ps.Cfg.ProvisioningPath, "datasources") if err := datasources.Provision(datasourcePath); err != nil { + return fmt.Errorf("Datasource provisioning error: %v", err) + } + + return nil +} + +func (ps *ProvisioningService) Run(ctx context.Context) error { + dashboardPath := path.Join(ps.Cfg.ProvisioningPath, "dashboards") + dashProvisioner := dashboards.NewDashboardProvisioner(dashboardPath) + + if err := dashProvisioner.Provision(ctx); err != nil { return err } - dashboardPath := path.Join(provisioningPath, "dashboards") - _, err := dashboards.Provision(ctx, dashboardPath) - return err -} - -func makeAbsolute(path string, root string) string { - if filepath.IsAbs(path) { - return path - } - return filepath.Join(root, path) + <-ctx.Done() + return ctx.Err() } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 40d7522f775..44d14e166d0 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -52,12 +52,11 @@ var ( ApplicationName string // Paths - LogsPath string - HomePath string - DataPath string - PluginsPath string - ProvisioningPath string - CustomInitPath = "conf/custom.ini" + LogsPath string + HomePath string + DataPath string + PluginsPath string + CustomInitPath = "conf/custom.ini" // Log settings. LogModes []string @@ -187,6 +186,9 @@ var ( type Cfg struct { Raw *ini.File + // Paths + ProvisioningPath string + // SMTP email settings Smtp SmtpSettings @@ -516,7 +518,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { Env = iniFile.Section("").Key("app_mode").MustString("development") InstanceName = iniFile.Section("").Key("instance_name").MustString("unknown_instance_name") PluginsPath = makeAbsolute(iniFile.Section("paths").Key("plugins").String(), HomePath) - ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath) + cfg.ProvisioningPath = makeAbsolute(iniFile.Section("paths").Key("provisioning").String(), HomePath) server := iniFile.Section("server") AppUrl, AppSubUrl = parseAppUrlAndSubUrl(server) @@ -719,6 +721,6 @@ func (cfg *Cfg) LogConfigSources() { logger.Info("Path Data", "path", DataPath) logger.Info("Path Logs", "path", LogsPath) logger.Info("Path Plugins", "path", PluginsPath) - logger.Info("Path Provisioning", "path", ProvisioningPath) + logger.Info("Path Provisioning", "path", cfg.ProvisioningPath) logger.Info("App mode " + Env) } From d04ad835e2d902a1d8fae33a871aa43befbbae04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 14:14:42 +0200 Subject: [PATCH 19/77] refactoring: lots of refactoring around server shutdown flows, making sure process is terminated when background service has crashed --- pkg/api/http_server.go | 9 +------ pkg/cmd/grafana-server/main.go | 31 ++++++--------------- pkg/cmd/grafana-server/server.go | 46 +++++++++++++++++--------------- 3 files changed, 33 insertions(+), 53 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index f978a8c220c..2cf8f3796ae 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -64,7 +64,7 @@ func (hs *HTTPServer) Run(ctx context.Context) error { hs.streamManager.Run(ctx) listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) - hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath) + hs.log.Info("HTTP Server Listen", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath) hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron} @@ -74,7 +74,6 @@ func (hs *HTTPServer) Run(ctx context.Context) error { if err := hs.httpSrv.Shutdown(context.Background()); err != nil { hs.log.Error("Failed to shutdown server", "error", err) } - hs.log.Info("Stopped HTTP Server") }() switch setting.Protocol { @@ -113,12 +112,6 @@ func (hs *HTTPServer) Run(ctx context.Context) error { return err } -func (hs *HTTPServer) Shutdown(ctx context.Context) error { - err := hs.httpSrv.Shutdown(ctx) - hs.log.Info("Stopped HTTP server") - return err -} - func (hs *HTTPServer) listenAndServeTLS(certfile, keyfile string) error { if certfile == "" { return fmt.Errorf("cert_file cannot be empty when using HTTPS") diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 466e97ff2d6..23cac74ccdb 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -81,29 +81,20 @@ func main() { setting.Enterprise, _ = strconv.ParseBool(enterprise) metrics.M_Grafana_Version.WithLabelValues(version).Set(1) - shutdownCompleted := make(chan int) + server := NewGrafanaServer() - go listenToSystemSignals(server, shutdownCompleted) + go listenToSystemSignals(server) - go func() { - code := 0 - if err := server.Start(); err != nil { - log.Error2("Startup failed", "error", err) - code = 1 - } + err := server.Start() - exitChan <- code - }() - - code := <-shutdownCompleted - log.Info2("Grafana shutdown completed.", "code", code) + trace.Stop() log.Close() - os.Exit(code) + + server.Exit(err) } -func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int) { - var code int +func listenToSystemSignals(server *GrafanaServerImpl) { signalChan := make(chan os.Signal, 1) ignoreChan := make(chan os.Signal, 1) @@ -112,12 +103,6 @@ func listenToSystemSignals(server *GrafanaServerImpl, shutdownCompleted chan int select { case sig := <-signalChan: - trace.Stop() // Stops trace if profiling has been enabled - server.Shutdown(0, fmt.Sprintf("system signal: %s", sig)) - shutdownCompleted <- 0 - case code = <-exitChan: - trace.Stop() // Stops trace if profiling has been enabled - server.Shutdown(code, "startup error") - shutdownCompleted <- code + server.Shutdown(fmt.Sprintf("System signal: %s", sig)) } } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 05d10ea1afd..aff40111ae9 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -54,11 +54,12 @@ func NewGrafanaServer() *GrafanaServerImpl { } type GrafanaServerImpl struct { - context context.Context - shutdownFn context.CancelFunc - childRoutines *errgroup.Group - log log.Logger - cfg *setting.Cfg + context context.Context + shutdownFn context.CancelFunc + childRoutines *errgroup.Group + log log.Logger + cfg *setting.Cfg + shutdownReason string RouteRegister api.RouteRegister `inject:""` HttpServer *api.HTTPServer `inject:""` @@ -135,7 +136,8 @@ func (g *GrafanaServerImpl) Start() error { } sendSystemdNotification("READY=1") - return g.startHttpServer() + + return g.childRoutines.Wait() } func (g *GrafanaServerImpl) loadConfiguration() { @@ -154,28 +156,28 @@ func (g *GrafanaServerImpl) loadConfiguration() { g.cfg.LogConfigSources() } -func (g *GrafanaServerImpl) startHttpServer() error { - g.HttpServer.Init() - - err := g.HttpServer.Start(g.context) - - if err != nil { - return fmt.Errorf("Fail to start server. error: %v", err) - } - - return nil -} - -func (g *GrafanaServerImpl) Shutdown(code int, reason string) { - g.log.Info("Shutdown started", "code", code, "reason", reason) +func (g *GrafanaServerImpl) Shutdown(reason string) { + g.log.Info("Shutdown started", "reason", reason) + g.shutdownReason = reason // call cancel func on root context g.shutdownFn() // wait for child routines - if err := g.childRoutines.Wait(); err != nil && err != context.Canceled { - g.log.Error("Server shutdown completed", "error", err) + g.childRoutines.Wait() +} + +func (g *GrafanaServerImpl) Exit(reason error) { + // default exit code is 1 + code := 1 + + if reason == context.Canceled && g.shutdownReason != "" { + reason = fmt.Errorf(g.shutdownReason) + code = 0 } + + g.log.Error("Server shutdown", "reason", reason) + os.Exit(code) } func (g *GrafanaServerImpl) writePIDFile() { From 23655315b877c18d0a73e0669e4b8cfea39a2255 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 18:10:21 +0200 Subject: [PATCH 20/77] fix: fixed race condition between http.Server ListenAndServe & Shutdown, now service crash during startup correctly closes http server every time --- pkg/api/http_server.go | 2 ++ pkg/cmd/grafana-server/main.go | 2 +- pkg/cmd/grafana-server/server.go | 34 ++++++++++++++++++++++++-------- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 2cf8f3796ae..f30e5602484 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -71,6 +71,8 @@ func (hs *HTTPServer) Run(ctx context.Context) error { // handle http shutdown on server context done go func() { <-ctx.Done() + // Hacky fix for race condition between ListenAndServe and Shutdown + time.Sleep(time.Millisecond * 100) if err := hs.httpSrv.Shutdown(context.Background()); err != nil { hs.log.Error("Failed to shutdown server", "error", err) } diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 23cac74ccdb..02ea7a7f8f1 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -86,7 +86,7 @@ func main() { go listenToSystemSignals(server) - err := server.Start() + err := server.Run() trace.Stop() log.Close() diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index aff40111ae9..43f6bed1646 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -54,18 +54,19 @@ func NewGrafanaServer() *GrafanaServerImpl { } type GrafanaServerImpl struct { - context context.Context - shutdownFn context.CancelFunc - childRoutines *errgroup.Group - log log.Logger - cfg *setting.Cfg - shutdownReason string + context context.Context + shutdownFn context.CancelFunc + childRoutines *errgroup.Group + log log.Logger + cfg *setting.Cfg + shutdownReason string + shutdownInProgress bool RouteRegister api.RouteRegister `inject:""` HttpServer *api.HTTPServer `inject:""` } -func (g *GrafanaServerImpl) Start() error { +func (g *GrafanaServerImpl) Run() error { g.loadConfiguration() g.writePIDFile() @@ -129,8 +130,24 @@ func (g *GrafanaServerImpl) Start() error { } g.childRoutines.Go(func() error { + // Skip starting new service is we are shutting down + // Ccan happen when service crash during startup + if g.shutdownInProgress { + return nil + } + err := service.Run(g.context) - g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + + // If error is not canceled then the service crashed + if err != context.Canceled { + g.log.Error("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + } else { + g.log.Info("Stopped "+reflect.TypeOf(service).Elem().Name(), "reason", err) + } + + // Mark that we are in shutdown mode + // So more services are not started + g.shutdownInProgress = true return err }) } @@ -159,6 +176,7 @@ func (g *GrafanaServerImpl) loadConfiguration() { func (g *GrafanaServerImpl) Shutdown(reason string) { g.log.Info("Shutdown started", "reason", reason) g.shutdownReason = reason + g.shutdownInProgress = true // call cancel func on root context g.shutdownFn() From e3ea6c683c928bcb84c5710b447b4b8f1ec087ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 18:27:54 +0200 Subject: [PATCH 21/77] fix: comment spell fix --- pkg/cmd/grafana-server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 43f6bed1646..e03018a4167 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -130,8 +130,8 @@ func (g *GrafanaServerImpl) Run() error { } g.childRoutines.Go(func() error { - // Skip starting new service is we are shutting down - // Ccan happen when service crash during startup + // Skip starting new service when shutting down + // Can happen when service stop/return during startup if g.shutdownInProgress { return nil } From b5e70d4607996120e78eca384ce5eb3643b6ff04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 19:00:16 +0200 Subject: [PATCH 22/77] fix: removed unused channel --- pkg/cmd/grafana-server/main.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 02ea7a7f8f1..c7ea6bb432b 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -39,7 +39,6 @@ var enterprise string var configFile = flag.String("config", "", "path to config file") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") var pidFile = flag.String("pidfile", "", "path to pid file") -var exitChan = make(chan int) func main() { v := flag.Bool("v", false, "prints current version and exits") From c40a50829d622f38c51462e13daec5d92bd75326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 2 May 2018 21:30:15 +0200 Subject: [PATCH 23/77] fix: removed manully added http server from inject graph as it is now a self registered service --- pkg/cmd/grafana-server/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index e03018a4167..0f364076a15 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -88,7 +88,6 @@ func (g *GrafanaServerImpl) Run() error { serviceGraph.Provide(&inject.Object{Value: g.cfg}) serviceGraph.Provide(&inject.Object{Value: dashboards.NewProvisioningService()}) serviceGraph.Provide(&inject.Object{Value: api.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) - serviceGraph.Provide(&inject.Object{Value: api.HTTPServer{}}) // self registered services services := registry.GetServices() From 764fa15e2415c9394d1064dc2256de33df691c12 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Wed, 2 May 2018 23:03:37 +0200 Subject: [PATCH 24/77] dont shadow format passed in as function parameter --- public/app/features/templating/template_srv.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index f6274a80165..e7c1dc7f102 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -179,16 +179,16 @@ export class TemplateSrv { return target; } - var variable, systemValue, value; + var variable, systemValue, value, fmt; this.regex.lastIndex = 0; return target.replace(this.regex, (match, var1, var2, fmt2, var3, fmt3) => { variable = this.index[var1 || var2 || var3]; - format = fmt2 || fmt3 || format; + fmt = fmt2 || fmt3 || format; if (scopedVars) { value = scopedVars[var1 || var2 || var3]; if (value) { - return this.formatValue(value.value, format, variable); + return this.formatValue(value.value, fmt, variable); } } @@ -198,7 +198,7 @@ export class TemplateSrv { systemValue = this.grafanaVariables[variable.current.value]; if (systemValue) { - return this.formatValue(systemValue, format, variable); + return this.formatValue(systemValue, fmt, variable); } value = variable.current.value; @@ -210,7 +210,7 @@ export class TemplateSrv { } } - var res = this.formatValue(value, format, variable); + var res = this.formatValue(value, fmt, variable); return res; }); } From fc7d8761588c7dde089b8b0707b239b6d776f6eb Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 00:17:18 +0200 Subject: [PATCH 25/77] try to fix table --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index 66b04ce3d50..ecab274bd32 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,7 +100,7 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string +`regex` | ${servers:regex} | `'test.', 'test2'` | ```(test\\.|test2)``` | Formats multi-value variable into a regex string `pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string @@ -122,4 +122,4 @@ More information in the [Provisioning documentation](/features/datasources/prome ## Changelog Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list -of new features, changes, and bug fixes. \ No newline at end of file +of new features, changes, and bug fixes. From 7f8dacd0879ff31e1d016ee1b066733062ca4c26 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 00:19:47 +0200 Subject: [PATCH 26/77] use ascii code for pipe symbol to not mess up markdown table --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index ecab274bd32..19317074d6e 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | ```(test\\.|test2)``` | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string +`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string +`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string ## Improved workflow for provisioned dashboards From f52920aa01c4c304b393a677b3194c321ca7caaa Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 00:23:35 +0200 Subject: [PATCH 27/77] pipe escape try #3 --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index 19317074d6e..64b9a73eb9b 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.|test2)` | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.|test2` | Formats multi-value variable into a pipe-separated string +`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.`|`test2)` | Formats multi-value variable into a regex string +`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.`|`test2` | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string ## Improved workflow for provisioned dashboards From fc0a4b34a1a91b0b5615123c797a67875c97e9aa Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Thu, 3 May 2018 01:25:52 +0200 Subject: [PATCH 28/77] Add missing items to Gopkg.lock --- Gopkg.lock | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Gopkg.lock b/Gopkg.lock index 3a7466c312a..41fc92313d1 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -111,6 +111,18 @@ ] revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" +[[projects]] + branch = "master" + name = "github.com/facebookgo/inject" + packages = ["."] + revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b" + +[[projects]] + branch = "master" + name = "github.com/facebookgo/structtag" + packages = ["."] + revision = "217e25fb96916cc60332e399c9aa63f5c422ceed" + [[projects]] name = "github.com/fatih/color" packages = ["."] @@ -649,6 +661,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "2bd5b309496d57e2189a1cc28f5c1c41398c19729ba0cf53c8cbb17ea3f706b5" + inputs-digest = "bd54a1a836599d90b36d4ac1af56d716ef9ca5be4865e217bddd49e3d32a1997" solver-name = "gps-cdcl" solver-version = 1 From 83d599670da3832982553327003dbb5f606e6bfe Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 3 May 2018 11:54:02 +0300 Subject: [PATCH 29/77] scroll: remove firefox scrollbars --- public/sass/pages/_dashboard.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index aeb9e28975b..471e90ed9cf 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -44,9 +44,8 @@ div.flot-text { padding: $panel-padding; height: calc(100% - 27px); position: relative; - overflow: hidden; // Fixes scrolling on mobile devices - overflow-y: scroll; + overflow: auto; } .panel-title-container { From d518ed5330e85d7ac192f52c6432d6f65024fbd8 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Thu, 3 May 2018 15:46:21 +0200 Subject: [PATCH 30/77] changelog: add notes for ##11754, #11758, #11710 --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b977edaadf0..3772812f254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) + +# 5.1.1 (unreleased) + +* **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754) +* **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758) +* **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710) + # 5.1.0 (2018-04-26) * **Folders**: Default permissions on folder are not shown as inherited in its dashboards [#11668](https://github.com/grafana/grafana/issues/11668) From 8a9da4ba66f472cee8f4c10791eda18da63a3017 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 3 May 2018 18:42:14 +0200 Subject: [PATCH 31/77] changelog: notes about closing #11690 [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3772812f254..f0c59ccc310 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) - +* **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) # 5.1.1 (unreleased) From a806f542c64a61cf41d7f9fc0620b6a69100100e Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Thu, 3 May 2018 18:42:58 +0200 Subject: [PATCH 32/77] test if default variable interpolation is effective when no specific format is specified --- .../app/features/templating/specs/template_srv.jest.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 5290a883c48..59915776b4f 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -136,6 +136,11 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this=${test:pipe}', {}); expect(target).toBe('this=value1|value2'); }); + + it('should replace ${test:pipe} with piped value and $test with globbed value', function() { + var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + expect(target).toBe('value1|value2,{value1,value2}'); + }); }); describe('variable with all option', function() { @@ -164,6 +169,11 @@ describe('templateSrv', function() { var target = _templateSrv.replace('this.${test:glob}.filters', {}); expect(target).toBe('this.{value1,value2}.filters'); }); + + it('should replace ${test:pipe} with piped value and $test with globbed value', function() { + var target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); + expect(target).toBe('value1|value2,{value1,value2}'); + }); }); describe('variable with all option and custom value', function() { From 4d2e6b4a34a0f675131de7776c30fe2b2b07fa3a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 3 May 2018 19:13:57 +0200 Subject: [PATCH 33/77] changelog: add notes about closing #11800 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c59ccc310..5dd958e237a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * **Graph**: Show invisible highest value bucket in histogram [#11498](https://github.com/grafana/grafana/issues/11498) * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) +* **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) # 5.1.1 (unreleased) From c897485958ff6932bb7277ed85913fad8a1172c0 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 4 May 2018 10:30:42 +0200 Subject: [PATCH 34/77] fixed text color in light theme --- public/sass/components/_timepicker.scss | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public/sass/components/_timepicker.scss b/public/sass/components/_timepicker.scss index 9b71e8e7c05..e4d8f4555e0 100644 --- a/public/sass/components/_timepicker.scss +++ b/public/sass/components/_timepicker.scss @@ -77,7 +77,7 @@ border: none; color: $text-color; &.active span { - color: $blue; + color: $query-blue; font-weight: bold; } .text-info { @@ -88,6 +88,12 @@ font-size: $font-size-sm; padding: 5px 11px; } + &:hover { + color: $text-color-strong; + } + &[disabled] { + color: $text-color; + } } } From 515eab240538b68932e0ee67def5b52b4b25987c Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 4 May 2018 11:46:17 +0200 Subject: [PATCH 35/77] added left:unset to counter left:0 in recent react-select release --- public/sass/components/_form_select_box.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_form_select_box.scss b/public/sass/components/_form_select_box.scss index beee2db15ab..0401f06eba8 100644 --- a/public/sass/components/_form_select_box.scss +++ b/public/sass/components/_form_select_box.scss @@ -102,5 +102,6 @@ $select-option-selected-bg: $dropdownLinkBackgroundActive; .gf-form-input--form-dropdown-right { .Select-menu-outer { right: 0; + left: unset; } } From 8523b1e4105568f63a484f8c7d627e074b300bfa Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 4 May 2018 15:03:30 +0200 Subject: [PATCH 36/77] changelog: add notes about closing #11616 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dd958e237a..1ae35bed3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * **Dashboard**: Enable "Save As..." if user has edit permission [#11625](https://github.com/grafana/grafana/issues/11625) * **Prometheus**: Table columns order now changes when rearrange queries [#11690](https://github.com/grafana/grafana/issues/11690), thx [@mtanda](https://github.com/mtanda) * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) +* **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616) # 5.1.1 (unreleased) From ed4dc241cc1dc1e48eaef72cf06642cd32f7ef68 Mon Sep 17 00:00:00 2001 From: Sven Klemm Date: Sat, 5 May 2018 10:47:44 +0200 Subject: [PATCH 37/77] escape pipe symbol same way as in templating docs --- docs/sources/guides/whats-new-in-v5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/whats-new-in-v5-1.md b/docs/sources/guides/whats-new-in-v5-1.md index 64b9a73eb9b..d992fd9062a 100644 --- a/docs/sources/guides/whats-new-in-v5-1.md +++ b/docs/sources/guides/whats-new-in-v5-1.md @@ -100,8 +100,8 @@ In the table below you can see some examples and you can find all different opti Filter Option | Example | Raw | Interpolated | Description ------------ | ------------- | ------------- | ------------- | ------------- `glob` | ${servers:glob} | `'test1', 'test2'` | `{test1,test2}` | Formats multi-value variable into a glob -`regex` | ${servers:regex} | `'test.', 'test2'` | `(test\\.`|`test2)` | Formats multi-value variable into a regex string -`pipe` | ${servers:pipe} | `'test.', 'test2'` | `test.`|`test2` | Formats multi-value variable into a pipe-separated string +`regex` | ${servers:regex} | `'test.', 'test2'` | (test\.|test2) | Formats multi-value variable into a regex string +`pipe` | ${servers:pipe} | `'test.', 'test2'` | test.|test2 | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string ## Improved workflow for provisioned dashboards From 2ee59ccad84fdaae580c5706c776c32f2da62bd4 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Mon, 7 May 2018 08:03:30 +0200 Subject: [PATCH 38/77] Add panel scrolling docs (#11826) --- docs/sources/plugins/developing/apps.md | 2 +- .../sources/plugins/developing/datasources.md | 2 +- docs/sources/plugins/developing/panels.md | 29 ++++++++++++------- .../sources/plugins/developing/plugin.json.md | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/sources/plugins/developing/apps.md b/docs/sources/plugins/developing/apps.md index a3fc35066f6..155f97461c9 100644 --- a/docs/sources/plugins/developing/apps.md +++ b/docs/sources/plugins/developing/apps.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "Developing App Plugins" parent = "developing" -weight = 6 +weight = 4 +++ # Grafana Apps diff --git a/docs/sources/plugins/developing/datasources.md b/docs/sources/plugins/developing/datasources.md index 09a005ba714..064f3a850ae 100644 --- a/docs/sources/plugins/developing/datasources.md +++ b/docs/sources/plugins/developing/datasources.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "Developing Datasource Plugins" parent = "developing" -weight = 6 +weight = 5 +++ # Datasources diff --git a/docs/sources/plugins/developing/panels.md b/docs/sources/plugins/developing/panels.md index 26db69c7c94..d679288e2d2 100644 --- a/docs/sources/plugins/developing/panels.md +++ b/docs/sources/plugins/developing/panels.md @@ -1,16 +1,11 @@ ---- -page_title: Plugin panel -page_description: Panel plugins for Grafana -page_keywords: grafana, plugins, documentation ---- - - +++ -title = "Installing Plugins" +title = "Developing Panel Plugins" +keywords = ["grafana", "plugins", "panel", "documentation"] type = "docs" [menu.docs] +name = "Developing Panel Plugins" parent = "developing" -weight = 1 +weight = 4 +++ @@ -20,7 +15,21 @@ Panels are the main building blocks of dashboards. ## Panel development -Examples + +### Scrolling +The grafana dashboard framework controls the panel height. To enable a scrollbar within the panel the PanelCtrl needs to set the scrollable static variable: + +```javascript +export class MyPanelCtrl extends PanelCtrl { + static scrollable = true; + ... +``` + +In this case, make sure the template has a single `
...
` root. The plugin loader will modifiy that element adding a scrollbar. + + + +### Examples - [clock-panel](https://github.com/grafana/clock-panel) - [singlestat-panel](https://github.com/grafana/grafana/blob/master/public/app/plugins/panel/singlestat/module.ts) diff --git a/docs/sources/plugins/developing/plugin.json.md b/docs/sources/plugins/developing/plugin.json.md index 7de5e91986f..2d21a665207 100644 --- a/docs/sources/plugins/developing/plugin.json.md +++ b/docs/sources/plugins/developing/plugin.json.md @@ -5,7 +5,7 @@ type = "docs" [menu.docs] name = "plugin.json Schema" parent = "developing" -weight = 6 +weight = 8 +++ # Plugin.json From 1fbac909cb6f3334af1788683bbc1819398f4904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Podlipsk=C3=BD?= Date: Mon, 7 May 2018 08:11:48 +0200 Subject: [PATCH 39/77] Remove preceding `/` from public JS path (#11804) --- scripts/webpack/webpack.common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.common.js b/scripts/webpack/webpack.common.js index c2909b1c8d3..01ef1bc56d3 100644 --- a/scripts/webpack/webpack.common.js +++ b/scripts/webpack/webpack.common.js @@ -12,7 +12,7 @@ module.exports = { output: { path: path.resolve(__dirname, '../../public/build'), filename: '[name].[hash].js', - publicPath: "/public/build/", + publicPath: "public/build/", }, resolve: { extensions: ['.ts', '.tsx', '.es6', '.js', '.json'], From 217d43e512e0f33be6411835e80886ea315b05f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 10:13:39 +0200 Subject: [PATCH 40/77] Update ROADMAP.md --- ROADMAP.md | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index e7bed99489e..adfde503432 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,25 +2,17 @@ This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. But it will give you an idea of our current vision and plan. - + ### Short term (1-2 months) -- v5.1 - - Build speed improvements & integration test execution - - Kubernetes friendly docker container - - Enterprise LDAP - - Provisioning workflow - - MSSQL datasource - -### Mid term (2-4 months) - -- v5.2 - - Azure monitor backend rewrite - Elasticsearch alerting - First login registration view - - Backend plugins? (alert notifiers, auth) - Crossplatform builds - - IFQL Initial support + - Backend service refactorings + +### Mid term (2-4 months) + - Multi-Stat panel + - Explore UI ### Long term (4 - 8 months) From ec7703bad7c4d4dec1315b0df299e8989af850a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 10:14:04 +0200 Subject: [PATCH 41/77] Update ROADMAP.md --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index adfde503432..ad351729309 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,4 +1,4 @@ -# Roadmap (2018-02-22) +# Roadmap (2018-05-06) This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. But it will give you an idea of our current vision and plan. From 871b85f199992fc80a96233d14ab57b68f693d85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 10:16:39 +0200 Subject: [PATCH 42/77] Update ROADMAP.md --- ROADMAP.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ROADMAP.md b/ROADMAP.md index ad351729309..fa3b837585a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -13,6 +13,8 @@ But it will give you an idea of our current vision and plan. ### Mid term (2-4 months) - Multi-Stat panel - Explore UI + - React Panels + - Templating Query Editor UI Plugin hook ### Long term (4 - 8 months) From 5d54bc00e1a958d994c27ee6a76b1909d82e83af Mon Sep 17 00:00:00 2001 From: Florian Plattner Date: Mon, 7 May 2018 10:22:54 +0200 Subject: [PATCH 43/77] Fix/improved csv output (#11740) * fix: initial cleanup and implementation * feat: finish special character escaping * feat: updates fileExport to generate RFC-4180 compliant CSV * chore: replace html decoder with the lodash version and final cleanup * fix: restore character html decoding --- public/app/core/specs/file_export.jest.ts | 100 +++++++++++--- public/app/core/utils/file_export.ts | 158 ++++++++++++++-------- 2 files changed, 185 insertions(+), 73 deletions(-) diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.jest.ts index bbb894094ff..82097227b97 100644 --- a/public/app/core/specs/file_export.jest.ts +++ b/public/app/core/specs/file_export.jest.ts @@ -30,17 +30,17 @@ describe('file_export', () => { it('should export points in proper order', () => { let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); const expectedText = - 'Series;Time;Value\n' + - 'series_1;1500026100;1\n' + - 'series_1;1500026200;2\n' + - 'series_1;1500026300;null\n' + - 'series_1;1500026400;null\n' + - 'series_1;1500026500;null\n' + - 'series_1;1500026600;6\n' + - 'series_2;1500026100;11\n' + - 'series_2;1500026200;12\n' + - 'series_2;1500026300;13\n' + - 'series_2;1500026500;15\n'; + '"Series";"Time";"Value"\r\n' + + '"series_1";"1500026100";1\r\n' + + '"series_1";"1500026200";2\r\n' + + '"series_1";"1500026300";null\r\n' + + '"series_1";"1500026400";null\r\n' + + '"series_1";"1500026500";null\r\n' + + '"series_1";"1500026600";6\r\n' + + '"series_2";"1500026100";11\r\n' + + '"series_2";"1500026200";12\r\n' + + '"series_2";"1500026300";13\r\n' + + '"series_2";"1500026500";15'; expect(text).toBe(expectedText); }); @@ -50,15 +50,79 @@ describe('file_export', () => { it('should export points in proper order', () => { let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); const expectedText = - 'Time;series_1;series_2\n' + - '1500026100;1;11\n' + - '1500026200;2;12\n' + - '1500026300;null;13\n' + - '1500026400;null;null\n' + - '1500026500;null;15\n' + - '1500026600;6;null\n'; + '"Time";"series_1";"series_2"\r\n' + + '"1500026100";1;11\r\n' + + '"1500026200";2;12\r\n' + + '"1500026300";null;13\r\n' + + '"1500026400";null;null\r\n' + + '"1500026500";null;15\r\n' + + '"1500026600";6;null'; expect(text).toBe(expectedText); }); }); + + describe('when exporting table data to csv', () => { + + it('should properly escape special characters and quote all string values', () => { + const inputTable = { + columns: [ + { title: 'integer_value' }, + { text: 'string_value' }, + { title: 'float_value' }, + { text: 'boolean_value' }, + ], + rows: [ + [123, 'some_string', 1.234, true], + [0o765, 'some string with " in the middle', 1e-2, false], + [0o765, 'some string with "" in the middle', 1e-2, false], + [0o765, 'some string with """ in the middle', 1e-2, false], + [0o765, '"some string with " at the beginning', 1e-2, false], + [0o765, 'some string with " at the end"', 1e-2, false], + [0x123, 'some string with \n in the middle', 10.01, false], + [0b1011, 'some string with ; in the middle', -12.34, true], + [123, 'some string with ;; in the middle', -12.34, true], + ], + }; + + const returnedText = fileExport.convertTableDataToCsv(inputTable, false); + + const expectedText = + '"integer_value";"string_value";"float_value";"boolean_value"\r\n' + + '123;"some_string";1.234;true\r\n' + + '501;"some string with "" in the middle";0.01;false\r\n' + + '501;"some string with """" in the middle";0.01;false\r\n' + + '501;"some string with """""" in the middle";0.01;false\r\n' + + '501;"""some string with "" at the beginning";0.01;false\r\n' + + '501;"some string with "" at the end""";0.01;false\r\n' + + '291;"some string with \n in the middle";10.01;false\r\n' + + '11;"some string with ; in the middle";-12.34;true\r\n' + + '123;"some string with ;; in the middle";-12.34;true'; + + expect(returnedText).toBe(expectedText); + }); + + it('should decode HTML encoded characters', function() { + const inputTable = { + columns: [ + { text: 'string_value' }, + ], + rows: [ + ['"&ä'], + ['"some html"'], + ['some text'] + ], + }; + + const returnedText = fileExport.convertTableDataToCsv(inputTable, false); + + const expectedText = + '"string_value"\r\n' + + '"""&ä"\r\n' + + '"""some html"""\r\n' + + '"some text"'; + + expect(returnedText).toBe(expectedText); + }); + }); }); diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 670326fc068..f25d340a0be 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -1,59 +1,108 @@ -import _ from 'lodash'; +import { isBoolean, isNumber, sortedUniq, sortedIndexOf, unescape as htmlUnescaped } from 'lodash'; import moment from 'moment'; import { saveAs } from 'file-saver'; +import { isNullOrUndefined } from 'util'; const DEFAULT_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ssZ'; const POINT_TIME_INDEX = 1; const POINT_VALUE_INDEX = 0; +const END_COLUMN = ';'; +const END_ROW = '\r\n'; +const QUOTE = '"'; +const EXPORT_FILENAME = 'grafana_data_export.csv'; + +function csvEscaped(text) { + if (!text) { + return text; + } + + return text.split(QUOTE).join(QUOTE + QUOTE); +} + +const domParser = new DOMParser(); +function htmlDecoded(text) { + if (!text) { + return text; + } + + const regexp = /&[^;]+;/g; + function htmlDecoded(value) { + const parsedDom = domParser.parseFromString(value, 'text/html'); + return parsedDom.body.textContent; + } + return text.replace(regexp, htmlDecoded).replace(regexp, htmlDecoded); +} + +function formatSpecialHeader(useExcelHeader) { + return useExcelHeader ? `sep=${END_COLUMN}${END_ROW}` : ''; +} + +function formatRow(row, addEndRowDelimiter = true) { + let text = ''; + for (let i = 0; i < row.length; i += 1) { + if (isBoolean(row[i]) || isNullOrUndefined(row[i])) { + text += row[i]; + } else if (isNumber(row[i])) { + text += row[i].toLocaleString(); + } else { + text += `${QUOTE}${csvEscaped(htmlUnescaped(htmlDecoded(row[i])))}${QUOTE}`; + } + + if (i < row.length - 1) { + text += END_COLUMN; + } + } + return addEndRowDelimiter ? text + END_ROW : text; +} + export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = (excel ? 'sep=;\n' : '') + 'Series;Time;Value\n'; - _.each(seriesList, function(series) { - _.each(series.datapoints, function(dp) { - text += - series.alias + ';' + moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat) + ';' + dp[POINT_VALUE_INDEX] + '\n'; - }); - }); + let text = formatSpecialHeader(excel) + formatRow(['Series', 'Time', 'Value']); + for (let seriesIndex = 0; seriesIndex < seriesList.length; seriesIndex += 1) { + for (let i = 0; i < seriesList[seriesIndex].datapoints.length; i += 1) { + text += formatRow( + [ + seriesList[seriesIndex].alias, + moment(seriesList[seriesIndex].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat), + seriesList[seriesIndex].datapoints[i][POINT_VALUE_INDEX], + ], + i < seriesList[seriesIndex].datapoints.length - 1 || seriesIndex < seriesList.length - 1 + ); + } + } return text; } export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - var text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); - saveSaveBlob(text, 'grafana_data_export.csv'); + let text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); + saveSaveBlob(text, EXPORT_FILENAME); } export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = (excel ? 'sep=;\n' : '') + 'Time;'; // add header - _.each(seriesList, function(series) { - text += series.alias + ';'; - }); - text = text.substring(0, text.length - 1); - text += '\n'; - + let text = + formatSpecialHeader(excel) + + formatRow( + ['Time'].concat( + seriesList.map(function(val) { + return val.alias; + }) + ) + ); // process data seriesList = mergeSeriesByTime(seriesList); - var dataArr = [[]]; - var sIndex = 1; - _.each(seriesList, function(series) { - var cIndex = 0; - dataArr.push([]); - _.each(series.datapoints, function(dp) { - dataArr[0][cIndex] = moment(dp[POINT_TIME_INDEX]).format(dateTimeFormat); - dataArr[sIndex][cIndex] = dp[POINT_VALUE_INDEX]; - cIndex++; - }); - sIndex++; - }); // make text - for (var i = 0; i < dataArr[0].length; i++) { - text += dataArr[0][i] + ';'; - for (var j = 1; j < dataArr.length; j++) { - text += dataArr[j][i] + ';'; - } - text = text.substring(0, text.length - 1); - text += '\n'; + for (let i = 0; i < seriesList[0].datapoints.length; i += 1) { + const timestamp = moment(seriesList[0].datapoints[i][POINT_TIME_INDEX]).format(dateTimeFormat); + text += formatRow( + [timestamp].concat( + seriesList.map(function(series) { + return series.datapoints[i][POINT_VALUE_INDEX]; + }) + ), + i < seriesList[0].datapoints.length - 1 + ); } return text; @@ -71,15 +120,15 @@ function mergeSeriesByTime(seriesList) { timestamps.push(seriesPoints[j][POINT_TIME_INDEX]); } } - timestamps = _.sortedUniq(timestamps.sort()); + timestamps = sortedUniq(timestamps.sort()); for (let i = 0; i < seriesList.length; i++) { let seriesPoints = seriesList[i].datapoints; - let seriesTimestamps = _.map(seriesPoints, p => p[POINT_TIME_INDEX]); + let seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); let extendedSeries = []; let pointIndex; for (let j = 0; j < timestamps.length; j++) { - pointIndex = _.sortedIndexOf(seriesTimestamps, timestamps[j]); + pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]); if (pointIndex !== -1) { extendedSeries.push(seriesPoints[pointIndex]); } else { @@ -93,27 +142,26 @@ function mergeSeriesByTime(seriesList) { export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); - saveSaveBlob(text, 'grafana_data_export.csv'); + saveSaveBlob(text, EXPORT_FILENAME); +} + +export function convertTableDataToCsv(table, excel = false) { + let text = formatSpecialHeader(excel); + // add headline + text += formatRow(table.columns.map(val => val.title || val.text)); + // process data + for (let i = 0; i < table.rows.length; i += 1) { + text += formatRow(table.rows[i], i < table.rows.length - 1); + } + return text; } export function exportTableDataToCsv(table, excel = false) { - var text = excel ? 'sep=;\n' : ''; - // add header - _.each(table.columns, function(column) { - text += (column.title || column.text) + ';'; - }); - text += '\n'; - // process data - _.each(table.rows, function(row) { - _.each(row, function(value) { - text += value + ';'; - }); - text += '\n'; - }); - saveSaveBlob(text, 'grafana_data_export.csv'); + let text = convertTableDataToCsv(table, excel); + saveSaveBlob(text, EXPORT_FILENAME); } export function saveSaveBlob(payload, fname) { - var blob = new Blob([payload], { type: 'text/csv;charset=utf-8' }); + let blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); saveAs(blob, fname); } From b4ad044044800b0e9687aeba6850c03dfbce6ff9 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 7 May 2018 04:33:33 -0400 Subject: [PATCH 44/77] better handling for special chars in db config (#11662) --- pkg/services/sqlstore/sqlstore.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index b804d8b1621..cac0c54226c 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -123,7 +123,7 @@ func getEngine() (*xorm.Engine, error) { } cnnstr = fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&allowNativePasswords=true", - DbCfg.User, DbCfg.Pwd, protocol, DbCfg.Host, DbCfg.Name) + url.QueryEscape(DbCfg.User), url.QueryEscape(DbCfg.Pwd), protocol, DbCfg.Host, url.PathEscape(DbCfg.Name)) if DbCfg.SslMode == "true" || DbCfg.SslMode == "skip-verify" { tlsCert, err := makeCert("custom", DbCfg) @@ -142,13 +142,17 @@ func getEngine() (*xorm.Engine, error) { if len(fields) > 1 && len(strings.TrimSpace(fields[1])) > 0 { port = fields[1] } - if DbCfg.Pwd == "" { - DbCfg.Pwd = "''" - } - if DbCfg.User == "" { - DbCfg.User = "''" - } - cnnstr = fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=%s sslcert=%s sslkey=%s sslrootcert=%s", DbCfg.User, DbCfg.Pwd, host, port, DbCfg.Name, DbCfg.SslMode, DbCfg.ClientCertPath, DbCfg.ClientKeyPath, DbCfg.CaCertPath) + cnnstr = fmt.Sprintf("user='%s' password='%s' host='%s' port='%s' dbname='%s' sslmode='%s' sslcert='%s' sslkey='%s' sslrootcert='%s'", + strings.Replace(DbCfg.User, `'`, `\'`, -1), + strings.Replace(DbCfg.Pwd, `'`, `\'`, -1), + strings.Replace(host, `'`, `\'`, -1), + strings.Replace(port, `'`, `\'`, -1), + strings.Replace(DbCfg.Name, `'`, `\'`, -1), + strings.Replace(DbCfg.SslMode, `'`, `\'`, -1), + strings.Replace(DbCfg.ClientCertPath, `'`, `\'`, -1), + strings.Replace(DbCfg.ClientKeyPath, `'`, `\'`, -1), + strings.Replace(DbCfg.CaCertPath, `'`, `\'`, -1), + ) case "sqlite3": if !filepath.IsAbs(DbCfg.Path) { DbCfg.Path = filepath.Join(setting.DataPath, DbCfg.Path) From 543c7fe587d85384b150dad1939101424430a676 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 7 May 2018 04:39:16 -0400 Subject: [PATCH 45/77] support additional fields in authproxy (#11661) --- docs/sources/installation/configuration.md | 4 ++++ pkg/middleware/auth_proxy.go | 11 +++++++++++ pkg/setting/setting.go | 9 +++++++++ 3 files changed, 24 insertions(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index b7fe9040574..baec76df5d9 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -659,6 +659,10 @@ Set to `true` to enable auto sign up of users who do not exist in Grafana DB. De Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. +### headers + +Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. +
## [session] diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index 36b059e4ae7..144a0ae3a69 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -4,6 +4,7 @@ import ( "fmt" "net" "net/mail" + "reflect" "strings" "time" @@ -111,6 +112,16 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { return true } + for _, field := range []string{"Name", "Email", "Login"} { + if setting.AuthProxyHeaders[field] == "" { + continue + } + + if val := ctx.Req.Header.Get(setting.AuthProxyHeaders[field]); val != "" { + reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(val) + } + } + // add/update user in grafana cmd := &m.UpsertUserCommand{ ReqContext: ctx, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 40d7522f775..4db1a2ad7a1 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -125,6 +125,7 @@ var ( AuthProxyAutoSignUp bool AuthProxyLdapSyncTtl int AuthProxyWhitelist string + AuthProxyHeaders map[string]string // Basic Auth BasicAuthEnabled bool @@ -611,6 +612,14 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { AuthProxyLdapSyncTtl = authProxy.Key("ldap_sync_ttl").MustInt() AuthProxyWhitelist = authProxy.Key("whitelist").String() + AuthProxyHeaders = make(map[string]string) + for _, propertyAndHeader := range util.SplitString(authProxy.Key("headers").String()) { + split := strings.SplitN(propertyAndHeader, ":", 2) + if len(split) == 2 { + AuthProxyHeaders[split[0]] = split[1] + } + } + // basic auth authBasic := iniFile.Section("auth.basic") BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) From afec9ec5be0a845619839f255556ccc85f5bd07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 11:16:59 +0200 Subject: [PATCH 46/77] Update ROADMAP.md --- ROADMAP.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index fa3b837585a..7b9c043fef1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,13 +6,13 @@ But it will give you an idea of our current vision and plan. ### Short term (1-2 months) - Elasticsearch alerting - - First login registration view - Crossplatform builds - Backend service refactorings + - Explore UI + - First login registration view ### Mid term (2-4 months) - Multi-Stat panel - - Explore UI - React Panels - Templating Query Editor UI Plugin hook From c3cc60b080a7fb56eae39cebe5dfdbed5f729ef1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 7 May 2018 12:32:18 +0200 Subject: [PATCH 47/77] Support for local Docker builds * add a git-ignored `local/` folder for Makefile/Dockerfile * import `local/Makefile` to root Makefile * add `.dockerignore` --- .dockerignore | 18 ++++++++++++++++++ .gitignore | 2 ++ Makefile | 2 ++ 3 files changed, 22 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..c79fe777899 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,18 @@ +.awcache +.dockerignore +.git +.gitignore +.github +data* +dist +docker +docs +dump.rdb +node_modules +/local +/tmp +/vendor +*.yml +*.md +/vendor +/tmp diff --git a/.gitignore b/.gitignore index cf13dac6d9b..953c98d04aa 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,9 @@ docker-compose.yaml /conf/provisioning/**/custom.yaml profile.cov /grafana +/local .notouch +/Makefile.local /pkg/cmd/grafana-cli/grafana-cli /pkg/cmd/grafana-server/grafana-server /pkg/cmd/grafana-server/debug diff --git a/Makefile b/Makefile index 6f7beb837d8..c1d755d247d 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ +-include local/Makefile + all: deps build deps-go: From 249c1e8d3dbbca74e7a060fccc1952c7179d94b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 May 2018 14:02:38 +0200 Subject: [PATCH 48/77] fix: loading of css url (images/fonts) --- scripts/webpack/webpack.dev.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 26af661bf9d..a2c60cf15c4 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -85,7 +85,7 @@ module.exports = merge(common, { ] }, require('./sass.rule.js')({ - sourceMap: true, minimize: false, preserveUrl: true + sourceMap: true, minimize: false, preserveUrl: false }, extractSass), { test: /\.(ttf|eot|svg|woff(2)?)(\?[a-z0-9=&.]+)?$/, From b804f6d9994f9105af4b394a6c060b890924ea2d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 7 May 2018 14:09:04 +0200 Subject: [PATCH 49/77] changelog: 5.1.1 update add notes about closing #11743 and add release data for 5.1.1 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae35bed3a8..74a9f292000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,12 @@ * **Variables**: Fix variable interpolation when using multiple formatting types [#11800](https://github.com/grafana/grafana/issues/11800), thx [@svenklemm](https://github.com/svenklemm) * **Dashboard**: Fix date selector styling for dark/light theme in time picker control [#11616](https://github.com/grafana/grafana/issues/11616) -# 5.1.1 (unreleased) +# 5.1.1 (2018-05-07) * **LDAP**: LDAP login with MariaDB/MySQL database and dn>100 chars not possible [#11754](https://github.com/grafana/grafana/issues/11754) * **Build**: AppVeyor Windows build missing version and commit info [#11758](https://github.com/grafana/grafana/issues/11758) * **Scroll**: Scroll can't start in graphs on Chrome mobile [#11710](https://github.com/grafana/grafana/issues/11710) +* **Units**: Revert renaming of unit key ppm [#11743](https://github.com/grafana/grafana/issues/11743) # 5.1.0 (2018-04-26) From 29c9d3f74c29d654cdaf5b1e9bda2237a8338c37 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Mon, 7 May 2018 08:18:21 -0400 Subject: [PATCH 50/77] fix root_url in docs & comments (#11819) * fix root_url in docs & comments * include ports in docker-compose config --- docker/blocks/apache_proxy/docker-compose.yaml | 2 +- docker/blocks/nginx_proxy/docker-compose.yaml | 2 +- docs/sources/installation/behind_proxy.md | 4 ++-- docs/sources/tutorials/iis.md | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docker/blocks/apache_proxy/docker-compose.yaml b/docker/blocks/apache_proxy/docker-compose.yaml index 2aec3d4bc4f..86d4befadd6 100644 --- a/docker/blocks/apache_proxy/docker-compose.yaml +++ b/docker/blocks/apache_proxy/docker-compose.yaml @@ -2,7 +2,7 @@ # http://localhost:3000 (Grafana running locally) # # Please note that you'll need to change the root_url in the Grafana configuration: -# root_url = %(protocol)s://%(domain)s:/grafana/ +# root_url = %(protocol)s://%(domain)s:10081/grafana/ apacheproxy: build: blocks/apache_proxy diff --git a/docker/blocks/nginx_proxy/docker-compose.yaml b/docker/blocks/nginx_proxy/docker-compose.yaml index 7c3447ade5c..a0ceceb83ac 100644 --- a/docker/blocks/nginx_proxy/docker-compose.yaml +++ b/docker/blocks/nginx_proxy/docker-compose.yaml @@ -2,7 +2,7 @@ # http://localhost:3000 (Grafana running locally) # # Please note that you'll need to change the root_url in the Grafana configuration: -# root_url = %(protocol)s://%(domain)s:/grafana/ +# root_url = %(protocol)s://%(domain)s:10080/grafana/ nginxproxy: build: blocks/nginx_proxy diff --git a/docs/sources/installation/behind_proxy.md b/docs/sources/installation/behind_proxy.md index f1a00a5b1cc..89711aecb46 100644 --- a/docs/sources/installation/behind_proxy.md +++ b/docs/sources/installation/behind_proxy.md @@ -53,7 +53,7 @@ server { ```bash [server] domain = foo.bar -root_url = %(protocol)s://%(domain)s:/grafana +root_url = %(protocol)s://%(domain)s/grafana/ ``` #### Nginx configuration with sub path @@ -98,7 +98,7 @@ Given: ```bash [server] domain = localhost:8080 - root_url = %(protocol)s://%(domain)s:/grafana + root_url = %(protocol)s://%(domain)s/grafana/ ``` Create an Inbound Rule for the parent website (localhost:8080 in this example) in IIS Manager with the following settings: diff --git a/docs/sources/tutorials/iis.md b/docs/sources/tutorials/iis.md index 63a41d67c16..896181c4a9f 100644 --- a/docs/sources/tutorials/iis.md +++ b/docs/sources/tutorials/iis.md @@ -16,7 +16,7 @@ Example: - Parent site: http://localhost:8080 - Grafana: http://localhost:3000 -Grafana as a subpath: http://localhost:8080/grafana +Grafana as a subpath: http://localhost:8080/grafana ## Setup @@ -33,7 +33,7 @@ Given that the subpath should be `grafana` and the parent site is `localhost:808 ```bash [server] domain = localhost:8080 -root_url = %(protocol)s://%(domain)s:/grafana +root_url = %(protocol)s://%(domain)s/grafana/ ``` Restart the Grafana server after changing the config file. @@ -74,11 +74,11 @@ When navigating to the grafana url (`http://localhost:8080/grafana` in the examp 1. The `root_url` setting in the Grafana config file does not match the parent url with subpath. This could happen if the root_url is commented out by mistake (`;` is used for commenting out a line in .ini files): - `; root_url = %(protocol)s://%(domain)s:/grafana` + `; root_url = %(protocol)s://%(domain)s/grafana/` 2. or if the subpath in the `root_url` setting does not match the subpath used in the pattern in the Inbound Rule in IIS: - `root_url = %(protocol)s://%(domain)s:/grafana` + `root_url = %(protocol)s://%(domain)s/grafana/` pattern in Inbound Rule: `wrongsubpath(/)?(.*)` From 23c88a3b3f5c215a196d977c17a00d9b3903c6df Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 7 May 2018 14:57:18 +0200 Subject: [PATCH 51/77] docs: update installation instructions targeting v5.1.1 stable --- docs/sources/installation/debian.md | 6 +++--- docs/sources/installation/rpm.md | 10 +++++----- docs/sources/installation/windows.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index dccb880ec74..83d26351295 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -15,7 +15,7 @@ weight = 1 Description | Download ------------ | ------------- -Stable for Debian-based Linux | [grafana_5.1.0_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb) +Stable for Debian-based Linux | [grafana_5.1.1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.1_amd64.deb) @@ -27,9 +27,9 @@ installation. ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.0_amd64.deb +wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.1_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.0_amd64.deb +sudo dpkg -i grafana_5.1.1_amd64.deb ``` @@ -28,7 +28,7 @@ installation. You can install Grafana using Yum directly. ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.0-1.x86_64.rpm +$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.1-1.x86_64.rpm ```