Make new frontend deployable.
* Get rid of old client and its grunt build. * Make `yarn run dist` bundle a working version with new frontend. * Make sure to handle history mode URLs on server side.
This commit is contained in:
parent
4035c22aca
commit
e48c2861c6
810 changed files with 109 additions and 348223 deletions
3
.bowerrc
3
.bowerrc
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"directory": "app/bower_components"
|
||||
}
|
||||
24
.jshintrc
24
.jshintrc
|
|
@ -1,24 +0,0 @@
|
|||
{
|
||||
"node": true,
|
||||
"browser": true,
|
||||
"esnext": true,
|
||||
"bitwise": true,
|
||||
"camelcase": true,
|
||||
"curly": true,
|
||||
"eqeqeq": true,
|
||||
"immed": true,
|
||||
"indent": 4,
|
||||
"latedef": true,
|
||||
"newcap": true,
|
||||
"noarg": true,
|
||||
"quotmark": "single",
|
||||
"regexp": true,
|
||||
"undef": true,
|
||||
"unused": true,
|
||||
"strict": true,
|
||||
"trailing": true,
|
||||
"smarttabs": true,
|
||||
"globals": {
|
||||
"angular": false
|
||||
}
|
||||
}
|
||||
399
Gruntfile.js
399
Gruntfile.js
|
|
@ -1,399 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = function (grunt) {
|
||||
|
||||
// Load grunt tasks automatically
|
||||
require('load-grunt-tasks')(grunt);
|
||||
|
||||
// Time how long tasks take. Can help when optimizing build times
|
||||
require('time-grunt')(grunt);
|
||||
|
||||
var serveStatic = require('serve-static');
|
||||
|
||||
// Define the configuration for all the tasks
|
||||
grunt.initConfig({
|
||||
|
||||
// Project settings
|
||||
yeoman: {
|
||||
// configurable paths
|
||||
app: require('./bower.json').appPath || 'app',
|
||||
dist: 'dist'
|
||||
},
|
||||
|
||||
// Watches files for changes and runs tasks based on the changed files
|
||||
watch: {
|
||||
bower: {
|
||||
files: ['bower.json'],
|
||||
tasks: ['wiredep']
|
||||
},
|
||||
js: {
|
||||
files: ['<%= yeoman.app %>/scripts/{,**/}*.js', 'shared/{,**/}*.js'],
|
||||
tasks: ['newer:jshint:all'],
|
||||
options: {
|
||||
livereload: true
|
||||
}
|
||||
},
|
||||
compass: {
|
||||
files: ['<%= yeoman.app %>/styles/{,**/}*.{scss,sass}'],
|
||||
tasks: ['compass:server']
|
||||
},
|
||||
gruntfile: {
|
||||
files: ['Gruntfile.js']
|
||||
},
|
||||
livereload: {
|
||||
options: {
|
||||
livereload: '<%= connect.options.livereload %>'
|
||||
},
|
||||
files: [
|
||||
'<%= yeoman.app %>/{,**/}*.html',
|
||||
'.tmp/styles/{,**/}*.css'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// The actual grunt server settings
|
||||
connect: {
|
||||
options: {
|
||||
port: 9000,
|
||||
// Change this to '0.0.0.0' to access the server from outside.
|
||||
hostname: 'localhost',
|
||||
livereload: 35729,
|
||||
middleware: function (connect, options) {
|
||||
if (!Array.isArray(options.base)) {
|
||||
options.base = [options.base];
|
||||
}
|
||||
|
||||
// Setup the proxy
|
||||
var middlewares = [require('grunt-connect-proxy/lib/utils').proxyRequest];
|
||||
|
||||
// Serve static files.
|
||||
options.base.forEach(function(base) {
|
||||
middlewares.push(serveStatic(base));
|
||||
});
|
||||
|
||||
// Make directory browse-able.
|
||||
var directory = options.directory || options.base[options.base.length - 1];
|
||||
middlewares.push(serveStatic(directory));
|
||||
|
||||
return middlewares;
|
||||
}
|
||||
},
|
||||
proxies: [{
|
||||
context: '/api/',
|
||||
host: '127.0.0.1',
|
||||
port: 8080
|
||||
}, {
|
||||
context: '/config.js',
|
||||
host: '127.0.0.1',
|
||||
port: 8080
|
||||
}],
|
||||
livereload: {
|
||||
options: {
|
||||
open: true,
|
||||
base: [
|
||||
'.tmp',
|
||||
'<%= yeoman.app %>'
|
||||
]
|
||||
}
|
||||
},
|
||||
dist: {
|
||||
options: {
|
||||
base: '<%= yeoman.dist %>'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Make sure code styles are up to par and there are no obvious mistakes
|
||||
jshint: {
|
||||
options: {
|
||||
jshintrc: '.jshintrc',
|
||||
reporter: require('jshint-stylish')
|
||||
},
|
||||
all: [
|
||||
'Gruntfile.js',
|
||||
'<%= yeoman.app %>/scripts/{,**/}*.js',
|
||||
'shared/{,**/}*.js',
|
||||
'server/{,**/}*.js',
|
||||
'!server/templates/{,**/}*.template'
|
||||
]
|
||||
},
|
||||
|
||||
// Empties folders to start fresh
|
||||
clean: {
|
||||
dist: {
|
||||
files: [
|
||||
{
|
||||
dot: true,
|
||||
src: [
|
||||
'.tmp',
|
||||
'<%= yeoman.dist %>/*',
|
||||
'!<%= yeoman.dist %>/.git*'
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
server: '.tmp'
|
||||
},
|
||||
|
||||
wiredep: {
|
||||
task: {
|
||||
src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
|
||||
ignorePath: '<%= yeoman.app %>/bower_components/'
|
||||
}
|
||||
},
|
||||
|
||||
// Compiles Sass to CSS and generates necessary files if requested
|
||||
compass: {
|
||||
options: {
|
||||
sassDir: '<%= yeoman.app %>/styles',
|
||||
cssDir: '.tmp/styles',
|
||||
generatedImagesDir: '.tmp/images/generated',
|
||||
javascriptsDir: '<%= yeoman.app %>/scripts',
|
||||
fontsDir: '<%= yeoman.app %>/fonts',
|
||||
importPath: '<%= yeoman.app %>/bower_components',
|
||||
httpImagesPath: '/images',
|
||||
httpGeneratedImagesPath: '/images/generated',
|
||||
httpFontsPath: '/fonts',
|
||||
relativeAssets: false,
|
||||
assetCacheBuster: false,
|
||||
raw: 'Sass::Script::Number.precision = 10\n'
|
||||
},
|
||||
dist: {
|
||||
options: {
|
||||
generatedImagesDir: '<%= yeoman.dist %>/client/images/generated'
|
||||
}
|
||||
},
|
||||
server: {
|
||||
options: {
|
||||
debugInfo: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
html2js: {
|
||||
options: {
|
||||
base: 'app',
|
||||
htmlmin: {
|
||||
collapseBooleanAttributes: true,
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true,
|
||||
removeComments: true,
|
||||
removeEmptyAttributes: true,
|
||||
removeRedundantAttributes: true,
|
||||
removeScriptTypeAttributes: true,
|
||||
removeStyleLinkTypeAttributes: true
|
||||
}
|
||||
},
|
||||
main: {
|
||||
src: ['<%= yeoman.app %>/views/{*,**/*}.html'],
|
||||
dest: '.tmp/scripts/templates.js'
|
||||
}
|
||||
},
|
||||
|
||||
// Renames files for browser caching purposes
|
||||
rev: {
|
||||
dist: {
|
||||
files: {
|
||||
src: [
|
||||
'<%= yeoman.dist %>/client/scripts/{,*/}*.js',
|
||||
'<%= yeoman.dist %>/client/styles/{,*/}*.css'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Reads HTML for usemin blocks to enable smart builds that automatically
|
||||
// concat, minify and revision files. Creates configurations in memory so
|
||||
// additional tasks can operate on them
|
||||
useminPrepare: {
|
||||
html: '<%= yeoman.app %>/index.html',
|
||||
options: {
|
||||
dest: '<%= yeoman.dist %>/client',
|
||||
flow: {
|
||||
html: {
|
||||
steps: {
|
||||
js: ['concat', 'uglifyjs'],
|
||||
css: ['cssmin']
|
||||
},
|
||||
post: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Performs rewrites based on rev and the useminPrepare configuration
|
||||
usemin: {
|
||||
html: ['<%= yeoman.dist %>/client/{,*/}*.html'],
|
||||
css: ['<%= yeoman.dist %>/client/styles/{,*/}*.css'],
|
||||
options: {
|
||||
assetsDirs: ['<%= yeoman.dist %>/client']
|
||||
}
|
||||
},
|
||||
|
||||
// The following *-min tasks produce minified files in the dist folder
|
||||
cssmin: {
|
||||
options: {
|
||||
root: '<%= yeoman.app %>'
|
||||
}
|
||||
},
|
||||
|
||||
htmlmin: {
|
||||
dist: {
|
||||
options: {
|
||||
collapseWhitespace: true,
|
||||
collapseBooleanAttributes: true,
|
||||
removeCommentsFromCDATA: true,
|
||||
removeOptionalTags: true
|
||||
},
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
cwd: '<%= yeoman.dist %>/client',
|
||||
src: ['*.html', 'views/{,*/}*.html'],
|
||||
dest: '<%= yeoman.dist %>/client'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
uglify: {
|
||||
options: {
|
||||
mangle: false
|
||||
}
|
||||
},
|
||||
|
||||
// Copies remaining files to places other tasks can use
|
||||
copy: {
|
||||
dist: {
|
||||
options: {
|
||||
mode: true
|
||||
},
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
dot: true,
|
||||
cwd: '<%= yeoman.app %>',
|
||||
dest: '<%= yeoman.dist %>/client',
|
||||
src: [
|
||||
'*.{ico,png,txt}',
|
||||
'.htaccess',
|
||||
'*.html',
|
||||
'views/{,**/}*.html',
|
||||
'images/{,**/}*.{webp}',
|
||||
'fonts/*'
|
||||
]
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: '<%= yeoman.app %>/bower_components/leaflet-dist/images',
|
||||
dest: '<%= yeoman.dist %>/client/styles/images',
|
||||
src: ['*.png']
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: '.tmp/images',
|
||||
dest: '<%= yeoman.dist %>/client/images',
|
||||
src: ['generated/*']
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: 'shared',
|
||||
dest: '<%= yeoman.dist %>/shared',
|
||||
src: ['{,**/}*.js']
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: '.',
|
||||
dest: '<%= yeoman.dist %>/',
|
||||
src: [
|
||||
'config.json.example',
|
||||
'LICENSE',
|
||||
'README.md'
|
||||
]
|
||||
},
|
||||
{
|
||||
expand: true,
|
||||
cwd: 'admin',
|
||||
dest: '<%= yeoman.dist %>/admin',
|
||||
src: ['{,**}/*.*']
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {
|
||||
expand: true,
|
||||
cwd: '<%= yeoman.app %>/styles',
|
||||
dest: '.tmp/styles/',
|
||||
src: '{,**/}*.css'
|
||||
}
|
||||
},
|
||||
|
||||
// Toggle private flag in package.json when copying to dist/.
|
||||
replace: {
|
||||
dist: {
|
||||
options: {
|
||||
patterns: [{
|
||||
match: /"private": true/g,
|
||||
replacement: '"private": false'
|
||||
}]
|
||||
},
|
||||
files: [{
|
||||
src: ['package.json'],
|
||||
dest: 'dist/',
|
||||
cwd: '.'
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
// Run some tasks in parallel to speed up the build process
|
||||
concurrent: {
|
||||
server: [
|
||||
'compass:server'
|
||||
],
|
||||
dist: [
|
||||
'compass:dist'
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
grunt.registerTask('serve', function (target) {
|
||||
if (target === 'dist') {
|
||||
return grunt.task.run(['build', 'connect:dist:keepalive']);
|
||||
}
|
||||
|
||||
return grunt.task.run([
|
||||
'clean:server',
|
||||
'wiredep',
|
||||
'concurrent:server',
|
||||
'configureProxies',
|
||||
'connect:livereload',
|
||||
'watch'
|
||||
]);
|
||||
});
|
||||
|
||||
grunt.registerTask('server', function (target) {
|
||||
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
|
||||
grunt.task.run(['serve:' + target]);
|
||||
});
|
||||
|
||||
grunt.registerTask('build', [
|
||||
'clean:dist',
|
||||
'wiredep',
|
||||
'html2js',
|
||||
'useminPrepare',
|
||||
'concurrent:dist',
|
||||
'concat',
|
||||
'copy:dist',
|
||||
'replace:dist',
|
||||
'cssmin',
|
||||
'uglify',
|
||||
'rev',
|
||||
'usemin',
|
||||
'htmlmin'
|
||||
]);
|
||||
|
||||
grunt.registerTask('default', [
|
||||
'newer:jshint',
|
||||
'build'
|
||||
]);
|
||||
};
|
||||
|
|
@ -346,8 +346,7 @@ fastd-Key und die MAC-Adresse angeben.
|
|||
### Abhängigkeiten
|
||||
|
||||
* node.js + yarn
|
||||
* compass (Installation z. B. via Ruby's `gem`)
|
||||
* ggf. bower (Installation z. B. via `yarn install bower`)
|
||||
* sass
|
||||
|
||||
|
||||
### Build
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@
|
|||
|
||||
## Short term
|
||||
|
||||
* Split into seperate packages for server and frontend.
|
||||
* Make admin panel part of new frontend package.
|
||||
* Get rid of grunt.
|
||||
* Use generated type guards.
|
||||
|
||||
## Mid term
|
||||
|
|
@ -22,5 +20,4 @@
|
|||
|
||||
* Rewrite the admin interface (used lib is unmaintained).
|
||||
* Rewrite the client in typescript (+ vue?).
|
||||
* Replace the grunt build system.
|
||||
* Decentralize node data.
|
||||
|
|
|
|||
1
admin/css/ng-admin.min.css
vendored
1
admin/css/ng-admin.min.css
vendored
|
|
@ -1 +0,0 @@
|
|||
../../node_modules/ng-admin/build/ng-admin.min.css
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../node_modules/ng-admin/build/ng-admin.min.css.map
|
||||
101
admin/index.html
101
admin/index.html
|
|
@ -1,101 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en" ng-app="ffffngAdmin">
|
||||
<head>
|
||||
<title>Knotenverwaltung - Admin-Panel</title>
|
||||
<link rel="stylesheet" href="css/ng-admin.min.css">
|
||||
|
||||
<style>
|
||||
.task-enabled {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.task-disabled {
|
||||
color: #eea236;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.task-state-running {
|
||||
color: #204d74;
|
||||
}
|
||||
|
||||
.task-state-failed {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.task-message.task-result-okay {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.task-message.task-result-warning {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.task-schedule {
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mails-pending {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.mails-failed {
|
||||
color: #eea236;
|
||||
}
|
||||
|
||||
.mails-failed-max {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.monitoring-active {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.monitoring-confirmation-pending {
|
||||
color: lightcoral;
|
||||
}
|
||||
|
||||
.monitoring-disabled {
|
||||
color: lightgrey;
|
||||
}
|
||||
|
||||
.node-online, .monitoring-state-online {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.node-offline, .monitoring-state-offline {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.vpn-key-set, .coords-set {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.vpn-key-unset, .coords-unset {
|
||||
color: lightgrey;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div ui-view="ng-admin"></div>
|
||||
|
||||
<script src="js/moment-with-locales.min.js"></script>
|
||||
<script src="js/ng-admin.min.js"></script>
|
||||
|
||||
<script src="js/app.js"></script>
|
||||
|
||||
<script src="js/validation/constraints.js"></script>
|
||||
|
||||
<script src="js/views/version.js"></script>
|
||||
<script src="js/views/dashboardStats.js"></script>
|
||||
<script src="js/views/mailActionButton.js"></script>
|
||||
<script src="js/views/taskActionButton.js"></script>
|
||||
|
||||
<script src="js/main.js"></script>
|
||||
<script src="../../config.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('ffffng', []);
|
||||
angular.module('ffffngAdmin', ['ng-admin', 'ffffng']);
|
||||
474
admin/js/main.js
474
admin/js/main.js
|
|
@ -1,474 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, RestangularProvider, Constraints, config) {
|
||||
RestangularProvider.addFullRequestInterceptor(function(element, operation, what, url, headers, params) {
|
||||
if (operation === 'getList') {
|
||||
if (params._filters) {
|
||||
// flatten filter query params
|
||||
|
||||
for (var filter in params._filters) {
|
||||
params[filter] = params._filters[filter];
|
||||
}
|
||||
delete params._filters;
|
||||
}
|
||||
}
|
||||
return { params: params };
|
||||
});
|
||||
|
||||
function nullable(value) {
|
||||
return value ? value : 'N/A';
|
||||
}
|
||||
|
||||
function formatMoment(unix) {
|
||||
return unix ? moment.unix(unix).fromNow() : 'N/A';
|
||||
}
|
||||
|
||||
function formatDuration(duration) {
|
||||
return typeof duration === 'number' ? moment.duration(duration).humanize() : 'N/A';
|
||||
}
|
||||
|
||||
function nodeConstraint(field) {
|
||||
var constraint = Constraints.node[field];
|
||||
var result = {
|
||||
required: !constraint.optional
|
||||
};
|
||||
if (constraint.type === 'string') {
|
||||
result.pattern = constraint.regex;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var nga = NgAdminConfigurationProvider;
|
||||
|
||||
var title = 'Knotenverwaltung - ' + config.community.name + ' - Admin-Panel';
|
||||
var admin = nga.application(title);
|
||||
document.title = title;
|
||||
|
||||
var pathPrefix = config.rootPath === '/' ? '' : config.rootPath;
|
||||
|
||||
var siteChoices = [];
|
||||
for (var i = 0; i < config.community.sites.length; i++) {
|
||||
var site = config.community.sites[i];
|
||||
siteChoices.push({
|
||||
label: site,
|
||||
value: site
|
||||
});
|
||||
}
|
||||
|
||||
var domainChoices = [];
|
||||
for (var i = 0; i < config.community.domains.length; i++) {
|
||||
var domain = config.community.domains[i];
|
||||
domainChoices.push({
|
||||
label: domain,
|
||||
value: domain
|
||||
});
|
||||
}
|
||||
|
||||
var header =
|
||||
'<div class="navbar-header">' +
|
||||
'<a class="navbar-brand" href="#" ng-click="appController.displayHome()">' +
|
||||
title + ' ' +
|
||||
'<small style="font-size: 0.7em;">(<fa-version></fa-version>)</small>' +
|
||||
'</a>' +
|
||||
'</div>';
|
||||
if (config.legal.imprintUrl) {
|
||||
header +=
|
||||
'<p class="navbar-text navbar-right">' +
|
||||
'<a href="' + config.legal.imprintUrl + '" target="_blank">' +
|
||||
'Imprint' +
|
||||
'</a>' +
|
||||
'</p>';
|
||||
}
|
||||
if (config.legal.privacyUrl) {
|
||||
header +=
|
||||
'<p class="navbar-text navbar-right">' +
|
||||
'<a href="' + config.legal.privacyUrl + '" target="_blank">' +
|
||||
'Privacy' +
|
||||
'</a>' +
|
||||
'</p>';
|
||||
}
|
||||
header +=
|
||||
'<p class="navbar-text navbar-right">' +
|
||||
'<a href="https://github.com/freifunkhamburg/ffffng/issues" target="_blank">' +
|
||||
'<i class="fa fa-bug" aria-hidden="true"></i> Report Error' +
|
||||
'</a>' +
|
||||
'</p>' +
|
||||
'<p class="navbar-text navbar-right">' +
|
||||
'<a href="https://github.com/freifunkhamburg/ffffng" target="_blank">' +
|
||||
'<i class="fa fa-code" aria-hidden="true"></i> Source Code' +
|
||||
'</a>' +
|
||||
'</p>' +
|
||||
'<p class="navbar-text navbar-right">' +
|
||||
'<a href="' + pathPrefix + '/" target="_blank">' +
|
||||
'<i class="fa fa-external-link" aria-hidden="true"></i> Frontend' +
|
||||
'</a>' +
|
||||
'</p>';
|
||||
|
||||
admin
|
||||
.header(header)
|
||||
.baseApiUrl(pathPrefix + '/internal/api/')
|
||||
.debug(true);
|
||||
|
||||
function nodeClasses(node) {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (node.values.onlineState) {
|
||||
case 'ONLINE':
|
||||
return 'node-online';
|
||||
|
||||
case 'OFFLINE':
|
||||
return 'node-offline';
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var nodes = nga.entity('nodes').label('Nodes').identifier(nga.field('token'));
|
||||
nodes
|
||||
.listView()
|
||||
.title('Nodes')
|
||||
.perPage(30)
|
||||
.sortDir('ASC')
|
||||
.sortField('hostname')
|
||||
.actions([])
|
||||
.batchActions([])
|
||||
.exportFields([])
|
||||
.fields([
|
||||
nga.field('hostname').cssClasses(nodeClasses),
|
||||
nga.field('nickname').cssClasses(nodeClasses),
|
||||
nga.field('email').cssClasses(nodeClasses),
|
||||
nga.field('token').cssClasses(nodeClasses),
|
||||
nga.field('mac').cssClasses(nodeClasses),
|
||||
nga.field('key').label('VPN').cssClasses(nodeClasses).template(function (node) {
|
||||
return node.values.key
|
||||
? '<i class="fa fa-lock vpn-key-set" aria-hidden="true" title="VPN key set"></i>'
|
||||
: '<i class="fa fa-times vpn-key-unset" aria-hidden="true" title="no VPN key"></i>';
|
||||
}),
|
||||
nga.field('site').map(nullable).cssClasses(nodeClasses),
|
||||
nga.field('domain').map(nullable).cssClasses(nodeClasses),
|
||||
nga.field('coords').label('GPS').cssClasses(nodeClasses).template(function (node) {
|
||||
return node.values.coords
|
||||
? '<i class="fa fa-map-marker coords-set" aria-hidden="true" title="coordinates set"></i>'
|
||||
: '<i class="fa fa-times coords-unset" aria-hidden="true" title="no coordinates"></i>';
|
||||
}),
|
||||
nga.field('onlineState').map(nullable).cssClasses(nodeClasses),
|
||||
nga.field('monitoringState').cssClasses(nodeClasses).template(function (node) {
|
||||
switch (node.values.monitoringState) {
|
||||
case 'active':
|
||||
return '<i class="fa fa-heartbeat monitoring-active" title="active"></i>';
|
||||
|
||||
case 'pending':
|
||||
return '<i class="fa fa-envelope monitoring-confirmation-pending" title="confirmation pending"></i>';
|
||||
|
||||
default:
|
||||
return '<i class="fa fa-times monitoring-disabled" title="disabled"></i>';
|
||||
}
|
||||
})
|
||||
])
|
||||
.filters([
|
||||
nga.field('q', 'template')
|
||||
.label('')
|
||||
.pinned(true)
|
||||
.template(
|
||||
'<div class="input-group">' +
|
||||
'<input type="text" ng-model="value" placeholder="Search" class="form-control"></input>' +
|
||||
'<span class="input-group-addon"><i class="fa fa-search"></i></span></div>'),
|
||||
nga.field('site', 'choice')
|
||||
.label('Site')
|
||||
.pinned(false)
|
||||
.choices(siteChoices),
|
||||
nga.field('domain', 'choice')
|
||||
.label('Domäne')
|
||||
.pinned(false)
|
||||
.choices(domainChoices),
|
||||
nga.field('hasKey', 'choice')
|
||||
.label('VPN key')
|
||||
.pinned(false)
|
||||
.choices([
|
||||
{ label: 'VPN key set', value: true },
|
||||
{ label: 'no VPN key', value: false }
|
||||
]),
|
||||
nga.field('hasCoords', 'choice')
|
||||
.label('GPS coordinates')
|
||||
.pinned(false)
|
||||
.choices([
|
||||
{ label: 'coordinates set', value: true },
|
||||
{ label: 'no coordinates', value: false }
|
||||
]),
|
||||
nga.field('onlineState', 'choice')
|
||||
.label('Online state')
|
||||
.pinned(false)
|
||||
.choices([
|
||||
{ label: 'online', value: 'ONLINE' },
|
||||
{ label: 'offline', value: 'OFFLINE' }
|
||||
]),
|
||||
nga.field('monitoringState', 'choice')
|
||||
.label('Monitoring')
|
||||
.pinned(false)
|
||||
.choices([
|
||||
{ label: 'pending', value: 'pending' },
|
||||
{ label: 'active', value: 'active' },
|
||||
{ label: 'disabled', value: 'disabled' }
|
||||
])
|
||||
])
|
||||
.actions(['<ma-filter-button filters="filters()" enabled-filters="enabledFilters" enable-filter="enableFilter()"></ma-filter-button>'])
|
||||
.listActions(
|
||||
'<ma-edit-button entry="entry" entity="entity" size="sm"></ma-edit-button> ' +
|
||||
'<ma-delete-button entry="entry" entity="entity" size="sm"></ma-delete-button> ' +
|
||||
'<form style="display: inline-block" action="' + pathPrefix + '/#/update" method="POST" target="_blank">' +
|
||||
'<input type="hidden" name="token" value="{{entry.values.token}}"/>' +
|
||||
'<button class="btn btn-primary btn-sm" type="submit"><i class="fa fa-external-link"></i> Open</button>' +
|
||||
'</form> ' +
|
||||
'<a class="btn btn-success btn-sm" href="' + config.map.mapUrl +
|
||||
'/#!v:m;n:{{entry.values.mapId}}" target="_blank"><i class="fa fa-map-o"></i> Map</a>'
|
||||
)
|
||||
;
|
||||
|
||||
nodes
|
||||
.editionView()
|
||||
.title('Edit node')
|
||||
.actions(['list', 'delete'])
|
||||
.addField(nga.field('token').editable(false))
|
||||
.addField(nga.field('hostname').label('Name').validation(nodeConstraint('hostname')))
|
||||
.addField(nga.field('key').label('Key').validation(nodeConstraint('key')))
|
||||
.addField(nga.field('mac').label('MAC').validation(nodeConstraint('mac')))
|
||||
.addField(nga.field('coords').label('GPS').validation(nodeConstraint('coords')))
|
||||
.addField(nga.field('nickname').validation(nodeConstraint('nickname')))
|
||||
.addField(nga.field('email').validation(nodeConstraint('email')))
|
||||
.addField(nga.field('monitoring', 'boolean').validation(nodeConstraint('monitoring')))
|
||||
.addField(nga.field('monitoringConfirmed').label('Monitoring confirmation').editable(false).map(
|
||||
function (monitoringConfirmed, node) {
|
||||
if (!node.monitoring) {
|
||||
return 'N/A';
|
||||
}
|
||||
|
||||
return monitoringConfirmed ? 'confirmed' : 'pending';
|
||||
}
|
||||
))
|
||||
;
|
||||
|
||||
admin.addEntity(nodes);
|
||||
|
||||
function monitoringStateClasses(monitoringState) {
|
||||
if (!monitoringState) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (monitoringState.values.state) {
|
||||
case 'ONLINE':
|
||||
return 'monitoring-state-online';
|
||||
|
||||
case 'OFFLINE':
|
||||
return 'monitoring-state-offline';
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var monitoringStates = nga.entity('monitoring').label('Monitoring');
|
||||
monitoringStates
|
||||
.listView()
|
||||
.title('Monitoring')
|
||||
.perPage(30)
|
||||
.sortDir('ASC')
|
||||
.sortField('id')
|
||||
.actions([])
|
||||
.batchActions([])
|
||||
.exportFields([])
|
||||
.fields([
|
||||
nga.field('id').cssClasses(monitoringStateClasses),
|
||||
nga.field('hostname').cssClasses(monitoringStateClasses),
|
||||
nga.field('mac').cssClasses(monitoringStateClasses),
|
||||
nga.field('site').map(nullable).cssClasses(monitoringStateClasses),
|
||||
nga.field('domain').map(nullable).cssClasses(monitoringStateClasses),
|
||||
nga.field('monitoring_state').cssClasses(monitoringStateClasses).template(function (monitoringState) {
|
||||
switch (monitoringState.values.monitoring_state) {
|
||||
case 'active':
|
||||
return '<i class="fa fa-heartbeat monitoring-active" title="active"></i>';
|
||||
|
||||
case 'pending':
|
||||
return '<i class="fa fa-envelope monitoring-confirmation-pending" title="confirmation pending"></i>';
|
||||
|
||||
default:
|
||||
return '<i class="fa fa-times monitoring-disabled" title="disabled"></i>';
|
||||
}
|
||||
}),
|
||||
nga.field('state').cssClasses(monitoringStateClasses),
|
||||
nga.field('last_seen').map(formatMoment).cssClasses(monitoringStateClasses),
|
||||
nga.field('import_timestamp').label('Imported').map(formatMoment).cssClasses(monitoringStateClasses),
|
||||
nga.field('last_status_mail_type').map(nullable).cssClasses(monitoringStateClasses),
|
||||
nga.field('last_status_mail_sent').map(formatMoment).cssClasses(monitoringStateClasses),
|
||||
nga.field('created_at').map(formatMoment).cssClasses(monitoringStateClasses),
|
||||
nga.field('modified_at').map(formatMoment).cssClasses(monitoringStateClasses)
|
||||
])
|
||||
.filters([
|
||||
nga.field('q')
|
||||
.label('')
|
||||
.pinned(true)
|
||||
.template(
|
||||
'<div class="input-group">' +
|
||||
'<input type="text" ng-model="value" placeholder="Search" class="form-control"></input>' +
|
||||
'<span class="input-group-addon"><i class="fa fa-search"></i></span></div>'),
|
||||
])
|
||||
.listActions(
|
||||
'<a class="btn btn-success btn-sm" href="' + config.map.mapUrl +
|
||||
'/#!v:m;n:{{entry.values.mapId}}" target="_blank"><i class="fa fa-map-o"></i> Map</a>'
|
||||
)
|
||||
;
|
||||
|
||||
admin.addEntity(monitoringStates);
|
||||
|
||||
function mailClasses(mail) {
|
||||
if (!mail) {
|
||||
return;
|
||||
}
|
||||
|
||||
var failures = mail.values.failures;
|
||||
|
||||
if (failures === 0) {
|
||||
return 'mails-pending';
|
||||
}
|
||||
|
||||
if (failures >= 5) {
|
||||
return 'mails-failed-max';
|
||||
}
|
||||
|
||||
return 'mails-failed';
|
||||
}
|
||||
|
||||
var mails = nga.entity('mails').label('Mail-Queue');
|
||||
mails
|
||||
.listView()
|
||||
.title('Mail-Queue')
|
||||
.perPage(30)
|
||||
.sortDir('ASC')
|
||||
.sortField('id')
|
||||
.actions([])
|
||||
.batchActions([])
|
||||
.exportFields([])
|
||||
.fields([
|
||||
nga.field('id').cssClasses(mailClasses),
|
||||
nga.field('failures').cssClasses(mailClasses),
|
||||
nga.field('sender').cssClasses(mailClasses),
|
||||
nga.field('recipient').cssClasses(mailClasses),
|
||||
nga.field('email').cssClasses(mailClasses),
|
||||
nga.field('created_at').map(formatMoment).cssClasses(mailClasses),
|
||||
nga.field('modified_at').map(formatMoment).cssClasses(mailClasses)
|
||||
])
|
||||
.filters([
|
||||
nga.field('q')
|
||||
.label('')
|
||||
.pinned(true)
|
||||
.template(
|
||||
'<div class="input-group">' +
|
||||
'<input type="text" ng-model="value" placeholder="Search" class="form-control"></input>' +
|
||||
'<span class="input-group-addon"><i class="fa fa-search"></i></span></div>'),
|
||||
])
|
||||
.listActions(
|
||||
'<fa-mail-action-button disabled="entry.values.failures === 0" action="reset" icon="refresh" label="Retry" mail="entry" button="success" label="run" size="sm"></fa-mail-action-button> ' +
|
||||
'<ma-delete-button entry="entry" entity="entity" size="sm"></ma-delete-button>'
|
||||
)
|
||||
;
|
||||
|
||||
admin.addEntity(mails);
|
||||
|
||||
function taskClasses(field) {
|
||||
return function(task) {
|
||||
if (!task) {
|
||||
return;
|
||||
}
|
||||
return 'task-' + field + ' ' +
|
||||
(task.values.enabled ? 'task-enabled' : 'task-disabled') + ' '
|
||||
+ 'task-state-' + task.values.state + ' '
|
||||
+ 'task-result-' + (task.values.result ? task.values.result : 'none');
|
||||
};
|
||||
}
|
||||
|
||||
var tasks = nga.entity('tasks').label('Background-Jobs');
|
||||
tasks
|
||||
.listView()
|
||||
.title('Background-Jobs')
|
||||
.perPage(30)
|
||||
.sortDir('ASC')
|
||||
.sortField('id')
|
||||
.actions([])
|
||||
.batchActions([])
|
||||
.exportFields([])
|
||||
.fields([
|
||||
nga.field('id').cssClasses(taskClasses('id')),
|
||||
nga.field('name').cssClasses(taskClasses('name')),
|
||||
nga.field('description').cssClasses(taskClasses('description')),
|
||||
nga.field('schedule').cssClasses(taskClasses('schedule')),
|
||||
nga.field('state').cssClasses(taskClasses('state')),
|
||||
nga.field('message').cssClasses(taskClasses('message')),
|
||||
nga.field('runningSince').map(formatMoment).cssClasses(taskClasses('runningSince')),
|
||||
nga.field('lastRunStarted').map(formatMoment).cssClasses(taskClasses('lastRunStarted')),
|
||||
nga.field('lastRunDuration').map(formatDuration).cssClasses(taskClasses('lastRunDuration'))
|
||||
])
|
||||
.filters([
|
||||
nga.field('q')
|
||||
.label('')
|
||||
.pinned(true)
|
||||
.template(
|
||||
'<div class="input-group">' +
|
||||
'<input type="text" ng-model="value" placeholder="Search" class="form-control"></input>' +
|
||||
'<span class="input-group-addon"><i class="fa fa-search"></i></span></div>'),
|
||||
])
|
||||
.listActions(
|
||||
'<fa-task-action-button action="run" task="entry" button="primary" label="run" size="sm"></fa-task-action-button> ' +
|
||||
'<fa-task-action-button ng-if="!entry.values.enabled" button="success" action="enable" icon="power-off" task="entry" label="enable" size="sm"></fa-task-action-button> ' +
|
||||
'<fa-task-action-button ng-if="entry.values.enabled" button="warning" action="disable" icon="power-off" task="entry" label="disable" size="sm"></fa-task-action-button>'
|
||||
)
|
||||
;
|
||||
|
||||
admin.addEntity(tasks);
|
||||
|
||||
admin.menu(
|
||||
nga.menu()
|
||||
.addChild(nga
|
||||
.menu()
|
||||
.template(
|
||||
'<a href="' + pathPrefix + '/internal/admin">' +
|
||||
'<span class="fa fa-dashboard"></span> Dashboard / Statistics' +
|
||||
'</a>'
|
||||
)
|
||||
)
|
||||
.addChild(nga
|
||||
.menu(nodes)
|
||||
.icon('<i class="fa fa-dot-circle-o"></i>')
|
||||
)
|
||||
.addChild(nga
|
||||
.menu(monitoringStates)
|
||||
.icon('<span class="fa fa-heartbeat"></span>')
|
||||
)
|
||||
.addChild(nga
|
||||
.menu(mails)
|
||||
.icon('<span class="fa fa-envelope"></span>')
|
||||
)
|
||||
.addChild(nga
|
||||
.menu(tasks)
|
||||
.icon('<span class="fa fa-cog"></span>')
|
||||
)
|
||||
);
|
||||
|
||||
admin.dashboard(nga.dashboard()
|
||||
.template(
|
||||
'<div class="row dashboard-starter"></div>' +
|
||||
'<fa-dashboard-stats></fa-dashboard-stats>' +
|
||||
|
||||
'<div class="row dashboard-content">' +
|
||||
'<div class="col-lg-6">' +
|
||||
'<div class="panel panel-default" ng-repeat="collection in dashboardController.collections | orderElement" ng-if="$even">' +
|
||||
'<ma-dashboard-panel collection="collection" entries="dashboardController.entries[collection.name()]"></ma-dashboard-panel>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)
|
||||
);
|
||||
|
||||
nga.configure(admin);
|
||||
});
|
||||
1
admin/js/moment-with-locales.min.js
vendored
1
admin/js/moment-with-locales.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
../../node_modules/moment/min/moment-with-locales.min.js
|
||||
1
admin/js/ng-admin.min.js
vendored
1
admin/js/ng-admin.min.js
vendored
|
|
@ -1 +0,0 @@
|
|||
../../node_modules/ng-admin/build/ng-admin.min.js
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../node_modules/ng-admin/build/ng-admin.min.js.map
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../shared/validation
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('ffffngAdmin')
|
||||
.directive('faDashboardStats', function ($http, $state, notification, config) {
|
||||
var pathPrefix = config.rootPath === '/' ? '' : config.rootPath;
|
||||
|
||||
var link = function (scope) {
|
||||
scope.stats = {};
|
||||
$http.get(pathPrefix + '/internal/api/statistics')
|
||||
.then(function (result) { scope.stats = result.data; })
|
||||
.catch(function (e) {
|
||||
notification.log('Error: ' + e.data, { addnCls: 'humane-flatty-error' });
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
'link': link,
|
||||
'restrict': 'E',
|
||||
'scope': {},
|
||||
|
||||
'templateUrl': 'views/dashboardStats.html'
|
||||
};
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('ffffngAdmin')
|
||||
.directive('faMailActionButton', function (Restangular, $state, notification) {
|
||||
var link = function (scope) {
|
||||
scope.label = scope.label || 'ACTION';
|
||||
scope.icon = scope.icon || 'envelope';
|
||||
scope.button = scope.button || 'default';
|
||||
|
||||
scope.perform = function () {
|
||||
var mail = scope.mail();
|
||||
|
||||
Restangular
|
||||
.one('mails/' + scope.action, mail.values.id).put()
|
||||
.then(function () { $state.reload() })
|
||||
.then(function () { notification.log('Done', { addnCls: 'humane-flatty-success' }); })
|
||||
.catch(function (e) {
|
||||
notification.log('Error: ' + e.data, { addnCls: 'humane-flatty-error' });
|
||||
console.error(e)
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
'link': link,
|
||||
'restrict': 'E',
|
||||
'scope': {
|
||||
'action': '@',
|
||||
'icon': '@',
|
||||
'mail': '&',
|
||||
'size': '@',
|
||||
'label': '@',
|
||||
'button': '@',
|
||||
'disabled': '='
|
||||
},
|
||||
|
||||
'template':
|
||||
'<button class="btn btn-{{ button }}" ng-disabled="disabled" ng-class="size ? \'btn-\' + size : \'\'" ng-click="perform()">' +
|
||||
'<span class="fa fa-{{ icon }}" aria-hidden="true"></span> <span class="hidden-xs">{{ label }}</span>' +
|
||||
'</button>'
|
||||
};
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('ffffngAdmin')
|
||||
.directive('faTaskActionButton', function (Restangular, $state, notification) {
|
||||
var link = function (scope) {
|
||||
scope.label = scope.label || 'ACTION';
|
||||
scope.icon = scope.icon || 'play';
|
||||
scope.button = scope.button || 'default';
|
||||
|
||||
scope.perform = function () {
|
||||
var task = scope.task();
|
||||
|
||||
Restangular
|
||||
.one('tasks/' + scope.action, task.values.id).put()
|
||||
.then(function () { $state.reload() })
|
||||
.then(function () { notification.log('Done', { addnCls: 'humane-flatty-success' }); })
|
||||
.catch(function (e) {
|
||||
notification.log('Error: ' + e.data, { addnCls: 'humane-flatty-error' });
|
||||
console.error(e)
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
'link': link,
|
||||
'restrict': 'E',
|
||||
'scope': {
|
||||
'action': '@',
|
||||
'icon': '@',
|
||||
'task': '&',
|
||||
'size': '@',
|
||||
'label': '@',
|
||||
'button': '@',
|
||||
'disabled': '='
|
||||
},
|
||||
|
||||
'template':
|
||||
'<button class="btn btn-{{ button }}" ng-disabled="disabled" ng-class="size ? \'btn-\' + size : \'\'" ng-click="perform()">' +
|
||||
'<span class="fa fa-{{ icon }}" aria-hidden="true"></span> <span class="hidden-xs">{{ label }}</span>' +
|
||||
'</button>'
|
||||
};
|
||||
});
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('ffffngAdmin')
|
||||
.directive('faVersion', function ($http, $state, notification, config) {
|
||||
var pathPrefix = config.rootPath === '/' ? '' : config.rootPath;
|
||||
|
||||
var link = function (scope) {
|
||||
scope.version = '?';
|
||||
$http.get(pathPrefix + '/api/version')
|
||||
.then(function (result) { scope.version = result.data.version; })
|
||||
.catch(function (e) {
|
||||
notification.log('Error: ' + e.data, { addnCls: 'humane-flatty-error' });
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
'link': link,
|
||||
'restrict': 'E',
|
||||
'scope': {},
|
||||
|
||||
'template': '{{version}}'
|
||||
};
|
||||
});
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
<div class="page-header">
|
||||
<h1>Dashboard / Statistics</h1>
|
||||
</div>
|
||||
|
||||
<h2><a ui-sref="list({entity: 'nodes'})">Nodes</a></h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-3">
|
||||
<a ui-sref="list({entity: 'nodes'})">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<i class="fa fa-circle-o fa-5x"></i>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<div class="huge">{{ stats.nodes.registered }}</div>
|
||||
<div>Total Registered</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<a ui-sref="list({entity: 'nodes', search: {hasKey: true}})">
|
||||
<div class="panel panel-yellow">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<i class="fa fa-lock fa-5x"></i>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<div class="huge">{{ stats.nodes.withVPN }}</div>
|
||||
<div>With VPN-Key</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-lg-3">
|
||||
<a ui-sref="list({entity: 'nodes', search: {hasCoords: true}})">
|
||||
<div class="panel panel-green">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<i class="fa fa-map-marker fa-5x"></i>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<div class="huge">{{ stats.nodes.withCoords }}</div>
|
||||
<div>With Coordinates</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<a ui-sref="list({entity: 'nodes', search: {monitoringState: 'active'}})">
|
||||
<div class="col-lg-3">
|
||||
<div class="panel panel-green">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<i class="fa fa-heartbeat fa-5x"></i>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<div class="huge">{{ stats.nodes.monitoring.active }}</div>
|
||||
<div>Monitoring active</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<a ui-sref="list({entity: 'nodes', search: {monitoringState: 'pending'}})">
|
||||
<div class="col-lg-3">
|
||||
<div class="panel panel-red">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-xs-3">
|
||||
<i class="fa fa-envelope fa-5x"></i>
|
||||
</div>
|
||||
<div class="col-xs-9 text-right">
|
||||
<div class="huge">{{ stats.nodes.monitoring.pending }}</div>
|
||||
<div>Monitoring pending</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
{
|
||||
"author": {
|
||||
"name": "https://github.com/angular-ui/bootstrap/graphs/contributors"
|
||||
},
|
||||
"name": "angular-bootstrap",
|
||||
"keywords": [
|
||||
"angular",
|
||||
"angular-ui",
|
||||
"bootstrap"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [],
|
||||
"description": "Native AngularJS (Angular) directives for Bootstrap.",
|
||||
"version": "2.5.0",
|
||||
"main": [
|
||||
"./ui-bootstrap-tpls.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"angular": ">=1.4.0"
|
||||
},
|
||||
"homepage": "https://github.com/angular-ui/bootstrap-bower",
|
||||
"_release": "2.5.0",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "2.5.0",
|
||||
"commit": "2ab82fe5b072269e897d5d11333e9925888df456"
|
||||
},
|
||||
"_source": "https://github.com/angular-ui/bootstrap-bower.git",
|
||||
"_target": "2.5.0",
|
||||
"_originalSource": "angular-bootstrap"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue