init in angular

This commit is contained in:
Martin Geno 2016-07-06 22:52:31 +02:00
commit b5555f043d
26 changed files with 1278 additions and 0 deletions

3
.bowerrc Normal file
View File

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

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
/node_modules
/public/bower_components
/build
/tmp
/.tmp
*.log
/public/translations.js

429
Gruntfile.js Normal file
View File

@ -0,0 +1,429 @@
'use strict';
module.exports = function (grunt) {
// Load grunt tasks automatically, when needed
require('jit-grunt')(grunt, {
useminPrepare: 'grunt-usemin',
ngtemplates: 'grunt-angular-templates',
injector: 'grunt-asset-injector',
cdnify: 'grunt-google-cdn',
replace: 'grunt-text-replace'
});
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// Define the configuration for all the tasks
grunt.initConfig({
open: {
public: {
url: 'http://localhost:8080'
}
},
watch: {
injectJS: {
files: [
'public/{app,components}/**/*.js',
'!public/app/app.js'],
tasks: ['injector:scripts']
},
injectCss: {
files: [
'public/{app,components}/**/*.css'
],
tasks: ['injector:css']
},
injectStylus: {
files: [
'public/{app,components}/**/*.styl'],
tasks: ['injector:stylus']
},
stylus: {
files: [
'public/{app,components}/**/*.styl'],
tasks: ['stylus', 'autoprefixer']
},
jade: {
files: [
'public/{app,components}/*',
'public/{app,components}/**/*.jade'],
tasks: ['jade']
},
gruntfile: {
files: ['Gruntfile.js']
},
livereload: {
files: [
'{.tmp,public}/{app,components}/**/*.css',
'{.tmp,public}/{app,components}/**/*.html',
'{.tmp,public}/{app,components}/**/*.js',
'public/img/{,*//*}*.{png,jpg,jpeg,gif,webp,svg}'
],
options: {
livereload: true
}
}
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: 'public/.jshintrc',
reporter: require('jshint-stylish')
},
all: [
'public/{app,components}/**/*.js',
]
},
// Empties folders to start fresh
clean: {
build: {
files: [{
dot: true,
src: [
'.tmp',
'tmp',
'build/*',
'!build/.git*',
'!build/.openshift',
'!build/Procfile'
]
}]
}
},
// Add vendor prefixed styles
autoprefixer: {
options: {
browsers: ['last 1 version']
},
build: {
files: [{
expand: true,
cwd: '.tmp/',
src: '{,*/}*.css',
dest: '.tmp/'
}]
}
},
// Automatically inject Bower components into the app
wiredep: {
target: {
src: 'public/index.html',
ignorePath: 'public/',
exclude: ['/json3/', '/es5-shim/' ]
}
},
// 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: ['public/index.html'],
options: {
dest: 'build'
}
},
// Performs rewrites based on rev and the useminPrepare configuration
usemin: {
html: ['build/{,*/}*.html'],
css: ['build/{,*/}*.css'],
js: ['build/{,*/}*.js'],
options: {
assetsDirs: [
'build',
'build/img'
],
// This is so we update image references in our ng-templates
patterns: {
js: [
[/(img\/.*?\.(?:gif|jpeg|jpg|png|webp|svg))/gm, 'Update the JS to reference our revved images']
]
}
}
},
// The following *-min tasks produce minified files in the dist folder
imagemin: {
build: {
files: [{
expand: true,
cwd: 'public/img',
src: '{,*/}*.{png,jpg,jpeg,gif}',
dest: 'build/img'
}]
}
},
svgmin: {
build: {
files: [{
expand: true,
cwd: 'public/img',
src: '{,*/}*.svg',
dest: 'build/img'
}]
}
},
// Allow the use of non-minsafe AngularJS files. Automatically makes it
// minsafe compatible so Uglify does not destroy the ng references
ngAnnotate: {
build: {
files: [{
expand: true,
cwd: '.tmp/concat',
src: '*/**.js',
dest: '.tmp/concat'
}]
}
},
// Package all the html partials into a single javascript payload
ngtemplates: {
options: {
// This should be the name of your apps angular module
module: 'ffhb',
htmlmin: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeEmptyAttributes: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true
},
usemin: 'app/app.js'
},
main: {
cwd: 'public',
src: ['{app,components}/**/*.html'],
dest: '.tmp/templates.js'
},
tmp: {
cwd: '.tmp',
src: ['{app,components}/**/*.html'],
dest: '.tmp/tmp-templates.js'
}
},
// Replace Google CDN references
cdnify: {
build: {
html: ['build/*.html']
}
},
// Copies remaining files to places other tasks can use
copy: {
build: {
files: [{
expand: true,
dot: true,
cwd: 'public',
dest: 'build',
src: [
'*.{ico,png,txt}',
'.htaccess',
//'bower_components/**/*',
'img/{,*/}*.{webp}',
'fonts/**/*',
'img/*.png',
'index.html'
]
},
{
expand: true,
cwd: 'public/bower_components/leaflet-draw/dist/images',
dest: 'build/app/images',
src: [
'*.*'
]
},
{
expand: true,
cwd: '.tmp/img',
dest: 'build/img',
src: ['generated/*']
}]
},
styles: {
expand: true,
cwd: 'public',
dest: '.tmp/',
src: ['{app,components}/**/*.css']
}
},
// Run some tasks in parallel to speed up the build process
concurrent: {
all: [
'jade',
'stylus',
'imagemin',
'svgmin'
]
},
connect:{
public:{
options:{
port:8080,
hostname:'*',
base:['.tmp','public']
}
},
},
// Compiles Jade to html
jade: {
compile: {
options: {
data: {
debug: false
}
},
files: [{
expand: true,
cwd: 'public',
src: [
'{app,components}/**/*.jade'
],
dest: '.tmp',
ext: '.html'
}]
}
},
// Compiles Stylus to CSS
stylus: {
build: {
options: {
paths: [
'public/bower_components',
'public/app',
'public/components'
],
"include css": true
},
files: {
'.tmp/app/app.css' : 'public/app/app.styl'
}
}
},
injector: {
options: {
},
// Inject application script files into index.html (doesn't include bower)
scripts: {
options: {
transform: function(filePath) {
filePath = filePath.replace('/public/', '');
filePath = filePath.replace('/.tmp/', '');
return '<script src="' + filePath + '"></script>';
},
starttag: '<!-- injector:js -->',
endtag: '<!-- endinjector -->'
},
files: {
'public/index.html': [
['{.tmp,public}/{app,components}/**/*.js',
'!{.tmp,public}/app/app.js']
]
}
},
// Inject component styl into app.styl
stylus: {
options: {
transform: function(filePath) {
filePath = filePath.replace('/public/app/', '');
filePath = filePath.replace('/public/components/', '');
return '@import \'' + filePath + '\';';
},
starttag: '// injector',
endtag: '// endinjector'
},
files: {
'public/app/app.styl': [
'public/{app,components}/**/*.styl',
'!public/app/app.styl'
]
}
},
// Inject component css into index.html
css: {
options: {
transform: function(filePath) {
filePath = filePath.replace('/public/', '');
filePath = filePath.replace('/.tmp/', '');
return '<link rel="stylesheet" href="' + filePath + '">';
},
starttag: '<!-- injector:css -->',
endtag: '<!-- endinjector -->'
},
files: {
'public/index.html': [
'public/{app,components}/**/*.css'
]
}
}
},
replace: {
url: {
src: ['build/app/app.js'],
overwrite:true,
replacements: [{
from: 'http://localhost:8080/',
to: 'https://mgmt.ffhb.de/'
}]
}
}
});
grunt.registerTask('serve', [
'clean:build',
'injector:stylus',
'concurrent:all',
'injector',
'wiredep',
'autoprefixer',
'connect:public',
'open:public',
'watch'
]);
grunt.registerTask('serve-build', [
'open:public',
'connect:build'
]);
grunt.registerTask('build', [
'newer:jshint',
'clean:build',
'injector:stylus',
'concurrent:all',
'injector',
'wiredep',
'useminPrepare',
'autoprefixer',
'ngtemplates',
'concat',
'ngAnnotate',
'copy:build',
'cssmin',
'uglify',
'usemin'
]);
grunt.registerTask('release', [
'build',
'replace:url',
]);
grunt.registerTask('default', [
'serve'
]);
};

