Commit 75e6947c by Torkel Ödegaard

Merge branch 'master' into docs-2.0

parents 3007add4 67fbb173
# 2.0.0 (2015-04-20)
# 2.1.0 (unreleased - master branch)
**Backend**
- [Issue #1905](https://github.com/grafana/grafana/issues/1905). Github OAuth: You can now configure a Github team membership requirement, thx @dewski
# 2.0.3 (unreleased - 2.0.x branch)
**Fixes**
- [Issue #1872](https://github.com/grafana/grafana/issues/1872). Firefox/IE issue, invisible text in dashboard search fixed
- [Issue #1857](https://github.com/grafana/grafana/issues/1857). /api/login/ping Fix for issue when behind reverse proxy and subpath
- [Issue #1863](https://github.com/grafana/grafana/issues/1863). MySQL: Dashboard.data column type changed to mediumtext (sql migration added)
# 2.0.2 (2015-04-22)
**Fixes**
- [Issue #1832](https://github.com/grafana/grafana/issues/1832). Graph Panel + Legend Table mode: Many series casued zero height graph, now legend will never reduce the height of the graph below 50% of row height.
- [Issue #1846](https://github.com/grafana/grafana/issues/1846). Snapshots: Fixed issue with snapshoting dashboards with an interval template variable
- [Issue #1848](https://github.com/grafana/grafana/issues/1848). Panel timeshift: You can now use panel timeshift without a relative time override
# 2.0.1 (2015-04-20)
**Fixes**
- [Issue #1784](https://github.com/grafana/grafana/issues/1784). Data source proxy: Fixed issue with using data source proxy when grafana is behind nginx suburl
......
{
"ImportPath": "github.com/grafana/grafana",
"GoVersion": "go1.4.2",
"GoVersion": "go1.3",
"Packages": [
"./pkg/..."
],
......@@ -14,6 +14,14 @@
"Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a"
},
{
"ImportPath": "github.com/dalu/slug",
"Rev": "6dbd13912e9be466e2c1de349a2c7d1466c97e07"
},
{
"ImportPath": "github.com/dalu/unidecode",
"Rev": "339814d47f3e32a6f7036a0a4c56ed9b373dd755"
},
{
"ImportPath": "github.com/go-sql-driver/mysql",
"Comment": "v1.2-26-g9543750",
"Rev": "9543750295406ef070f7de8ae9c43ccddd44e15e"
......@@ -28,10 +36,6 @@
"Rev": "e2889e5517600b82905f1d2ba8b70deb71823ffe"
},
{
"ImportPath": "github.com/gosimple/slug",
"Rev": "a2392a4a87fa0366cbff131d3fd421f83f52492f"
},
{
"ImportPath": "github.com/jtolds/gls",
"Rev": "f1ac7f4f24f50328e6bc838ca4437d1612a0243c"
},
......@@ -87,10 +91,6 @@
{
"ImportPath": "gopkgs.com/pool.v1",
"Rev": "c850f092aad1780cbffff25f471c5cc32097932a"
},
{
"ImportPath": "gopkgs.com/unidecode.v1",
"Rev": "4deae2c05236b41cc39f8144ac87a837ba974d40"
}
]
}
......@@ -4,10 +4,9 @@ slug
Package `slug` generate slug from unicode string, URL-friendly slugify with
multiple languages support.
[![GoDoc](https://godoc.org/github.com/gosimple/slug?status.png)](https://godoc.org/github.com/gosimple/slug)
[![Build Status](https://drone.io/github.com/gosimple/slug/status.png)](https://drone.io/github.com/gosimple/slug/latest)
[![GoDoc](https://godoc.org/github.com/dalu/slug?status.png)](https://godoc.org/github.com/dalu/slug)
[Documentation online](http://godoc.org/github.com/gosimple/slug)
[Documentation online](http://godoc.org/github.com/dalu/slug)
## Example
......@@ -38,12 +37,9 @@ multiple languages support.
fmt.Println(textSub) // Will print 'sand-is-hot'
}
### Requests or bugs?
<https://github.com/gosimple/slug/issues>
## Installation
go get -u github.com/gosimple/slug
go get -u github.com/dalu/slug
## License
......
......@@ -12,7 +12,7 @@ Example:
package main
import(
"github.com/gosimple/slug"
"github.com/dalu/slug"
"fmt"
)
......@@ -35,9 +35,5 @@ Example:
textSub := slug.Make("water is hot")
fmt.Println(textSub) // Will print 'sand-is-hot'
}
Requests or bugs?
https://github.com/gosimple/slug/issues
*/
package slug
......@@ -6,7 +6,7 @@
package slug
import (
"gopkgs.com/unidecode.v1"
"github.com/dalu/unidecode"
"regexp"
"strings"
)
......
......@@ -3,10 +3,4 @@ unidecode
Unicode transliterator in Golang - Replaces non-ASCII characters with their ASCII approximations.
Please, use the following import path to ensure a stable API:
```go
import "gopkgs.com/unidecode.v1"
```
View other available versions, documentation and examples at http://gopkgs.com/unidecode
package unidecode
import (
"fmt"
"reflect"
)
// gopkgs.go: v1
// NOTE: This file is autogenerated by gopkgs.com.
const (
goPkgsSrcPath = "github.com/rainycape/unidecode"
goPkgsName = "unidecode"
goPkgsErrFmt = "invalid import path %s - please use gopkgs.com/%s.v1 or see http://gopkgs.com/%s"
)
type goPkgsCheck struct{}
func init() {
typ := reflect.TypeOf(goPkgsCheck{})
if typ.PkgPath() == goPkgsSrcPath {
panic(fmt.Errorf(goPkgsErrFmt, typ.PkgPath(), goPkgsName, goPkgsName))
}
}
/* jshint node:true */
'use strict';
module.exports = function (grunt) {
var os = require('os');
var config = {
pkg: grunt.file.readJSON('package.json'),
......@@ -13,6 +12,10 @@ module.exports = function (grunt) {
platform: process.platform.replace('win32', 'windows'),
};
if (process.platform.match(/^win/)) {
config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
}
config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
// load plugins
......@@ -35,7 +38,6 @@ module.exports = function (grunt) {
// Merge that object with what with whatever we have here
loadConfig(config,'./tasks/options/');
// pass the config to grunt
grunt.initConfig(config);
};
......@@ -111,7 +111,7 @@ bra run
### Running
```
./grafana web
./grafana
```
Open grafana in your browser (default http://localhost:3000) and login with admin user (default user/pass = admin/admin).
......
......@@ -22,13 +22,16 @@ import (
)
var (
versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
goarch string
goos string
version string = "v1"
race bool
workingDir string
serverBinaryName string = "grafana-server"
versionRe = regexp.MustCompile(`-[0-9]{1,3}-g[0-9a-f]{5,10}`)
goarch string
goos string
version string = "v1"
// deb & rpm does not support semver so have to handle their version a little differently
linuxPackageVersion string = "v1"
linuxPackageIteration string = ""
race bool
workingDir string
serverBinaryName string = "grafana-server"
)
const minGoVersion = 1.3
......@@ -40,7 +43,7 @@ func main() {
ensureGoPath()
readVersionFromPackageJson()
log.Printf("Version: %s\n", version)
log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration)
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
......@@ -70,7 +73,7 @@ func main() {
case "package":
//verifyGitRepoIsClean()
grunt("release", "--pkgVer="+version)
grunt("release")
createLinuxPackages()
case "latest":
......@@ -107,6 +110,16 @@ func readVersionFromPackageJson() {
}
version = jsonObj["version"].(string)
linuxPackageVersion = version
linuxPackageIteration = ""
// handle pre version stuff (deb / rpm does not support semver)
parts := strings.Split(version, "-")
if len(parts) > 1 {
linuxPackageVersion = parts[0]
linuxPackageIteration = parts[1]
}
}
type linuxPackageOptions struct {
......@@ -208,10 +221,14 @@ func createPackage(options linuxPackageOptions) {
"--config-files", options.systemdServiceFilePath,
"--after-install", options.postinstSrc,
"--name", "grafana",
"--version", version,
"--version", linuxPackageVersion,
"-p", "./dist",
}
if linuxPackageIteration != "" {
args = append(args, "--iteration", linuxPackageIteration)
}
// add dependenciesj
for _, dep := range options.depends {
args = append(args, "--depends", dep)
......@@ -259,6 +276,7 @@ func grunt(params ...string) {
func setup() {
runPrint("go", "get", "-v", "github.com/tools/godep")
runPrint("go", "get", "-v", "github.com/blang/semver")
runPrint("go", "get", "-v", "github.com/mattn/go-sqlite3")
runPrint("go", "install", "-v", "github.com/mattn/go-sqlite3")
}
......
......@@ -7,7 +7,7 @@ app_mode = production
#################################### Paths ####################################
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD)
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
#
data = data
#
......@@ -62,7 +62,7 @@ path = grafana.db
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", default is "memory"
# Either "memory", "file", "redis", "mysql", "postgresql", default is "file"
provider = file
# Provider config options
......@@ -70,6 +70,7 @@ provider = file
# file: session dir path, is relative to grafana data_path
# redis: config like redis server addr, poolSize, password, e.g. `127.0.0.1:6379,100,grafana`
# mysql: go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1)/database_name`
provider_config = sessions
# Session cookie name
......@@ -139,6 +140,7 @@ enabled = false
client_id = some_id
client_secret = some_secret
scopes = user:email
team_ids =
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
api_url = https://api.github.com/user
......
......@@ -7,7 +7,7 @@
#################################### Paths ####################################
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is useD)
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
#
;data = /var/lib/grafana
#
......@@ -62,7 +62,7 @@
#################################### Session ####################################
[session]
# Either "memory", "file", "redis", "mysql", default is "memory"
# Either "memory", "file", "redis", "mysql", "postgresql", default is "file"
;provider = file
# Provider config options
......@@ -142,8 +142,8 @@
;auth_url = https://github.com/login/oauth/authorize
;token_url = https://github.com/login/oauth/access_token
;api_url = https://api.github.com/user
# Uncomment bellow to only allow specific email domains
; allowed_domains = mycompany.com othercompany.com
;team_ids =
;allowed_domains =
#################################### Google Auth ##########################
[auth.google]
......@@ -154,8 +154,7 @@
;auth_url = https://accounts.google.com/o/oauth2/auth
;token_url = https://accounts.google.com/o/oauth2/token
;api_url = https://www.googleapis.com/oauth2/v1/userinfo
# Uncomment bellow to only allow specific email domains
; allowed_domains = mycompany.com othercompany.com
;allowed_domains =
#################################### Logging ##########################
[log]
......
......@@ -44,7 +44,7 @@ docs-test: docs-build
$(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh
docs-build:
git fetch https://github.com/grafana/grafana.git docs-1.x && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files
git fetch https://github.com/grafana/grafana.git docs-2.0 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files
echo "$(GIT_BRANCH)" > GIT_BRANCH
echo "$(GITCOMMIT)" > GITCOMMIT
docker build -t "$(DOCKER_DOCS_IMAGE)" .
......@@ -4,9 +4,9 @@ page_keywords: grafana, introduction, documentation, about
# About Grafana
Grafana is a leading open source applications for visualizing large-scale measurement data.
Grafana is a leading open source applications for visualizing large-scale measurement data.
It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world.
It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world.
Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control.
......@@ -16,7 +16,7 @@ Version 2.0 was released in April 2015: Grafana now ships with its own backend s
## Community Resources, Feedback, and Support
Thousands of organizations large and small rely on Grafana, and we have a vibrant and active community that constantly inspires us.
Thousands of organizations large and small rely on Grafana, and we have a vibrant and active community that constantly inspires us.
Please don't hesitate to [open a new issue on Github](https://github.com/grafana/grafana/issues) with your suggestions, ideas, and bug reports.
......@@ -35,4 +35,4 @@ If you have any trouble with Grafana, whether you can't get it set up or you jus
## License
By utilizing this software, you agree to the terms of the included license. Grafana is licensed under the Apache 2.0 agreement. See [LICENSE](https://github.com/grafana/grafana/blob/master/LICENSE.mdhttps://github.com/grafana/grafana/blob/master/LICENSE.md) for the full license terms.
By utilizing this software, you agree to the terms of the included license. Grafana is licensed under the Apache 2.0 agreement. See [LICENSE](https://github.com/grafana/grafana/blob/master/LICENSE.md) for the full license terms.
......@@ -179,6 +179,7 @@ Client ID and a Client Secret. Specify these in the grafana config file. Example
client_id = YOUR_GITHUB_APP_CLIENT_ID
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
scopes = user:email
team_ids =
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
allow_sign_up = false
......@@ -189,6 +190,21 @@ now login or signup with your github accounts.
You may allow users to sign-up via github auth by setting allow_sign_up to true. When this option is
set to true, any user successfully authenticating via github auth will be automatically signed up.
### team_ids
Require an active team membership for at least one of the given teams on GitHub.
If the authenticated user isn't a member of at least one the teams they will not
be able to register or authenticate with your Grafana instance. Example:
[auth.github]
enabled = true
client_id = YOUR_GITHUB_APP_CLIENT_ID
client_secret = YOUR_GITHUB_APP_CLIENT_SECRET
scopes = user:email
team_ids = 150,300
auth_url = https://github.com/login/oauth/authorize
token_url = https://github.com/login/oauth/access_token
allow_sign_up = false
## [auth.google]
You need to create a google project. You can do this in the [Google Developer Console](https://console.developers.google.com/project).
When you create the project you will need to specify a callback URL. Specify this as callback:
......@@ -219,7 +235,7 @@ set to true, any user successfully authenticating via google auth will be automa
## [session]
### provider
Valid values are "memory", "file", "mysql", 'postgres'. Default is "memory".
Valid values are "memory", "file", "mysql", 'postgres'. Default is "file".
### provider_config
This option should be configured differently depending on what type of session provider you have configured.
......@@ -252,10 +268,8 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours).
When enabled Grafana will send anonymous usage statistics to stats.grafana.org.
No ip addresses are being tracked, only simple counters to track running instances,
versions, dashboard & error counts. It is very helpful to us, please leave this
enabled. Counters are sent every 24 hours.
enabled. Counters are sent every 24 hours. Default value is `true`.
### google_analytics_ua_id
If you want to track Grafana usage via Google analytics specify *your* Univeral Analytics ID
here. By defualt this feature is disabled.
......@@ -10,11 +10,11 @@ page_keywords: grafana, installation, debian, ubuntu, guide
Description | Download
------------ | -------------
.deb for Debian-based Linux | [grafana_2.0.1_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.1_amd64.deb)
.deb for Debian-based Linux | [grafana_2.0.2_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb)
## Install
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.1_amd64.deb
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_2.0.2_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_2.0.2_amd64.deb
......
......@@ -10,12 +10,12 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
Description | Download
------------ | -------------
.RPM for Fedora / RHEL / CentOS Linux | [grafana-2.0.1-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.1-1.x86_64.rpm)
.RPM for Fedora / RHEL / CentOS Linux | [grafana-2.0.2-1.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm)
## Install
You can install using yum
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.1-1.x86_64.rpm
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-2.0.2-1.x86_64.rpm
Or manually using `rpm`
......@@ -30,9 +30,9 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo`
name=grafana
baseurl=https://packagecloud.io/grafana/stable/el/6/$basearch
repo_gpgcheck=1
gpgcheck=0
enabled=1
gpgkey=https://packagecloud.io/gpg.key
gpgcheck=1
gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana
sslverify=1
sslcacert=/etc/pki/tls/certs/ca-bundle.crt
......
......@@ -6,8 +6,23 @@ page_keywords: grafana, installation, windows guide
# Installing on Windows
There are currently no binary build for Windows. But read the [build from source](../project/building_from_source)
page for instructions on how to build it yourself.
## Download
Description | Download
------------ | -------------
Zip package for Windows | [grafana.2.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-2.0.2.windows-x64.zip)
## Configure
The zip file contains a folder with the current grafana version. Extract this folder to anywhere you want Grafana to run from.
Go into the `conf` directory and copy `sample.ini` to `custom.ini`. You should edit `custom.ini`, never `defaults.ini`.
The default grafana port is `3000`, this port requires extra permissions on windows. Edit `custom.ini` and uncomment the `http_port`
config and change it to something like `8080` or similar. That port should not require extra windows privileges.
Start grafana by executing `grafana-server.exe`, preferbly from the command line. If you want to run Grafana as
windows service, download [NSSM](https://nssm.cc/). It is very easy add grafana as windows service using that tool.
Read more about the [configuration options](configuration.md).
## Building on Windows
......
......@@ -6,5 +6,147 @@ page_keywords: grafana, admin, http, api, documentation
# HTTP API Reference
This documentation page has yet to be written.
The Grafana backend exposes an HTTP API, the same API is used by the frontend to do everything from saving
dashboards, creating users and updating data sources.
## Authorization
Currently you can authenticate via an `API Token` or via a `Session cookie` (acquired using regular login or oauth).
### Create API Token
Open the sidemenu and click the organization dropdown and select the `API Keys` option.
![](/img/v2/orgdropdown_api_keys.png)
You use the token in all requests in the `Authorization` header, like this:
**Example**:
GET http://your.grafana.com/api/dashboards/db/mydash HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
The `Authorization` header value should be `Bearer <your api key>`.
## Dashboards
### Create / Update dashboard
`POST /api/dashboards/db`
Creates a new dashboard or updates an existing dashboard.
**Example Request for new dashboard**:
POST /api/dashboards/db HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
]
"schemaVersion": 6,
"version": 0
},
"overwrite": false
}
JSON Body schema:
- **dashboard** – The complete dashboard model, id = null to create a new dashboard
- **overwrite** – Set to true if you want to overwrite existing dashboard with newer version or with same dashboard title.
**Example Response**:
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 78
{
"slug": "production-overview",
"status": "success",
"version": 1
}
Status Codes:
- **200** – Created
- **400** – Errors (invalid json, missing or invalid fields, etc)
- **401** – Unauthorized
- **412** – Precondition failed
The **412** status code is used when a newer dashboard already exists (newer, its version is greater than the verison that was sent). The
same status code is also used if another dashboar exists with the same title. The response body will look like this:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json; charset=UTF-8
Content-Length: 97
{
"message": "The dashboard has been changed by someone else",
"status": "version-mismatch"
}
In in case of title already exists the `status` property will be `name-exists`.
### Get dashboard
`GET /api/dashboards/db/:slug`
Will return the dashboard given the dashboard slug. Slug is the url friendly version of the dashboard title.
**Example Request**:
GET /api/dashboards/db/production-overview HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"meta": {
"isStarred": false,
"slug": "production-overview"
},
"dashboard": {
"id": null,
"title": "Production Overview",
"tags": [ "templated" ],
"timezone": "browser",
"rows": [
{
}
]
"schemaVersion": 6,
"version": 0
},
}
### Delete dashboard
`DELETE /api/dashboards/db/:slug`
The above will delete the dashboard with the specified slug. The slug is the url friendly (unique) version of the dashboard title.
## Data sources
### Create data source
## Organizations
## Users
......@@ -24,7 +24,7 @@ All of this applies to all Panels in the Dashboard (except those with Panel Time
It's possible to customize the options displayed for relative time and the auto-refresh options.
From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis.
From Dashboard setttings, click the Timepicker tab. From here you can specify the relative and auto refresh intervals. The Timepicker tab settings are saved on a per Dashboard basis. Entries are comma seperated and accept a number followed by one of the following units: s (seconds), m (minutes), h (hours), d (days), w (weeks), M (months), y (years).
![](/img/v1/timepicker_editor.png)
......
{
"version": "2.0.0-beta3",
"version": "2.0.1",
}
......@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "2.0.1",
"version": "2.1.0-pre1",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
......
......@@ -87,6 +87,7 @@ case "$1" in
# check if pid file has been written two
if ! [[ -s $PID_FILE ]]; then
log_end_msg 1
exit 1
fi
i=0
......@@ -96,7 +97,10 @@ case "$1" in
do
sleep 1
i=$(($i + 1))
[ $i -gt $timeout ] && log_end_msg 1
if [ $i -gt $timeout ]; then
log_end_msg 1
exit 1
fi
done
fi
log_end_msg $return
......
......@@ -144,5 +144,3 @@ case "$1" in
exit 1
;;
esac
exit 0
......@@ -21,16 +21,17 @@ type CurrentUser struct {
Email string `json:"email"`
Name string `json:"name"`
LightTheme bool `json:"lightTheme"`
OrgRole m.RoleType `json:"orgRole"`
OrgId int64 `json:"orgId"`
OrgName string `json:"orgName"`
OrgRole m.RoleType `json:"orgRole"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
GravatarUrl string `json:"gravatarUrl"`
}
type DashboardMeta struct {
IsStarred bool `json:"isStarred"`
IsHome bool `json:"isHome"`
IsSnapshot bool `json:"isSnapshot"`
IsStarred bool `json:"isStarred,omitempty"`
IsHome bool `json:"isHome,omitempty"`
IsSnapshot bool `json:"isSnapshot,omitempty"`
Slug string `json:"slug"`
Expires time.Time `json:"expires"`
Created time.Time `json:"created"`
......
......@@ -18,6 +18,7 @@ func setIndexViewData(c *middleware.Context) error {
Email: c.Email,
Name: c.Name,
LightTheme: c.Theme == "light",
OrgId: c.OrgId,
OrgName: c.OrgName,
OrgRole: c.OrgRole,
GravatarUrl: dtos.GetGravatarUrl(c.Email),
......
......@@ -3,6 +3,7 @@ package api
import (
"errors"
"fmt"
"net/url"
"golang.org/x/oauth2"
......@@ -45,7 +46,11 @@ func OAuthLogin(ctx *middleware.Context) {
userInfo, err := connect.UserInfo(token)
if err != nil {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
if err == social.ErrMissingTeamMembership {
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required Github team membership not fulfilled"))
} else {
ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
}
return
}
......@@ -54,7 +59,7 @@ func OAuthLogin(ctx *middleware.Context) {
// validate that the email is allowed to login to grafana
if !connect.IsEmailAllowed(userInfo.Email) {
log.Info("OAuth login attempt with unallowed email, %s", userInfo.Email)
ctx.Redirect(setting.AppSubUrl + "/login?email_not_allowed=1")
ctx.Redirect(setting.AppSubUrl + "/login?failedMsg=" + url.QueryEscape("Required email domain not fulfilled"))
return
}
......
......@@ -5,7 +5,7 @@ import (
"strings"
"time"
"github.com/gosimple/slug"
"github.com/dalu/slug"
)
// Typed errors
......
......@@ -86,4 +86,10 @@ func addDashboardMigration(mg *Migrator) {
}))
mg.AddMigration("drop table dashboard_v1", NewDropTableMigration("dashboard_v1"))
// change column type of dashboard.data
mg.AddMigration("alter dashboard.data to mediumtext v1", new(RawSqlMigration).
Sqlite("SELECT 0 WHERE 0;").
Postgres("SELECT 0;").
Mysql("ALTER TABLE dashboard MODIFY data MEDIUMTEXT;"))
}
......@@ -48,4 +48,10 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
mg.AddMigration("create dashboard_snapshot table v5 #2", NewAddTableMigration(snapshotV5))
addTableIndicesMigrations(mg, "v5", snapshotV5)
// change column type of dashboard
mg.AddMigration("alter dashboard_snapshot to mediumtext v2", new(RawSqlMigration).
Sqlite("SELECT 0 WHERE 0;").
Postgres("SELECT 0;").
Mysql("ALTER TABLE dashboard_snapshot MODIFY dashboard MEDIUMTEXT;"))
}
......@@ -25,8 +25,9 @@ func (m *MigrationBase) GetCondition() MigrationCondition {
type RawSqlMigration struct {
MigrationBase
sqlite string
mysql string
sqlite string
mysql string
postgres string
}
func (m *RawSqlMigration) Sql(dialect Dialect) string {
......@@ -35,6 +36,8 @@ func (m *RawSqlMigration) Sql(dialect Dialect) string {
return m.mysql
case SQLITE:
return m.sqlite
case POSTGRES:
return m.postgres
}
panic("db type not supported")
......@@ -50,6 +53,11 @@ func (m *RawSqlMigration) Mysql(sql string) *RawSqlMigration {
return m
}
func (m *RawSqlMigration) Postgres(sql string) *RawSqlMigration {
m.postgres = sql
return m
}
type AddColumnMigration struct {
MigrationBase
tableName string
......
......@@ -2,7 +2,9 @@ package social
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
......@@ -75,13 +77,24 @@ func NewOAuthService() {
// GitHub.
if name == "github" {
setting.OAuthService.GitHub = true
SocialMap["github"] = &SocialGithub{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup}
teamIds := sec.Key("team_ids").Ints(",")
SocialMap["github"] = &SocialGithub{
Config: &config,
allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
teamIds: teamIds,
}
}
// Google.
if name == "google" {
setting.OAuthService.Google = true
SocialMap["google"] = &SocialGoogle{Config: &config, allowedDomains: info.AllowedDomains, ApiUrl: info.ApiUrl, allowSignup: info.AllowSignup}
SocialMap["google"] = &SocialGoogle{
Config: &config, allowedDomains: info.AllowedDomains,
apiUrl: info.ApiUrl,
allowSignup: info.AllowSignup,
}
}
}
}
......@@ -103,10 +116,15 @@ func isEmailAllowed(email string, allowedDomains []string) bool {
type SocialGithub struct {
*oauth2.Config
allowedDomains []string
ApiUrl string
apiUrl string
allowSignup bool
teamIds []int
}
var (
ErrMissingTeamMembership = errors.New("User not a member of one of the required teams")
)
func (s *SocialGithub) Type() int {
return int(models.GITHUB)
}
......@@ -119,6 +137,28 @@ func (s *SocialGithub) IsSignupAllowed() bool {
return s.allowSignup
}
func (s *SocialGithub) IsTeamMember(client *http.Client, username string, teamId int) bool {
var data struct {
Url string `json:"url"`
State string `json:"state"`
}
membershipUrl := fmt.Sprintf("https://api.github.com/teams/%d/memberships/%s", teamId, username)
r, err := client.Get(membershipUrl)
if err != nil {
return false
}
defer r.Body.Close()
if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
return false
}
active := data.State == "active"
return active
}
func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
......@@ -128,7 +168,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
var err error
client := s.Client(oauth2.NoContext, token)
r, err := client.Get(s.ApiUrl)
r, err := client.Get(s.apiUrl)
if err != nil {
return nil, err
}
......@@ -139,11 +179,23 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
return nil, err
}
return &BasicUserInfo{
userInfo := &BasicUserInfo{
Identity: strconv.Itoa(data.Id),
Name: data.Name,
Email: data.Email,
}, nil
}
if len(s.teamIds) > 0 {
for _, teamId := range s.teamIds {
if s.IsTeamMember(client, data.Name, teamId) {
return userInfo, nil
}
}
return nil, ErrMissingTeamMembership
} else {
return userInfo, nil
}
}
// ________ .__
......@@ -156,7 +208,7 @@ func (s *SocialGithub) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
type SocialGoogle struct {
*oauth2.Config
allowedDomains []string
ApiUrl string
apiUrl string
allowSignup bool
}
......@@ -181,7 +233,7 @@ func (s *SocialGoogle) UserInfo(token *oauth2.Token) (*BasicUserInfo, error) {
var err error
client := s.Client(oauth2.NoContext, token)
r, err := client.Get(s.ApiUrl)
r, err := client.Get(s.apiUrl)
if err != nil {
return nil, err
}
......
......@@ -380,6 +380,9 @@ function($, _, moment) {
kbn.valueFormats.Bps = kbn.formatFuncCreator(1000, [' Bps', ' KBps', ' MBps', ' GBps', ' TBps', ' PBps', ' EBps', ' ZBps', ' YBps']);
kbn.valueFormats.short = kbn.formatFuncCreator(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Qaudr', ' Quint', ' Sext', ' Sept']);
kbn.valueFormats.joule = kbn.formatFuncCreator(1000, [' J', ' kJ', ' MJ', ' GJ', ' TJ', ' PJ', ' EJ', ' ZJ', ' YJ']);
kbn.valueFormats.amp = kbn.formatFuncCreator(1000, [' A', ' kA', ' MA', ' GA', ' TA', ' PA', ' EA', ' ZA', ' YA']);
kbn.valueFormats.volt = kbn.formatFuncCreator(1000, [' V', ' kV', ' MV', ' GV', ' TV', ' PV', ' EV', ' ZV', ' YV']);
kbn.valueFormats.hertz = kbn.formatFuncCreator(1000, [' Hz', ' kHz', ' MHz', ' GHz', ' THz', ' PHz', ' EHz', ' ZHz', ' YHz']);
kbn.valueFormats.watt = kbn.formatFuncCreator(1000, [' W', ' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']);
kbn.valueFormats.kwatt = kbn.formatFuncCreator(1000, [' kW', ' MW', ' GW', ' TW', ' PW', ' EW', ' ZW', ' YW']);
kbn.valueFormats.watth = kbn.formatFuncCreator(1000, [' Wh', ' kWh', ' MWh', ' GWh', ' TWh', ' PWh', ' EWh', ' ZWh', ' YWh']);
......@@ -534,6 +537,7 @@ function($, _, moment) {
{text: 'microseconds (µs)', value: 'µs'},
{text: 'milliseconds (ms)', value: 'ms'},
{text: 'seconds (s)', value: 's'},
{text: 'Hertz (1/s)', value: 'hertz'},
]
},
{
......@@ -561,6 +565,8 @@ function($, _, moment) {
{text: 'kilowatt-hour (kWh)', value: 'kwatth'},
{text: 'joule (J)', value: 'joule'},
{text: 'electron volt (eV)', value: 'ev'},
{text: 'Ampere (A)', value: 'amp'},
{text: 'Volt (V)', value: 'volt'},
]
},
{
......
......@@ -7,7 +7,7 @@ function (angular, config) {
var module = angular.module('grafana.controllers');
module.controller('LoginCtrl', function($scope, backendSrv, contextSrv) {
module.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) {
$scope.formModel = {
user: '',
email: '',
......@@ -28,6 +28,13 @@ function (angular, config) {
$scope.init = function() {
$scope.$watch("loginMode", $scope.loginModeChanged);
$scope.passwordChanged();
var params = $location.search();
if (params.failedMsg) {
$scope.appEvent('alert-warning', ['Login Failed', params.failedMsg]);
delete params.failedMsg;
$location.search(params);
}
};
// build info view model
......
......@@ -52,6 +52,10 @@ function (angular, _) {
};
$scope.saveDashboard = function(options) {
if ($scope.dashboardMeta.canSave === false) {
return;
}
var clone = $scope.dashboard.getSaveModelClone();
backendSrv.saveDashboard(clone, options).then(function(data) {
......@@ -119,6 +123,8 @@ function (angular, _) {
$scope.saveDashboardAs = function() {
var newScope = $rootScope.$new();
newScope.clone = $scope.dashboard.getSaveModelClone();
newScope.clone.editable = true;
newScope.clone.hideControls = false;
$scope.appEvent('show-modal', {
src: './app/features/dashboard/partials/saveDashboardAs.html',
......
......@@ -52,24 +52,22 @@ function (angular, $, kbn, _, moment) {
p._initMeta = function(meta) {
meta = meta || {};
meta.canShare = true;
meta.canSave = true;
meta.canEdit = true;
meta.canStar = true;
if (contextSrv.hasRole('Viewer')) {
meta.canSave = false;
}
meta.canShare = meta.canShare === false ? false : true;
meta.canSave = meta.canSave === false ? false : true;
meta.canEdit = meta.canEdit === false ? false : true;
meta.canStar = meta.canStar === false ? false : true;
meta.canDelete = meta.canDelete === false ? false : true;
if (meta.isSnapshot) {
if (contextSrv.hasRole('Viewer')) {
meta.canSave = false;
}
if (meta.isHome) {
meta.canShare = false;
meta.canStar = false;
meta.canSave = false;
if (!this.editable) {
meta.canEdit = false;
meta.canDelete = false;
meta.canSave = false;
this.hideControls = true;
}
this.meta = meta;
......
......@@ -30,16 +30,16 @@
<li ng-show="dashboardMeta.canSave">
<a ng-click="saveDashboard()" bs-tooltip="'Save dashboard'" data-placement="bottom"><i class="fa fa-save"></i></a>
</li>
<li class="dropdown" ng-if="dashboardMeta.canEdit">
<li class="dropdown">
<a class="pointer" data-toggle="dropdown"><i class="fa fa-cog"></i></a>
<ul class="dropdown-menu">
<li><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
<li><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
<li><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
<li><a class="pointer" ng-click="exportDashboard();">Export</a></li>
<li><a class="pointer" ng-click="editJson();">View JSON</a></li>
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
<li ng-if="dashboardMeta.canDelete"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
</ul>
</li>
</ul>
......
......@@ -37,7 +37,7 @@ function(angular, _, config) {
});
this.ignoreChanges = function() {
if (!self.current) { return true; }
if (!self.current || !self.current.meta) { return true; }
var meta = self.current.meta;
return !meta.canSave || meta.fromScript || meta.fromFile;
......
......@@ -43,7 +43,7 @@ function (angular, _, kbn, $) {
}
if (scope.panel.timeShift) {
if (!kbn.isValidTimeSpan(scope.panel.timeFrom)) {
if (!kbn.isValidTimeSpan(scope.panel.timeShift)) {
scope.panelMeta.timeInfo = 'invalid timeshift';
return;
}
......
......@@ -70,6 +70,14 @@ function (angular, _, config) {
};
$scope.toggleFullscreen = function(edit) {
if (edit && $scope.dashboardMeta.canEdit === false) {
$scope.appEvent('alert-warning', [
'Dashboard not editable',
'Use Save As.. feature to create an editable copy of this dashboard.'
]);
return;
}
$scope.dashboardViewState.update({ fullscreen: true, edit: edit, panelId: $scope.panel.id });
};
......
......@@ -29,13 +29,7 @@ function (angular, _, kbn) {
var variable = this.variables[i];
var urlValue = queryParams['var-' + variable.name];
if (urlValue !== void 0) {
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
var promise = this.setVariableValue(variable, option, true);
this.updateAutoInterval(variable);
promises.push(promise);
promises.push(this.setVariableFromUrl(variable, urlValue));
}
else if (variable.refresh) {
promises.push(this.updateOptions(variable));
......@@ -48,11 +42,30 @@ function (angular, _, kbn) {
return $q.all(promises);
};
this.setVariableFromUrl = function(variable, urlValue) {
if (variable.refresh) {
var self = this;
//refresh the list of options before setting the value
return this.updateOptions(variable).then(function() {
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
self.updateAutoInterval(variable);
return self.setVariableValue(variable, option);
});
}
var option = _.findWhere(variable.options, { text: urlValue });
option = option || { text: urlValue, value: urlValue };
this.updateAutoInterval(variable);
return this.setVariableValue(variable, option);
};
this.updateAutoInterval = function(variable) {
if (!variable.auto) { return; }
// add auto option if missing
if (variable.options[0].text !== 'auto') {
if (variable.options.length && variable.options[0].text !== 'auto') {
variable.options.unshift({ text: 'auto', value: '$__auto_interval' });
}
......
......@@ -228,10 +228,10 @@
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<li class="tight-form-item" style="width: 105px">
<strong>Decimals</strong>
</li>
<li>
<li style="width: 105px">
<input type="number" class="input-small tight-form-input" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right"
ng-model="panel.decimals" ng-change="render()" ng-model-onblur>
</li>
......@@ -242,4 +242,3 @@
</div>
</div>
......@@ -63,12 +63,13 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
render_panel();
});
function getLegendHeight() {
function getLegendHeight(panelHeight) {
if (!scope.panel.legend.show || scope.panel.legend.rightSide) {
return 0;
}
if (scope.panel.legend.alignAsTable) {
return 30 + (25 * data.length);
var total = 30 + (25 * data.length);
return Math.min(total, Math.floor(panelHeight/2));
} else {
return 26;
}
......@@ -84,7 +85,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
graphHeight -= 5; // padding
graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar
graphHeight = graphHeight - getLegendHeight(); // subtract one line legend
graphHeight = graphHeight - getLegendHeight(graphHeight); // subtract one line legend
elem.css('height', graphHeight + 'px');
......
......@@ -29,7 +29,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
panelName: 'Graph',
editIcon: "fa fa-bar-chart",
fullscreen: true,
metricsEditor: true
metricsEditor: true,
});
$scope.panelMeta.addEditorTab('Axes & Grid', 'app/panels/graph/axisEditor.html');
......@@ -67,9 +67,9 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
// show/hide lines
lines : true,
// fill factor
fill : 0,
fill : 1,
// line width in pixels
linewidth : 1,
linewidth : 2,
// show hide points
points : false,
// point radius in pixels
......
......@@ -24,7 +24,7 @@
<div class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></div>
</div>
<div class="row-open" ng-show="!row.collapse">
<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
<div class='row-tab bgSuccess dropdown' ng-show="dashboardMeta.canEdit">
<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-bars"></i>
</span>
......@@ -99,7 +99,7 @@
</div>
</div>
<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
<div ng-show='dashboardMeta.canEdit' class="row-fluid add-row-panel-hint">
<div class="span12" style="text-align:right;">
<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-small">
<span><i class="fa fa-plus"></i> ADD ROW</span>
......
......@@ -17,63 +17,118 @@
</div>
<div class="gf-box-body">
<div ng-if="editor.index == 0">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
</div>
<div class="editor-option">
<label class="small">Time correction</label>
<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<editor-opt-bool text="Hide controls (CTRL+H)" model="dashboard.hideControls"></editor-opt-bool>
<editor-opt-bool text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-opt-bool>
<div class="gf-box-body" style="padding-bottom: 50px;">
<div ng-if="editor.index == 0">
<div class="editor-row">
<div class="section">
<h5>Dashboard info</h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 90px">
Title
</li>
<li>
<input type="text" class="input-xlarge tight-form-input" ng-model='dashboard.title'></input>
</li>
<li class="tight-form-item">
Tags
<tip>Press enter to a add tag</tip>
</li>
<li>
<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Tags</label>
<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
<tip>Press enter to a add tag</tip>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 90px">
Timezone
</li>
<li>
<select ng-model="dashboard.timezone" class='input-small tight-form-input' ng-options="f for f in ['browser','utc']"></select>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div ng-if="editor.index == 1">
<div class="editor-row">
<div class="span6">
<table class="grafana-options-table">
<tr ng-repeat="row in dashboard.rows">
<td style="width: 97%">
{{row.title}}
</td>
<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
<td>
<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
<div class="section">
<h5>Toggles</h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 181px">
<label class="checkbox-label" for="dashboard.editable">Editable</label>
</li>
<li>
<li class="tight-form-item last">
<input class="cr1" id="dashboard.editable" type="checkbox" ng-model="dashboard.editable" ng-checked="dashboard.editable">
<label for="dashboard.editable" class="cr1"></label>
</li>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 181px">
<label class="checkbox-label" for="dashboard.hideControls">Hide Controls (CTRL+H)</label>
</li>
<li class="tight-form-item last">
<input class="cr1" id="dashboard.hideControls" type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
<label for="dashboard.hideControls" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 181px">
<label class="checkbox-label" for="dashboard.sharedCrosshair">Shared Crosshair (CTRL+H)</label>
</li>
<li class="tight-form-item last">
<input class="cr1" id="dashboard.sharedCrosshair" type="checkbox" ng-model="dashboard.sharedCrosshair" ng-checked="dashboard.sharedCrosshair">
<label for="dashboard.sharedCrosshair" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 2+$index">
<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
<div ng-if="editor.index == 1">
<div class="editor-row">
<div class="span6">
<table class="grafana-options-table">
<tr ng-repeat="row in dashboard.rows">
<td style="width: 97%">
{{row.title}}
</td>
<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
<td>
<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-small">
<i class="fa fa-remove"></i>
</a>
</td>
</tr>
</table>
</div>
<div class="clearfix"></div>
</div>
</div>
<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 2+$index">
<ng-include ng-show="pulldown.enable" src="pulldownEditorPath(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
</div>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
</div>
<div class="gf-box-footer">
......
......@@ -74,7 +74,7 @@ function (angular, _, config, kbn, moment) {
var data = {
"fields": [timeField, "_source"],
"query" : { "filtered": { "query" : query, "filter": filter } },
"size": 100
"size": 10000
};
return this._request('POST', '/_search', annotation.index, data).then(function(results) {
......
......@@ -6,12 +6,11 @@
<p>
This is just a test data source that generates random walk series. If this is your only data source
open the left side menu and navigate to the data sources admin screen and add your data sources. You can change
data source using the button to the left of the <strong>Add query</strong> button.
open the left side menu and navigate to the data sources admin screen and add your data sources (you need to be
logged in to do this). You can change data source using the button to the left of the <strong>Add query</strong> button.
</p>
</div>
<div class="span2"></div>
<div class="clearfix"></div>
</div>
......@@ -18,6 +18,8 @@ function (angular, _, kbn, moment, $) {
if (!$routeParams.slug) {
backendSrv.get('/api/dashboards/home').then(function(result) {
var meta = result.meta;
meta.canSave = meta.canShare = meta.canEdit = meta.canStar = false;
$scope.initDashboard(result, $scope);
},function() {
dashboardLoadFailed('Not found');
......@@ -38,7 +40,16 @@ function (angular, _, kbn, moment, $) {
backendSrv.get('/api/snapshots/' + $routeParams.key).then(function(result) {
$scope.initDashboard(result, $scope);
}, function() {
$scope.initDashboard({meta: {isSnapshot: true}, model: {title: 'Snapshot not found'}}, $scope);
$scope.initDashboard({
meta: {
isSnapshot: true,
canSave: false,
canEdit: false,
},
model: {
title: 'Snapshot not found'
}
}, $scope);
});
});
......@@ -48,15 +59,18 @@ function (angular, _, kbn, moment, $) {
$location.path('');
return;
}
$scope.initDashboard({meta: {}, model: window.grafanaImportDashboard }, $scope);
$scope.initDashboard({
meta: { canShare: false, canStar: false },
model: window.grafanaImportDashboard
}, $scope);
});
module.controller('NewDashboardCtrl', function($scope) {
$scope.initDashboard({
meta: {},
meta: { canStar: false, canShare: false },
model: {
title: "New dashboard",
rows: [{ height: '250px', panels:[] }]
rows: [{ height: '250px', panels:[] }]
},
}, $scope);
});
......@@ -66,10 +80,10 @@ function (angular, _, kbn, moment, $) {
var file_load = function(file) {
return $http({
url: "public/dashboards/"+file.replace(/\.(?!json)/,"/")+'?' + new Date().getTime(),
method: "GET",
transformResponse: function(response) {
return angular.fromJson(response);
}
method: "GET",
transformResponse: function(response) {
return angular.fromJson(response);
}
}).then(function(result) {
if(!result) {
return false;
......@@ -82,7 +96,10 @@ function (angular, _, kbn, moment, $) {
};
file_load($routeParams.jsonFile).then(function(result) {
$scope.initDashboard({meta: {fromFile: true}, model: result}, $scope);
$scope.initDashboard({
meta: { canSave: false, canDelete: false },
model: result
}, $scope);
});
});
......@@ -92,8 +109,8 @@ function (angular, _, kbn, moment, $) {
var execute_script = function(result) {
var services = {
dashboardSrv: dashboardSrv,
datasourceSrv: datasourceSrv,
$q: $q,
datasourceSrv: datasourceSrv,
$q: $q,
};
/*jshint -W054 */
......@@ -118,16 +135,19 @@ function (angular, _, kbn, moment, $) {
var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime();
return $http({ url: url, method: "GET" })
.then(execute_script)
.then(null,function(err) {
console.log('Script dashboard error '+ err);
$scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
return false;
});
.then(execute_script)
.then(null,function(err) {
console.log('Script dashboard error '+ err);
$scope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]);
return false;
});
};
script_load($routeParams.jsFile).then(function(result) {
$scope.initDashboard({meta: {fromScript: true}, model: result.data}, $scope);
$scope.initDashboard({
meta: {fromScript: true, canDelete: false, canSave: false},
model: result.data
}, $scope);
});
});
......
......@@ -63,8 +63,9 @@ function (angular, _, config) {
var requestIsLocal = options.url.indexOf('/') === 0;
var firstAttempt = options.retry === 0;
if (requestIsLocal && firstAttempt) {
if (requestIsLocal && !options.hasSubUrl) {
options.url = config.appSubUrl + options.url;
options.hasSubUrl = true;
}
return $http(options).then(function(results) {
......
......@@ -18,13 +18,6 @@ function (angular, _, store, config) {
}
}
this.version = config.buildInfo.version;
this.lightTheme = false;
this.user = new User();
this.isSignedIn = this.user.isSignedIn;
this.isGrafanaAdmin = this.user.isGrafanaAdmin;
this.sidemenu = store.getBool('grafana.sidemenu');
// events
$rootScope.$on('toggle-sidemenu', function() {
self.toggleSideMenu();
......@@ -47,6 +40,12 @@ function (angular, _, store, config) {
}, 50);
};
this.version = config.buildInfo.version;
this.lightTheme = false;
this.user = new User();
this.isSignedIn = this.user.isSignedIn;
this.isGrafanaAdmin = this.user.isGrafanaAdmin;
this.sidemenu = store.getBool('grafana.sidemenu');
this.isEditor = this.hasRole('Editor') || this.hasRole('Admin');
});
});
.bootstrap-tagsinput {
display: inline-block;
padding: 4px 6px;
margin-bottom: 10px;
color: #555;
padding: 0 0 0 6px;
vertical-align: middle;
border-radius: 4px;
max-width: 100%;
line-height: 22px;
background-color: @inputBackground;
border: 1px solid @inputBorder;
.box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
.transition(~"border linear .2s, box-shadow linear .2s");
input {
border: none;
box-shadow: none;
outline: none;
background-color: transparent;
padding: 0;
padding-left: 5px;
margin: 0;
width: auto !important;
max-width: inherit;
&:focus {
border: none;
box-shadow: none;
}
border-right: 1px solid @grafanaTargetSegmentBorder;
margin: 0px;
border-radius: 0;
padding: 8px 6px;
height: 100%;
box-sizing: border-box;
}
.tag {
......@@ -49,4 +35,4 @@
}
}
}
}
\ No newline at end of file
}
......@@ -22,7 +22,8 @@
padding-bottom: 10px;
input {
width: 100%;
padding: 18px 8px;
padding: 8px 8px;
height: 100%;
box-sizing: border-box;
}
button {
......
......@@ -185,10 +185,26 @@ define([
expect(model.annotations.list.length).to.be(0);
expect(model.templating.list.length).to.be(0);
});
});
});
describe('Given editable false dashboard', function() {
var model;
beforeEach(function() {
model = _dashboardSrv.create({
editable: false,
});
});
it('Should set meta canEdit and canSave to false', function() {
expect(model.meta.canSave).to.be(false);
expect(model.meta.canEdit).to.be(false);
});
it('getSaveModelClone should remove meta', function() {
var clone = model.getSaveModelClone();
expect(clone.meta).to.be(undefined);
});
});
});
});
......@@ -500,4 +500,4 @@
$(function() {
$("input[data-role=tagsinput], select[multiple][data-role=tagsinput]").tagsinput();
});
})(window.jQuery);
\ No newline at end of file
})(window.jQuery);
......@@ -16,7 +16,7 @@ module.exports = function(config) {
{
expand: true,
src: ['LICENSE.md', 'README.md', 'NOTICE.md'],
dest: '<%= pkg.name %>/',
dest: '<%= pkg.name %>-<%= pkg.version %>/',
}
]
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment