Add support for full logging of all actions in Quay, and the ability to view and filter these logs in the org’s admin view
This commit is contained in:
parent
d5c0f768c2
commit
cca5daf097
16 changed files with 25024 additions and 16 deletions
|
@ -127,6 +127,30 @@ html, body {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.logs-view-element .header {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logs-view-element .header b {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.logs-view-element .header .right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.logs-view-element .log .circle {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.billing-options-element .current-card {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
@ -2043,13 +2067,13 @@ p.editable:hover i {
|
|||
|
||||
/** D3 tooltip styling */
|
||||
|
||||
.d3-tip {
|
||||
line-height: 1;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
max-width: 500px;
|
||||
.d3-tip, .nvtooltip {
|
||||
line-height: 1 !important;
|
||||
padding: 12px !important;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
color: #fff !important;
|
||||
border-radius: 2px !important;
|
||||
max-width: 500px !important;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
|
|
50
static/directives/logs-view.html
Normal file
50
static/directives/logs-view.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
<div class="logs-view-element">
|
||||
<div ng-show="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||
</div>
|
||||
<div ng-show="!loading">
|
||||
<div class="container header">
|
||||
<b>Usage logs for the last seven days</b>
|
||||
<span class="right">
|
||||
<button class="btn btn-default btn-toggle" ng-class="chartVisible ? 'active' : ''" ng-click="toggleChart()">
|
||||
Show Chart
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">
|
||||
<svg style="width: 800px; height: 500px;"></svg>
|
||||
</div>
|
||||
|
||||
<div class="side-controls">
|
||||
<div class="result-count">
|
||||
Showing {{(logs | visibleLogFilter:kindsAllowed | filter:search | limitTo:150).length}} of
|
||||
{{(logs | visibleLogFilter:kindsAllowed | filter:search).length}} matching logs
|
||||
</div>
|
||||
<div class="filter-input">
|
||||
<input id="log-filter" class="form-control" placeholder="Filter Logs" type="text" ng-model="search.$">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Description</th>
|
||||
<th>Date/Time</th>
|
||||
<th>User</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr class="log" ng-repeat="log in (logs | visibleLogFilter:kindsAllowed | filter:search | limitTo:150)">
|
||||
<td>
|
||||
<span class="circle" style="{{ 'background: ' + getColor(log.kind) }}"></span>
|
||||
<span ng-bind-html="getDescription(log)"></span>
|
||||
</td>
|
||||
<td>{{ log.datetime }}</td>
|
||||
<td>
|
||||
<span class="entity-reference" name="log.performer.username" isrobot="log.performer.is_robot"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
124
static/js/app.js
124
static/js/app.js
|
@ -650,6 +650,130 @@ quayApp.directive('dockerAuthDialog', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.filter('visibleLogFilter', function () {
|
||||
return function (logs, allowed) {
|
||||
if (!allowed) {
|
||||
return logs;
|
||||
}
|
||||
|
||||
var filtered = [];
|
||||
angular.forEach(logs, function (log) {
|
||||
if (allowed[log.kind]) {
|
||||
filtered.push(log);
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('logsView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/logs-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user',
|
||||
'visible': '=visible'
|
||||
},
|
||||
controller: function($scope, $element, $sce, Restangular) {
|
||||
$scope.loading = true;
|
||||
$scope.logs = null;
|
||||
$scope.kindsAllowed = null;
|
||||
$scope.chartVisible = true;
|
||||
|
||||
var logKinds = {
|
||||
'account_change_plan': 'Change plan',
|
||||
'account_change_cc': 'Update credit card',
|
||||
'account_change_password': 'Change password',
|
||||
'account_convert': 'Convert account to organization',
|
||||
'create_robot': 'Create Robot Account',
|
||||
'delete_robot': 'Delete Robot Account',
|
||||
'create_repo': 'Create Repository',
|
||||
'push_repo': 'Push to repository',
|
||||
'pull_repo': 'Pull repository',
|
||||
'delete_repo': 'Delete repository',
|
||||
'add_repo_permission': 'Add user permission to repository',
|
||||
'change_repo_permission': 'Change repository permission',
|
||||
'delete_repo_permission': 'Remove user permission from repository',
|
||||
'change_repo_visibility': 'Change repository visibility',
|
||||
'add_repo_accesstoken': 'Create access token',
|
||||
'delete_repo_accesstoken': 'Delete access token',
|
||||
'add_repo_webhook': 'Add webhook',
|
||||
'delete_repo_webhook': 'Delete webhook',
|
||||
'set_repo_description': 'Change repository description',
|
||||
'build_dockerfile': 'Build image from Dockerfile',
|
||||
'org_create_team': 'Create team',
|
||||
'org_delete_team': 'Delete team',
|
||||
'org_add_team_member': 'Add team member',
|
||||
'org_remove_team_member': 'Remove team member',
|
||||
'org_set_team_description': 'Change team description',
|
||||
'org_set_team_role': 'Change team permission'
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.visible || (!$scope.organization && !$scope.user)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.logs) {
|
||||
return;
|
||||
}
|
||||
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'logs') :
|
||||
getRestUrl('user/logs');
|
||||
var loadLogs = Restangular.one(url);
|
||||
loadLogs.customGET().then(function(resp) {
|
||||
if (!$scope.chart) {
|
||||
$scope.chart = new LogUsageChart(resp.logs, logKinds);
|
||||
$scope.chart.draw('bar-chart');
|
||||
$($scope.chart).bind('filteringChanged', function(e) {
|
||||
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
|
||||
});
|
||||
}
|
||||
|
||||
$scope.logs = resp.logs;
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleChart = function() {
|
||||
$scope.chartVisible = !$scope.chartVisible;
|
||||
};
|
||||
|
||||
$scope.isVisible = function(allowed, kind) {
|
||||
return allowed == null || allowed.hasOwnProperty(kind);
|
||||
};
|
||||
|
||||
$scope.getColor = function(kind) {
|
||||
return $scope.chart.getColor(kind);
|
||||
};
|
||||
|
||||
$scope.getDescription = function(log) {
|
||||
var description = log.description;
|
||||
for (var key in log.metadata) {
|
||||
if (log.metadata.hasOwnProperty(key)) {
|
||||
var markedDown = getMarkedDown(log.metadata[key].toString());
|
||||
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
||||
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
|
||||
}
|
||||
}
|
||||
return $sce.trustAsHtml(description);
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
$scope.$watch('visible', update);
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
quayApp.directive('robotsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
|
|
@ -1154,7 +1154,12 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
|||
$scope.membersLoading = true;
|
||||
$scope.membersFound = null;
|
||||
$scope.invoiceLoading = true;
|
||||
$scope.logsShown = false;
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsShown = true;
|
||||
};
|
||||
|
||||
$scope.planChanged = function(plan) {
|
||||
$scope.hasPaidPlan = plan && plan.price > 0;
|
||||
};
|
||||
|
|
|
@ -1261,4 +1261,221 @@ RepositoryUsageChart.prototype.draw = function(container) {
|
|||
this.arc_ = arc;
|
||||
this.width_ = cw;
|
||||
this.drawInternal_();
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* A chart which displays the last seven days of actions in the account.
|
||||
*/
|
||||
function LogUsageChart(logData, titleMap) {
|
||||
this.logs_ = logData;
|
||||
this.titleMap_ = titleMap;
|
||||
this.colorScale_ = d3.scale.category20();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds the D3-representation of the data.
|
||||
*/
|
||||
LogUsageChart.prototype.buildData_ = function() {
|
||||
var parseDate = d3.time.format("%a, %d %b %Y %H:%M:%S GMT").parse
|
||||
|
||||
// Build entries for each kind of event that occurred, on each day. We have one
|
||||
// entry per {kind, day} pair.
|
||||
var map = {};
|
||||
var entries = [];
|
||||
for (var i = 0; i < this.logs_.length; ++i) {
|
||||
var log = this.logs_[i];
|
||||
var title = this.titleMap_[log.kind] || log.kind;
|
||||
var datetime = parseDate(log.datetime);
|
||||
var formatted = (datetime.getMonth() + 1) + '/' + datetime.getDate();
|
||||
var adjusted = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate());
|
||||
var key = title + '_' + formatted;
|
||||
var found = map[key];
|
||||
if (!found) {
|
||||
found = {
|
||||
'kind': log.kind,
|
||||
'title': title,
|
||||
'adjusted': adjusted,
|
||||
'formatted': datetime.getDate(),
|
||||
'count': 0
|
||||
};
|
||||
|
||||
map[key] = found;
|
||||
entries.push(found);
|
||||
}
|
||||
found['count']++;
|
||||
}
|
||||
|
||||
this.entries_ = map;
|
||||
|
||||
// Build the data itself. We create a single entry for each possible kind of data, and then add (x, y) pairs
|
||||
// for the number of times that kind of event occurred on a particular day.
|
||||
var dataArray = [];
|
||||
var dataMap = {};
|
||||
var dateMap = {};
|
||||
|
||||
for (var i = 0; i < entries.length; ++i) {
|
||||
var entry = entries[i];
|
||||
var key = entry.title;
|
||||
var found = dataMap[key];
|
||||
if (!found) {
|
||||
found = {'key': key, 'values': [], 'kind': entry.kind};
|
||||
dataMap[key] = found;
|
||||
dataArray.push(found);
|
||||
}
|
||||
|
||||
found.values.push({
|
||||
'x': entry.adjusted,
|
||||
'y': entry.count
|
||||
});
|
||||
|
||||
dateMap[entry.adjusted.toString()] = entry.adjusted;
|
||||
}
|
||||
|
||||
// Note: nvd3 has a bug that causes d3 to fail if there is not an entry for every single
|
||||
// kind on each day that has data. Therefore, we pad those days with 0-length entries for each
|
||||
// kind.
|
||||
for (var i = 0; i < dataArray.length; ++i) {
|
||||
var datum = dataArray[i];
|
||||
for (var sDate in dateMap) {
|
||||
if (!dateMap.hasOwnProperty(sDate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cDate = dateMap[sDate];
|
||||
var found = false;
|
||||
for (var j = 0; j < datum.values.length; ++j) {
|
||||
if (datum.values[j]['x'].getDate() == cDate.getDate()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
datum.values.push({
|
||||
'x': cDate,
|
||||
'y': 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
datum.values.sort(function(a, b) {
|
||||
return a['x'].getDate() - b['x'].getDate();
|
||||
});
|
||||
}
|
||||
|
||||
return this.data_ = dataArray;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Renders the tooltip when hovering over an element in the chart.
|
||||
*/
|
||||
LogUsageChart.prototype.renderTooltip_ = function(d, e) {
|
||||
var entry = this.entries_[d + '_' + e];
|
||||
if (!entry) {
|
||||
entry = {'count': 0};
|
||||
}
|
||||
|
||||
var s = entry.count == 1 ? '' : 's';
|
||||
return d + ' - ' + entry.count + ' time' + s + ' on ' + e;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the color used in the chart for log entries of the given
|
||||
* kind.
|
||||
*/
|
||||
LogUsageChart.prototype.getColor = function(kind) {
|
||||
var colors = this.colorScale_.range();
|
||||
var index = 0;
|
||||
for (var i = 0; i < this.data_.length; ++i) {
|
||||
var datum = this.data_[i];
|
||||
var key = this.titleMap_[kind] || kind;
|
||||
if (datum.key == key) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
|
||||
LogUsageChart.prototype.handleStateChange_ = function(e) {
|
||||
var allowed = {};
|
||||
var disabled = e.disabled;
|
||||
for (var i = 0; i < this.data_.length; ++i) {
|
||||
if (!disabled[i]) {
|
||||
allowed[this.data_[i].kind] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$(this).trigger({
|
||||
'type': 'filteringChanged',
|
||||
'allowed': allowed
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Draws the chart in the given container element.
|
||||
*/
|
||||
LogUsageChart.prototype.draw = function(container) {
|
||||
// Returns a date offset from the given date by "days" Days.
|
||||
var offsetDate = function(d, days) {
|
||||
var copy = new Date(d.getTime());
|
||||
copy.setDate(copy.getDate() + days);
|
||||
return copy;
|
||||
};
|
||||
|
||||
var that = this;
|
||||
var data = this.buildData_();
|
||||
nv.addGraph(function() {
|
||||
// Build the chart itself.
|
||||
var chart = nv.models.multiBarChart()
|
||||
.margin({top: 30, right: 30, bottom: 50, left: 30})
|
||||
.stacked(false)
|
||||
.staggerLabels(false)
|
||||
.tooltip(function(d, e) {
|
||||
return that.renderTooltip_(d, e);
|
||||
})
|
||||
.color(that.colorScale_.range())
|
||||
.groupSpacing(0.1);
|
||||
|
||||
chart.multibar.delay(0);
|
||||
|
||||
// Create the x-axis domain to encompass a week from today.
|
||||
var domain = [];
|
||||
var datetime = new Date();
|
||||
datetime = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate());
|
||||
for (var i = 7; i >= 0; --i) {
|
||||
domain.push(offsetDate(datetime, -1 * i));
|
||||
}
|
||||
|
||||
chart.xDomain(domain);
|
||||
|
||||
// Finish setting up the chart.
|
||||
chart.xAxis
|
||||
.tickFormat(d3.time.format("%m/%d"));
|
||||
|
||||
chart.yAxis
|
||||
.tickFormat(d3.format(',f'));
|
||||
|
||||
d3.select('#bar-chart svg')
|
||||
.datum(data)
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function(e) { window.console.log(e); });
|
||||
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
||||
|
||||
return chart;
|
||||
});
|
||||
};
|
9275
static/lib/d3.js
vendored
Normal file
9275
static/lib/d3.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
769
static/lib/nv.d3.css
Normal file
769
static/lib/nv.d3.css
Normal file
|
@ -0,0 +1,769 @@
|
|||
|
||||
/********************
|
||||
* HTML CSS
|
||||
*/
|
||||
|
||||
|
||||
.chartWrap {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/********************
|
||||
Box shadow and border radius styling
|
||||
*/
|
||||
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
|
||||
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
|
||||
-webkit-border-radius: 6px;
|
||||
-moz-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/********************
|
||||
* TOOLTIP CSS
|
||||
*/
|
||||
|
||||
.nvtooltip {
|
||||
position: absolute;
|
||||
background-color: rgba(255,255,255,1.0);
|
||||
padding: 1px;
|
||||
border: 1px solid rgba(0,0,0,.2);
|
||||
z-index: 10000;
|
||||
|
||||
font-family: Arial;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/*Give tooltips that old fade in transition by
|
||||
putting a "with-transitions" class on the container div.
|
||||
*/
|
||||
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
|
||||
transition: opacity 250ms linear;
|
||||
-moz-transition: opacity 250ms linear;
|
||||
-webkit-transition: opacity 250ms linear;
|
||||
|
||||
transition-delay: 250ms;
|
||||
-moz-transition-delay: 250ms;
|
||||
-webkit-transition-delay: 250ms;
|
||||
}
|
||||
|
||||
.nvtooltip.x-nvtooltip,
|
||||
.nvtooltip.y-nvtooltip {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.nvtooltip h3 {
|
||||
margin: 0;
|
||||
padding: 4px 14px;
|
||||
line-height: 18px;
|
||||
font-weight: normal;
|
||||
background-color: rgba(247,247,247,0.75);
|
||||
text-align: center;
|
||||
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
|
||||
-webkit-border-radius: 5px 5px 0 0;
|
||||
-moz-border-radius: 5px 5px 0 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.nvtooltip p {
|
||||
margin: 0;
|
||||
padding: 5px 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nvtooltip span {
|
||||
display: inline-block;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.nvtooltip table {
|
||||
margin: 6px;
|
||||
border-spacing:0;
|
||||
}
|
||||
|
||||
|
||||
.nvtooltip table td {
|
||||
padding: 2px 9px 2px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nvtooltip table td.key {
|
||||
font-weight:normal;
|
||||
}
|
||||
.nvtooltip table td.value {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvtooltip table tr.highlight td {
|
||||
padding: 1px 9px 1px 0;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.nvtooltip table td.legend-color-guide div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nvtooltip .footer {
|
||||
padding: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.nvtooltip-pending-removal {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/********************
|
||||
* SVG CSS
|
||||
*/
|
||||
|
||||
|
||||
svg {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* Trying to get SVG to act like a greedy block in all browsers */
|
||||
display: block;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
|
||||
svg text {
|
||||
font: normal 12px Arial;
|
||||
}
|
||||
|
||||
svg .title {
|
||||
font: bold 14px Arial;
|
||||
}
|
||||
|
||||
.nvd3 .nv-background {
|
||||
fill: white;
|
||||
fill-opacity: 0;
|
||||
/*
|
||||
pointer-events: none;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3.nv-noData {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Brush
|
||||
*/
|
||||
|
||||
.nv-brush .extent {
|
||||
fill-opacity: .125;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Legend
|
||||
*/
|
||||
|
||||
.nvd3 .nv-legend .nv-series {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nvd3 .nv-legend .disabled circle {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Axes
|
||||
*/
|
||||
.nvd3 .nv-axis {
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-opacity: .75;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis path.domain {
|
||||
stroke-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis.nv-x path.domain {
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis line {
|
||||
fill: none;
|
||||
stroke: #e5e5e5;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis .zero line,
|
||||
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
|
||||
stroke-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis .nv-axisMaxMin text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvd3 .x .nv-axis .nv-axisMaxMin text,
|
||||
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
|
||||
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
|
||||
text-anchor: middle
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Brush
|
||||
*/
|
||||
|
||||
.nv-brush .resize path {
|
||||
fill: #eee;
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Bars
|
||||
*/
|
||||
|
||||
.nvd3 .nv-bars .negative rect {
|
||||
zfill: brown;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars rect {
|
||||
zfill: steelblue;
|
||||
fill-opacity: .75;
|
||||
|
||||
transition: fill-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars rect.hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars .hover rect {
|
||||
fill: lightblue;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars text {
|
||||
fill: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars .hover text {
|
||||
fill: rgba(0,0,0,1);
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Bars
|
||||
*/
|
||||
|
||||
.nvd3 .nv-multibar .nv-groups rect,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups rect,
|
||||
.nvd3 .nv-discretebar .nv-groups rect {
|
||||
stroke-opacity: 0;
|
||||
|
||||
transition: fill-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3 .nv-multibar .nv-groups rect:hover,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
|
||||
.nvd3 .nv-discretebar .nv-groups rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-discretebar .nv-groups text,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups text {
|
||||
font-weight: bold;
|
||||
fill: rgba(0,0,0,1);
|
||||
stroke: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
/***********
|
||||
* Pie Chart
|
||||
*/
|
||||
|
||||
.nvd3.nv-pie path {
|
||||
stroke-opacity: 0;
|
||||
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .nv-slice text {
|
||||
stroke: #000;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-pie path {
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .hover path {
|
||||
fill-opacity: .7;
|
||||
}
|
||||
.nvd3.nv-pie .nv-label {
|
||||
pointer-events: none;
|
||||
}
|
||||
.nvd3.nv-pie .nv-label rect {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
/**********
|
||||
* Lines
|
||||
*/
|
||||
|
||||
.nvd3 .nv-groups path.nv-line {
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
/*
|
||||
stroke-linecap: round;
|
||||
shape-rendering: geometricPrecision;
|
||||
|
||||
transition: stroke-width 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear;
|
||||
|
||||
transition-delay: 250ms
|
||||
-moz-transition-delay: 250ms;
|
||||
-webkit-transition-delay: 250ms;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3 .nv-groups path.nv-line.nv-thin-line {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .nv-groups path.nv-area {
|
||||
stroke: none;
|
||||
/*
|
||||
stroke-linecap: round;
|
||||
shape-rendering: geometricPrecision;
|
||||
|
||||
stroke-width: 2.5px;
|
||||
transition: stroke-width 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear;
|
||||
|
||||
transition-delay: 250ms
|
||||
-moz-transition-delay: 250ms;
|
||||
-webkit-transition-delay: 250ms;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3 .nv-line.hover path {
|
||||
stroke-width: 6px;
|
||||
}
|
||||
|
||||
/*
|
||||
.nvd3.scatter .groups .point {
|
||||
fill-opacity: 0.1;
|
||||
stroke-opacity: 0.1;
|
||||
}
|
||||
*/
|
||||
|
||||
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
|
||||
fill-opacity: .5 !important;
|
||||
stroke-opacity: .5 !important;
|
||||
}
|
||||
|
||||
|
||||
.with-transitions .nvd3 .nv-groups .nv-point {
|
||||
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-scatter .nv-groups .nv-point.hover,
|
||||
.nvd3 .nv-groups .nv-point.hover {
|
||||
stroke-width: 7px;
|
||||
fill-opacity: .95 !important;
|
||||
stroke-opacity: .95 !important;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .nv-point-paths path {
|
||||
stroke: #aaa;
|
||||
stroke-opacity: 0;
|
||||
fill: #eee;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nvd3 .nv-indexLine {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Distribution
|
||||
*/
|
||||
|
||||
.nvd3 .nv-distribution {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Scatter
|
||||
*/
|
||||
|
||||
/* **Attempting to remove this for useVoronoi(false), need to see if it's required anywhere
|
||||
.nvd3 .nv-groups .nv-point {
|
||||
pointer-events: none;
|
||||
}
|
||||
*/
|
||||
|
||||
.nvd3 .nv-groups .nv-point.hover {
|
||||
stroke-width: 20px;
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.nvd3 .nv-scatter .nv-point.hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
.nv-group.hover .nv-point {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/**********
|
||||
* Stacked Area
|
||||
*/
|
||||
|
||||
.nvd3.nv-stackedarea path.nv-area {
|
||||
fill-opacity: .7;
|
||||
/*
|
||||
stroke-opacity: .65;
|
||||
fill-opacity: 1;
|
||||
*/
|
||||
stroke-opacity: 0;
|
||||
|
||||
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
/*
|
||||
transition-delay: 500ms;
|
||||
-moz-transition-delay: 500ms;
|
||||
-webkit-transition-delay: 500ms;
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-stackedarea path.nv-area.hover {
|
||||
fill-opacity: .9;
|
||||
/*
|
||||
stroke-opacity: .85;
|
||||
*/
|
||||
}
|
||||
/*
|
||||
.d3stackedarea .groups path {
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
.nvd3.nv-stackedarea .nv-groups .nv-point {
|
||||
stroke-opacity: 0;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
.nvd3.nv-stackedarea .nv-groups .nv-point.hover {
|
||||
stroke-width: 20px;
|
||||
stroke-opacity: .75;
|
||||
fill-opacity: 1;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Line Plus Bar
|
||||
*/
|
||||
|
||||
.nvd3.nv-linePlusBar .nv-bar rect {
|
||||
fill-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3.nv-linePlusBar .nv-bar rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Bullet
|
||||
*/
|
||||
|
||||
.nvd3.nv-bullet { font: 10px sans-serif; }
|
||||
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
|
||||
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
|
||||
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
|
||||
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
|
||||
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
|
||||
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
|
||||
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
|
||||
|
||||
|
||||
.nvd3.nv-bullet .nv-range {
|
||||
fill: #bababa;
|
||||
fill-opacity: .4;
|
||||
}
|
||||
.nvd3.nv-bullet .nv-range:hover {
|
||||
fill-opacity: .7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Sparkline
|
||||
*/
|
||||
|
||||
.nvd3.nv-sparkline path {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus g.nv-hoverValue {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-hoverValue line {
|
||||
stroke: #333;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus,
|
||||
.nvd3.nv-sparklineplus g {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.nvd3 .nv-hoverArea {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-xValue,
|
||||
.nvd3.nv-sparklineplus .nv-yValue {
|
||||
/*
|
||||
stroke: #666;
|
||||
*/
|
||||
stroke-width: 0;
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-yValue {
|
||||
stroke: #f66;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-maxValue {
|
||||
stroke: #2ca02c;
|
||||
fill: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-minValue {
|
||||
stroke: #d62728;
|
||||
fill: #d62728;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-currentValue {
|
||||
/*
|
||||
stroke: #444;
|
||||
fill: #000;
|
||||
*/
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/**********
|
||||
* historical stock
|
||||
*/
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
|
||||
stroke: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
|
||||
stroke: #d62728;
|
||||
}
|
||||
|
||||
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvd3.nv-historicalStockChart .nv-dragTarget {
|
||||
fill-opacity: 0;
|
||||
stroke: none;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.nvd3 .nv-brush .extent {
|
||||
/*
|
||||
cursor: ew-resize !important;
|
||||
*/
|
||||
fill-opacity: 0 !important;
|
||||
}
|
||||
|
||||
.nvd3 .nv-brushBackground rect {
|
||||
stroke: #000;
|
||||
stroke-width: .4;
|
||||
fill: #fff;
|
||||
fill-opacity: .7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Indented Tree
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library
|
||||
*/
|
||||
.nvd3.nv-indentedtree .name {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree .clickable {
|
||||
color: #08C;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree span.clickable:hover {
|
||||
color: #005580;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
.nvd3.nv-indentedtree .nv-childrenCount {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree .nv-treeicon {
|
||||
cursor: pointer;
|
||||
/*
|
||||
cursor: n-resize;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree .nv-treeicon.nv-folded {
|
||||
cursor: pointer;
|
||||
/*
|
||||
cursor: s-resize;
|
||||
*/
|
||||
}
|
||||
|
||||
/**********
|
||||
* Parallel Coordinates
|
||||
*/
|
||||
|
||||
.nvd3 .background path {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-opacity: .4;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .foreground path {
|
||||
fill: none;
|
||||
stroke: steelblue;
|
||||
stroke-opacity: .7;
|
||||
}
|
||||
|
||||
.nvd3 .brush .extent {
|
||||
fill-opacity: .3;
|
||||
stroke: #fff;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .axis line, .axis path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .axis text {
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
|
||||
/****
|
||||
Interactive Layer
|
||||
*/
|
||||
.nvd3 .nv-interactiveGuideLine {
|
||||
pointer-events:none;
|
||||
}
|
||||
.nvd3 line.nv-guideline {
|
||||
stroke: #ccc;
|
||||
}
|
14346
static/lib/nv.d3.js
Normal file
14346
static/lib/nv.d3.js
Normal file
File diff suppressed because it is too large
Load diff
6
static/lib/nv.d3.min.js
vendored
Normal file
6
static/lib/nv.d3.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -14,6 +14,7 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
|
||||
|
@ -34,6 +35,11 @@
|
|||
<div class="robots-manager" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Logs tab -->
|
||||
<div id="logs" class="tab-pane">
|
||||
<div class="logs-view" organization="organization" visible="logsShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Options tab -->
|
||||
<div id="billingoptions" class="tab-pane">
|
||||
<div class="billing-options" organization="organization"></div>
|
||||
|
|
Reference in a new issue