28
bower.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "freifunkmanager",
"homepage": "https://github.com/FreifunkBremen/freifunkmanager",
"authors": [
"Martin Geno <geno+dev@fireorbit.de>"
],
"description": "",
"main": "",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"ng-table": "^1.0.0",
"angular-ui-router": "^0.3.1",
"angular-resource": "^1.5.7",
"angular-bootstrap": "^1.3.3",
"bootstrap": "^3.3.6",
"angular-moment": "^0.10.3",
"angular-web-notification": "^0.0.83",
"ui-leaflet": "^1.0.1",
"angular-cookies": "^1.5.7"
}
}

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "freifunkmanager",
"version": "1.0.0",
"description": "Eventmanager for respond-collector",
"main": "Gruntfile.js",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-angular-templates": "^0.5.9",
"grunt-ng-annotate": "^1.0.1",
"grunt-autoprefixer": "^3.0.3",
"grunt-concurrent": "^2.1.0",
"grunt-contrib-clean": "^0.7.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-connect": "^0.11.2",
"grunt-contrib-copy": "^0.8.2",
"grunt-contrib-cssmin": "^0.14.0",
"grunt-contrib-htmlmin": "^0.6.0",
"grunt-contrib-imagemin": "^1.0.0",
"grunt-contrib-jade": "^0.15.0",
"grunt-contrib-jshint": "^0.11.3",
"grunt-contrib-stylus": "^0.22.0",
"grunt-contrib-uglify": "^0.11.0",
"grunt-contrib-watch": "^0.6.1",
"grunt-google-cdn": "^0.4.3",
"grunt-newer": "^1.1.1",
"grunt-open": "^0.2.3",
"grunt-svgmin": "^3.1.0",
"grunt-text-replace": "^0.4.0",
"grunt-usemin": "^3.1.1",
"grunt-wiredep": "^2.0.0",
"jit-grunt": "^0.9.1",
"jshint-stylish": "^2.1.0",
"time-grunt": "^1.2.2"
},
"scripts": {
"start": "grunt serve"
},
"repository": {
"type": "git",
"url": "git+https://github.com/FreifunkBremen/freifunkmanager.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/FreifunkBremen/freifunkmanager/issues"
},
"homepage": "https://github.com/FreifunkBremen/freifunkmanager#readme",
"dependencies": {
"grunt-asset-injector": "^0.1.0",
"livereload-js": "^2.2.2"
}
}

