mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into alerting_reminder
* master: (30 commits) changelog: add notes about closing #11882 renamed variable in tests added comment, variableChange -> variableValueChange added a test added if to check if new variable has been added Gravatar fallback does not respect 'AppSubUrl'-setting (#12149) change admin password after first login changelog: adds note about closing #11958 revert: reverted singlestat panel position change PR #12004 Revert "provisioning: turn relative symlinked path into absolut paths" provisioning: turn relative symlinked path into absolut paths changelog: adds note about closing #11670 elasticsearch: sort bucket keys to fix issue wth response parser tests docs: what's new in v5.2 changelog: add notes about closing #11167 docs: docker secrets support. (#12141) alerting: show alerts for user with Viewer role datasource: added option no-direct-access to ds-http-settings diretive, closes #12138 provisioning: adds fallback if evalsymlink/abs fails tests: uses different paths depending on os ...
This commit is contained in:
commit
0e647db485
@ -3,6 +3,7 @@
|
||||
### New Features
|
||||
|
||||
* **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
|
||||
* **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882)
|
||||
* **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541)
|
||||
|
||||
### Minor
|
||||
@ -30,6 +31,10 @@
|
||||
* **Singlestat**: Fix singlestat threshold tooltip [#11971](https://github.com/grafana/grafana/issues/11971)
|
||||
* **Dashboard**: Hide grid controls in fullscreen/low-activity views [#11771](https://github.com/grafana/grafana/issues/11771)
|
||||
* **Dashboard**: Validate uid when importing dashboards [#11515](https://github.com/grafana/grafana/issues/11515)
|
||||
* **Docker**: Support for env variables ending with _FILE [grafana-docker #166](https://github.com/grafana/grafana-docker/pull/166), thx [@efrecon](https://github.com/efrecon)
|
||||
* **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167)
|
||||
* **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670)
|
||||
* **Provisioning**: Support symlinked files in dashboard provisioning config files [#11958](https://github.com/grafana/grafana/issues/11958)
|
||||
|
||||
# 5.1.3 (2018-05-16)
|
||||
|
||||
|
@ -1137,4 +1137,4 @@
|
||||
"title": "Big Dashboard",
|
||||
"uid": "000000003",
|
||||
"version": 16
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ bulkDashboard() {
|
||||
MAX=400
|
||||
while [ $COUNTER -lt $MAX ]; do
|
||||
jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }"
|
||||
let COUNTER=COUNTER+1
|
||||
let COUNTER=COUNTER+1
|
||||
done
|
||||
|
||||
ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml
|
||||
@ -58,4 +58,4 @@ main() {
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
main "$@"
|
||||
|
@ -197,6 +197,7 @@ providers:
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
```
|
||||
|
70
docs/sources/guides/whats-new-in-v5-2.md
Normal file
70
docs/sources/guides/whats-new-in-v5-2.md
Normal file
@ -0,0 +1,70 @@
|
||||
+++
|
||||
title = "What's New in Grafana v5.2"
|
||||
description = "Feature & improvement highlights for Grafana v5.2"
|
||||
keywords = ["grafana", "new", "documentation", "5.2"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 5.2"
|
||||
identifier = "v5.2"
|
||||
parent = "whatsnew"
|
||||
weight = -8
|
||||
+++
|
||||
|
||||
# What's New in Grafana v5.2
|
||||
|
||||
Grafana v5.2 brings new features, many enhancements and bug fixes. This article will detail the major new features and enhancements.
|
||||
|
||||
* [Elasticsearch alerting]({{< relref "#elasticsearch-alerting" >}}) it's finally here!
|
||||
* [Cross platform build support]({{< relref "#cross-platform-build-support" >}}) enables native builds of Grafana for many more platforms!
|
||||
* [Improved Docker image]({{< relref "#improved-docker-image" >}}) with support for docker secrets
|
||||
* [Prometheus]({{< relref "#prometheus" >}}) with alignment enhancements
|
||||
* [Alerting]({{< relref "#alerting" >}}) with alert notification channel type for Discord
|
||||
* [Dashboards & Panels]({{< relref "#dashboards-panels" >}})
|
||||
|
||||
## Elasticsearch alerting
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v52/elasticsearch_alerting.png" max-width="800px" class="docs-image--right" >}}
|
||||
|
||||
Grafana v5.2 ships with an updated Elasticsearch datasource with support for alerting. Alerting support for Elasticsearch has been one of
|
||||
the most requested features by our community and now it's finally here. Please try it out and let us know what you think.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## Cross platform build support
|
||||
|
||||
Grafana v5.2 brings an improved build pipeline with cross platform support. This enables native builds of Grafana for ARMv7 (x32), ARM64 (x64),
|
||||
MacOS/Darwin (x64) and Windows (x64) in both stable and nightly builds.
|
||||
|
||||
We've been longing for native ARM build support for a long time. With the help from our amazing community this is now finally available.
|
||||
|
||||
## Improved Docker image
|
||||
|
||||
The Grafana docker image now includes support for Docker secrets which enables you to supply Grafana with configuration through files. More
|
||||
information in the [Installing using Docker documentation](/installation/docker/#reading-secrets-from-files-support-for-docker-secrets).
|
||||
|
||||
## Prometheus
|
||||
|
||||
The Prometheus datasource now aligns the start/end of the query sent to Prometheus with the step, which ensures PromQL expressions with *rate*
|
||||
functions get consistent results, and thus avoid graphs jumping around on reload.
|
||||
|
||||
## Alerting
|
||||
|
||||
By popular demand Grafana now includes support for an alert notification channel type for [Discord](https://discordapp.com/).
|
||||
|
||||
## Dashboards & Panels
|
||||
|
||||
### Modified time range and variables are no longer saved by default
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v52/dashboard_save_modal.png" max-width="800px" class="docs-image--right" >}}
|
||||
|
||||
Starting from Grafana v5.2 a modified time range or variable are no longer saved by default. To save a modified
|
||||
time range or variable you'll need to actively select that when saving a dashboard, see screenshot.
|
||||
This should hopefully make it easier to have sane defaults of time and variables in dashboards and make it more explicit
|
||||
when you actually want to overwrite those settings.
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
## 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.
|
@ -130,6 +130,18 @@ ID=$(id -u) # saves your user id in the ID variable
|
||||
docker run -d --user $ID --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 grafana/grafana:5.1.0
|
||||
```
|
||||
|
||||
## Reading secrets from files (support for Docker Secrets)
|
||||
|
||||
It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/<name of secret>` of the container.
|
||||
|
||||
You can do this with any of the configuration options in conf/grafana.ini by setting `GF_<SectionName>_<KeyName>_FILE` to the path of the file holding the secret.
|
||||
|
||||
Let's say you want to set the admin password this way.
|
||||
|
||||
- Admin password secret: `/run/secrets/admin_password`
|
||||
- Environment variable: `GF_SECURITY_ADMIN_PASSWORD_FILE=/run/secrets/admin_password`
|
||||
|
||||
|
||||
## Migration from a previous version of the docker container to 5.1 or later
|
||||
|
||||
The docker container for Grafana has seen a major rewrite for 5.1.
|
||||
|
@ -79,7 +79,7 @@ func GetAlerts(c *m.ReqContext) Response {
|
||||
DashboardIds: dashboardIDs,
|
||||
Type: string(search.DashHitDB),
|
||||
FolderIds: folderIDs,
|
||||
Permission: m.PERMISSION_EDIT,
|
||||
Permission: m.PERMISSION_VIEW,
|
||||
}
|
||||
|
||||
err := bus.Dispatch(&searchQuery)
|
||||
|
@ -77,6 +77,9 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
|
||||
r.Get("/explore/", reqEditorRole, Index)
|
||||
r.Get("/explore/*", reqEditorRole, Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, Index)
|
||||
r.Get("/playlists/*", reqSignedIn, Index)
|
||||
r.Get("/alerting/", reqSignedIn, Index)
|
||||
|
@ -52,7 +52,7 @@ type UserStars struct {
|
||||
|
||||
func GetGravatarUrl(text string) string {
|
||||
if setting.DisableGravatar {
|
||||
return "/public/img/user_profile.png"
|
||||
return setting.AppSubUrl + "/public/img/user_profile.png"
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
|
@ -128,7 +128,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
Children: dashboardChildNavs,
|
||||
})
|
||||
|
||||
if setting.ExploreEnabled {
|
||||
if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
||||
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
||||
Text: "Explore",
|
||||
Id: "explore",
|
||||
|
@ -254,6 +254,7 @@ type DashboardProvisioning struct {
|
||||
DashboardId int64
|
||||
Name string
|
||||
ExternalId string
|
||||
CheckSum string
|
||||
Updated int64
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,10 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
|
||||
if dashboards[i].OrgId == 0 {
|
||||
dashboards[i].OrgId = 1
|
||||
}
|
||||
|
||||
if dashboards[i].UpdateIntervalSeconds == 0 {
|
||||
dashboards[i].UpdateIntervalSeconds = 3
|
||||
}
|
||||
}
|
||||
|
||||
return dashboards, nil
|
||||
|
@ -22,7 +22,7 @@ func TestDashboardsAsConfig(t *testing.T) {
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
validateDashboardAsConfig(cfg)
|
||||
validateDashboardAsConfig(t, cfg)
|
||||
})
|
||||
|
||||
Convey("Can read config file in version 0 format", func() {
|
||||
@ -30,7 +30,7 @@ func TestDashboardsAsConfig(t *testing.T) {
|
||||
cfg, err := cfgProvider.readConfig()
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
validateDashboardAsConfig(cfg)
|
||||
validateDashboardAsConfig(t, cfg)
|
||||
})
|
||||
|
||||
Convey("Should skip invalid path", func() {
|
||||
@ -56,7 +56,9 @@ func TestDashboardsAsConfig(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
func validateDashboardAsConfig(t *testing.T, cfg []*DashboardsAsConfig) {
|
||||
t.Helper()
|
||||
|
||||
So(len(cfg), ShouldEqual, 2)
|
||||
|
||||
ds := cfg[0]
|
||||
@ -68,6 +70,7 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
So(len(ds.Options), ShouldEqual, 1)
|
||||
So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds.DisableDeletion, ShouldBeTrue)
|
||||
So(ds.UpdateIntervalSeconds, ShouldEqual, 10)
|
||||
|
||||
ds2 := cfg[1]
|
||||
So(ds2.Name, ShouldEqual, "default")
|
||||
@ -78,4 +81,5 @@ func validateDashboardAsConfig(cfg []*DashboardsAsConfig) {
|
||||
So(len(ds2.Options), ShouldEqual, 1)
|
||||
So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
So(ds2.DisableDeletion, ShouldBeFalse)
|
||||
So(ds2.UpdateIntervalSeconds, ShouldEqual, 3)
|
||||
}
|
||||
|
@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
@ -19,8 +21,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
checkDiskForChangesInterval = time.Second * 3
|
||||
|
||||
ErrFolderNameMissing = errors.New("Folder name missing")
|
||||
)
|
||||
|
||||
@ -47,15 +47,25 @@ func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReade
|
||||
log.Error("Cannot read directory", "error", err)
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
copy := path
|
||||
path, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
log.Error("Could not create absolute path ", "path", path)
|
||||
absPath = path //if .Abs return an error we fallback to path
|
||||
}
|
||||
|
||||
path, err = filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
log.Error("Failed to read content of symlinked path: %s", path)
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = copy
|
||||
log.Info("falling back to original path due to EvalSymlink/Abs failure")
|
||||
}
|
||||
|
||||
return &fileReader{
|
||||
Cfg: cfg,
|
||||
Path: absPath,
|
||||
Path: path,
|
||||
log: log,
|
||||
dashboardService: dashboards.NewProvisioningService(),
|
||||
}, nil
|
||||
@ -66,7 +76,7 @@ func (fr *fileReader) ReadAndListen(ctx context.Context) error {
|
||||
fr.log.Error("failed to search for dashboards", "error", err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(checkDiskForChangesInterval)
|
||||
ticker := time.NewTicker(time.Duration(int64(time.Second) * fr.Cfg.UpdateIntervalSeconds))
|
||||
|
||||
running := false
|
||||
|
||||
@ -159,15 +169,20 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
||||
}
|
||||
|
||||
provisionedData, alreadyProvisioned := provisionedDashboardRefs[path]
|
||||
upToDate := alreadyProvisioned && provisionedData.Updated == resolvedFileInfo.ModTime().Unix()
|
||||
upToDate := alreadyProvisioned && provisionedData.Updated >= resolvedFileInfo.ModTime().Unix()
|
||||
|
||||
dash, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
||||
jsonFile, err := fr.readDashboardFromFile(path, resolvedFileInfo.ModTime(), folderId)
|
||||
if err != nil {
|
||||
fr.log.Error("failed to load dashboard from ", "file", path, "error", err)
|
||||
return provisioningMetadata, nil
|
||||
}
|
||||
|
||||
if provisionedData != nil && jsonFile.checkSum == provisionedData.CheckSum {
|
||||
upToDate = true
|
||||
}
|
||||
|
||||
// keeps track of what uid's and title's we have already provisioned
|
||||
dash := jsonFile.dashboard
|
||||
provisioningMetadata.uid = dash.Dashboard.Uid
|
||||
provisioningMetadata.title = dash.Dashboard.Title
|
||||
|
||||
@ -185,7 +200,13 @@ func (fr *fileReader) saveDashboard(path string, folderId int64, fileInfo os.Fil
|
||||
}
|
||||
|
||||
fr.log.Debug("saving new dashboard", "file", path)
|
||||
dp := &models.DashboardProvisioning{ExternalId: path, Name: fr.Cfg.Name, Updated: resolvedFileInfo.ModTime().Unix()}
|
||||
dp := &models.DashboardProvisioning{
|
||||
ExternalId: path,
|
||||
Name: fr.Cfg.Name,
|
||||
Updated: resolvedFileInfo.ModTime().Unix(),
|
||||
CheckSum: jsonFile.checkSum,
|
||||
}
|
||||
|
||||
_, err = fr.dashboardService.SaveProvisionedDashboard(dash, dp)
|
||||
return provisioningMetadata, err
|
||||
}
|
||||
@ -283,14 +304,30 @@ func validateWalkablePath(fileInfo os.FileInfo) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
type dashboardJsonFile struct {
|
||||
dashboard *dashboards.SaveDashboardDTO
|
||||
checkSum string
|
||||
lastModified time.Time
|
||||
}
|
||||
|
||||
func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, folderId int64) (*dashboardJsonFile, error) {
|
||||
reader, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
data, err := simplejson.NewFromReader(reader)
|
||||
all, err := ioutil.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkSum, err := util.Md5SumString(string(all))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := simplejson.NewJson(all)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -300,7 +337,11 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dash, nil
|
||||
return &dashboardJsonFile{
|
||||
dashboard: dash,
|
||||
checkSum: checkSum,
|
||||
lastModified: lastModified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type provisioningMetadata struct {
|
||||
@ -328,7 +369,6 @@ func (checker provisioningSanityChecker) track(pm provisioningMetadata) {
|
||||
if len(pm.title) > 0 {
|
||||
checker.titleUsage[pm.title] += 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
||||
@ -343,5 +383,4 @@ func (checker provisioningSanityChecker) logWarnings(log log.Logger) {
|
||||
log.Error("the same 'title' is used more than once", "title", title, "provider", checker.provisioningProvider)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,39 @@
|
||||
// +build linux
|
||||
|
||||
package dashboards
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
)
|
||||
|
||||
var (
|
||||
symlinkedFolder = "testdata/test-dashboards/symlink"
|
||||
)
|
||||
|
||||
func TestProvsionedSymlinkedFolder(t *testing.T) {
|
||||
cfg := &DashboardsAsConfig{
|
||||
Name: "Default",
|
||||
Type: "file",
|
||||
OrgId: 1,
|
||||
Folder: "",
|
||||
Options: map[string]interface{}{"path": symlinkedFolder},
|
||||
}
|
||||
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
if err != nil {
|
||||
t.Error("expected err to be nil")
|
||||
}
|
||||
|
||||
want, err := filepath.Abs(containingId)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("expected err to be nill")
|
||||
}
|
||||
|
||||
if reader.Path != want {
|
||||
t.Errorf("got %s want %s", reader.Path, want)
|
||||
}
|
||||
}
|
@ -49,13 +49,16 @@ func TestCreatingNewDashboardFileReader(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("using full path", func() {
|
||||
cfg.Options["folder"] = "/var/lib/grafana/dashboards"
|
||||
fullPath := "/var/lib/grafana/dashboards"
|
||||
if runtime.GOOS == "windows" {
|
||||
fullPath = `c:\var\lib\grafana`
|
||||
}
|
||||
|
||||
cfg.Options["folder"] = fullPath
|
||||
reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
So(reader.Path, ShouldEqual, "/var/lib/grafana/dashboards")
|
||||
}
|
||||
So(reader.Path, ShouldEqual, fullPath)
|
||||
So(filepath.IsAbs(reader.Path), ShouldBeTrue)
|
||||
})
|
||||
|
||||
|
@ -6,6 +6,7 @@ providers:
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
@ -3,6 +3,7 @@
|
||||
folder: 'developers'
|
||||
editable: true
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
type: file
|
||||
options:
|
||||
path: /var/lib/grafana/dashboards
|
||||
|
1
pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink
vendored
Symbolic link
1
pkg/services/provisioning/dashboards/testdata/test-dashboards/symlink
vendored
Symbolic link
@ -0,0 +1 @@
|
||||
containing-id/
|
@ -10,23 +10,25 @@ import (
|
||||
)
|
||||
|
||||
type DashboardsAsConfig struct {
|
||||
Name string
|
||||
Type string
|
||||
OrgId int64
|
||||
Folder string
|
||||
Editable bool
|
||||
Options map[string]interface{}
|
||||
DisableDeletion bool
|
||||
Name string
|
||||
Type string
|
||||
OrgId int64
|
||||
Folder string
|
||||
Editable bool
|
||||
Options map[string]interface{}
|
||||
DisableDeletion bool
|
||||
UpdateIntervalSeconds int64
|
||||
}
|
||||
|
||||
type DashboardsAsConfigV0 struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
|
||||
}
|
||||
|
||||
type ConfigVersion struct {
|
||||
@ -38,13 +40,14 @@ type DashboardAsConfigV1 struct {
|
||||
}
|
||||
|
||||
type DashboardProviderConfigs struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
OrgId int64 `json:"orgId" yaml:"orgId"`
|
||||
Folder string `json:"folder" yaml:"folder"`
|
||||
Editable bool `json:"editable" yaml:"editable"`
|
||||
Options map[string]interface{} `json:"options" yaml:"options"`
|
||||
DisableDeletion bool `json:"disableDeletion" yaml:"disableDeletion"`
|
||||
UpdateIntervalSeconds int64 `json:"updateIntervalSeconds" yaml:"updateIntervalSeconds"`
|
||||
}
|
||||
|
||||
func createDashboardJson(data *simplejson.Json, lastModified time.Time, cfg *DashboardsAsConfig, folderId int64) (*dashboards.SaveDashboardDTO, error) {
|
||||
@ -68,13 +71,14 @@ func mapV0ToDashboardAsConfig(v0 []*DashboardsAsConfigV0) []*DashboardsAsConfig
|
||||
|
||||
for _, v := range v0 {
|
||||
r = append(r, &DashboardsAsConfig{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
UpdateIntervalSeconds: v.UpdateIntervalSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
@ -86,13 +90,14 @@ func (dc *DashboardAsConfigV1) mapToDashboardAsConfig() []*DashboardsAsConfig {
|
||||
|
||||
for _, v := range dc.Providers {
|
||||
r = append(r, &DashboardsAsConfig{
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
Name: v.Name,
|
||||
Type: v.Type,
|
||||
OrgId: v.OrgId,
|
||||
Folder: v.Folder,
|
||||
Editable: v.Editable,
|
||||
Options: v.Options,
|
||||
DisableDeletion: v.DisableDeletion,
|
||||
UpdateIntervalSeconds: v.UpdateIntervalSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
|
||||
}
|
||||
|
||||
if query.User.OrgRole != m.ROLE_ADMIN {
|
||||
builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_EDIT)
|
||||
builder.writeDashboardPermissionFilter(query.User, m.PERMISSION_VIEW)
|
||||
}
|
||||
|
||||
builder.Write(" ORDER BY name ASC")
|
||||
|
@ -2,7 +2,6 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -110,11 +109,12 @@ func TestAlertingDataAccess(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Viewer cannot read alerts", func() {
|
||||
alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: &m.SignedInUser{OrgRole: m.ROLE_VIEWER}}
|
||||
viewerUser := &m.SignedInUser{OrgRole: m.ROLE_VIEWER, OrgId: 1}
|
||||
alertQuery := m.GetAlertsQuery{DashboardIDs: []int64{testDash.Id}, PanelId: 1, OrgId: 1, User: viewerUser}
|
||||
err2 := HandleAlertsQuery(&alertQuery)
|
||||
|
||||
So(err2, ShouldBeNil)
|
||||
So(alertQuery.Result, ShouldHaveLength, 0)
|
||||
So(alertQuery.Result, ShouldHaveLength, 1)
|
||||
})
|
||||
|
||||
Convey("Alerts with same dashboard id and panel id should update", func() {
|
||||
|
@ -211,4 +211,8 @@ func addDashboardMigration(mg *Migrator) {
|
||||
"name": "name",
|
||||
"external_id": "external_id",
|
||||
})
|
||||
|
||||
mg.AddMigration("Add check_sum column", NewAddColumnMigration(dashboardExtrasTableV2, &Column{
|
||||
Name: "check_sum", Type: DB_NVarchar, Length: 32, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
@ -113,15 +113,22 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range esAgg.Get("buckets").MustMap() {
|
||||
bucket := simplejson.NewFromAny(v)
|
||||
buckets := esAgg.Get("buckets").MustMap()
|
||||
bucketKeys := make([]string, 0)
|
||||
for k := range buckets {
|
||||
bucketKeys = append(bucketKeys, k)
|
||||
}
|
||||
sort.Strings(bucketKeys)
|
||||
|
||||
for _, bucketKey := range bucketKeys {
|
||||
bucket := simplejson.NewFromAny(buckets[bucketKey])
|
||||
newProps := make(map[string]string, 0)
|
||||
|
||||
for k, v := range props {
|
||||
newProps[k] = v
|
||||
}
|
||||
|
||||
newProps["filter"] = k
|
||||
newProps["filter"] = bucketKey
|
||||
|
||||
err = rp.processBuckets(bucket.MustMap(), target, series, table, newProps, depth+1)
|
||||
if err != nil {
|
||||
|
26
pkg/util/md5.go
Normal file
26
pkg/util/md5.go
Normal file
@ -0,0 +1,26 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Md5Sum calculates the md5sum of a stream
|
||||
func Md5Sum(reader io.Reader) (string, error) {
|
||||
var returnMD5String string
|
||||
hash := md5.New()
|
||||
if _, err := io.Copy(hash, reader); err != nil {
|
||||
return returnMD5String, err
|
||||
}
|
||||
hashInBytes := hash.Sum(nil)[:16]
|
||||
returnMD5String = hex.EncodeToString(hashInBytes)
|
||||
return returnMD5String, nil
|
||||
}
|
||||
|
||||
// Md5Sum calculates the md5sum of a string
|
||||
func Md5SumString(input string) (string, error) {
|
||||
buffer := strings.NewReader(input)
|
||||
return Md5Sum(buffer)
|
||||
}
|
17
pkg/util/md5_test.go
Normal file
17
pkg/util/md5_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMd5Sum(t *testing.T) {
|
||||
input := "dont hash passwords with md5"
|
||||
|
||||
have, err := Md5SumString(input)
|
||||
if err != nil {
|
||||
t.Fatal("expected err to be nil")
|
||||
}
|
||||
|
||||
want := "2d6a56c82d09d374643b926d3417afba"
|
||||
if have != want {
|
||||
t.Fatalf("expected: %s got: %s", want, have)
|
||||
}
|
||||
}
|
@ -11,10 +11,15 @@ export class LoginCtrl {
|
||||
password: '',
|
||||
};
|
||||
|
||||
$scope.command = {};
|
||||
$scope.result = '';
|
||||
|
||||
contextSrv.sidemenu = false;
|
||||
|
||||
$scope.oauth = config.oauth;
|
||||
$scope.oauthEnabled = _.keys(config.oauth).length > 0;
|
||||
$scope.ldapEnabled = config.ldapEnabled;
|
||||
$scope.authProxyEnabled = config.authProxyEnabled;
|
||||
|
||||
$scope.disableLoginForm = config.disableLoginForm;
|
||||
$scope.disableUserSignUp = config.disableUserSignUp;
|
||||
@ -39,6 +44,43 @@ export class LoginCtrl {
|
||||
}
|
||||
};
|
||||
|
||||
$scope.changeView = function() {
|
||||
let loginView = document.querySelector('#login-view');
|
||||
let changePasswordView = document.querySelector('#change-password-view');
|
||||
|
||||
loginView.className += ' add';
|
||||
setTimeout(() => {
|
||||
loginView.className += ' hidden';
|
||||
}, 250);
|
||||
setTimeout(() => {
|
||||
changePasswordView.classList.remove('hidden');
|
||||
}, 251);
|
||||
setTimeout(() => {
|
||||
changePasswordView.classList.remove('remove');
|
||||
}, 301);
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('newPassword').focus();
|
||||
}, 400);
|
||||
};
|
||||
|
||||
$scope.changePassword = function() {
|
||||
$scope.command.oldPassword = 'admin';
|
||||
|
||||
if ($scope.command.newPassword !== $scope.command.confirmNew) {
|
||||
$scope.appEvent('alert-warning', ['New passwords do not match', '']);
|
||||
return;
|
||||
}
|
||||
|
||||
backendSrv.put('/api/user/password', $scope.command).then(function() {
|
||||
$scope.toGrafana();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.skip = function() {
|
||||
$scope.toGrafana();
|
||||
};
|
||||
|
||||
$scope.loginModeChanged = function(newValue) {
|
||||
$scope.submitBtnText = newValue ? 'Log in' : 'Sign up';
|
||||
};
|
||||
@ -65,18 +107,28 @@ export class LoginCtrl {
|
||||
}
|
||||
|
||||
backendSrv.post('/login', $scope.formModel).then(function(result) {
|
||||
var params = $location.search();
|
||||
$scope.result = result;
|
||||
|
||||
if (params.redirect && params.redirect[0] === '/') {
|
||||
window.location.href = config.appSubUrl + params.redirect;
|
||||
} else if (result.redirectUrl) {
|
||||
window.location.href = result.redirectUrl;
|
||||
} else {
|
||||
window.location.href = config.appSubUrl + '/';
|
||||
if ($scope.formModel.password !== 'admin' || $scope.ldapEnabled || $scope.authProxyEnabled) {
|
||||
$scope.toGrafana();
|
||||
return;
|
||||
}
|
||||
$scope.changeView();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toGrafana = function() {
|
||||
var params = $location.search();
|
||||
|
||||
if (params.redirect && params.redirect[0] === '/') {
|
||||
window.location.href = config.appSubUrl + params.redirect;
|
||||
} else if ($scope.result.redirectUrl) {
|
||||
window.location.href = $scope.result.redirectUrl;
|
||||
} else {
|
||||
window.location.href = config.appSubUrl + '/';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ export class KeybindingSrv {
|
||||
timepickerOpen = false;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv) {
|
||||
constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) {
|
||||
// clear out all shortcuts on route change
|
||||
$rootScope.$on('$routeChangeSuccess', () => {
|
||||
Mousetrap.reset();
|
||||
@ -177,21 +177,24 @@ 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 range = this.timeSrv.timeRangeForUrl();
|
||||
const state = {
|
||||
...datasource.getExploreState(panel),
|
||||
range,
|
||||
};
|
||||
const exploreState = encodePathComponent(JSON.stringify(state));
|
||||
this.$location.url(`/explore/${exploreState}`);
|
||||
// jump to explore if permissions allow
|
||||
if (this.contextSrv.isEditor) {
|
||||
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 range = this.timeSrv.timeRangeForUrl();
|
||||
const state = {
|
||||
...datasource.getExploreState(panel),
|
||||
range,
|
||||
};
|
||||
const exploreState = encodePathComponent(JSON.stringify(state));
|
||||
this.$location.url(`/explore/${exploreState}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// delete panel
|
||||
this.bind('p r', () => {
|
||||
|
@ -16,13 +16,13 @@ const template = `
|
||||
|
||||
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content" novalidate>
|
||||
<div class="p-t-1">
|
||||
<div class="gf-form-group" ng-if="ctrl.timeChange || ctrl.variableChange">
|
||||
<div class="gf-form-group" ng-if="ctrl.timeChange || ctrl.variableValueChange">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Save current time range" ng-if="ctrl.timeChange" label-class="width-12" switch-class="max-width-6"
|
||||
checked="ctrl.saveTimerange" on-change="buildUrl()">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Save current variables" ng-if="ctrl.variableChange" label-class="width-12" switch-class="max-width-6"
|
||||
label="Save current variables" ng-if="ctrl.variableValueChange" label-class="width-12" switch-class="max-width-6"
|
||||
checked="ctrl.saveVariables" on-change="buildUrl()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
@ -70,7 +70,7 @@ export class SaveDashboardModalCtrl {
|
||||
saveForm: any;
|
||||
dismiss: () => void;
|
||||
timeChange = false;
|
||||
variableChange = false;
|
||||
variableValueChange = false;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private dashboardSrv) {
|
||||
@ -91,18 +91,24 @@ export class SaveDashboardModalCtrl {
|
||||
}
|
||||
|
||||
compareTemplating() {
|
||||
//checks if variables has been added or removed, if so variables will be saved automatically
|
||||
if (this.dashboardSrv.dash.originalTemplating.length !== this.dashboardSrv.dash.templating.list.length) {
|
||||
return (this.variableValueChange = false);
|
||||
}
|
||||
|
||||
//checks if variable value has changed
|
||||
if (this.dashboardSrv.dash.templating.list.length > 0) {
|
||||
for (let i = 0; i < this.dashboardSrv.dash.templating.list.length; i++) {
|
||||
if (
|
||||
this.dashboardSrv.dash.templating.list[i].current.text !==
|
||||
this.dashboardSrv.dash.originalTemplating[i].current.text
|
||||
) {
|
||||
return (this.variableChange = true);
|
||||
return (this.variableValueChange = true);
|
||||
}
|
||||
}
|
||||
return (this.variableChange = false);
|
||||
return (this.variableValueChange = false);
|
||||
} else {
|
||||
return (this.variableChange = false);
|
||||
return (this.variableValueChange = false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ describe('SaveDashboardModal', () => {
|
||||
let modal = new SaveDashboardModalCtrl(fakeDashboardSrv);
|
||||
|
||||
expect(modal.timeChange).toBe(true);
|
||||
expect(modal.variableChange).toBe(true);
|
||||
expect(modal.variableValueChange).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide checkboxes', () => {
|
||||
@ -54,7 +54,6 @@ describe('SaveDashboardModal', () => {
|
||||
{
|
||||
current: {
|
||||
selected: true,
|
||||
//tags: Array(0),
|
||||
text: 'server_002',
|
||||
value: 'server_002',
|
||||
},
|
||||
@ -84,7 +83,46 @@ describe('SaveDashboardModal', () => {
|
||||
};
|
||||
let modal = new SaveDashboardModalCtrl(fakeDashboardSrv);
|
||||
expect(modal.timeChange).toBe(false);
|
||||
expect(modal.variableChange).toBe(false);
|
||||
expect(modal.variableValueChange).toBe(false);
|
||||
});
|
||||
|
||||
it('should hide variable checkboxes', () => {
|
||||
let fakeDashboardSrv = {
|
||||
dash: {
|
||||
templating: {
|
||||
list: [
|
||||
{
|
||||
current: {
|
||||
selected: true,
|
||||
text: 'server_002',
|
||||
value: 'server_002',
|
||||
},
|
||||
name: 'Server',
|
||||
},
|
||||
{
|
||||
current: {
|
||||
selected: true,
|
||||
text: 'web_002',
|
||||
value: 'web_002',
|
||||
},
|
||||
name: 'Web',
|
||||
},
|
||||
],
|
||||
},
|
||||
originalTemplating: [
|
||||
{
|
||||
current: {
|
||||
selected: true,
|
||||
text: 'server_002',
|
||||
value: 'server_002',
|
||||
},
|
||||
name: 'Server',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
let modal = new SaveDashboardModalCtrl(fakeDashboardSrv);
|
||||
expect(modal.variableValueChange).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,9 @@
|
||||
import config from 'app/core/config';
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
|
||||
import config from 'app/core/config';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
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';
|
||||
@ -16,6 +16,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
datasourceName: any;
|
||||
$q: any;
|
||||
$timeout: any;
|
||||
contextSrv: any;
|
||||
datasourceSrv: any;
|
||||
timeSrv: any;
|
||||
templateSrv: any;
|
||||
@ -37,6 +38,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
// make metrics tab the default
|
||||
this.editorTabIndex = 1;
|
||||
this.$q = $injector.get('$q');
|
||||
this.contextSrv = $injector.get('contextSrv');
|
||||
this.datasourceSrv = $injector.get('datasourceSrv');
|
||||
this.timeSrv = $injector.get('timeSrv');
|
||||
this.templateSrv = $injector.get('templateSrv');
|
||||
@ -312,7 +314,7 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
|
||||
getAdditionalMenuItems() {
|
||||
const items = [];
|
||||
if (this.datasource && this.datasource.supportsExplore) {
|
||||
if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
|
||||
items.push({
|
||||
text: 'Explore',
|
||||
click: 'ctrl.explore();',
|
||||
|
@ -24,8 +24,9 @@ describe('MetricsPanelCtrl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('and has datasource set that supports explore', () => {
|
||||
describe('and has datasource set that supports explore and user has powers', () => {
|
||||
beforeEach(() => {
|
||||
ctrl.contextSrv = { isEditor: true };
|
||||
ctrl.datasource = { supportsExplore: true };
|
||||
additionalItems = ctrl.getAdditionalMenuItems();
|
||||
});
|
||||
|
@ -204,10 +204,14 @@ coreModule.directive('datasourceHttpSettings', function() {
|
||||
scope: {
|
||||
current: '=',
|
||||
suggestUrl: '@',
|
||||
noDirectAccess: '@',
|
||||
},
|
||||
templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html',
|
||||
link: {
|
||||
pre: function($scope, elem, attrs) {
|
||||
// do not show access option if direct access is disabled
|
||||
$scope.showAccessOption = $scope.noDirectAccess !== 'true';
|
||||
|
||||
$scope.getSuggestUrls = function() {
|
||||
return [$scope.suggestUrl];
|
||||
};
|
||||
|
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form-inline" ng-if="showAccessOption">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-7">Access</span>
|
||||
<div class="gf-form-select-wrapper max-width-24">
|
||||
|
@ -4,70 +4,101 @@
|
||||
<img class="logo-icon" src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||
<i class="icon-gf icon-gf-grafana_wordmark"></i>
|
||||
</div>
|
||||
<div class="login-inner-box">
|
||||
<form name="loginForm" class="login-form-group gf-form-group" ng-hide="disableLoginForm">
|
||||
<div class="login-form">
|
||||
<input type="text" name="username" class="gf-form-input login-form-input" required ng-model='formModel.user' placeholder={{loginHint}}
|
||||
autofocus>
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<input type="password" name="password" class="gf-form-input login-form-input" required ng-model="formModel.password" id="inputPassword"
|
||||
placeholder="password">
|
||||
</div>
|
||||
<div class="login-button-group">
|
||||
<button type="submit" class="btn btn-large p-x-2" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
|
||||
Log In
|
||||
</button>
|
||||
<div class="login-outer-box">
|
||||
<div class="login-inner-box" id="login-view">
|
||||
<form name="loginForm" class="login-form-group gf-form-group" ng-hide="disableLoginForm">
|
||||
<div class="login-form">
|
||||
<input type="text" name="username" class="gf-form-input login-form-input" required ng-model='formModel.user' placeholder={{loginHint}}
|
||||
autofocus>
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<input type="password" name="password" class="gf-form-input login-form-input" required ng-model="formModel.password" id="inputPassword"
|
||||
placeholder="password">
|
||||
</div>
|
||||
<div class="login-button-group">
|
||||
<button type="submit" class="btn btn-large p-x-2" ng-click="submit();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-primary': loginForm.$valid}">
|
||||
Log In
|
||||
</button>
|
||||
<div class="small login-button-forgot-password">
|
||||
<a href="user/password/send-reset-email">
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center login-divider" ng-show="oauthEnabled">
|
||||
<div>
|
||||
<div class="login-divider-line">
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="login-divider-text">
|
||||
<span ng-hide="disableLoginForm">or</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="login-divider-line">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="text-center login-divider" ng-show="oauthEnabled">
|
||||
<div>
|
||||
<div class="login-divider-line">
|
||||
<div class="clearfix"></div>
|
||||
<div class="login-oauth text-center" ng-show="oauthEnabled">
|
||||
<a class="btn btn-medium btn-service btn-service--google login-btn" href="login/google" target="_self" ng-if="oauth.google">
|
||||
<i class="btn-service-icon fa fa-google"></i>
|
||||
Sign in with Google
|
||||
</a>
|
||||
<a class="btn btn-medium btn-service btn-service--github login-btn" href="login/github" target="_self" ng-if="oauth.github">
|
||||
<i class="btn-service-icon fa fa-github"></i>
|
||||
Sign in with GitHub
|
||||
</a>
|
||||
<a class="btn btn-medium btn-inverse btn-service btn-service--grafanacom login-btn" href="login/grafana_com" target="_self"
|
||||
ng-if="oauth.grafana_com">
|
||||
<i class="btn-service-icon"></i>
|
||||
Sign in with Grafana.com
|
||||
</a>
|
||||
<a class="btn btn-medium btn-inverse btn-service btn-service--oauth login-btn" href="login/generic_oauth" target="_self"
|
||||
ng-if="oauth.generic_oauth">
|
||||
<i class="btn-service-icon fa fa-sign-in"></i>
|
||||
Sign in with {{oauth.generic_oauth.name}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="login-signup-box" ng-show="!disableUserSignUp">
|
||||
<div class="login-signup-title p-r-1">
|
||||
New to Grafana?
|
||||
</div>
|
||||
<a href="signup" class="btn btn-medium btn-signup btn-p-x-2">
|
||||
Sign Up
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<span class="login-divider-text">
|
||||
<span ng-hide="disableLoginForm">or</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="login-inner-box remove hidden" id="change-password-view">
|
||||
<div class="text-left login-change-password-info">
|
||||
<h5>Change Password</h5>
|
||||
Before you can get started with awesome dashboards we need you to make your account more secure by changing your password.
|
||||
<br />You can change your password again later.
|
||||
</div>
|
||||
<div>
|
||||
<div class="login-divider-line">
|
||||
<form class="login-form-group gf-form-group">
|
||||
<div class="login-form">
|
||||
<input type="password" id="newPassword" name="newPassword" class="gf-form-input login-form-input" required ng-model='command.newPassword'
|
||||
placeholder="New password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="login-form">
|
||||
<input type="password" name="confirmNew" class="gf-form-input login-form-input" required ng-model="command.confirmNew" placeholder="Confirm new password">
|
||||
</div>
|
||||
<div class="login-button-group login-button-group--right text-right">
|
||||
<a class="btn btn-link" ng-click="skip();">
|
||||
Skip
|
||||
<info-popover mode="no-padding">
|
||||
If you skip you will be promted to change password next time you login.
|
||||
</info-popover>
|
||||
</a>
|
||||
<button type="submit" class="btn btn-large p-x-2" ng-click="changePassword();" ng-class="{'btn-inverse': !loginForm.$valid, 'btn-success': loginForm.$valid}">
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
<div class="login-oauth text-center" ng-show="oauthEnabled">
|
||||
<a class="btn btn-medium btn-service btn-service--google login-btn" href="login/google" target="_self" ng-if="oauth.google">
|
||||
<i class="btn-service-icon fa fa-google"></i>
|
||||
Sign in with Google
|
||||
</a>
|
||||
<a class="btn btn-medium btn-service btn-service--github login-btn" href="login/github" target="_self" ng-if="oauth.github">
|
||||
<i class="btn-service-icon fa fa-github"></i>
|
||||
Sign in with GitHub
|
||||
</a>
|
||||
<a class="btn btn-medium btn-inverse btn-service btn-service--grafanacom login-btn" href="login/grafana_com" target="_self" ng-if="oauth.grafana_com">
|
||||
<i class="btn-service-icon"></i>
|
||||
Sign in with Grafana.com
|
||||
</a>
|
||||
<a class="btn btn-medium btn-inverse btn-service btn-service--oauth login-btn" href="login/generic_oauth" target="_self" ng-if="oauth.generic_oauth">
|
||||
<i class="btn-service-icon fa fa-sign-in"></i>
|
||||
Sign in with {{oauth.generic_oauth.name}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="login-signup-box" ng-show="!disableUserSignUp">
|
||||
<div class="login-signup-title p-r-1">
|
||||
New to Grafana?
|
||||
</div>
|
||||
<a href="signup" class="btn btn-medium btn-signup btn-p-x-2">
|
||||
Sign Up
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,6 +6,7 @@ import coreModule from 'app/core/core_module';
|
||||
import { store } from 'app/stores/store';
|
||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
function WrapInProvider(store, Component, props) {
|
||||
return (
|
||||
@ -16,16 +17,31 @@ function WrapInProvider(store, Component, props) {
|
||||
}
|
||||
|
||||
/** @ngInject */
|
||||
export function reactContainer($route, $location, backendSrv: BackendSrv, datasourceSrv: DatasourceSrv) {
|
||||
export function reactContainer(
|
||||
$route,
|
||||
$location,
|
||||
backendSrv: BackendSrv,
|
||||
datasourceSrv: DatasourceSrv,
|
||||
contextSrv: ContextSrv
|
||||
) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '',
|
||||
link(scope, elem) {
|
||||
let component = $route.current.locals.component;
|
||||
// Check permissions for this component
|
||||
const { roles } = $route.current.locals;
|
||||
if (roles && roles.length) {
|
||||
if (!roles.some(r => contextSrv.hasRole(r))) {
|
||||
$location.url('/');
|
||||
}
|
||||
}
|
||||
|
||||
let { component } = $route.current.locals;
|
||||
// Dynamic imports return whole module, need to extract default export
|
||||
if (component.default) {
|
||||
component = component.default;
|
||||
}
|
||||
|
||||
const props = {
|
||||
backendSrv: backendSrv,
|
||||
datasourceSrv: datasourceSrv,
|
||||
|
@ -113,6 +113,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
.when('/explore/:initial?', {
|
||||
template: '<react-container />',
|
||||
resolve: {
|
||||
roles: () => ['Editor', 'Admin'],
|
||||
component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Wrapper'),
|
||||
},
|
||||
})
|
||||
|
@ -384,6 +384,10 @@ $input-border: 1px solid $input-border-color;
|
||||
&--header {
|
||||
margin-bottom: $gf-form-margin;
|
||||
}
|
||||
|
||||
&--no-padding {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
select.gf-form-input ~ .gf-form-help-icon {
|
||||
|
@ -7,14 +7,13 @@
|
||||
|
||||
.singlestat-panel-value-container {
|
||||
line-height: 1;
|
||||
position: absolute;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 3em;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-weight: $font-weight-semi-bold;
|
||||
}
|
||||
|
||||
.singlestat-panel-prefix {
|
||||
|
@ -59,6 +59,14 @@ select:-webkit-autofill:focus {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
&--right {
|
||||
justify-content: flex-end;
|
||||
|
||||
& .btn {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-button-forgot-password {
|
||||
@ -75,7 +83,9 @@ select:-webkit-autofill:focus {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
height: 320px;
|
||||
}
|
||||
|
||||
.login-branding {
|
||||
@ -100,6 +110,11 @@ select:-webkit-autofill:focus {
|
||||
}
|
||||
}
|
||||
|
||||
.login-outer-box {
|
||||
display: flex;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.login-inner-box {
|
||||
text-align: center;
|
||||
padding: 2rem 4rem;
|
||||
@ -109,6 +124,22 @@ select:-webkit-autofill:focus {
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
max-width: 415px;
|
||||
transform: tranlate(0px, 0px);
|
||||
transition: 0.25s ease;
|
||||
|
||||
&.add {
|
||||
transform: translate(0px, -320px);
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.remove {
|
||||
transform: translate(0px, 320px);
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-tab-header {
|
||||
@ -117,6 +148,13 @@ select:-webkit-autofill:focus {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.login-change-password-info {
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
& h5 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.btn-signup {
|
||||
color: $white;
|
||||
border: 1px solid $login-border;
|
||||
|
@ -11,6 +11,7 @@ export function ControllerTestContext() {
|
||||
this.$element = {};
|
||||
this.$sanitize = {};
|
||||
this.annotationsSrv = {};
|
||||
this.contextSrv = {};
|
||||
this.timeSrv = new TimeSrvStub();
|
||||
this.templateSrv = new TemplateSrvStub();
|
||||
this.datasourceSrv = {
|
||||
@ -27,6 +28,7 @@ export function ControllerTestContext() {
|
||||
|
||||
this.providePhase = function(mocks) {
|
||||
return angularMocks.module(function($provide) {
|
||||
$provide.value('contextSrv', self.contextSrv);
|
||||
$provide.value('datasourceSrv', self.datasourceSrv);
|
||||
$provide.value('annotationsSrv', self.annotationsSrv);
|
||||
$provide.value('timeSrv', self.timeSrv);
|
||||
|
Loading…
Reference in New Issue
Block a user