Compare commits

...
Sign in to create a new pull request.
Author SHA1 Message Date
baldo
c68a23dcca Get rid of unused dependencies by using depcheck. 2022-10-04 18:14:53 +02:00
baldo
3b4e4b85de Fix: Use a valid SPDX license expression for AGPL. 2022-10-04 18:02:40 +02:00
baldo
b734c4705a Merge branch 'main' into new-admin 2022-10-04 18:01:05 +02:00
baldo
e48c2861c6 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.
2022-10-04 17:53:45 +02:00
baldo
4035c22aca Merge branch 'main' into new-admin 2022-10-04 16:09:34 +02:00
baldo
8a49ff410d Update frontend dependencies. 2022-10-04 14:45:43 +02:00
baldo
356e8a8ee8 Merge branch 'main' into new-admin 2022-10-04 14:43:13 +02:00
baldo
c87bccae95 Handle legacy frontend URLs. 2022-09-27 16:44:48 +02:00
baldo
32803e0ea1 Update the document title when navigating to a new route 2022-09-27 15:52:12 +02:00
baldo
4a0b6d80e4 Update frontend dependencies. 2022-09-27 14:35:00 +02:00
baldo
4295368e2b Merge branch 'main' into new-admin 2022-09-27 14:32:38 +02:00
baldo
004a2aec6e Merge branch 'main' into new-admin 2022-09-20 19:20:37 +02:00
baldo
511f7c4f80 Remove duplicate code for parsing numbers. 2022-09-20 19:11:36 +02:00
baldo
8f8194467c Refactor api utils and replace some magic values by enums. 2022-09-20 19:09:49 +02:00
baldo
518d986c20 Update frontend dependencies. 2022-09-20 15:38:25 +02:00
baldo
de9c05fc3b Merge branch 'main' into new-admin 2022-09-20 15:37:30 +02:00
baldo
9954a9d1ca Remove duplicate type. 2022-09-14 16:35:54 +02:00
baldo
50dc4733e0 Merge branch 'main' into new-admin 2022-09-14 16:35:03 +02:00
baldo
bf16ee0ca7 Update frontend dependencies. 2022-09-14 16:19:31 +02:00
baldo
7cb11259dc Merge branch 'main' into new-admin 2022-09-14 16:17:51 +02:00
baldo
5af12eba41 Update frontend dependencies. 2022-09-06 16:54:49 +02:00
baldo
c42ec84bde Merge branch 'main' into new-admin 2022-09-06 16:53:54 +02:00
baldo
b53ae271ed Update frontend dependencies. 2022-09-01 14:59:32 +02:00
baldo
d2c90ff833 Merge branch 'main' into new-admin 2022-09-01 14:57:51 +02:00
baldo
4c6556de3f Ask for confirmation for nodes outside of community bounds. 2022-09-01 14:38:41 +02:00
baldo
9387df8dd3 Re-validate form inputs when model is changed from outside.
* This fixes the issue of the coordinates input still being displayed as
  invalid after pickig coordinates from the map.
