commit b5555f043d30270613c2c6a7ada30857c1c68080 Author: Martin Geno Date: Wed Jul 6 22:52:31 2016 +0200 init in angular diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..0de6429 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,3 @@ +{ + "directory": "public/bower_components" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2b13c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/node_modules +/public/bower_components +/build + +/tmp +/.tmp +*.log + +/public/translations.js diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..3abecb2 --- /dev/null +++ b/Gruntfile.js @@ -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 ''; + }, + starttag: '', + endtag: '' + }, + 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 ''; + }, + starttag: '', + endtag: '' + }, + 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' + ]); +}; diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..ebd1b57 --- /dev/null +++ b/bower.json @@ -0,0 +1,28 @@ +{ + "name": "freifunkmanager", + "homepage": "https://github.com/FreifunkBremen/freifunkmanager", + "authors": [ + "Martin Geno " + ], + "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" + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b7baade --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/public/.jshintrc b/public/.jshintrc new file mode 100644 index 0000000..f406740 --- /dev/null +++ b/public/.jshintrc @@ -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 + } +} diff --git a/public/app/app.js b/public/app/app.js new file mode 100644 index 0000000..6337160 --- /dev/null +++ b/public/app/app.js @@ -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 + } + }); diff --git a/public/app/app.styl b/public/app/app.styl new file mode 100644 index 0000000..8f503a8 --- /dev/null +++ b/public/app/app.styl @@ -0,0 +1,7 @@ +form > .btn+.btn + margin-left 5px + +.table + td.split + span + display block diff --git a/public/app/changes.jade b/public/app/changes.jade new file mode 100644 index 0000000..8b94a96 --- /dev/null +++ b/public/app/changes.jade @@ -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}} diff --git a/public/app/changes.js b/public/app/changes.js new file mode 100644 index 0000000..9d9901f --- /dev/null +++ b/public/app/changes.js @@ -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); + }); + }); diff --git a/public/app/index.js b/public/app/index.js new file mode 100644 index 0000000..5efcffc --- /dev/null +++ b/public/app/index.js @@ -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' + }); + }]); diff --git a/public/app/main.jade b/public/app/main.jade new file mode 100644 index 0000000..4d2b2f0 --- /dev/null +++ b/public/app/main.jade @@ -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;") diff --git a/public/app/main.js b/public/app/main.js new file mode 100644 index 0000000..3bc6214 --- /dev/null +++ b/public/app/main.js @@ -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); + }; + }); diff --git a/public/app/node.jade b/public/app/node.jade new file mode 100644 index 0000000..c175fe0 --- /dev/null +++ b/public/app/node.jade @@ -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 diff --git a/public/app/node.js b/public/app/node.js new file mode 100644 index 0000000..efbf2d1 --- /dev/null +++ b/public/app/node.js @@ -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); + }; + }); diff --git a/public/app/nodes/nodes.jade b/public/app/nodes/nodes.jade new file mode 100644 index 0000000..31d3408 --- /dev/null +++ b/public/app/nodes/nodes.jade @@ -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="") diff --git a/public/app/nodes/nodes.js b/public/app/nodes/nodes.js new file mode 100644 index 0000000..833dc4d --- /dev/null +++ b/public/app/nodes/nodes.js @@ -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); + }; + }); diff --git a/public/app/nodes/nodesGroup.jade b/public/app/nodes/nodesGroup.jade new file mode 100644 index 0000000..576438b --- /dev/null +++ b/public/app/nodes/nodesGroup.jade @@ -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 diff --git a/public/app/nodes/nodesGroup.js b/public/app/nodes/nodesGroup.js new file mode 100644 index 0000000..0ea23f3 --- /dev/null +++ b/public/app/nodes/nodesGroup.js @@ -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); + }); + }); diff --git a/public/app/nodes/nodesSort.jade b/public/app/nodes/nodesSort.jade new file mode 100644 index 0000000..25afb04 --- /dev/null +++ b/public/app/nodes/nodesSort.jade @@ -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 diff --git a/public/app/nodes/nodesSort.js b/public/app/nodes/nodesSort.js new file mode 100644 index 0000000..65b1084 --- /dev/null +++ b/public/app/nodes/nodesSort.js @@ -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); + }); + }); diff --git a/public/components/basicauth.js b/public/components/basicauth.js new file mode 100644 index 0000000..7895987 --- /dev/null +++ b/public/components/basicauth.js @@ -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 */ +}); diff --git a/public/components/config.js b/public/components/config.js new file mode 100644 index 0000000..e4c46c0 --- /dev/null +++ b/public/components/config.js @@ -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 + }; + }); diff --git a/public/components/store.js b/public/components/store.js new file mode 100644 index 0000000..85b9adf --- /dev/null +++ b/public/components/store.js @@ -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; + }); diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..53d79e5 Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..e9061fd --- /dev/null +++ b/public/index.html @@ -0,0 +1,72 @@ + + + + + + + + + + Freifunk Manager + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +