mirror of
https://github.com/ziirish/burp-ui.git
synced 2026-05-16 14:16:11 -06:00
753 lines
20 KiB
JavaScript
753 lines
20 KiB
JavaScript
/***
|
|
* The User page is managed with AngularJS for some parts.
|
|
* Following is the AngularJS Application and Controller.
|
|
*/
|
|
var app = angular.module('MainApp', ['ngSanitize', 'frapontillo.bootstrap-switch', 'mgcrea.ngStrap', 'ui.select']);
|
|
|
|
app.config(function(uiSelectConfig) {
|
|
uiSelectConfig.theme = 'bootstrap';
|
|
});
|
|
|
|
app.directive('compareTo', function() {
|
|
return {
|
|
require: "ngModel",
|
|
scope: {
|
|
otherModelValue: "=compareTo"
|
|
},
|
|
link: function(scope, element, attributes, ngModel) {
|
|
ngModel.$validators.compareTo = function(modelValue) {
|
|
return modelValue == scope.otherModelValue;
|
|
};
|
|
|
|
scope.$watch("otherModelValue", function() {
|
|
ngModel.$validate();
|
|
});
|
|
}
|
|
};
|
|
});
|
|
|
|
app.controller('UserCtrl', function($timeout, $scope, $http, $scrollspy) {
|
|
$scope.spy = {};
|
|
$scope.user = {
|
|
oldPassword: '',
|
|
newPassword: '',
|
|
confPassword: ''
|
|
};
|
|
$scope.prefs = {};
|
|
$scope.all_clients = [];
|
|
$scope.hide_client = {};
|
|
$scope.hide_disabled = true;
|
|
|
|
// Get the available pageLength
|
|
var table = $('#table-sessions').DataTable();
|
|
var settings = table.settings();
|
|
var table_hidden = $('#table-hidden').DataTable();
|
|
var settings_hidden = table_hidden.settings();
|
|
$scope.lengthMenu = settings[0].aLengthMenu;
|
|
|
|
// Available languages
|
|
$scope.languages = [
|
|
{% for key, val in config['LANGUAGES'].items() %}
|
|
{ 'name': {{ val|tojson }}, 'id': {{ key|tojson }} },
|
|
{% endfor %}
|
|
];
|
|
|
|
$http.get("{{ url_for('api.prefs_ui') }}", { headers: { 'X-From-UI': true } })
|
|
.then(function(response) {
|
|
$scope.prefs = response.data;
|
|
|
|
$scope.prefs.language = '{{ g.locale }}';
|
|
|
|
// current pageLength
|
|
$scope.prefs.pageLength = table.page.len();
|
|
});
|
|
|
|
$http.get("{{ url_for('api.clients_all') }}?_session=" + SESSION_TAG, { headers: { 'X-From-UI': true } })
|
|
.then(function(response) {
|
|
$scope.all_clients = response.data;
|
|
$scope.hide_disabled = false;
|
|
load_d3($scope.all_clients);
|
|
});
|
|
|
|
$scope.groupClientsFn = function(item) {
|
|
if (!item.agent)
|
|
return '{{ _("Clients") }}';
|
|
return item.agent;
|
|
};
|
|
|
|
$scope.hideSelected = function() {
|
|
console.log($scope.hide_client.selected);
|
|
$http.put("{{ url_for('api.prefs_ui_hide') }}", $scope.hide_client.selected, { headers: { 'X-From-UI': true } })
|
|
.then(function(response) {
|
|
notif(NOTIF_SUCCESS, "{{ _('Client will be hidden') }}");
|
|
AJAX_CACHE = false;
|
|
_hidden();
|
|
$scope.hide_client = {};
|
|
}, buiFail);
|
|
};
|
|
|
|
$scope.submitChangePrefs = function(e) {
|
|
e.preventDefault();
|
|
var form = $(e.target);
|
|
var submit = form.find('button[type="submit"]');
|
|
var sav = submit.html();
|
|
submit.html('<i class="fa fa-fw fa-spinner fa-pulse" aria-hidden="true"></i> {{ _("Saving...") }}');
|
|
submit.attr('disabled', true);
|
|
table.page.len($scope.prefs.pageLength).draw();
|
|
table_hidden.page.len($scope.prefs.pageLength).draw();
|
|
/* submit the data */
|
|
$.ajax({
|
|
url: form.attr('action'),
|
|
type: 'POST',
|
|
data: $scope.prefs,
|
|
headers: { 'X-From-UI': true },
|
|
})
|
|
.fail(buiFail)
|
|
.done(function(data) {
|
|
notif(NOTIF_SUCCESS, "{{ _('Preferences successfuly saved') }}");
|
|
})
|
|
.always(function() {
|
|
/* reset the submit button state */
|
|
submit.html(sav);
|
|
submit.attr('disabled', false);
|
|
});
|
|
};
|
|
|
|
$scope.submitChangePass = function(e) {
|
|
e.preventDefault();
|
|
var form = $(e.target);
|
|
submit = form.find('button[type="submit"]');
|
|
sav = submit.html();
|
|
submit.html('<i class="fa fa-fw fa-spinner fa-pulse" aria-hidden="true"></i> {{ _("Saving...") }}');
|
|
submit.attr('disabled', true);
|
|
/* submit the data */
|
|
$.ajax({
|
|
url: form.attr('action'),
|
|
type: 'POST',
|
|
data: {
|
|
'password': $scope.user.newPassword,
|
|
'old_password': $scope.user.oldPassword,
|
|
'backend': "{{ current_user.back.name }}"
|
|
},
|
|
headers: { 'X-From-UI': true },
|
|
})
|
|
.fail(buiFail)
|
|
.done(function(data) {
|
|
notifAll(data);
|
|
})
|
|
.always(function() {
|
|
/* reset the submit button state */
|
|
submit.html(sav);
|
|
submit.attr('disabled', false);
|
|
});
|
|
};
|
|
|
|
$scope.refreshScrollspy = function() {
|
|
angular.forEach($('.bui-scrollspy > li'), function(e) {
|
|
var ae = angular.element(e);
|
|
var target = e.dataset.target;
|
|
var options = {
|
|
scope: $scope,
|
|
target: target
|
|
};
|
|
if (target in $scope.spy) {
|
|
var oldSpy = $scope.spy[target];
|
|
oldSpy.untrackElement(options.target, ae);
|
|
oldSpy.destroy();
|
|
}
|
|
var scrollspy = $scrollspy(options);
|
|
scrollspy.trackElement(options.target, ae);
|
|
$scope.spy[target] = scrollspy;
|
|
});
|
|
};
|
|
|
|
$timeout(function() {
|
|
$scope.refreshScrollspy();
|
|
});
|
|
});
|
|
|
|
/***
|
|
* The session part is handled by datatables
|
|
* the resulting JSON looks like:
|
|
* [
|
|
* {
|
|
* "api": false,
|
|
* "current": false,
|
|
* "expire": "1970-01-01T00:00:00+01:00",
|
|
* "ip": "::ffff:10.0.0.100",
|
|
* "permanent": false,
|
|
* "timestamp": "2016-09-01T15:28:59.222028+02:00",
|
|
* "ua": "Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0",
|
|
* "uuid": "feaac9c7-6f61-4103-9536-d65aba335892"
|
|
* }
|
|
* ]
|
|
*/
|
|
{% import 'macros.html' as macros %}
|
|
|
|
var _cache_id = _EXTRA;
|
|
var first = true;
|
|
|
|
{{ macros.timestamp_filter() }}
|
|
|
|
var _sessions_table = $('#table-sessions').DataTable( {
|
|
{{ macros.translate_datatable() }}
|
|
{{ macros.get_page_length() }}
|
|
responsive: true,
|
|
processing: true,
|
|
fixedHeader: true,
|
|
select: {
|
|
style: 'os',
|
|
},
|
|
ajax: {
|
|
url: '{{ url_for("api.user_sessions") }}',
|
|
headers: { 'X-From-UI': true },
|
|
cache: AJAX_CACHE,
|
|
error: buiFail,
|
|
data: function (request) {
|
|
request._extra = _cache_id;
|
|
},
|
|
dataSrc: function (data) {
|
|
return data;
|
|
}
|
|
},
|
|
dom: "<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
|
"<'row'<'col-sm-12'tr>>" +
|
|
"<'row'<'col-sm-5'><'col-sm-7'p>>" +
|
|
"<'row'<'col-sm-5'i>>" +
|
|
"<'row'B>",
|
|
buttons: [
|
|
{
|
|
text: '<i class="fa fa-check-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Select all") }}',
|
|
extend: 'selectAll',
|
|
},
|
|
{
|
|
text: '<i class="fa fa-filter" aria-hidden="true"></i> <i class="fa fa-check-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Select all filtered") }}',
|
|
extend: 'selectAll',
|
|
action: function ( e, dt, node, config ) {
|
|
dt.rows( { search: 'applied' } ).select();
|
|
}
|
|
},
|
|
{
|
|
text: '<i class="fa fa-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Deselect all") }}',
|
|
extend: 'selectNone',
|
|
},
|
|
{
|
|
text : '<i class="fa fa-filter" aria-hidden="true"></i> <i class="fa fa-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Deselect all filtered") }}',
|
|
extend: 'selectNone',
|
|
action: function ( e, dt, node, config ) {
|
|
dt.rows( { search: 'applied' } ).deselect();
|
|
}
|
|
},
|
|
{
|
|
text: '<span aria-label="{{ _("Revoke") }}"><i class="fa fa-trash-o" aria-hidden="true"></i></span>',
|
|
titleAttr: '{{ _("Revoke selected") }}',
|
|
className: 'btn-danger',
|
|
action: function ( e, dt, node, config ) {
|
|
var rows = dt.rows( { selected: true } ).data();
|
|
var current = false;
|
|
var output = '';
|
|
$.each(rows, function(i, row) {
|
|
output += JSON.stringify(row, null, 4)+'\n';
|
|
if (row.current) {
|
|
current = true;
|
|
}
|
|
});
|
|
if (current) {
|
|
$('#error-confirm').show();
|
|
} else {
|
|
$('#error-confirm').hide();
|
|
}
|
|
$('#session-details').empty().text(output);
|
|
$('#perform-revoke').data('multi', true);
|
|
$('#confirmation-modal').modal('toggle');
|
|
$('pre code').each(function(i, block) {
|
|
hljs.highlightBlock(block);
|
|
});
|
|
},
|
|
enabled: false
|
|
}
|
|
],
|
|
order: [[1, 'desc']],
|
|
rowId: 'uuid',
|
|
columns: [
|
|
{ data: 'ip' },
|
|
{
|
|
data: 'timestamp',
|
|
type: 'timestamp',
|
|
render: function( data, type, row ) {
|
|
if (type === 'filter') {
|
|
return data;
|
|
}
|
|
return '<span data-toggle="tooltip" title="'+data+'">'+moment(data, moment.ISO_8601).tz(TIMEZONE).subtract(3, 'seconds').fromNow()+'</span>';
|
|
}
|
|
},
|
|
{
|
|
data: 'ua',
|
|
render: function( data, type, row ) {
|
|
if (type === 'filter' || type === 'sort') {
|
|
return data;
|
|
}
|
|
ret = '';
|
|
// Browser version
|
|
if (data.lastIndexOf('MSIE') > 0 || data.lastIndexOf('Trident') > 0) {
|
|
ret += '<i class="fa fa-internet-explorer" aria-hidden="true"></i>';
|
|
} else if (data.lastIndexOf('Edge') > 0) {
|
|
ret += '<i class="fa fa-edge" aria-hidden="true"></i>';
|
|
} else if (/Firefox[\/\s](\d+\.\d+)/.test(data)) {
|
|
ret += '<i class="fa fa-firefox" aria-hidden="true"></i>';
|
|
} else if (data.lastIndexOf('Chrome/') > 0) {
|
|
ret += '<i class="fa fa-chrome" aria-hidden="true"></i>';
|
|
} else if (data.lastIndexOf('Safari/') > 0) {
|
|
ret += '<i class="fa fa-safari" aria-hidden="true"></i>';
|
|
} else {
|
|
ret += '<i class="fa fa-question" aria-hidden="true"></i>';
|
|
}
|
|
// Optionally add OS version
|
|
if (data.lastIndexOf('Android') > 0) {
|
|
ret += ' <i class="fa fa-android" aria-hidden="true"></i>';
|
|
} else if (data.lastIndexOf('iPhone') > 0 || data.lastIndexOf('iPad') > 0 || data.lastIndexOf('Macintosh') > 0) {
|
|
ret += ' <i class="fa fa-apple" aria-hidden="true"></i>';
|
|
if (data.lastIndexOf('iPhone') > 0) {
|
|
ret += ' <i class="fa fa-mobile" aria-hidden="true"></i>';
|
|
} else if (data.lastIndexOf('iPad') > 0) {
|
|
ret += ' <i class="fa fa-tablet" aria-hidden="true"></i>';
|
|
}
|
|
} else if (data.lastIndexOf('Linux') > 0) {
|
|
ret += ' <i class="fa fa-linux" aria-hidden="true"></i>';
|
|
} else if (data.lastIndexOf('Windows') > 0) {
|
|
ret += ' <i class="fa fa-windows" aria-hidden="true"></i>';
|
|
}
|
|
return '<span data-toggle="tooltip" title="'+data+'">'+ret+'</span>';
|
|
}
|
|
},
|
|
{
|
|
data: 'api',
|
|
render: function( data, type, row ) {
|
|
return '<i class="fa fa-'+(data?'check':'remove')+'" aria-hidden="true"></i>';
|
|
}
|
|
},
|
|
{
|
|
data: 'current',
|
|
render: function( data, type, row ) {
|
|
return '<i class="fa fa-'+(data?'check':'remove')+'" aria-hidden="true"></i>';
|
|
}
|
|
},
|
|
{
|
|
data: 'expire',
|
|
type: 'timestamp',
|
|
render: function( data, type, row ) {
|
|
if (type === 'filter') {
|
|
return data;
|
|
}
|
|
return '<span data-toggle="tooltip" title="'+data+'">'+moment(data, moment.ISO_8601).tz(TIMEZONE).fromNow()+'</span>';
|
|
}
|
|
},
|
|
],
|
|
});
|
|
|
|
var _events_callback = function() {
|
|
$('[data-toggle="tooltip"]').tooltip();
|
|
$('[data-toggle="revoke-session"]').on('click', function(e) {
|
|
$me = $(this);
|
|
if ($me.data('current')) {
|
|
$('#error-confirm').show();
|
|
} else {
|
|
$('#error-confirm').hide();
|
|
}
|
|
$('#session-details').empty().text(unescape($me.data('misc')));
|
|
$('#perform-revoke').data('id', $me.data('id'));
|
|
$('#perform-revoke').data('current', $me.data('current'));
|
|
$('#confirmation-modal').modal('toggle');
|
|
});
|
|
};
|
|
|
|
var select_event = function( e, dt, type, indexes ) {
|
|
var selectedRows = _sessions_table.rows( { selected: true } ).count();
|
|
_sessions_table.buttons( [3, 4] ).enable( selectedRows > 0 );
|
|
};
|
|
|
|
_sessions_table.on('select.dt', select_event);
|
|
_sessions_table.on('deselect.dt', select_event);
|
|
_sessions_table.on('draw.dt', _events_callback);
|
|
_sessions_table.on('responsive-display.dt', function ( e, datatable, row, showHide, update ) {
|
|
_events_callback();
|
|
});
|
|
{{ macros.page_length('#table-sessions') }}
|
|
|
|
var revoke_session = function(id, refresh) {
|
|
return $.ajax({
|
|
url: '{{ url_for("api.user_sessions") }}/'+id,
|
|
headers: { 'X-From-UI': true },
|
|
type: 'DELETE'
|
|
}).done(function(data) {
|
|
notifAll(data);
|
|
if (refresh && data[0] == NOTIF_SUCCESS) {
|
|
AJAX_CACHE = false;
|
|
_sessions();
|
|
}
|
|
}).fail(buiFail);
|
|
};
|
|
|
|
$('#perform-revoke').on('click', function(e) {
|
|
$me = $(this);
|
|
if ($me.data('multi')) {
|
|
var rows = _sessions_table.rows( { selected: true } ).data();
|
|
var current = undefined;
|
|
var requests = [];
|
|
var last;
|
|
$.each(rows, function(i, row) {
|
|
if (row.current) {
|
|
current = row;
|
|
return;
|
|
}
|
|
last = revoke_session(row.uuid, false);
|
|
requests.push(last);
|
|
});
|
|
if (current) {
|
|
$.when.apply( $, requests ).done(function() {
|
|
window.location = '{{ url_for("view.logout") }}';
|
|
return;
|
|
});
|
|
} else {
|
|
$.when.apply( $, requests ).done(function() {
|
|
AJAX_CACHE = false;
|
|
_sessions();
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if ($me.data('current')) {
|
|
window.location = '{{ url_for("view.logout") }}';
|
|
return;
|
|
}
|
|
revoke_session($me.data('id'), true);
|
|
});
|
|
|
|
/**
|
|
* The hidden part is handled by datatables
|
|
* The resulting JSON looks like:
|
|
*
|
|
* [
|
|
* {
|
|
* 'client': 'cobalt',
|
|
* 'server': null
|
|
* },
|
|
* {
|
|
* 'client': null,
|
|
* 'server': 'BURP1'
|
|
* }
|
|
* ]
|
|
*/
|
|
|
|
var _hidden_table = $('#table-hidden').DataTable( {
|
|
{{ macros.translate_datatable() }}
|
|
{{ macros.get_page_length() }}
|
|
responsive: true,
|
|
processing: true,
|
|
fixedHeader: true,
|
|
select: {
|
|
style: 'os',
|
|
},
|
|
ajax: {
|
|
url: '{{ url_for("api.prefs_ui_hide") }}',
|
|
headers: { 'X-From-UI': true },
|
|
cache: AJAX_CACHE,
|
|
error: buiFail,
|
|
data: function (request) {
|
|
request._extra = _cache_id;
|
|
},
|
|
dataSrc: function (data) {
|
|
return data;
|
|
}
|
|
},
|
|
dom: "<'row'<'col-sm-6'l><'col-sm-6'f>>" +
|
|
"<'row'<'col-sm-12'tr>>" +
|
|
"<'row'<'col-sm-5'><'col-sm-7'p>>" +
|
|
"<'row'<'col-sm-5'i>>" +
|
|
"<'row'B>",
|
|
buttons: [
|
|
{
|
|
text: '<i class="fa fa-check-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Select all") }}',
|
|
extend: 'selectAll',
|
|
},
|
|
{
|
|
text: '<i class="fa fa-filter" aria-hidden="true"></i> <i class="fa fa-check-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Select all filtered") }}',
|
|
extend: 'selectAll',
|
|
action: function ( e, dt, node, config ) {
|
|
dt.rows( { search: 'applied' } ).select();
|
|
}
|
|
},
|
|
{
|
|
text: '<i class="fa fa-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Deselect all") }}',
|
|
extend: 'selectNone',
|
|
},
|
|
{
|
|
text : '<i class="fa fa-filter" aria-hidden="true"></i> <i class="fa fa-square-o" aria-hidden="true"></i>',
|
|
titleAttr: '{{ _("Deselect all filtered") }}',
|
|
extend: 'selectNone',
|
|
action: function ( e, dt, node, config ) {
|
|
dt.rows( { search: 'applied' } ).deselect();
|
|
}
|
|
},
|
|
{
|
|
text: '<span aria-label="{{ _("Show") }}"><i class="fa fa-eye" aria-hidden="true"></i></span>',
|
|
titleAttr: '{{ _("Show selected") }}',
|
|
className: 'btn-info',
|
|
action: function ( e, dt, node, config ) {
|
|
var rows = dt.rows( { selected: true } ).data();
|
|
var promises = [];
|
|
$.each(rows, function(i, row) {
|
|
var p = $.ajax({
|
|
url: "{{ url_for('api.prefs_ui_hide') }}",
|
|
type: 'DELETE',
|
|
data: {
|
|
'name': row.client,
|
|
'agent': row.server,
|
|
}
|
|
}).fail(buiFail);
|
|
promises.push(p);
|
|
});
|
|
$.when.apply( $, promises ).always(function() {
|
|
AJAX_CACHE = false;
|
|
_hidden();
|
|
_hidden_table.buttons( [3, 4] ).enable( false );
|
|
}).then(function() {
|
|
notif(NOTIF_SUCCESS, "{{ _('The selected hosts will be shown again') }}");
|
|
});
|
|
},
|
|
enabled: false
|
|
}
|
|
],
|
|
order: [[1, 'desc']],
|
|
rowId: 'client',
|
|
columns: [
|
|
{ data: 'client' },
|
|
{ data: 'server' },
|
|
],
|
|
});
|
|
|
|
var select_hidden_event = function( e, dt, type, indexes ) {
|
|
var selectedRows = _hidden_table.rows( { selected: true } ).count();
|
|
_hidden_table.buttons( [3, 4] ).enable( selectedRows > 0 );
|
|
};
|
|
|
|
_hidden_table.on('select.dt', select_hidden_event);
|
|
_hidden_table.on('deselect.dt', select_hidden_event);
|
|
|
|
var _sessions = function() {
|
|
if (!AJAX_CACHE) {
|
|
_cache_id = new Date().getTime();
|
|
}
|
|
_sessions_table.ajax.reload( null, false );
|
|
AJAX_CACHE = true;
|
|
};
|
|
|
|
var _hidden = function() {
|
|
if (!AJAX_CACHE) {
|
|
_cache_id = new Date().getTime();
|
|
}
|
|
_hidden_table.ajax.reload( null, false );
|
|
AJAX_CACHE = true;
|
|
};
|
|
|
|
var _me = function() {
|
|
if (first) {
|
|
first = false;
|
|
} else {
|
|
if (!AJAX_CACHE) {
|
|
_cache_id = new Date().getTime();
|
|
}
|
|
_sessions_table.ajax.reload( null, false );
|
|
_hidden_table.ajax.reload( null, false );
|
|
AJAX_CACHE = true;
|
|
}
|
|
};
|
|
|
|
// adapted from http://bl.ocks.org/d3noob/8375092
|
|
// ************** Generate the tree diagram *****************
|
|
var w = window,
|
|
d = document,
|
|
e = d.documentElement,
|
|
g = d.getElementsByTagName('body')[0],
|
|
x = $('#acl-tree').width(),
|
|
y = w.innerHeight|| e.clientHeight|| g.clientHeight;
|
|
|
|
var margin = {top: 20, right: 120, bottom: 20, left: 120},
|
|
width = x - margin.right - margin.left,
|
|
height = (y*0.9) - margin.top - margin.bottom;
|
|
|
|
var i = 0,
|
|
duration = 750,
|
|
root;
|
|
|
|
var tree = d3.layout.tree()
|
|
.size([height, width]);
|
|
|
|
var diagonal = d3.svg.diagonal()
|
|
.projection(function(d) { return [d.y, d.x]; });
|
|
|
|
var svg = d3.select("#acl-tree").append("svg")
|
|
.attr("width", width + margin.right + margin.left)
|
|
.attr("height", height + margin.top + margin.bottom)
|
|
.append("g")
|
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
|
|
|
d3.select(self.frameElement).style("height", height+"px");
|
|
|
|
function update_tree(source) {
|
|
|
|
// Compute the new tree layout.
|
|
var nodes = tree.nodes(root).reverse(),
|
|
links = tree.links(nodes);
|
|
|
|
// Normalize for fixed-depth.
|
|
nodes.forEach(function(d) { d.y = d.depth * 180; });
|
|
|
|
// Update the nodes…
|
|
var node = svg.selectAll("g.node")
|
|
.data(nodes, function(d) { return d.id || (d.id = ++i); });
|
|
|
|
// Enter any new nodes at the parent's previous position.
|
|
var nodeEnter = node.enter().append("g")
|
|
.attr("class", "node")
|
|
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
|
|
.on("click", click);
|
|
|
|
nodeEnter.append("circle")
|
|
.attr("r", 1e-6)
|
|
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
|
|
|
|
nodeEnter.append("text")
|
|
.attr("x", function(d) { return d.children || d._children ? -13 : 13; })
|
|
.attr("dy", ".35em")
|
|
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
|
|
.text(function(d) { return d.name; })
|
|
.style("fill-opacity", 1e-6);
|
|
|
|
// Transition nodes to their new position.
|
|
var nodeUpdate = node.transition()
|
|
.duration(duration)
|
|
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
|
|
|
|
nodeUpdate.select("circle")
|
|
.attr("r", 10)
|
|
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
|
|
|
|
nodeUpdate.select("text")
|
|
.style("fill-opacity", 1);
|
|
|
|
// Transition exiting nodes to the parent's new position.
|
|
var nodeExit = node.exit().transition()
|
|
.duration(duration)
|
|
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
|
|
.remove();
|
|
|
|
nodeExit.select("circle")
|
|
.attr("r", 1e-6);
|
|
|
|
nodeExit.select("text")
|
|
.style("fill-opacity", 1e-6);
|
|
|
|
// Update the links…
|
|
var link = svg.selectAll("path.link")
|
|
.data(links, function(d) { return d.target.id; });
|
|
|
|
// Enter any new links at the parent's previous position.
|
|
link.enter().insert("path", "g")
|
|
.attr("class", "link")
|
|
.attr("d", function(d) {
|
|
var o = {x: source.x0, y: source.y0};
|
|
return diagonal({source: o, target: o});
|
|
});
|
|
|
|
// Transition links to their new position.
|
|
link.transition()
|
|
.duration(duration)
|
|
.attr("d", diagonal);
|
|
|
|
// Transition exiting nodes to the parent's new position.
|
|
link.exit().transition()
|
|
.duration(duration)
|
|
.attr("d", function(d) {
|
|
var o = {x: source.x, y: source.y};
|
|
return diagonal({source: o, target: o});
|
|
})
|
|
.remove();
|
|
|
|
// Stash the old positions for transition.
|
|
nodes.forEach(function(d) {
|
|
d.x0 = d.x;
|
|
d.y0 = d.y;
|
|
});
|
|
}
|
|
|
|
// Toggle children on click.
|
|
function click(d) {
|
|
if (d.children) {
|
|
d._children = d.children;
|
|
d.children = null;
|
|
} else {
|
|
d.children = d._children;
|
|
d._children = null;
|
|
}
|
|
update_tree(d);
|
|
}
|
|
|
|
function load_d3(data) {
|
|
root = {
|
|
{% if config.STANDALONE -%}
|
|
"name": "{{ _('clients') }}",
|
|
{% else -%}
|
|
"name": "{{ _('servers') }}",
|
|
{% endif -%}
|
|
"parent": "null",
|
|
"children": []
|
|
};
|
|
var children = {};
|
|
|
|
$.each(data, function(i, node) {
|
|
if (! node.agent) {
|
|
root.children.push({
|
|
'name': node.name,
|
|
'parent': "{{ _('clients') }}"
|
|
});
|
|
return;
|
|
}
|
|
if (! (node.agent in children)) {
|
|
children[node.agent] = {
|
|
'name': node.agent,
|
|
'parent': "{{ _('servers') }}",
|
|
'children': []
|
|
};
|
|
}
|
|
children[node.agent]['children'].push({
|
|
'name': node.name,
|
|
'parent': node.agent
|
|
});
|
|
});
|
|
|
|
$.each(children, function(i, nodes) {
|
|
root.children.push(nodes);
|
|
});
|
|
|
|
function collapse(d) {
|
|
if (d.children) {
|
|
d._children = d.children;
|
|
d._children.forEach(collapse);
|
|
d.children = null;
|
|
}
|
|
}
|
|
|
|
root.children.forEach(collapse);
|
|
root.x0 = height / 2;
|
|
root.y0 = 0;
|
|
|
|
update_tree(root);
|
|
}
|