41
public/.jshintrc Normal file
View File

@ -0,0 +1,41 @@
{
"node": true,
"browser": true,
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"smarttabs": true,
"globals": {
"jQuery": true,
"angular": true,
"L": true,
"lvector": true,
"sypOn": true,
"console": true,
"$": true,
"_": true,
"moment": true,
"describe": true,
"beforeEach": true,
"module": true,
"inject": true,
"it": true,
"expect": true,
"browser": true,
"element": true,
"by": true
}
}

25
public/app/app.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
angular.module('ffhb', [
'ngTable',
'ngResource',
'ngCookies',
'ui.router',
'angularMoment',
'ui-leaflet',
'Authentication',
'angular-web-notification',
'config'
])
.config(['$urlRouterProvider',function ($urlRouterProvider){
//,$httpProvider) {
$urlRouterProvider.otherwise('/nodes/sort');
//$locationProvider.html5Mode(true).hashPrefix('!');
//$httpProvider.defaults.withCredentials = true;
}]).run(function(amMoment,$cookieStore,$rootScope,$http) {
amMoment.changeLocale('de');
$rootScope.globals = $cookieStore.get('globals') || {};
if ($rootScope.globals.currentUser) {
$http.defaults.headers.common['Authorization'] = 'Basic ' + $rootScope.globals.currentUser.authdata; // jshint ignore:line
}
});

7
public/app/app.styl Normal file
View File

@ -0,0 +1,7 @@
form > .btn+.btn
margin-left 5px
.table
td.split
span
display block

15
public/app/changes.jade Normal file
View File

@ -0,0 +1,15 @@
.page-header
h1 Changes
table.table.table-striped.table-condensed( ng-table="tableParams")
tr(ng-repeat='row in $data',demo-tracked-table-row="row")
td(data-title="'Nodeid'", sortable="'row.nodeid'", filter="{'nodeid': 'text'}") {{row.nodeid}}
td(data-title="'Hostname'", sortable="'row.hostname'", filter="{'hostname': 'text'}") {{row.hostname}}
td.split.text-right(data-title="'Freq'")
span 2.4 Ghz
span 5 Ghz
td.split.text-right(data-title="'Channel'", sortable="'row.wireless.channel24'",filter="{'wireless.channel24': 'number'}")
span {{row.wireless.channel24}}
span {{row.wireless.channel5}}
td.split.text-right(data-title="'TxPower'", sortable="'row.wireless.txpower24'",filter="{'wireless.txpower24': 'number'}")
span {{row.wireless.txpower24}}
span {{row.wireless.txpower5}}

25
public/app/changes.js Normal file
View File

@ -0,0 +1,25 @@
'use strict';
angular.module('ffhb')
.controller('ChangesCtrl',function(NgTableParams,$scope,store){
$scope.tableParams = new NgTableParams({
sorting: { hostname: 'asc' },
total: 0,
count: 50
}, {
dataset: []
});
function render(prom){
prom.then(function(data){
var result = Object.keys(data.aliases).map(function(nodeid){
data.aliases[nodeid].nodeid = nodeid;
return data.aliases[nodeid];
});
$scope.tableParams.settings({dataset: result,total: data.aliasesCount});
});
}
render(store.getData);
$scope.$on('store', function(ev, prom) {
render(prom);
});
});