2022-09-01 14:36:55 +02:00
baldo
903a1bcf8a On submit focus first input having a validation error. 2022-09-01 14:36:55 +02:00
baldo
8d73e1bb80 Fix: Avoid coordinates picked from map not validating. 2022-09-01 14:36:55 +02:00
baldo
0bf8a149e7 Add icon to reset coordinates input. 2022-09-01 13:13:01 +02:00
baldo
e9b8e0b7ae Make title text for info icons the actual help text. 2022-08-30 17:10:53 +02:00
baldo
ac6fb7b57a Add checkbox for monitoring when creating node. 2022-08-30 17:08:27 +02:00
baldo
f80657e4e0 Frontend: Update typescript. 2022-08-30 15:21:13 +02:00
baldo
1f24c0f550 Update frontend dependencies. 2022-08-30 15:18:16 +02:00
baldo
781cbca94e Merge branch 'main' into new-admin 2022-08-30 15:16:06 +02:00
baldo
7e2769cfa5 Zoom out map a little bit in node preview. 2022-08-26 18:13:05 +02:00
baldo
5b0ec235cb Fix: Make response body readable in ApiError message. 2022-08-26 18:09:33 +02:00
baldo
b5eaf0b637 Allow picking coordinates from map. 2022-08-26 18:01:06 +02:00
baldo
56e247e031 Merge branch 'main' into new-admin 2022-08-26 16:29:16 +02:00
baldo
53bb20781d Add pre-commit hook that runs all the checks. 2022-08-26 16:23:36 +02:00
baldo
8cf692b523 Merge branch 'main' into new-admin 2022-08-25 23:08:58 +02:00
baldo
f799765317 ESLint fix warnings. 2022-08-25 22:46:01 +02:00
baldo
82f298ccc0 ESLint: Allow NodeJS global. 2022-08-25 22:45:38 +02:00
baldo
e9eae82d92 Merge branch 'main' into new-admin 2022-08-25 22:19:45 +02:00
baldo
1a0fcbdd6f ESLint: Fix warnings. 2022-08-25 22:15:59 +02:00
baldo
2e72d42d22 Get rid of any. 2022-08-25 22:14:59 +02:00
baldo
312a6066e4 Replace EnumValue type by more general TypeOf type. 2022-08-25 21:10:37 +02:00
baldo
1de5bc0604 Allow passing hostname, fastd-key and MAC as query-parameters when creating new node 2022-08-25 20:37:34 +02:00
baldo
fb5bf934ff Basic node registration form. 2022-08-25 18:21:42 +02:00
baldo
fb73dac224 Fix: Use scss math.div instead of deprecated /. 2022-08-25 16:11:06 +02:00
baldo
aa0d63fd44 Show node on map in confirmation before deleting it. 2022-08-25 15:47:32 +02:00
baldo
3b79e807ba Merge branch 'main' into new-admin 2022-08-24 23:49:21 +02:00
baldo
dcfaae7955 Make sure config and version are loaded before app runs to avoid null checks. 2022-08-24 23:46:56 +02:00
baldo
0bbb2113e9 Merge branch 'main' into new-admin 2022-08-24 18:22:41 +02:00
baldo
df82b70756 Update frontend dependencies. 2022-08-24 18:22:01 +02:00
baldo
22d7755215 Fix sorting after merge. 2022-08-23 22:20:14 +02:00
baldo
4602aaa871 Merge branch 'main' into new-admin 2022-08-23 22:14:54 +02:00
baldo
867be21f68 ESLint: Auto format and fix some warnings / errors. 2022-08-23 19:08:39 +02:00
baldo
90ac67efbe Fix: no-unused-vars linter warning for variables set in script setup. 2022-08-23 17:52:23 +02:00
baldo
3312930d5a Local testing. 2022-08-23 17:17:51 +02:00
baldo
704620e2f7 More readable link styling in ErrorCards. 2022-08-23 17:17:11 +02:00
baldo
3a2f0799eb Frontend: Added forms to delete nodes. 2022-08-23 17:05:13 +02:00
baldo
a0d186da3a Add RouteButton 2022-08-23 15:52:26 +02:00
baldo
5a944f9916 Add card to display errors that scrolls into view. 2022-08-23 15:41:01 +02:00
baldo
4ed749eee3 First shot on form validation. 2022-08-23 15:38:10 +02:00
baldo
096b792caa Some styling. 2022-08-23 15:34:20 +02:00
baldo
6ff2fe2acf Add DELETE method and more generic error handling to API helper. 2022-08-23 15:18:22 +02:00
baldo
cf134eb199 Make page padding dependend on screen size. 2022-08-23 15:16:35 +02:00
baldo
e403e9858d Minor cleanup. 2022-08-23 15:14:52 +02:00
baldo
2cbdc92ef1 Refactoring: Pinia stores are called *Store now. 2022-08-23 15:14:07 +02:00
baldo
5ae27f530e Allow specifying alignment for ButtonGroup. 2022-08-23 15:11:09 +02:00
baldo
4db53af7ce Use enum to identify routes. 2022-08-23 15:07:37 +02:00
baldo
68f893aa91 Merge branch 'main' into new-admin 2022-08-23 14:58:24 +02:00
baldo
4167b5ee14 Update frontend dependencies. 2022-08-23 14:58:19 +02:00
baldo
0878ffcc0b Merge branch 'main' into new-admin 2022-08-18 12:53:26 +02:00
baldo
7160c8563d Update frontend dependencies. 2022-08-09 17:57:22 +02:00
baldo
71c885cd5e Merge branch 'main' into new-admin 2022-08-09 14:02:30 +02:00
baldo
81579cbac4 Merge branch 'main' into new-admin 2022-08-04 18:41:39 +02:00
baldo
7ffe7e7e3f Merge branch 'main' into new-admin 2022-08-04 17:18:32 +02:00
baldo
6f8240ac84 Move shared code into own directory for use in new frontend. 2022-08-04 15:33:41 +02:00
baldo
0ad1d4bcc9 Updated frontend dependencies. 2022-08-04 14:16:17 +02:00
baldo
b5bcb620fd Removed unused import. 2022-08-02 18:17:36 +02:00
baldo
e80b6b2028 Responsive button layout via ButtonGroup 2022-08-02 18:16:30 +02:00
baldo
33f076b1b6 Responsive statistics card in admin dashboard. 2022-08-02 17:21:05 +02:00
baldo
e885975aff Added PageContainer handling responsive breakpoints. 2022-08-02 17:07:57 +02:00
baldo
215f70db26 Introduced ActionButton component as a replacement for HTML's button. 2022-08-02 16:31:38 +02:00
baldo
d159e22c50 Update frontend dependencies. 2022-08-02 15:36:04 +02:00
baldo
a87a8f7e70 Merge branch 'main' into new-admin 2022-08-02 15:27:04 +02:00
baldo
7102337233 Update frontend dependencies. 2022-07-28 15:10:28 +02:00
baldo
697df1ca99 Merge branch 'main' into new-admin 2022-07-28 15:09:12 +02:00
baldo
6fd4dbb33e Merge branch 'main' into new-admin 2022-07-22 17:19:13 +02:00
baldo
6057ad5a2a Add sorting by column and fix page reload issue. 2022-07-22 16:23:13 +02:00
baldo
ed29b96d45 Merge branch 'main' into new-admin 2022-07-22 14:47:50 +02:00
baldo
0fa8b77e77 Merge branch 'main' into new-admin 2022-07-21 19:30:10 +02:00
baldo
e6b50a0291 Merge branch 'main' into new-admin 2022-07-21 18:54:26 +02:00
baldo
edc5c20fdb Fix some types after refactoring on main branch. 2022-07-21 18:44:52 +02:00
baldo
d53e45cd81 Merge branch 'main' into new-admin 2022-07-21 18:41:08 +02:00
baldo
8a5c8c332c Merge branch 'main' into new-admin 2022-07-21 12:07:57 +02:00
baldo
ca2a067250 Update frontend dependencies. 2022-07-21 11:53:35 +02:00
baldo
b09457e576 Merge branch 'main' into new-admin 2022-07-21 11:52:33 +02:00
baldo
3a6790ff52 Merge branch 'main' into new-admin 2022-07-18 18:31:38 +02:00
baldo
f1feec4fe7 Fix: Make typecheck not complain about fetch(). 2022-07-18 18:30:49 +02:00
baldo
eca003b1d6 Merge branch 'main' into new-admin 2022-07-14 20:14:53 +02:00
baldo
6f3eb92c45 Refactor some server-side string types into newtypes. 2022-07-14 20:07:51 +02:00
baldo
22b80ef26f Mute incorrect warning. 2022-07-14 20:07:51 +02:00
baldo
403a537869 Fixed vite ssl. 2022-07-14 20:07:51 +02:00
baldo
f7075533a0 Update frontend dependencies. 2022-07-14 20:07:51 +02:00
baldo
1d50920ca9 Update server dependencies. 2022-07-14 20:07:51 +02:00
baldo
2437559d29 Merge branch 'main' into new-admin 2022-07-14 10:58:15 +02:00
baldo
42474a9267 Merge branch 'main' into new-admin 2022-07-11 10:52:45 +02:00
baldo
bb0ffe6725 Merge branch 'main' into new-admin 2022-07-07 11:20:52 +02:00
baldo
68dce6e9ce New TODO. 2022-07-07 10:59:41 +02:00
baldo
a3dce0b8a2 Add sorting parameters and refactor server part to use enums for sorting. 2022-07-07 10:59:41 +02:00
baldo
f95829adc6 Admin: Added filter / search panel to nodes list. 2022-07-07 10:59:41 +02:00
baldo
ac7f8c98b0 Fix type error. 2022-07-07 10:59:41 +02:00
baldo
cce665d149 Apply filters to nodes list. 2022-07-07 10:59:41 +02:00
baldo
5b703917b5 Showing spinner when loading nodes. 2022-07-07 10:59:41 +02:00
baldo
87839f4faa Added paging. 2022-07-07 10:59:41 +02:00
baldo
956889be11 Adjust page padding to make space for page footer. 2022-07-07 10:59:41 +02:00
baldo
5ff51f03ae Fix reactivity for props. 2022-07-07 10:59:41 +02:00
baldo
7f52ffe45f Update frontend dependencies 2022-07-07 10:59:41 +02:00
baldo
70acb62ec1 Update server dependencies 2022-07-07 10:59:41 +02:00
baldo
ea2f424efa Add toggle to show / hide all sensitive fields. 2022-05-30 14:08:03 +02:00
baldo
cb4bb9e817 Remove redundant main tags. 2022-05-30 13:56:24 +02:00
baldo
cbd5624fb2 Nicer display of node list 2022-05-30 13:54:57 +02:00
baldo
9b5f8f3f37 Fix some header / footer issues. 2022-05-30 12:41:15 +02:00
baldo
72a54c8325 Admin: Show certain fields redacted by default 2022-05-30 12:37:46 +02:00
baldo
0f60436b2c Admin: Basic node listing 2022-05-30 12:36:28 +02:00
baldo
c9ac65eaad Suppress wrong typeguard warnings 2022-05-26 13:03:46 +02:00
baldo
3ce2ba5013 Statistics styling 2022-05-26 12:52:48 +02:00
baldo
6c47c5b178 Updated new frontend dependencies. 2022-05-26 12:52:41 +02:00
baldo
6f489c131c Updated server dependencies. 2022-05-26 12:52:41 +02:00
baldo
2e039a7b27 Basic styling done. 2022-05-23 12:49:05 +02:00
baldo
9e29034a0b Added AGPL v3 license for new frontend. 2022-05-23 12:27:05 +02:00
baldo
4dc1ae0920 Version => Snapshot 2022-05-23 10:51:26 +02:00
baldo
2f8bee77da Update frontend dependencies. 2022-05-23 10:49:53 +02:00
baldo
a07d5e7b52 Update server dependencies. 2022-05-23 10:49:37 +02:00
baldo
4c1f6f8b53 Fix: Workaround GLIBC not found issue in Webstorm 2022-05-23 10:49:09 +02:00
baldo
3c4f7d2875 Fix: Run NixOS wrappers in interactive shell
* Makes sure run configurations terminate properly.
2022-05-19 13:34:32 +02:00
baldo
fde340ead0 Display config values in header and footer. 2022-05-19 13:33:48 +02:00
baldo
59f7897d8e Initial setup for new frontend. 2022-05-19 12:05:44 +02:00
874 changed files with 9455 additions and 348163 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 ### Abhängigkeiten
* node.js + yarn * node.js + yarn
* compass (Installation z. B. via Ruby's `gem`) * sass
* ggf. bower (Installation z. B. via `yarn install bower`)
### Build ### Build

View file

@ -6,9 +6,7 @@
## Short term ## Short term
* Split into seperate packages for server and frontend.
* Make admin panel part of new frontend package. * Make admin panel part of new frontend package.
* Get rid of grunt.
* Use generated type guards. * Use generated type guards.
## Mid term ## Mid term
@ -22,5 +20,4 @@
* Rewrite the admin interface (used lib is unmaintained). * Rewrite the admin interface (used lib is unmaintained).
* Rewrite the client in typescript (+ vue?). * Rewrite the client in typescript (+ vue?).
* Replace the grunt build system.
* Decentralize node data. * 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