Merge branch 'develop' of github.com:grafana/grafana into develop

This commit is contained in:
Torkel Ödegaard
2017-10-11 16:48:30 +02:00
53 changed files with 957 additions and 1542 deletions

View File

@@ -110,18 +110,21 @@
"angular-sanitize": "^1.6.6",
"babel-polyfill": "^6.26.0",
"brace": "^0.10.0",
"classnames": "^2.2.5",
"clipboard": "^1.7.1",
"eventemitter3": "^2.0.2",
"gridstack": "https://github.com/grafana/gridstack.js#grafana",
"gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana",
"file-saver": "^1.3.3",
"gemini-scrollbar": "https://github.com/grafana/gemini-scrollbar#grafana",
"jquery": "^3.2.1",
"lodash": "^4.17.4",
"moment": "^2.18.1",
"mousetrap": "^1.6.0",
"ngreact": "^0.4.1",
"prop-types": "^15.6.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-grid-layout": "^0.16.0",
"react-sizeme": "^2.3.6",
"remarkable": "^1.7.1",
"rxjs": "^5.4.3",
"tether": "^1.4.0",

View File

@@ -278,12 +278,14 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
panels := dash.Get("panels").MustArray()
newpanel := simplejson.NewFromAny(map[string]interface{}{
"type": "gettingstarted",
"id": 123123,
"x": 0,
"y": 3,
"width": 12,
"height": 4,
"type": "gettingstarted",
"id": 123123,
"gridPos": map[string]interface{}{
"x": 0,
"y": 3,
"w": 12,
"h": 4,
},
})
panels = append(panels, newpanel)

View File