42
public/app/index.js Normal file
View File

@ -0,0 +1,42 @@
'use strict';
angular.module('ffhb')
.config(['$stateProvider',function ($stateProvider) {
$stateProvider
.state('app', {
templateUrl: 'app/main.html',
controller: 'MainCtrl'
})
.state('app.nodes', {
url:'/nodes',
templateUrl: 'app/nodes/nodes.html',
controller: 'NodesCtrl'
})
.state('app.nodes.sort', {
url:'/sort',
templateUrl: 'app/nodes/nodesSort.html',
controller: 'NodesSortCtrl'
})
.state('app.nodes.group', {
url:'/group',
templateUrl: 'app/nodes/nodesGroup.html',
controller: 'NodesGroupCtrl'
})
.state('app.node', {
url:'/n/:nodeid',
templateUrl: 'app/node.html',
controller: 'NodeCtrl'
})
.state('app.changes',{
url:'/changes',
templateUrl: 'app/changes.html',
controller: 'ChangesCtrl'
})
.state('app.mapwithNodeid',{
url:'/map/:nodeid',
templateUrl: 'app/nodes/nodes.html'
})
.state('app.map',{
url:'/map',
templateUrl: 'app/nodes/nodes.html'
});
}]);

21
public/app/main.jade Normal file
View File

@ -0,0 +1,21 @@
.navbar.navbar-default.navbar-fixed-top
.container-fluid
.navbar-header
a.navbar-brand(ui-sref="app")
img(src="/favicon.ico")
.navbar-collapse
ui.nav.navbar-nav
li(ui-sref="app.nodes.sort",ng-class="{ active: $state.includes('app.nodes') }")
a(nav navbar-nav) Nodes
li(ui-sref="app.changes",ui-sref-active="active")
a(nav navbar-nav) Changes
li(ui-sref="app.map",ui-sref-active="active")
a(nav navbar-nav) Map
ui.nav.navbar-nav.navbar-right
li
a.btn.btn-link(ng-click="refresh()")
span.glyphicon.glyphicon-refresh(aria-hidden="true")
| {{timeRefresh}} Sec
form.navbar-form.navbar-right
input.form-control(type="password",ng-change="passphraseUpdate()",ng-model="passphrase",placeholder="Passphrase")
div(ui-view="",style="margin-top:100px;")

24
public/app/main.js Normal file
View File

@ -0,0 +1,24 @@
'use strict';
angular.module('ffhb')
.controller('MainCtrl',function($scope,$interval,store,$state,AuthenticationService){
$scope.$state = $state;
$scope.refresh = store.refresh;
$scope.passphrase = '';
var timediff = new Date(1970,1,1);
function render(prom){
prom.then(function(data){
timediff = data.lastupdate;
});
}
$interval(function() {
$scope.timeRefresh = parseInt((new Date() - timediff) / 1000);
},100);
render(store.getData);
$scope.$on('store', function(ev, prom) {
render(prom);
});
$scope.passphraseUpdate = function(){
AuthenticationService.SetCredentials('client',$scope.passphrase);
};
});

19
public/app/node.jade Normal file
View File

@ -0,0 +1,19 @@
.container
.page-header
h1 {{node.nodeinfo.hostname}}
small {{nodeid}}
form(name="rowForm",ng-submit="save()")
.form-group(ng-class="rowForm.group.$invalid ? 'has-error' : ''")
label(for="formGroup") Group
input.form-control(id="formGroup",placeholder="Group",type="text",name="group",pattern="[a-zA-Z0-9-]*",ng-model='node.nodeinfo.owner.contact')
.form-group(ng-class="rowForm.hostname.$invalid ? 'has-error' : ''")
label(for="formHostname") Hostname
input.form-control(id="formHostname",placeholder="Hostname",type="text",name="hostname",pattern="[a-zA-Z0-9-]*",ng-model='node.nodeinfo.hostname',required)
leaflet(geojson=geojson,center=center,markers=markers)
button.btn.btn-default(type="submit")
span.glyphicon.glyphicon-floppy-disk(aria-hidden="true")
| Save
span.btn.btn-default(ng-click="gps()")
span.glyphicon.glyphicon-map-marker(aria-hidden="true")
| GPS

28
public/app/node.js Normal file
View File

@ -0,0 +1,28 @@
'use strict';
angular.module('ffhb')
.controller('NodeCtrl',function($stateParams,$scope,store,config){
$scope.nodeid = $stateParams.nodeid;
$scope.node = {};
$scope.center = config.map.view;
$scope.markers = [];
store.getGeojson.then(function(data){
$scope.geojson = data;
});
function render(prom){
prom.then(function(data){
$scope.node = data.merged[$stateParams.nodeid];
});
}
render(store.getData);
$scope.$on('store', function(ev, prom) {
render(prom);
});
$scope.gps = function() {
console.log('gps');
};
$scope.save = function() {
store.saveNode($stateParams.nodeid);
};
});

