Merge branch 'master' of github.com:grafana/grafana

This commit is contained in:
Torkel Ödegaard 2018-07-18 12:46:42 +02:00
commit 660dc09fa9
18 changed files with 411 additions and 57 deletions

View File

@ -1,3 +1,4 @@
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY nginx.conf /etc/nginx/nginx.conf
COPY htpasswd /etc/nginx/htpasswd

View File

@ -0,0 +1,3 @@
user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0
user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9.
admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1

View File

@ -13,7 +13,26 @@ http {
listen 10080;
location /grafana/ {
################################################################
# Enable these settings to test with basic auth and an auth proxy header
# the htpasswd file contains an admin user with password admin and
# user1: grafana and user2: grafana
################################################################
# auth_basic "Restricted Content";
# auth_basic_user_file /etc/nginx/htpasswd;
################################################################
# To use the auth proxy header, set the following in custom.ini:
# [auth.proxy]
# enabled = true
# header_name = X-WEBAUTH-USER
# header_property = username
################################################################
# proxy_set_header X-WEBAUTH-USER $remote_user;
proxy_pass http://localhost:3000/;
}
}
}
}

View File

@ -0,0 +1,286 @@
+++
title = "Playlist HTTP API "
description = "Playlist Admin HTTP API"
keywords = ["grafana", "http", "documentation", "api", "playlist"]
aliases = ["/http_api/playlist/"]
type = "docs"
[menu.docs]
name = "Playlist"
parent = "http_api"
+++
# Playlist API
## Search Playlist
`GET /api/playlists`
Get all existing playlist for the current organization using pagination
**Example Request**:
```bash
GET /api/playlists HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
Querystring Parameters:
These parameters are used as querystring parameters.
- **query** - Limit response to playlist having a name like this value.
- **limit** - Limit response to *X* number of playlist.
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 1,
"name": "my playlist",
"interval": "5m"
}
]
```
## Get one playlist
`GET /api/playlists/:id`
**Example Request**:
```bash
GET /api/playlists/1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{
"id" : 1,
"name": "my playlist",
"interval": "5m",
"orgId": "my org",
"items": [
{
"id": 1,
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"id": 2,
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
## Get Playlist items
`GET /api/playlists/:id/items`
**Example Request**:
```bash
GET /api/playlists/1/items HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 1,
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"id": 2,
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
```
## Get Playlist dashboards
`GET /api/playlists/:id/dashboards`
**Example Request**:
```bash
GET /api/playlists/1/dashboards HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
[
{
"id": 3,
"title": "my third dasboard",
"order": 1,
},
{
"id": 5,
"title":"my other dasboard"
"order": 2,
}
]
```
## Create a playlist
`POST /api/playlists/`
**Example Request**:
```bash
PUT /api/playlists/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name": "my playlist",
"interval": "5m",
"items": [
{
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{
"id": 1,
"name": "my playlist",
"interval": "5m"
}
```
## Update a playlist
`PUT /api/playlists/:id`
**Example Request**:
```bash
PUT /api/playlists/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name": "my playlist",
"interval": "5m",
"items": [
{
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{
"id" : 1,
"name": "my playlist",
"interval": "5m",
"orgId": "my org",
"items": [
{
"id": 1,
"playlistId": 1,
"type": "dashboard_by_id",
"value": "3",
"order": 1,
"title":"my third dasboard"
},
{
"id": 2,
"playlistId": 1,
"type": "dashboard_by_tag",
"value": "myTag",
"order": 2,
"title":"my other dasboard"
}
]
}
```
## Delete a playlist
`DELETE /api/playlists/:id`
**Example Request**:
```bash
DELETE /api/playlists/1 HTTP/1.1
Accept: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```json
HTTP/1.1 200
Content-Type: application/json
{}
```

View File

@ -863,7 +863,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Url to where Grafana will send PUT request with images
### public_url
Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name.
Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged.
### username
basic auth username

View File

@ -73,8 +73,7 @@ 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("/explore", reqEditorRole, Index)
r.Get("/playlists/", reqSignedIn, Index)
r.Get("/playlists/*", reqSignedIn, Index)

View File

@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response {
func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response {
cmd.OrgId = c.OrgId
cmd.Id = c.ParamsInt64(":id")
if err := bus.Dispatch(&cmd); err != nil {
return Error(500, "Failed to save playlist", err)

View File

@ -9,6 +9,7 @@ import (
"net/http"
"net/url"
"path"
"strings"
"time"
"github.com/grafana/grafana/pkg/util"
@ -35,6 +36,16 @@ var netClient = &http.Client{
Transport: netTransport,
}
func (u *WebdavUploader) PublicURL(filename string) string {
if strings.Contains(u.public_url, "${file}") {
return strings.Replace(u.public_url, "${file}", filename, -1)
} else {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String()
}
}
func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) {
url, _ := url.Parse(u.url)
filename := util.GetRandomString(20) + ".png"
@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error)
}
if u.public_url != "" {
publicURL, _ := url.Parse(u.public_url)
publicURL.Path = path.Join(publicURL.Path, filename)
return publicURL.String(), nil
return u.PublicURL(filename), nil
}
return url.String(), nil

View File

@ -2,6 +2,7 @@ package imguploader
import (
"context"
"net/url"
"testing"
. "github.com/smartystreets/goconvey/convey"
@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) {
So(path, ShouldStartWith, "http://publicurl:8888/webdav/")
})
}
func TestPublicURL(t *testing.T) {
Convey("Given a public URL with parameters, and no template", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=")
parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png"))
So(parsed.Path, ShouldEndWith, "fileyfile.png")
})
Convey("Given a public URL with parameters, and a template", t, func() {
webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}")
So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png")
})
}

View File

@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard
type UpdatePlaylistCommand struct {
OrgId int64 `json:"-"`
Id int64 `json:"id" binding:"Required"`
Id int64 `json:"id"`
Name string `json:"name" binding:"Required"`
Interval string `json:"interval"`
Items []PlaylistItemDTO `json:"items"`

View File

@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
alert.name,
alert.state,
alert.new_state_date,
alert.eval_data,
alert.eval_date,
alert.execution_error,
dashboard.uid as dashboard_uid,

View File

@ -13,7 +13,7 @@ func mockTimeNow() {
var timeSeed int64
timeNow = func() time.Time {
fakeNow := time.Unix(timeSeed, 0)
timeSeed += 1
timeSeed++
return fakeNow
}
}
@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) {
InitTestDB(t)
testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert")
evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`))
items := []*m.Alert{
{
PanelId: 1,
@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) {
Message: "Alerting message",
Settings: simplejson.New(),
Frequency: 1,
EvalData: evalData,
},
}
@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) {
alert := alertQuery.Result[0]
So(err2, ShouldBeNil)
So(alert.Id, ShouldBeGreaterThan, 0)
So(alert.DashboardId, ShouldEqual, testDash.Id)
So(alert.PanelId, ShouldEqual, 1)
So(alert.Name, ShouldEqual, "Alerting title")
So(alert.State, ShouldEqual, "pending")
So(alert.NewStateDate, ShouldNotBeNil)
So(alert.EvalData, ShouldNotBeNil)
So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test")
So(alert.EvalDate, ShouldNotBeNil)
So(alert.ExecutionError, ShouldEqual, "")
So(alert.DashboardUid, ShouldNotBeNil)
So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts")
})
Convey("Viewer cannot read alerts", func() {

View File

@ -2,16 +2,18 @@ import React from 'react';
import { hot } from 'react-hot-loader';
import Select from 'react-select';
import kbn from 'app/core/utils/kbn';
import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2';
import { decodePathComponent } from 'app/core/utils/location_util';
import { parse as parseDate } from 'app/core/utils/datemath';
import ElapsedTime from './ElapsedTime';
import QueryRows from './QueryRows';
import Graph from './Graph';
import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => {
@ -31,18 +33,20 @@ function makeTimeSeriesList(dataList, options) {
});
}
function parseInitialState(initial) {
try {
const parsed = JSON.parse(decodePathComponent(initial));
return {
datasource: parsed.datasource,
queries: parsed.queries.map(q => q.query),
range: parsed.range,
};
} catch (e) {
console.error(e);
return { queries: [], range: DEFAULT_RANGE };
function parseInitialState(initial: string | undefined) {
if (initial) {
try {
const parsed = JSON.parse(decodePathComponent(initial));
return {
datasource: parsed.datasource,
queries: parsed.queries.map(q => q.query),
range: parsed.range,
};
} catch (e) {
console.error(e);
}
}
return { datasource: null, queries: [], range: DEFAULT_RANGE };
}
interface IExploreState {
@ -63,11 +67,12 @@ interface IExploreState {
tableResult: any;
}
// @observer
export class Explore extends React.Component<any, IExploreState> {
el: any;
constructor(props) {
super(props);
const { datasource, queries, range } = parseInitialState(props.routeParams.initial);
const { datasource, queries, range } = parseInitialState(props.routeParams.state);
this.state = {
datasource: null,
datasourceError: null,
@ -132,6 +137,10 @@ export class Explore extends React.Component<any, IExploreState> {
}
}
getRef = el => {
this.el = el;
};
handleAddQueryRow = index => {
const { queries } = this.state;
const nextQueries = [
@ -214,20 +223,33 @@ export class Explore extends React.Component<any, IExploreState> {
}
};
async runGraphQuery() {
buildQueryOptions(targetOptions: { format: string; instant: boolean }) {
const { datasource, queries, range } = this.state;
const resolution = this.el.offsetWidth;
const absoluteRange = {
from: parseDate(range.from, false),
to: parseDate(range.to, true),
};
const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval);
const targets = queries.map(q => ({
...targetOptions,
expr: q.query,
}));
return {
interval,
range,
targets,
};
}
async runGraphQuery() {
const { datasource, queries } = this.state;
if (!hasQuery(queries)) {
return;
}
this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
const now = Date.now();
const options = buildQueryOptions({
format: 'time_series',
interval: datasource.interval,
instant: false,
range,
queries: queries.map(q => q.query),
});
const options = this.buildQueryOptions({ format: 'time_series', instant: false });
try {
const res = await datasource.query(options);
const result = makeTimeSeriesList(res.data, options);
@ -241,18 +263,15 @@ export class Explore extends React.Component<any, IExploreState> {
}
async runTableQuery() {
const { datasource, queries, range } = this.state;
const { datasource, queries } = this.state;
if (!hasQuery(queries)) {
return;
}
this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
const now = Date.now();
const options = buildQueryOptions({
const options = this.buildQueryOptions({
format: 'table',
interval: datasource.interval,
instant: true,
range,
queries: queries.map(q => q.query),
});
try {
const res = await datasource.query(options);
@ -301,7 +320,7 @@ export class Explore extends React.Component<any, IExploreState> {
const selectedDatasource = datasource ? datasource.name : undefined;
return (
<div className={exploreClass}>
<div className={exploreClass} ref={this.getRef}>
<div className="navbar">
{position === 'left' ? (
<div>

View File

@ -1,15 +1,3 @@
export function buildQueryOptions({ format, interval, instant, range, queries }) {
return {
interval,
range,
targets: queries.map(expr => ({
expr,
format,
instant,
})),
};
}
export function generateQueryKey(index = 0) {
return `Q-${Date.now()}-${Math.random()}-${index}`;
}

View File

@ -191,7 +191,7 @@ export class KeybindingSrv {
range,
};
const exploreState = encodePathComponent(JSON.stringify(state));
this.$location.url(`/explore/${exploreState}`);
this.$location.url(`/explore?state=${exploreState}`);
}
}
});

View File

@ -332,7 +332,7 @@ class MetricsPanelCtrl extends PanelCtrl {
range,
};
const exploreState = encodePathComponent(JSON.stringify(state));
this.$location.url(`/explore/${exploreState}`);
this.$location.url(`/explore?state=${exploreState}`);
}
addQuery(target) {

View File

@ -112,7 +112,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
controller: 'FolderDashboardsCtrl',
controllerAs: 'ctrl',
})
.when('/explore/:initial?', {
.when('/explore', {
template: '<react-container />',
resolve: {
roles: () => ['Editor', 'Admin'],

View File

@ -24,7 +24,7 @@ small {
font-size: 85%;
}
strong {
font-weight: bold;
font-weight: $font-weight-semi-bold;
}
em {
font-style: italic;
@ -249,7 +249,7 @@ dd {
line-height: $line-height-base;
}
dt {
font-weight: bold;
font-weight: $font-weight-semi-bold;
}
dd {
margin-left: $line-height-base / 2;
@ -376,7 +376,7 @@ a.external-link {
padding: $spacer*0.5 $spacer;
}
th {
font-weight: normal;
font-weight: $font-weight-semi-bold;
background: $table-bg-accent;
}
}
@ -415,3 +415,7 @@ a.external-link {
color: $yellow;
padding: 0;
}
th {
font-weight: $font-weight-semi-bold;
}