2015-09-15 08:17:40 +02:00
|
|
|
define([
|
|
|
|
|
'angular',
|
2017-05-20 10:48:47 +02:00
|
|
|
'require',
|
2015-09-15 08:17:40 +02:00
|
|
|
'../core_module',
|
2015-10-30 14:44:40 +01:00
|
|
|
'app/core/utils/kbn',
|
2015-09-15 08:17:40 +02:00
|
|
|
],
|
2017-05-20 10:48:47 +02:00
|
|
|
function (angular, require, coreModule, kbn) {
|
2015-09-15 08:17:40 +02:00
|
|
|
'use strict';
|
|
|
|
|
|
2015-12-17 16:30:53 +01:00
|
|
|
coreModule.default.directive('tip', function($compile) {
|
2015-09-15 08:17:40 +02:00
|
|
|
return {
|
|
|
|
|
restrict: 'E',
|
|
|
|
|
link: function(scope, elem, attrs) {
|
|
|
|
|
var _t = '<i class="grafana-tip fa fa-'+(attrs.icon||'question-circle')+'" bs-tooltip="\''+
|
|
|
|
|
kbn.addslashes(elem.text())+'\'"></i>';
|
|
|
|
|
_t = _t.replace(/{/g, '\\{').replace(/}/g, '\\}');
|
|
|
|
|
elem.replaceWith($compile(angular.element(_t))(scope));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2017-05-20 14:52:28 +02:00
|
|
|
coreModule.default.directive('clipboardButton', function() {
|
|
|
|
|
return {
|
|
|
|
|
scope: {
|
|
|
|
|
getText: '&clipboardButton'
|
|
|
|
|
},
|
|
|
|
|
link: function(scope, elem) {
|
2017-09-22 09:47:25 +02:00
|
|
|
require(['clipboard'], function(Clipboard) {
|
2017-05-20 14:52:28 +02:00
|
|
|
scope.clipboard = new Clipboard(elem[0], {
|
|
|
|
|
text: function() {
|
|
|
|
|
return scope.getText();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2017-05-20 10:48:47 +02:00
|
|
|
|
2017-05-20 14:52:28 +02:00
|
|
|
scope.$on('$destroy', function() {
|
|
|
|
|
if (scope.clipboard) {
|
|
|
|
|
scope.clipboard.destroy();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-05-20 10:48:47 +02:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
History and Version Control for Dashboard Updates
A simple version control system for dashboards. Closes #1504.
Goals
1. To create a new dashboard version every time a dashboard is saved.
2. To allow users to view all versions of a given dashboard.
3. To allow users to rollback to a previous version of a dashboard.
4. To allow users to compare two versions of a dashboard.
Usage
Navigate to a dashboard, and click the settings cog. From there, click
the "Changelog" button to be brought to the Changelog view. In this
view, a table containing each version of a dashboard can be seen. Each
entry in the table represents a dashboard version. A selectable
checkbox, the version number, date created, name of the user who created
that version, and commit message is shown in the table, along with a
button that allows a user to restore to a previous version of that
dashboard. If a user wants to restore to a previous version of their
dashboard, they can do so by clicking the previously mentioned button.
If a user wants to compare two different versions of a dashboard, they
can do so by clicking the checkbox of two different dashboard versions,
then clicking the "Compare versions" button located below the dashboard.
From there, the user is brought to a view showing a summary of the
dashboard differences. Each summarized change contains a link that can
be clicked to take the user a JSON diff highlighting the changes line by
line.
Overview of Changes
Backend Changes
- A `dashboard_version` table was created to store each dashboard
version, along with a dashboard version model and structs to represent
the queries and commands necessary for the dashboard version API
methods.
- API endpoints were created to support working with dashboard
versions.
- Methods were added to create, update, read, and destroy dashboard
versions in the database.
- Logic was added to compute the diff between two versions, and
display it to the user.
- The dashboard migration logic was updated to save a "Version
1" of each existing dashboard in the database.
Frontend Changes
- New views
- Methods to pull JSON and HTML from endpoints
New API Endpoints
Each endpoint requires the authorization header to be sent in
the format,
```
Authorization: Bearer <jwt>
```
where `<jwt>` is a JSON web token obtained from the Grafana
admin panel.
`GET "/api/dashboards/db/:dashboardId/versions?orderBy=<string>&limit=<int>&start=<int>"`
Get all dashboard versions for the given dashboard ID. Accepts
three URL parameters:
- `orderBy` String to order the results by. Possible values
are `version`, `created`, `created_by`, `message`. Default
is `versions`. Ordering is always in descending order.
- `limit` Maximum number of results to return
- `start` Position in results to start from
`GET "/api/dashboards/db/:dashboardId/versions/:id"`
Get an individual dashboard version by ID, for the given
dashboard ID.
`POST "/api/dashboards/db/:dashboardId/restore"`
Restore to the given dashboard version. Post body is of
content-type `application/json`, and must contain.
```json
{
"dashboardId": <int>,
"version": <int>
}
```
`GET "/api/dashboards/db/:dashboardId/compare/:versionA...:versionB"`
Compare two dashboard versions by ID for the given
dashboard ID, returning a JSON delta formatted
representation of the diff. The URL format follows
what GitHub does. For example, visiting
[/api/dashboards/db/18/compare/22...33](http://ec2-54-80-139-44.compute-1.amazonaws.com:3000/api/dashboards/db/18/compare/22...33)
will return the diff between versions 22 and 33 for
the dashboard ID 18.
Dependencies Added
- The Go package [gojsondiff](https://github.com/yudai/gojsondiff)
was added and vendored.
2017-05-24 19:14:39 -04:00
|
|
|
coreModule.default.directive('compile', function($compile) {
|
|
|
|
|
return {
|
|
|
|
|
restrict: 'A',
|
|
|
|
|
link: function(scope, element, attrs) {
|
|
|
|
|
scope.$watch(function(scope) {
|
|
|
|
|
return scope.$eval(attrs.compile);
|
|
|
|
|
}, function(value) {
|
|
|
|
|
element.html(value);
|
|
|
|
|
$compile(element.contents())(scope);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2015-12-17 16:30:53 +01:00
|
|
|
coreModule.default.directive('watchChange', function() {
|
2015-09-15 08:17:40 +02:00
|
|
|
return {
|
|
|
|
|
scope: { onchange: '&watchChange' },
|
|
|
|
|
link: function(scope, element) {
|
|
|
|
|
element.on('input', function() {
|
|
|
|
|
scope.$apply(function () {
|
|
|
|
|
scope.onchange({ inputValue: element.val() });
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2015-12-17 16:30:53 +01:00
|
|
|
coreModule.default.directive('editorOptBool', function($compile) {
|
2015-09-15 08:17:40 +02:00
|
|
|
return {
|
|
|
|
|
restrict: 'E',
|
|
|
|
|
link: function(scope, elem, attrs) {
|
|
|
|
|
var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
|
|
|
|
|
var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
|
|
|
|
|
var showIf = attrs.showIf ? (' ng-show="' + attrs.showIf + '" ') : '';
|
|
|
|
|
|
2016-02-19 16:01:02 +01:00
|
|
|
var template = '<div class="editor-option gf-form-checkbox text-center"' + showIf + '>' +
|
2015-09-15 08:17:40 +02:00
|
|
|
' <label for="' + attrs.model + '" class="small">' +
|
|
|
|
|
attrs.text + tip + '</label>' +
|
|
|
|
|
'<input class="cr1" id="' + attrs.model + '" type="checkbox" ' +
|
|
|
|
|
' ng-model="' + attrs.model + '"' + ngchange +
|
|
|
|
|
' ng-checked="' + attrs.model + '"></input>' +
|
|
|
|
|
' <label for="' + attrs.model + '" class="cr1"></label>';
|
|
|
|
|
elem.replaceWith($compile(angular.element(template))(scope));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2015-12-17 16:30:53 +01:00
|
|
|
coreModule.default.directive('editorCheckbox', function($compile, $interpolate) {
|
2015-09-15 08:17:40 +02:00
|
|
|
return {
|
|
|
|
|
restrict: 'E',
|
|
|
|
|
link: function(scope, elem, attrs) {
|
|
|
|
|
var text = $interpolate(attrs.text)(scope);
|
|
|
|
|
var model = $interpolate(attrs.model)(scope);
|
|
|
|
|
var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : '';
|
|
|
|
|
var tip = attrs.tip ? (' <tip>' + attrs.tip + '</tip>') : '';
|
|
|
|
|
var label = '<label for="' + scope.$id + model + '" class="checkbox-label">' +
|
|
|
|
|
text + tip + '</label>';
|
|
|
|
|
|
2016-01-09 11:07:34 +01:00
|
|
|
var template =
|
2017-05-20 14:52:28 +02:00
|
|
|
'<input class="cr1" id="' + scope.$id + model + '" type="checkbox" ' +
|
|
|
|
|
' ng-model="' + model + '"' + ngchange +
|
|
|
|
|
' ng-checked="' + model + '"></input>' +
|
|
|
|
|
' <label for="' + scope.$id + model + '" class="cr1"></label>';
|
2015-09-15 08:17:40 +02:00
|
|
|
|
2016-01-09 11:07:34 +01:00
|
|
|
template = template + label;
|
2016-02-15 22:24:28 +01:00
|
|
|
elem.addClass('gf-form-checkbox');
|
|
|
|
|
elem.html($compile(angular.element(template))(scope));
|
2015-09-15 08:17:40 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
2015-12-17 16:30:53 +01:00
|
|
|
coreModule.default.directive('gfDropdown', function ($parse, $compile, $timeout) {
|
2015-09-15 08:17:40 +02:00
|
|
|
function buildTemplate(items, placement) {
|
|
|
|
|
var upclass = placement === 'top' ? 'dropup' : '';
|
|
|
|
|
var ul = [
|
|
|
|
|
'<ul class="dropdown-menu ' + upclass + '" role="menu" aria-labelledby="drop1">',
|
|
|
|
|
'</ul>'
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
angular.forEach(items, function (item, index) {
|
|
|
|
|
if (item.divider) {
|
|
|
|
|
return ul.splice(index + 1, 0, '<li class="divider"></li>');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
|
|
|
|
|
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
|
|
|
|
|
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
|
2017-05-20 14:52:28 +02:00
|
|
|
'>' + (item.text || '') + '</a>';
|
2015-09-15 08:17:40 +02:00
|
|
|
|
|
|
|
|
if (item.submenu && item.submenu.length) {
|
|
|
|
|
li += buildTemplate(item.submenu).join('\n');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
li += '</li>';
|
|
|
|
|
ul.splice(index + 1, 0, li);
|
|
|
|
|
});
|
|
|
|
|
return ul;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
restrict: 'EA',
|
|
|
|
|
scope: true,
|
|
|
|
|
link: function postLink(scope, iElement, iAttrs) {
|
|
|
|
|
var getter = $parse(iAttrs.gfDropdown), items = getter(scope);
|
|
|
|
|
$timeout(function () {
|
|
|
|
|
var placement = iElement.data('placement');
|
|
|
|
|
var dropdown = angular.element(buildTemplate(items, placement).join(''));
|
|
|
|
|
dropdown.insertAfter(iElement);
|
|
|
|
|
$compile(iElement.next('ul.dropdown-menu'))(scope);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
});
|