View File

@ -0,0 +1,7 @@
.page-header
h1 Nodes
.btn-group.btn-group-xs
a.btn.btn-default(ui-sref="app.nodes.sort",ui-sref-active="active") Sortiert
a.btn.btn-default(ui-sref="app.nodes.group",ui-sref-active="active") Groupiert
.table-responsive(ui-view="")

15
public/app/nodes/nodes.js Normal file
View File

@ -0,0 +1,15 @@
'use strict';
angular.module('ffhb')
.controller('NodesCtrl',function(NgTableParams,$scope){
$scope.cancel = function(row, rowForm) {
console.log('cancel',row,rowForm);
row.isEditing = false;
//angular.extend(row, originalRow);
};
$scope.save = function(row, rowForm) {
console.log('save',row,rowForm);
row.isEditing = false;
//angular.extend(row, originalRow);
};
});

View File

@ -0,0 +1,46 @@
table.table.table-striped.table-condensed( ng-table="tableParams")
tr.ng-table-group(ng-repeat-start="group in $groups")
td(colspan=9)
a(ng-click="group.$hideRows = !group.$hideRows")
span.glyphicon(ng-class="{ 'glyphicon-chevron-right': group.$hideRows, 'glyphicon-chevron-down': !group.$hideRows }")
strong {{ group.value }}
div Anzahl: {{ group.data.length }}
tr(ng-hide='group.$hideRows',ng-repeat='row in group.data',ng-repeat-end,ng-form="rowForm",ng-class="{'danger':!row.flags.online}")
td(data-title="'Last'",sortable="'lastseen'", am-time-ago="row.lastseen")
td(data-title="'ID'", filter="{nodeid: 'text'}", sortable="'nodeid'") {{row.nodeid}}
td(data-title="'Group'",groupable="'nodeinfo.owner.contact'",sortable="'nodeinfo.owner.contact'",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.owner.contact}}
div.controls(ng-switch-when="true",ng-class="rowForm.group.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="group",pattern="[a-zA-Z0-9-]*",ng-model='row.nodeinfo.owner.contact',required)
td(data-title="'Hostname'", filter="{'nodeinfo.hostname': 'text'}", sortable="'nodeinfo.hostname'",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.hostname}}
div.controls(ng-switch-when="true",ng-class="rowForm.hostname.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="hostname",pattern="[a-zA-Z0-9-]*",ng-model='row.nodeinfo.hostname',required)
td(data-title="'First'",sortable="'firstseen'", am-time-ago="row.firstseen")
td.split.text-right(data-title="'Freq'")
span 2.4 Ghz
span 5 Ghz
td.split.text-right(data-title="'Clients'", sortable="'statistics.clients.wifi24'")
span {{row.statistics.clients.wifi24}}
span {{row.statistics.clients.wifi5}}
td.text-right.split(data-title="'Channel'",filter="{'nodeinfo.wireless.channel5': 'number'}", groupable="'nodeinfo.wireless.channel24'", sortable="'nodeinfo.wireless.channel5'",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.wireless.channel24}}
div.controls(ng-switch-when="true",ng-class="rowForm.channel24.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="channel24",ng-model='row.nodeinfo.wireless.channel24',required)
span(ng-switch-default) {{row.nodeinfo.wireless.channel5}}
div.controls(ng-switch-when="true",ng-class="rowForm.channel5.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="channel5",ng-model='row.nodeinfo.wireless.channel5',required)
td.text-right.split(data-title="'Power'",filter="{'txpower24': 'number'}",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.wireless.txpower24}}
div.controls(ng-switch-when="true",ng-class="rowForm.txpower24.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="txpower24",ng-model='row.nodeinfo.wireless.txpower24',required)
span(ng-switch-default) {{row.nodeinfo.wireless.channel5}}
div.controls(ng-switch-when="true",ng-class="rowForm.txpower5.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="txpower5",ng-model='row.nodeinfo.wireless.txpower5',required)
td(data-title="'Options'")
.btn.btn-success.btn-sm(ng-click="save(row, rowForm)",ng-if="row.isEditing",ng-disabled="rowForm.$pristine || rowForm.$invalid")
span.glyphicon.glyphicon-ok
.btn.btn-warning.btn-sm(ng-click="cancel(row, rowForm)",ng-if="row.isEditing")
span.glyphicon.glyphicon-remove
.btn.btn-primary.btn-sm(ng-click="row.isEditing = true",ng-if="!row.isEditing")
span.glyphicon.glyphicon-pencil

View File

@ -0,0 +1,26 @@
'use strict';
angular.module('ffhb')
.controller('NodesGroupCtrl',function(NgTableParams,$scope,store){
$scope.tableParams = new NgTableParams({
sorting: { hostname: 'asc' },
group: 'nodeinfo.owner.contact',
total: 0,
count: 50
}, {
dataset: []
});
function render(prom){
prom.then(function(data){
var result = Object.keys(data.nodes).map(function(nodeid){
data.merged[nodeid].nodeid = nodeid;
return data.merged[nodeid];
});
$scope.tableParams.settings({dataset: result,total: data.nodesCount});
});
}
render(store.getData);
$scope.$on('store', function(ev, prom) {
render(prom);
});
});

View File

@ -0,0 +1,40 @@
table.table.table-striped.table-condensed( ng-table="tableParams")
tr(ng-repeat='row in $data',ng-class="{'danger':!row.flags.online}",ng-form="rowForm",demo-tracked-table-row="row")
td(data-title="'Last'",sortable="'lastseen'", am-time-ago="row.lastseen")
td(data-title="'ID'", filter="{nodeid: 'text'}", sortable="'nodeid'") {{row.nodeid}}
td(data-title="'Group'",sortable="'nodeinfo.owner.contact'",filter="{'nodeinfo.owner.contact': 'text'}",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.owner.contact}}
div.controls(ng-switch-when="true",ng-class="rowForm.group.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text",name="group",pattern="[a-zA-Z0-9-]*",ng-model='row.nodeinfo.owner.contact')
td(data-title="'Hostname'", filter="{'nodeinfo.hostname': 'text'}", sortable="'nodeinfo.hostname'",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.hostname}}
div.controls(ng-switch-when="true",ng-class="rowForm.hostname.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="hostname",pattern="[a-zA-Z0-9-]*",ng-model='row.nodeinfo.hostname',required)
td(data-title="'First'",sortable="'firstseen'", am-time-ago="row.firstseen")
td.split.text-right(data-title="'Freq'")
span 2.4 Ghz
span 5 Ghz
td.split.text-right(data-title="'Clients'", sortable="'statistics.clients.wifi24'")
span {{row.statistics.clients.wifi24}}
span {{row.statistics.clients.wifi5}}
td.text-right.split(data-title="'Channel'",filter="{'nodeinfo.wireless.channel5': 'number'}", sortable="'nodeinfo.wireless.channel5'",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.wireless.channel24}}
div.controls(ng-switch-when="true",ng-class="rowForm.channel24.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="channel24",ng-model='row.nodeinfo.wireless.channel24',required)
span(ng-switch-default) {{row.nodeinfo.wireless.channel5}}
div.controls(ng-switch-when="true",ng-class="rowForm.channel5.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="channel5",ng-model='row.nodeinfo.wireless.channel5',required)
td.text-right.split(data-title="'Power'",filter="{'txpower24': 'number'}",ng-switch="row.isEditing")
span(ng-switch-default) {{row.nodeinfo.wireless.txpower24}}
div.controls(ng-switch-when="true",ng-class="rowForm.txpower24.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="txpower24",ng-model='row.nodeinfo.wireless.txpower24',required)
span(ng-switch-default) {{row.nodeinfo.wireless.channel5}}
div.controls(ng-switch-when="true",ng-class="rowForm.txpower5.$invalid ? 'has-error' : ''")
input.editable-input.form-control.input-sm(type="text" name="txpower5",ng-model='row.nodeinfo.wireless.txpower5',required)
td(data-title="'Options'")
.btn.btn-success.btn-sm(ng-click="save(row, rowForm)",ng-if="row.isEditing",ng-disabled="rowForm.$pristine || rowForm.$invalid")
span.glyphicon.glyphicon-ok
.btn.btn-warning.btn-sm(ng-click="cancel(row, rowForm)",ng-if="row.isEditing")
span.glyphicon.glyphicon-remove
.btn.btn-primary.btn-sm(ng-click="row.isEditing = true",ng-if="!row.isEditing")
span.glyphicon.glyphicon-pencil

View File

@ -0,0 +1,25 @@
'use strict';
angular.module('ffhb')
.controller('NodesSortCtrl',function(NgTableParams,$scope,store){
$scope.tableParams = new NgTableParams({
sorting: { hostname: 'asc' },
total: 0,
count: 50
}, {
dataset: []
});
function render(prom){
prom.then(function(data){
var result = Object.keys(data.nodes).map(function(nodeid){
data.merged[nodeid].nodeid = nodeid;
return data.merged[nodeid];
});
$scope.tableParams.settings({dataset: result,total: data.nodesCount});
});
}
render(store.getData);
$scope.$on('store', function(ev, prom) {
render(prom);
});
});

View File

@ -0,0 +1,139 @@
'use strict';
angular.module('Authentication',[])
.factory('AuthenticationService',
['Base64', '$http', '$cookieStore', '$rootScope', '$timeout',
function (Base64, $http, $cookieStore, $rootScope, $timeout) {
var service = {};
service.Login = function (username, password, callback) {
/* Dummy authentication for testing, uses $timeout to simulate api call
----------------------------------------------*/
$timeout(function(){
var response = { success: username === 'test' && password === 'test' };
if(!response.success) {
response.message = 'Username or password is incorrect';
}
callback(response);
}, 1000);
/* Use this for real authentication
----------------------------------------------*/
//$http.post('/api/authenticate', { username: username, password: password })
// .success(function (response) {
// callback(response);
// });
};
service.SetCredentials = function (username, password) {
var authdata = Base64.encode(username + ':' + password);
$rootScope.globals = {
currentUser: {
username: username,
authdata: authdata
}
};
$http.defaults.headers.common['Authorization'] = 'Basic ' + authdata; // jshint ignore:line
$cookieStore.put('globals', $rootScope.globals);
};
service.ClearCredentials = function () {
$rootScope.globals = {};
$cookieStore.remove('globals');
$http.defaults.headers.common.Authorization = 'Basic ';
};
return service;
}])
.factory('Base64', function () {
/* jshint ignore:start */
var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
return {
encode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
do {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
keyStr.charAt(enc1) +
keyStr.charAt(enc2) +
keyStr.charAt(enc3) +
keyStr.charAt(enc4);
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i < input.length);
return output;
},
decode: function (input) {
var output = "";
var chr1, chr2, chr3 = "";
var enc1, enc2, enc3, enc4 = "";
var i = 0;
// remove all characters that are not A-Z, a-z, 0-9, +, /, or =
var base64test = /[^A-Za-z0-9\+\/\=]/g;
if (base64test.exec(input)) {
window.alert("There were invalid base64 characters in the input text.\n" +
"Valid base64 characters are A-Z, a-z, 0-9, '+', '/',and '='\n" +
"Expect errors in decoding.");
}
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
do {
enc1 = keyStr.indexOf(input.charAt(i++));
enc2 = keyStr.indexOf(input.charAt(i++));
enc3 = keyStr.indexOf(input.charAt(i++));
enc4 = keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
chr1 = chr2 = chr3 = "";
enc1 = enc2 = enc3 = enc4 = "";
} while (i < input.length);
return output;
}
};
/* jshint ignore:end */
});

View File

@ -0,0 +1,12 @@
'use strict';
angular.module('config', [])
.factory('config', function() {
return {
api: 'http://mgmt.ffhb.de/api',
map: {
view: {lat: 53.0702, lng: 8.815}
},
geojson: 'https://raw.githubusercontent.com/FreifunkBremen/internal-maps/master/breminale.geojson',
refresh: 60000
};
});

128
public/components/store.js Normal file
View File

@ -0,0 +1,128 @@
'use strict';
angular.module('ffhb')
.factory('store', function($state, $q, $http, $rootScope,config,$interval,$cookieStore,webNotification) {
function notifyNew(nodeid){
webNotification.showNotification('New Node',{
body: '"'+nodeid+'"',
icon: '/favicon.ico',
onClick: function() {
$state.go('app.node', {nodeid: nodeid});
}
},function(){});
}
function notifyOffline(nodeid){
webNotification.showNotification('Offline Node',{
body: '"'+nodeid+'"',
icon: '/favicon.ico',
onClick: function() {
$state.go('app.node', {nodeid: nodeid});
}
},function(){});
}
var myservice = {};
myservice._initialized = false;
myservice._data = $cookieStore.get('data') ||{
nodes: {},nodesCount:0,
aliases: {},aliasesCount:0
};
var geojsonDeferred = $q.defer();
$http.get(config.geojson).success(function(geojson) {
geojsonDeferred.resolve(geojson);
});
myservice.getGeojson = geojsonDeferred.promise;
myservice.refresh = function() {
var dataDeferred = $q.defer();
$http.get(config.api+'/nodes').success(function(nodes) {
$http.get(config.api+'/aliases').success(function(aliases) {
Object.keys(nodes).map(function(key){
if(myservice._data.nodes === undefined || myservice._data.nodes[key] === undefined){
notifyNew(key);
}
if(myservice._data.nodes !== undefined && myservice._data.nodes[key].flags.offline){
notifyOffline(key);
}
myservice._data.nodes[key] = nodes[key];
});
angular.copy(nodes, myservice._data.merged);
Object.keys(aliases).map(function(key){
var node = myservice._data.merged[key],
alias = aliases[key];
node.nodeinfo.hostname = alias.hostname;
if(!node.nodeinfo.owner){
node.nodeinfo.owner = {};
}
node.nodeinfo.owner.contact = alias.owner;
if(!node.nodeinfo.wireless){
node.nodeinfo.wireless = {};
}
if(alias.wireless){
if(alias.wireless.channel24){
node.nodeinfo.wireless.channel24 = alias.wireless.channel24;
}
if(alias.wireless.channel5){
node.nodeinfo.wireless.channel5 = alias.wireless.channel5;
}
if(alias.wireless.txpower24){
node.nodeinfo.wireless.txpower24 = alias.wireless.txpower24;
}
if(alias.wireless.txpower5){
node.nodeinfo.wireless.txpower5 = alias.wireless.txpower5;
}
}
if(!node.nodeinfo.location){
node.nodeinfo.location = {};
}
if(alias.location){
if(alias.location.latitude){
node.nodeinfo.location.latitude = alias.location.latitude;
}
if(alias.location.longitude){
node.nodeinfo.location.longitude = alias.location.longitude;
}
}
});
myservice._data.nodesCount = Object.keys(nodes).length || 0;
myservice._data.aliases = aliases;
myservice._data.aliasesCount = Object.keys(aliases).length || 0;
myservice._data.lastupdate = new Date();
dataDeferred.resolve(myservice._data);
if (myservice._initialized) {
$rootScope.$broadcast('store', dataDeferred.promise);
}
$cookieStore.put('data',myservice._data);
myservice._initialized = true;
});
});
myservice.getData = dataDeferred.promise;
return dataDeferred.promise;
};
myservice.refresh();
myservice.saveNode = function(nodeid){
var result = $q.defer();
if(myservice._data.merged && myservice._data.merged[nodeid]){
var node = myservice._data.merged[nodeid];
$http.post(config.api+'/aliases/alias/'+nodeid,{
'hostname':node.nodeinfo.hostname,
'owner':node.owner.contact
}).then(function(){
result.resolve(true);
myservice.refresh();
});
}else{
result.resolve(false);
}
return result.promise;
};
if(config.refresh){
$interval(function () {
myservice.refresh();
}, config.refresh);
}
return myservice;
});

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

72
public/index.html Normal file
View File

@ -0,0 +1,72 @@
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<base href="/">
<title>Freifunk Manager</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel="icon" href="/favicon.ico" type="image/x-icon">
<!-- build:css(public) app/vendor.css -->
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/ng-table/dist/ng-table.css" />
<link rel="stylesheet" href="bower_components/leaflet/dist/leaflet.css" />
<!-- endbower -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css" />
<!-- endbuild -->
<!-- build:css({.tmp,public}) app/app.css -->
<link rel="stylesheet" href="app/app.css">
<!-- injector:css -->
<!-- endinjector -->
<!-- endbuild -->
</head>
<body ng-app="ffhb">
<!--[if lt IE 9]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<!-- Add your site or application content here -->
<div ui-view=""></div>
<!-- build:js(public) app/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/ng-table/dist/ng-table.js"></script>
<script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-bootstrap/ui-bootstrap-tpls.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script src="bower_components/moment/moment.js"></script>
<script src="bower_components/angular-moment/angular-moment.js"></script>
<script src="bower_components/HTML5-Desktop-Notifications2/desktop-notify.js"></script>
<script src="bower_components/angular-web-notification/angular-web-notification.js"></script>
<script src="bower_components/angular-simple-logger/dist/angular-simple-logger.js"></script>
<script src="bower_components/leaflet/dist/leaflet-src.js"></script>
<script src="bower_components/ui-leaflet/dist/ui-leaflet.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
<!-- endbower -->
<script src="bower_components/angular-bootstrap/ui-bootstrap.min.js"></script>
<!-- endbuild -->
<!-- build:js({.tmp,public}) app/app.js -->
<script src="app/app.js"></script>
<!-- injector:js -->
<script src="app/changes.js"></script>
<script src="app/index.js"></script>
<script src="app/main.js"></script>
<script src="app/node.js"></script>
<script src="app/nodes/nodes.js"></script>
<script src="app/nodes/nodesGroup.js"></script>
<script src="app/nodes/nodesSort.js"></script>
<script src="components/basicauth.js"></script>
<script src="components/config.js"></script>
<script src="components/store.js"></script>
<!-- endinjector -->
<!-- endbuild -->
</body>
</html>