Commit a9e22730 by Daniel Lee

Merge remote-tracking branch 'upstream/master' into dashboard_permissions

parents a0fc5383 4ac21d2e
......@@ -9,5 +9,6 @@ FROM grafana/docs-base:latest
COPY config.toml /site
COPY awsconfig /site
COPY versions.json /site/static/js
VOLUME ["/site/content"]
......@@ -3,6 +3,7 @@ title = "Provisioning"
description = ""
keywords = ["grafana", "provisioning"]
type = "docs"
aliases = ["/installation/provisioning"]
[menu.docs]
parent = "admin"
weight = 8
......@@ -66,7 +67,6 @@ Tool | Project
-----|------------
Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana)
Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana)
Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana)
Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana)
Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana)
......
......@@ -5,7 +5,7 @@ type = "docs"
[menu.docs]
name = "Features"
identifier = "features"
weight = 3
weight = 4
+++
......@@ -7,6 +7,7 @@ type = "docs"
name = "Basic Concepts"
identifier = "basic_concepts"
parent = "guides"
weight = 2
+++
# Basic Concepts
......
......@@ -8,6 +8,7 @@ aliases = ["/guides/gettingstarted"]
name = "Getting Started"
identifier = "getting_started_guide"
parent = "guides"
weight = 1
+++
# Getting started
......@@ -24,7 +25,7 @@ Read the [Basic Concepts](/guides/basic_concepts) document to get a crash course
### Top header
Let's start with creating a new Dashboard. You can find the new Dashboard link on the right side of the Dashboard picker. You now have a blank Dashboard.
Let's start with creating a new Dashboard. You can find the new Dashboard link on the right side of the Dashboard picker. You now have a blank Dashboard.
<img class="no-shadow" src="/img/docs/v45/top_nav_annotated.png">
......
......@@ -4,6 +4,6 @@ type = "docs"
[menu.docs]
name = "Getting Started"
identifier = "guides"
weight = 2
weight = 3
+++
......@@ -4,10 +4,6 @@ description = "Install guide for Grafana"
keywords = ["grafana", "installation", "documentation"]
type = "docs"
aliases = ["v1.1", "guides/reference/admin"]
[menu.docs]
name = "Welcome to the Docs"
identifier = "root"
weight = -1
+++
# Welcome to the Grafana Documentation
......@@ -22,7 +18,7 @@ other domains including industrial sensors, home automation, weather, and proces
- [Installing on Mac OS X](installation/mac)
- [Installing on Windows](installation/windows)
- [Installing on Docker](installation/docker)
- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning)
- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](administration/provisioning#configuration-management-tools)
- [Nightly Builds](https://grafana.com/grafana/download)
For other platforms Read the [build from source]({{< relref "project/building_from_source.md" >}})
......
......@@ -7,6 +7,7 @@ aliases = ["installation/installation/", "v2.1/installation/install/"]
[menu.docs]
name = "Installation"
identifier = "installation"
weight = 1
+++
## Installing Grafana
......
......@@ -3,7 +3,7 @@ title = "What's New in Grafana"
[menu.docs]
name = "What's New In Grafana"
identifier = "whatsnew"
weight = 2
weight = 3
+++
[
{ "version": "v5.0", "path": "/v5.0", "archived": false },
{ "version": "v4.6", "path": "/", "archived": false, "current": true },
{ "version": "v4.5", "path": "/v4.5", "archived": true },
{ "version": "v4.4", "path": "/v4.4", "archived": true },
{ "version": "v4.3", "path": "/v4.3", "archived": true },
{ "version": "v4.1", "path": "/v4.1", "archived": true },
{ "version": "v3.1", "path": "/v3.1", "archived": true }
]
......@@ -71,6 +71,7 @@ func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSou
qr := &tsdb.QueryResult{
RefId: r.RefId,
Series: []*tsdb.TimeSeries{},
Tables: []*tsdb.Table{},
}
if r.Error != "" {
......
......@@ -75,7 +75,7 @@ func TestMappingRowValue(t *testing.T) {
boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
haveBool, ok := boolRowValue.(bool)
if !ok || haveBool != true {
t.Fatalf("Expected true, was %s", haveBool)
t.Fatalf("Expected true, was %v", haveBool)
}
intRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_INT64, Int64Value: 42})
......
package alerting
import (
"testing"
"time"
"github.com/benbjohnson/clock"
)
func inspectTick(tick time.Time, last time.Time, offset time.Duration, t *testing.T) {
if !tick.Equal(last.Add(time.Duration(1) * time.Second)) {
t.Fatalf("expected a tick 1 second more than prev, %s. got: %s", last, tick)
}
}
// returns the new last tick seen
func assertAdvanceUntil(ticker *Ticker, last, desiredLast time.Time, offset, wait time.Duration, t *testing.T) time.Time {
for {
select {
case tick := <-ticker.C:
inspectTick(tick, last, offset, t)
last = tick
case <-time.NewTimer(wait).C:
if last.Before(desiredLast) {
t.Fatalf("waited %s for ticker to advance to %s, but only went up to %s", wait, desiredLast, last)
}
if last.After(desiredLast) {
t.Fatalf("timer advanced too far. should only have gone up to %s, but it went up to %s", desiredLast, last)
}
return last
}
}
}
func assertNoAdvance(ticker *Ticker, desiredLast time.Time, wait time.Duration, t *testing.T) {
for {
select {
case tick := <-ticker.C:
t.Fatalf("timer should have stayed at %s, instead it advanced to %s", desiredLast, tick)
case <-time.NewTimer(wait).C:
return
}
}
}
func TestTickerRetro1Hour(t *testing.T) {
offset := time.Duration(10) * time.Second
last := time.Unix(0, 0)
mock := clock.NewMock()
mock.Add(time.Duration(1) * time.Hour)
desiredLast := mock.Now().Add(-offset)
ticker := NewTicker(last, offset, mock)
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
}
func TestAdvanceWithUpdateOffset(t *testing.T) {
offset := time.Duration(10) * time.Second
last := time.Unix(0, 0)
mock := clock.NewMock()
mock.Add(time.Duration(1) * time.Hour)
desiredLast := mock.Now().Add(-offset)
ticker := NewTicker(last, offset, mock)
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
// lowering offset should see a few more ticks
offset = time.Duration(5) * time.Second
ticker.updateOffset(offset)
desiredLast = mock.Now().Add(-offset)
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(9)*time.Millisecond, t)
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
// advancing clock should see even more ticks
mock.Add(time.Duration(1) * time.Hour)
desiredLast = mock.Now().Add(-offset)
last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(8)*time.Millisecond, t)
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
}
func getCase(lastSeconds, offsetSeconds int) (time.Time, time.Duration) {
last := time.Unix(int64(lastSeconds), 0)
offset := time.Duration(offsetSeconds) * time.Second
return last, offset
}
func TestTickerNoAdvance(t *testing.T) {
// it's 00:01:00 now. what are some cases where we don't want the ticker to advance?
mock := clock.NewMock()
mock.Add(time.Duration(60) * time.Second)
type Case struct {
last int
offset int
}
// note that some cases add up to now, others go into the future
cases := []Case{
{50, 10},
{50, 30},
{59, 1},
{59, 10},
{59, 30},
{60, 1},
{60, 10},
{60, 30},
{90, 1},
{90, 10},
{90, 30},
}
for _, c := range cases {
last, offset := getCase(c.last, c.offset)
ticker := NewTicker(last, offset, mock)
assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
}
}
//import (
// "testing"
// "time"
//
// "github.com/benbjohnson/clock"
//)
//
//func inspectTick(tick time.Time, last time.Time, offset time.Duration, t *testing.T) {
// if !tick.Equal(last.Add(time.Duration(1) * time.Second)) {
// t.Fatalf("expected a tick 1 second more than prev, %s. got: %s", last, tick)
// }
//}
//
//// returns the new last tick seen
//func assertAdvanceUntil(ticker *Ticker, last, desiredLast time.Time, offset, wait time.Duration, t *testing.T) time.Time {
// for {
// select {
// case tick := <-ticker.C:
// inspectTick(tick, last, offset, t)
// last = tick
// case <-time.NewTimer(wait).C:
// if last.Before(desiredLast) {
// t.Fatalf("waited %s for ticker to advance to %s, but only went up to %s", wait, desiredLast, last)
// }
// if last.After(desiredLast) {
// t.Fatalf("timer advanced too far. should only have gone up to %s, but it went up to %s", desiredLast, last)
// }
// return last
// }
// }
//}
//
//func assertNoAdvance(ticker *Ticker, desiredLast time.Time, wait time.Duration, t *testing.T) {
// for {
// select {
// case tick := <-ticker.C:
// t.Fatalf("timer should have stayed at %s, instead it advanced to %s", desiredLast, tick)
// case <-time.NewTimer(wait).C:
// return
// }
// }
//}
//
//func TestTickerRetro1Hour(t *testing.T) {
// offset := time.Duration(10) * time.Second
// last := time.Unix(0, 0)
// mock := clock.NewMock()
// mock.Add(time.Duration(1) * time.Hour)
// desiredLast := mock.Now().Add(-offset)
// ticker := NewTicker(last, offset, mock)
//
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
//
//}
//
//func TestAdvanceWithUpdateOffset(t *testing.T) {
// offset := time.Duration(10) * time.Second
// last := time.Unix(0, 0)
// mock := clock.NewMock()
// mock.Add(time.Duration(1) * time.Hour)
// desiredLast := mock.Now().Add(-offset)
// ticker := NewTicker(last, offset, mock)
//
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(10)*time.Millisecond, t)
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
//
// // lowering offset should see a few more ticks
// offset = time.Duration(5) * time.Second
// ticker.updateOffset(offset)
// desiredLast = mock.Now().Add(-offset)
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(9)*time.Millisecond, t)
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
//
// // advancing clock should see even more ticks
// mock.Add(time.Duration(1) * time.Hour)
// desiredLast = mock.Now().Add(-offset)
// last = assertAdvanceUntil(ticker, last, desiredLast, offset, time.Duration(8)*time.Millisecond, t)
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
//
//}
//
//func getCase(lastSeconds, offsetSeconds int) (time.Time, time.Duration) {
// last := time.Unix(int64(lastSeconds), 0)
// offset := time.Duration(offsetSeconds) * time.Second
// return last, offset
//}
//
//func TestTickerNoAdvance(t *testing.T) {
//
// // it's 00:01:00 now. what are some cases where we don't want the ticker to advance?
// mock := clock.NewMock()
// mock.Add(time.Duration(60) * time.Second)
//
// type Case struct {
// last int
// offset int
// }
//
// // note that some cases add up to now, others go into the future
// cases := []Case{
// {50, 10},
// {50, 30},
// {59, 1},
// {59, 10},
// {59, 30},
// {60, 1},
// {60, 10},
// {60, 30},
// {90, 1},
// {90, 10},
// {90, 30},
// }
// for _, c := range cases {
// last, offset := getCase(c.last, c.offset)
// ticker := NewTicker(last, offset, mock)
// assertNoAdvance(ticker, last, time.Duration(500)*time.Millisecond, t)
// }
//}
......@@ -12,7 +12,11 @@ export function toUrlParams(a) {
let add = function(k, v) {
v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v;
s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v);
if (typeof v !== 'boolean') {
s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v);
} else {
s[s.length] = encodeURIComponent(k);
}
};
let buildParams = function(prefix, obj) {
......
......@@ -352,16 +352,10 @@ export class DashboardModel {
copy.scopedVars[variable.name] = option;
if (panel.repeatDirection === REPEAT_DIR_VERTICAL) {
copy.gridPos.y = yPos;
yPos += copy.gridPos.h;
// Update gridPos for panels below
let panelBelowIndex = panelIndex + index + 1;
for (let i = panelBelowIndex; i < this.panels.length; i++) {
if (this.panels[i].gridPos.y < yPos) {
this.panels[i].gridPos.y += copy.gridPos.h;
}
if (index > 0) {
yPos += copy.gridPos.h;
}
copy.gridPos.y = yPos;
} else {
// set width based on how many are selected
// assumed the repeated panels should take up full row width
......@@ -378,6 +372,15 @@ export class DashboardModel {
}
}
}
// Update gridPos for panels below
let yOffset = yPos - panel.gridPos.y;
if (yOffset > 0) {
let panelBelowIndex = panelIndex + selectedOptions.length;
for (let i = panelBelowIndex; i < this.panels.length; i++) {
this.panels[i].gridPos.y += yOffset;
}
}
}
repeatRow(panel: PanelModel, panelIndex: number, variable) {
......@@ -566,6 +569,7 @@ export class DashboardModel {
if (row.collapsed) {
row.collapsed = false;
let hasRepeat = false;
if (row.panels.length > 0) {
// Use first panel to figure out if it was moved or pushed
......@@ -586,6 +590,10 @@ export class DashboardModel {
// update insert post and y max
insertPos += 1;
yMax = Math.max(yMax, panel.gridPos.y + panel.gridPos.h);
if (panel.repeat) {
hasRepeat = true;
}
}
const pushDownAmount = yMax - row.gridPos.y;
......@@ -596,6 +604,10 @@ export class DashboardModel {
}
row.panels = [];
if (hasRepeat) {
this.processRepeats();
}
}
// sort panels
......
......@@ -4,6 +4,57 @@ import { expect } from 'test/lib/common';
jest.mock('app/core/services/context_srv', () => ({}));
describe('given dashboard with panel repeat', function() {
var dashboard;
beforeEach(function() {
let dashboardJSON = {
panels: [
{ id: 1, type: 'row', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
{ id: 2, repeat: 'apps', repeatDirection: 'h', gridPos: { x: 0, y: 1, h: 2, w: 8 } },
],
templating: {
list: [
{
name: 'apps',
current: {
text: 'se1, se2, se3',
value: ['se1', 'se2', 'se3'],
},
options: [
{ text: 'se1', value: 'se1', selected: true },
{ text: 'se2', value: 'se2', selected: true },
{ text: 'se3', value: 'se3', selected: true },
{ text: 'se4', value: 'se4', selected: false },
],
},
],
},
};
dashboard = new DashboardModel(dashboardJSON);
dashboard.processRepeats();
});
it('should repeat panels when row is expanding', function() {
expect(dashboard.panels.length).toBe(4);
// toggle row
dashboard.toggleRow(dashboard.panels[0]);
expect(dashboard.panels.length).toBe(1);
// change variable
dashboard.templating.list[0].options[2].selected = false;
dashboard.templating.list[0].current = {
text: 'se1, se2',
value: ['se1', 'se2'],
};
// toggle row back
dashboard.toggleRow(dashboard.panels[0]);
expect(dashboard.panels.length).toBe(3);
});
});
describe('given dashboard with panel repeat in horizontal direction', function() {
var dashboard;
......@@ -178,6 +229,88 @@ describe('given dashboard with panel repeat in vertical direction', function() {
});
});
describe('given dashboard with row repeat and panel repeat in horizontal direction', () => {
let dashboard, dashboardJSON;
beforeEach(() => {
dashboardJSON = {
panels: [
{ id: 1, type: 'row', repeat: 'region', gridPos: { x: 0, y: 0, h: 1, w: 24 } },
{ id: 2, type: 'graph', repeat: 'app', gridPos: { x: 0, y: 1, h: 2, w: 6 } },
],
templating: {
list: [
{
name: 'region',
current: {
text: 'reg1, reg2',
value: ['reg1', 'reg2'],
},
options: [{ text: 'reg1', value: 'reg1', selected: true }, { text: 'reg2', value: 'reg2', selected: true }],
},
{
name: 'app',
current: {
text: 'se1, se2, se3, se4, se5, se6',
value: ['se1', 'se2', 'se3', 'se4', 'se5', 'se6'],
},
options: [
{ text: 'se1', value: 'se1', selected: true },
{ text: 'se2', value: 'se2', selected: true },
{ text: 'se3', value: 'se3', selected: true },
{ text: 'se4', value: 'se4', selected: true },
{ text: 'se5', value: 'se5', selected: true },
{ text: 'se6', value: 'se6', selected: true },
],
},
],
},
};
dashboard = new DashboardModel(dashboardJSON);
dashboard.processRepeats(false);
});
it('should panels in self row', () => {
const panel_types = _.map(dashboard.panels, 'type');
expect(panel_types).toEqual([
'row',
'graph',
'graph',
'graph',
'graph',
'graph',
'graph',
'row',
'graph',
'graph',
'graph',
'graph',
'graph',
'graph',
]);
});
it('should be placed in their places', function() {
expect(dashboard.panels[0].gridPos).toMatchObject({ x: 0, y: 0, h: 1, w: 24 }); // 1st row
expect(dashboard.panels[1].gridPos).toMatchObject({ x: 0, y: 1, h: 2, w: 6 });
expect(dashboard.panels[2].gridPos).toMatchObject({ x: 6, y: 1, h: 2, w: 6 });
expect(dashboard.panels[3].gridPos).toMatchObject({ x: 12, y: 1, h: 2, w: 6 });
expect(dashboard.panels[4].gridPos).toMatchObject({ x: 18, y: 1, h: 2, w: 6 });
expect(dashboard.panels[5].gridPos).toMatchObject({ x: 0, y: 3, h: 2, w: 6 }); // next row
expect(dashboard.panels[6].gridPos).toMatchObject({ x: 6, y: 3, h: 2, w: 6 });
expect(dashboard.panels[7].gridPos).toMatchObject({ x: 0, y: 5, h: 1, w: 24 });
expect(dashboard.panels[8].gridPos).toMatchObject({ x: 0, y: 6, h: 2, w: 6 }); // 2nd row
expect(dashboard.panels[9].gridPos).toMatchObject({ x: 6, y: 6, h: 2, w: 6 });
expect(dashboard.panels[10].gridPos).toMatchObject({ x: 12, y: 6, h: 2, w: 6 });
expect(dashboard.panels[11].gridPos).toMatchObject({ x: 18, y: 6, h: 2, w: 6 }); // next row
expect(dashboard.panels[12].gridPos).toMatchObject({ x: 0, y: 8, h: 2, w: 6 });
expect(dashboard.panels[13].gridPos).toMatchObject({ x: 6, y: 8, h: 2, w: 6 });
});
});
describe('given dashboard with row repeat', function() {
let dashboard, dashboardJSON;
......
......@@ -12,6 +12,7 @@ export class PlaylistSearchCtrl {
$timeout(() => {
this.query.query = '';
this.query.type = 'dash-db';
this.searchDashboards();
}, 100);
}
......
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Metric</label>
<label class="gf-form-label query-keyword width-8">Metric</label>
<metric-segment segment="regionSegment" get-options="getRegions()" on-change="regionChanged()"></metric-segment>
<metric-segment segment="namespaceSegment" get-options="getNamespaces()" on-change="namespaceChanged()"></metric-segment>
......@@ -22,7 +22,7 @@
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Dimensions</label>
<label class="gf-form-label query-keyword width-8">Dimensions</label>
<metric-segment ng-repeat="segment in dimSegments" segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
</div>
......@@ -33,9 +33,9 @@
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">
Period
<info-popover mode="right-normal">Interval between points in seconds</info-popover>
<label class="gf-form-label query-keyword width-8">
Min period
<info-popover mode="right-normal">Minimum interval between points in seconds</info-popover>
</label>
<input type="text" class="gf-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="onChange()" />
</div>
......
<div class="panel-alert-list">
<div class="panel-alert-list__no-alerts" ng-show="ctrl.noAlertsMessage">
{{ctrl.noAlertsMessage}}
</div>
<div class="panel-alert-list__no-alerts" ng-show="ctrl.noAlertsMessage">
{{ctrl.noAlertsMessage}}
</div>
<section ng-if="ctrl.panel.show === 'current'">
<ol class="alert-rule-list">
<li class="alert-rule-item" ng-repeat="alert in ctrl.currentAlerts">
<div class="alert-rule-item__icon {{alert.stateModel.stateClass}}">
<i class="{{alert.stateModel.iconClass}}"></i>
</div>
<div class="alert-rule-item__body">
<div class="alert-rule-item__icon {{alert.stateModel.stateClass}}">
<i class="{{alert.stateModel.iconClass}}"></i>
</div>
<div class="alert-rule-item__header">
<p class="alert-rule-item__name">
<a href="dashboard/{{alert.dashboardUri}}?panelId={{alert.panelId}}&fullscreen&edit&tab=alert">
......
......@@ -24,4 +24,10 @@ describe('ViewStore', () => {
expect(toJS(store.query.get('values'))).toMatchObject(['A', 'B']);
expect(store.currentUrl).toBe('/hello?values=A&values=B');
});
it('Query can contain boolean', () => {
store.updatePathAndQuery('/hello', { abool: true });
expect(toJS(store.query.get('abool'))).toBe(true);
expect(store.currentUrl).toBe('/hello?abool');
});
});
......@@ -135,6 +135,7 @@ $list-item-bg: $card-background;
$list-item-hover-bg: lighten($gray-blue, 2%);
$list-item-link-color: $text-color;
$list-item-shadow: $card-shadow;
$empty-list-cta-bg: $gray-blue;
// Scrollbars
$scrollbarBackground: #404357;
......
......@@ -133,6 +133,7 @@ $list-item-bg: linear-gradient(135deg, $gray-5, $gray-6); //$card-background;
$list-item-hover-bg: darken($gray-5, 5%);
$list-item-link-color: $text-color;
$list-item-shadow: $card-shadow;
$empty-list-cta-bg: $gray-6;
// Tables
// -------------------------
......
.empty-list-cta {
background-color: $search-filter-box-bg;
background-color: $empty-list-cta-bg;
text-align: center;
padding: $spacer*2;
border-radius: $border-radius;
......
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