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:
baldo 2022-10-04 15:33:55 +02:00
commit e48c2861c6
810 changed files with 109 additions and 348223 deletions

View file

@ -1,3 +0,0 @@
{
"directory": "app/bower_components"
}

View file

@ -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
}
}

View file

@ -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'
]);
};

View file

@ -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

View file

@ -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.

View file

@ -1 +0,0 @@
../../node_modules/ng-admin/build/ng-admin.min.css

View file

@ -1 +0,0 @@
../../node_modules/ng-admin/build/ng-admin.min.css.map

View file

@ -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>

View file

@ -1,4 +0,0 @@
'use strict';
angular.module('ffffng', []);
angular.module('ffffngAdmin', ['ng-admin', 'ffffng']);

View file

@ -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);
});

View file

@ -1 +0,0 @@
../../node_modules/moment/min/moment-with-locales.min.js

View file

@ -1 +0,0 @@
../../node_modules/ng-admin/build/ng-admin.min.js

View file

@ -1 +0,0 @@
../../node_modules/ng-admin/build/ng-admin.min.js.map

View file

@ -1 +0,0 @@
../../shared/validation

View file

@ -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'
};
});

View file

@ -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>&nbsp;<span class="hidden-xs">{{ label }}</span>' +
'</button>'
};
});

View file

@ -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>&nbsp;<span class="hidden-xs">{{ label }}</span>' +
'</button>'
};
});

View file

@ -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}}'
};
});

View file

@ -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>

View file

@ -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