@@ -189,14 +189,13 @@ type DashboardSearchProjection struct {
}
func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) {
var sql bytes.Buffer
params := make([]interface{}, 0)
limit := query.Limit
if limit == 0 {
limit = 1000
}
var sql bytes.Buffer
params := make([]interface{}, 0)
sql.WriteString(`
SELECT
dashboard.id,
@@ -207,36 +206,69 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
dashboard.folder_id,
folder.slug as folder_slug,
folder.title as folder_title
FROM (
SELECT
dashboard.id FROM dashboard
LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id
`)
FROM `)
// add tags filter
if len(query.Tags) > 0 {
sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `)`)
sql.WriteString(
`(
SELECT
dashboard.id FROM dashboard
LEFT OUTER JOIN dashboard_tag ON dashboard_tag.dashboard_id = dashboard.id
`)
if query.IsStarred {
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
}
sql.WriteString(` WHERE dashboard_tag.term IN (?` + strings.Repeat(",?", len(query.Tags)-1) + `) AND `)
for _, tag := range query.Tags {
params = append(params, tag)
}
params = createSearchWhereClause(query, &sql, params)
fmt.Printf("params2 %v", params)
// this ends the inner select (tag filtered part)
sql.WriteString(`
GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
LIMIT ?) as ids
INNER JOIN dashboard on ids.id = dashboard.id
`)
params = append(params, len(query.Tags))
params = append(params, limit)
} else {
sql.WriteString(`( SELECT dashboard.id FROM dashboard `)
if query.IsStarred {
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
}
sql.WriteString(` WHERE `)
params = createSearchWhereClause(query, &sql, params)
sql.WriteString(`
LIMIT ?) as ids
INNER JOIN dashboard on ids.id = dashboard.id
`)
params = append(params, limit)
}
// this ends the inner select (tag filtered part)
sql.WriteString(`
GROUP BY dashboard.id HAVING COUNT(dashboard.id) >= ?
ORDER BY dashboard.title ASC LIMIT ?) as ids`)
params = append(params, len(query.Tags))
params = append(params, limit)
sql.WriteString(`
INNER JOIN dashboard on ids.id = dashboard.id
LEFT OUTER JOIN dashboard folder on folder.id = dashboard.folder_id
LEFT OUTER JOIN dashboard_tag on dashboard.id = dashboard_tag.dashboard_id`)
if query.IsStarred {
sql.WriteString(" INNER JOIN star on star.dashboard_id = dashboard.id")
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 5000"))
var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res)
if err != nil {
return nil, err
}
sql.WriteString(` WHERE dashboard.org_id=?`)
return res, nil
}
func createSearchWhereClause(query *search.FindPersistedDashboardsQuery, sql *bytes.Buffer, params []interface{}) []interface{} {
sql.WriteString(` dashboard.org_id=?`)
params = append(params, query.SignedInUser.OrgId)
if query.IsStarred {
@@ -253,16 +285,17 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
if query.SignedInUser.OrgRole != m.ROLE_ADMIN {
allowedDashboardsSubQuery := ` AND (dashboard.has_acl = 0 OR dashboard.id in (
SELECT distinct d.id AS DashboardId
SELECT distinct d.id AS DashboardId
FROM dashboard AS d
LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
LEFT JOIN user_group_member as ugm on ugm.user_group_id = da.user_group_id
LEFT JOIN org_user ou on ou.role = da.role
LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id
LEFT JOIN user_group_member as ugm on ugm.user_group_id = da.user_group_id
LEFT JOIN org_user ou on ou.role = da.role
WHERE
d.has_acl = 1 and
(da.user_id = ? or ugm.user_id = ? or ou.id is not null)
and d.org_id = ?
))`
)
)`
sql.WriteString(allowedDashboardsSubQuery)
params = append(params, query.SignedInUser.UserId, query.SignedInUser.UserId, query.SignedInUser.OrgId)
@@ -286,16 +319,7 @@ func findDashboards(query *search.FindPersistedDashboardsQuery) ([]DashboardSear
params = append(params, query.FolderId)
}
sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000"))
var res []DashboardSearchProjection
err := x.Sql(sql.String(), params...).Find(&res)
if err != nil {
return nil, err
}
return res, nil
return params
}
func SearchDashboards(query *search.FindPersistedDashboardsQuery) error {

View File

@@ -3,6 +3,7 @@ package sqlstore
import (
"testing"
"github.com/go-xorm/xorm"
. "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/components/simplejson"
@@ -12,9 +13,10 @@ import (
)
func TestDashboardDataAccess(t *testing.T) {
var x *xorm.Engine
Convey("Testing DB", t, func() {
InitTestDB(t)
x = InitTestDB(t)
Convey("Given saved dashboard", func() {
savedFolder := insertTestDashboard("1 test dash folder", 1, 0, true, "prod", "webapp")

View File

@@ -11,11 +11,10 @@ import (
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
)
func InitTestDB(t *testing.T) {
func InitTestDB(t *testing.T) *xorm.Engine {
x, err := xorm.NewEngine(sqlutil.TestDB_Sqlite3.DriverName, sqlutil.TestDB_Sqlite3.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
// x.ShowSQL()
// x.ShowSQL()
@@ -28,6 +27,8 @@ func InitTestDB(t *testing.T) {
if err := SetEngine(x); err != nil {
t.Fatal(err)
}
return x
}
type Test struct {

View File

@@ -19,7 +19,7 @@ function (_, $, coreModule) {
});
var lastHideControlsVal;
$scope.$watch('dashboard.hideControls', function() {
$scope.$watch('ctrl.dashboard.hideControls', function() {
if (!$scope.dashboard) {
return;
}
@@ -31,7 +31,7 @@ function (_, $, coreModule) {
}
});
$scope.$watch('playlistSrv', function(newValue) {
$scope.$watch('ctrl.playlistSrv', function(newValue) {
elem.toggleClass('playlist-active', _.isObject(newValue));
});
}

View File

@@ -123,7 +123,7 @@ function ($, angular, coreModule, _) {
}, 10);
}
scope.$watch("dashboardViewState.state.editview", function(newValue, oldValue) {
scope.$watch("ctrl.dashboardViewState.state.editview", function(newValue, oldValue) {
if (newValue) {
showEditorPane(null, {editview: newValue});
} else if (oldValue) {

View File

@@ -235,42 +235,54 @@ export class BackendSrv {
createDashboardFolder(name) {
const dash = {
schemaVersion: 16,
title: name,
editable: true,
hideControls: true,
rows: [
panels: [
{
panels: [
{
folderId: 0,
headings: false,
limit: 1000,
links: [],
query: '',
recent: false,
search: true,
span: 4,
starred: false,
tags: [],
title: 'Dashboards in this folder',
type: 'dashlist'
},
{
onlyAlertsOnDashboard: true,
span: 4,
title: 'Alerts in this folder',
type: 'alertlist'
},
{
span: 4,
title: 'Permissions for this folder',
type: 'permissionlist',
folderId: 0
}
],
showTitle: true,
title: name,
titleSize: 'h1'
id: 1,
folderId: 0,
headings: false,
limit: 1000,
links: [],
query: '',
recent: false,
search: true,
starred: false,
tags: [],
title: 'Dashboards in this folder',
type: 'dashlist',
gridPos: {
x: 0,
y: 0,
w: 4,
h: 10
}
},
{
id: 2,
onlyAlertsOnDashboard: true,
title: 'Alerts in this folder',
type: 'alertlist',
gridPos: {
x: 4,
y: 0,
w: 4,
h: 10
}
},
{
id: 3,
title: 'Permissions for this folder',
type: 'permissionlist',
folderId: 0,
gridPos: {
x: 8,
y: 0,
w: 4,
h: 10
}
}
]
};
@@ -280,8 +292,8 @@ export class BackendSrv {
return this.getDashboard('db', res.slug);
})
.then(res => {
res.dashboard.rows[0].panels[0].folderId = res.dashboard.id;
res.dashboard.rows[0].panels[2].folderId = res.dashboard.id;
res.dashboard.panels[0].folderId = res.dashboard.id;
res.dashboard.panels[2].folderId = res.dashboard.id;
return this.saveDashboard(res.dashboard, {overwrite: false});
});
}

View File

@@ -1,5 +1,3 @@
///<reference path="../../headers/common.d.ts" />
import angular from 'angular';
import moment from 'moment';
import _ from 'lodash';
@@ -8,20 +6,11 @@ import $ from 'jquery';
import {DEFAULT_ANNOTATION_COLOR} from 'app/core/utils/colors';
import {Emitter, contextSrv, appEvents} from 'app/core/core';
import {DashboardRow} from './row/row_model';
import {PanelModel} from './PanelModel';
import sortByKeys from 'app/core/utils/sort_by_keys';
export interface Panel {
id: number;
x: number;
y: number;
width: number;
height: number;
type: string;
title: string;
}
export const CELL_HEIGHT = 30;
export const CELL_VMARGIN = 15;
export const CELL_VMARGIN = 10;
export class DashboardModel {
id: any;
@@ -50,7 +39,7 @@ export class DashboardModel {
events: any;
editMode: boolean;
folderId: number;
panels: Panel[];
panels: PanelModel[];
constructor(data, meta?) {
if (!data) {
@@ -80,8 +69,7 @@ export class DashboardModel {
this.links = data.links || [];
this.gnetId = data.gnetId || null;
this.folderId = data.folderId || null;
this.panels = data.panels || [];
this.rows = [];
this.panels = _.map(data.panels || [], panelData => new PanelModel(panelData));
this.addBuiltInAnnotationQuery();
this.initMeta(meta);
@@ -134,32 +122,39 @@ export class DashboardModel {
// temp remove stuff
var events = this.events;
var meta = this.meta;
var rows = this.rows;
var variables = this.templating.list;
var panels = this.panels;
delete this.events;
delete this.meta;
delete this.panels;
// prepare save model
this.rows = _.map(rows, row => row.getSaveModel());
this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
this.panels = _.map(panels, panel => panel.getSaveModel());
// make clone
var copy = $.extend(true, {}, this);
// sort clone
copy = sortByKeys(copy);
console.log(copy.panels);
// restore properties
this.events = events;
this.meta = meta;
this.rows = rows;
this.templating.list = variables;
this.panels = panels;
return copy;
}
addEmptyRow() {
this.rows.push(new DashboardRow({isNew: true}));
setViewMode(panel: PanelModel, fullscreen: boolean, isEditing: boolean) {
this.meta.fullscreen = fullscreen;
this.meta.isEditing = isEditing && this.meta.canEdit;
panel.setViewMode(fullscreen, this.meta.isEditing);
this.events.emit('view-mode-changed', panel);
}
private ensureListExist(data) {
@@ -194,7 +189,8 @@ export class DashboardModel {
addPanel(panel) {
panel.id = this.getNextPanelId();
this.panels.push(panel);
this.panels.unshift(new PanelModel(panel));
this.events.emit('panel-added', panel);
}
removePanel(panel, ask?) {
@@ -296,6 +292,14 @@ export class DashboardModel {
}
}
on(eventName, callback) {
this.events.on(eventName, callback);
}
off(eventName, callback?) {
this.events.off(eventName, callback);
}
cycleGraphTooltip() {
this.graphTooltip = (this.graphTooltip + 1) % 3;
}
@@ -338,7 +342,7 @@ export class DashboardModel {
var i, j, k;
var oldVersion = this.schemaVersion;
var panelUpgrades = [];
this.schemaVersion = 15;
this.schemaVersion = 16;
if (oldVersion === this.schemaVersion) {
return;
@@ -647,7 +651,7 @@ export class DashboardModel {
this.graphTooltip = old.sharedCrosshair ? 1 : 0;
}
if (oldVersion < 15) {
if (oldVersion < 16) {
this.upgradeToGridLayout(old);
}
@@ -655,60 +659,59 @@ export class DashboardModel {
return;
}
for (i = 0; i < this.rows.length; i++) {
var row = this.rows[i];
for (j = 0; j < row.panels.length; j++) {
for (k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k].call(this, row.panels[j]);
}
for (j = 0; j < this.panels.length; j++) {
for (k = 0; k < panelUpgrades.length; k++) {
panelUpgrades[k].call(this, this.panels[j]);
}
}
}
upgradeToGridLayout(old) {
let yPos = 0;
let rowIds = 1000;
//let rowIds = 1000;
//
if (!old.rows) {
return;
}
for (let row of old.rows) {
let xPos = 0;
let height: any = row.height;
let height: any = row.height || 250;
if (this.meta.keepRows) {
this.panels.push({
id: rowIds++,
type: 'row',
title: row.title,
x: 0,
y: yPos,
height: 1,
width: 12
});
yPos += 1;
}
// if (this.meta.keepRows) {
// this.panels.push({
// id: rowIds++,
// type: 'row',
// title: row.title,
// x: 0,
// y: yPos,
// height: 1,
// width: 12
// });
//
// yPos += 1;
// }
if (_.isString(height)) {
height = parseInt(height.replace('px', ''), 10);
}
height = Math.ceil(height / CELL_HEIGHT);
const rowGridHeight = Math.ceil(height / CELL_HEIGHT);
for (let panel of row.panels) {
// should wrap to next row?
if (xPos + panel.span >= 12) {
yPos += height;
yPos += rowGridHeight;
}
panel.x = xPos;
panel.y = yPos;
panel.width = panel.span;
panel.height = height;
panel.gridPos = { x: xPos, y: yPos, w: panel.span, h: rowGridHeight };
delete panel.span;
xPos += panel.width;
xPos += rowGridHeight;
this.panels.push(panel);
this.panels.push(new PanelModel(panel));
}
yPos += height;

View File

@@ -0,0 +1,74 @@
import {Emitter} from 'app/core/core';
export interface GridPos {
x: number;
y: number;
w: number;
h: number;
}
const notPersistedProperties: {[str: string]: boolean} = {
"events": true,
"fullscreen": true,
"isEditing": true,
};
export class PanelModel {
id: number;
gridPos: GridPos;
type: string;
title: string;
alert?: any;
// non persisted
fullscreen: boolean;
isEditing: boolean;
events: Emitter;
constructor(model) {
this.events = new Emitter();
// copy properties from persisted model
for (var property in model) {
this[property] = model[property];
}
}
getSaveModel() {
const model: any = {};
for (var property in this) {
if (notPersistedProperties[property] || !this.hasOwnProperty(property)) {
continue;
}
model[property] = this[property];
}
return model;
}
setViewMode(fullscreen: boolean, isEditing: boolean) {
this.fullscreen = fullscreen;
this.isEditing = isEditing;
this.events.emit('panel-size-changed');
}
updateGridPos(newPos: GridPos) {
let sizeChanged = false;
if (this.gridPos.w !== newPos.w || this.gridPos.h !== newPos.h) {
sizeChanged = true;
}
this.gridPos.x = newPos.x;
this.gridPos.y = newPos.y;
this.gridPos.w = newPos.w;
this.gridPos.h = newPos.h;
if (sizeChanged) {
console.log('PanelModel sizeChanged event and render events fired');
this.events.emit('panel-size-changed');
}
}
}

View File

@@ -15,7 +15,6 @@ define([
'./unsavedChangesSrv',
'./unsaved_changes_modal',
'./timepicker/timepicker',
'./graphiteImportCtrl',
'./impression_store',
'./upload',
'./import/dash_import',
@@ -23,7 +22,9 @@ define([
'./export_data/export_data_modal',
'./ad_hoc_filters',
'./repeat_option/repeat_option',
'./dashgrid/dashgrid',
'./dashgrid/DashboardGrid',
'./dashgrid/PanelLoader',
'./row/add_panel',
'./acl/acl',
'./folder_picker/picker',
'./folder_modal/folder'

View File

@@ -1,143 +1,143 @@
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import coreModule from 'app/core/core_module';
import {PanelContainer} from './dashgrid/PanelContainer';
import {DashboardModel} from './DashboardModel';
export class DashboardCtrl {
export class DashboardCtrl implements PanelContainer {
dashboard: DashboardModel;
dashboardViewState: any;
loadedFallbackDashboard: boolean;
editTab: number;
/** @ngInject */
constructor(
private $scope,
$rootScope,
keybindingSrv,
timeSrv,
variableSrv,
alertingSrv,
dashboardSrv,
unsavedChangesSrv,
dynamicDashboardSrv,
dashboardViewStateSrv,
contextSrv,
alertSrv,
$timeout) {
private $rootScope,
private keybindingSrv,
private timeSrv,
private variableSrv,
private alertingSrv,
private dashboardSrv,
private unsavedChangesSrv,
private dynamicDashboardSrv,
private dashboardViewStateSrv,
private panelLoader) {
// temp hack due to way dashboards are loaded
// can't use controllerAs on route yet
$scope.ctrl = this;
$scope.editor = { index: 0 };
// TODO: break out settings view to separate view & controller
this.editTab = 0;
var resizeEventTimeout;
// funcs called from React component bindings and needs this binding
this.getPanelContainer = this.getPanelContainer.bind(this);
}
$scope.setupDashboard = function(data) {
try {
$scope.setupDashboardInternal(data);
} catch (err) {
$scope.onInitFailed(err, 'Dashboard init failed', true);
}
};
setupDashboard(data) {
try {
this.setupDashboardInternal(data);
} catch (err) {
this.onInitFailed(err, 'Dashboard init failed', true);
}
}
$scope.setupDashboardInternal = function(data) {
var dashboard = dashboardSrv.create(data.dashboard, data.meta);
dashboardSrv.setCurrent(dashboard);
setupDashboardInternal(data) {
const dashboard = this.dashboardSrv.create(data.dashboard, data.meta);
this.dashboardSrv.setCurrent(dashboard);
// init services
timeSrv.init(dashboard);
alertingSrv.init(dashboard, data.alerts);
// init services
this.timeSrv.init(dashboard);
this.alertingSrv.init(dashboard, data.alerts);
// template values service needs to initialize completely before
// the rest of the dashboard can load
variableSrv.init(dashboard)
// template values failes are non fatal
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
// continue
.finally(function() {
dynamicDashboardSrv.init(dashboard);
dynamicDashboardSrv.process();
// template values service needs to initialize completely before
// the rest of the dashboard can load
this.variableSrv.init(dashboard)
// template values failes are non fatal
.catch(this.onInitFailed.bind(this, 'Templating init failed', false))
// continue
.finally(() => {
this.dashboard = dashboard;
unsavedChangesSrv.init(dashboard, $scope);
this.dynamicDashboardSrv.init(dashboard);
this.dynamicDashboardSrv.process();
$scope.dashboard = dashboard;
$scope.dashboardMeta = dashboard.meta;
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
this.unsavedChangesSrv.init(dashboard, this.$scope);
keybindingSrv.setupDashboardBindings($scope, dashboard);
// TODO refactor ViewStateSrv
this.$scope.dashboard = dashboard;
this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope);
$scope.dashboard.updateSubmenuVisibility();
$scope.setWindowTitleAndTheme();
this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard);
$scope.appEvent("dashboard-initialized", $scope.dashboard);
})
.catch($scope.onInitFailed.bind(this, 'Dashboard init failed', true));
};
this.dashboard.updateSubmenuVisibility();
this.setWindowTitleAndTheme();
$scope.onInitFailed = function(msg, fatal, err) {
console.log(msg, err);
this.$scope.appEvent("dashboard-initialized", dashboard);
})
.catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
}
if (err.data && err.data.message) {
err.message = err.data.message;
} else if (!err.message) {
err = {message: err.toString()};
}
onInitFailed(msg, fatal, err) {
console.log(msg, err);
$scope.appEvent("alert-error", [msg, err.message]);
if (err.data && err.data.message) {
err.message = err.data.message;
} else if (!err.message) {
err = {message: err.toString()};
}
// protect against recursive fallbacks
if (fatal && !$scope.loadedFallbackDashboard) {
$scope.loadedFallbackDashboard = true;
$scope.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
}
};
this.$scope.appEvent("alert-error", [msg, err.message]);
$scope.templateVariableUpdated = function() {
dynamicDashboardSrv.process();
};
// protect against recursive fallbacks
if (fatal && !this.loadedFallbackDashboard) {
this.loadedFallbackDashboard = true;
this.setupDashboard({dashboard: {title: 'Dashboard Init failed'}});
}
}
$scope.setWindowTitleAndTheme = function() {
window.document.title = config.window_title_prefix + $scope.dashboard.title;
};
templateVariableUpdated() {
this.dynamicDashboardSrv.process();
}
$scope.broadcastRefresh = function() {
$rootScope.$broadcast('refresh');
};
setWindowTitleAndTheme() {
window.document.title = config.window_title_prefix + this.dashboard.title;
}
$scope.addRowDefault = function() {
$scope.dashboard.addEmptyRow();
};
showJsonEditor(evt, options) {
var editScope = this.$rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
this.$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
}
$scope.showJsonEditor = function(evt, options) {
var editScope = $rootScope.$new();
editScope.object = options.object;
editScope.updateHandler = options.updateHandler;
$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope });
};
getDashboard() {
return this.dashboard;
}
$scope.registerWindowResizeEvent = function() {
angular.element(window).bind('resize', function() {
$timeout.cancel(resizeEventTimeout);
resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
});
getPanelLoader() {
return this.panelLoader;
}
$scope.$on('$destroy', function() {
angular.element(window).unbind('resize');
$scope.dashboard.destroy();
});
};
timezoneChanged() {
this.$rootScope.$broadcast("refresh");
}
$scope.timezoneChanged = function() {
$rootScope.$broadcast("refresh");
};
onFolderChange(folder) {
this.dashboard.folderId = folder.id;
this.dashboard.meta.folderId = folder.id;
this.dashboard.meta.folderTitle= folder.title;
}
$scope.onFolderChange = function(folder) {
$scope.dashboard.folderId = folder.id;
$scope.dashboard.meta.folderId = folder.id;
$scope.dashboard.meta.folderTitle= folder.title;
};
getPanelContainer() {
console.log('DashboardCtrl:getPanelContainer()');
return this;
}
init(dashboard) {
this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor);
this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated);
this.$scope.setupDashboard(dashboard);
this.$scope.registerWindowResizeEvent();
this.setupDashboard(dashboard);
}
}

View File

@@ -1,7 +1,5 @@
///<reference path="../../headers/common.d.ts" />
import coreModule from 'app/core/core_module';
import {DashboardModel} from './model';
import {DashboardModel} from './DashboardModel';
export class DashboardSrv {
dash: any;

View File

@@ -0,0 +1,131 @@
import React from 'react';
import coreModule from 'app/core/core_module';
import ReactGridLayout from 'react-grid-layout';
import {CELL_HEIGHT, CELL_VMARGIN} from '../DashboardModel';
import {DashboardPanel} from './DashboardPanel';
import {DashboardModel} from '../DashboardModel';
import {PanelContainer} from './PanelContainer';
import {PanelModel} from '../PanelModel';
import classNames from 'classnames';
import sizeMe from 'react-sizeme';
const COLUMN_COUNT = 12;
function GridWrapper({size, layout, onLayoutChange, children, onResize}) {
if (size.width === 0) {
console.log('size is zero!');
}
const gridWidth = size.width > 0 ? size.width : 1200;
return (
<ReactGridLayout
width={gridWidth}
className="layout"
isDraggable={true}
isResizable={true}
measureBeforeMount={false}
containerPadding={[0, 0]}
useCSSTransforms={true}
margin={[CELL_VMARGIN, CELL_VMARGIN]}
cols={COLUMN_COUNT}
rowHeight={CELL_HEIGHT}
draggableHandle=".grid-drag-handle"
layout={layout}
onResize={onResize}
onLayoutChange={onLayoutChange}>
{children}
</ReactGridLayout>
);
}
const SizedReactLayoutGrid = sizeMe({monitorWidth: true})(GridWrapper);
export interface DashboardGridProps {
getPanelContainer: () => PanelContainer;
}
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
gridToPanelMap: any;
panelContainer: PanelContainer;
dashboard: DashboardModel;
panelMap: {[id: string]: PanelModel};
constructor(props) {
super(props);
this.panelContainer = this.props.getPanelContainer();
this.onLayoutChange = this.onLayoutChange.bind(this);
this.onResize = this.onResize.bind(this);
// subscribe to dashboard events
this.dashboard = this.panelContainer.getDashboard();
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
}
buildLayout() {
const layout = [];
this.panelMap = {};
for (let panel of this.dashboard.panels) {
let stringId = panel.id.toString();
this.panelMap[stringId] = panel;
if (!panel.gridPos) {
console.log('panel without gridpos');
continue;
}
layout.push({
i: stringId,
x: panel.gridPos.x,
y: panel.gridPos.y,
w: panel.gridPos.w,
h: panel.gridPos.h,
});
}
return layout;
}
onLayoutChange(newLayout) {
for (const newPos of newLayout) {
this.panelMap[newPos.i].updateGridPos(newPos);
}
}
triggerForceUpdate() {
this.forceUpdate();
}
onResize(layout, oldItem, newItem) {
this.panelMap[newItem.i].updateGridPos(newItem);
}
renderPanels() {
const panelElements = [];
for (let panel of this.dashboard.panels) {
const panelClasses = classNames({panel: true, 'panel--fullscreen': panel.fullscreen});
panelElements.push(
<div key={panel.id.toString()} className={panelClasses}>
<DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
</div>,
);
}
return panelElements;
}
render() {
return (
<SizedReactLayoutGrid layout={this.buildLayout()} onLayoutChange={this.onLayoutChange} onResize={this.onResize}>
{this.renderPanels()}
</SizedReactLayoutGrid>
);
}
}
coreModule.directive('dashboardGrid', function(reactDirective) {
return reactDirective(DashboardGrid, [['getPanelContainer', {watchDepth: 'reference', wrapApply: false}]]);
});

View File

@@ -0,0 +1,40 @@
import React from 'react';
import {PanelModel} from '../PanelModel';
import {PanelContainer} from './PanelContainer';
import {AttachedPanel} from './PanelLoader';
export interface DashboardPanelProps {
panel: PanelModel;
getPanelContainer: () => PanelContainer;
}
export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
element: any;
attachedPanel: AttachedPanel;
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {
const panelContainer = this.props.getPanelContainer();
const dashboard = panelContainer.getDashboard();
const loader = panelContainer.getPanelLoader();
this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
}
componentWillUnmount() {
if (this.attachedPanel) {
this.attachedPanel.destroy();
}
}
render() {
return (
<div ref={element => this.element = element} />
);
}
}

View File

@@ -0,0 +1,8 @@
import {DashboardModel}  from '../DashboardModel';
import {PanelLoader} from './PanelLoader';
export interface PanelContainer {
getPanelLoader(): PanelLoader;
getDashboard(): DashboardModel;
}

View File

@@ -0,0 +1,34 @@
import angular from 'angular';
import coreModule from 'app/core/core_module';
export interface AttachedPanel {
destroy();
}
export class PanelLoader {
/** @ngInject */
constructor(private $compile, private $rootScope) {
}
load(elem, panel, dashboard): AttachedPanel {
var template = '<plugin-component type="panel"></plugin-component>';
var panelScope = this.$rootScope.$new();
panelScope.panel = panel;
panelScope.dashboard = dashboard;
const compiledElem = this.$compile(template)(panelScope);
const rootNode = angular.element(elem);
rootNode.append(compiledElem);
return {
destroy: () => {
console.log('AttachedPanel:Destroy, id' + panel.id);
panelScope.$destroy();
compiledElem.remove();
}
};
}
}
coreModule.service('panelLoader', PanelLoader);

View File

@@ -1,211 +0,0 @@
// ///<reference path="../../../headers/common.d.ts" />
//
// import coreModule from 'app/core/core_module';
// import {CELL_HEIGHT, CELL_VMARGIN} from '../model';
//
// import 'jquery-ui';
// import 'gridstack/dist/jquery.jQueryUI';
// import 'gridstack';
//
// const template = `
// <div class="grid-stack">
// <dash-grid-item ng-repeat="panel in ctrl.dashboard.panels track by panel.id"
// class="grid-stack-item"
// grid-ctrl="ctrl"
// panel="panel">
// <plugin-component type="panel" class="grid-stack-item-content">
// </plugin-component>
// </dash-grid-item>
// </div>
// `;
//
// var rowIndex = 0;
//
// export class GridCtrl {
// options: any;
// dashboard: any;
// panels: any;
// gridstack: any;
// gridElem: any;
// isInitialized: boolean;
// isDestroyed: boolean;
// index: number;
// changeRenderPromise: any;
//
// #<{(|* @ngInject |)}>#
// constructor(private $scope, private $element, private $timeout) {
// console.log(this.dashboard);
// this.index = rowIndex;
// rowIndex += 1;
// }
//
// init() {
// this.gridElem = this.$element.find('.grid-stack');
//
// this.gridstack = this.gridElem.gridstack({
// animate: true,
// cellHeight: CELL_HEIGHT,
// verticalMargin: CELL_VMARGIN,
// acceptWidgets: '.grid-stack-item',
// handle: '.grid-drag-handle'
// }).data('gridstack');
//
// this.isInitialized = true;
//
// this.gridElem.on('added', (e, items) => {
// for (let item of items) {
// this.onGridStackItemAdded(item);
// }
// });
//
// this.gridElem.on('removed', (e, items) => {
// for (let item of items) {
// this.onGridStackItemRemoved(item);
// }
// });
//
// this.gridElem.on('change', (e, items) => {
// this.$timeout(() => this.onGridStackItemsChanged(items), 50);
// });
// }
//
// onGridStackItemAdded(item) {
// console.log('row: ' + this.index + ' item added', item);
// }
//
// onGridStackItemRemoved(item) {
// console.log('row: ' + this.index + ' item removed', item.id, item);
// }
//
// onGridStackItemsChanged(items) {
// console.log('onGridStackItemsChanged');
//
// for (let item of items) {
// // find panel
// var panel = this.dashboard.getPanelById(parseInt(item.id));
//
// if (!panel) {
// console.log('item change but no panel found for item', item);
// continue;
// }
//
// // update panel model position
// panel.x = item.x;
// panel.y = item.y;
// panel.width = item.width;
// panel.height = item.height;
//
// console.log('updating panel: ' + panel.id + ' x: ' + panel.x + ' y: ' + panel.y);
// }
//
// this.dashboard.panels.sort(function (a, b) {
// let aScore = a.x + (a.y * 12);
// let bScore = b.x + (b.y * 12);
// if (aScore < bScore) { return -1; }
// if (aScore > bScore) { return 1; }
// return 0;
// });
//
// if (this.changeRenderPromise) {
// this.$timeout.cancel(this.changeRenderPromise);
// }
//
// this.changeRenderPromise = this.$timeout(() => {
// console.log('broadcasting render');
// this.$scope.$broadcast('render');
// });
// }
//
// destroy() {
// this.gridstack.destroy();
// this.gridstack = null;
// this.isDestroyed = true;
// }
// }
//
// #<{(|* @ngInject *|)}>#
// export function dashGrid($timeout) {
// return {
// restrict: 'E',
// template: template,
// controller: GridCtrl,
// bindToController: true,
// controllerAs: 'ctrl',
// scope: {
// dashboard: "=",
// },
// link: function(scope, elem, attrs, ctrl) {
// $timeout(function() {
// ctrl.init();
// });
//
// scope.$on('$destroy', () => {
// ctrl.destroy();
// });
// }
// };
// }
//
// #<{(|* @ngInject *|)}>#
// export function dashGridItem($timeout, $rootScope) {
// return {
// restrict: "E",
// scope: {
// panel: '=',
// gridCtrl: '='
// },
// link: function (scope, element, attrs) {
// let gridCtrl = scope.gridCtrl;
// let panel = scope.panel;
// let gridStackNode = null;
//
// element.attr({
// 'data-gs-id': panel.id,
// 'data-gs-x': panel.x,
// 'data-gs-y': panel.y,
// 'data-gs-width': panel.width,
// 'data-gs-height': panel.height,
// 'data-gs-no-resize': panel.type === 'row',
// });
//
// $rootScope.onAppEvent('panel-fullscreen-exit', (evt, payload) => {
// if (panel.id !== payload.panelId) {
// return;
// }
// gridCtrl.gridstack.locked(element, false);
// element.removeClass('panel-fullscreen');
// }, scope);
//
// $rootScope.onAppEvent('panel-fullscreen-enter', (evt, payload) => {
// if (panel.id !== payload.panelId) {
// return;
// }
// element.addClass('panel-fullscreen');
// }, scope);
//
// scope.$on('$destroy', () => {
// console.log('grid-item scope $destroy');
// if (gridCtrl.isDestroyed) {
// return;
// }
//
// if (gridStackNode) {
// console.log('grid-item scope $destroy removeWidget');
// gridStackNode._grid.removeWidget(element);
// }
// });
//
// if (gridCtrl.isInitialized) {
// gridCtrl.gridstack.makeWidget(element);
// gridStackNode = element.data('_gridstack_node');
// } else {
// setTimeout(function() {
// gridStackNode = element.data('_gridstack_node');
// }, 500);
// }
// }
// };
// }
//
// coreModule.directive('dashGrid', dashGrid);
// coreModule.directive('dashGridItem', dashGridItem);

View File

@@ -1,232 +0,0 @@
Skip to content
This repository
Search
Pull requests
Issues
Marketplace
Gist
@torkelo
Sign out
Unwatch 946
Unstar 17,021
Fork 2,862 grafana/grafana
Code Issues 1,079 Pull requests 46 Projects 1 Wiki Settings Insights
Branch: gridstack Find file Copy pathgrafana/public/app/core/components/dashgrid/dashgrid.ts
a6bbcb8 on Jun 13
@torkelo torkelo ux: gridstack poc
1 contributor
RawBlameHistory
213 lines (181 sloc) 5.45 KB
///<reference path="../../../headers/common.d.ts" />
import $ from 'jquery';
import coreModule from '../../core_module';
import 'jquery-ui';
import 'gridstack';
import 'gridstack.jquery-ui';
const template = `
<div gridstack gridstack-handler="ctrl.gridstack" class="grid-stack"
options="ctrl.options"
on-change="ctrl.onChange(event,items)"
on-drag-start="ctrl.onDragStart(event,ui)"
on-drag-stop="ctrl.onDragStop(event, ui)"
on-resize-start="ctrl.onResizeStart(event, ui)"
on-resize-stop="ctrl.onResizeStop(event, ui)">
<div gridstack-item ng-repeat="panel in ctrl.panels"
class="grid-stack-item"
gs-item-id="panel.id"
gs-item-x="panel.x"
gs-item-y="panel.y"
gs-item-width="panel.width"
gs-item-height="panel.height"
gs-item-autopos="1"
on-item-added="ctrl.onItemAdded(item)"
on-item-removed="ctrl.onItemRemoved(item)">
<plugin-component type="panel" class="panel-margin grid-stack-item-content">
</plugin-component>
</div>
</div>
`;
export class DashGridCtrl {
options: any;
/** @ngInject */
constructor(private $rootScope) {
this.options = {
animate: true,
};
}
onResizeStop() {
this.$rootScope.$broadcast('render');
}
}
export function dashGrid($timeout) {
return {
restrict: 'E',
template: template,
controller: DashGridCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
dashboard: "="
},
link: function(scope, elem, attrs, ctrl) {
ctrl.panels = [];
ctrl.dashboard.forEachPanel((panel, panelIndex, row, rowIndex) => {
panel.width = 4;
panel.height = 4;
panel.x = panelIndex * 4;
panel.y = rowIndex * 4;
ctrl.panels.push(panel);
});
}
};
}
/** @ngInject */
coreModule.controller('GridstackController', ['$scope', function($scope) {
var gridstack = null;
this.init = function(element, options) {
gridstack = element.gridstack(options).data('gridstack');
return gridstack;
};
this.removeItem = function(element) {
if (gridstack) {
return gridstack.removeWidget(element, false);
}
return null;
};
this.addItem = function(element) {
if (gridstack) {
gridstack.makeWidget(element);
return element;
}
return null;
};
}]);
/** @ngInject */
coreModule.directive('gridstack', ['$timeout', function($timeout) {
return {
restrict: "A",
controller: 'GridstackController',
scope: {
onChange: '&',
onDragStart: '&',
onDragStop: '&',
onResizeStart: '&',
onResizeStop: '&',
gridstackHandler: '=',
options: '='
},
link: function (scope, element, attrs, controller, ngModel) {
var gridstack = controller.init(element, scope.options);
scope.gridstackHandler = gridstack;
element.on('change', function (e, items) {
$timeout(function() {
scope.$apply();
scope.onChange({event: e, items: items});
});
});
element.on('dragstart', function(e, ui) {
scope.onDragStart({event: e, ui: ui});
});
element.on('dragstop', function(e, ui) {
$timeout(function() {
scope.$apply();
scope.onDragStop({event: e, ui: ui});
});
});
element.on('resizestart', function(e, ui) {
scope.onResizeStart({event: e, ui: ui});
});
element.on('resizestop', function(e, ui) {
$timeout(function() {
scope.$apply();
scope.onResizeStop({event: e, ui: ui});
});
});
}
};
}]);
/** @ngInject */
coreModule.directive('gridstackItem', ['$timeout', function($timeout) {
return {
restrict: "A",
controller: 'GridstackController',
require: '^gridstack',
scope: {
gridstackItem: '=',
onItemAdded: '&',
onItemRemoved: '&',
gsItemId: '=',
gsItemX: '=',
gsItemY: '=',
gsItemWidth: '=',
gsItemHeight: '=',
gsItemAutopos: '='
},
link: function (scope, element, attrs, controller) {
$(element).attr('data-gs-id', scope.gsItemId);
$(element).attr('data-gs-x', scope.gsItemX);
$(element).attr('data-gs-y', scope.gsItemY);
$(element).attr('data-gs-width', scope.gsItemWidth);
$(element).attr('data-gs-height', scope.gsItemHeight);
$(element).attr('data-gs-auto-position', scope.gsItemAutopos);
var widget = controller.addItem(element);
var item = element.data('_gridstack_node');
$timeout(function() {
scope.onItemAdded({item: item});
});
scope.$watch(function () { return $(element).attr('data-gs-id'); }, function (val) {
scope.gsItemId = val;
});
scope.$watch(function(){ return $(element).attr('data-gs-x'); }, function(val) {
scope.gsItemX = val;
});
scope.$watch(function(){ return $(element).attr('data-gs-y'); }, function(val) {
scope.gsItemY = val;
});
scope.$watch(function(){ return $(element).attr('data-gs-width'); }, function(val) {
scope.gsItemWidth = val;
});
scope.$watch(function(){ return $(element).attr('data-gs-height'); }, function(val) {
scope.gsItemHeight = val;
});
element.bind('$destroy', function() {
var item = element.data('_gridstack_node');
scope.onItemRemoved({item: item});
controller.removeItem(element);
});
}
};
}]);
coreModule.directive('dashGrid', dashGrid);
Contact GitHub API Training Shop Blog About
© 2017 GitHub, Inc. Terms Privacy Security Status Help

View File

@@ -70,7 +70,7 @@
</ul>
</li>
<li class="navbar-mini-btn-wrapper" ng-show="::ctrl.dashboard.meta.canSave">
<button class="btn btn-secondary btn-mini" ng-click="ctrl.openEditView('add-panel')">
<button class="btn btn-secondary btn-mini" ng-click="ctrl.addPanel()">
<i class="fa fa-plus-circle"></i> Add Panel
</button>
</li>

View File

@@ -4,7 +4,7 @@ import _ from 'lodash';
import moment from 'moment';
import angular from 'angular';
import {appEvents, NavModel} from 'app/core/core';
import {DashboardModel} from '../model';
import {DashboardModel} from '../DashboardModel';
export class DashNavCtrl {
dashboard: DashboardModel;
@@ -92,16 +92,16 @@ export class DashNavCtrl {
}
deleteDashboard() {
var confirmText = "";
var confirmText = '';
var text2 = this.dashboard.title;
var alerts = this.dashboard.rows.reduce((memo, row) => {
memo += row.panels.filter(panel => panel.alert).length;
return memo;
}, 0);
const alerts = _.sumBy(this.dashboard.panels, panel => {
return panel.alert ? 1 : 0;
});
if (alerts > 0) {
confirmText = 'DELETE';
text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboad will also delete those alerts`;
text2 = `This dashboard contains ${alerts} alerts. Deleting this dashboard will also delete those alerts`;
}
appEvents.emit('confirm-modal', {
@@ -145,6 +145,14 @@ export class DashNavCtrl {
this.$rootScope.appEvent('show-dash-search');
}
addPanel() {
this.dashboard.addPanel({
type: 'graph',
gridPos: {x: 0, y: 0, w: 6, h: 5},
title: 'New Graph',
});
}
navItemClicked(navItem, evt) {
if (navItem.clickHandler) {
navItem.clickHandler();

View File

@@ -1,95 +0,0 @@
define([
'angular',
'lodash',
'app/core/utils/kbn'
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.controllers');
module.controller('GraphiteImportCtrl', function($scope, datasourceSrv, dashboardSrv, $location) {
$scope.options = {};
$scope.init = function() {
$scope.datasources = [];
_.each(datasourceSrv.getAll(), function(ds) {
if (ds.type === 'graphite') {
$scope.options.sourceName = ds.name;
$scope.datasources.push(ds.name);
}
});
};
$scope.listAll = function() {
datasourceSrv.get($scope.options.sourceName).then(function(datasource) {
$scope.datasource = datasource;
$scope.datasource.listDashboards('').then(function(results) {
$scope.dashboards = results;
}, function(err) {
var message = err.message || err.statusText || 'Error';
$scope.appEvent('alert-error', ['Failed to load dashboard list from graphite', message]);
});
});
};
$scope.import = function(dashName) {
$scope.datasource.loadDashboard(dashName).then(function(results) {
if (!results.data || !results.data.state) {
throw { message: 'no dashboard state received from graphite' };
}
graphiteToGrafanaTranslator(results.data.state, $scope.datasource.name);
}, function(err) {
var message = err.message || err.statusText || 'Error';
$scope.appEvent('alert-error', ['Failed to load dashboard from graphite', message]);
});
};
function graphiteToGrafanaTranslator(state, datasource) {
var graphsPerRow = 2;
var rowHeight = 300;
var rowTemplate;
var currentRow;
var panel;
rowTemplate = {
title: '',
panels: [],
height: rowHeight
};
currentRow = angular.copy(rowTemplate);
var newDashboard = dashboardSrv.create({});
newDashboard.rows = [];
newDashboard.title = state.name;
newDashboard.rows.push(currentRow);
_.each(state.graphs, function(graph, index) {
if (currentRow.panels.length === graphsPerRow) {
currentRow = angular.copy(rowTemplate);
newDashboard.rows.push(currentRow);
}
panel = {
type: 'graph',
span: 12 / graphsPerRow,
title: graph[1].title,
targets: [],
datasource: datasource,
id: index + 1
};
_.each(graph[1].target, function(target) {
panel.targets.push({ target: target });
});
currentRow.panels.push(panel);
});
window.grafanaImportDashboard = newDashboard;
$location.path('/dashboard-import/' + kbn.slugifyForUrl(newDashboard.title));
}
});
});

View File

@@ -6,7 +6,7 @@ import _ from 'lodash';
import angular from 'angular';
import moment from 'moment';
import {DashboardModel} from '../model';
import {DashboardModel} from '../DashboardModel';
import {HistoryListOpts, RevisionsModel, CalculateDiffOptions, HistorySrv} from './history_srv';
export class HistoryListCtrl {

View File

@@ -1,8 +1,6 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import coreModule from 'app/core/core_module';
import {DashboardModel} from '../model';
import {DashboardModel} from '../DashboardModel';
export interface HistoryListOpts {
limit: number;

View File

@@ -5,7 +5,7 @@
<ul class="gf-tabs">
<li class="gf-tabs-item" ng-repeat="tab in ::['General', 'Links', 'Time picker']">
<a class="gf-tabs-link" ng-click="editor.index = $index" ng-class="{active: editor.index === $index}">
<a class="gf-tabs-link" ng-click="ctrl.editTab = $index" ng-class="{active: ctrl.editTab === $index}">
{{::tab}}
</a>
</li>
@@ -17,30 +17,30 @@
</div>
<div class="tabbed-view-body">
<div ng-if="editor.index == 0">
<div ng-if="ctrl.editTab == 0">
<div class="gf-form-group section">
<h5 class="section-heading">Details</h5>
<div class="gf-form">
<label class="gf-form-label width-7">Name</label>
<input type="text" class="gf-form-input width-30" ng-model='dashboard.title'></input>
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.title'></input>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">Description</label>
<input type="text" class="gf-form-input width-30" ng-model='dashboard.description'></input>
<input type="text" class="gf-form-input width-30" ng-model='ctrl.dashboard.description'></input>
</div>
<div class="gf-form">
<label class="gf-form-label width-7">
Tags
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
</label>
<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
<bootstrap-tagsinput ng-model="ctrl.dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</div>
<folder-picker ng-if="!dashboardMeta.isFolder"
initial-title="dashboardMeta.folderTitle"
on-change="onFolderChange($folder)"
<folder-picker ng-if="!ctrl.dashboard.meta.isFolder"
initial-title="ctrl.dashboard.meta.folderTitle"
on-change="ctrl.onFolderChange($folder)"
label-class="width-7">
</folder-picker>
</div>
@@ -51,19 +51,19 @@
<div class="gf-form">
<label class="gf-form-label width-11">Timezone</label>
<div class="gf-form-select-wrapper">
<select ng-model="dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
</div>
</div>
<gf-form-switch class="gf-form"
label="Editable"
tooltip="Uncheck, then save and reload to disable all dashboard editing"
checked="dashboard.editable"
checked="ctrl.dashboard.editable"
label-class="width-11">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="Hide Controls"
tooltip="Hide row controls. Shortcut: CTRL+H or CMD+H"
checked="dashboard.hideControls"
checked="ctrl.dashboard.hideControls"
label-class="width-11">
</gf-form-switch>
</div>
@@ -79,7 +79,7 @@
</info-popover>
</label>
<div class="gf-form-select-wrapper">
<select ng-model="dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
<select ng-model="ctrl.dashboard.graphTooltip" class='gf-form-input' ng-options="f.value as f.text for f in [{value: 0, text: 'Default'}, {value: 1, text: 'Shared crosshair'},{value: 2, text: 'Shared Tooltip'}]"></select>
</div>
</div>
</div>
@@ -90,7 +90,7 @@
</div>
<div ng-if="editor.index == 2">
<gf-time-picker-settings dashboard="dashboard"></gf-time-picker-settings>
<gf-time-picker-settings dashboard="ctrl.dashboard"></gf-time-picker-settings>
</div>
</div>

View File

@@ -55,11 +55,9 @@ export class SaveDashboardAsModalCtrl {
// remove alerts if source dashboard is already persisted
// do not want to create alert dupes
if (dashboard.id > 0) {
this.clone.rows.forEach(row => {
row.panels.forEach(panel => {
delete panel.thresholds;
delete panel.alert;
});
this.clone.panels.forEach(panel => {
delete panel.thresholds;
delete panel.alert;
});
}

View File

@@ -1,7 +1,7 @@
import {describe, beforeEach, it, expect} from 'test/lib/common';
import _ from 'lodash';
import {DashboardModel} from '../model';
import {DashboardModel} from '../DashboardModel';
describe('DashboardModel', function() {

View File

@@ -3,7 +3,7 @@ import {describe, beforeEach, it, sinon, expect} from 'test/lib/common';
import _ from 'lodash';
import config from 'app/core/config';
import {DashboardExporter} from '../export/exporter';
import {DashboardModel} from '../model';
import {DashboardModel} from '../DashboardModel';
describe('given dashboard with repeated panels', function() {
var dash, exported;

View File

@@ -9,7 +9,7 @@ function (angular, _, $, config) {
var module = angular.module('grafana.services');
module.factory('dashboardViewStateSrv', function($location, $timeout) {
module.factory('dashboardViewStateSrv', function($location, $timeout, $rootScope) {
// represents the transient view state
// like fullscreen panel & edit
@@ -154,7 +154,8 @@ function (angular, _, $, config) {
ctrl.editMode = false;
ctrl.fullscreen = false;
ctrl.dashboard.editMode = this.oldDashboardEditMode;
this.dashboard.setViewMode(ctrl.panel, false, false);
this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id});
@@ -162,9 +163,9 @@ function (angular, _, $, config) {
$timeout(function() {
if (self.oldTimeRange !== ctrl.range) {
self.$scope.broadcastRefresh();
$rootScope.$broadcast('refresh');
} else {
self.$scope.$broadcast('render');
$rootScope.$broadcast('render');
}
delete self.fullscreenPanel;
});
@@ -176,18 +177,11 @@ function (angular, _, $, config) {
ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
ctrl.fullscreen = true;
this.oldDashboardEditMode = this.dashboard.editMode;
this.oldTimeRange = ctrl.range;
this.fullscreenPanel = panelScope;
this.dashboard.editMode = false;
$(window).scrollTop(0);
this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
this.$scope.appEvent('panel-fullscreen-enter', {panelId: ctrl.panel.id});
$timeout(function() {
ctrl.render();
});
};
DashboardViewState.prototype.registerPanel = function(panelScope) {

View File

@@ -1,6 +1,4 @@
///<reference path="../../headers/common.d.ts" />
import {DashboardModel} from '../dashboard/model';
import {DashboardModel} from '../dashboard/DashboardModel';
import Remarkable from 'remarkable';
export class MetricsTabCtrl {

View File

@@ -3,7 +3,7 @@ import _ from 'lodash';
import $ from 'jquery';
import {profiler} from 'app/core/profiler';
import Remarkable from 'remarkable';
import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/model';
import {CELL_HEIGHT, CELL_VMARGIN} from '../dashboard/DashboardModel';
const TITLE_HEIGHT = 25;
const EMPTY_TITLE_HEIGHT = 9;
@@ -37,7 +37,7 @@ export class PanelCtrl {
this.$scope = $scope;
this.$timeout = $injector.get('$timeout');
this.editorTabIndex = 0;
this.events = new Emitter();
this.events = this.panel.events;
this.timing = {};
var plugin = config.panels[this.panel.type];
@@ -47,21 +47,14 @@ export class PanelCtrl {
}
$scope.$on("refresh", () => this.refresh());
$scope.$on("render", () => this.render());
$scope.$on("$destroy", () => {
this.events.emit('panel-teardown');
this.events.removeAllListeners();
});
// we should do something interesting
// with newly added panels
if (this.panel.isNew) {
delete this.panel.isNew;
}
}
init() {
this.calculatePanelHeight();
this.events.on('panel-size-changed', this.onSizeChanged.bind(this));
this.publishAppEvent('panel-initialized', {scope: this.$scope});
this.events.emit('panel-initialized');
}
@@ -71,7 +64,7 @@ export class PanelCtrl {
}
refresh() {
this.events.emit('refresh', null);
this.events.emit('refresh', null);
}
publishAppEvent(evtName, evt) {
@@ -170,23 +163,24 @@ export class PanelCtrl {
var fullscreenHeight = Math.floor(docHeight * 0.8);
this.containerHeight = this.editMode ? editHeight : fullscreenHeight;
} else {
this.containerHeight = this.panel.height * CELL_HEIGHT + ((this.panel.height-1) * CELL_VMARGIN);
this.containerHeight = this.panel.gridPos.h * CELL_HEIGHT + ((this.panel.gridPos.h-1) * CELL_VMARGIN);
}
this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
}
render(payload?) {
// ignore if other panel is in fullscreen mode
if (this.otherPanelInFullscreenMode()) {
return;
}
this.calculatePanelHeight();
this.timing.renderStart = new Date().getTime();
this.events.emit('render', payload);
}
private onSizeChanged() {
this.calculatePanelHeight();
this.$timeout(() => {
this.render();
}, 100);
}
duplicate() {
this.dashboard.duplicatePanel(this.panel);
this.$timeout(() => {

View File

@@ -98,6 +98,7 @@ module.directive('grafanaPanel', function($rootScope, $document) {
}
ctrl.events.on('render', () => {
console.log('panelDirective::render!');
if (lastHeight !== ctrl.containerHeight) {
panelContainer.css({minHeight: ctrl.containerHeight});
lastHeight = ctrl.containerHeight;

View File

@@ -88,7 +88,7 @@ function panelHeader($compile) {
let menuScope;
elem.click(function(evt) {
const targetClass = evt.target.className;
//const targetClass = evt.target.className;
// remove existing scope
if (menuScope) {
@@ -100,10 +100,10 @@ function panelHeader($compile) {
menuElem.html(menuHtml);
$compile(menuElem)(menuScope);
if (targetClass === 'panel-title-text' || targetClass === 'panel-title') {
evt.stopPropagation();
elem.find('[data-toggle=dropdown]').dropdown('toggle');
}
// if (targetClass === 'panel-title-text' || targetClass === 'panel-title') {
// evt.stopPropagation();
// elem.find('[data-toggle=dropdown]').dropdown('toggle');
// }
});
}
};

View File

@@ -68,7 +68,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
var componentInfo: any = {
name: 'panel-plugin-' + scope.panel.type,
bindings: {dashboard: "=", panel: "=", row: "="},
attrs: {dashboard: "ctrl.dashboard", panel: "panel", row: "ctrl.row"},
attrs: {dashboard: "dashboard", panel: "panel"},
};
let panelInfo = config.panels[scope.panel.type];

View File

@@ -1,14 +1,16 @@
<div dash-class ng-if="dashboard">
<dashnav dashboard="dashboard"></dashnav>
<div dash-class ng-if="ctrl.dashboard">
<dashnav dashboard="ctrl.dashboard"></dashnav>
<div class="scroll-canvas scroll-canvas--dashboard">
<div gemini-scrollbar>
<div dash-editor-view class="dash-edit-view"></div>
<div class="dashboard-container">
<dashboard-submenu ng-if="dashboard.meta.submenuEnabled" dashboard="dashboard"></dashboard-submenu>
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
</dashboard-submenu>
<dash-grid dashboard="dashboard"></dash-grid>
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
</dashboard-grid>
</div>
</div>

View File

@@ -26,9 +26,9 @@ describe('grafanaGraph', function() {
beforeEach(angularMocks.inject(function($rootScope, $compile) {
var ctrl: any = {
events: new Emitter(),
height: 200,
panel: {
events: new Emitter(),
legend: {},
grid: { },
yaxes: [
@@ -65,7 +65,7 @@ describe('grafanaGraph', function() {
var scope = $rootScope.$new();
scope.ctrl = ctrl;
scope.ctrl.events = ctrl.panel.events;
$rootScope.onAppEvent = sinon.spy();

View File

@@ -13,7 +13,6 @@
{
"content": "<div class=\"text-center dashboard-header\">\n <span>Home Dashboard</span>\n</div>",
"editable": true,
"height": 2,
"id": 1,
"links": [],
"mode": "html",
@@ -21,14 +20,16 @@
"title": "",
"transparent": true,
"type": "text",
"width": 12,
"x": 0,
"y": 0
"gridPos": {
"w": 12,
"h": 2,
"x": 0,
"y": 0
}
},
{
"folderId": 0,
"headings": true,
"height": 17,
"id": 3,
"limit": 4,
"links": [],
@@ -40,26 +41,31 @@
"title": "",
"transparent": false,
"type": "dashlist",
"width": 7,
"x": 0,
"y": 6
"gridPos": {
"w": 7,
"h": 17,
"x": 0,
"y": 6
}
},
{
"editable": true,
"error": false,
"height": 17,
"id": 4,
"links": [],
"title": "",
"transparent": false,
"type": "pluginlist",
"width": 5,
"x": 7,
"y": 6
"gridPos": {
"w": 5,
"h": 17,
"x": 7,
"y": 6
}
}
],
"rows": [],
"schemaVersion": 15,
"schemaVersion": 16,
"style": "dark",
"tags": [],
"templating": {

View File

@@ -77,11 +77,12 @@
@import "components/tabbed_view";
@import "components/query_part";
@import "components/jsontree";
@import "components/edit_sidemenu.scss";
@import "components/edit_sidemenu";
@import "components/row.scss";
@import "components/gridstack.scss";
@import "components/json_explorer.scss";
@import "components/code_editor.scss";
@import "components/json_explorer";
@import "components/code_editor";
@import "components/dashboard_grid";
// PAGES
@import "pages/login";

View File

@@ -105,9 +105,9 @@ $tight-form-bg: $dark-3;
$tight-form-func-bg: #333;
$tight-form-func-highlight-bg: #444;
$modal-background: $black;
$code-tag-bg: $gray-1;
$code-tag-border: lighten($code-tag-bg, 2%);
$modal-backdrop-bg: $dark-3;
$code-tag-bg: $gray-1;
$code-tag-border: lighten($code-tag-bg, 2%);
// Lists

View File

@@ -112,9 +112,9 @@ $tight-form-bg: $gray-6;
$tight-form-func-bg: $gray-5;
$tight-form-func-highlight-bg: $gray-6;
$modal-background: $body-bg;
$code-tag-bg: $gray-6;
$code-tag-border: darken($code-tag-bg, 3%);
$modal-backdrop-bg: $body-bg;
$code-tag-bg: $gray-6;
$code-tag-border: darken($code-tag-bg, 3%);
// Lists
$grafanaListBackground: $gray-6;

View File

@@ -222,8 +222,8 @@ $btn-border-radius: 3px;
$side-menu-width: 60px;
// dashboard
$panel-margin: 0.4rem;
$dashboard-padding: ($panel-margin * 2) $panel-margin $panel-margin $panel-margin;
$panel-margin: 10px;
$dashboard-padding: $panel-margin * 2;
// tabs
$tabs-padding-top: 0.6rem;

View File

@@ -0,0 +1,24 @@
@import "~react-grid-layout/css/styles.css";
@import "~react-resizable/css/styles.css";
.panel-in-fullscreen {
.react-grid-layout {
height: 100% !important;
}
.react-grid-item {
display: none;
transition-property: none !important;
}
.panel--fullscreen {
display: block !important;
position: unset !important;
width: 100% !important;
height: 100% !important;
transform: translate(0px, 0px) !important;
}
}

View File

@@ -1,325 +0,0 @@
.grid-stack-item > .ui-resizable-handle {
filter: none;
}
.grid-stack {
position: relative;
min-height: 150px;
}
.grid-stack.grid-stack-rtl {
direction: ltr;
}
.grid-stack.grid-stack-rtl > .grid-stack-item {
direction: rtl;
}
.grid-stack .grid-stack-placeholder > .placeholder-content {
background: $input-label-bg;
box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 5px rgba(82,168,236,10.8);
margin: 0;
position: absolute;
top: 0;
left: 5px;
right: 5px;
bottom: 0;
width: auto;
text-align: center;
}
.grid-stack > .grid-stack-item {
min-width: 8.3333333333%;
position: absolute;
padding: 0;
}
.grid-stack > .grid-stack-item > .grid-stack-item-content {
margin: 0;
position: absolute;
top: 0;
left: 7px;
right: 7px;
bottom: 0;
width: auto;
}
.grid-stack > .grid-stack-item > .ui-resizable-handle {
position: absolute;
display: block;
-ms-touch-action: none;
touch-action: none;
font-size: 10px;
color: $text-color-weak;
}
.grid-stack > .grid-stack-item.ui-resizable-disabled > .ui-resizable-handle,
.grid-stack > .grid-stack-item.ui-resizable-autohide > .ui-resizable-handle {
display: none;
}
.grid-stack > .grid-stack-item.ui-draggable-dragging, .grid-stack > .grid-stack-item.ui-resizable-resizing {
z-index: 100;
}
.grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content,
.grid-stack > .grid-stack-item.ui-draggable-dragging > .grid-stack-item-content, .grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content,
.grid-stack > .grid-stack-item.ui-resizable-resizing > .grid-stack-item-content {
box-shadow: 1px 4px 6px rgba(0, 0, 0, 0.2);
opacity: 0.8;
}
.grid-stack > .grid-stack-item > .ui-resizable-se,
.grid-stack > .grid-stack-item > .ui-resizable-sw {
font-family: 'grafana-icons' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
&::before {
content: "\e90b";
}
}
.grid-stack > .grid-stack-item > .ui-resizable-se {
cursor: se-resize;
width: 16px;
height: 16px;
right: 6px;
bottom: -2px;
}
.grid-stack > .grid-stack-item.ui-draggable-dragging > .ui-resizable-handle {
display: none !important;
}
.grid-stack > .grid-stack-item[data-gs-width='1'] {
width: 8.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-x='1'] {
left: 8.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='1'] {
min-width: 8.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='1'] {
max-width: 8.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-width='2'] {
width: 16.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-x='2'] {
left: 16.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='2'] {
min-width: 16.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='2'] {
max-width: 16.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-width='3'] {
width: 25%;
}
.grid-stack > .grid-stack-item[data-gs-x='3'] {
left: 25%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='3'] {
min-width: 25%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='3'] {
max-width: 25%;
}
.grid-stack > .grid-stack-item[data-gs-width='4'] {
width: 33.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-x='4'] {
left: 33.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='4'] {
min-width: 33.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='4'] {
max-width: 33.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-width='5'] {
width: 41.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-x='5'] {
left: 41.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='5'] {
min-width: 41.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='5'] {
max-width: 41.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-width='6'] {
width: 50%;
}
.grid-stack > .grid-stack-item[data-gs-x='6'] {
left: 50%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='6'] {
min-width: 50%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='6'] {
max-width: 50%;
}
.grid-stack > .grid-stack-item[data-gs-width='7'] {
width: 58.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-x='7'] {
left: 58.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='7'] {
min-width: 58.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='7'] {
max-width: 58.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-width='8'] {
width: 66.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-x='8'] {
left: 66.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='8'] {
min-width: 66.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='8'] {
max-width: 66.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-width='9'] {
width: 75%;
}
.grid-stack > .grid-stack-item[data-gs-x='9'] {
left: 75%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='9'] {
min-width: 75%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='9'] {
max-width: 75%;
}
.grid-stack > .grid-stack-item[data-gs-width='10'] {
width: 83.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-x='10'] {
left: 83.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='10'] {
min-width: 83.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='10'] {
max-width: 83.3333333333%;
}
.grid-stack > .grid-stack-item[data-gs-width='11'] {
width: 91.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-x='11'] {
left: 91.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='11'] {
min-width: 91.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='11'] {
max-width: 91.6666666667%;
}
.grid-stack > .grid-stack-item[data-gs-width='12'] {
width: 100%;
}
.grid-stack > .grid-stack-item[data-gs-x='12'] {
left: 100%;
}
.grid-stack > .grid-stack-item[data-gs-min-width='12'] {
min-width: 100%;
}
.grid-stack > .grid-stack-item[data-gs-max-width='12'] {
max-width: 100%;
}
.grid-stack.grid-stack-animate,
.grid-stack.grid-stack-animate .grid-stack-item {
-webkit-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-moz-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-ms-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
-o-transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
transition: left 0.3s, top 0.3s, height 0.3s, width 0.3s;
}
.grid-stack.grid-stack-animate .grid-stack-item.ui-draggable-dragging,
.grid-stack.grid-stack-animate .grid-stack-item.ui-resizable-resizing,
.grid-stack.grid-stack-animate .grid-stack-item.grid-stack-placeholder {
-webkit-transition: left 0s, top 0s, height 0s, width 0s;
-moz-transition: left 0s, top 0s, height 0s, width 0s;
-ms-transition: left 0s, top 0s, height 0s, width 0s;
-o-transition: left 0s, top 0s, height 0s, width 0s;
transition: left 0s, top 0s, height 0s, width 0s;
}
.grid-stack.grid-stack-one-column-mode {
height: auto !important;
}
.grid-stack.grid-stack-one-column-mode > .grid-stack-item {
position: relative !important;
width: auto !important;
left: 0 !important;
top: auto !important;
margin-bottom: 20px;
max-width: none !important;
}
.grid-stack.grid-stack-one-column-mode > .grid-stack-item > .ui-resizable-handle {
display: none;
}

View File

@@ -10,7 +10,7 @@
bottom: 0;
left: 0;
z-index: $zindex-modal-backdrop;
background-color: $black;
background-color: $modal-backdrop-bg;
}
.modal-backdrop,

View File

@@ -17,7 +17,7 @@
.sidemenu-open {
.navbar {
padding-left: 0;
padding-left: $panel-margin;
}
}

View File

@@ -5,7 +5,7 @@
align-content: flex-start;
align-items: flex-start;
margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
margin: 0 0 $panel-margin 0;
}
.annotation-disabled, .annotation-disabled a {

View File

@@ -25,6 +25,7 @@ div.flot-text {
background-color: $panel-bg;
border: $panel-border;
position: relative;
height: 100%;
&.panel-transparent {
background-color: transparent;
@@ -34,6 +35,7 @@ div.flot-text {
.panel-content {
padding: 0px 10px 5px 10px;
height: 100%;
}
.panel-title-container {

View File

@@ -1,4 +0,0 @@
declare let helpers: any;
export default helpers;

View File

@@ -1,180 +0,0 @@
define([
'lodash',
'app/core/config',
'app/core/utils/datemath',
], function(_, config, dateMath) {
'use strict';
function ControllerTestContext() {
var self = this;
this.datasource = {};
this.$element = {};
this.annotationsSrv = {};
this.timeSrv = new TimeSrvStub();
this.templateSrv = new TemplateSrvStub();
this.datasourceSrv = {
getMetricSources: function() {},
get: function() {
return {
then: function(callback) {
callback(self.datasource);
}
};
}
};
this.providePhase = function(mocks) {
return window.module(function($provide) {
$provide.value('datasourceSrv', self.datasourceSrv);
$provide.value('annotationsSrv', self.annotationsSrv);
$provide.value('timeSrv', self.timeSrv);
$provide.value('templateSrv', self.templateSrv);
$provide.value('$element', self.$element);
_.each(mocks, function(value, key) {
$provide.value(key, value);
});
});
};
this.createPanelController = function(Ctrl) {
return window.inject(function($controller, $rootScope, $q, $location, $browser) {
self.scope = $rootScope.$new();
self.$location = $location;
self.$browser = $browser;
self.$q = $q;
self.panel = {type: 'test'};
self.dashboard = {meta: {}};
$rootScope.appEvent = sinon.spy();
$rootScope.onAppEvent = sinon.spy();
$rootScope.colors = [];
for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); }
config.panels['test'] = {info: {}};
self.ctrl = $controller(Ctrl, {$scope: self.scope}, {
panel: self.panel, dashboard: self.dashboard, row: {}
});
});
};
this.createControllerPhase = function(controllerName) {
return window.inject(function($controller, $rootScope, $q, $location, $browser) {
self.scope = $rootScope.$new();
self.$location = $location;
self.$browser = $browser;
self.scope.contextSrv = {};
self.scope.panel = {};
self.scope.row = { panels:[] };
self.scope.dashboard = {meta: {}};
self.scope.dashboardMeta = {};
self.scope.dashboardViewState = new DashboardViewStateStub();
self.scope.appEvent = sinon.spy();
self.scope.onAppEvent = sinon.spy();
$rootScope.colors = [];
for (var i = 0; i < 50; i++) { $rootScope.colors.push('#' + i); }
self.$q = $q;
self.scope.skipDataOnInit = true;
self.scope.skipAutoInit = true;
self.controller = $controller(controllerName, {
$scope: self.scope
});
});
};
}
function ServiceTestContext() {
var self = this;
self.templateSrv = new TemplateSrvStub();
self.timeSrv = new TimeSrvStub();
self.datasourceSrv = {};
self.backendSrv = {};
self.$routeParams = {};
this.providePhase = function(mocks) {
return window.module(function($provide) {
_.each(mocks, function(key) {
$provide.value(key, self[key]);
});
});
};
this.createService = function(name) {
return window.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) {
self.$q = $q;
self.$rootScope = $rootScope;
self.$httpBackend = $httpBackend;
self.$location = $location;
self.$rootScope.onAppEvent = function() {};
self.$rootScope.appEvent = function() {};
self.$timeout = $timeout;
self.service = $injector.get(name);
});
};
}
function DashboardViewStateStub() {
this.registerPanel = function() {
};
}
function TimeSrvStub() {
this.init = sinon.spy();
this.time = { from:'now-1h', to: 'now'};
this.timeRange = function(parse) {
if (parse === false) {
return this.time;
}
return {
from : dateMath.parse(this.time.from, false),
to : dateMath.parse(this.time.to, true)
};
};
this.replace = function(target) {
return target;
};
this.setTime = function(time) {
this.time = time;
};
}
function ContextSrvStub() {
this.hasRole = function() {
return true;
};
}
function TemplateSrvStub() {
this.variables = [];
this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g };
this.data = {};
this.replace = function(text) {
return _.template(text, this.templateSettings)(this.data);
};
this.init = function() {};
this.getAdhocFilters = function() { return []; };
this.fillVariableValuesForUrl = function() {};
this.updateTemplateData = function() { };
this.variableExists = function() { return false; };
this.variableInitialized = function() { };
this.highlightVariablesAsHtml = function(str) { return str; };
this.setGrafanaVariable = function(name, value) {
this.data[name] = value;
};
}
return {
ControllerTestContext: ControllerTestContext,
TimeSrvStub: TimeSrvStub,
ContextSrvStub: ContextSrvStub,
ServiceTestContext: ServiceTestContext
};
});

View File

@@ -0,0 +1,195 @@
import _ from 'lodash';
import config from 'app/core/config';
import * as dateMath from 'app/core/utils/datemath';
import {angularMocks, sinon} from '../lib/common';
import {PanelModel} from 'app/features/dashboard/PanelModel';
export function ControllerTestContext() {
var self = this;
this.datasource = {};
this.$element = {};
this.annotationsSrv = {};
this.timeSrv = new TimeSrvStub();
this.templateSrv = new TemplateSrvStub();
this.datasourceSrv = {
getMetricSources: function() {},
get: function() {
return {
then: function(callback) {
callback(self.datasource);
},
};
},
};
this.providePhase = function(mocks) {
return angularMocks.module(function($provide) {
$provide.value('datasourceSrv', self.datasourceSrv);
$provide.value('annotationsSrv', self.annotationsSrv);
$provide.value('timeSrv', self.timeSrv);
$provide.value('templateSrv', self.templateSrv);
$provide.value('$element', self.$element);
_.each(mocks, function(value, key) {
$provide.value(key, value);
});
});
};
this.createPanelController = function(Ctrl) {
return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) {
self.scope = $rootScope.$new();
self.$location = $location;
self.$browser = $browser;
self.$q = $q;
self.panel = new PanelModel({type: 'test'});
self.dashboard = {meta: {}};
$rootScope.appEvent = sinon.spy();
$rootScope.onAppEvent = sinon.spy();
$rootScope.colors = [];
for (var i = 0; i < 50; i++) {
$rootScope.colors.push('#' + i);
}
config.panels['test'] = {info: {}};
self.ctrl = $controller(
Ctrl,
{$scope: self.scope},
{
panel: self.panel,
dashboard: self.dashboard,
},
);
});
};
this.createControllerPhase = function(controllerName) {
return angularMocks.inject(function($controller, $rootScope, $q, $location, $browser) {
self.scope = $rootScope.$new();
self.$location = $location;
self.$browser = $browser;
self.scope.contextSrv = {};
self.scope.panel = {};
self.scope.dashboard = {meta: {}};
self.scope.dashboardMeta = {};
self.scope.dashboardViewState = new DashboardViewStateStub();
self.scope.appEvent = sinon.spy();
self.scope.onAppEvent = sinon.spy();
$rootScope.colors = [];
for (var i = 0; i < 50; i++) {
$rootScope.colors.push('#' + i);
}
self.$q = $q;
self.scope.skipDataOnInit = true;
self.scope.skipAutoInit = true;
self.controller = $controller(controllerName, {
$scope: self.scope,
});
});
};
}
export function ServiceTestContext() {
var self = this;
self.templateSrv = new TemplateSrvStub();
self.timeSrv = new TimeSrvStub();
self.datasourceSrv = {};
self.backendSrv = {};
self.$routeParams = {};
this.providePhase = function(mocks) {
return angularMocks.module(function($provide) {
_.each(mocks, function(key) {
$provide.value(key, self[key]);
});
});
};
this.createService = function(name) {
return angularMocks.inject(function($q, $rootScope, $httpBackend, $injector, $location, $timeout) {
self.$q = $q;
self.$rootScope = $rootScope;
self.$httpBackend = $httpBackend;
self.$location = $location;
self.$rootScope.onAppEvent = function() {};
self.$rootScope.appEvent = function() {};
self.$timeout = $timeout;
self.service = $injector.get(name);
});
};
}
export function DashboardViewStateStub() {
this.registerPanel = function() {};
}
export function TimeSrvStub() {
this.init = sinon.spy();
this.time = {from: 'now-1h', to: 'now'};
this.timeRange = function(parse) {
if (parse === false) {
return this.time;
}
return {
from: dateMath.parse(this.time.from, false),
to: dateMath.parse(this.time.to, true),
};
};
this.replace = function(target) {
return target;
};
this.setTime = function(time) {
this.time = time;
};
}
export function ContextSrvStub() {
this.hasRole = function() {
return true;
};
}
export function TemplateSrvStub() {
this.variables = [];
this.templateSettings = {interpolate: /\[\[([\s\S]+?)\]\]/g};
this.data = {};
this.replace = function(text) {
return _.template(text, this.templateSettings)(this.data);
};
this.init = function() {};
this.getAdhocFilters = function() {
return [];
};
this.fillVariableValuesForUrl = function() {};
this.updateTemplateData = function() {};
this.variableExists = function() {
return false;
};
this.variableInitialized = function() {};
this.highlightVariablesAsHtml = function(str) {
return str;
};
this.setGrafanaVariable = function(name, value) {
this.data[name] = value;
};
}
var allDeps = {
ContextSrvStub: ContextSrvStub,
TemplateSrvStub: TemplateSrvStub,
TimeSrvStub: TimeSrvStub,
ControllerTestContext: ControllerTestContext,
ServiceTestContext: ServiceTestContext,
DashboardViewStateStub: DashboardViewStateStub
};
// for legacy
export default allDeps;

View File

@@ -1,130 +0,0 @@
(function() {
"use strict";
// Tun on full stack traces in errors to help debugging
Error.stackTraceLimit=Infinity;
window.__karma__.loaded = function() {};
System.config({
baseURL: '/base/',
defaultJSExtensions: true,
paths: {
'mousetrap': 'vendor/npm/mousetrap/mousetrap.js',
'eventemitter3': 'vendor/npm/eventemitter3/index.js',
'remarkable': 'vendor/npm/remarkable/dist/remarkable.js',
'tether': 'vendor/npm/tether/dist/js/tether.js',
'tether-drop': 'vendor/npm/tether-drop/dist/js/drop.js',
'moment': 'vendor/moment.js',
"jquery": "vendor/jquery/dist/jquery.js",
'lodash-src': 'vendor/lodash/dist/lodash.js',
"lodash": 'app/core/lodash_extended.js',
"angular": 'vendor/angular/angular.js',
'angular-mocks': 'vendor/angular-mocks/angular-mocks.js',
"bootstrap": "vendor/bootstrap/bootstrap.js",
'angular-route': 'vendor/angular-route/angular-route.js',
'angular-sanitize': 'vendor/angular-sanitize/angular-sanitize.js',
"angular-ui": "vendor/angular-ui/ui-bootstrap-tpls.js",
"angular-strap": "vendor/angular-other/angular-strap.js",
"angular-dragdrop": "vendor/angular-native-dragdrop/draganddrop.js",
"angular-bindonce": "vendor/angular-bindonce/bindonce.js",
"spectrum": "vendor/spectrum.js",
"bootstrap-tagsinput": "vendor/tagsinput/bootstrap-tagsinput.js",
"jquery.flot": "vendor/flot/jquery.flot",
"jquery.flot.pie": "vendor/flot/jquery.flot.pie",
"jquery.flot.selection": "vendor/flot/jquery.flot.selection",
"jquery.flot.stack": "vendor/flot/jquery.flot.stack",
"jquery.flot.stackpercent": "vendor/flot/jquery.flot.stackpercent",
"jquery.flot.time": "vendor/flot/jquery.flot.time",
"jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair",
"jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow",
"jquery.flot.gauge": "vendor/flot/jquery.flot.gauge",
"d3": "vendor/d3/d3.js",
"jquery.flot.dashes": "vendor/flot/jquery.flot.dashes",
"twemoji": "vendor/npm/twemoji/2/twemoji.amd.js",
"ace": "vendor/npm/ace-builds/src-noconflict/ace",
},
packages: {
app: {
defaultExtension: 'js',
},
vendor: {
defaultExtension: 'js',
},
},
map: {
},
meta: {
'vendor/angular/angular.js': {
format: 'global',
deps: ['jquery'],
exports: 'angular',
},
'vendor/angular-mocks/angular-mocks.js': {
format: 'global',
deps: ['angular'],
},
'vendor/npm/eventemitter3/index.js': {
format: 'cjs',
exports: 'EventEmitter'
},
'vendor/npm/mousetrap/mousetrap.js': {
format: 'global',
exports: 'Mousetrap'
},
'vendor/npm/ace-builds/src-noconflict/ace.js': {
format: 'global',
exports: 'ace'
},
}
});
function file2moduleName(filePath) {
return filePath.replace(/\\/g, '/')
.replace(/^\/base\//, '')
.replace(/\.\w*$/, '');
}
function onlySpecFiles(path) {
return /specs.*/.test(path);
}
window.grafanaBootData = {settings: {}};
var modules = ['angular', 'angular-mocks', 'app/app'];
var promises = modules.map(function(name) {
return System.import(name);
});
Promise.all(promises).then(function(deps) {
var angular = deps[0];
angular.module('grafana', ['ngRoute']);
angular.module('grafana.services', ['ngRoute', '$strap.directives']);
angular.module('grafana.panels', []);
angular.module('grafana.controllers', []);
angular.module('grafana.directives', []);
angular.module('grafana.filters', []);
angular.module('grafana.routes', ['ngRoute']);
// load specs
return Promise.all(
Object.keys(window.__karma__.files) // All files served by Karma.
.filter(onlySpecFiles)
.map(file2moduleName)
.map(function(path) {
// console.log(path);
return System.import(path);
}));
}).then(function() {
window.__karma__.start();
}, function(error) {
window.__karma__.error(error.stack || error);
}).catch(function(error) {
window.__karma__.error(error.stack || error);
});
})();

View File

@@ -10,7 +10,7 @@ const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = merge(common, {
devtool: "eval-source-map",
devtool: "cheap-module-source-map",
entry: {
dark: './public/sass/grafana.dark.scss',

View File

@@ -1072,6 +1072,10 @@ base@^0.11.1:
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
batch-processor@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8"
bcrypt-pbkdf@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d"
@@ -1509,6 +1513,10 @@ class-utils@^0.3.5:
lazy-cache "^2.0.2"
static-extend "^0.1.1"
classnames@2.x, classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
clean-css@3.4.x, clean-css@~3.4.2:
version "3.4.28"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.28.tgz#bf1945e82fc808f55695e6ddeaec01400efd03ff"
@@ -2411,6 +2419,12 @@ elegant-spinner@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
element-resize-detector@^1.1.12:
version "1.1.12"
resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.1.12.tgz#8b3fd6eedda17f9c00b360a0ea2df9927ae80ba2"
dependencies:
batch-processor "^1.0.0"
elliptic@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
@@ -3445,14 +3459,6 @@ graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6,
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
"gridstack@https://github.com/grafana/gridstack.js#grafana":
version "1.0.0-dev"
resolved "https://github.com/grafana/gridstack.js#bd40b3fe4dafc99350145c7b4761d8693593f6fe"
dependencies:
jquery "^3.1.0"
jquery-ui "^1.12.0"
lodash "^4.14.2"
growl@1.10.3:
version "1.10.3"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f"
@@ -4434,11 +4440,7 @@ jest-validate@^21.1.0:
leven "^2.1.0"
pretty-format "^21.2.1"
jquery-ui@^1.12.0:
version "1.12.1"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.12.1.tgz#bcb4045c8dd0539c134bc1488cdd3e768a7a9e51"
jquery@^3.1.0, jquery@^3.2.1:
jquery@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.2.1.tgz#5c4d9de652af6cd0a770154a631bba12b015c787"
@@ -4975,6 +4977,10 @@ lodash.flattendeep@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
lodash.isequal@^4.0.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
lodash.kebabcase@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
@@ -5011,7 +5017,7 @@ lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, loda
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.14.2, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4:
lodash@^4.0.0, lodash@^4.0.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0, lodash@^4.5.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -6719,7 +6725,7 @@ promzard@^0.3.0:
dependencies:
read "1"
prop-types@^15.5.10, prop-types@^15.6.0:
prop-types@15.x, prop-types@^15.5.10, prop-types@^15.6.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856"
dependencies:
@@ -6877,6 +6883,38 @@ react-dom@^16.0.0:
object-assign "^4.1.1"
prop-types "^15.6.0"
"react-draggable@^2.2.6 || ^3.0.3", react-draggable@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-3.0.3.tgz#a6f9b3a7171981b76dadecf238316925cb9eacf4"
dependencies:
classnames "^2.2.5"
prop-types "^15.5.10"
react-grid-layout@^0.16.0:
version "0.16.0"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-0.16.0.tgz#f74363cd134b2f8a763224d7b6287cbb68e6de05"
dependencies:
classnames "2.x"
lodash.isequal "^4.0.0"
prop-types "15.x"
react-draggable "^3.0.3"
react-resizable "^1.7.5"
react-resizable@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e"
dependencies:
prop-types "15.x"
react-draggable "^2.2.6 || ^3.0.3"
react-sizeme@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.3.6.tgz#d60ea2634acc3fd827a3c7738d41eea0992fa678"
dependencies:
element-resize-detector "^1.1.12"
invariant "^2.2.2"
lodash "^4.17.4"
react-test-renderer@^16.0.0:
version "16.0.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.0.0.tgz#9fe7b8308f2f71f29fc356d4102086f131c9cb15"